summaryrefslogtreecommitdiff
path: root/www/wiki/tests
diff options
context:
space:
mode:
authorYaco <franco@reevo.org>2020-06-04 11:01:00 -0300
committerYaco <franco@reevo.org>2020-06-04 11:01:00 -0300
commitfc7369835258467bf97eb64f184b93691f9a9fd5 (patch)
treedaabd60089d2dd76d9f5fb416b005fbe159c799d /www/wiki/tests
first commit
Diffstat (limited to 'www/wiki/tests')
-rw-r--r--www/wiki/tests/.htaccess1
-rw-r--r--www/wiki/tests/common/TestSetup.php113
-rw-r--r--www/wiki/tests/common/TestsAutoLoader.php222
-rw-r--r--www/wiki/tests/integration/includes/http/CurlHttpRequestTest.php9
-rw-r--r--www/wiki/tests/integration/includes/http/MWHttpRequestTestCase.php251
-rw-r--r--www/wiki/tests/integration/includes/http/PhpHttpRequestTest.php9
-rw-r--r--www/wiki/tests/integration/includes/shell/FirejailCommandTest.php78
-rw-r--r--www/wiki/tests/parser/DbTestPreviewer.php205
-rw-r--r--www/wiki/tests/parser/DbTestRecorder.php87
-rw-r--r--www/wiki/tests/parser/DjVuSupport.php57
-rw-r--r--www/wiki/tests/parser/MultiTestRecorder.php55
-rw-r--r--www/wiki/tests/parser/ParserTestMockParser.php20
-rw-r--r--www/wiki/tests/parser/ParserTestParserHook.php66
-rw-r--r--www/wiki/tests/parser/ParserTestPrinter.php328
-rw-r--r--www/wiki/tests/parser/ParserTestResult.php44
-rw-r--r--www/wiki/tests/parser/ParserTestResultNormalizer.php87
-rw-r--r--www/wiki/tests/parser/ParserTestRunner.php1737
-rw-r--r--www/wiki/tests/parser/PhpunitTestRecorder.php18
-rw-r--r--www/wiki/tests/parser/README12
-rw-r--r--www/wiki/tests/parser/TestFileEditor.php196
-rw-r--r--www/wiki/tests/parser/TestFileReader.php335
-rw-r--r--www/wiki/tests/parser/TestRecorder.php93
-rw-r--r--www/wiki/tests/parser/TidySupport.php77
-rw-r--r--www/wiki/tests/parser/editTests.php488
-rw-r--r--www/wiki/tests/parser/extraParserTests.txtbin0 -> 1994 bytes
-rw-r--r--www/wiki/tests/parser/fuzzTest.php202
-rw-r--r--www/wiki/tests/parser/parserTests.php199
-rw-r--r--www/wiki/tests/parser/parserTests.txt30829
-rw-r--r--www/wiki/tests/parser/preprocess/All_system_messages.expected5604
-rw-r--r--www/wiki/tests/parser/preprocess/All_system_messages.txt5603
-rw-r--r--www/wiki/tests/parser/preprocess/Factorial.expected17
-rw-r--r--www/wiki/tests/parser/preprocess/Factorial.txt16
-rw-r--r--www/wiki/tests/parser/preprocess/Fundraising.expected18
-rw-r--r--www/wiki/tests/parser/preprocess/Fundraising.txt17
-rw-r--r--www/wiki/tests/parser/preprocess/NestedTemplates.expected90
-rw-r--r--www/wiki/tests/parser/preprocess/NestedTemplates.txt89
-rw-r--r--www/wiki/tests/parser/preprocess/QuoteQuran.expected140
-rw-r--r--www/wiki/tests/parser/preprocess/QuoteQuran.txt139
-rwxr-xr-xwww/wiki/tests/phan/bin/phan90
-rw-r--r--www/wiki/tests/phan/bin/postprocess-phan.php146
-rw-r--r--www/wiki/tests/phan/config.php497
-rw-r--r--www/wiki/tests/phan/stubs/README3
-rw-r--r--www/wiki/tests/phan/stubs/hhvm.php26
-rw-r--r--www/wiki/tests/phan/stubs/mail.php89
-rw-r--r--www/wiki/tests/phan/stubs/memcached.php16
-rw-r--r--www/wiki/tests/phan/stubs/phpunit4.php11
-rw-r--r--www/wiki/tests/phan/stubs/tideways.php12
-rw-r--r--www/wiki/tests/phan/stubs/wikidiff.php39
-rw-r--r--www/wiki/tests/phpunit/HamcrestPHPUnitIntegration.php34
-rw-r--r--www/wiki/tests/phpunit/LessFileCompilationTest.php54
-rw-r--r--www/wiki/tests/phpunit/Makefile78
-rw-r--r--www/wiki/tests/phpunit/MediaWikiCoversValidator.php50
-rw-r--r--www/wiki/tests/phpunit/MediaWikiLangTestCase.php24
-rw-r--r--www/wiki/tests/phpunit/MediaWikiPHPUnitTestListener.php130
-rw-r--r--www/wiki/tests/phpunit/MediaWikiTestCase.php2043
-rw-r--r--www/wiki/tests/phpunit/PHPUnit4And6Compat.php121
-rw-r--r--www/wiki/tests/phpunit/README50
-rw-r--r--www/wiki/tests/phpunit/ResourceLoaderTestCase.php185
-rw-r--r--www/wiki/tests/phpunit/TODO20
-rw-r--r--www/wiki/tests/phpunit/autoload.ide.php109
-rw-r--r--www/wiki/tests/phpunit/bootstrap.php29
-rw-r--r--www/wiki/tests/phpunit/data/autoloader/TestAutoloadedCamlClass.php4
-rw-r--r--www/wiki/tests/phpunit/data/autoloader/TestAutoloadedClass.php4
-rw-r--r--www/wiki/tests/phpunit/data/autoloader/TestAutoloadedLocalClass.php4
-rw-r--r--www/wiki/tests/phpunit/data/autoloader/TestAutoloadedSerializedClass.php4
-rw-r--r--www/wiki/tests/phpunit/data/categoriesrdf/categoriesRdf-out.nt23
-rw-r--r--www/wiki/tests/phpunit/data/composer/composer.json48
-rw-r--r--www/wiki/tests/phpunit/data/composer/composer.lock1195
-rw-r--r--www/wiki/tests/phpunit/data/composer/installed.json1682
-rw-r--r--www/wiki/tests/phpunit/data/composer/new-composer.json48
-rw-r--r--www/wiki/tests/phpunit/data/css/bom.css1
-rw-r--r--www/wiki/tests/phpunit/data/css/comments.css7
-rw-r--r--www/wiki/tests/phpunit/data/css/expected.css11
-rw-r--r--www/wiki/tests/phpunit/data/css/simple-ltr.gifbin0 -> 35 bytes
-rw-r--r--www/wiki/tests/phpunit/data/css/simple-rtl.gifbin0 -> 35 bytes
-rw-r--r--www/wiki/tests/phpunit/data/css/test.css11
-rw-r--r--www/wiki/tests/phpunit/data/cssmin/circle.svg5
-rw-r--r--www/wiki/tests/phpunit/data/cssmin/green.gifbin0 -> 35 bytes
-rw-r--r--www/wiki/tests/phpunit/data/cssmin/large.pngbin0 -> 36462 bytes
-rw-r--r--www/wiki/tests/phpunit/data/cssmin/red.gifbin0 -> 35 bytes
-rw-r--r--www/wiki/tests/phpunit/data/db/mysql/functions.sql12
-rw-r--r--www/wiki/tests/phpunit/data/db/postgres/functions.sql12
-rw-r--r--www/wiki/tests/phpunit/data/db/sqlite/tables-1.13.sql342
-rw-r--r--www/wiki/tests/phpunit/data/db/sqlite/tables-1.15.sql454
-rw-r--r--www/wiki/tests/phpunit/data/db/sqlite/tables-1.16.sql478
-rw-r--r--www/wiki/tests/phpunit/data/db/sqlite/tables-1.17.sql511
-rw-r--r--www/wiki/tests/phpunit/data/db/sqlite/tables-1.18.sql530
-rw-r--r--www/wiki/tests/phpunit/data/db/sqlite/tables-1.19.sql531
-rw-r--r--www/wiki/tests/phpunit/data/db/sqlite/tables-1.20.sql534
-rw-r--r--www/wiki/tests/phpunit/data/db/sqlite/tables-1.21.sql577
-rw-r--r--www/wiki/tests/phpunit/data/db/sqlite/tables-1.22.sql575
-rw-r--r--www/wiki/tests/phpunit/data/db/sqlite/tables-1.23.sql580
-rw-r--r--www/wiki/tests/phpunit/data/filecontentshasher/hash.svg110
-rw-r--r--www/wiki/tests/phpunit/data/filecontentshasher/primes.txt105
-rw-r--r--www/wiki/tests/phpunit/data/filerepo/video.pngbin0 -> 116 bytes
-rw-r--r--www/wiki/tests/phpunit/data/filerepo/wiki.pngbin0 -> 22589 bytes
-rw-r--r--www/wiki/tests/phpunit/data/gitinfo/extension/gitinfo.json7
-rw-r--r--www/wiki/tests/phpunit/data/gitinfo/info-testValidJsonData.json7
-rw-r--r--www/wiki/tests/phpunit/data/import/ImportLinkCacheIntegrationTest.xml43
-rw-r--r--www/wiki/tests/phpunit/data/less/common/test.common.mixins.less4
-rw-r--r--www/wiki/tests/phpunit/data/less/module/dependency.less3
-rw-r--r--www/wiki/tests/phpunit/data/less/module/styles.css5
-rw-r--r--www/wiki/tests/phpunit/data/less/module/styles.less6
-rw-r--r--www/wiki/tests/phpunit/data/localisationcache/ba.json3
-rw-r--r--www/wiki/tests/phpunit/data/localisationcache/en.json5
-rw-r--r--www/wiki/tests/phpunit/data/localisationcache/ru.json4
-rw-r--r--www/wiki/tests/phpunit/data/media/1bit-png.pngbin0 -> 167 bytes
-rw-r--r--www/wiki/tests/phpunit/data/media/2_webp_a.webpbin0 -> 17128 bytes
-rw-r--r--www/wiki/tests/phpunit/data/media/2_webp_ll.webpbin0 -> 29360 bytes
-rw-r--r--www/wiki/tests/phpunit/data/media/80x60-2layers.xcfbin0 -> 1162 bytes
-rw-r--r--www/wiki/tests/phpunit/data/media/80x60-Greyscale.xcfbin0 -> 667 bytes
-rw-r--r--www/wiki/tests/phpunit/data/media/80x60-RGB.xcfbin0 -> 677 bytes
-rw-r--r--www/wiki/tests/phpunit/data/media/Animated_PNG_example_bouncing_beach_ball.pngbin0 -> 72209 bytes
-rw-r--r--www/wiki/tests/phpunit/data/media/Bishzilla_blink.gifbin0 -> 39057 bytes
-rw-r--r--www/wiki/tests/phpunit/data/media/Gtk-media-play-ltr.svg35
-rw-r--r--www/wiki/tests/phpunit/data/media/LoremIpsum.djvubin0 -> 3249 bytes
-rw-r--r--www/wiki/tests/phpunit/data/media/Png-native-test.pngbin0 -> 4665 bytes
-rw-r--r--www/wiki/tests/phpunit/data/media/QA_icon.svg77
-rw-r--r--www/wiki/tests/phpunit/data/media/README61
-rw-r--r--www/wiki/tests/phpunit/data/media/Soccer_ball_animated.svg55
-rw-r--r--www/wiki/tests/phpunit/data/media/Speech_bubbles.svg14
-rw-r--r--www/wiki/tests/phpunit/data/media/Toll_Texas_1.svg150
-rw-r--r--www/wiki/tests/phpunit/data/media/Tux.svg902
-rw-r--r--www/wiki/tests/phpunit/data/media/US_states_by_total_state_tax_revenue.svg248
-rw-r--r--www/wiki/tests/phpunit/data/media/Wikimedia-logo.svg14
-rw-r--r--www/wiki/tests/phpunit/data/media/Xmp-exif-multilingual_test.jpgbin0 -> 12544 bytes
-rw-r--r--www/wiki/tests/phpunit/data/media/adobergb.jpgbin0 -> 5308 bytes
-rw-r--r--www/wiki/tests/phpunit/data/media/animated-xmp.gifbin0 -> 3864 bytes
-rw-r--r--www/wiki/tests/phpunit/data/media/animated.gifbin0 -> 497 bytes
-rw-r--r--www/wiki/tests/phpunit/data/media/broken_exif_date.jpgbin0 -> 3233 bytes
-rw-r--r--www/wiki/tests/phpunit/data/media/exif-gps.jpgbin0 -> 665 bytes
-rw-r--r--www/wiki/tests/phpunit/data/media/exif-user-comment.jpgbin0 -> 484 bytes
-rw-r--r--www/wiki/tests/phpunit/data/media/greyscale-na-png.pngbin0 -> 365 bytes
-rw-r--r--www/wiki/tests/phpunit/data/media/greyscale-png.pngbin0 -> 415 bytes
-rw-r--r--www/wiki/tests/phpunit/data/media/iptc-invalid-psir.jpgbin0 -> 9574 bytes
-rw-r--r--www/wiki/tests/phpunit/data/media/iptc-timetest-invalid.jpgbin0 -> 9573 bytes
-rw-r--r--www/wiki/tests/phpunit/data/media/iptc-timetest.jpgbin0 -> 9573 bytes
-rw-r--r--www/wiki/tests/phpunit/data/media/jpeg-comment-binary.jpgbin0 -> 448 bytes
-rw-r--r--www/wiki/tests/phpunit/data/media/jpeg-comment-iso8859-1.jpgbin0 -> 447 bytes
-rw-r--r--www/wiki/tests/phpunit/data/media/jpeg-comment-multiple.jpgbin0 -> 431 bytes
-rw-r--r--www/wiki/tests/phpunit/data/media/jpeg-comment-utf.jpgbin0 -> 445 bytes
-rw-r--r--www/wiki/tests/phpunit/data/media/jpeg-iptc-bad-hash.jpgbin0 -> 499 bytes
-rw-r--r--www/wiki/tests/phpunit/data/media/jpeg-iptc-good-hash.jpgbin0 -> 499 bytes
-rw-r--r--www/wiki/tests/phpunit/data/media/jpeg-padding-even.jpgbin0 -> 450 bytes
-rw-r--r--www/wiki/tests/phpunit/data/media/jpeg-padding-odd.jpgbin0 -> 451 bytes
-rw-r--r--www/wiki/tests/phpunit/data/media/jpeg-segment-loop1.jpgbin0 -> 20 bytes
-rw-r--r--www/wiki/tests/phpunit/data/media/jpeg-segment-loop2.jpgbin0 -> 24 bytes
-rw-r--r--www/wiki/tests/phpunit/data/media/jpeg-xmp-alt.jpgbin0 -> 3255 bytes
-rw-r--r--www/wiki/tests/phpunit/data/media/jpeg-xmp-psir.jpgbin0 -> 3308 bytes
-rw-r--r--www/wiki/tests/phpunit/data/media/jpeg-xmp-psir.xmp35
-rw-r--r--www/wiki/tests/phpunit/data/media/landscape-plain.jpgbin0 -> 38771 bytes
-rw-r--r--www/wiki/tests/phpunit/data/media/missingprofile.jpgbin0 -> 4576 bytes
-rw-r--r--www/wiki/tests/phpunit/data/media/nonanimated.gifbin0 -> 200 bytes
-rw-r--r--www/wiki/tests/phpunit/data/media/portrait-rotated.jpgbin0 -> 38577 bytes
-rw-r--r--www/wiki/tests/phpunit/data/media/rgb-na-png.pngbin0 -> 593 bytes
-rw-r--r--www/wiki/tests/phpunit/data/media/rgb-png.pngbin0 -> 663 bytes
-rw-r--r--www/wiki/tests/phpunit/data/media/say-test-mpeg1.mp3bin0 -> 8776 bytes
-rw-r--r--www/wiki/tests/phpunit/data/media/say-test-mpeg2.5.mp3bin0 -> 1224 bytes
-rw-r--r--www/wiki/tests/phpunit/data/media/say-test-mpeg2.mp3bin0 -> 4389 bytes
-rw-r--r--www/wiki/tests/phpunit/data/media/say-test-with-id3.mp3bin0 -> 8795 bytes
-rw-r--r--www/wiki/tests/phpunit/data/media/say-test.oggbin0 -> 5132 bytes
-rw-r--r--www/wiki/tests/phpunit/data/media/say-test.opusbin0 -> 9798 bytes
-rw-r--r--www/wiki/tests/phpunit/data/media/srgb.jpgbin0 -> 7738 bytes
-rw-r--r--www/wiki/tests/phpunit/data/media/test.jpgbin0 -> 437 bytes
-rw-r--r--www/wiki/tests/phpunit/data/media/test.tiffbin0 -> 566 bytes
-rw-r--r--www/wiki/tests/phpunit/data/media/tinyrgb.iccbin0 -> 524 bytes
-rw-r--r--www/wiki/tests/phpunit/data/media/tinyrgb.jpgbin0 -> 5118 bytes
-rw-r--r--www/wiki/tests/phpunit/data/media/webp_animated.webpbin0 -> 380850 bytes
-rw-r--r--www/wiki/tests/phpunit/data/media/xmp.pngbin0 -> 582 bytes
-rw-r--r--www/wiki/tests/phpunit/data/media/yuv420.jpgbin0 -> 59007 bytes
-rw-r--r--www/wiki/tests/phpunit/data/media/yuv444.jpgbin0 -> 70637 bytes
-rw-r--r--www/wiki/tests/phpunit/data/parser/320x240.ogvbin0 -> 322279 bytes
-rw-r--r--www/wiki/tests/phpunit/data/parser/LoremIpsum.djvubin0 -> 3249 bytes
-rw-r--r--www/wiki/tests/phpunit/data/parser/headbg.jpgbin0 -> 7881 bytes
-rw-r--r--www/wiki/tests/phpunit/data/parser/wiki.pngbin0 -> 22589 bytes
-rw-r--r--www/wiki/tests/phpunit/data/registration/bad_spdx.json6
-rw-r--r--www/wiki/tests/phpunit/data/registration/good.json4
-rw-r--r--www/wiki/tests/phpunit/data/registration/invalid.json5
-rw-r--r--www/wiki/tests/phpunit/data/registration/newer_manifest_version.json4
-rw-r--r--www/wiki/tests/phpunit/data/registration/no_manifest_version.json3
-rw-r--r--www/wiki/tests/phpunit/data/registration/notjson.txt1
-rw-r--r--www/wiki/tests/phpunit/data/registration/old_manifest_version.json4
-rw-r--r--www/wiki/tests/phpunit/data/resourceloader/abc.gifbin0 -> 74 bytes
-rw-r--r--www/wiki/tests/phpunit/data/resourceloader/def.svg6
-rw-r--r--www/wiki/tests/phpunit/data/resourceloader/def_variantize.svg6
-rw-r--r--www/wiki/tests/phpunit/data/resourceloader/ghi.svg4
-rw-r--r--www/wiki/tests/phpunit/data/resourceloader/ghi_massage.svg4
-rw-r--r--www/wiki/tests/phpunit/data/resourceloader/jkl.svg4
-rw-r--r--www/wiki/tests/phpunit/data/resourceloader/mno-ltr.svg10
-rw-r--r--www/wiki/tests/phpunit/data/resourceloader/mno-rtl.svg10
-rw-r--r--www/wiki/tests/phpunit/data/resourceloader/oouiimagemodule/apex/icons.json6
-rw-r--r--www/wiki/tests/phpunit/data/resourceloader/oouiimagemodule/apex/images/icons/stu.svg6
-rw-r--r--www/wiki/tests/phpunit/data/resourceloader/oouiimagemodule/wikimediaui/icons.json6
-rw-r--r--www/wiki/tests/phpunit/data/resourceloader/oouiimagemodule/wikimediaui/images/icons/stu.svg6
-rw-r--r--www/wiki/tests/phpunit/data/resourceloader/pqr-a.svg6
-rw-r--r--www/wiki/tests/phpunit/data/resourceloader/pqr-b.svg6
-rw-r--r--www/wiki/tests/phpunit/data/resourceloader/pqr-f.svg6
-rw-r--r--www/wiki/tests/phpunit/data/resourceloader/script-comment.js3
-rw-r--r--www/wiki/tests/phpunit/data/resourceloader/script-nosemi.js2
-rw-r--r--www/wiki/tests/phpunit/data/templates/bad_partial.mustache1
-rw-r--r--www/wiki/tests/phpunit/data/templates/foobar.mustache1
-rw-r--r--www/wiki/tests/phpunit/data/templates/foobar_args.mustache1
-rw-r--r--www/wiki/tests/phpunit/data/templates/has_partial.mustache1
-rw-r--r--www/wiki/tests/phpunit/data/templates/recurse.mustache1
-rw-r--r--www/wiki/tests/phpunit/data/upload/buggynamespace-bad.svg24
-rw-r--r--www/wiki/tests/phpunit/data/upload/buggynamespace-evilhtml.svg12
-rw-r--r--www/wiki/tests/phpunit/data/upload/buggynamespace-okay.svg52
-rw-r--r--www/wiki/tests/phpunit/data/upload/buggynamespace-okay2.svg52
-rw-r--r--www/wiki/tests/phpunit/data/upload/buggynamespace-original.svg33
-rw-r--r--www/wiki/tests/phpunit/data/upload/headbg.jpgbin0 -> 7881 bytes
-rw-r--r--www/wiki/tests/phpunit/data/xmp/1.result.php8
-rw-r--r--www/wiki/tests/phpunit/data/xmp/1.xmp11
-rw-r--r--www/wiki/tests/phpunit/data/xmp/2.result.php8
-rw-r--r--www/wiki/tests/phpunit/data/xmp/2.xmp12
-rw-r--r--www/wiki/tests/phpunit/data/xmp/3-invalid.result.php7
-rw-r--r--www/wiki/tests/phpunit/data/xmp/3-invalid.xmp31
-rw-r--r--www/wiki/tests/phpunit/data/xmp/3.result.php8
-rw-r--r--www/wiki/tests/phpunit/data/xmp/3.xmp29
-rw-r--r--www/wiki/tests/phpunit/data/xmp/4.result.php7
-rw-r--r--www/wiki/tests/phpunit/data/xmp/4.xmp22
-rw-r--r--www/wiki/tests/phpunit/data/xmp/5.result.php7
-rw-r--r--www/wiki/tests/phpunit/data/xmp/5.xmp16
-rw-r--r--www/wiki/tests/phpunit/data/xmp/6.result.php8
-rw-r--r--www/wiki/tests/phpunit/data/xmp/6.xmp18
-rw-r--r--www/wiki/tests/phpunit/data/xmp/7.result.php52
-rw-r--r--www/wiki/tests/phpunit/data/xmp/7.xmp67
-rw-r--r--www/wiki/tests/phpunit/data/xmp/README3
-rw-r--r--www/wiki/tests/phpunit/data/xmp/bag-for-seq.result.php10
-rw-r--r--www/wiki/tests/phpunit/data/xmp/bag-for-seq.xmp1
-rw-r--r--www/wiki/tests/phpunit/data/xmp/doctype-included.result.php3
-rw-r--r--www/wiki/tests/phpunit/data/xmp/doctype-included.xmp12
-rw-r--r--www/wiki/tests/phpunit/data/xmp/doctype-not-included.xmp11
-rw-r--r--www/wiki/tests/phpunit/data/xmp/flash.result.php8
-rw-r--r--www/wiki/tests/phpunit/data/xmp/flash.xmp11
-rw-r--r--www/wiki/tests/phpunit/data/xmp/gps.result.php11
-rw-r--r--www/wiki/tests/phpunit/data/xmp/gps.xmp17
-rw-r--r--www/wiki/tests/phpunit/data/xmp/invalid-child-not-struct.result.php7
-rw-r--r--www/wiki/tests/phpunit/data/xmp/invalid-child-not-struct.xmp12
-rw-r--r--www/wiki/tests/phpunit/data/xmp/no-namespace.result.php7
-rw-r--r--www/wiki/tests/phpunit/data/xmp/no-namespace.xmp11
-rw-r--r--www/wiki/tests/phpunit/data/xmp/no-recognized-props.result.php2
-rw-r--r--www/wiki/tests/phpunit/data/xmp/no-recognized-props.xmp8
-rw-r--r--www/wiki/tests/phpunit/data/xmp/utf16BE.result.php12
-rw-r--r--www/wiki/tests/phpunit/data/xmp/utf16BE.xmpbin0 -> 930 bytes
-rw-r--r--www/wiki/tests/phpunit/data/xmp/utf16LE.result.php12
-rw-r--r--www/wiki/tests/phpunit/data/xmp/utf16LE.xmpbin0 -> 930 bytes
-rw-r--r--www/wiki/tests/phpunit/data/xmp/utf32BE.result.php12
-rw-r--r--www/wiki/tests/phpunit/data/xmp/utf32BE.xmpbin0 -> 1856 bytes
-rw-r--r--www/wiki/tests/phpunit/data/xmp/utf32LE.result.php12
-rw-r--r--www/wiki/tests/phpunit/data/xmp/utf32LE.xmpbin0 -> 1856 bytes
-rw-r--r--www/wiki/tests/phpunit/data/xmp/xmpExt.result.php8
-rw-r--r--www/wiki/tests/phpunit/data/xmp/xmpExt.xmp13
-rw-r--r--www/wiki/tests/phpunit/data/xmp/xmpExt2.xmp8
-rw-r--r--www/wiki/tests/phpunit/data/zip/cd-gap.zipbin0 -> 182 bytes
-rw-r--r--www/wiki/tests/phpunit/data/zip/cd-truncated.zipbin0 -> 171 bytes
-rw-r--r--www/wiki/tests/phpunit/data/zip/class-trailing-null.zipbin0 -> 173 bytes
-rw-r--r--www/wiki/tests/phpunit/data/zip/class-trailing-slash.zipbin0 -> 173 bytes
-rw-r--r--www/wiki/tests/phpunit/data/zip/class.zipbin0 -> 173 bytes
-rw-r--r--www/wiki/tests/phpunit/data/zip/empty.zipbin0 -> 22 bytes
-rw-r--r--www/wiki/tests/phpunit/data/zip/looks-like-zip64.zipbin0 -> 173 bytes
-rw-r--r--www/wiki/tests/phpunit/data/zip/nosig.zipbin0 -> 173 bytes
-rw-r--r--www/wiki/tests/phpunit/data/zip/split.zipbin0 -> 196 bytes
-rw-r--r--www/wiki/tests/phpunit/data/zip/trail.zipbin0 -> 181 bytes
-rw-r--r--www/wiki/tests/phpunit/data/zip/wrong-cd-start-disk.zipbin0 -> 173 bytes
-rw-r--r--www/wiki/tests/phpunit/data/zip/wrong-central-entry-sig.zipbin0 -> 173 bytes
-rw-r--r--www/wiki/tests/phpunit/docs/ExportDemoTest.php31
-rw-r--r--www/wiki/tests/phpunit/includes/ActorMigrationTest.php695
-rw-r--r--www/wiki/tests/phpunit/includes/AutopromoteTest.php56
-rw-r--r--www/wiki/tests/phpunit/includes/BlockTest.php463
-rw-r--r--www/wiki/tests/phpunit/includes/CommentStoreTest.php778
-rw-r--r--www/wiki/tests/phpunit/includes/DeprecatedGlobalTest.php81
-rw-r--r--www/wiki/tests/phpunit/includes/DiffHistoryBlobTest.php45
-rw-r--r--www/wiki/tests/phpunit/includes/EditPageTest.php727
-rw-r--r--www/wiki/tests/phpunit/includes/ExportTest.php67
-rw-r--r--www/wiki/tests/phpunit/includes/ExtraParserTest.php216
-rw-r--r--www/wiki/tests/phpunit/includes/FauxRequestTest.php245
-rw-r--r--www/wiki/tests/phpunit/includes/FauxResponseTest.php148
-rw-r--r--www/wiki/tests/phpunit/includes/FormOptionsInitializationTest.php87
-rw-r--r--www/wiki/tests/phpunit/includes/FormOptionsTest.php100
-rw-r--r--www/wiki/tests/phpunit/includes/GitInfoTest.php102
-rw-r--r--www/wiki/tests/phpunit/includes/GlobalFunctions/GlobalTest.php812
-rw-r--r--www/wiki/tests/phpunit/includes/GlobalFunctions/GlobalWithDBTest.php32
-rw-r--r--www/wiki/tests/phpunit/includes/GlobalFunctions/README2
-rw-r--r--www/wiki/tests/phpunit/includes/GlobalFunctions/wfAppendQueryTest.php79
-rw-r--r--www/wiki/tests/phpunit/includes/GlobalFunctions/wfArrayFilterTest.php42
-rw-r--r--www/wiki/tests/phpunit/includes/GlobalFunctions/wfArrayPlus2dTest.php94
-rw-r--r--www/wiki/tests/phpunit/includes/GlobalFunctions/wfAssembleUrlTest.php112
-rw-r--r--www/wiki/tests/phpunit/includes/GlobalFunctions/wfBaseNameTest.php40
-rw-r--r--www/wiki/tests/phpunit/includes/GlobalFunctions/wfEscapeShellArgTest.php43
-rw-r--r--www/wiki/tests/phpunit/includes/GlobalFunctions/wfExpandUrlTest.php117
-rw-r--r--www/wiki/tests/phpunit/includes/GlobalFunctions/wfGetCallerTest.php46
-rw-r--r--www/wiki/tests/phpunit/includes/GlobalFunctions/wfParseUrlTest.php157
-rw-r--r--www/wiki/tests/phpunit/includes/GlobalFunctions/wfRemoveDotSegmentsTest.php93
-rw-r--r--www/wiki/tests/phpunit/includes/GlobalFunctions/wfShellExecTest.php20
-rw-r--r--www/wiki/tests/phpunit/includes/GlobalFunctions/wfShorthandToIntegerTest.php31
-rw-r--r--www/wiki/tests/phpunit/includes/GlobalFunctions/wfStringToBoolTest.php51
-rw-r--r--www/wiki/tests/phpunit/includes/GlobalFunctions/wfThumbIsStandardTest.php105
-rw-r--r--www/wiki/tests/phpunit/includes/GlobalFunctions/wfTimestampTest.php194
-rw-r--r--www/wiki/tests/phpunit/includes/GlobalFunctions/wfUrlencodeTest.php125
-rw-r--r--www/wiki/tests/phpunit/includes/HooksTest.php253
-rw-r--r--www/wiki/tests/phpunit/includes/HtmlTest.php794
-rw-r--r--www/wiki/tests/phpunit/includes/LicensesTest.php25
-rw-r--r--www/wiki/tests/phpunit/includes/LinkFilterTest.php254
-rw-r--r--www/wiki/tests/phpunit/includes/LinkerTest.php480
-rw-r--r--www/wiki/tests/phpunit/includes/ListToggleTest.php49
-rw-r--r--www/wiki/tests/phpunit/includes/MWNamespaceTest.php578
-rw-r--r--www/wiki/tests/phpunit/includes/MWTimestampTest.php243
-rw-r--r--www/wiki/tests/phpunit/includes/MediaWikiServicesTest.php379
-rw-r--r--www/wiki/tests/phpunit/includes/MediaWikiTest.php157
-rw-r--r--www/wiki/tests/phpunit/includes/MediaWikiVersionFetcherTest.php22
-rw-r--r--www/wiki/tests/phpunit/includes/MergeHistoryTest.php124
-rw-r--r--www/wiki/tests/phpunit/includes/MessageTest.php855
-rw-r--r--www/wiki/tests/phpunit/includes/MovePageTest.php63
-rw-r--r--www/wiki/tests/phpunit/includes/OutputPageTest.php707
-rw-r--r--www/wiki/tests/phpunit/includes/PageArchiveTest.php265
-rw-r--r--www/wiki/tests/phpunit/includes/PagePropsTest.php303
-rw-r--r--www/wiki/tests/phpunit/includes/PathRouterTest.php269
-rw-r--r--www/wiki/tests/phpunit/includes/PreferencesTest.php90
-rw-r--r--www/wiki/tests/phpunit/includes/PrefixSearchTest.php386
-rw-r--r--www/wiki/tests/phpunit/includes/ReadOnlyModeTest.php194
-rw-r--r--www/wiki/tests/phpunit/includes/RevisionContentHandlerDbTest.php14
-rw-r--r--www/wiki/tests/phpunit/includes/RevisionDbTestBase.php1505
-rw-r--r--www/wiki/tests/phpunit/includes/RevisionNoContentHandlerDbTest.php14
-rw-r--r--www/wiki/tests/phpunit/includes/RevisionTest.php1498
-rw-r--r--www/wiki/tests/phpunit/includes/RevisionTestModifyableContent.php23
-rw-r--r--www/wiki/tests/phpunit/includes/RevisionTestModifyableContentHandler.php19
-rw-r--r--www/wiki/tests/phpunit/includes/SampleTest.php106
-rw-r--r--www/wiki/tests/phpunit/includes/SanitizerValidateEmailTest.php105
-rw-r--r--www/wiki/tests/phpunit/includes/SiteConfigurationTest.php363
-rw-r--r--www/wiki/tests/phpunit/includes/SiteStatsTest.php42
-rw-r--r--www/wiki/tests/phpunit/includes/StatusTest.php722
-rw-r--r--www/wiki/tests/phpunit/includes/Storage/BlobStoreFactoryTest.php46
-rw-r--r--www/wiki/tests/phpunit/includes/Storage/MutableRevisionRecordTest.php212
-rw-r--r--www/wiki/tests/phpunit/includes/Storage/MutableRevisionSlotsTest.php76
-rw-r--r--www/wiki/tests/phpunit/includes/Storage/NameTableStoreTest.php298
-rw-r--r--www/wiki/tests/phpunit/includes/Storage/RevisionArchiveRecordTest.php272
-rw-r--r--www/wiki/tests/phpunit/includes/Storage/RevisionRecordTests.php512
-rw-r--r--www/wiki/tests/phpunit/includes/Storage/RevisionSlotsTest.php139
-rw-r--r--www/wiki/tests/phpunit/includes/Storage/RevisionStoreDbTest.php1281
-rw-r--r--www/wiki/tests/phpunit/includes/Storage/RevisionStoreRecordTest.php363
-rw-r--r--www/wiki/tests/phpunit/includes/Storage/RevisionStoreTest.php690
-rw-r--r--www/wiki/tests/phpunit/includes/Storage/SlotRecordTest.php298
-rw-r--r--www/wiki/tests/phpunit/includes/Storage/SqlBlobStoreTest.php241
-rw-r--r--www/wiki/tests/phpunit/includes/TemplateCategoriesTest.php98
-rw-r--r--www/wiki/tests/phpunit/includes/TemplateParserTest.php123
-rw-r--r--www/wiki/tests/phpunit/includes/TestLogger.php127
-rw-r--r--www/wiki/tests/phpunit/includes/TestUser.php171
-rw-r--r--www/wiki/tests/phpunit/includes/TestUserRegistry.php125
-rw-r--r--www/wiki/tests/phpunit/includes/TimeAdjustTest.php39
-rw-r--r--www/wiki/tests/phpunit/includes/TitleArrayFromResultTest.php121
-rw-r--r--www/wiki/tests/phpunit/includes/TitleMethodsTest.php367
-rw-r--r--www/wiki/tests/phpunit/includes/TitlePermissionTest.php928
-rw-r--r--www/wiki/tests/phpunit/includes/TitleTest.php968
-rw-r--r--www/wiki/tests/phpunit/includes/WebRequestTest.php626
-rw-r--r--www/wiki/tests/phpunit/includes/WikiMapTest.php253
-rw-r--r--www/wiki/tests/phpunit/includes/WikiReferenceTest.php166
-rw-r--r--www/wiki/tests/phpunit/includes/XmlJsTest.php26
-rw-r--r--www/wiki/tests/phpunit/includes/XmlSelectTest.php182
-rw-r--r--www/wiki/tests/phpunit/includes/XmlTest.php617
-rw-r--r--www/wiki/tests/phpunit/includes/actions/ActionTest.php208
-rw-r--r--www/wiki/tests/phpunit/includes/api/ApiBaseTest.php1275
-rw-r--r--www/wiki/tests/phpunit/includes/api/ApiBlockTest.php252
-rw-r--r--www/wiki/tests/phpunit/includes/api/ApiCheckTokenTest.php95
-rw-r--r--www/wiki/tests/phpunit/includes/api/ApiClearHasMsgTest.php24
-rw-r--r--www/wiki/tests/phpunit/includes/api/ApiComparePagesTest.php653
-rw-r--r--www/wiki/tests/phpunit/includes/api/ApiContinuationManagerTest.php198
-rw-r--r--www/wiki/tests/phpunit/includes/api/ApiDeleteTest.php168
-rw-r--r--www/wiki/tests/phpunit/includes/api/ApiDisabledTest.php19
-rw-r--r--www/wiki/tests/phpunit/includes/api/ApiEditPageTest.php1604
-rw-r--r--www/wiki/tests/phpunit/includes/api/ApiErrorFormatterTest.php642
-rw-r--r--www/wiki/tests/phpunit/includes/api/ApiLoginTest.php301
-rw-r--r--www/wiki/tests/phpunit/includes/api/ApiLogoutTest.php75
-rw-r--r--www/wiki/tests/phpunit/includes/api/ApiMainTest.php1072
-rw-r--r--www/wiki/tests/phpunit/includes/api/ApiMessageTest.php189
-rw-r--r--www/wiki/tests/phpunit/includes/api/ApiModuleManagerTest.php330
-rw-r--r--www/wiki/tests/phpunit/includes/api/ApiMoveTest.php393
-rw-r--r--www/wiki/tests/phpunit/includes/api/ApiOpenSearchTest.php67
-rw-r--r--www/wiki/tests/phpunit/includes/api/ApiOptionsTest.php418
-rw-r--r--www/wiki/tests/phpunit/includes/api/ApiPageSetTest.php179
-rw-r--r--www/wiki/tests/phpunit/includes/api/ApiParseTest.php849
-rw-r--r--www/wiki/tests/phpunit/includes/api/ApiPurgeTest.php40
-rw-r--r--www/wiki/tests/phpunit/includes/api/ApiQueryAllPagesTest.php36
-rw-r--r--www/wiki/tests/phpunit/includes/api/ApiQueryRecentChangesIntegrationTest.php976
-rw-r--r--www/wiki/tests/phpunit/includes/api/ApiQueryWatchlistIntegrationTest.php1608
-rw-r--r--www/wiki/tests/phpunit/includes/api/ApiQueryWatchlistRawIntegrationTest.php542
-rw-r--r--www/wiki/tests/phpunit/includes/api/ApiResultTest.php1410
-rw-r--r--www/wiki/tests/phpunit/includes/api/ApiRevisionDeleteTest.php117
-rw-r--r--www/wiki/tests/phpunit/includes/api/ApiSetNotificationTimestampIntegrationTest.php51
-rw-r--r--www/wiki/tests/phpunit/includes/api/ApiStashEditTest.php27
-rw-r--r--www/wiki/tests/phpunit/includes/api/ApiTestCase.php260
-rw-r--r--www/wiki/tests/phpunit/includes/api/ApiTestCaseUpload.php8
-rw-r--r--www/wiki/tests/phpunit/includes/api/ApiTestContext.php21
-rw-r--r--www/wiki/tests/phpunit/includes/api/ApiTokensTest.php40
-rw-r--r--www/wiki/tests/phpunit/includes/api/ApiUnblockTest.php23
-rw-r--r--www/wiki/tests/phpunit/includes/api/ApiUploadTest.php560
-rw-r--r--www/wiki/tests/phpunit/includes/api/ApiUploadTestCase.php153
-rw-r--r--www/wiki/tests/phpunit/includes/api/ApiUsageExceptionTest.php44
-rw-r--r--www/wiki/tests/phpunit/includes/api/ApiUserrightsTest.php358
-rw-r--r--www/wiki/tests/phpunit/includes/api/ApiWatchTest.php148
-rw-r--r--www/wiki/tests/phpunit/includes/api/MockApi.php27
-rw-r--r--www/wiki/tests/phpunit/includes/api/MockApiQueryBase.php19
-rw-r--r--www/wiki/tests/phpunit/includes/api/PrefixUniquenessTest.php30
-rw-r--r--www/wiki/tests/phpunit/includes/api/RandomImageGenerator.php497
-rw-r--r--www/wiki/tests/phpunit/includes/api/UserWrapper.php25
-rw-r--r--www/wiki/tests/phpunit/includes/api/format/ApiFormatBaseTest.php388
-rw-r--r--www/wiki/tests/phpunit/includes/api/format/ApiFormatJsonTest.php129
-rw-r--r--www/wiki/tests/phpunit/includes/api/format/ApiFormatNoneTest.php51
-rw-r--r--www/wiki/tests/phpunit/includes/api/format/ApiFormatPhpTest.php139
-rw-r--r--www/wiki/tests/phpunit/includes/api/format/ApiFormatRawTest.php120
-rw-r--r--www/wiki/tests/phpunit/includes/api/format/ApiFormatTestBase.php93
-rw-r--r--www/wiki/tests/phpunit/includes/api/format/ApiFormatXmlTest.php123
-rw-r--r--www/wiki/tests/phpunit/includes/api/generateRandomImages.php45
-rw-r--r--www/wiki/tests/phpunit/includes/api/query/ApiQueryBasicTest.php346
-rw-r--r--www/wiki/tests/phpunit/includes/api/query/ApiQueryContinue2Test.php71
-rw-r--r--www/wiki/tests/phpunit/includes/api/query/ApiQueryContinueTest.php323
-rw-r--r--www/wiki/tests/phpunit/includes/api/query/ApiQueryContinueTestBase.php210
-rw-r--r--www/wiki/tests/phpunit/includes/api/query/ApiQueryRevisionsTest.php44
-rw-r--r--www/wiki/tests/phpunit/includes/api/query/ApiQueryTest.php151
-rw-r--r--www/wiki/tests/phpunit/includes/api/query/ApiQueryTestBase.php158
-rw-r--r--www/wiki/tests/phpunit/includes/api/query/ApiQueryUserContributionsTest.php194
-rw-r--r--www/wiki/tests/phpunit/includes/api/words.txt1000
-rw-r--r--www/wiki/tests/phpunit/includes/auth/AbstractAuthenticationProviderTest.php30
-rw-r--r--www/wiki/tests/phpunit/includes/auth/AbstractPasswordPrimaryAuthenticationProviderTest.php228
-rw-r--r--www/wiki/tests/phpunit/includes/auth/AbstractPreAuthenticationProviderTest.php45
-rw-r--r--www/wiki/tests/phpunit/includes/auth/AbstractPrimaryAuthenticationProviderTest.php174
-rw-r--r--www/wiki/tests/phpunit/includes/auth/AbstractSecondaryAuthenticationProviderTest.php84
-rw-r--r--www/wiki/tests/phpunit/includes/auth/AuthManagerTest.php3629
-rw-r--r--www/wiki/tests/phpunit/includes/auth/AuthPluginPrimaryAuthenticationProviderTest.php716
-rw-r--r--www/wiki/tests/phpunit/includes/auth/AuthenticationRequestTest.php517
-rw-r--r--www/wiki/tests/phpunit/includes/auth/AuthenticationRequestTestCase.php94
-rw-r--r--www/wiki/tests/phpunit/includes/auth/AuthenticationResponseTest.php112
-rw-r--r--www/wiki/tests/phpunit/includes/auth/ButtonAuthenticationRequestTest.php64
-rw-r--r--www/wiki/tests/phpunit/includes/auth/CheckBlocksSecondaryAuthenticationProviderTest.php191
-rw-r--r--www/wiki/tests/phpunit/includes/auth/ConfirmLinkAuthenticationRequestTest.php68
-rw-r--r--www/wiki/tests/phpunit/includes/auth/ConfirmLinkSecondaryAuthenticationProviderTest.php289
-rw-r--r--www/wiki/tests/phpunit/includes/auth/CreateFromLoginAuthenticationRequestTest.php57
-rw-r--r--www/wiki/tests/phpunit/includes/auth/CreatedAccountAuthenticationRequestTest.php30
-rw-r--r--www/wiki/tests/phpunit/includes/auth/CreationReasonAuthenticationRequestTest.php34
-rw-r--r--www/wiki/tests/phpunit/includes/auth/EmailNotificationSecondaryAuthenticationProviderTest.php109
-rw-r--r--www/wiki/tests/phpunit/includes/auth/LegacyHookPreAuthenticationProviderTest.php373
-rw-r--r--www/wiki/tests/phpunit/includes/auth/LocalPasswordPrimaryAuthenticationProviderTest.php658
-rw-r--r--www/wiki/tests/phpunit/includes/auth/PasswordAuthenticationRequestTest.php138
-rw-r--r--www/wiki/tests/phpunit/includes/auth/PasswordDomainAuthenticationRequestTest.php159
-rw-r--r--www/wiki/tests/phpunit/includes/auth/RememberMeAuthenticationRequestTest.php57
-rw-r--r--www/wiki/tests/phpunit/includes/auth/ResetPasswordSecondaryAuthenticationProviderTest.php310
-rw-r--r--www/wiki/tests/phpunit/includes/auth/TemporaryPasswordAuthenticationRequestTest.php79
-rw-r--r--www/wiki/tests/phpunit/includes/auth/TemporaryPasswordPrimaryAuthenticationProviderTest.php720
-rw-r--r--www/wiki/tests/phpunit/includes/auth/ThrottlePreAuthenticationProviderTest.php236
-rw-r--r--www/wiki/tests/phpunit/includes/auth/ThrottlerTest.php238
-rw-r--r--www/wiki/tests/phpunit/includes/auth/UserDataAuthenticationRequestTest.php176
-rw-r--r--www/wiki/tests/phpunit/includes/auth/UsernameAuthenticationRequestTest.php34
-rw-r--r--www/wiki/tests/phpunit/includes/cache/GenderCacheTest.php87
-rw-r--r--www/wiki/tests/phpunit/includes/cache/LocalisationCacheTest.php107
-rw-r--r--www/wiki/tests/phpunit/includes/cache/MessageCacheTest.php174
-rw-r--r--www/wiki/tests/phpunit/includes/changes/CategoryMembershipChangeTest.php156
-rw-r--r--www/wiki/tests/phpunit/includes/changes/ChangesListBooleanFilterGroupTest.php96
-rw-r--r--www/wiki/tests/phpunit/includes/changes/ChangesListBooleanFilterTest.php166
-rw-r--r--www/wiki/tests/phpunit/includes/changes/ChangesListFilterGroupTest.php79
-rw-r--r--www/wiki/tests/phpunit/includes/changes/ChangesListFilterTest.php116
-rw-r--r--www/wiki/tests/phpunit/includes/changes/ChangesListStringOptionsFilterGroupTest.php279
-rw-r--r--www/wiki/tests/phpunit/includes/changes/EnhancedChangesListTest.php228
-rw-r--r--www/wiki/tests/phpunit/includes/changes/OldChangesListTest.php234
-rw-r--r--www/wiki/tests/phpunit/includes/changes/RCCacheEntryFactoryTest.php236
-rw-r--r--www/wiki/tests/phpunit/includes/changes/RecentChangeTest.php224
-rw-r--r--www/wiki/tests/phpunit/includes/changes/TestRecentChangesHelper.php170
-rw-r--r--www/wiki/tests/phpunit/includes/changetags/ChangeTagsTest.php309
-rw-r--r--www/wiki/tests/phpunit/includes/collation/CollationFaTest.php55
-rw-r--r--www/wiki/tests/phpunit/includes/collation/CollationTest.php118
-rw-r--r--www/wiki/tests/phpunit/includes/collation/CustomUppercaseCollationTest.php68
-rw-r--r--www/wiki/tests/phpunit/includes/composer/ComposerVersionNormalizerTest.php163
-rw-r--r--www/wiki/tests/phpunit/includes/config/ConfigFactoryTest.php168
-rw-r--r--www/wiki/tests/phpunit/includes/config/EtcdConfigTest.php621
-rw-r--r--www/wiki/tests/phpunit/includes/config/GlobalVarConfigTest.php97
-rw-r--r--www/wiki/tests/phpunit/includes/config/HashConfigTest.php63
-rw-r--r--www/wiki/tests/phpunit/includes/config/MultiConfigTest.php39
-rw-r--r--www/wiki/tests/phpunit/includes/content/ContentHandlerTest.php497
-rw-r--r--www/wiki/tests/phpunit/includes/content/CssContentHandlerTest.php41
-rw-r--r--www/wiki/tests/phpunit/includes/content/CssContentTest.php133
-rw-r--r--www/wiki/tests/phpunit/includes/content/FileContentHandlerTest.php52
-rw-r--r--www/wiki/tests/phpunit/includes/content/JavaScriptContentHandlerTest.php41
-rw-r--r--www/wiki/tests/phpunit/includes/content/JavaScriptContentTest.php327
-rw-r--r--www/wiki/tests/phpunit/includes/content/JsonContentHandlerTest.php14
-rw-r--r--www/wiki/tests/phpunit/includes/content/JsonContentTest.php152
-rw-r--r--www/wiki/tests/phpunit/includes/content/TextContentHandlerTest.php55
-rw-r--r--www/wiki/tests/phpunit/includes/content/TextContentTest.php477
-rw-r--r--www/wiki/tests/phpunit/includes/content/WikitextContentHandlerTest.php365
-rw-r--r--www/wiki/tests/phpunit/includes/content/WikitextContentTest.php443
-rw-r--r--www/wiki/tests/phpunit/includes/content/WikitextStructureTest.php110
-rw-r--r--www/wiki/tests/phpunit/includes/context/RequestContextTest.php117
-rw-r--r--www/wiki/tests/phpunit/includes/db/DatabaseOracleTest.php52
-rw-r--r--www/wiki/tests/phpunit/includes/db/DatabasePostgresTest.php177
-rw-r--r--www/wiki/tests/phpunit/includes/db/DatabaseSqliteTest.php519
-rw-r--r--www/wiki/tests/phpunit/includes/db/DatabaseTestHelper.php267
-rw-r--r--www/wiki/tests/phpunit/includes/db/LBFactoryTest.php530
-rw-r--r--www/wiki/tests/phpunit/includes/db/LoadBalancerTest.php305
-rw-r--r--www/wiki/tests/phpunit/includes/debug/MWDebugTest.php140
-rw-r--r--www/wiki/tests/phpunit/includes/debug/logger/LegacyLoggerTest.php175
-rw-r--r--www/wiki/tests/phpunit/includes/debug/logger/MonologSpiTest.php136
-rw-r--r--www/wiki/tests/phpunit/includes/debug/logger/monolog/AvroFormatterTest.php76
-rw-r--r--www/wiki/tests/phpunit/includes/debug/logger/monolog/KafkaHandlerTest.php227
-rw-r--r--www/wiki/tests/phpunit/includes/debug/logger/monolog/LineFormatterTest.php75
-rw-r--r--www/wiki/tests/phpunit/includes/debug/logger/monolog/LogstashFormatterTest.php55
-rw-r--r--www/wiki/tests/phpunit/includes/deferred/CdnCacheUpdateTest.php31
-rw-r--r--www/wiki/tests/phpunit/includes/deferred/DeferredUpdatesTest.php338
-rw-r--r--www/wiki/tests/phpunit/includes/deferred/LinksUpdateTest.php422
-rw-r--r--www/wiki/tests/phpunit/includes/deferred/MWCallableUpdateTest.php82
-rw-r--r--www/wiki/tests/phpunit/includes/deferred/SearchUpdateTest.php87
-rw-r--r--www/wiki/tests/phpunit/includes/deferred/SiteStatsUpdateTest.php77
-rw-r--r--www/wiki/tests/phpunit/includes/deferred/TransactionRoundDefiningUpdateTest.php19
-rw-r--r--www/wiki/tests/phpunit/includes/diff/ArrayDiffFormatterTest.php134
-rw-r--r--www/wiki/tests/phpunit/includes/diff/DiffOpTest.php68
-rw-r--r--www/wiki/tests/phpunit/includes/diff/DiffTest.php19
-rw-r--r--www/wiki/tests/phpunit/includes/diff/DifferenceEngineTest.php148
-rw-r--r--www/wiki/tests/phpunit/includes/diff/FakeDiffOp.php11
-rw-r--r--www/wiki/tests/phpunit/includes/editpage/TextboxBuilderTest.php210
-rw-r--r--www/wiki/tests/phpunit/includes/exception/BadTitleErrorTest.php30
-rw-r--r--www/wiki/tests/phpunit/includes/exception/ErrorPageErrorTest.php52
-rw-r--r--www/wiki/tests/phpunit/includes/exception/HttpErrorTest.php63
-rw-r--r--www/wiki/tests/phpunit/includes/exception/MWExceptionHandlerTest.php74
-rw-r--r--www/wiki/tests/phpunit/includes/exception/MWExceptionTest.php193
-rw-r--r--www/wiki/tests/phpunit/includes/exception/ReadOnlyErrorTest.php16
-rw-r--r--www/wiki/tests/phpunit/includes/exception/ThrottledErrorTest.php31
-rw-r--r--www/wiki/tests/phpunit/includes/exception/UserNotLoggedInTest.php16
-rw-r--r--www/wiki/tests/phpunit/includes/externalstore/ExternalStoreFactoryTest.php41
-rw-r--r--www/wiki/tests/phpunit/includes/externalstore/ExternalStoreForTesting.php46
-rw-r--r--www/wiki/tests/phpunit/includes/externalstore/ExternalStoreTest.php53
-rw-r--r--www/wiki/tests/phpunit/includes/filebackend/FileBackendTest.php2641
-rw-r--r--www/wiki/tests/phpunit/includes/filebackend/SwiftFileBackendTest.php216
-rw-r--r--www/wiki/tests/phpunit/includes/filerepo/FileBackendDBRepoWrapperTest.php140
-rw-r--r--www/wiki/tests/phpunit/includes/filerepo/FileRepoTest.php55
-rw-r--r--www/wiki/tests/phpunit/includes/filerepo/MigrateFileRepoLayoutTest.php142
-rw-r--r--www/wiki/tests/phpunit/includes/filerepo/RepoGroupTest.php63
-rw-r--r--www/wiki/tests/phpunit/includes/filerepo/StoreBatchTest.php141
-rw-r--r--www/wiki/tests/phpunit/includes/filerepo/file/FileTest.php389
-rw-r--r--www/wiki/tests/phpunit/includes/filerepo/file/LocalFileTest.php183
-rw-r--r--www/wiki/tests/phpunit/includes/htmlform/HTMLAutoCompleteSelectFieldTest.php68
-rw-r--r--www/wiki/tests/phpunit/includes/htmlform/HTMLCheckMatrixTest.php105
-rw-r--r--www/wiki/tests/phpunit/includes/htmlform/HTMLFormTest.php57
-rw-r--r--www/wiki/tests/phpunit/includes/htmlform/HTMLRestrictionsFieldTest.php71
-rw-r--r--www/wiki/tests/phpunit/includes/http/HttpTest.php548
-rw-r--r--www/wiki/tests/phpunit/includes/import/ImportLinkCacheIntegrationTest.php104
-rw-r--r--www/wiki/tests/phpunit/includes/import/ImportTest.php329
-rw-r--r--www/wiki/tests/phpunit/includes/installer/InstallDocFormatterTest.php83
-rw-r--r--www/wiki/tests/phpunit/includes/installer/OracleInstallerTest.php51
-rw-r--r--www/wiki/tests/phpunit/includes/interwiki/ClassicInterwikiLookupTest.php273
-rw-r--r--www/wiki/tests/phpunit/includes/interwiki/InterwikiLookupAdapterTest.php133
-rw-r--r--www/wiki/tests/phpunit/includes/interwiki/InterwikiTest.php122
-rw-r--r--www/wiki/tests/phpunit/includes/jobqueue/JobQueueMemoryTest.php63
-rw-r--r--www/wiki/tests/phpunit/includes/jobqueue/JobQueueTest.php393
-rw-r--r--www/wiki/tests/phpunit/includes/jobqueue/JobTest.php133
-rw-r--r--www/wiki/tests/phpunit/includes/jobqueue/RefreshLinksPartitionTest.php113
-rw-r--r--www/wiki/tests/phpunit/includes/jobqueue/jobs/CategoryMembershipChangeJobTest.php87
-rw-r--r--www/wiki/tests/phpunit/includes/jobqueue/jobs/ClearUserWatchlistJobTest.php79
-rw-r--r--www/wiki/tests/phpunit/includes/json/FormatJsonTest.php375
-rw-r--r--www/wiki/tests/phpunit/includes/libs/ArrayUtilsTest.php310
-rw-r--r--www/wiki/tests/phpunit/includes/libs/CSSMinTest.php640
-rw-r--r--www/wiki/tests/phpunit/includes/libs/DeferredStringifierTest.php54
-rw-r--r--www/wiki/tests/phpunit/includes/libs/DnsSrvDiscovererTest.php144
-rw-r--r--www/wiki/tests/phpunit/includes/libs/GenericArrayObjectTest.php279
-rw-r--r--www/wiki/tests/phpunit/includes/libs/HashRingTest.php59
-rw-r--r--www/wiki/tests/phpunit/includes/libs/HtmlArmorTest.php55
-rw-r--r--www/wiki/tests/phpunit/includes/libs/IEUrlExtensionTest.php209
-rw-r--r--www/wiki/tests/phpunit/includes/libs/IPTest.php672
-rw-r--r--www/wiki/tests/phpunit/includes/libs/JavaScriptMinifierTest.php242
-rw-r--r--www/wiki/tests/phpunit/includes/libs/MWMessagePackTest.php77
-rw-r--r--www/wiki/tests/phpunit/includes/libs/MapCacheLRUTest.php117
-rw-r--r--www/wiki/tests/phpunit/includes/libs/MemoizedCallableTest.php142
-rw-r--r--www/wiki/tests/phpunit/includes/libs/ProcessCacheLRUTest.php268
-rw-r--r--www/wiki/tests/phpunit/includes/libs/SamplingStatsdClientTest.php77
-rw-r--r--www/wiki/tests/phpunit/includes/libs/StringUtilsTest.php128
-rw-r--r--www/wiki/tests/phpunit/includes/libs/TimingTest.php115
-rw-r--r--www/wiki/tests/phpunit/includes/libs/XhprofDataTest.php278
-rw-r--r--www/wiki/tests/phpunit/includes/libs/XhprofTest.php40
-rw-r--r--www/wiki/tests/phpunit/includes/libs/XmlTypeCheckTest.php79
-rw-r--r--www/wiki/tests/phpunit/includes/libs/composer/ComposerInstalledTest.php499
-rw-r--r--www/wiki/tests/phpunit/includes/libs/composer/ComposerJsonTest.php42
-rw-r--r--www/wiki/tests/phpunit/includes/libs/composer/ComposerLockTest.php121
-rw-r--r--www/wiki/tests/phpunit/includes/libs/http/HttpAcceptNegotiatorTest.php150
-rw-r--r--www/wiki/tests/phpunit/includes/libs/http/HttpAcceptParserTest.php56
-rw-r--r--www/wiki/tests/phpunit/includes/libs/mime/MimeAnalyzerTest.php131
-rw-r--r--www/wiki/tests/phpunit/includes/libs/objectcache/BagOStuffTest.php300
-rw-r--r--www/wiki/tests/phpunit/includes/libs/objectcache/CachedBagOStuffTest.php158
-rw-r--r--www/wiki/tests/phpunit/includes/libs/objectcache/HashBagOStuffTest.php163
-rw-r--r--www/wiki/tests/phpunit/includes/libs/objectcache/MultiWriteBagOStuffTest.php140
-rw-r--r--www/wiki/tests/phpunit/includes/libs/objectcache/ReplicatedBagOStuffTest.php62
-rw-r--r--www/wiki/tests/phpunit/includes/libs/objectcache/WANObjectCacheTest.php1711
-rw-r--r--www/wiki/tests/phpunit/includes/libs/rdbms/TransactionProfilerTest.php147
-rw-r--r--www/wiki/tests/phpunit/includes/libs/rdbms/connectionmanager/ConnectionManagerTest.php139
-rw-r--r--www/wiki/tests/phpunit/includes/libs/rdbms/connectionmanager/SessionConsistentConnectionManagerTest.php108
-rw-r--r--www/wiki/tests/phpunit/includes/libs/rdbms/database/DBConnRefTest.php148
-rw-r--r--www/wiki/tests/phpunit/includes/libs/rdbms/database/DatabaseDomainTest.php133
-rw-r--r--www/wiki/tests/phpunit/includes/libs/rdbms/database/DatabaseMssqlTest.php55
-rw-r--r--www/wiki/tests/phpunit/includes/libs/rdbms/database/DatabaseMysqlBaseTest.php743
-rw-r--r--www/wiki/tests/phpunit/includes/libs/rdbms/database/DatabaseSQLTest.php2067
-rw-r--r--www/wiki/tests/phpunit/includes/libs/rdbms/database/DatabaseSqliteRdbmsTest.php60
-rw-r--r--www/wiki/tests/phpunit/includes/libs/rdbms/database/DatabaseTest.php613
-rw-r--r--www/wiki/tests/phpunit/includes/libs/xmp/XMPTest.php227
-rw-r--r--www/wiki/tests/phpunit/includes/libs/xmp/XMPValidateTest.php55
-rw-r--r--www/wiki/tests/phpunit/includes/linkeddata/PageDataRequestHandlerTest.php284
-rw-r--r--www/wiki/tests/phpunit/includes/linker/LinkRendererFactoryTest.php82
-rw-r--r--www/wiki/tests/phpunit/includes/linker/LinkRendererTest.php189
-rw-r--r--www/wiki/tests/phpunit/includes/logging/BlockLogFormatterTest.php379
-rw-r--r--www/wiki/tests/phpunit/includes/logging/ContentModelLogFormatterTest.php60
-rw-r--r--www/wiki/tests/phpunit/includes/logging/DatabaseLogEntryTest.php162
-rw-r--r--www/wiki/tests/phpunit/includes/logging/DeleteLogFormatterTest.php556
-rw-r--r--www/wiki/tests/phpunit/includes/logging/ImportLogFormatterTest.php126
-rw-r--r--www/wiki/tests/phpunit/includes/logging/LogFormatterTest.php664
-rw-r--r--www/wiki/tests/phpunit/includes/logging/LogFormatterTestCase.php68
-rw-r--r--www/wiki/tests/phpunit/includes/logging/LogTests.i18n.php15
-rw-r--r--www/wiki/tests/phpunit/includes/logging/MergeLogFormatterTest.php70
-rw-r--r--www/wiki/tests/phpunit/includes/logging/MoveLogFormatterTest.php273
-rw-r--r--www/wiki/tests/phpunit/includes/logging/NewUsersLogFormatterTest.php204
-rw-r--r--www/wiki/tests/phpunit/includes/logging/PageLangLogFormatterTest.php56
-rw-r--r--www/wiki/tests/phpunit/includes/logging/PatrolLogFormatterTest.php121
-rw-r--r--www/wiki/tests/phpunit/includes/logging/ProtectLogFormatterTest.php431
-rw-r--r--www/wiki/tests/phpunit/includes/logging/RightsLogFormatterTest.php219
-rw-r--r--www/wiki/tests/phpunit/includes/logging/UploadLogFormatterTest.php169
-rw-r--r--www/wiki/tests/phpunit/includes/mail/MailAddressTest.php77
-rw-r--r--www/wiki/tests/phpunit/includes/mail/UserMailerTest.php14
-rw-r--r--www/wiki/tests/phpunit/includes/media/BitmapMetadataHandlerTest.php167
-rw-r--r--www/wiki/tests/phpunit/includes/media/BitmapScalingTest.php151
-rw-r--r--www/wiki/tests/phpunit/includes/media/DjVuTest.php69
-rw-r--r--www/wiki/tests/phpunit/includes/media/ExifBitmapTest.php142
-rw-r--r--www/wiki/tests/phpunit/includes/media/ExifRotationTest.php283
-rw-r--r--www/wiki/tests/phpunit/includes/media/ExifTest.php47
-rw-r--r--www/wiki/tests/phpunit/includes/media/FakeDimensionFile.php35
-rw-r--r--www/wiki/tests/phpunit/includes/media/FormatMetadataTest.php144
-rw-r--r--www/wiki/tests/phpunit/includes/media/GIFMetadataExtractorTest.php110
-rw-r--r--www/wiki/tests/phpunit/includes/media/GIFTest.php172
-rw-r--r--www/wiki/tests/phpunit/includes/media/IPTCTest.php85
-rw-r--r--www/wiki/tests/phpunit/includes/media/JpegMetadataExtractorTest.php128
-rw-r--r--www/wiki/tests/phpunit/includes/media/JpegPixelFormatTest.php115
-rw-r--r--www/wiki/tests/phpunit/includes/media/JpegTest.php122
-rw-r--r--www/wiki/tests/phpunit/includes/media/MediaHandlerTest.php68
-rw-r--r--www/wiki/tests/phpunit/includes/media/MediaWikiMediaTestCase.php86
-rw-r--r--www/wiki/tests/phpunit/includes/media/PNGMetadataExtractorTest.php137
-rw-r--r--www/wiki/tests/phpunit/includes/media/PNGTest.php161
-rw-r--r--www/wiki/tests/phpunit/includes/media/SVGMetadataExtractorTest.php155
-rw-r--r--www/wiki/tests/phpunit/includes/media/SVGTest.php113
-rw-r--r--www/wiki/tests/phpunit/includes/media/TiffTest.php44
-rw-r--r--www/wiki/tests/phpunit/includes/media/WebPTest.php145
-rw-r--r--www/wiki/tests/phpunit/includes/media/XCFTest.php83
-rw-r--r--www/wiki/tests/phpunit/includes/objectcache/MemcachedBagOStuffTest.php107
-rw-r--r--www/wiki/tests/phpunit/includes/objectcache/ObjectCacheTest.php115
-rw-r--r--www/wiki/tests/phpunit/includes/objectcache/RESTBagOStuffTest.php90
-rw-r--r--www/wiki/tests/phpunit/includes/objectcache/RedisBagOStuffTest.php110
-rw-r--r--www/wiki/tests/phpunit/includes/page/ArticleTablesTest.php52
-rw-r--r--www/wiki/tests/phpunit/includes/page/ArticleTest.php57
-rw-r--r--www/wiki/tests/phpunit/includes/page/ImagePage404Test.php54
-rw-r--r--www/wiki/tests/phpunit/includes/page/ImagePageTest.php92
-rw-r--r--www/wiki/tests/phpunit/includes/page/WikiCategoryPageTest.php63
-rw-r--r--www/wiki/tests/phpunit/includes/page/WikiPageContentHandlerDbTest.php42
-rw-r--r--www/wiki/tests/phpunit/includes/page/WikiPageDbTestBase.php1903
-rw-r--r--www/wiki/tests/phpunit/includes/page/WikiPageNoContentHandlerDbTest.php14
-rw-r--r--www/wiki/tests/phpunit/includes/pager/RangeChronologicalPagerTest.php99
-rw-r--r--www/wiki/tests/phpunit/includes/pager/ReverseChronologicalPagerTest.php68
-rw-r--r--www/wiki/tests/phpunit/includes/parser/CoreParserFunctionsTest.php21
-rw-r--r--www/wiki/tests/phpunit/includes/parser/MagicVariableTest.php232
-rw-r--r--www/wiki/tests/phpunit/includes/parser/ParserIntegrationTest.php65
-rw-r--r--www/wiki/tests/phpunit/includes/parser/ParserMethodsTest.php185
-rw-r--r--www/wiki/tests/phpunit/includes/parser/ParserOptionsTest.php223
-rw-r--r--www/wiki/tests/phpunit/includes/parser/ParserOutputTest.php294
-rw-r--r--www/wiki/tests/phpunit/includes/parser/ParserPreloadTest.php95
-rw-r--r--www/wiki/tests/phpunit/includes/parser/PreprocessorTest.php294
-rw-r--r--www/wiki/tests/phpunit/includes/parser/SanitizerTest.php571
-rw-r--r--www/wiki/tests/phpunit/includes/parser/StripStateTest.php136
-rw-r--r--www/wiki/tests/phpunit/includes/parser/TagHooksTest.php134
-rw-r--r--www/wiki/tests/phpunit/includes/parser/TidyTest.php63
-rw-r--r--www/wiki/tests/phpunit/includes/password/BcryptPasswordTest.php44
-rw-r--r--www/wiki/tests/phpunit/includes/password/EncryptedPasswordTest.php84
-rw-r--r--www/wiki/tests/phpunit/includes/password/LayeredParameterizedPasswordTest.php63
-rw-r--r--www/wiki/tests/phpunit/includes/password/MWOldPasswordTest.php24
-rw-r--r--www/wiki/tests/phpunit/includes/password/MWSaltedPasswordTest.php21
-rw-r--r--www/wiki/tests/phpunit/includes/password/PasswordFactoryTest.php110
-rw-r--r--www/wiki/tests/phpunit/includes/password/PasswordPolicyChecksTest.php159
-rw-r--r--www/wiki/tests/phpunit/includes/password/PasswordTest.php41
-rw-r--r--www/wiki/tests/phpunit/includes/password/PasswordTestCase.php112
-rw-r--r--www/wiki/tests/phpunit/includes/password/Pbkdf2PasswordFallbackTest.php29
-rw-r--r--www/wiki/tests/phpunit/includes/password/Pbkdf2PasswordTest.php29
-rw-r--r--www/wiki/tests/phpunit/includes/password/UserPasswordPolicyTest.php232
-rw-r--r--www/wiki/tests/phpunit/includes/poolcounter/PoolCounterTest.php84
-rw-r--r--www/wiki/tests/phpunit/includes/preferences/DefaultPreferencesFactoryTest.php183
-rw-r--r--www/wiki/tests/phpunit/includes/rcfeed/RCFeedIntegrationTest.php98
-rw-r--r--www/wiki/tests/phpunit/includes/registration/ExtensionJsonValidatorTest.php84
-rw-r--r--www/wiki/tests/phpunit/includes/registration/ExtensionProcessorTest.php742
-rw-r--r--www/wiki/tests/phpunit/includes/registration/ExtensionRegistryTest.php352
-rw-r--r--www/wiki/tests/phpunit/includes/registration/VersionCheckerTest.php207
-rw-r--r--www/wiki/tests/phpunit/includes/resourceloader/DerivativeResourceLoaderContextTest.php128
-rw-r--r--www/wiki/tests/phpunit/includes/resourceloader/MessageBlobStoreTest.php224
-rw-r--r--www/wiki/tests/phpunit/includes/resourceloader/ResourceLoaderClientHtmlTest.php405
-rw-r--r--www/wiki/tests/phpunit/includes/resourceloader/ResourceLoaderContextTest.php120
-rw-r--r--www/wiki/tests/phpunit/includes/resourceloader/ResourceLoaderFileModuleTest.php353
-rw-r--r--www/wiki/tests/phpunit/includes/resourceloader/ResourceLoaderImageModuleTest.php265
-rw-r--r--www/wiki/tests/phpunit/includes/resourceloader/ResourceLoaderImageTest.php136
-rw-r--r--www/wiki/tests/phpunit/includes/resourceloader/ResourceLoaderModuleTest.php224
-rw-r--r--www/wiki/tests/phpunit/includes/resourceloader/ResourceLoaderOOUIImageModuleTest.php65
-rw-r--r--www/wiki/tests/phpunit/includes/resourceloader/ResourceLoaderSkinModuleTest.php207
-rw-r--r--www/wiki/tests/phpunit/includes/resourceloader/ResourceLoaderStartUpModuleTest.php494
-rw-r--r--www/wiki/tests/phpunit/includes/resourceloader/ResourceLoaderTest.php911
-rw-r--r--www/wiki/tests/phpunit/includes/resourceloader/ResourceLoaderWikiModuleTest.php380
-rw-r--r--www/wiki/tests/phpunit/includes/resourceloader/templates/template.html1
-rw-r--r--www/wiki/tests/phpunit/includes/resourceloader/templates/template2.html1
-rw-r--r--www/wiki/tests/phpunit/includes/resourceloader/templates/template_awesome.handlebars1
-rw-r--r--www/wiki/tests/phpunit/includes/search/ParserOutputSearchDataExtractorTest.php70
-rw-r--r--www/wiki/tests/phpunit/includes/search/SearchEnginePrefixTest.php362
-rw-r--r--www/wiki/tests/phpunit/includes/search/SearchEngineTest.php368
-rw-r--r--www/wiki/tests/phpunit/includes/search/SearchIndexFieldTest.php56
-rw-r--r--www/wiki/tests/phpunit/includes/search/SearchSuggestionSetTest.php104
-rw-r--r--www/wiki/tests/phpunit/includes/services/ServiceContainerTest.php414
-rw-r--r--www/wiki/tests/phpunit/includes/services/TestWiring1.php10
-rw-r--r--www/wiki/tests/phpunit/includes/services/TestWiring2.php10
-rw-r--r--www/wiki/tests/phpunit/includes/session/BotPasswordSessionProviderTest.php340
-rw-r--r--www/wiki/tests/phpunit/includes/session/CookieSessionProviderTest.php842
-rw-r--r--www/wiki/tests/phpunit/includes/session/ImmutableSessionProviderWithCookieTest.php305
-rw-r--r--www/wiki/tests/phpunit/includes/session/MetadataMergeExceptionTest.php30
-rw-r--r--www/wiki/tests/phpunit/includes/session/PHPSessionHandlerTest.php361
-rw-r--r--www/wiki/tests/phpunit/includes/session/SessionBackendTest.php963
-rw-r--r--www/wiki/tests/phpunit/includes/session/SessionIdTest.php22
-rw-r--r--www/wiki/tests/phpunit/includes/session/SessionInfoTest.php356
-rw-r--r--www/wiki/tests/phpunit/includes/session/SessionManagerTest.php1521
-rw-r--r--www/wiki/tests/phpunit/includes/session/SessionProviderTest.php206
-rw-r--r--www/wiki/tests/phpunit/includes/session/SessionTest.php373
-rw-r--r--www/wiki/tests/phpunit/includes/session/TestBagOStuff.php81
-rw-r--r--www/wiki/tests/phpunit/includes/session/TestUtils.php106
-rw-r--r--www/wiki/tests/phpunit/includes/session/TokenTest.php67
-rw-r--r--www/wiki/tests/phpunit/includes/session/UserInfoTest.php186
-rw-r--r--www/wiki/tests/phpunit/includes/shell/CommandFactoryTest.php50
-rw-r--r--www/wiki/tests/phpunit/includes/shell/CommandTest.php181
-rw-r--r--www/wiki/tests/phpunit/includes/shell/FirejailCommandTest.php85
-rw-r--r--www/wiki/tests/phpunit/includes/shell/ShellTest.php105
-rw-r--r--www/wiki/tests/phpunit/includes/site/CachingSiteStoreTest.php163
-rw-r--r--www/wiki/tests/phpunit/includes/site/DBSiteStoreTest.php166
-rw-r--r--www/wiki/tests/phpunit/includes/site/FileBasedSiteLookupTest.php103
-rw-r--r--www/wiki/tests/phpunit/includes/site/HashSiteStoreTest.php105
-rw-r--r--www/wiki/tests/phpunit/includes/site/MediaWikiPageNameNormalizerTest.php117
-rw-r--r--www/wiki/tests/phpunit/includes/site/MediaWikiSiteTest.php108
-rw-r--r--www/wiki/tests/phpunit/includes/site/SiteExporterTest.php150
-rw-r--r--www/wiki/tests/phpunit/includes/site/SiteImporterTest.php202
-rw-r--r--www/wiki/tests/phpunit/includes/site/SiteImporterTest.xml19
-rw-r--r--www/wiki/tests/phpunit/includes/site/SiteListTest.php239
-rw-r--r--www/wiki/tests/phpunit/includes/site/SiteTest.php295
-rw-r--r--www/wiki/tests/phpunit/includes/site/SitesCacheFileBuilderTest.php137
-rw-r--r--www/wiki/tests/phpunit/includes/site/TestSites.php112
-rw-r--r--www/wiki/tests/phpunit/includes/skins/SkinFactoryTest.php82
-rw-r--r--www/wiki/tests/phpunit/includes/skins/SkinTemplateTest.php101
-rw-r--r--www/wiki/tests/phpunit/includes/sparql/SparqlClientTest.php190
-rw-r--r--www/wiki/tests/phpunit/includes/specialpage/AbstractChangesListSpecialPageTestCase.php151
-rw-r--r--www/wiki/tests/phpunit/includes/specialpage/ChangesListSpecialPageTest.php1098
-rw-r--r--www/wiki/tests/phpunit/includes/specialpage/SpecialPageFactoryTest.php285
-rw-r--r--www/wiki/tests/phpunit/includes/specialpage/SpecialPageTest.php104
-rw-r--r--www/wiki/tests/phpunit/includes/specialpage/SpecialPageTestHelper.php24
-rw-r--r--www/wiki/tests/phpunit/includes/specials/ContribsPagerTest.php118
-rw-r--r--www/wiki/tests/phpunit/includes/specials/ImageListPagerTest.php21
-rw-r--r--www/wiki/tests/phpunit/includes/specials/QueryAllSpecialPagesTest.php80
-rw-r--r--www/wiki/tests/phpunit/includes/specials/SpecialBlankPageTest.php25
-rw-r--r--www/wiki/tests/phpunit/includes/specials/SpecialBooksourcesTest.php51
-rw-r--r--www/wiki/tests/phpunit/includes/specials/SpecialEditWatchlistTest.php50
-rw-r--r--www/wiki/tests/phpunit/includes/specials/SpecialMIMESearchTest.php49
-rw-r--r--www/wiki/tests/phpunit/includes/specials/SpecialMyLanguageTest.php78
-rw-r--r--www/wiki/tests/phpunit/includes/specials/SpecialPageDataTest.php146
-rw-r--r--www/wiki/tests/phpunit/includes/specials/SpecialPageExecutor.php129
-rw-r--r--www/wiki/tests/phpunit/includes/specials/SpecialPageTestBase.php72
-rw-r--r--www/wiki/tests/phpunit/includes/specials/SpecialPreferencesTest.php58
-rw-r--r--www/wiki/tests/phpunit/includes/specials/SpecialRecentchangesTest.php52
-rw-r--r--www/wiki/tests/phpunit/includes/specials/SpecialSearchTest.php296
-rw-r--r--www/wiki/tests/phpunit/includes/specials/SpecialShortpagesTest.php43
-rw-r--r--www/wiki/tests/phpunit/includes/specials/SpecialUncategorizedcategoriesTest.php63
-rw-r--r--www/wiki/tests/phpunit/includes/specials/SpecialUploadTest.php29
-rw-r--r--www/wiki/tests/phpunit/includes/specials/SpecialWatchlistTest.php192
-rw-r--r--www/wiki/tests/phpunit/includes/tidy/BalancerTest.php169
-rw-r--r--www/wiki/tests/phpunit/includes/tidy/RemexDriverTest.php307
-rw-r--r--www/wiki/tests/phpunit/includes/tidy/html5lib-tests.json80692
-rw-r--r--www/wiki/tests/phpunit/includes/title/ForeignTitleTest.php103
-rw-r--r--www/wiki/tests/phpunit/includes/title/MediaWikiTitleCodecTest.php415
-rw-r--r--www/wiki/tests/phpunit/includes/title/NaiveForeignTitleFactoryTest.php91
-rw-r--r--www/wiki/tests/phpunit/includes/title/NaiveImportTitleFactoryTest.php90
-rw-r--r--www/wiki/tests/phpunit/includes/title/NamespaceAwareForeignTitleFactoryTest.php101
-rw-r--r--www/wiki/tests/phpunit/includes/title/NamespaceImportTitleFactoryTest.php78
-rw-r--r--www/wiki/tests/phpunit/includes/title/SubpageImportTitleFactoryTest.php86
-rw-r--r--www/wiki/tests/phpunit/includes/title/TitleValueTest.php148
-rw-r--r--www/wiki/tests/phpunit/includes/upload/UploadBaseTest.php614
-rw-r--r--www/wiki/tests/phpunit/includes/upload/UploadFromUrlTest.php151
-rw-r--r--www/wiki/tests/phpunit/includes/upload/UploadStashTest.php113
-rw-r--r--www/wiki/tests/phpunit/includes/user/BotPasswordTest.php420
-rw-r--r--www/wiki/tests/phpunit/includes/user/CentralIdLookupTest.php183
-rw-r--r--www/wiki/tests/phpunit/includes/user/ExternalUserNamesTest.php131
-rw-r--r--www/wiki/tests/phpunit/includes/user/LocalIdLookupTest.php156
-rw-r--r--www/wiki/tests/phpunit/includes/user/PasswordResetTest.php193
-rw-r--r--www/wiki/tests/phpunit/includes/user/UserArrayFromResultTest.php114
-rw-r--r--www/wiki/tests/phpunit/includes/user/UserGroupMembershipTest.php153
-rw-r--r--www/wiki/tests/phpunit/includes/user/UserTest.php1208
-rw-r--r--www/wiki/tests/phpunit/includes/utils/AvroValidatorTest.php118
-rw-r--r--www/wiki/tests/phpunit/includes/utils/BatchRowUpdateTest.php252
-rw-r--r--www/wiki/tests/phpunit/includes/utils/ClassCollectorTest.php56
-rw-r--r--www/wiki/tests/phpunit/includes/utils/FileContentsHasherTest.php57
-rw-r--r--www/wiki/tests/phpunit/includes/utils/MWCryptHKDFTest.php98
-rw-r--r--www/wiki/tests/phpunit/includes/utils/MWCryptHashTest.php64
-rw-r--r--www/wiki/tests/phpunit/includes/utils/MWGrantsTest.php117
-rw-r--r--www/wiki/tests/phpunit/includes/utils/MWRestrictionsTest.php217
-rw-r--r--www/wiki/tests/phpunit/includes/utils/UIDGeneratorTest.php173
-rw-r--r--www/wiki/tests/phpunit/includes/utils/ZipDirectoryReaderTest.php87
-rw-r--r--www/wiki/tests/phpunit/includes/watcheditem/NoWriteWatchedItemStoreUnitTest.php246
-rw-r--r--www/wiki/tests/phpunit/includes/watcheditem/WatchedItemQueryServiceUnitTest.php1706
-rw-r--r--www/wiki/tests/phpunit/includes/watcheditem/WatchedItemStoreIntegrationTest.php231
-rw-r--r--www/wiki/tests/phpunit/includes/watcheditem/WatchedItemStoreUnitTest.php2753
-rw-r--r--www/wiki/tests/phpunit/languages/LanguageClassesTestCase.php74
-rw-r--r--www/wiki/tests/phpunit/languages/LanguageCodeTest.php161
-rw-r--r--www/wiki/tests/phpunit/languages/LanguageConverterTest.php207
-rw-r--r--www/wiki/tests/phpunit/languages/LanguageTest.php1869
-rw-r--r--www/wiki/tests/phpunit/languages/SpecialPageAliasTest.php62
-rw-r--r--www/wiki/tests/phpunit/languages/classes/LanguageAmTest.php35
-rw-r--r--www/wiki/tests/phpunit/languages/classes/LanguageArTest.php89
-rw-r--r--www/wiki/tests/phpunit/languages/classes/LanguageArqTest.php26
-rw-r--r--www/wiki/tests/phpunit/languages/classes/LanguageBeTest.php42
-rw-r--r--www/wiki/tests/phpunit/languages/classes/LanguageBe_taraskTest.php100
-rw-r--r--www/wiki/tests/phpunit/languages/classes/LanguageBhoTest.php35
-rw-r--r--www/wiki/tests/phpunit/languages/classes/LanguageBsTest.php46
-rw-r--r--www/wiki/tests/phpunit/languages/classes/LanguageCrhTest.php92
-rw-r--r--www/wiki/tests/phpunit/languages/classes/LanguageCsTest.php41
-rw-r--r--www/wiki/tests/phpunit/languages/classes/LanguageCuTest.php44
-rw-r--r--www/wiki/tests/phpunit/languages/classes/LanguageCyTest.php43
-rw-r--r--www/wiki/tests/phpunit/languages/classes/LanguageDsbTest.php43
-rw-r--r--www/wiki/tests/phpunit/languages/classes/LanguageFrTest.php35
-rw-r--r--www/wiki/tests/phpunit/languages/classes/LanguageGaTest.php35
-rw-r--r--www/wiki/tests/phpunit/languages/classes/LanguageGanTest.php38
-rw-r--r--www/wiki/tests/phpunit/languages/classes/LanguageGdTest.php53
-rw-r--r--www/wiki/tests/phpunit/languages/classes/LanguageGvTest.php44
-rw-r--r--www/wiki/tests/phpunit/languages/classes/LanguageHeTest.php132
-rw-r--r--www/wiki/tests/phpunit/languages/classes/LanguageHiTest.php35
-rw-r--r--www/wiki/tests/phpunit/languages/classes/LanguageHrTest.php42
-rw-r--r--www/wiki/tests/phpunit/languages/classes/LanguageHsbTest.php43
-rw-r--r--www/wiki/tests/phpunit/languages/classes/LanguageHuTest.php37
-rw-r--r--www/wiki/tests/phpunit/languages/classes/LanguageHyTest.php39
-rw-r--r--www/wiki/tests/phpunit/languages/classes/LanguageIuTest.php38
-rw-r--r--www/wiki/tests/phpunit/languages/classes/LanguageKkTest.php33
-rw-r--r--www/wiki/tests/phpunit/languages/classes/LanguageKshTest.php37
-rw-r--r--www/wiki/tests/phpunit/languages/classes/LanguageKuTest.php44
-rw-r--r--www/wiki/tests/phpunit/languages/classes/LanguageLnTest.php35
-rw-r--r--www/wiki/tests/phpunit/languages/classes/LanguageLtTest.php63
-rw-r--r--www/wiki/tests/phpunit/languages/classes/LanguageLvTest.php44
-rw-r--r--www/wiki/tests/phpunit/languages/classes/LanguageMgTest.php36
-rw-r--r--www/wiki/tests/phpunit/languages/classes/LanguageMkTest.php40
-rw-r--r--www/wiki/tests/phpunit/languages/classes/LanguageMlTest.php40
-rw-r--r--www/wiki/tests/phpunit/languages/classes/LanguageMoTest.php45
-rw-r--r--www/wiki/tests/phpunit/languages/classes/LanguageMtTest.php77
-rw-r--r--www/wiki/tests/phpunit/languages/classes/LanguageNlTest.php24
-rw-r--r--www/wiki/tests/phpunit/languages/classes/LanguageNsoTest.php34
-rw-r--r--www/wiki/tests/phpunit/languages/classes/LanguagePlTest.php104
-rw-r--r--www/wiki/tests/phpunit/languages/classes/LanguageRoTest.php45
-rw-r--r--www/wiki/tests/phpunit/languages/classes/LanguageRuTest.php189
-rw-r--r--www/wiki/tests/phpunit/languages/classes/LanguageSeTest.php53
-rw-r--r--www/wiki/tests/phpunit/languages/classes/LanguageSgsTest.php71
-rw-r--r--www/wiki/tests/phpunit/languages/classes/LanguageShTest.php42
-rw-r--r--www/wiki/tests/phpunit/languages/classes/LanguageShiTest.php36
-rw-r--r--www/wiki/tests/phpunit/languages/classes/LanguageSkTest.php42
-rw-r--r--www/wiki/tests/phpunit/languages/classes/LanguageSlTest.php46
-rw-r--r--www/wiki/tests/phpunit/languages/classes/LanguageSmaTest.php53
-rw-r--r--www/wiki/tests/phpunit/languages/classes/LanguageSrTest.php252
-rw-r--r--www/wiki/tests/phpunit/languages/classes/LanguageTgTest.php34
-rw-r--r--www/wiki/tests/phpunit/languages/classes/LanguageTiTest.php34
-rw-r--r--www/wiki/tests/phpunit/languages/classes/LanguageTlTest.php36
-rw-r--r--www/wiki/tests/phpunit/languages/classes/LanguageTrTest.php63
-rw-r--r--www/wiki/tests/phpunit/languages/classes/LanguageUkTest.php109
-rw-r--r--www/wiki/tests/phpunit/languages/classes/LanguageUzTest.php127
-rw-r--r--www/wiki/tests/phpunit/languages/classes/LanguageWaTest.php36
-rw-r--r--www/wiki/tests/phpunit/languages/classes/LanguageZhTest.php126
-rw-r--r--www/wiki/tests/phpunit/maintenance/BenchmarkerTest.php142
-rw-r--r--www/wiki/tests/phpunit/maintenance/DumpTestCase.php417
-rw-r--r--www/wiki/tests/phpunit/maintenance/MaintenanceBaseTestCase.php93
-rw-r--r--www/wiki/tests/phpunit/maintenance/MaintenanceTest.php536
-rw-r--r--www/wiki/tests/phpunit/maintenance/backupPrefetchTest.php279
-rw-r--r--www/wiki/tests/phpunit/maintenance/backupTextPassTest.php701
-rw-r--r--www/wiki/tests/phpunit/maintenance/backup_LogTest.php241
-rw-r--r--www/wiki/tests/phpunit/maintenance/backup_PageTest.php443
-rw-r--r--www/wiki/tests/phpunit/maintenance/categoriesRdfTest.php102
-rw-r--r--www/wiki/tests/phpunit/maintenance/deleteAutoPatrolLogsTest.php252
-rw-r--r--www/wiki/tests/phpunit/maintenance/fetchTextTest.php272
-rw-r--r--www/wiki/tests/phpunit/mocks/MockChangesListFilter.php15
-rw-r--r--www/wiki/tests/phpunit/mocks/MockChangesListFilterGroup.php21
-rw-r--r--www/wiki/tests/phpunit/mocks/MockMessageLocalizer.php49
-rw-r--r--www/wiki/tests/phpunit/mocks/MockWebRequest.php25
-rw-r--r--www/wiki/tests/phpunit/mocks/content/DummyContentForTesting.php123
-rw-r--r--www/wiki/tests/phpunit/mocks/content/DummyContentHandlerForTesting.php42
-rw-r--r--www/wiki/tests/phpunit/mocks/content/DummyNonTextContent.php121
-rw-r--r--www/wiki/tests/phpunit/mocks/content/DummyNonTextContentHandler.php47
-rw-r--r--www/wiki/tests/phpunit/mocks/content/DummySerializeErrorContentHandler.php51
-rw-r--r--www/wiki/tests/phpunit/mocks/filebackend/MockFSFile.php66
-rw-r--r--www/wiki/tests/phpunit/mocks/filebackend/MockFileBackend.php39
-rw-r--r--www/wiki/tests/phpunit/mocks/filerepo/MockLocalRepo.php23
-rw-r--r--www/wiki/tests/phpunit/mocks/media/MockBitmapHandler.php32
-rw-r--r--www/wiki/tests/phpunit/mocks/media/MockDjVuHandler.php53
-rw-r--r--www/wiki/tests/phpunit/mocks/media/MockImageHandler.php86
-rw-r--r--www/wiki/tests/phpunit/mocks/media/MockSvgHandler.php28
-rw-r--r--www/wiki/tests/phpunit/mocks/session/DummySessionBackend.php29
-rw-r--r--www/wiki/tests/phpunit/mocks/session/DummySessionProvider.php60
-rwxr-xr-xwww/wiki/tests/phpunit/phpunit.php173
-rw-r--r--www/wiki/tests/phpunit/run-tests.bat1
-rw-r--r--www/wiki/tests/phpunit/skins/SideBarTest.php221
-rw-r--r--www/wiki/tests/phpunit/structure/ApiStructureTest.php612
-rw-r--r--www/wiki/tests/phpunit/structure/AutoLoaderTest.php171
-rw-r--r--www/wiki/tests/phpunit/structure/AvailableRightsTest.php53
-rw-r--r--www/wiki/tests/phpunit/structure/ContentHandlerSanityTest.php59
-rw-r--r--www/wiki/tests/phpunit/structure/DatabaseIntegrationTest.php56
-rw-r--r--www/wiki/tests/phpunit/structure/ExtensionJsonValidationTest.php67
-rw-r--r--www/wiki/tests/phpunit/structure/ResourcesTest.php349
-rw-r--r--www/wiki/tests/phpunit/structure/StructureTest.php69
-rw-r--r--www/wiki/tests/phpunit/suite.xml77
-rw-r--r--www/wiki/tests/phpunit/suites/CoreParserTestSuite.php9
-rw-r--r--www/wiki/tests/phpunit/suites/ExtensionsParserTestSuite.php8
-rw-r--r--www/wiki/tests/phpunit/suites/ExtensionsTestSuite.php51
-rw-r--r--www/wiki/tests/phpunit/suites/LessTestSuite.php34
-rw-r--r--www/wiki/tests/phpunit/suites/ParserTestFileSuite.php32
-rw-r--r--www/wiki/tests/phpunit/suites/ParserTestTopLevelSuite.php160
-rw-r--r--www/wiki/tests/phpunit/suites/UploadFromUrlTestSuite.php99
-rw-r--r--www/wiki/tests/phpunit/tests/MediaWikiTestCaseSchema1Test.php51
-rw-r--r--www/wiki/tests/phpunit/tests/MediaWikiTestCaseSchema2Test.php48
-rw-r--r--www/wiki/tests/phpunit/tests/MediaWikiTestCaseSchemaTest.sql18
-rw-r--r--www/wiki/tests/phpunit/tests/MediaWikiTestCaseTest.php184
-rw-r--r--www/wiki/tests/qunit/.htaccess1
-rw-r--r--www/wiki/tests/qunit/QUnitTestResources.php149
-rw-r--r--www/wiki/tests/qunit/data/defineCallMwLoaderTestCallback.js1
-rw-r--r--www/wiki/tests/qunit/data/generateJqueryMsgData.php148
-rw-r--r--www/wiki/tests/qunit/data/load.mock.php107
-rw-r--r--www/wiki/tests/qunit/data/mediawiki.jqueryMsg.data.js492
-rw-r--r--www/wiki/tests/qunit/data/mwLoaderTestCallback.js1
-rw-r--r--www/wiki/tests/qunit/data/requireCallMwLoaderTestCallback.js6
-rw-r--r--www/wiki/tests/qunit/data/styleTest.css.php61
-rw-r--r--www/wiki/tests/qunit/data/testrunner.js652
-rw-r--r--www/wiki/tests/qunit/suites/resources/jquery/jquery.accessKeyLabel.test.js121
-rw-r--r--www/wiki/tests/qunit/suites/resources/jquery/jquery.color.test.js15
-rw-r--r--www/wiki/tests/qunit/suites/resources/jquery/jquery.colorUtil.test.js63
-rw-r--r--www/wiki/tests/qunit/suites/resources/jquery/jquery.getAttrs.test.js14
-rw-r--r--www/wiki/tests/qunit/suites/resources/jquery/jquery.hidpi.test.js38
-rw-r--r--www/wiki/tests/qunit/suites/resources/jquery/jquery.highlightText.test.js235
-rw-r--r--www/wiki/tests/qunit/suites/resources/jquery/jquery.lengthLimit.test.js286
-rw-r--r--www/wiki/tests/qunit/suites/resources/jquery/jquery.localize.test.js135
-rw-r--r--www/wiki/tests/qunit/suites/resources/jquery/jquery.makeCollapsible.test.js377
-rw-r--r--www/wiki/tests/qunit/suites/resources/jquery/jquery.tabIndex.test.js35
-rw-r--r--www/wiki/tests/qunit/suites/resources/jquery/jquery.tablesorter.parsers.test.js267
-rw-r--r--www/wiki/tests/qunit/suites/resources/jquery/jquery.tablesorter.test.js1498
-rw-r--r--www/wiki/tests/qunit/suites/resources/jquery/jquery.textSelection.test.js266
-rw-r--r--www/wiki/tests/qunit/suites/resources/mediawiki.api/mediawiki.ForeignApi.test.js32
-rw-r--r--www/wiki/tests/qunit/suites/resources/mediawiki.api/mediawiki.api.category.test.js114
-rw-r--r--www/wiki/tests/qunit/suites/resources/mediawiki.api/mediawiki.api.edit.test.js220
-rw-r--r--www/wiki/tests/qunit/suites/resources/mediawiki.api/mediawiki.api.messages.test.js29
-rw-r--r--www/wiki/tests/qunit/suites/resources/mediawiki.api/mediawiki.api.options.test.js141
-rw-r--r--www/wiki/tests/qunit/suites/resources/mediawiki.api/mediawiki.api.parse.test.js45
-rw-r--r--www/wiki/tests/qunit/suites/resources/mediawiki.api/mediawiki.api.test.js456
-rw-r--r--www/wiki/tests/qunit/suites/resources/mediawiki.api/mediawiki.api.upload.test.js33
-rw-r--r--www/wiki/tests/qunit/suites/resources/mediawiki.api/mediawiki.api.watch.test.js60
-rw-r--r--www/wiki/tests/qunit/suites/resources/mediawiki.rcfilters/UriProcessor.test.js354
-rw-r--r--www/wiki/tests/qunit/suites/resources/mediawiki.rcfilters/dm.FilterItem.test.js205
-rw-r--r--www/wiki/tests/qunit/suites/resources/mediawiki.rcfilters/dm.FiltersViewModel.test.js1562
-rw-r--r--www/wiki/tests/qunit/suites/resources/mediawiki.rcfilters/dm.SavedQueriesModel.test.js520
-rw-r--r--www/wiki/tests/qunit/suites/resources/mediawiki.rcfilters/dm.SavedQueryItemModel.test.js89
-rw-r--r--www/wiki/tests/qunit/suites/resources/mediawiki.special/mediawiki.special.recentchanges.test.js64
-rw-r--r--www/wiki/tests/qunit/suites/resources/mediawiki/mediawiki.RegExp.test.js38
-rw-r--r--www/wiki/tests/qunit/suites/resources/mediawiki/mediawiki.String.byteLength.test.js39
-rw-r--r--www/wiki/tests/qunit/suites/resources/mediawiki/mediawiki.String.trimByteLength.test.js150
-rw-r--r--www/wiki/tests/qunit/suites/resources/mediawiki/mediawiki.Title.test.js738
-rw-r--r--www/wiki/tests/qunit/suites/resources/mediawiki/mediawiki.Uri.test.js509
-rw-r--r--www/wiki/tests/qunit/suites/resources/mediawiki/mediawiki.cldr.test.js82
-rw-r--r--www/wiki/tests/qunit/suites/resources/mediawiki/mediawiki.cookie.test.js179
-rw-r--r--www/wiki/tests/qunit/suites/resources/mediawiki/mediawiki.errorLogger.test.js42
-rw-r--r--www/wiki/tests/qunit/suites/resources/mediawiki/mediawiki.experiments.test.js63
-rw-r--r--www/wiki/tests/qunit/suites/resources/mediawiki/mediawiki.html.test.js105
-rw-r--r--www/wiki/tests/qunit/suites/resources/mediawiki/mediawiki.inspect.test.js74
-rw-r--r--www/wiki/tests/qunit/suites/resources/mediawiki/mediawiki.jqueryMsg.test.js1233
-rw-r--r--www/wiki/tests/qunit/suites/resources/mediawiki/mediawiki.jscompat.test.js69
-rw-r--r--www/wiki/tests/qunit/suites/resources/mediawiki/mediawiki.language.test.js708
-rw-r--r--www/wiki/tests/qunit/suites/resources/mediawiki/mediawiki.loader.test.js980
-rw-r--r--www/wiki/tests/qunit/suites/resources/mediawiki/mediawiki.messagePoster.factory.test.js28
-rw-r--r--www/wiki/tests/qunit/suites/resources/mediawiki/mediawiki.requestIdleCallback.test.js108
-rw-r--r--www/wiki/tests/qunit/suites/resources/mediawiki/mediawiki.storage.test.js56
-rw-r--r--www/wiki/tests/qunit/suites/resources/mediawiki/mediawiki.template.mustache.test.js32
-rw-r--r--www/wiki/tests/qunit/suites/resources/mediawiki/mediawiki.template.test.js63
-rw-r--r--www/wiki/tests/qunit/suites/resources/mediawiki/mediawiki.test.js447
-rw-r--r--www/wiki/tests/qunit/suites/resources/mediawiki/mediawiki.toc.test.js39
-rw-r--r--www/wiki/tests/qunit/suites/resources/mediawiki/mediawiki.track.test.js60
-rw-r--r--www/wiki/tests/qunit/suites/resources/mediawiki/mediawiki.user.test.js115
-rw-r--r--www/wiki/tests/qunit/suites/resources/mediawiki/mediawiki.util.test.js465
-rw-r--r--www/wiki/tests/qunit/suites/resources/mediawiki/mediawiki.viewport.test.js112
-rw-r--r--www/wiki/tests/qunit/suites/resources/mediawiki/mediawiki.visibleTimeout.test.js115
-rw-r--r--www/wiki/tests/qunit/suites/resources/startup.test.js160
-rw-r--r--www/wiki/tests/qunit/suites/resources/test.sinonjs/index.js3
-rw-r--r--www/wiki/tests/selenium/README.md61
-rw-r--r--www/wiki/tests/selenium/pageobjects/createaccount.page.js49
-rw-r--r--www/wiki/tests/selenium/pageobjects/delete.page.js39
-rw-r--r--www/wiki/tests/selenium/pageobjects/edit.page.js39
-rw-r--r--www/wiki/tests/selenium/pageobjects/history.page.js13
-rw-r--r--www/wiki/tests/selenium/pageobjects/page.js8
-rw-r--r--www/wiki/tests/selenium/pageobjects/preferences.page.js20
-rw-r--r--www/wiki/tests/selenium/pageobjects/restore.page.js21
-rw-r--r--www/wiki/tests/selenium/pageobjects/userlogin.page.js27
-rwxr-xr-xwww/wiki/tests/selenium/selenium.sh9
-rw-r--r--www/wiki/tests/selenium/specs/page.js136
-rw-r--r--www/wiki/tests/selenium/specs/user.js69
-rw-r--r--www/wiki/tests/selenium/wdio.conf.js328
998 files changed, 301794 insertions, 0 deletions
diff --git a/www/wiki/tests/.htaccess b/www/wiki/tests/.htaccess
new file mode 100644
index 00000000..3a428827
--- /dev/null
+++ b/www/wiki/tests/.htaccess
@@ -0,0 +1 @@
+Deny from all
diff --git a/www/wiki/tests/common/TestSetup.php b/www/wiki/tests/common/TestSetup.php
new file mode 100644
index 00000000..c176a67f
--- /dev/null
+++ b/www/wiki/tests/common/TestSetup.php
@@ -0,0 +1,113 @@
+<?php
+
+/**
+ * Common code for test environment initialisation and teardown
+ */
+class TestSetup {
+ /**
+ * This should be called before Setup.php, e.g. from the finalSetup() method
+ * of a Maintenance subclass
+ */
+ public static function applyInitialConfig() {
+ global $wgMainCacheType, $wgMessageCacheType, $wgParserCacheType, $wgMainWANCache;
+ global $wgMainStash;
+ global $wgLanguageConverterCacheType, $wgUseDatabaseMessages;
+ global $wgLocaltimezone, $wgLocalisationCacheConf;
+ global $wgSearchType;
+ global $wgDevelopmentWarnings;
+ global $wgSessionProviders, $wgSessionPbkdf2Iterations;
+ global $wgJobTypeConf;
+ global $wgAuthManagerConfig, $wgAuth;
+
+ // wfWarn should cause tests to fail
+ $wgDevelopmentWarnings = true;
+
+ // Make sure all caches and stashes are either disabled or use
+ // in-process cache only to prevent tests from using any preconfigured
+ // cache meant for the local wiki from outside the test run.
+ // See also MediaWikiTestCase::run() which mocks CACHE_DB and APC.
+
+ // Disabled in DefaultSettings, override local settings
+ $wgMainWANCache =
+ $wgMainCacheType = CACHE_NONE;
+ // Uses CACHE_ANYTHING in DefaultSettings, use hash instead of db
+ $wgMessageCacheType =
+ $wgParserCacheType =
+ $wgSessionCacheType =
+ $wgLanguageConverterCacheType = 'hash';
+ // Uses db-replicated in DefaultSettings
+ $wgMainStash = 'hash';
+ // Use memory job queue
+ $wgJobTypeConf = [
+ 'default' => [ 'class' => JobQueueMemory::class, 'order' => 'fifo' ],
+ ];
+
+ $wgUseDatabaseMessages = false; # Set for future resets
+
+ // Assume UTC for testing purposes
+ $wgLocaltimezone = 'UTC';
+
+ $wgLocalisationCacheConf['storeClass'] = LCStoreNull::class;
+
+ // Do not bother updating search tables
+ $wgSearchType = SearchEngineDummy::class;
+
+ // Generic MediaWiki\Session\SessionManager configuration for tests
+ // We use CookieSessionProvider because things might be expecting
+ // cookies to show up in a FauxRequest somewhere.
+ $wgSessionProviders = [
+ [
+ 'class' => MediaWiki\Session\CookieSessionProvider::class,
+ 'args' => [ [
+ 'priority' => 30,
+ 'callUserSetCookiesHook' => true,
+ ] ],
+ ],
+ ];
+
+ // Single-iteration PBKDF2 session secret derivation, for speed.
+ $wgSessionPbkdf2Iterations = 1;
+
+ // Generic AuthManager configuration for testing
+ $wgAuthManagerConfig = [
+ 'preauth' => [],
+ 'primaryauth' => [
+ [
+ 'class' => MediaWiki\Auth\TemporaryPasswordPrimaryAuthenticationProvider::class,
+ 'args' => [ [
+ 'authoritative' => false,
+ ] ],
+ ],
+ [
+ 'class' => MediaWiki\Auth\LocalPasswordPrimaryAuthenticationProvider::class,
+ 'args' => [ [
+ 'authoritative' => true,
+ ] ],
+ ],
+ ],
+ 'secondaryauth' => [],
+ ];
+ $wgAuth = new MediaWiki\Auth\AuthManagerAuthPlugin();
+
+ // T46192 Do not attempt to send a real e-mail
+ Hooks::clear( 'AlternateUserMailer' );
+ Hooks::register(
+ 'AlternateUserMailer',
+ function () {
+ return false;
+ }
+ );
+ // xdebug's default of 100 is too low for MediaWiki
+ ini_set( 'xdebug.max_nesting_level', 1000 );
+
+ // Bug T116683 serialize_precision of 100
+ // may break testing against floating point values
+ // treated with PHP's serialize()
+ ini_set( 'serialize_precision', 17 );
+
+ // TODO: we should call MediaWikiTestCase::prepareServices( new GlobalVarConfig() ) here.
+ // But PHPUnit may not be loaded yet, so we have to wait until just
+ // before PHPUnit_TextUI_Command::main() is executed.
+ }
+
+}
diff --git a/www/wiki/tests/common/TestsAutoLoader.php b/www/wiki/tests/common/TestsAutoLoader.php
new file mode 100644
index 00000000..abf718d0
--- /dev/null
+++ b/www/wiki/tests/common/TestsAutoLoader.php
@@ -0,0 +1,222 @@
+<?php
+/**
+ * AutoLoader for the testing suite.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ * @ingroup Testing
+ */
+
+global $wgAutoloadClasses;
+$testDir = __DIR__ . "/..";
+
+// phpcs:disable Generic.Files.LineLength
+$wgAutoloadClasses += [
+
+ # tests/common
+ 'TestSetup' => "$testDir/common/TestSetup.php",
+
+ # tests/integration
+ 'MWHttpRequestTestCase' => "$testDir/integration/includes/http/MWHttpRequestTestCase.php",
+
+ # tests/parser
+ 'DbTestPreviewer' => "$testDir/parser/DbTestPreviewer.php",
+ 'DbTestRecorder' => "$testDir/parser/DbTestRecorder.php",
+ 'DjVuSupport' => "$testDir/parser/DjVuSupport.php",
+ 'MultiTestRecorder' => "$testDir/parser/MultiTestRecorder.php",
+ 'ParserTestMockParser' => "$testDir/parser/ParserTestMockParser.php",
+ 'ParserTestRunner' => "$testDir/parser/ParserTestRunner.php",
+ 'ParserTestParserHook' => "$testDir/parser/ParserTestParserHook.php",
+ 'ParserTestPrinter' => "$testDir/parser/ParserTestPrinter.php",
+ 'ParserTestResult' => "$testDir/parser/ParserTestResult.php",
+ 'ParserTestResultNormalizer' => "$testDir/parser/ParserTestResultNormalizer.php",
+ 'PhpunitTestRecorder' => "$testDir/parser/PhpunitTestRecorder.php",
+ 'TestFileEditor' => "$testDir/parser/TestFileEditor.php",
+ 'TestFileReader' => "$testDir/parser/TestFileReader.php",
+ 'TestRecorder' => "$testDir/parser/TestRecorder.php",
+ 'TidySupport' => "$testDir/parser/TidySupport.php",
+
+ # tests/phpunit
+ 'MediaWikiTestCase' => "$testDir/phpunit/MediaWikiTestCase.php",
+ 'MediaWikiPHPUnitTestListener' => "$testDir/phpunit/MediaWikiPHPUnitTestListener.php",
+ 'MediaWikiLangTestCase' => "$testDir/phpunit/MediaWikiLangTestCase.php",
+ 'ResourceLoaderTestCase' => "$testDir/phpunit/ResourceLoaderTestCase.php",
+ 'ResourceLoaderTestModule' => "$testDir/phpunit/ResourceLoaderTestCase.php",
+ 'ResourceLoaderFileTestModule' => "$testDir/phpunit/ResourceLoaderTestCase.php",
+ 'ResourceLoaderFileModuleTestModule' => "$testDir/phpunit/ResourceLoaderTestCase.php",
+ 'EmptyResourceLoader' => "$testDir/phpunit/ResourceLoaderTestCase.php",
+ 'TestUser' => "$testDir/phpunit/includes/TestUser.php",
+ 'TestUserRegistry' => "$testDir/phpunit/includes/TestUserRegistry.php",
+ 'LessFileCompilationTest' => "$testDir/phpunit/LessFileCompilationTest.php",
+ 'MediaWikiCoversValidator' => "$testDir/phpunit/MediaWikiCoversValidator.php",
+ 'PHPUnit4And6Compat' => "$testDir/phpunit/PHPUnit4And6Compat.php",
+ 'HamcrestPHPUnitIntegration' => "$testDir/phpunit/HamcrestPHPUnitIntegration.php",
+
+ # tests/phpunit/includes
+ 'RevisionDbTestBase' => "$testDir/phpunit/includes/RevisionDbTestBase.php",
+ 'RevisionTestModifyableContent' => "$testDir/phpunit/includes/RevisionTestModifyableContent.php",
+ 'RevisionTestModifyableContentHandler' => "$testDir/phpunit/includes/RevisionTestModifyableContentHandler.php",
+ 'TestLogger' => "$testDir/phpunit/includes/TestLogger.php",
+
+ # tests/phpunit/includes/api
+ 'ApiFormatTestBase' => "$testDir/phpunit/includes/api/format/ApiFormatTestBase.php",
+ 'ApiQueryTestBase' => "$testDir/phpunit/includes/api/query/ApiQueryTestBase.php",
+ 'ApiQueryContinueTestBase' => "$testDir/phpunit/includes/api/query/ApiQueryContinueTestBase.php",
+ 'ApiTestCase' => "$testDir/phpunit/includes/api/ApiTestCase.php",
+ 'ApiTestCaseUpload' => "$testDir/phpunit/includes/api/ApiTestCaseUpload.php",
+ 'ApiTestContext' => "$testDir/phpunit/includes/api/ApiTestContext.php",
+ 'ApiUploadTestCase' => "$testDir/phpunit/includes/api/ApiUploadTestCase.php",
+ 'MockApi' => "$testDir/phpunit/includes/api/MockApi.php",
+ 'MockApiQueryBase' => "$testDir/phpunit/includes/api/MockApiQueryBase.php",
+ 'UserWrapper' => "$testDir/phpunit/includes/api/UserWrapper.php",
+ 'RandomImageGenerator' => "$testDir/phpunit/includes/api/RandomImageGenerator.php",
+
+ # tests/phpunit/includes/auth
+ 'MediaWiki\\Auth\\AuthenticationRequestTestCase' =>
+ "$testDir/phpunit/includes/auth/AuthenticationRequestTestCase.php",
+
+ # tests/phpunit/includes/changes
+ 'TestRecentChangesHelper' => "$testDir/phpunit/includes/changes/TestRecentChangesHelper.php",
+
+ # tests/phpunit/includes/content
+ 'DummyContentHandlerForTesting' =>
+ "$testDir/phpunit/mocks/content/DummyContentHandlerForTesting.php",
+ 'DummyContentForTesting' => "$testDir/phpunit/mocks/content/DummyContentForTesting.php",
+ 'DummyNonTextContentHandler' => "$testDir/phpunit/mocks/content/DummyNonTextContentHandler.php",
+ 'DummyNonTextContent' => "$testDir/phpunit/mocks/content/DummyNonTextContent.php",
+ 'DummySerializeErrorContentHandler' =>
+ "$testDir/phpunit/mocks/content/DummySerializeErrorContentHandler.php",
+ 'ContentHandlerTest' => "$testDir/phpunit/includes/content/ContentHandlerTest.php",
+ 'JavaScriptContentTest' => "$testDir/phpunit/includes/content/JavaScriptContentTest.php",
+ 'TextContentTest' => "$testDir/phpunit/includes/content/TextContentTest.php",
+ 'WikitextContentTest' => "$testDir/phpunit/includes/content/WikitextContentTest.php",
+
+ # tests/phpunit/includes/db
+ 'DatabaseTestHelper' => "$testDir/phpunit/includes/db/DatabaseTestHelper.php",
+
+ # tests/phpunit/includes/diff
+ 'FakeDiffOp' => "$testDir/phpunit/includes/diff/FakeDiffOp.php",
+
+ # tests/phpunit/includes/externalstore
+ 'ExternalStoreForTesting' => "$testDir/phpunit/includes/externalstore/ExternalStoreForTesting.php",
+
+ # tests/phpunit/includes/logging
+ 'LogFormatterTestCase' => "$testDir/phpunit/includes/logging/LogFormatterTestCase.php",
+
+ # tests/phpunit/includes/page
+ 'WikiPageDbTestBase' => "$testDir/phpunit/includes/page/WikiPageDbTestBase.php",
+
+ # tests/phpunit/includes/parser
+ 'ParserIntegrationTest' => "$testDir/phpunit/includes/parser/ParserIntegrationTest.php",
+
+ # tests/phpunit/includes/password
+ 'PasswordTestCase' => "$testDir/phpunit/includes/password/PasswordTestCase.php",
+
+ # tests/phpunit/includes/resourceloader
+ 'ResourceLoaderImageModuleTest' =>
+ "$testDir/phpunit/includes/resourceloader/ResourceLoaderImageModuleTest.php",
+ 'ResourceLoaderImageModuleTestable' =>
+ "$testDir/phpunit/includes/resourceloader/ResourceLoaderImageModuleTest.php",
+
+ # tests/phpunit/includes/session
+ 'MediaWiki\\Session\\TestBagOStuff' => "$testDir/phpunit/includes/session/TestBagOStuff.php",
+ 'MediaWiki\\Session\\TestUtils' => "$testDir/phpunit/includes/session/TestUtils.php",
+
+ # tests/phpunit/includes/site
+ 'SiteTest' => "$testDir/phpunit/includes/site/SiteTest.php",
+ 'TestSites' => "$testDir/phpunit/includes/site/TestSites.php",
+
+ # tests/phpunit/includes/specialpage
+ 'SpecialPageTestHelper' => "$testDir/phpunit/includes/specialpage/SpecialPageTestHelper.php",
+ 'AbstractChangesListSpecialPageTestCase' => "$testDir/phpunit/includes/specialpage/AbstractChangesListSpecialPageTestCase.php",
+
+ # tests/phpunit/includes/specials
+ 'SpecialPageTestBase' => "$testDir/phpunit/includes/specials/SpecialPageTestBase.php",
+ 'SpecialPageExecutor' => "$testDir/phpunit/includes/specials/SpecialPageExecutor.php",
+
+ # tests/phpunit/includes/Storage
+ 'MediaWiki\Tests\Storage\RevisionSlotsTest' => "$testDir/phpunit/includes/Storage/RevisionSlotsTest.php",
+ 'MediaWiki\Tests\Storage\RevisionRecordTests' => "$testDir/phpunit/includes/Storage/RevisionRecordTests.php",
+
+ # tests/phpunit/languages
+ 'LanguageClassesTestCase' => "$testDir/phpunit/languages/LanguageClassesTestCase.php",
+
+ # tests/phpunit/includes/libs
+ 'GenericArrayObjectTest' => "$testDir/phpunit/includes/libs/GenericArrayObjectTest.php",
+
+ # tests/phpunit/maintenance
+ 'MediaWiki\Tests\Maintenance\DumpTestCase' => "$testDir/phpunit/maintenance/DumpTestCase.php",
+ 'MediaWiki\Tests\Maintenance\MaintenanceBaseTestCase' => "$testDir/phpunit/maintenance/MaintenanceBaseTestCase.php",
+
+ # tests/phpunit/media
+ 'FakeDimensionFile' => "$testDir/phpunit/includes/media/FakeDimensionFile.php",
+ 'MediaWikiMediaTestCase' => "$testDir/phpunit/includes/media/MediaWikiMediaTestCase.php",
+
+ # tests/phpunit/mocks
+ 'MockFSFile' => "$testDir/phpunit/mocks/filebackend/MockFSFile.php",
+ 'MockFileBackend' => "$testDir/phpunit/mocks/filebackend/MockFileBackend.php",
+ 'MockLocalRepo' => "$testDir/phpunit/mocks/filerepo/MockLocalRepo.php",
+ 'MockBitmapHandler' => "$testDir/phpunit/mocks/media/MockBitmapHandler.php",
+ 'MockImageHandler' => "$testDir/phpunit/mocks/media/MockImageHandler.php",
+ 'MockSvgHandler' => "$testDir/phpunit/mocks/media/MockSvgHandler.php",
+ 'MockDjVuHandler' => "$testDir/phpunit/mocks/media/MockDjVuHandler.php",
+ 'MockChangesListFilter' => "$testDir/phpunit/mocks/MockChangesListFilter.php",
+ 'MockChangesListFilterGroup' => "$testDir/phpunit/mocks/MockChangesListFilterGroup.php",
+ 'MockWebRequest' => "$testDir/phpunit/mocks/MockWebRequest.php",
+ 'MediaWiki\\Session\\DummySessionBackend'
+ => "$testDir/phpunit/mocks/session/DummySessionBackend.php",
+ 'DummySessionProvider' => "$testDir/phpunit/mocks/session/DummySessionProvider.php",
+ 'MockMessageLocalizer' => "$testDir/phpunit/mocks/MockMessageLocalizer.php",
+
+ # tests/suites
+ 'ParserTestFileSuite' => "$testDir/phpunit/suites/ParserTestFileSuite.php",
+ 'ParserTestTopLevelSuite' => "$testDir/phpunit/suites/ParserTestTopLevelSuite.php",
+];
+// phpcs:enable
+
+/**
+ * Alias any PHPUnit 4 era PHPUnit_... class
+ * to it's PHPUnit 6 replacement. For most classes
+ * this is a direct _ -> \ replacement, but for
+ * some others we might need to maintain a manual
+ * mapping. Once we drop support for PHPUnit 4 this
+ * should be considered deprecated and eventually removed.
+ */
+spl_autoload_register( function ( $class ) {
+ if ( strpos( $class, 'PHPUnit_' ) !== 0 ) {
+ // Skip if it doesn't start with the old prefix
+ return;
+ }
+
+ // Classes that don't map 100%
+ $map = [
+ 'PHPUnit_Framework_TestSuite_DataProvider' => 'PHPUnit\Framework\DataProviderTestSuite',
+ 'PHPUnit_Framework_Error' => 'PHPUnit\Framework\Error\Error',
+ ];
+
+ if ( isset( $map[$class] ) ) {
+ $newForm = $map[$class];
+ } else {
+ $newForm = str_replace( '_', '\\', $class );
+ }
+
+ if ( class_exists( $newForm ) || interface_exists( $newForm ) ) {
+ // If the new class name exists, alias
+ // the old name to it.
+ class_alias( $newForm, $class );
+ }
+} );
diff --git a/www/wiki/tests/integration/includes/http/CurlHttpRequestTest.php b/www/wiki/tests/integration/includes/http/CurlHttpRequestTest.php
new file mode 100644
index 00000000..c1884b87
--- /dev/null
+++ b/www/wiki/tests/integration/includes/http/CurlHttpRequestTest.php
@@ -0,0 +1,9 @@
+<?php
+
+/**
+ * @group large
+ * @covers CurlHttpRequest
+ */
+class CurlHttpRequestTest extends MWHttpRequestTestCase {
+ protected static $httpEngine = 'curl';
+}
diff --git a/www/wiki/tests/integration/includes/http/MWHttpRequestTestCase.php b/www/wiki/tests/integration/includes/http/MWHttpRequestTestCase.php
new file mode 100644
index 00000000..262eb350
--- /dev/null
+++ b/www/wiki/tests/integration/includes/http/MWHttpRequestTestCase.php
@@ -0,0 +1,251 @@
+<?php
+
+use Wikimedia\TestingAccessWrapper;
+
+abstract class MWHttpRequestTestCase extends PHPUnit\Framework\TestCase {
+ protected static $httpEngine;
+ protected $oldHttpEngine;
+
+ public function setUp() {
+ parent::setUp();
+ $this->oldHttpEngine = Http::$httpEngine;
+ Http::$httpEngine = static::$httpEngine;
+
+ try {
+ $request = MWHttpRequest::factory( 'null:' );
+ } catch ( DomainException $e ) {
+ $this->markTestSkipped( static::$httpEngine . ' engine not supported' );
+ }
+
+ if ( static::$httpEngine === 'php' ) {
+ $this->assertInstanceOf( PhpHttpRequest::class, $request );
+ } else {
+ $this->assertInstanceOf( CurlHttpRequest::class, $request );
+ }
+ }
+
+ public function tearDown() {
+ parent::tearDown();
+ Http::$httpEngine = $this->oldHttpEngine;
+ }
+
+ // --------------------
+
+ public function testIsRedirect() {
+ $request = MWHttpRequest::factory( 'http://httpbin.org/get' );
+ $status = $request->execute();
+ $this->assertTrue( $status->isGood() );
+ $this->assertFalse( $request->isRedirect() );
+
+ $request = MWHttpRequest::factory( 'http://httpbin.org/redirect/1' );
+ $status = $request->execute();
+ $this->assertTrue( $status->isGood() );
+ $this->assertTrue( $request->isRedirect() );
+ }
+
+ public function testgetFinalUrl() {
+ $request = MWHttpRequest::factory( 'http://httpbin.org/redirect/3' );
+ if ( !$request->canFollowRedirects() ) {
+ $this->markTestSkipped( 'cannot follow redirects' );
+ }
+ $status = $request->execute();
+ $this->assertTrue( $status->isGood() );
+ $this->assertNotSame( 'http://httpbin.org/get', $request->getFinalUrl() );
+
+ $request = MWHttpRequest::factory( 'http://httpbin.org/redirect/3', [ 'followRedirects'
+ => true ] );
+ $status = $request->execute();
+ $this->assertTrue( $status->isGood() );
+ $this->assertSame( 'http://httpbin.org/get', $request->getFinalUrl() );
+ $this->assertResponseFieldValue( 'url', 'http://httpbin.org/get', $request );
+
+ $request = MWHttpRequest::factory( 'http://httpbin.org/redirect/3', [ 'followRedirects'
+ => true ] );
+ $status = $request->execute();
+ $this->assertTrue( $status->isGood() );
+ $this->assertSame( 'http://httpbin.org/get', $request->getFinalUrl() );
+ $this->assertResponseFieldValue( 'url', 'http://httpbin.org/get', $request );
+
+ if ( static::$httpEngine === 'curl' ) {
+ $this->markTestIncomplete( 'maxRedirects seems to be ignored by CurlHttpRequest' );
+ return;
+ }
+
+ $request = MWHttpRequest::factory( 'http://httpbin.org/redirect/3', [ 'followRedirects'
+ => true, 'maxRedirects' => 1 ] );
+ $status = $request->execute();
+ $this->assertTrue( $status->isGood() );
+ $this->assertNotSame( 'http://httpbin.org/get', $request->getFinalUrl() );
+ }
+
+ public function testSetCookie() {
+ $request = MWHttpRequest::factory( 'http://httpbin.org/cookies' );
+ $request->setCookie( 'foo', 'bar' );
+ $request->setCookie( 'foo2', 'bar2', [ 'domain' => 'example.com' ] );
+ $status = $request->execute();
+ $this->assertTrue( $status->isGood() );
+ $this->assertResponseFieldValue( 'cookies', [ 'foo' => 'bar' ], $request );
+ }
+
+ public function testSetCookieJar() {
+ $request = MWHttpRequest::factory( 'http://httpbin.org/cookies' );
+ $cookieJar = new CookieJar();
+ $cookieJar->setCookie( 'foo', 'bar', [ 'domain' => 'httpbin.org' ] );
+ $cookieJar->setCookie( 'foo2', 'bar2', [ 'domain' => 'example.com' ] );
+ $request->setCookieJar( $cookieJar );
+ $status = $request->execute();
+ $this->assertTrue( $status->isGood() );
+ $this->assertResponseFieldValue( 'cookies', [ 'foo' => 'bar' ], $request );
+
+ $request = MWHttpRequest::factory( 'http://httpbin.org/cookies/set?foo=bar' );
+ $cookieJar = new CookieJar();
+ $request->setCookieJar( $cookieJar );
+ $status = $request->execute();
+ $this->assertTrue( $status->isGood() );
+ $this->assertHasCookie( 'foo', 'bar', $request->getCookieJar() );
+
+ $this->markTestIncomplete( 'CookieJar does not handle deletion' );
+ return;
+
+ $request = MWHttpRequest::factory( 'http://httpbin.org/cookies/delete?foo' );
+ $cookieJar = new CookieJar();
+ $cookieJar->setCookie( 'foo', 'bar', [ 'domain' => 'httpbin.org' ] );
+ $cookieJar->setCookie( 'foo2', 'bar2', [ 'domain' => 'httpbin.org' ] );
+ $request->setCookieJar( $cookieJar );
+ $status = $request->execute();
+ $this->assertTrue( $status->isGood() );
+ $this->assertNotHasCookie( 'foo', $request->getCookieJar() );
+ $this->assertHasCookie( 'foo2', 'bar2', $request->getCookieJar() );
+ }
+
+ public function testGetResponseHeaders() {
+ $request = MWHttpRequest::factory( 'http://httpbin.org/response-headers?Foo=bar' );
+ $status = $request->execute();
+ $this->assertTrue( $status->isGood() );
+ $headers = array_change_key_case( $request->getResponseHeaders(), CASE_LOWER );
+ $this->assertArrayHasKey( 'foo', $headers );
+ $this->assertSame( $request->getResponseHeader( 'Foo' ), 'bar' );
+ }
+
+ public function testSetHeader() {
+ $request = MWHttpRequest::factory( 'http://httpbin.org/headers' );
+ $request->setHeader( 'Foo', 'bar' );
+ $status = $request->execute();
+ $this->assertTrue( $status->isGood() );
+ $this->assertResponseFieldValue( [ 'headers', 'Foo' ], 'bar', $request );
+ }
+
+ public function testGetStatus() {
+ $request = MWHttpRequest::factory( 'http://httpbin.org/status/418' );
+ $status = $request->execute();
+ $this->assertFalse( $status->isOK() );
+ $this->assertSame( $request->getStatus(), 418 );
+ }
+
+ public function testSetUserAgent() {
+ $request = MWHttpRequest::factory( 'http://httpbin.org/user-agent' );
+ $request->setUserAgent( 'foo' );
+ $status = $request->execute();
+ $this->assertTrue( $status->isGood() );
+ $this->assertResponseFieldValue( 'user-agent', 'foo', $request );
+ }
+
+ public function testSetData() {
+ $request = MWHttpRequest::factory( 'http://httpbin.org/post', [ 'method' => 'POST' ] );
+ $request->setData( [ 'foo' => 'bar', 'foo2' => 'bar2' ] );
+ $status = $request->execute();
+ $this->assertTrue( $status->isGood() );
+ $this->assertResponseFieldValue( 'form', [ 'foo' => 'bar', 'foo2' => 'bar2' ], $request );
+ }
+
+ public function testSetCallback() {
+ if ( static::$httpEngine === 'php' ) {
+ $this->markTestIncomplete( 'PhpHttpRequest does not use setCallback()' );
+ return;
+ }
+
+ $request = MWHttpRequest::factory( 'http://httpbin.org/ip' );
+ $data = '';
+ $request->setCallback( function ( $fh, $content ) use ( &$data ) {
+ $data .= $content;
+ return strlen( $content );
+ } );
+ $status = $request->execute();
+ $this->assertTrue( $status->isGood() );
+ $data = json_decode( $data, true );
+ $this->assertInternalType( 'array', $data );
+ $this->assertArrayHasKey( 'origin', $data );
+ }
+
+ public function testBasicAuthentication() {
+ $request = MWHttpRequest::factory( 'http://httpbin.org/basic-auth/user/pass', [
+ 'username' => 'user',
+ 'password' => 'pass',
+ ] );
+ $status = $request->execute();
+ $this->assertTrue( $status->isGood() );
+ $this->assertResponseFieldValue( 'authenticated', true, $request );
+
+ $request = MWHttpRequest::factory( 'http://httpbin.org/basic-auth/user/pass', [
+ 'username' => 'user',
+ 'password' => 'wrongpass',
+ ] );
+ $status = $request->execute();
+ $this->assertFalse( $status->isOK() );
+ $this->assertSame( 401, $request->getStatus() );
+ }
+
+ public function testFactoryDefaults() {
+ $request = MWHttpRequest::factory( 'http://acme.test' );
+ $this->assertInstanceOf( MWHttpRequest::class, $request );
+ }
+
+ // --------------------
+
+ /**
+ * Verifies that the request was successful, returned valid JSON and the given field of that
+ * JSON data is as expected.
+ * @param string|string[] $key Path to the data in the response object
+ * @param mixed $expectedValue
+ * @param MWHttpRequest $response
+ */
+ protected function assertResponseFieldValue( $key, $expectedValue, MWHttpRequest $response ) {
+ $this->assertSame( 200, $response->getStatus(), 'response status is not 200' );
+ $data = json_decode( $response->getContent(), true );
+ $this->assertInternalType( 'array', $data, 'response is not JSON' );
+ $keyPath = '';
+ foreach ( (array)$key as $keySegment ) {
+ $keyPath .= ( $keyPath ? '.' : '' ) . $keySegment;
+ $this->assertArrayHasKey( $keySegment, $data, $keyPath . ' not found' );
+ $data = $data[$keySegment];
+ }
+ $this->assertSame( $expectedValue, $data );
+ }
+
+ /**
+ * Asserts that the cookie jar has the given cookie with the given value.
+ * @param string $expectedName Cookie name
+ * @param string $expectedValue Cookie value
+ * @param CookieJar $cookieJar
+ */
+ protected function assertHasCookie( $expectedName, $expectedValue, CookieJar $cookieJar ) {
+ $cookieJar = TestingAccessWrapper::newFromObject( $cookieJar );
+ $cookies = array_change_key_case( $cookieJar->cookie, CASE_LOWER );
+ $this->assertArrayHasKey( strtolower( $expectedName ), $cookies );
+ $cookie = TestingAccessWrapper::newFromObject(
+ $cookies[strtolower( $expectedName )] );
+ $this->assertSame( $expectedValue, $cookie->value );
+ }
+
+ /**
+ * Asserts that the cookie jar does not have the given cookie.
+ * @param string $name Cookie name
+ * @param CookieJar $cookieJar
+ */
+ protected function assertNotHasCookie( $name, CookieJar $cookieJar ) {
+ $cookieJar = TestingAccessWrapper::newFromObject( $cookieJar );
+ $this->assertArrayNotHasKey( strtolower( $name ),
+ array_change_key_case( $cookieJar->cookie, CASE_LOWER ) );
+ }
+
+}
diff --git a/www/wiki/tests/integration/includes/http/PhpHttpRequestTest.php b/www/wiki/tests/integration/includes/http/PhpHttpRequestTest.php
new file mode 100644
index 00000000..8c461f35
--- /dev/null
+++ b/www/wiki/tests/integration/includes/http/PhpHttpRequestTest.php
@@ -0,0 +1,9 @@
+<?php
+
+/**
+ * @group large
+ * @covers PhpHttpRequest
+ */
+class PhpHttpRequestTest extends MWHttpRequestTestCase {
+ protected static $httpEngine = 'php';
+}
diff --git a/www/wiki/tests/integration/includes/shell/FirejailCommandTest.php b/www/wiki/tests/integration/includes/shell/FirejailCommandTest.php
new file mode 100644
index 00000000..1e008ee2
--- /dev/null
+++ b/www/wiki/tests/integration/includes/shell/FirejailCommandTest.php
@@ -0,0 +1,78 @@
+<?php
+
+use MediaWiki\Shell\FirejailCommand;
+use MediaWiki\Shell\Shell;
+
+/**
+ * Integration tests to ensure that firejail actually prevents execution.
+ * Meant to run on vagrant, although will probably work on other setups
+ * as long as firejail and sudo has similar config.
+ *
+ * @group large
+ * @group Shell
+ * @covers FirejailCommand
+ */
+class FirejailCommandIntegrationTest extends PHPUnit\Framework\TestCase {
+
+ public function setUp() {
+ parent::setUp();
+ if ( Shell::command( 'which', 'firejail' )->execute()->getExitCode() ) {
+ $this->markTestSkipped( 'firejail not installed' );
+ } elseif ( wfIsWindows() ) {
+ $this->markTestSkipped( 'test supports POSIX environments only' );
+ }
+ }
+
+ public function testSanity() {
+ // Make sure that firejail works at all.
+ $command = new FirejailCommand( 'firejail' );
+ $command
+ ->unsafeParams( 'ls .' )
+ ->restrict( Shell::RESTRICT_DEFAULT );
+ $result = $command->execute();
+ $this->assertSame( 0, $result->getExitCode() );
+ }
+
+ /**
+ * @coversNothing
+ * @dataProvider provideExecute
+ */
+ public function testExecute( $testCommand, $flag ) {
+ if ( preg_match( '/^sudo /', $testCommand ) ) {
+ if ( Shell::command( 'sudo', '-n', 'ls', '/' )->execute()->getExitCode() ) {
+ $this->markTestSkipped( 'need passwordless sudo' );
+ }
+ }
+
+ $command = new FirejailCommand( 'firejail' );
+ $command
+ ->unsafeParams( $testCommand )
+ // If we don't restrict at all, firejail won't be invoked,
+ // so the test will give a false positive if firejail breaks
+ // the command for some non-flag-related reason. Instead,
+ // set some flag that won't get in the way.
+ ->restrict( $flag === Shell::NO_NETWORK ? Shell::PRIVATE_DEV : Shell::NO_NETWORK );
+ $result = $command->execute();
+ $this->assertSame( 0, $result->getExitCode(), 'sanity check' );
+
+ $command = new FirejailCommand( 'firejail' );
+ $command
+ ->unsafeParams( $testCommand )
+ ->restrict( $flag );
+ $result = $command->execute();
+ $this->assertNotSame( 0, $result->getExitCode(), 'real check' );
+ }
+
+ public function provideExecute() {
+ global $IP;
+ return [
+ [ 'sudo -n ls /', Shell::NO_ROOT ],
+ [ 'sudo -n ls /', Shell::SECCOMP ], // not a great test but seems to work
+ [ 'ls /dev/cpu', Shell::PRIVATE_DEV ],
+ [ 'curl -fsSo /dev/null https://wikipedia.org/', Shell::NO_NETWORK ],
+ [ 'exec ls /', Shell::NO_EXECVE ],
+ [ "cat $IP/LocalSettings.php", Shell::NO_LOCALSETTINGS ],
+ ];
+ }
+
+}
diff --git a/www/wiki/tests/parser/DbTestPreviewer.php b/www/wiki/tests/parser/DbTestPreviewer.php
new file mode 100644
index 00000000..33aee7d3
--- /dev/null
+++ b/www/wiki/tests/parser/DbTestPreviewer.php
@@ -0,0 +1,205 @@
+<?php
+/**
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ * @ingroup Testing
+ */
+
+class DbTestPreviewer extends TestRecorder {
+ protected $filter; // /< Test name filter callback
+ protected $lb; // /< Database load balancer
+ protected $db; // /< Database connection to the main DB
+ protected $curRun; // /< run ID number for the current run
+ protected $prevRun; // /< run ID number for the previous run, if any
+ protected $results; // /< Result array
+
+ /**
+ * This should be called before the table prefix is changed
+ * @param IDatabase $db
+ * @param bool|string $filter
+ */
+ function __construct( $db, $filter = false ) {
+ $this->db = $db;
+ $this->filter = $filter;
+ }
+
+ /**
+ * Set up result recording; insert a record for the run with the date
+ * and all that fun stuff
+ */
+ function start() {
+ if ( !$this->db->tableExists( 'testrun', __METHOD__ )
+ || !$this->db->tableExists( 'testitem', __METHOD__ )
+ ) {
+ print "WARNING> `testrun` table not found in database.\n";
+ $this->prevRun = false;
+ } else {
+ // We'll make comparisons against the previous run later...
+ $this->prevRun = $this->db->selectField( 'testrun', 'MAX(tr_id)' );
+ }
+
+ $this->results = [];
+ }
+
+ function record( $test, ParserTestResult $result ) {
+ $this->results[$test['desc']] = $result->isSuccess() ? 1 : 0;
+ }
+
+ function report() {
+ if ( $this->prevRun ) {
+ // f = fail, p = pass, n = nonexistent
+ // codes show before then after
+ $table = [
+ 'fp' => 'previously failing test(s) now PASSING! :)',
+ 'pn' => 'previously PASSING test(s) removed o_O',
+ 'np' => 'new PASSING test(s) :)',
+
+ 'pf' => 'previously passing test(s) now FAILING! :(',
+ 'fn' => 'previously FAILING test(s) removed O_o',
+ 'nf' => 'new FAILING test(s) :(',
+ 'ff' => 'still FAILING test(s) :(',
+ ];
+
+ $prevResults = [];
+
+ $res = $this->db->select( 'testitem', [ 'ti_name', 'ti_success' ],
+ [ 'ti_run' => $this->prevRun ], __METHOD__ );
+ $filter = $this->filter;
+
+ foreach ( $res as $row ) {
+ if ( !$filter || $filter( $row->ti_name ) ) {
+ $prevResults[$row->ti_name] = $row->ti_success;
+ }
+ }
+
+ $combined = array_keys( $this->results + $prevResults );
+
+ # Determine breakdown by change type
+ $breakdown = [];
+ foreach ( $combined as $test ) {
+ if ( !isset( $prevResults[$test] ) ) {
+ $before = 'n';
+ } elseif ( $prevResults[$test] == 1 ) {
+ $before = 'p';
+ } else /* if ( $prevResults[$test] == 0 ) */ {
+ $before = 'f';
+ }
+
+ if ( !isset( $this->results[$test] ) ) {
+ $after = 'n';
+ } elseif ( $this->results[$test] == 1 ) {
+ $after = 'p';
+ } else /* if ( $this->results[$test] == 0 ) */ {
+ $after = 'f';
+ }
+
+ $code = $before . $after;
+
+ if ( isset( $table[$code] ) ) {
+ $breakdown[$code][$test] = $this->getTestStatusInfo( $test, $after );
+ }
+ }
+
+ # Write out results
+ foreach ( $table as $code => $label ) {
+ if ( !empty( $breakdown[$code] ) ) {
+ $count = count( $breakdown[$code] );
+ printf( "\n%4d %s\n", $count, $label );
+
+ foreach ( $breakdown[$code] as $differing_test_name => $statusInfo ) {
+ print " * $differing_test_name [$statusInfo]\n";
+ }
+ }
+ }
+ } else {
+ print "No previous test runs to compare against.\n";
+ }
+
+ print "\n";
+ }
+
+ /**
+ * Returns a string giving information about when a test last had a status change.
+ * Could help to track down when regressions were introduced, as distinct from tests
+ * which have never passed (which are more change requests than regressions).
+ * @param string $testname
+ * @param string $after
+ * @return string
+ */
+ private function getTestStatusInfo( $testname, $after ) {
+ // If we're looking at a test that has just been removed, then say when it first appeared.
+ if ( $after == 'n' ) {
+ $changedRun = $this->db->selectField( 'testitem',
+ 'MIN(ti_run)',
+ [ 'ti_name' => $testname ],
+ __METHOD__ );
+ $appear = $this->db->selectRow( 'testrun',
+ [ 'tr_date', 'tr_mw_version' ],
+ [ 'tr_id' => $changedRun ],
+ __METHOD__ );
+
+ return "First recorded appearance: "
+ . date( "d-M-Y H:i:s", strtotime( $appear->tr_date ) )
+ . ", " . $appear->tr_mw_version;
+ }
+
+ // Otherwise, this test has previous recorded results.
+ // See when this test last had a different result to what we're seeing now.
+ $conds = [
+ 'ti_name' => $testname,
+ 'ti_success' => ( $after == 'f' ? "1" : "0" ) ];
+
+ if ( $this->curRun ) {
+ $conds[] = "ti_run != " . $this->db->addQuotes( $this->curRun );
+ }
+
+ $changedRun = $this->db->selectField( 'testitem', 'MAX(ti_run)', $conds, __METHOD__ );
+
+ // If no record of ever having had a different result.
+ if ( is_null( $changedRun ) ) {
+ if ( $after == "f" ) {
+ return "Has never passed";
+ } else {
+ return "Has never failed";
+ }
+ }
+
+ // Otherwise, we're looking at a test whose status has changed.
+ // (i.e. it used to work, but now doesn't; or used to fail, but is now fixed.)
+ // In this situation, give as much info as we can as to when it changed status.
+ $pre = $this->db->selectRow( 'testrun',
+ [ 'tr_date', 'tr_mw_version' ],
+ [ 'tr_id' => $changedRun ],
+ __METHOD__ );
+ $post = $this->db->selectRow( 'testrun',
+ [ 'tr_date', 'tr_mw_version' ],
+ [ "tr_id > " . $this->db->addQuotes( $changedRun ) ],
+ __METHOD__,
+ [ "LIMIT" => 1, "ORDER BY" => 'tr_id' ]
+ );
+
+ if ( $post ) {
+ $postDate = date( "d-M-Y H:i:s", strtotime( $post->tr_date ) ) . ", {$post->tr_mw_version}";
+ } else {
+ $postDate = 'now';
+ }
+
+ return ( $after == "f" ? "Introduced" : "Fixed" ) . " between "
+ . date( "d-M-Y H:i:s", strtotime( $pre->tr_date ) ) . ", " . $pre->tr_mw_version
+ . " and $postDate";
+ }
+}
diff --git a/www/wiki/tests/parser/DbTestRecorder.php b/www/wiki/tests/parser/DbTestRecorder.php
new file mode 100644
index 00000000..2089f64a
--- /dev/null
+++ b/www/wiki/tests/parser/DbTestRecorder.php
@@ -0,0 +1,87 @@
+<?php
+/**
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ * @ingroup Testing
+ */
+
+use Wikimedia\Rdbms\IMaintainableDatabase;
+
+class DbTestRecorder extends TestRecorder {
+ public $version;
+ /** @var Database */
+ private $db;
+
+ public function __construct( IMaintainableDatabase $db ) {
+ $this->db = $db;
+ }
+
+ /**
+ * Set up result recording; insert a record for the run with the date
+ * and all that fun stuff
+ */
+ function start() {
+ $this->db->begin( __METHOD__ );
+
+ if ( !$this->db->tableExists( 'testrun' )
+ || !$this->db->tableExists( 'testitem' )
+ ) {
+ print "WARNING> `testrun` table not found in database. Trying to create table.\n";
+ $updater = DatabaseUpdater::newForDB( $this->db );
+ $this->db->sourceFile( $updater->patchPath( $this->db, 'patch-testrun.sql' ) );
+ echo "OK, resuming.\n";
+ }
+
+ $this->db->insert( 'testrun',
+ [
+ 'tr_date' => $this->db->timestamp(),
+ 'tr_mw_version' => $this->version,
+ 'tr_php_version' => PHP_VERSION,
+ 'tr_db_version' => $this->db->getServerVersion(),
+ 'tr_uname' => php_uname()
+ ],
+ __METHOD__ );
+ if ( $this->db->getType() === 'postgres' ) {
+ $this->curRun = $this->db->currentSequenceValue( 'testrun_id_seq' );
+ } else {
+ $this->curRun = $this->db->insertId();
+ }
+ }
+
+ /**
+ * Record an individual test item's success or failure to the db
+ *
+ * @param array $test
+ * @param ParserTestResult $result
+ */
+ function record( $test, ParserTestResult $result ) {
+ $this->db->insert( 'testitem',
+ [
+ 'ti_run' => $this->curRun,
+ 'ti_name' => $test['desc'],
+ 'ti_success' => $result->isSuccess() ? 1 : 0,
+ ],
+ __METHOD__ );
+ }
+
+ /**
+ * Commit transaction and clean up for result recording
+ */
+ function end() {
+ $this->db->commit( __METHOD__ );
+ }
+}
diff --git a/www/wiki/tests/parser/DjVuSupport.php b/www/wiki/tests/parser/DjVuSupport.php
new file mode 100644
index 00000000..73d4a47f
--- /dev/null
+++ b/www/wiki/tests/parser/DjVuSupport.php
@@ -0,0 +1,57 @@
+<?php
+
+/**
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ * @ingroup Testing
+ */
+
+/**
+ * Initialize and detect the DjVu files support
+ */
+class DjVuSupport {
+
+ /**
+ * Initialises DjVu tools global with default values
+ */
+ public function __construct() {
+ global $wgDjvuRenderer, $wgDjvuDump, $wgDjvuToXML, $wgFileExtensions, $wgDjvuTxt;
+
+ $wgDjvuRenderer = $wgDjvuRenderer ? $wgDjvuRenderer : '/usr/bin/ddjvu';
+ $wgDjvuDump = $wgDjvuDump ? $wgDjvuDump : '/usr/bin/djvudump';
+ $wgDjvuToXML = $wgDjvuToXML ? $wgDjvuToXML : '/usr/bin/djvutoxml';
+ $wgDjvuTxt = $wgDjvuTxt ? $wgDjvuTxt : '/usr/bin/djvutxt';
+
+ if ( !in_array( 'djvu', $wgFileExtensions ) ) {
+ $wgFileExtensions[] = 'djvu';
+ }
+ }
+
+ /**
+ * Returns true if the DjVu tools are usable
+ *
+ * @return bool
+ */
+ public function isEnabled() {
+ global $wgDjvuRenderer, $wgDjvuDump, $wgDjvuToXML, $wgDjvuTxt;
+
+ return is_executable( $wgDjvuRenderer )
+ && is_executable( $wgDjvuDump )
+ && is_executable( $wgDjvuToXML )
+ && is_executable( $wgDjvuTxt );
+ }
+}
diff --git a/www/wiki/tests/parser/MultiTestRecorder.php b/www/wiki/tests/parser/MultiTestRecorder.php
new file mode 100644
index 00000000..5fbfecf8
--- /dev/null
+++ b/www/wiki/tests/parser/MultiTestRecorder.php
@@ -0,0 +1,55 @@
+<?php
+
+/**
+ * This is a TestRecorder representing a collection of other TestRecorders.
+ * It proxies calls to all constituent objects.
+ */
+class MultiTestRecorder extends TestRecorder {
+ private $recorders = [];
+
+ public function addRecorder( TestRecorder $recorder ) {
+ $this->recorders[] = $recorder;
+ }
+
+ private function proxy( $funcName, $args ) {
+ foreach ( $this->recorders as $recorder ) {
+ call_user_func_array( [ $recorder, $funcName ], $args );
+ }
+ }
+
+ public function start() {
+ $this->proxy( __FUNCTION__, func_get_args() );
+ }
+
+ public function startTest( $test ) {
+ $this->proxy( __FUNCTION__, func_get_args() );
+ }
+
+ public function startSuite( $path ) {
+ $this->proxy( __FUNCTION__, func_get_args() );
+ }
+
+ public function endSuite( $path ) {
+ $this->proxy( __FUNCTION__, func_get_args() );
+ }
+
+ public function record( $test, ParserTestResult $result ) {
+ $this->proxy( __FUNCTION__, func_get_args() );
+ }
+
+ public function warning( $message ) {
+ $this->proxy( __FUNCTION__, func_get_args() );
+ }
+
+ public function skipped( $test, $subtest ) {
+ $this->proxy( __FUNCTION__, func_get_args() );
+ }
+
+ public function report() {
+ $this->proxy( __FUNCTION__, func_get_args() );
+ }
+
+ public function end() {
+ $this->proxy( __FUNCTION__, func_get_args() );
+ }
+}
diff --git a/www/wiki/tests/parser/ParserTestMockParser.php b/www/wiki/tests/parser/ParserTestMockParser.php
new file mode 100644
index 00000000..0757b34c
--- /dev/null
+++ b/www/wiki/tests/parser/ParserTestMockParser.php
@@ -0,0 +1,20 @@
+<?php
+
+/**
+ * A parser used during article insertion which does nothing, to avoid
+ * unnecessary log noise and other interference with debugging.
+ */
+class ParserTestMockParser {
+ public function preSaveTransform( $text, Title $title, User $user,
+ ParserOptions $options, $clearState = true
+ ) {
+ return $text;
+ }
+
+ public function parse(
+ $text, Title $title, ParserOptions $options,
+ $linestart = true, $clearState = true, $revid = null
+ ) {
+ return new ParserOutput;
+ }
+}
diff --git a/www/wiki/tests/parser/ParserTestParserHook.php b/www/wiki/tests/parser/ParserTestParserHook.php
new file mode 100644
index 00000000..5995012b
--- /dev/null
+++ b/www/wiki/tests/parser/ParserTestParserHook.php
@@ -0,0 +1,66 @@
+<?php
+/**
+ * A basic extension that's used by the parser tests to test whether input and
+ * arguments are passed to extensions properly.
+ *
+ * Copyright © 2005, 2006 Ævar Arnfjörð Bjarmason
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ * @ingroup Testing
+ * @author Ævar Arnfjörð Bjarmason <avarab@gmail.com>
+ */
+
+class ParserTestParserHook {
+
+ static function setup( &$parser ) {
+ $parser->setHook( 'tag', [ __CLASS__, 'dumpHook' ] );
+ $parser->setHook( 'tåg', [ __CLASS__, 'dumpHook' ] );
+ $parser->setHook( 'statictag', [ __CLASS__, 'staticTagHook' ] );
+ return true;
+ }
+
+ static function dumpHook( $in, $argv ) {
+ return "<pre>\n" .
+ var_export( $in, true ) . "\n" .
+ var_export( $argv, true ) . "\n" .
+ "</pre>";
+ }
+
+ static function staticTagHook( $in, $argv, $parser ) {
+ if ( !count( $argv ) ) {
+ $parser->static_tag_buf = $in;
+ return '';
+ } elseif ( count( $argv ) === 1 && isset( $argv['action'] )
+ && $argv['action'] === 'flush' && $in === null
+ ) {
+ // Clear the buffer, we probably don't need to
+ if ( isset( $parser->static_tag_buf ) ) {
+ $tmp = $parser->static_tag_buf;
+ } else {
+ $tmp = '';
+ }
+ $parser->static_tag_buf = null;
+ return $tmp;
+ } else { // wtf?
+ return "\nCall this extension as <statictag>string</statictag> or as" .
+ " <statictag action=flush/>, not in any other way.\n" .
+ "text: " . var_export( $in, true ) . "\n" .
+ "argv: " . var_export( $argv, true ) . "\n";
+ }
+ }
+}
diff --git a/www/wiki/tests/parser/ParserTestPrinter.php b/www/wiki/tests/parser/ParserTestPrinter.php
new file mode 100644
index 00000000..94d226c1
--- /dev/null
+++ b/www/wiki/tests/parser/ParserTestPrinter.php
@@ -0,0 +1,328 @@
+<?php
+/**
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ * @ingroup Testing
+ */
+
+/**
+ * This is a TestRecorder responsible for printing information about progress,
+ * success and failure to the console. It is specific to the parserTests.php
+ * frontend.
+ */
+class ParserTestPrinter extends TestRecorder {
+ private $total;
+ private $success;
+ private $skipped;
+ private $term;
+ private $showDiffs;
+ private $showProgress;
+ private $showFailure;
+ private $showOutput;
+ private $useDwdiff;
+ private $markWhitespace;
+ private $xmlError;
+
+ function __construct( $term, $options ) {
+ $this->term = $term;
+ $options += [
+ 'showDiffs' => true,
+ 'showProgress' => true,
+ 'showFailure' => true,
+ 'showOutput' => false,
+ 'useDwdiff' => false,
+ 'markWhitespace' => false,
+ ];
+ $this->showDiffs = $options['showDiffs'];
+ $this->showProgress = $options['showProgress'];
+ $this->showFailure = $options['showFailure'];
+ $this->showOutput = $options['showOutput'];
+ $this->useDwdiff = $options['useDwdiff'];
+ $this->markWhitespace = $options['markWhitespace'];
+ }
+
+ public function start() {
+ $this->total = 0;
+ $this->success = 0;
+ $this->skipped = 0;
+ }
+
+ public function startTest( $test ) {
+ if ( $this->showProgress ) {
+ $this->showTesting( $test['desc'] );
+ }
+ }
+
+ private function showTesting( $desc ) {
+ print "Running test $desc... ";
+ }
+
+ /**
+ * Show "Reading tests from ..."
+ *
+ * @param string $path
+ */
+ public function startSuite( $path ) {
+ print $this->term->color( 1 ) .
+ "Running parser tests from \"$path\"..." .
+ $this->term->reset() .
+ "\n";
+ }
+
+ public function endSuite( $path ) {
+ print "\n";
+ }
+
+ public function record( $test, ParserTestResult $result ) {
+ $this->total++;
+ $this->success += ( $result->isSuccess() ? 1 : 0 );
+
+ if ( $result->isSuccess() ) {
+ $this->showSuccess( $result );
+ } else {
+ $this->showFailure( $result );
+ }
+ }
+
+ /**
+ * Print a happy success message.
+ *
+ * @param ParserTestResult $testResult
+ * @return bool
+ */
+ private function showSuccess( ParserTestResult $testResult ) {
+ if ( $this->showProgress ) {
+ print $this->term->color( '1;32' ) . 'PASSED' . $this->term->reset() . "\n";
+ }
+ }
+
+ /**
+ * Print a failure message and provide some explanatory output
+ * about what went wrong if so configured.
+ *
+ * @param ParserTestResult $testResult
+ * @return bool
+ */
+ private function showFailure( ParserTestResult $testResult ) {
+ if ( $this->showFailure ) {
+ if ( !$this->showProgress ) {
+ # In quiet mode we didn't show the 'Testing' message before the
+ # test, in case it succeeded. Show it now:
+ $this->showTesting( $testResult->getDescription() );
+ }
+
+ print $this->term->color( '31' ) . 'FAILED!' . $this->term->reset() . "\n";
+
+ if ( $this->showOutput ) {
+ print "--- Expected ---\n{$testResult->expected}\n";
+ print "--- Actual ---\n{$testResult->actual}\n";
+ }
+
+ if ( $this->showDiffs ) {
+ print $this->quickDiff( $testResult->expected, $testResult->actual );
+ if ( !$this->wellFormed( $testResult->actual ) ) {
+ print "XML error: $this->xmlError\n";
+ }
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Run given strings through a diff and return the (colorized) output.
+ * Requires writable /tmp directory and a 'diff' command in the PATH.
+ *
+ * @param string $input
+ * @param string $output
+ * @param string $inFileTail Tailing for the input file name
+ * @param string $outFileTail Tailing for the output file name
+ * @return string
+ */
+ private function quickDiff( $input, $output,
+ $inFileTail = 'expected', $outFileTail = 'actual'
+ ) {
+ if ( $this->markWhitespace ) {
+ $pairs = [
+ "\n" => '¶',
+ ' ' => '·',
+ "\t" => '→'
+ ];
+ $input = strtr( $input, $pairs );
+ $output = strtr( $output, $pairs );
+ }
+
+ # Windows, or at least the fc utility, is retarded
+ $slash = wfIsWindows() ? '\\' : '/';
+ $prefix = wfTempDir() . "{$slash}mwParser-" . mt_rand();
+
+ $infile = "$prefix-$inFileTail";
+ $this->dumpToFile( $input, $infile );
+
+ $outfile = "$prefix-$outFileTail";
+ $this->dumpToFile( $output, $outfile );
+
+ $shellInfile = wfEscapeShellArg( $infile );
+ $shellOutfile = wfEscapeShellArg( $outfile );
+
+ global $wgDiff3;
+ // we assume that people with diff3 also have usual diff
+ if ( $this->useDwdiff ) {
+ $shellCommand = 'dwdiff -Pc';
+ } else {
+ $shellCommand = ( wfIsWindows() && !$wgDiff3 ) ? 'fc' : 'diff -au';
+ }
+
+ $diff = wfShellExec( "$shellCommand $shellInfile $shellOutfile" );
+
+ unlink( $infile );
+ unlink( $outfile );
+
+ if ( $this->useDwdiff ) {
+ return $diff;
+ } else {
+ return $this->colorDiff( $diff );
+ }
+ }
+
+ /**
+ * Write the given string to a file, adding a final newline.
+ *
+ * @param string $data
+ * @param string $filename
+ */
+ private function dumpToFile( $data, $filename ) {
+ $file = fopen( $filename, "wt" );
+ fwrite( $file, $data . "\n" );
+ fclose( $file );
+ }
+
+ /**
+ * Colorize unified diff output if set for ANSI color output.
+ * Subtractions are colored blue, additions red.
+ *
+ * @param string $text
+ * @return string
+ */
+ private function colorDiff( $text ) {
+ return preg_replace(
+ [ '/^(-.*)$/m', '/^(\+.*)$/m' ],
+ [ $this->term->color( 34 ) . '$1' . $this->term->reset(),
+ $this->term->color( 31 ) . '$1' . $this->term->reset() ],
+ $text );
+ }
+
+ private function wellFormed( $text ) {
+ $html =
+ Sanitizer::hackDocType() .
+ '<html>' .
+ $text .
+ '</html>';
+
+ $parser = xml_parser_create( "UTF-8" );
+
+ # case folding violates XML standard, turn it off
+ xml_parser_set_option( $parser, XML_OPTION_CASE_FOLDING, false );
+
+ if ( !xml_parse( $parser, $html, true ) ) {
+ $err = xml_error_string( xml_get_error_code( $parser ) );
+ $position = xml_get_current_byte_index( $parser );
+ $fragment = $this->extractFragment( $html, $position );
+ $this->xmlError = "$err at byte $position:\n$fragment";
+ xml_parser_free( $parser );
+
+ return false;
+ }
+
+ xml_parser_free( $parser );
+
+ return true;
+ }
+
+ private function extractFragment( $text, $position ) {
+ $start = max( 0, $position - 10 );
+ $before = $position - $start;
+ $fragment = '...' .
+ $this->term->color( 34 ) .
+ substr( $text, $start, $before ) .
+ $this->term->color( 0 ) .
+ $this->term->color( 31 ) .
+ $this->term->color( 1 ) .
+ substr( $text, $position, 1 ) .
+ $this->term->color( 0 ) .
+ $this->term->color( 34 ) .
+ substr( $text, $position + 1, 9 ) .
+ $this->term->color( 0 ) .
+ '...';
+ $display = str_replace( "\n", ' ', $fragment );
+ $caret = ' ' .
+ str_repeat( ' ', $before ) .
+ $this->term->color( 31 ) .
+ '^' .
+ $this->term->color( 0 );
+
+ return "$display\n$caret";
+ }
+
+ /**
+ * Show a warning to the user
+ * @param string $message
+ */
+ public function warning( $message ) {
+ echo "$message\n";
+ }
+
+ /**
+ * Mark a test skipped
+ * @param string $test
+ * @param string $subtest
+ */
+ public function skipped( $test, $subtest ) {
+ if ( $this->showProgress ) {
+ print $this->term->color( '1;33' ) . 'SKIPPED' . $this->term->reset() . "\n";
+ }
+ $this->skipped++;
+ }
+
+ public function report() {
+ if ( $this->total > 0 ) {
+ $this->reportPercentage( $this->success, $this->total );
+ } else {
+ print $this->term->color( 31 ) . "No tests found." . $this->term->reset() . "\n";
+ }
+ }
+
+ private function reportPercentage( $success, $total ) {
+ $ratio = wfPercent( 100 * $success / $total );
+ print $this->term->color( 1 ) . "Passed $success of $total tests ($ratio)";
+ if ( $this->skipped ) {
+ print ", skipped {$this->skipped}";
+ }
+ print "... ";
+
+ if ( $success == $total ) {
+ print $this->term->color( 32 ) . "ALL TESTS PASSED!";
+ } else {
+ $failed = $total - $success;
+ print $this->term->color( 31 ) . "$failed tests failed!";
+ }
+
+ print $this->term->reset() . "\n";
+
+ return ( $success == $total );
+ }
+}
diff --git a/www/wiki/tests/parser/ParserTestResult.php b/www/wiki/tests/parser/ParserTestResult.php
new file mode 100644
index 00000000..6396a018
--- /dev/null
+++ b/www/wiki/tests/parser/ParserTestResult.php
@@ -0,0 +1,44 @@
+<?php
+/**
+ * @file
+ *
+ * @copyright Copyright © 2013, Antoine Musso
+ * @copyright Copyright © 2013, Wikimedia Foundation Inc.
+ */
+
+/**
+ * Represent the result of a parser test.
+ *
+ * @since 1.22
+ */
+class ParserTestResult {
+ /** The test info array */
+ public $test;
+ /** Text that was expected */
+ public $expected;
+ /** Actual text rendered */
+ public $actual;
+
+ /**
+ * @param array $test The test info array from TestIterator
+ * @param string $expected The normalized expected output
+ * @param string $actual The actual output
+ */
+ public function __construct( $test, $expected, $actual ) {
+ $this->test = $test;
+ $this->expected = $expected;
+ $this->actual = $actual;
+ }
+
+ /**
+ * Whether the test passed
+ * @return bool
+ */
+ public function isSuccess() {
+ return $this->expected === $this->actual;
+ }
+
+ public function getDescription() {
+ return $this->test['desc'];
+ }
+}
diff --git a/www/wiki/tests/parser/ParserTestResultNormalizer.php b/www/wiki/tests/parser/ParserTestResultNormalizer.php
new file mode 100644
index 00000000..fbeed97b
--- /dev/null
+++ b/www/wiki/tests/parser/ParserTestResultNormalizer.php
@@ -0,0 +1,87 @@
+<?php
+/**
+ * @file
+ * @ingroup Testing
+ */
+
+class ParserTestResultNormalizer {
+ protected $doc, $xpath, $invalid;
+
+ public static function normalize( $text, $funcs ) {
+ $norm = new self( $text );
+ if ( $norm->invalid ) {
+ return $text;
+ }
+ foreach ( $funcs as $func ) {
+ $norm->$func();
+ }
+ return $norm->serialize();
+ }
+
+ protected function __construct( $text ) {
+ $this->doc = new DOMDocument( '1.0', 'utf-8' );
+
+ // Note: parsing a supposedly XHTML document with an XML parser is not
+ // guaranteed to give accurate results. For example, it may introduce
+ // differences in the number of line breaks in <pre> tags.
+
+ Wikimedia\suppressWarnings();
+ if ( !$this->doc->loadXML( '<html><body>' . $text . '</body></html>' ) ) {
+ $this->invalid = true;
+ }
+ Wikimedia\restoreWarnings();
+ $this->xpath = new DOMXPath( $this->doc );
+ $this->body = $this->xpath->query( '//body' )->item( 0 );
+ }
+
+ protected function removeTbody() {
+ foreach ( $this->xpath->query( '//tbody' ) as $tbody ) {
+ while ( $tbody->firstChild ) {
+ $child = $tbody->firstChild;
+ $tbody->removeChild( $child );
+ $tbody->parentNode->insertBefore( $child, $tbody );
+ }
+ $tbody->parentNode->removeChild( $tbody );
+ }
+ }
+
+ /**
+ * The point of this function is to produce a normalized DOM in which
+ * Tidy's output matches the output of html5depurate. Tidy both trims
+ * and pretty-prints, so this requires fairly aggressive treatment.
+ *
+ * In particular, note that Tidy converts <pre>x</pre> to <pre>\nx\n</pre>,
+ * which theoretically affects display since the second line break is not
+ * ignored by compliant HTML parsers.
+ *
+ * This function also removes empty elements, as does Tidy.
+ */
+ protected function trimWhitespace() {
+ foreach ( $this->xpath->query( '//text()' ) as $child ) {
+ if ( strtolower( $child->parentNode->nodeName ) === 'pre' ) {
+ // Just trim one line break from the start and end
+ if ( substr_compare( $child->data, "\n", 0 ) === 0 ) {
+ $child->data = substr( $child->data, 1 );
+ }
+ if ( substr_compare( $child->data, "\n", -1 ) === 0 ) {
+ $child->data = substr( $child->data, 0, -1 );
+ }
+ } else {
+ // Trim all whitespace
+ $child->data = trim( $child->data );
+ }
+ if ( $child->data === '' ) {
+ $child->parentNode->removeChild( $child );
+ }
+ }
+ }
+
+ /**
+ * Serialize the XML DOM for comparison purposes. This does not generate HTML.
+ * @return string
+ */
+ protected function serialize() {
+ return strtr( $this->doc->saveXML( $this->body ),
+ [ '<body>' => '', '</body>' => '' ] );
+ }
+}
diff --git a/www/wiki/tests/parser/ParserTestRunner.php b/www/wiki/tests/parser/ParserTestRunner.php
new file mode 100644
index 00000000..844a43f3
--- /dev/null
+++ b/www/wiki/tests/parser/ParserTestRunner.php
@@ -0,0 +1,1737 @@
+<?php
+/**
+ * Generic backend for the MediaWiki parser test suite, used by both the
+ * standalone parserTests.php and the PHPUnit "parsertests" suite.
+ *
+ * Copyright © 2004, 2010 Brion Vibber <brion@pobox.com>
+ * https://www.mediawiki.org/
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @todo Make this more independent of the configuration (and if possible the database)
+ * @file
+ * @ingroup Testing
+ */
+use Wikimedia\Rdbms\IDatabase;
+use MediaWiki\MediaWikiServices;
+use Wikimedia\ScopedCallback;
+use Wikimedia\TestingAccessWrapper;
+
+/**
+ * @ingroup Testing
+ */
+class ParserTestRunner {
+
+ /**
+ * MediaWiki core parser test files, paths
+ * will be prefixed with __DIR__ . '/'
+ *
+ * @var array
+ */
+ private static $coreTestFiles = [
+ 'parserTests.txt',
+ 'extraParserTests.txt',
+ ];
+
+ /**
+ * @var bool $useTemporaryTables Use temporary tables for the temporary database
+ */
+ private $useTemporaryTables = true;
+
+ /**
+ * @var array $setupDone The status of each setup function
+ */
+ private $setupDone = [
+ 'staticSetup' => false,
+ 'perTestSetup' => false,
+ 'setupDatabase' => false,
+ 'setDatabase' => false,
+ 'setupUploads' => false,
+ ];
+
+ /**
+ * Our connection to the database
+ * @var Database
+ */
+ private $db;
+
+ /**
+ * Database clone helper
+ * @var CloneDatabase
+ */
+ private $dbClone;
+
+ /**
+ * @var TidySupport
+ */
+ private $tidySupport;
+
+ /**
+ * @var TidyDriverBase
+ */
+ private $tidyDriver = null;
+
+ /**
+ * @var TestRecorder
+ */
+ private $recorder;
+
+ /**
+ * The upload directory, or null to not set up an upload directory
+ *
+ * @var string|null
+ */
+ private $uploadDir = null;
+
+ /**
+ * The name of the file backend to use, or null to use MockFileBackend.
+ * @var string|null
+ */
+ private $fileBackendName;
+
+ /**
+ * A complete regex for filtering tests.
+ * @var string
+ */
+ private $regex;
+
+ /**
+ * A list of normalization functions to apply to the expected and actual
+ * output.
+ * @var array
+ */
+ private $normalizationFunctions = [];
+
+ /**
+ * @param TestRecorder $recorder
+ * @param array $options
+ */
+ public function __construct( TestRecorder $recorder, $options = [] ) {
+ $this->recorder = $recorder;
+
+ if ( isset( $options['norm'] ) ) {
+ foreach ( $options['norm'] as $func ) {
+ if ( in_array( $func, [ 'removeTbody', 'trimWhitespace' ] ) ) {
+ $this->normalizationFunctions[] = $func;
+ } else {
+ $this->recorder->warning(
+ "Warning: unknown normalization option \"$func\"\n" );
+ }
+ }
+ }
+
+ if ( isset( $options['regex'] ) && $options['regex'] !== false ) {
+ $this->regex = $options['regex'];
+ } else {
+ # Matches anything
+ $this->regex = '//';
+ }
+
+ $this->keepUploads = !empty( $options['keep-uploads'] );
+
+ $this->fileBackendName = isset( $options['file-backend'] ) ?
+ $options['file-backend'] : false;
+
+ $this->runDisabled = !empty( $options['run-disabled'] );
+ $this->runParsoid = !empty( $options['run-parsoid'] );
+
+ $this->tidySupport = new TidySupport( !empty( $options['use-tidy-config'] ) );
+ if ( !$this->tidySupport->isEnabled() ) {
+ $this->recorder->warning(
+ "Warning: tidy is not installed, skipping some tests\n" );
+ }
+
+ if ( isset( $options['upload-dir'] ) ) {
+ $this->uploadDir = $options['upload-dir'];
+ }
+ }
+
+ /**
+ * Get list of filenames to extension and core parser tests
+ *
+ * @return array
+ */
+ public static function getParserTestFiles() {
+ global $wgParserTestFiles;
+
+ // Add core test files
+ $files = array_map( function ( $item ) {
+ return __DIR__ . "/$item";
+ }, self::$coreTestFiles );
+
+ // Plus legacy global files
+ $files = array_merge( $files, $wgParserTestFiles );
+
+ // Auto-discover extension parser tests
+ $registry = ExtensionRegistry::getInstance();
+ foreach ( $registry->getAllThings() as $info ) {
+ $dir = dirname( $info['path'] ) . '/tests/parser';
+ if ( !file_exists( $dir ) ) {
+ continue;
+ }
+ $counter = 1;
+ $dirIterator = new RecursiveIteratorIterator(
+ new RecursiveDirectoryIterator( $dir )
+ );
+ foreach ( $dirIterator as $fileInfo ) {
+ /** @var SplFileInfo $fileInfo */
+ if ( substr( $fileInfo->getFilename(), -4 ) === '.txt' ) {
+ $name = $info['name'] . $counter;
+ while ( isset( $files[$name] ) ) {
+ $name = $info['name'] . '_' . $counter++;
+ }
+ $files[$name] = $fileInfo->getPathname();
+ }
+ }
+ }
+
+ return array_unique( $files );
+ }
+
+ public function getRecorder() {
+ return $this->recorder;
+ }
+
+ /**
+ * Do any setup which can be done once for all tests, independent of test
+ * options, except for database setup.
+ *
+ * Public setup functions in this class return a ScopedCallback object. When
+ * this object is destroyed by going out of scope, teardown of the
+ * corresponding test setup is performed.
+ *
+ * Teardown objects may be chained by passing a ScopedCallback from a
+ * previous setup stage as the $nextTeardown parameter. This enforces the
+ * convention that teardown actions are taken in reverse order to the
+ * corresponding setup actions. When $nextTeardown is specified, a
+ * ScopedCallback will be returned which first tears down the current
+ * setup stage, and then tears down the previous setup stage which was
+ * specified by $nextTeardown.
+ *
+ * @param ScopedCallback|null $nextTeardown
+ * @return ScopedCallback
+ */
+ public function staticSetup( $nextTeardown = null ) {
+ // A note on coding style:
+
+ // The general idea here is to keep setup code together with
+ // corresponding teardown code, in a fine-grained manner. We have two
+ // arrays: $setup and $teardown. The code snippets in the $setup array
+ // are executed at the end of the method, before it returns, and the
+ // code snippets in the $teardown array are executed in reverse order
+ // when the Wikimedia\ScopedCallback object is consumed.
+
+ // Because it is a common operation to save, set and restore global
+ // variables, we have an additional convention: when the array key of
+ // $setup is a string, the string is taken to be the name of the global
+ // variable, and the element value is taken to be the desired new value.
+
+ // It's acceptable to just do the setup immediately, instead of adding
+ // a closure to $setup, except when the setup action depends on global
+ // variable initialisation being done first. In this case, you have to
+ // append a closure to $setup after the global variable is appended.
+
+ // When you add to setup functions in this class, please keep associated
+ // setup and teardown actions together in the source code, and please
+ // add comments explaining why the setup action is necessary.
+
+ $setup = [];
+ $teardown = [];
+
+ $teardown[] = $this->markSetupDone( 'staticSetup' );
+
+ // Some settings which influence HTML output
+ $setup['wgSitename'] = 'MediaWiki';
+ $setup['wgServer'] = 'http://example.org';
+ $setup['wgServerName'] = 'example.org';
+ $setup['wgScriptPath'] = '';
+ $setup['wgScript'] = '/index.php';
+ $setup['wgResourceBasePath'] = '';
+ $setup['wgStylePath'] = '/skins';
+ $setup['wgExtensionAssetsPath'] = '/extensions';
+ $setup['wgArticlePath'] = '/wiki/$1';
+ $setup['wgActionPaths'] = [];
+ $setup['wgVariantArticlePath'] = false;
+ $setup['wgUploadNavigationUrl'] = false;
+ $setup['wgCapitalLinks'] = true;
+ $setup['wgNoFollowLinks'] = true;
+ $setup['wgNoFollowDomainExceptions'] = [ 'no-nofollow.org' ];
+ $setup['wgExternalLinkTarget'] = false;
+ $setup['wgExperimentalHtmlIds'] = false;
+ $setup['wgLocaltimezone'] = 'UTC';
+ $setup['wgHtml5'] = true;
+ $setup['wgDisableLangConversion'] = false;
+ $setup['wgDisableTitleConversion'] = false;
+
+ // "extra language links"
+ // see https://gerrit.wikimedia.org/r/111390
+ $setup['wgExtraInterlanguageLinkPrefixes'] = [ 'mul' ];
+
+ // All FileRepo changes should be done here by injecting services,
+ // there should be no need to change global variables.
+ RepoGroup::setSingleton( $this->createRepoGroup() );
+ $teardown[] = function () {
+ RepoGroup::destroySingleton();
+ };
+
+ // Set up null lock managers
+ $setup['wgLockManagers'] = [ [
+ 'name' => 'fsLockManager',
+ 'class' => NullLockManager::class,
+ ], [
+ 'name' => 'nullLockManager',
+ 'class' => NullLockManager::class,
+ ] ];
+ $reset = function () {
+ LockManagerGroup::destroySingletons();
+ };
+ $setup[] = $reset;
+ $teardown[] = $reset;
+
+ // This allows article insertion into the prefixed DB
+ $setup['wgDefaultExternalStore'] = false;
+
+ // This might slightly reduce memory usage
+ $setup['wgAdaptiveMessageCache'] = true;
+
+ // This is essential and overrides disabling of database messages in TestSetup
+ $setup['wgUseDatabaseMessages'] = true;
+ $reset = function () {
+ MessageCache::destroyInstance();
+ };
+ $setup[] = $reset;
+ $teardown[] = $reset;
+
+ // It's not necessary to actually convert any files
+ $setup['wgSVGConverter'] = 'null';
+ $setup['wgSVGConverters'] = [ 'null' => 'echo "1">$output' ];
+
+ // Fake constant timestamp
+ Hooks::register( 'ParserGetVariableValueTs', function ( &$parser, &$ts ) {
+ $ts = $this->getFakeTimestamp();
+ return true;
+ } );
+ $teardown[] = function () {
+ Hooks::clear( 'ParserGetVariableValueTs' );
+ };
+
+ $this->appendNamespaceSetup( $setup, $teardown );
+
+ // Set up interwikis and append teardown function
+ $teardown[] = $this->setupInterwikis();
+
+ // This affects title normalization in links. It invalidates
+ // MediaWikiTitleCodec objects.
+ $setup['wgLocalInterwikis'] = [ 'local', 'mi' ];
+ $reset = function () {
+ $this->resetTitleServices();
+ };
+ $setup[] = $reset;
+ $teardown[] = $reset;
+
+ // Set up a mock MediaHandlerFactory
+ MediaWikiServices::getInstance()->disableService( 'MediaHandlerFactory' );
+ MediaWikiServices::getInstance()->redefineService(
+ 'MediaHandlerFactory',
+ function ( MediaWikiServices $services ) {
+ $handlers = $services->getMainConfig()->get( 'ParserTestMediaHandlers' );
+ return new MediaHandlerFactory( $handlers );
+ }
+ );
+ $teardown[] = function () {
+ MediaWikiServices::getInstance()->resetServiceForTesting( 'MediaHandlerFactory' );
+ };
+
+ // SqlBagOStuff broke when using temporary tables on r40209 (T17892).
+ // It seems to have been fixed since (r55079?), but regressed at some point before r85701.
+ // This works around it for now...
+ global $wgObjectCaches;
+ $setup['wgObjectCaches'] = [ CACHE_DB => $wgObjectCaches['hash'] ] + $wgObjectCaches;
+ if ( isset( ObjectCache::$instances[CACHE_DB] ) ) {
+ $savedCache = ObjectCache::$instances[CACHE_DB];
+ ObjectCache::$instances[CACHE_DB] = new HashBagOStuff;
+ $teardown[] = function () use ( $savedCache ) {
+ ObjectCache::$instances[CACHE_DB] = $savedCache;
+ };
+ }
+
+ $teardown[] = $this->executeSetupSnippets( $setup );
+
+ // Schedule teardown snippets in reverse order
+ return $this->createTeardownObject( $teardown, $nextTeardown );
+ }
+
+ private function appendNamespaceSetup( &$setup, &$teardown ) {
+ // Add a namespace shadowing a interwiki link, to test
+ // proper precedence when resolving links. (T53680)
+ $setup['wgExtraNamespaces'] = [
+ 100 => 'MemoryAlpha',
+ 101 => 'MemoryAlpha_talk'
+ ];
+ // Changing wgExtraNamespaces invalidates caches in MWNamespace and
+ // any live Language object, both on setup and teardown
+ $reset = function () {
+ MWNamespace::clearCaches();
+ $GLOBALS['wgContLang']->resetNamespaces();
+ };
+ $setup[] = $reset;
+ $teardown[] = $reset;
+ }
+
+ /**
+ * Create a RepoGroup object appropriate for the current configuration
+ * @return RepoGroup
+ */
+ protected function createRepoGroup() {
+ if ( $this->uploadDir ) {
+ if ( $this->fileBackendName ) {
+ throw new MWException( 'You cannot specify both use-filebackend and upload-dir' );
+ }
+ $backend = new FSFileBackend( [
+ 'name' => 'local-backend',
+ 'wikiId' => wfWikiID(),
+ 'basePath' => $this->uploadDir,
+ 'tmpDirectory' => wfTempDir()
+ ] );
+ } elseif ( $this->fileBackendName ) {
+ global $wgFileBackends;
+ $name = $this->fileBackendName;
+ $useConfig = false;
+ foreach ( $wgFileBackends as $conf ) {
+ if ( $conf['name'] === $name ) {
+ $useConfig = $conf;
+ }
+ }
+ if ( $useConfig === false ) {
+ throw new MWException( "Unable to find file backend \"$name\"" );
+ }
+ $useConfig['name'] = 'local-backend'; // swap name
+ unset( $useConfig['lockManager'] );
+ unset( $useConfig['fileJournal'] );
+ $class = $useConfig['class'];
+ $backend = new $class( $useConfig );
+ } else {
+ # Replace with a mock. We do not care about generating real
+ # files on the filesystem, just need to expose the file
+ # informations.
+ $backend = new MockFileBackend( [
+ 'name' => 'local-backend',
+ 'wikiId' => wfWikiID()
+ ] );
+ }
+
+ return new RepoGroup(
+ [
+ 'class' => MockLocalRepo::class,
+ 'name' => 'local',
+ 'url' => 'http://example.com/images',
+ 'hashLevels' => 2,
+ 'transformVia404' => false,
+ 'backend' => $backend
+ ],
+ []
+ );
+ }
+
+ /**
+ * Execute an array in which elements with integer keys are taken to be
+ * callable objects, and other elements are taken to be global variable
+ * set operations, with the key giving the variable name and the value
+ * giving the new global variable value. A closure is returned which, when
+ * executed, sets the global variables back to the values they had before
+ * this function was called.
+ *
+ * @see staticSetup
+ *
+ * @param array $setup
+ * @return closure
+ */
+ protected function executeSetupSnippets( $setup ) {
+ $saved = [];
+ foreach ( $setup as $name => $value ) {
+ if ( is_int( $name ) ) {
+ $value();
+ } else {
+ $saved[$name] = isset( $GLOBALS[$name] ) ? $GLOBALS[$name] : null;
+ $GLOBALS[$name] = $value;
+ }
+ }
+ return function () use ( $saved ) {
+ $this->executeSetupSnippets( $saved );
+ };
+ }
+
+ /**
+ * Take a setup array in the same format as the one given to
+ * executeSetupSnippets(), and return a ScopedCallback which, when consumed,
+ * executes the snippets in the setup array in reverse order. This is used
+ * to create "teardown objects" for the public API.
+ *
+ * @see staticSetup
+ *
+ * @param array $teardown The snippet array
+ * @param ScopedCallback|null $nextTeardown A ScopedCallback to consume
+ * @return ScopedCallback
+ */
+ protected function createTeardownObject( $teardown, $nextTeardown = null ) {
+ return new ScopedCallback( function () use ( $teardown, $nextTeardown ) {
+ // Schedule teardown snippets in reverse order
+ $teardown = array_reverse( $teardown );
+
+ $this->executeSetupSnippets( $teardown );
+ if ( $nextTeardown ) {
+ ScopedCallback::consume( $nextTeardown );
+ }
+ } );
+ }
+
+ /**
+ * Set a setupDone flag to indicate that setup has been done, and return
+ * the teardown closure. If the flag was already set, throw an exception.
+ *
+ * @param string $funcName The setup function name
+ * @return closure
+ */
+ protected function markSetupDone( $funcName ) {
+ if ( $this->setupDone[$funcName] ) {
+ throw new MWException( "$funcName is already done" );
+ }
+ $this->setupDone[$funcName] = true;
+ return function () use ( $funcName ) {
+ $this->setupDone[$funcName] = false;
+ };
+ }
+
+ /**
+ * Ensure a given setup stage has been done, throw an exception if it has
+ * not.
+ * @param string $funcName
+ * @param string|null $funcName2
+ */
+ protected function checkSetupDone( $funcName, $funcName2 = null ) {
+ if ( !$this->setupDone[$funcName]
+ && ( $funcName === null || !$this->setupDone[$funcName2] )
+ ) {
+ throw new MWException( "$funcName must be called before calling " .
+ wfGetCaller() );
+ }
+ }
+
+ /**
+ * Determine whether a particular setup function has been run
+ *
+ * @param string $funcName
+ * @return bool
+ */
+ public function isSetupDone( $funcName ) {
+ return isset( $this->setupDone[$funcName] ) ? $this->setupDone[$funcName] : false;
+ }
+
+ /**
+ * Insert hardcoded interwiki in the lookup table.
+ *
+ * This function insert a set of well known interwikis that are used in
+ * the parser tests. They can be considered has fixtures are injected in
+ * the interwiki cache by using the 'InterwikiLoadPrefix' hook.
+ * Since we are not interested in looking up interwikis in the database,
+ * the hook completely replace the existing mechanism (hook returns false).
+ *
+ * @return closure for teardown
+ */
+ private function setupInterwikis() {
+ # Hack: insert a few Wikipedia in-project interwiki prefixes,
+ # for testing inter-language links
+ Hooks::register( 'InterwikiLoadPrefix', function ( $prefix, &$iwData ) {
+ static $testInterwikis = [
+ 'local' => [
+ 'iw_url' => 'http://doesnt.matter.org/$1',
+ 'iw_api' => '',
+ 'iw_wikiid' => '',
+ 'iw_local' => 0 ],
+ 'wikipedia' => [
+ 'iw_url' => 'http://en.wikipedia.org/wiki/$1',
+ 'iw_api' => '',
+ 'iw_wikiid' => '',
+ 'iw_local' => 0 ],
+ 'meatball' => [
+ 'iw_url' => 'http://www.usemod.com/cgi-bin/mb.pl?$1',
+ 'iw_api' => '',
+ 'iw_wikiid' => '',
+ 'iw_local' => 0 ],
+ 'memoryalpha' => [
+ 'iw_url' => 'http://www.memory-alpha.org/en/index.php/$1',
+ 'iw_api' => '',
+ 'iw_wikiid' => '',
+ 'iw_local' => 0 ],
+ 'zh' => [
+ 'iw_url' => 'http://zh.wikipedia.org/wiki/$1',
+ 'iw_api' => '',
+ 'iw_wikiid' => '',
+ 'iw_local' => 1 ],
+ 'es' => [
+ 'iw_url' => 'http://es.wikipedia.org/wiki/$1',
+ 'iw_api' => '',
+ 'iw_wikiid' => '',
+ 'iw_local' => 1 ],
+ 'fr' => [
+ 'iw_url' => 'http://fr.wikipedia.org/wiki/$1',
+ 'iw_api' => '',
+ 'iw_wikiid' => '',
+ 'iw_local' => 1 ],
+ 'ru' => [
+ 'iw_url' => 'http://ru.wikipedia.org/wiki/$1',
+ 'iw_api' => '',
+ 'iw_wikiid' => '',
+ 'iw_local' => 1 ],
+ 'mi' => [
+ 'iw_url' => 'http://mi.wikipedia.org/wiki/$1',
+ 'iw_api' => '',
+ 'iw_wikiid' => '',
+ 'iw_local' => 1 ],
+ 'mul' => [
+ 'iw_url' => 'http://wikisource.org/wiki/$1',
+ 'iw_api' => '',
+ 'iw_wikiid' => '',
+ 'iw_local' => 1 ],
+ ];
+ if ( array_key_exists( $prefix, $testInterwikis ) ) {
+ $iwData = $testInterwikis[$prefix];
+ }
+
+ // We only want to rely on the above fixtures
+ return false;
+ } );// hooks::register
+
+ // Reset the service in case any other tests already cached some prefixes.
+ MediaWikiServices::getInstance()->resetServiceForTesting( 'InterwikiLookup' );
+
+ return function () {
+ // Tear down
+ Hooks::clear( 'InterwikiLoadPrefix' );
+ MediaWikiServices::getInstance()->resetServiceForTesting( 'InterwikiLookup' );
+ };
+ }
+
+ /**
+ * Reset the Title-related services that need resetting
+ * for each test
+ */
+ private function resetTitleServices() {
+ $services = MediaWikiServices::getInstance();
+ $services->resetServiceForTesting( 'TitleFormatter' );
+ $services->resetServiceForTesting( 'TitleParser' );
+ $services->resetServiceForTesting( '_MediaWikiTitleCodec' );
+ $services->resetServiceForTesting( 'LinkRenderer' );
+ $services->resetServiceForTesting( 'LinkRendererFactory' );
+ }
+
+ /**
+ * Remove last character if it is a newline
+ * @param string $s
+ * @return string
+ */
+ public static function chomp( $s ) {
+ if ( substr( $s, -1 ) === "\n" ) {
+ return substr( $s, 0, -1 );
+ } else {
+ return $s;
+ }
+ }
+
+ /**
+ * Run a series of tests listed in the given text files.
+ * Each test consists of a brief description, wikitext input,
+ * and the expected HTML output.
+ *
+ * Prints status updates on stdout and counts up the total
+ * number and percentage of passed tests.
+ *
+ * Handles all setup and teardown.
+ *
+ * @param array $filenames Array of strings
+ * @return bool True if passed all tests, false if any tests failed.
+ */
+ public function runTestsFromFiles( $filenames ) {
+ $ok = false;
+
+ $teardownGuard = $this->staticSetup();
+ $teardownGuard = $this->setupDatabase( $teardownGuard );
+ $teardownGuard = $this->setupUploads( $teardownGuard );
+
+ $this->recorder->start();
+ try {
+ $ok = true;
+
+ foreach ( $filenames as $filename ) {
+ $testFileInfo = TestFileReader::read( $filename, [
+ 'runDisabled' => $this->runDisabled,
+ 'runParsoid' => $this->runParsoid,
+ 'regex' => $this->regex ] );
+
+ // Don't start the suite if there are no enabled tests in the file
+ if ( !$testFileInfo['tests'] ) {
+ continue;
+ }
+
+ $this->recorder->startSuite( $filename );
+ $ok = $this->runTests( $testFileInfo ) && $ok;
+ $this->recorder->endSuite( $filename );
+ }
+
+ $this->recorder->report();
+ } catch ( DBError $e ) {
+ $this->recorder->warning( $e->getMessage() );
+ }
+ $this->recorder->end();
+
+ ScopedCallback::consume( $teardownGuard );
+
+ return $ok;
+ }
+
+ /**
+ * Determine whether the current parser has the hooks registered in it
+ * that are required by a file read by TestFileReader.
+ * @param array $requirements
+ * @return bool
+ */
+ public function meetsRequirements( $requirements ) {
+ foreach ( $requirements as $requirement ) {
+ switch ( $requirement['type'] ) {
+ case 'hook':
+ $ok = $this->requireHook( $requirement['name'] );
+ break;
+ case 'functionHook':
+ $ok = $this->requireFunctionHook( $requirement['name'] );
+ break;
+ case 'transparentHook':
+ $ok = $this->requireTransparentHook( $requirement['name'] );
+ break;
+ }
+ if ( !$ok ) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Run the tests from a single file. staticSetup() and setupDatabase()
+ * must have been called already.
+ *
+ * @param array $testFileInfo Parsed file info returned by TestFileReader
+ * @return bool True if passed all tests, false if any tests failed.
+ */
+ public function runTests( $testFileInfo ) {
+ $ok = true;
+
+ $this->checkSetupDone( 'staticSetup' );
+
+ // Don't add articles from the file if there are no enabled tests from the file
+ if ( !$testFileInfo['tests'] ) {
+ return true;
+ }
+
+ // If any requirements are not met, mark all tests from the file as skipped
+ if ( !$this->meetsRequirements( $testFileInfo['requirements'] ) ) {
+ foreach ( $testFileInfo['tests'] as $test ) {
+ $this->recorder->startTest( $test );
+ $this->recorder->skipped( $test, 'required extension not enabled' );
+ }
+ return true;
+ }
+
+ // Add articles
+ $this->addArticles( $testFileInfo['articles'] );
+
+ // Run tests
+ foreach ( $testFileInfo['tests'] as $test ) {
+ $this->recorder->startTest( $test );
+ $result =
+ $this->runTest( $test );
+ if ( $result !== false ) {
+ $ok = $ok && $result->isSuccess();
+ $this->recorder->record( $test, $result );
+ }
+ }
+
+ return $ok;
+ }
+
+ /**
+ * Get a Parser object
+ *
+ * @param string $preprocessor
+ * @return Parser
+ */
+ function getParser( $preprocessor = null ) {
+ global $wgParserConf;
+
+ $class = $wgParserConf['class'];
+ $parser = new $class( [ 'preprocessorClass' => $preprocessor ] + $wgParserConf );
+ ParserTestParserHook::setup( $parser );
+
+ return $parser;
+ }
+
+ /**
+ * Run a given wikitext input through a freshly-constructed wiki parser,
+ * and compare the output against the expected results.
+ * Prints status and explanatory messages to stdout.
+ *
+ * staticSetup() and setupWikiData() must be called before this function
+ * is entered.
+ *
+ * @param array $test The test parameters:
+ * - test: The test name
+ * - desc: The subtest description
+ * - input: Wikitext to try rendering
+ * - options: Array of test options
+ * - config: Overrides for global variables, one per line
+ *
+ * @return ParserTestResult or false if skipped
+ */
+ public function runTest( $test ) {
+ wfDebug( __METHOD__.": running {$test['desc']}" );
+ $opts = $this->parseOptions( $test['options'] );
+ $teardownGuard = $this->perTestSetup( $test );
+
+ $context = RequestContext::getMain();
+ $user = $context->getUser();
+ $options = ParserOptions::newFromContext( $context );
+ $options->setTimestamp( $this->getFakeTimestamp() );
+
+ if ( isset( $opts['tidy'] ) ) {
+ if ( !$this->tidySupport->isEnabled() ) {
+ $this->recorder->skipped( $test, 'tidy extension is not installed' );
+ return false;
+ } else {
+ $options->setTidy( true );
+ }
+ }
+
+ if ( isset( $opts['title'] ) ) {
+ $titleText = $opts['title'];
+ } else {
+ $titleText = 'Parser test';
+ }
+
+ $local = isset( $opts['local'] );
+ $preprocessor = isset( $opts['preprocessor'] ) ? $opts['preprocessor'] : null;
+ $parser = $this->getParser( $preprocessor );
+ $title = Title::newFromText( $titleText );
+
+ if ( isset( $opts['styletag'] ) ) {
+ // For testing the behavior of <style> (including those deduplicated
+ // into <link> tags), add tag hooks to allow them to be generated.
+ $parser->setHook( 'style', function ( $content, $attributes, $parser ) {
+ $marker = Parser::MARKER_PREFIX . '-style-' . md5( $content ) . Parser::MARKER_SUFFIX;
+ $parser->mStripState->addNoWiki( $marker, $content );
+ return Html::inlineStyle( $marker, 'all', $attributes );
+ } );
+ $parser->setHook( 'link', function ( $content, $attributes, $parser ) {
+ return Html::element( 'link', $attributes );
+ } );
+ }
+
+ if ( isset( $opts['pst'] ) ) {
+ $out = $parser->preSaveTransform( $test['input'], $title, $user, $options );
+ $output = $parser->getOutput();
+ } elseif ( isset( $opts['msg'] ) ) {
+ $out = $parser->transformMsg( $test['input'], $options, $title );
+ } elseif ( isset( $opts['section'] ) ) {
+ $section = $opts['section'];
+ $out = $parser->getSection( $test['input'], $section );
+ } elseif ( isset( $opts['replace'] ) ) {
+ $section = $opts['replace'][0];
+ $replace = $opts['replace'][1];
+ $out = $parser->replaceSection( $test['input'], $section, $replace );
+ } elseif ( isset( $opts['comment'] ) ) {
+ $out = Linker::formatComment( $test['input'], $title, $local );
+ } elseif ( isset( $opts['preload'] ) ) {
+ $out = $parser->getPreloadText( $test['input'], $title, $options );
+ } else {
+ $output = $parser->parse( $test['input'], $title, $options, true, true, 1337 );
+ $out = $output->getText( [
+ 'allowTOC' => !isset( $opts['notoc'] ),
+ 'unwrap' => !isset( $opts['wrap'] ),
+ ] );
+ if ( isset( $opts['tidy'] ) ) {
+ $out = preg_replace( '/\s+$/', '', $out );
+ }
+
+ if ( isset( $opts['showtitle'] ) ) {
+ if ( $output->getTitleText() ) {
+ $title = $output->getTitleText();
+ }
+
+ $out = "$title\n$out";
+ }
+
+ if ( isset( $opts['showindicators'] ) ) {
+ $indicators = '';
+ foreach ( $output->getIndicators() as $id => $content ) {
+ $indicators .= "$id=$content\n";
+ }
+ $out = $indicators . $out;
+ }
+
+ if ( isset( $opts['ill'] ) ) {
+ $out = implode( ' ', $output->getLanguageLinks() );
+ } elseif ( isset( $opts['cat'] ) ) {
+ $out = '';
+ foreach ( $output->getCategories() as $name => $sortkey ) {
+ if ( $out !== '' ) {
+ $out .= "\n";
+ }
+ $out .= "cat=$name sort=$sortkey";
+ }
+ }
+ }
+
+ if ( isset( $output ) && isset( $opts['showflags'] ) ) {
+ $actualFlags = array_keys( TestingAccessWrapper::newFromObject( $output )->mFlags );
+ sort( $actualFlags );
+ $out .= "\nflags=" . implode( ', ', $actualFlags );
+ }
+
+ ScopedCallback::consume( $teardownGuard );
+
+ $expected = $test['result'];
+ if ( count( $this->normalizationFunctions ) ) {
+ $expected = ParserTestResultNormalizer::normalize(
+ $test['expected'], $this->normalizationFunctions );
+ $out = ParserTestResultNormalizer::normalize( $out, $this->normalizationFunctions );
+ }
+
+ $testResult = new ParserTestResult( $test, $expected, $out );
+ return $testResult;
+ }
+
+ /**
+ * Use a regex to find out the value of an option
+ * @param string $key Name of option val to retrieve
+ * @param array $opts Options array to look in
+ * @param mixed $default Default value returned if not found
+ * @return mixed
+ */
+ private static function getOptionValue( $key, $opts, $default ) {
+ $key = strtolower( $key );
+
+ if ( isset( $opts[$key] ) ) {
+ return $opts[$key];
+ } else {
+ return $default;
+ }
+ }
+
+ /**
+ * Given the options string, return an associative array of options.
+ * @todo Move this to TestFileReader
+ *
+ * @param string $instring
+ * @return array
+ */
+ private function parseOptions( $instring ) {
+ $opts = [];
+ // foo
+ // foo=bar
+ // foo="bar baz"
+ // foo=[[bar baz]]
+ // foo=bar,"baz quux"
+ // foo={...json...}
+ $defs = '(?(DEFINE)
+ (?<qstr> # Quoted string
+ "
+ (?:[^\\\\"] | \\\\.)*
+ "
+ )
+ (?<json>
+ \{ # Open bracket
+ (?:
+ [^"{}] | # Not a quoted string or object, or
+ (?&qstr) | # A quoted string, or
+ (?&json) # A json object (recursively)
+ )*
+ \} # Close bracket
+ )
+ (?<value>
+ (?:
+ (?&qstr) # Quoted val
+ |
+ \[\[
+ [^]]* # Link target
+ \]\]
+ |
+ [\w-]+ # Plain word
+ |
+ (?&json) # JSON object
+ )
+ )
+ )';
+ $regex = '/' . $defs . '\b
+ (?<k>[\w-]+) # Key
+ \b
+ (?:\s*
+ = # First sub-value
+ \s*
+ (?<v>
+ (?&value)
+ (?:\s*
+ , # Sub-vals 1..N
+ \s*
+ (?&value)
+ )*
+ )
+ )?
+ /x';
+ $valueregex = '/' . $defs . '(?&value)/x';
+
+ if ( preg_match_all( $regex, $instring, $matches, PREG_SET_ORDER ) ) {
+ foreach ( $matches as $bits ) {
+ $key = strtolower( $bits['k'] );
+ if ( !isset( $bits['v'] ) ) {
+ $opts[$key] = true;
+ } else {
+ preg_match_all( $valueregex, $bits['v'], $vmatches );
+ $opts[$key] = array_map( [ $this, 'cleanupOption' ], $vmatches[0] );
+ if ( count( $opts[$key] ) == 1 ) {
+ $opts[$key] = $opts[$key][0];
+ }
+ }
+ }
+ }
+ return $opts;
+ }
+
+ private function cleanupOption( $opt ) {
+ if ( substr( $opt, 0, 1 ) == '"' ) {
+ return stripcslashes( substr( $opt, 1, -1 ) );
+ }
+
+ if ( substr( $opt, 0, 2 ) == '[[' ) {
+ return substr( $opt, 2, -2 );
+ }
+
+ if ( substr( $opt, 0, 1 ) == '{' ) {
+ return FormatJson::decode( $opt, true );
+ }
+ return $opt;
+ }
+
+ /**
+ * Do any required setup which is dependent on test options.
+ *
+ * @see staticSetup() for more information about setup/teardown
+ *
+ * @param array $test Test info supplied by TestFileReader
+ * @param callable|null $nextTeardown
+ * @return ScopedCallback
+ */
+ public function perTestSetup( $test, $nextTeardown = null ) {
+ $teardown = [];
+
+ $this->checkSetupDone( 'setupDatabase', 'setDatabase' );
+ $teardown[] = $this->markSetupDone( 'perTestSetup' );
+
+ $opts = $this->parseOptions( $test['options'] );
+ $config = $test['config'];
+
+ // Find out values for some special options.
+ $langCode =
+ self::getOptionValue( 'language', $opts, 'en' );
+ $variant =
+ self::getOptionValue( 'variant', $opts, false );
+ $maxtoclevel =
+ self::getOptionValue( 'wgMaxTocLevel', $opts, 999 );
+ $linkHolderBatchSize =
+ self::getOptionValue( 'wgLinkHolderBatchSize', $opts, 1000 );
+
+ // Default to fallback skin, but allow it to be overridden
+ $skin = self::getOptionValue( 'skin', $opts, 'fallback' );
+
+ $setup = [
+ 'wgEnableUploads' => self::getOptionValue( 'wgEnableUploads', $opts, true ),
+ 'wgLanguageCode' => $langCode,
+ 'wgRawHtml' => self::getOptionValue( 'wgRawHtml', $opts, false ),
+ 'wgNamespacesWithSubpages' => array_fill_keys(
+ MWNamespace::getValidNamespaces(), isset( $opts['subpage'] )
+ ),
+ 'wgMaxTocLevel' => $maxtoclevel,
+ 'wgAllowExternalImages' => self::getOptionValue( 'wgAllowExternalImages', $opts, true ),
+ 'wgThumbLimits' => [ self::getOptionValue( 'thumbsize', $opts, 180 ) ],
+ 'wgDefaultLanguageVariant' => $variant,
+ 'wgLinkHolderBatchSize' => $linkHolderBatchSize,
+ // Set as a JSON object like:
+ // wgEnableMagicLinks={"ISBN":false, "PMID":false, "RFC":false}
+ 'wgEnableMagicLinks' => self::getOptionValue( 'wgEnableMagicLinks', $opts, [] )
+ + [ 'ISBN' => true, 'PMID' => true, 'RFC' => true ],
+ // Test with legacy encoding by default until HTML5 is very stable and default
+ 'wgFragmentMode' => [ 'legacy' ],
+ ];
+
+ if ( $config ) {
+ $configLines = explode( "\n", $config );
+
+ foreach ( $configLines as $line ) {
+ list( $var, $value ) = explode( '=', $line, 2 );
+ $setup[$var] = eval( "return $value;" );
+ }
+ }
+
+ /** @since 1.20 */
+ Hooks::run( 'ParserTestGlobals', [ &$setup ] );
+
+ // Create tidy driver
+ if ( isset( $opts['tidy'] ) ) {
+ // Cache a driver instance
+ if ( $this->tidyDriver === null ) {
+ $this->tidyDriver = MWTidy::factory( $this->tidySupport->getConfig() );
+ }
+ $tidy = $this->tidyDriver;
+ } else {
+ $tidy = false;
+ }
+ MWTidy::setInstance( $tidy );
+ $teardown[] = function () {
+ MWTidy::destroySingleton();
+ };
+
+ // Set content language. This invalidates the magic word cache and title services
+ $lang = Language::factory( $langCode );
+ $lang->resetNamespaces();
+ $setup['wgContLang'] = $lang;
+ $reset = function () {
+ MagicWord::clearCache();
+ $this->resetTitleServices();
+ };
+ $setup[] = $reset;
+ $teardown[] = $reset;
+
+ // Make a user object with the same language
+ $user = new User;
+ $user->setOption( 'language', $langCode );
+ $setup['wgLang'] = $lang;
+
+ // We (re)set $wgThumbLimits to a single-element array above.
+ $user->setOption( 'thumbsize', 0 );
+
+ $setup['wgUser'] = $user;
+
+ // And put both user and language into the context
+ $context = RequestContext::getMain();
+ $context->setUser( $user );
+ $context->setLanguage( $lang );
+ // And the skin!
+ $oldSkin = $context->getSkin();
+ $skinFactory = MediaWikiServices::getInstance()->getSkinFactory();
+ $context->setSkin( $skinFactory->makeSkin( $skin ) );
+ $context->setOutput( new OutputPage( $context ) );
+ $setup['wgOut'] = $context->getOutput();
+ $teardown[] = function () use ( $context, $oldSkin ) {
+ // Clear language conversion tables
+ $wrapper = TestingAccessWrapper::newFromObject(
+ $context->getLanguage()->getConverter()
+ );
+ $wrapper->reloadTables();
+ // Reset context to the restored globals
+ $context->setUser( $GLOBALS['wgUser'] );
+ $context->setLanguage( $GLOBALS['wgContLang'] );
+ $context->setSkin( $oldSkin );
+ $context->setOutput( $GLOBALS['wgOut'] );
+ };
+
+ $teardown[] = $this->executeSetupSnippets( $setup );
+
+ return $this->createTeardownObject( $teardown, $nextTeardown );
+ }
+
+ /**
+ * List of temporary tables to create, without prefix.
+ * Some of these probably aren't necessary.
+ * @return array
+ */
+ private function listTables() {
+ global $wgCommentTableSchemaMigrationStage, $wgActorTableSchemaMigrationStage;
+
+ $tables = [ 'user', 'user_properties', 'user_former_groups', 'page', 'page_restrictions',
+ 'protected_titles', 'revision', 'ip_changes', 'text', 'pagelinks', 'imagelinks',
+ 'categorylinks', 'templatelinks', 'externallinks', 'langlinks', 'iwlinks',
+ 'site_stats', 'ipblocks', 'image', 'oldimage',
+ 'recentchanges', 'watchlist', 'interwiki', 'logging', 'log_search',
+ 'querycache', 'objectcache', 'job', 'l10n_cache', 'redirect', 'querycachetwo',
+ 'archive', 'user_groups', 'page_props', 'category'
+ ];
+
+ if ( $wgCommentTableSchemaMigrationStage >= MIGRATION_WRITE_BOTH ) {
+ // The new tables for comments are in use
+ $tables[] = 'comment';
+ $tables[] = 'revision_comment_temp';
+ $tables[] = 'image_comment_temp';
+ }
+
+ if ( $wgActorTableSchemaMigrationStage >= MIGRATION_WRITE_BOTH ) {
+ // The new tables for actors are in use
+ $tables[] = 'actor';
+ $tables[] = 'revision_actor_temp';
+ }
+
+ if ( in_array( $this->db->getType(), [ 'mysql', 'sqlite', 'oracle' ] ) ) {
+ array_push( $tables, 'searchindex' );
+ }
+
+ // Allow extensions to add to the list of tables to duplicate;
+ // may be necessary if they hook into page save or other code
+ // which will require them while running tests.
+ Hooks::run( 'ParserTestTables', [ &$tables ] );
+
+ return $tables;
+ }
+
+ public function setDatabase( IDatabase $db ) {
+ $this->db = $db;
+ $this->setupDone['setDatabase'] = true;
+ }
+
+ /**
+ * Set up temporary DB tables.
+ *
+ * For best performance, call this once only for all tests. However, it can
+ * be called at the start of each test if more isolation is desired.
+ *
+ * @todo: This is basically an unrefactored copy of
+ * MediaWikiTestCase::setupAllTestDBs. They should be factored out somehow.
+ *
+ * Do not call this function from a MediaWikiTestCase subclass, since
+ * MediaWikiTestCase does its own DB setup. Instead use setDatabase().
+ *
+ * @see staticSetup() for more information about setup/teardown
+ *
+ * @param ScopedCallback|null $nextTeardown The next teardown object
+ * @return ScopedCallback The teardown object
+ */
+ public function setupDatabase( $nextTeardown = null ) {
+ global $wgDBprefix;
+
+ $this->db = wfGetDB( DB_MASTER );
+ $dbType = $this->db->getType();
+
+ if ( $dbType == 'oracle' ) {
+ $suspiciousPrefixes = [ 'pt_', MediaWikiTestCase::ORA_DB_PREFIX ];
+ } else {
+ $suspiciousPrefixes = [ 'parsertest_', MediaWikiTestCase::DB_PREFIX ];
+ }
+ if ( in_array( $wgDBprefix, $suspiciousPrefixes ) ) {
+ throw new MWException( "\$wgDBprefix=$wgDBprefix suggests DB setup is already done" );
+ }
+
+ $teardown = [];
+
+ $teardown[] = $this->markSetupDone( 'setupDatabase' );
+
+ # CREATE TEMPORARY TABLE breaks if there is more than one server
+ if ( wfGetLB()->getServerCount() != 1 ) {
+ $this->useTemporaryTables = false;
+ }
+
+ $temporary = $this->useTemporaryTables || $dbType == 'postgres';
+ $prefix = $dbType != 'oracle' ? 'parsertest_' : 'pt_';
+
+ $this->dbClone = new CloneDatabase( $this->db, $this->listTables(), $prefix );
+ $this->dbClone->useTemporaryTables( $temporary );
+ $this->dbClone->cloneTableStructure();
+
+ if ( $dbType == 'oracle' ) {
+ $this->db->query( 'BEGIN FILL_WIKI_INFO; END;' );
+ # Insert 0 user to prevent FK violations
+
+ # Anonymous user
+ $this->db->insert( 'user', [
+ 'user_id' => 0,
+ 'user_name' => 'Anonymous' ] );
+ }
+
+ $teardown[] = function () {
+ $this->teardownDatabase();
+ };
+
+ // Wipe some DB query result caches on setup and teardown
+ $reset = function () {
+ LinkCache::singleton()->clear();
+
+ // Clear the message cache
+ MessageCache::singleton()->clear();
+ };
+ $reset();
+ $teardown[] = $reset;
+ return $this->createTeardownObject( $teardown, $nextTeardown );
+ }
+
+ /**
+ * Add data about uploads to the new test DB, and set up the upload
+ * directory. This should be called after either setDatabase() or
+ * setupDatabase().
+ *
+ * @param ScopedCallback|null $nextTeardown The next teardown object
+ * @return ScopedCallback The teardown object
+ */
+ public function setupUploads( $nextTeardown = null ) {
+ $teardown = [];
+
+ $this->checkSetupDone( 'setupDatabase', 'setDatabase' );
+ $teardown[] = $this->markSetupDone( 'setupUploads' );
+
+ // Create the files in the upload directory (or pretend to create them
+ // in a MockFileBackend). Append teardown callback.
+ $teardown[] = $this->setupUploadBackend();
+
+ // Create a user
+ $user = User::createNew( 'WikiSysop' );
+
+ // Register the uploads in the database
+
+ $image = wfLocalFile( Title::makeTitle( NS_FILE, 'Foobar.jpg' ) );
+ # note that the size/width/height/bits/etc of the file
+ # are actually set by inspecting the file itself; the arguments
+ # to recordUpload2 have no effect. That said, we try to make things
+ # match up so it is less confusing to readers of the code & tests.
+ $image->recordUpload2( '', 'Upload of some lame file', 'Some lame file', [
+ 'size' => 7881,
+ 'width' => 1941,
+ 'height' => 220,
+ 'bits' => 8,
+ 'media_type' => MEDIATYPE_BITMAP,
+ 'mime' => 'image/jpeg',
+ 'metadata' => serialize( [] ),
+ 'sha1' => Wikimedia\base_convert( '1', 16, 36, 31 ),
+ 'fileExists' => true
+ ], $this->db->timestamp( '20010115123500' ), $user );
+
+ $image = wfLocalFile( Title::makeTitle( NS_FILE, 'Thumb.png' ) );
+ # again, note that size/width/height below are ignored; see above.
+ $image->recordUpload2( '', 'Upload of some lame thumbnail', 'Some lame thumbnail', [
+ 'size' => 22589,
+ 'width' => 135,
+ 'height' => 135,
+ 'bits' => 8,
+ 'media_type' => MEDIATYPE_BITMAP,
+ 'mime' => 'image/png',
+ 'metadata' => serialize( [] ),
+ 'sha1' => Wikimedia\base_convert( '2', 16, 36, 31 ),
+ 'fileExists' => true
+ ], $this->db->timestamp( '20130225203040' ), $user );
+
+ $image = wfLocalFile( Title::makeTitle( NS_FILE, 'Foobar.svg' ) );
+ $image->recordUpload2( '', 'Upload of some lame SVG', 'Some lame SVG', [
+ 'size' => 12345,
+ 'width' => 240,
+ 'height' => 180,
+ 'bits' => 0,
+ 'media_type' => MEDIATYPE_DRAWING,
+ 'mime' => 'image/svg+xml',
+ 'metadata' => serialize( [] ),
+ 'sha1' => Wikimedia\base_convert( '', 16, 36, 31 ),
+ 'fileExists' => true
+ ], $this->db->timestamp( '20010115123500' ), $user );
+
+ # This image will be blacklisted in [[MediaWiki:Bad image list]]
+ $image = wfLocalFile( Title::makeTitle( NS_FILE, 'Bad.jpg' ) );
+ $image->recordUpload2( '', 'zomgnotcensored', 'Borderline image', [
+ 'size' => 12345,
+ 'width' => 320,
+ 'height' => 240,
+ 'bits' => 24,
+ 'media_type' => MEDIATYPE_BITMAP,
+ 'mime' => 'image/jpeg',
+ 'metadata' => serialize( [] ),
+ 'sha1' => Wikimedia\base_convert( '3', 16, 36, 31 ),
+ 'fileExists' => true
+ ], $this->db->timestamp( '20010115123500' ), $user );
+
+ $image = wfLocalFile( Title::makeTitle( NS_FILE, 'Video.ogv' ) );
+ $image->recordUpload2( '', 'A pretty movie', 'Will it play', [
+ 'size' => 12345,
+ 'width' => 320,
+ 'height' => 240,
+ 'bits' => 0,
+ 'media_type' => MEDIATYPE_VIDEO,
+ 'mime' => 'application/ogg',
+ 'metadata' => serialize( [] ),
+ 'sha1' => Wikimedia\base_convert( '', 16, 36, 31 ),
+ 'fileExists' => true
+ ], $this->db->timestamp( '20010115123500' ), $user );
+
+ $image = wfLocalFile( Title::makeTitle( NS_FILE, 'Audio.oga' ) );
+ $image->recordUpload2( '', 'An awesome hitsong', 'Will it play', [
+ 'size' => 12345,
+ 'width' => 0,
+ 'height' => 0,
+ 'bits' => 0,
+ 'media_type' => MEDIATYPE_AUDIO,
+ 'mime' => 'application/ogg',
+ 'metadata' => serialize( [] ),
+ 'sha1' => Wikimedia\base_convert( '', 16, 36, 31 ),
+ 'fileExists' => true
+ ], $this->db->timestamp( '20010115123500' ), $user );
+
+ # A DjVu file
+ $image = wfLocalFile( Title::makeTitle( NS_FILE, 'LoremIpsum.djvu' ) );
+ $image->recordUpload2( '', 'Upload a DjVu', 'A DjVu', [
+ 'size' => 3249,
+ 'width' => 2480,
+ 'height' => 3508,
+ 'bits' => 0,
+ 'media_type' => MEDIATYPE_BITMAP,
+ 'mime' => 'image/vnd.djvu',
+ 'metadata' => '<?xml version="1.0" ?>
+<!DOCTYPE DjVuXML PUBLIC "-//W3C//DTD DjVuXML 1.1//EN" "pubtext/DjVuXML-s.dtd">
+<DjVuXML>
+<HEAD></HEAD>
+<BODY><OBJECT height="3508" width="2480">
+<PARAM name="DPI" value="300" />
+<PARAM name="GAMMA" value="2.2" />
+</OBJECT>
+<OBJECT height="3508" width="2480">
+<PARAM name="DPI" value="300" />
+<PARAM name="GAMMA" value="2.2" />
+</OBJECT>
+<OBJECT height="3508" width="2480">
+<PARAM name="DPI" value="300" />
+<PARAM name="GAMMA" value="2.2" />
+</OBJECT>
+<OBJECT height="3508" width="2480">
+<PARAM name="DPI" value="300" />
+<PARAM name="GAMMA" value="2.2" />
+</OBJECT>
+<OBJECT height="3508" width="2480">
+<PARAM name="DPI" value="300" />
+<PARAM name="GAMMA" value="2.2" />
+</OBJECT>
+</BODY>
+</DjVuXML>',
+ 'sha1' => Wikimedia\base_convert( '', 16, 36, 31 ),
+ 'fileExists' => true
+ ], $this->db->timestamp( '20010115123600' ), $user );
+
+ return $this->createTeardownObject( $teardown, $nextTeardown );
+ }
+
+ /**
+ * Helper for database teardown, called from the teardown closure. Destroy
+ * the database clone and fix up some things that CloneDatabase doesn't fix.
+ *
+ * @todo Move most things here to CloneDatabase
+ */
+ private function teardownDatabase() {
+ $this->checkSetupDone( 'setupDatabase' );
+
+ $this->dbClone->destroy();
+ $this->databaseSetupDone = false;
+
+ if ( $this->useTemporaryTables ) {
+ if ( $this->db->getType() == 'sqlite' ) {
+ # Under SQLite the searchindex table is virtual and need
+ # to be explicitly destroyed. See T31912
+ # See also MediaWikiTestCase::destroyDB()
+ wfDebug( __METHOD__ . " explicitly destroying sqlite virtual table parsertest_searchindex\n" );
+ $this->db->query( "DROP TABLE `parsertest_searchindex`" );
+ }
+ # Don't need to do anything
+ return;
+ }
+
+ $tables = $this->listTables();
+
+ foreach ( $tables as $table ) {
+ if ( $this->db->getType() == 'oracle' ) {
+ $this->db->query( "DROP TABLE pt_$table DROP CONSTRAINTS" );
+ } else {
+ $this->db->query( "DROP TABLE `parsertest_$table`" );
+ }
+ }
+
+ if ( $this->db->getType() == 'oracle' ) {
+ $this->db->query( 'BEGIN FILL_WIKI_INFO; END;' );
+ }
+ }
+
+ /**
+ * Upload test files to the backend created by createRepoGroup().
+ *
+ * @return callable The teardown callback
+ */
+ private function setupUploadBackend() {
+ global $IP;
+
+ $repo = RepoGroup::singleton()->getLocalRepo();
+ $base = $repo->getZonePath( 'public' );
+ $backend = $repo->getBackend();
+ $backend->prepare( [ 'dir' => "$base/3/3a" ] );
+ $backend->store( [
+ 'src' => "$IP/tests/phpunit/data/parser/headbg.jpg",
+ 'dst' => "$base/3/3a/Foobar.jpg"
+ ] );
+ $backend->prepare( [ 'dir' => "$base/e/ea" ] );
+ $backend->store( [
+ 'src' => "$IP/tests/phpunit/data/parser/wiki.png",
+ 'dst' => "$base/e/ea/Thumb.png"
+ ] );
+ $backend->prepare( [ 'dir' => "$base/0/09" ] );
+ $backend->store( [
+ 'src' => "$IP/tests/phpunit/data/parser/headbg.jpg",
+ 'dst' => "$base/0/09/Bad.jpg"
+ ] );
+ $backend->prepare( [ 'dir' => "$base/5/5f" ] );
+ $backend->store( [
+ 'src' => "$IP/tests/phpunit/data/parser/LoremIpsum.djvu",
+ 'dst' => "$base/5/5f/LoremIpsum.djvu"
+ ] );
+
+ // No helpful SVG file to copy, so make one ourselves
+ $data = '<?xml version="1.0" encoding="utf-8"?>' .
+ '<svg xmlns="http://www.w3.org/2000/svg"' .
+ ' version="1.1" width="240" height="180"/>';
+
+ $backend->prepare( [ 'dir' => "$base/f/ff" ] );
+ $backend->quickCreate( [
+ 'content' => $data, 'dst' => "$base/f/ff/Foobar.svg"
+ ] );
+
+ return function () use ( $backend ) {
+ if ( $backend instanceof MockFileBackend ) {
+ // In memory backend, so dont bother cleaning them up.
+ return;
+ }
+ $this->teardownUploadBackend();
+ };
+ }
+
+ /**
+ * Remove the dummy uploads directory
+ */
+ private function teardownUploadBackend() {
+ if ( $this->keepUploads ) {
+ return;
+ }
+
+ $repo = RepoGroup::singleton()->getLocalRepo();
+ $public = $repo->getZonePath( 'public' );
+
+ $this->deleteFiles(
+ [
+ "$public/3/3a/Foobar.jpg",
+ "$public/e/ea/Thumb.png",
+ "$public/0/09/Bad.jpg",
+ "$public/5/5f/LoremIpsum.djvu",
+ "$public/f/ff/Foobar.svg",
+ "$public/0/00/Video.ogv",
+ "$public/4/41/Audio.oga",
+ ]
+ );
+ }
+
+ /**
+ * Delete the specified files and their parent directories
+ * @param array $files File backend URIs mwstore://...
+ */
+ private function deleteFiles( $files ) {
+ // Delete the files
+ $backend = RepoGroup::singleton()->getLocalRepo()->getBackend();
+ foreach ( $files as $file ) {
+ $backend->delete( [ 'src' => $file ], [ 'force' => 1 ] );
+ }
+
+ // Delete the parent directories
+ foreach ( $files as $file ) {
+ $tmp = FileBackend::parentStoragePath( $file );
+ while ( $tmp ) {
+ if ( !$backend->clean( [ 'dir' => $tmp ] )->isOK() ) {
+ break;
+ }
+ $tmp = FileBackend::parentStoragePath( $tmp );
+ }
+ }
+ }
+
+ /**
+ * Add articles to the test DB.
+ *
+ * @param array $articles Article info array from TestFileReader
+ */
+ public function addArticles( $articles ) {
+ global $wgContLang;
+ $setup = [];
+ $teardown = [];
+
+ // Be sure ParserTestRunner::addArticle has correct language set,
+ // so that system messages get into the right language cache
+ if ( $wgContLang->getCode() !== 'en' ) {
+ $setup['wgLanguageCode'] = 'en';
+ $setup['wgContLang'] = Language::factory( 'en' );
+ }
+
+ // Add special namespaces, in case that hasn't been done by staticSetup() yet
+ $this->appendNamespaceSetup( $setup, $teardown );
+
+ // wgCapitalLinks obviously needs initialisation
+ $setup['wgCapitalLinks'] = true;
+
+ $teardown[] = $this->executeSetupSnippets( $setup );
+
+ foreach ( $articles as $info ) {
+ $this->addArticle( $info['name'], $info['text'], $info['file'], $info['line'] );
+ }
+
+ // Wipe WANObjectCache process cache, which is invalidated by article insertion
+ // due to T144706
+ ObjectCache::getMainWANInstance()->clearProcessCache();
+
+ $this->executeSetupSnippets( $teardown );
+ }
+
+ /**
+ * Insert a temporary test article
+ * @param string $name The title, including any prefix
+ * @param string $text The article text
+ * @param string $file The input file name
+ * @param int|string $line The input line number, for reporting errors
+ * @throws Exception
+ * @throws MWException
+ */
+ private function addArticle( $name, $text, $file, $line ) {
+ $text = self::chomp( $text );
+ $name = self::chomp( $name );
+
+ $title = Title::newFromText( $name );
+ wfDebug( __METHOD__ . ": adding $name" );
+
+ if ( is_null( $title ) ) {
+ throw new MWException( "invalid title '$name' at $file:$line\n" );
+ }
+
+ $newContent = ContentHandler::makeContent( $text, $title );
+
+ $page = WikiPage::factory( $title );
+ $page->loadPageData( 'fromdbmaster' );
+
+ if ( $page->exists() ) {
+ $content = $page->getContent( Revision::RAW );
+ // Only reject the title, if the content/content model is different.
+ // This makes it easier to create Template:(( or Template:)) in different extensions
+ if ( $newContent->equals( $content ) ) {
+ return;
+ }
+ throw new MWException(
+ "duplicate article '$name' with different content at $file:$line\n"
+ );
+ }
+
+ // Use mock parser, to make debugging of actual parser tests simpler.
+ // But initialise the MessageCache clone first, don't let MessageCache
+ // get a reference to the mock object.
+ MessageCache::singleton()->getParser();
+ $restore = $this->executeSetupSnippets( [ 'wgParser' => new ParserTestMockParser ] );
+ try {
+ $status = $page->doEditContent(
+ $newContent,
+ '',
+ EDIT_NEW | EDIT_INTERNAL
+ );
+ } finally {
+ $restore();
+ }
+
+ if ( !$status->isOK() ) {
+ throw new MWException( $status->getWikiText( false, false, 'en' ) );
+ }
+
+ // The RepoGroup cache is invalidated by the creation of file redirects
+ if ( $title->inNamespace( NS_FILE ) ) {
+ RepoGroup::singleton()->clearCache( $title );
+ }
+ }
+
+ /**
+ * Check if a hook is installed
+ *
+ * @param string $name
+ * @return bool True if tag hook is present
+ */
+ public function requireHook( $name ) {
+ global $wgParser;
+
+ $wgParser->firstCallInit(); // make sure hooks are loaded.
+ if ( isset( $wgParser->mTagHooks[$name] ) ) {
+ return true;
+ } else {
+ $this->recorder->warning( " This test suite requires the '$name' hook " .
+ "extension, skipping." );
+ return false;
+ }
+ }
+
+ /**
+ * Check if a function hook is installed
+ *
+ * @param string $name
+ * @return bool True if function hook is present
+ */
+ public function requireFunctionHook( $name ) {
+ global $wgParser;
+
+ $wgParser->firstCallInit(); // make sure hooks are loaded.
+
+ if ( isset( $wgParser->mFunctionHooks[$name] ) ) {
+ return true;
+ } else {
+ $this->recorder->warning( " This test suite requires the '$name' function " .
+ "hook extension, skipping." );
+ return false;
+ }
+ }
+
+ /**
+ * Check if a transparent tag hook is installed
+ *
+ * @param string $name
+ * @return bool True if function hook is present
+ */
+ public function requireTransparentHook( $name ) {
+ global $wgParser;
+
+ $wgParser->firstCallInit(); // make sure hooks are loaded.
+
+ if ( isset( $wgParser->mTransparentTagHooks[$name] ) ) {
+ return true;
+ } else {
+ $this->recorder->warning( " This test suite requires the '$name' transparent " .
+ "hook extension, skipping.\n" );
+ return false;
+ }
+ }
+
+ /**
+ * Fake constant timestamp to make sure time-related parser
+ * functions give a persistent value.
+ *
+ * - Parser::getVariableValue (via ParserGetVariableValueTs hook)
+ * - Parser::preSaveTransform (via ParserOptions)
+ */
+ private function getFakeTimestamp() {
+ // parsed as '1970-01-01T00:02:03Z'
+ return 123;
+ }
+}
diff --git a/www/wiki/tests/parser/PhpunitTestRecorder.php b/www/wiki/tests/parser/PhpunitTestRecorder.php
new file mode 100644
index 00000000..1a2cfc91
--- /dev/null
+++ b/www/wiki/tests/parser/PhpunitTestRecorder.php
@@ -0,0 +1,18 @@
+<?php
+
+class PhpunitTestRecorder extends TestRecorder {
+ private $testCase;
+
+ public function setTestCase( PHPUnit\Framework\TestCase $testCase ) {
+ $this->testCase = $testCase;
+ }
+
+ /**
+ * Mark a test skipped
+ * @param string $test
+ * @param string $reason
+ */
+ public function skipped( $test, $reason ) {
+ $this->testCase->markTestSkipped( "SKIPPED: $reason" );
+ }
+}
diff --git a/www/wiki/tests/parser/README b/www/wiki/tests/parser/README
new file mode 100644
index 00000000..a62db5ac
--- /dev/null
+++ b/www/wiki/tests/parser/README
@@ -0,0 +1,12 @@
+Parser tests can be run either via PHPUnit or by using the standalone
+parserTests.php in this directory. The standalone version provides more
+options.
+
+To run parser tests via PHPUnit:
+
+ $ cd tests/phpunit
+ ./phpunit.php --testsuite parsertests
+
+You can optionally filter by title using --filter, e.g.
+
+ ./phpunit.php --testsuite parsertests --filter="T6400"
diff --git a/www/wiki/tests/parser/TestFileEditor.php b/www/wiki/tests/parser/TestFileEditor.php
new file mode 100644
index 00000000..1bee31ea
--- /dev/null
+++ b/www/wiki/tests/parser/TestFileEditor.php
@@ -0,0 +1,196 @@
+<?php
+
+class TestFileEditor {
+ private $lines;
+ private $numLines;
+ private $deletions;
+ private $changes;
+ private $pos;
+ private $warningCallback;
+ private $result;
+
+ public static function edit( $text, array $deletions, array $changes, $warningCallback = null ) {
+ $editor = new self( $text, $deletions, $changes, $warningCallback );
+ $editor->execute();
+ return $editor->result;
+ }
+
+ private function __construct( $text, array $deletions, array $changes, $warningCallback ) {
+ $this->lines = explode( "\n", $text );
+ $this->numLines = count( $this->lines );
+ $this->deletions = array_flip( $deletions );
+ $this->changes = $changes;
+ $this->pos = 0;
+ $this->warningCallback = $warningCallback;
+ $this->result = '';
+ }
+
+ private function execute() {
+ while ( $this->pos < $this->numLines ) {
+ $line = $this->lines[$this->pos];
+ switch ( $this->getHeading( $line ) ) {
+ case 'test':
+ $this->parseTest();
+ break;
+ case 'hooks':
+ case 'functionhooks':
+ case 'transparenthooks':
+ $this->parseHooks();
+ break;
+ default:
+ if ( $this->pos < $this->numLines - 1 ) {
+ $line .= "\n";
+ }
+ $this->emitComment( $line );
+ $this->pos++;
+ }
+ }
+ foreach ( $this->deletions as $deletion => $unused ) {
+ $this->warning( "Could not find test \"$deletion\" to delete it" );
+ }
+ foreach ( $this->changes as $test => $sectionChanges ) {
+ foreach ( $sectionChanges as $section => $change ) {
+ $this->warning( "Could not find section \"$section\" in test \"$test\" " .
+ "to {$change['op']} it" );
+ }
+ }
+ }
+
+ private function warning( $text ) {
+ $cb = $this->warningCallback;
+ if ( $cb ) {
+ $cb( $text );
+ }
+ }
+
+ private function getHeading( $line ) {
+ if ( preg_match( '/^!!\s*(\S+)/', $line, $m ) ) {
+ return $m[1];
+ } else {
+ return false;
+ }
+ }
+
+ private function parseTest() {
+ $test = [];
+ $line = $this->lines[$this->pos++];
+ $heading = $this->getHeading( $line );
+ $section = [
+ 'name' => $heading,
+ 'headingLine' => $line,
+ 'contents' => ''
+ ];
+
+ while ( $this->pos < $this->numLines ) {
+ $line = $this->lines[$this->pos++];
+ $nextHeading = $this->getHeading( $line );
+ if ( $nextHeading === 'end' ) {
+ $test[] = $section;
+
+ // Add trailing line breaks to the "end" section, to allow for neat deletions
+ $trail = '';
+ for ( $i = 0; $i < $this->numLines - $this->pos - 1; $i++ ) {
+ if ( $this->lines[$this->pos + $i] === '' ) {
+ $trail .= "\n";
+ } else {
+ break;
+ }
+ }
+ $this->pos += strlen( $trail );
+
+ $test[] = [
+ 'name' => 'end',
+ 'headingLine' => $line,
+ 'contents' => $trail
+ ];
+ $this->emitTest( $test );
+ return;
+ } elseif ( $nextHeading !== false ) {
+ $test[] = $section;
+ $heading = $nextHeading;
+ $section = [
+ 'name' => $heading,
+ 'headingLine' => $line,
+ 'contents' => ''
+ ];
+ } else {
+ $section['contents'] .= "$line\n";
+ }
+ }
+
+ throw new Exception( 'Unexpected end of file' );
+ }
+
+ private function parseHooks() {
+ $line = $this->lines[$this->pos++];
+ $heading = $this->getHeading( $line );
+ $expectedEnd = 'end' . $heading;
+ $contents = "$line\n";
+
+ do {
+ $line = $this->lines[$this->pos++];
+ $nextHeading = $this->getHeading( $line );
+ $contents .= "$line\n";
+ } while ( $this->pos < $this->numLines && $nextHeading !== $expectedEnd );
+
+ if ( $nextHeading !== $expectedEnd ) {
+ throw new Exception( 'Unexpected end of file' );
+ }
+ $this->emitHooks( $heading, $contents );
+ }
+
+ protected function emitComment( $contents ) {
+ $this->result .= $contents;
+ }
+
+ protected function emitTest( $test ) {
+ $testName = false;
+ foreach ( $test as $section ) {
+ if ( $section['name'] === 'test' ) {
+ $testName = rtrim( $section['contents'], "\n" );
+ }
+ }
+ if ( isset( $this->deletions[$testName] ) ) {
+ // Acknowledge deletion
+ unset( $this->deletions[$testName] );
+ return;
+ }
+ if ( isset( $this->changes[$testName] ) ) {
+ $changes =& $this->changes[$testName];
+ foreach ( $test as $i => $section ) {
+ $sectionName = $section['name'];
+ if ( isset( $changes[$sectionName] ) ) {
+ $change = $changes[$sectionName];
+ switch ( $change['op'] ) {
+ case 'rename':
+ $test[$i]['name'] = $change['value'];
+ $test[$i]['headingLine'] = "!! {$change['value']}";
+ break;
+ case 'update':
+ $test[$i]['contents'] = $change['value'];
+ break;
+ case 'delete':
+ $test[$i]['deleted'] = true;
+ break;
+ default:
+ throw new Exception( "Unknown op: ${change['op']}" );
+ }
+ // Acknowledge
+ // Note that we use the old section name for the rename op
+ unset( $changes[$sectionName] );
+ }
+ }
+ }
+ foreach ( $test as $section ) {
+ if ( isset( $section['deleted'] ) ) {
+ continue;
+ }
+ $this->result .= $section['headingLine'] . "\n";
+ $this->result .= $section['contents'];
+ }
+ }
+
+ protected function emitHooks( $heading, $contents ) {
+ $this->result .= $contents;
+ }
+}
diff --git a/www/wiki/tests/parser/TestFileReader.php b/www/wiki/tests/parser/TestFileReader.php
new file mode 100644
index 00000000..a96485d4
--- /dev/null
+++ b/www/wiki/tests/parser/TestFileReader.php
@@ -0,0 +1,335 @@
+<?php
+/**
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ * @ingroup Testing
+ */
+
+class TestFileReader {
+ private $file;
+ private $fh;
+ private $section = null;
+ /** String|null: current test section being analyzed */
+ private $sectionData = [];
+ private $sectionLineNum = [];
+ private $lineNum = 0;
+ private $runDisabled;
+ private $runParsoid;
+ private $regex;
+
+ private $articles = [];
+ private $requirements = [];
+ private $tests = [];
+
+ public static function read( $file, array $options = [] ) {
+ $reader = new self( $file, $options );
+ $reader->execute();
+
+ $requirements = [];
+ foreach ( $reader->requirements as $type => $reqsOfType ) {
+ foreach ( $reqsOfType as $name => $unused ) {
+ $requirements[] = [
+ 'type' => $type,
+ 'name' => $name
+ ];
+ }
+ }
+
+ return [
+ 'requirements' => $requirements,
+ 'tests' => $reader->tests,
+ 'articles' => $reader->articles
+ ];
+ }
+
+ private function __construct( $file, $options ) {
+ $this->file = $file;
+ $this->fh = fopen( $this->file, "rt" );
+
+ if ( !$this->fh ) {
+ throw new MWException( "Couldn't open file '$file'\n" );
+ }
+
+ $options = $options + [
+ 'runDisabled' => false,
+ 'runParsoid' => false,
+ 'regex' => '//',
+ ];
+ $this->runDisabled = $options['runDisabled'];
+ $this->runParsoid = $options['runParsoid'];
+ $this->regex = $options['regex'];
+ }
+
+ private function addCurrentTest() {
+ // "input" and "result" are old section names allowed
+ // for backwards-compatibility.
+ $input = $this->checkSection( [ 'wikitext', 'input' ], false );
+ $nonTidySection = $this->checkSection(
+ [ 'html/php', 'html/*', 'html', 'result' ], false );
+ // Some tests have "with tidy" and "without tidy" variants
+ $tidySection = $this->checkSection( [ 'html/php+tidy', 'html+tidy' ], false );
+
+ // Remove trailing newline
+ $data = array_map( 'ParserTestRunner::chomp', $this->sectionData );
+
+ // Apply defaults
+ $data += [
+ 'options' => '',
+ 'config' => ''
+ ];
+
+ if ( $input === false ) {
+ throw new MWException( "Test at {$this->file}:{$this->sectionLineNum['test']} " .
+ "lacks input section" );
+ }
+
+ if ( preg_match( '/\\bdisabled\\b/i', $data['options'] ) && !$this->runDisabled ) {
+ // Disabled
+ return;
+ }
+
+ if ( $tidySection === false && $nonTidySection === false ) {
+ if ( isset( $data['html/parsoid'] ) || isset( $data['wikitext/edited'] ) ) {
+ // Parsoid only
+ return;
+ } else {
+ throw new MWException( "Test at {$this->file}:{$this->sectionLineNum['test']} " .
+ "lacks result section" );
+ }
+ }
+
+ if ( preg_match( '/\\bparsoid\\b/i', $data['options'] ) && $nonTidySection === 'html'
+ && !$this->runParsoid
+ ) {
+ // A test which normally runs on Parsoid but can optionally be run with MW
+ return;
+ }
+
+ if ( !preg_match( $this->regex, $data['test'] ) ) {
+ // Filtered test
+ return;
+ }
+
+ $commonInfo = [
+ 'test' => $data['test'],
+ 'desc' => $data['test'],
+ 'input' => $data[$input],
+ 'options' => $data['options'],
+ 'config' => $data['config'],
+ 'line' => $this->sectionLineNum['test'],
+ 'file' => $this->file
+ ];
+
+ if ( $nonTidySection !== false ) {
+ // Add non-tidy test
+ $this->tests[] = [
+ 'result' => $data[$nonTidySection],
+ 'resultSection' => $nonTidySection
+ ] + $commonInfo;
+
+ if ( $tidySection !== false ) {
+ // Add tidy subtest
+ $this->tests[] = [
+ 'desc' => $data['test'] . ' (with tidy)',
+ 'result' => $data[$tidySection],
+ 'resultSection' => $tidySection,
+ 'options' => $data['options'] . ' tidy',
+ 'isSubtest' => true,
+ ] + $commonInfo;
+ }
+ } elseif ( $tidySection !== false ) {
+ // No need to override desc when there is no subtest
+ $this->tests[] = [
+ 'result' => $data[$tidySection],
+ 'resultSection' => $tidySection,
+ 'options' => $data['options'] . ' tidy'
+ ] + $commonInfo;
+ } else {
+ throw new MWException( "Test at {$this->file}:{$this->sectionLineNum['test']} " .
+ "lacks result section" );
+ }
+ }
+
+ private function execute() {
+ while ( false !== ( $line = fgets( $this->fh ) ) ) {
+ $this->lineNum++;
+ $matches = [];
+
+ if ( preg_match( '/^!!\s*(\S+)/', $line, $matches ) ) {
+ $this->section = strtolower( $matches[1] );
+
+ if ( $this->section == 'endarticle' ) {
+ $this->checkSection( 'text' );
+ $this->checkSection( 'article' );
+
+ $this->addArticle(
+ ParserTestRunner::chomp( $this->sectionData['article'] ),
+ $this->sectionData['text'], $this->lineNum );
+
+ $this->clearSection();
+
+ continue;
+ }
+
+ if ( $this->section == 'endhooks' ) {
+ $this->checkSection( 'hooks' );
+
+ foreach ( explode( "\n", $this->sectionData['hooks'] ) as $line ) {
+ $line = trim( $line );
+
+ if ( $line ) {
+ $this->addRequirement( 'hook', $line );
+ }
+ }
+
+ $this->clearSection();
+
+ continue;
+ }
+
+ if ( $this->section == 'endfunctionhooks' ) {
+ $this->checkSection( 'functionhooks' );
+
+ foreach ( explode( "\n", $this->sectionData['functionhooks'] ) as $line ) {
+ $line = trim( $line );
+
+ if ( $line ) {
+ $this->addRequirement( 'functionHook', $line );
+ }
+ }
+
+ $this->clearSection();
+
+ continue;
+ }
+
+ if ( $this->section == 'endtransparenthooks' ) {
+ $this->checkSection( 'transparenthooks' );
+
+ foreach ( explode( "\n", $this->sectionData['transparenthooks'] ) as $line ) {
+ $line = trim( $line );
+
+ if ( $line ) {
+ $this->addRequirement( 'transparentHook', $line );
+ }
+ }
+
+ $this->clearSection();
+
+ continue;
+ }
+
+ if ( $this->section == 'end' ) {
+ $this->checkSection( 'test' );
+ $this->addCurrentTest();
+ $this->clearSection();
+ continue;
+ }
+
+ if ( isset( $this->sectionData[$this->section] ) ) {
+ throw new MWException( "duplicate section '$this->section' "
+ . "at line {$this->lineNum} of $this->file\n" );
+ }
+
+ $this->sectionLineNum[$this->section] = $this->lineNum;
+ $this->sectionData[$this->section] = '';
+
+ continue;
+ }
+
+ if ( $this->section ) {
+ $this->sectionData[$this->section] .= $line;
+ }
+ }
+ }
+
+ /**
+ * Clear section name and its data
+ */
+ private function clearSection() {
+ $this->sectionLineNum = [];
+ $this->sectionData = [];
+ $this->section = null;
+ }
+
+ /**
+ * Verify the current section data has some value for the given token
+ * name(s) (first parameter).
+ * Throw an exception if it is not set, referencing current section
+ * and adding the current file name and line number
+ *
+ * @param string|array $tokens Expected token(s) that should have been
+ * mentioned before closing this section
+ * @param bool $fatal True iff an exception should be thrown if
+ * the section is not found.
+ * @return bool|string
+ * @throws MWException
+ */
+ private function checkSection( $tokens, $fatal = true ) {
+ if ( is_null( $this->section ) ) {
+ throw new MWException( __METHOD__ . " can not verify a null section!\n" );
+ }
+ if ( !is_array( $tokens ) ) {
+ $tokens = [ $tokens ];
+ }
+ if ( count( $tokens ) == 0 ) {
+ throw new MWException( __METHOD__ . " can not verify zero sections!\n" );
+ }
+
+ $data = $this->sectionData;
+ $tokens = array_filter( $tokens, function ( $token ) use ( $data ) {
+ return isset( $data[$token] );
+ } );
+
+ if ( count( $tokens ) == 0 ) {
+ if ( !$fatal ) {
+ return false;
+ }
+ throw new MWException( sprintf(
+ "'%s' without '%s' at line %s of %s\n",
+ $this->section,
+ implode( ',', $tokens ),
+ $this->lineNum,
+ $this->file
+ ) );
+ }
+ if ( count( $tokens ) > 1 ) {
+ throw new MWException( sprintf(
+ "'%s' with unexpected tokens '%s' at line %s of %s\n",
+ $this->section,
+ implode( ',', $tokens ),
+ $this->lineNum,
+ $this->file
+ ) );
+ }
+
+ return array_values( $tokens )[0];
+ }
+
+ private function addArticle( $name, $text, $line ) {
+ $this->articles[] = [
+ 'name' => $name,
+ 'text' => $text,
+ 'line' => $line,
+ 'file' => $this->file
+ ];
+ }
+
+ private function addRequirement( $type, $name ) {
+ $this->requirements[$type][$name] = true;
+ }
+}
diff --git a/www/wiki/tests/parser/TestRecorder.php b/www/wiki/tests/parser/TestRecorder.php
new file mode 100644
index 00000000..2731c4c5
--- /dev/null
+++ b/www/wiki/tests/parser/TestRecorder.php
@@ -0,0 +1,93 @@
+<?php
+/**
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ * @ingroup Testing
+ */
+
+/**
+ * Interface to record parser test results.
+ *
+ * The TestRecorder is an class hierarchy to record the result of
+ * MediaWiki parser tests. One should call start() before running the
+ * full parser tests and end() once all the tests have been finished.
+ * After each test, you should use record() to keep track of your tests
+ * results. Finally, report() is used to generate a summary of your
+ * test run, one could dump it to the console for human consumption or
+ * register the result in a database for tracking purposes.
+ *
+ * @since 1.22
+ */
+class TestRecorder {
+
+ /**
+ * Called at beginning of the parser test run
+ */
+ public function start() {
+ }
+
+ /**
+ * Called before starting a test
+ */
+ public function startTest( $test ) {
+ }
+
+ /**
+ * Called before starting an input file
+ */
+ public function startSuite( $path ) {
+ }
+
+ /**
+ * Called after ending an input file
+ */
+ public function endSuite( $path ) {
+ }
+
+ /**
+ * Called after each test
+ * @param array $test
+ * @param ParserTestResult $result
+ */
+ public function record( $test, ParserTestResult $result ) {
+ }
+
+ /**
+ * Show a warning to the user
+ */
+ public function warning( $message ) {
+ }
+
+ /**
+ * Mark a test skipped
+ */
+ public function skipped( $test, $subtest ) {
+ }
+
+ /**
+ * Called before finishing the test run
+ */
+ public function report() {
+ }
+
+ /**
+ * Called at the end of the parser test run
+ */
+ public function end() {
+ }
+
+}
diff --git a/www/wiki/tests/parser/TidySupport.php b/www/wiki/tests/parser/TidySupport.php
new file mode 100644
index 00000000..559960de
--- /dev/null
+++ b/www/wiki/tests/parser/TidySupport.php
@@ -0,0 +1,77 @@
+<?php
+
+/**
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ * @ingroup Testing
+ */
+
+/**
+ * Initialize and detect the tidy support
+ */
+class TidySupport {
+ private $enabled;
+ private $config;
+
+ /**
+ * Determine if there is a usable tidy.
+ * @param bool $useConfiguration
+ */
+ public function __construct( $useConfiguration = false ) {
+ global $wgUseTidy, $wgTidyBin, $wgTidyInternal, $wgTidyConfig,
+ $wgTidyConf, $wgTidyOpts;
+
+ $this->enabled = true;
+ if ( $useConfiguration ) {
+ if ( $wgTidyConfig !== null ) {
+ $this->config = $wgTidyConfig;
+ } elseif ( $wgUseTidy ) {
+ $this->config = [
+ 'tidyConfigFile' => $wgTidyConf,
+ 'debugComment' => false,
+ 'tidyBin' => $wgTidyBin,
+ 'tidyCommandLine' => $wgTidyOpts
+ ];
+ if ( $wgTidyInternal ) {
+ $this->config['driver'] = wfIsHHVM() ? 'RaggettInternalHHVM' : 'RaggettInternalPHP';
+ } else {
+ $this->config['driver'] = 'RaggettExternal';
+ }
+ } else {
+ $this->enabled = false;
+ }
+ } else {
+ $this->config = [ 'driver' => 'RemexHtml' ];
+ }
+ if ( !$this->enabled ) {
+ $this->config = [ 'driver' => 'disabled' ];
+ }
+ }
+
+ /**
+ * Returns true if tidy is usable
+ *
+ * @return bool
+ */
+ public function isEnabled() {
+ return $this->enabled;
+ }
+
+ public function getConfig() {
+ return $this->config;
+ }
+}
diff --git a/www/wiki/tests/parser/editTests.php b/www/wiki/tests/parser/editTests.php
new file mode 100644
index 00000000..3e18370a
--- /dev/null
+++ b/www/wiki/tests/parser/editTests.php
@@ -0,0 +1,488 @@
+<?php
+
+require __DIR__.'/../../maintenance/Maintenance.php';
+
+define( 'MW_PARSER_TEST', true );
+
+/**
+ * Interactive parser test runner and test file editor
+ */
+class ParserEditTests extends Maintenance {
+ private $termWidth;
+ private $testFiles;
+ private $testCount;
+ private $recorder;
+ private $runner;
+ private $numExecuted;
+ private $numSkipped;
+ private $numFailed;
+
+ function __construct() {
+ parent::__construct();
+ $this->addOption( 'session-data', 'internal option, do not use', false, true );
+ $this->addOption( 'use-tidy-config',
+ 'Use the wiki\'s Tidy configuration instead of known-good' .
+ 'defaults.' );
+ }
+
+ public function finalSetup() {
+ parent::finalSetup();
+ self::requireTestsAutoloader();
+ TestSetup::applyInitialConfig();
+ }
+
+ public function execute() {
+ $this->termWidth = $this->getTermSize()[0] - 1;
+
+ $this->recorder = new TestRecorder();
+ $this->setupFileData();
+
+ if ( $this->hasOption( 'session-data' ) ) {
+ $this->session = json_decode( $this->getOption( 'session-data' ), true );
+ } else {
+ $this->session = [ 'options' => [] ];
+ }
+ if ( $this->hasOption( 'use-tidy-config' ) ) {
+ $this->session['options']['use-tidy-config'] = true;
+ }
+ $this->runner = new ParserTestRunner( $this->recorder, $this->session['options'] );
+
+ $this->runTests();
+
+ if ( $this->numFailed === 0 ) {
+ if ( $this->numSkipped === 0 ) {
+ print "All tests passed!\n";
+ } else {
+ print "All tests passed (but skipped {$this->numSkipped})\n";
+ }
+ return;
+ }
+ print "{$this->numFailed} test(s) failed.\n";
+ $this->showResults();
+ }
+
+ protected function setupFileData() {
+ $this->testFiles = [];
+ $this->testCount = 0;
+ foreach ( ParserTestRunner::getParserTestFiles() as $file ) {
+ $fileInfo = TestFileReader::read( $file );
+ $this->testFiles[$file] = $fileInfo;
+ $this->testCount += count( $fileInfo['tests'] );
+ }
+ }
+
+ protected function runTests() {
+ $teardown = $this->runner->staticSetup();
+ $teardown = $this->runner->setupDatabase( $teardown );
+ $teardown = $this->runner->setupUploads( $teardown );
+
+ print "Running tests...\n";
+ $this->results = [];
+ $this->numExecuted = 0;
+ $this->numSkipped = 0;
+ $this->numFailed = 0;
+ foreach ( $this->testFiles as $fileName => $fileInfo ) {
+ $this->runner->addArticles( $fileInfo['articles'] );
+ foreach ( $fileInfo['tests'] as $testInfo ) {
+ $result = $this->runner->runTest( $testInfo );
+ if ( $result === false ) {
+ $this->numSkipped++;
+ } elseif ( !$result->isSuccess() ) {
+ $this->results[$fileName][$testInfo['desc']] = $result;
+ $this->numFailed++;
+ }
+ $this->numExecuted++;
+ $this->showProgress();
+ }
+ }
+ print "\n";
+ }
+
+ protected function showProgress() {
+ $done = $this->numExecuted;
+ $total = $this->testCount;
+ $width = $this->termWidth - 9;
+ $pos = round( $width * $done / $total );
+ printf( '│' . str_repeat( '█', $pos ) . str_repeat( '-', $width - $pos ) .
+ "│ %5.1f%%\r", $done / $total * 100 );
+ }
+
+ protected function showResults() {
+ if ( isset( $this->session['startFile'] ) ) {
+ $startFile = $this->session['startFile'];
+ $startTest = $this->session['startTest'];
+ $foundStart = false;
+ } else {
+ $startFile = false;
+ $startTest = false;
+ $foundStart = true;
+ }
+
+ $testIndex = 0;
+ foreach ( $this->testFiles as $fileName => $fileInfo ) {
+ if ( !isset( $this->results[$fileName] ) ) {
+ continue;
+ }
+ if ( !$foundStart && $startFile !== false && $fileName !== $startFile ) {
+ $testIndex += count( $this->results[$fileName] );
+ continue;
+ }
+ foreach ( $fileInfo['tests'] as $testInfo ) {
+ if ( !isset( $this->results[$fileName][$testInfo['desc']] ) ) {
+ continue;
+ }
+ $result = $this->results[$fileName][$testInfo['desc']];
+ $testIndex++;
+ if ( !$foundStart && $startTest !== false ) {
+ if ( $testInfo['desc'] !== $startTest ) {
+ continue;
+ }
+ $foundStart = true;
+ }
+
+ $this->handleFailure( $testIndex, $testInfo, $result );
+ }
+ }
+
+ if ( !$foundStart ) {
+ print "Could not find the test after a restart, did you rename it?";
+ unset( $this->session['startFile'] );
+ unset( $this->session['startTest'] );
+ $this->showResults();
+ }
+ print "All done\n";
+ }
+
+ protected function heading( $text ) {
+ $term = new AnsiTermColorer;
+ $heading = "─── $text ";
+ $heading .= str_repeat( '─', $this->termWidth - mb_strlen( $heading ) );
+ $heading = $term->color( 34 ) . $heading . $term->reset() . "\n";
+ return $heading;
+ }
+
+ protected function unifiedDiff( $left, $right ) {
+ $fromLines = explode( "\n", $left );
+ $toLines = explode( "\n", $right );
+ $formatter = new UnifiedDiffFormatter;
+ return $formatter->format( new Diff( $fromLines, $toLines ) );
+ }
+
+ protected function handleFailure( $index, $testInfo, $result ) {
+ $term = new AnsiTermColorer;
+ $div1 = $term->color( 34 ) . str_repeat( '━', $this->termWidth ) .
+ $term->reset() . "\n";
+ $div2 = $term->color( 34 ) . str_repeat( '─', $this->termWidth ) .
+ $term->reset() . "\n";
+
+ print $div1;
+ print "Failure $index/{$this->numFailed}: {$testInfo['file']} line {$testInfo['line']}\n" .
+ "{$testInfo['desc']}\n";
+
+ print $this->heading( 'Input' );
+ print "{$testInfo['input']}\n";
+
+ print $this->heading( 'Alternating expected/actual output' );
+ print $this->alternatingAligned( $result->expected, $result->actual );
+
+ print $this->heading( 'Diff' );
+
+ $dwdiff = $this->dwdiff( $result->expected, $result->actual );
+ if ( $dwdiff !== false ) {
+ $diff = $dwdiff;
+ } else {
+ $diff = $this->unifiedDiff( $result->expected, $result->actual );
+ }
+ print $diff;
+
+ if ( $testInfo['options'] || $testInfo['config'] ) {
+ print $this->heading( 'Options / Config' );
+ if ( $testInfo['options'] ) {
+ print $testInfo['options'] . "\n";
+ }
+ if ( $testInfo['config'] ) {
+ print $testInfo['config'] . "\n";
+ }
+ }
+
+ print $div2;
+ print "What do you want to do?\n";
+ $specs = [
+ '[R]eload code and run again',
+ '[U]pdate source file, copy actual to expected',
+ '[I]gnore' ];
+
+ if ( strpos( $testInfo['options'], ' tidy' ) === false ) {
+ if ( empty( $testInfo['isSubtest'] ) ) {
+ $specs[] = "Enable [T]idy";
+ }
+ } else {
+ $specs[] = 'Disable [T]idy';
+ }
+
+ if ( !empty( $testInfo['isSubtest'] ) ) {
+ $specs[] = 'Delete [s]ubtest';
+ }
+ $specs[] = '[D]elete test';
+ $specs[] = '[Q]uit';
+
+ $options = [];
+ foreach ( $specs as $spec ) {
+ if ( !preg_match( '/^(.*\[)(.)(\].*)$/', $spec, $m ) ) {
+ throw new MWException( 'Invalid option spec: ' . $spec );
+ }
+ print '* ' . $m[1] . $term->color( 35 ) . $m[2] . $term->color( 0 ) . $m[3] . "\n";
+ $options[strtoupper( $m[2] )] = true;
+ }
+
+ do {
+ $response = $this->readconsole();
+ $cmdResult = false;
+ if ( $response === false ) {
+ exit( 0 );
+ }
+
+ $response = strtoupper( trim( $response ) );
+ if ( !isset( $options[$response] ) ) {
+ print "Invalid response, please enter a single letter from the list above\n";
+ continue;
+ }
+
+ switch ( strtoupper( trim( $response ) ) ) {
+ case 'R':
+ $cmdResult = $this->reload( $testInfo );
+ break;
+ case 'U':
+ $cmdResult = $this->update( $testInfo, $result );
+ break;
+ case 'I':
+ return;
+ case 'T':
+ $cmdResult = $this->switchTidy( $testInfo );
+ break;
+ case 'S':
+ $cmdResult = $this->deleteSubtest( $testInfo );
+ break;
+ case 'D':
+ $cmdResult = $this->deleteTest( $testInfo );
+ break;
+ case 'Q':
+ exit( 0 );
+ }
+ } while ( !$cmdResult );
+ }
+
+ protected function dwdiff( $expected, $actual ) {
+ if ( !is_executable( '/usr/bin/dwdiff' ) ) {
+ return false;
+ }
+
+ $markers = [
+ "\n" => '¶',
+ ' ' => '·',
+ "\t" => '→'
+ ];
+ $markedExpected = strtr( $expected, $markers );
+ $markedActual = strtr( $actual, $markers );
+ $diff = $this->unifiedDiff( $markedExpected, $markedActual );
+
+ $tempFile = tmpfile();
+ fwrite( $tempFile, $diff );
+ fseek( $tempFile, 0 );
+ $pipes = [];
+ $proc = proc_open( '/usr/bin/dwdiff -Pc --diff-input',
+ [ 0 => $tempFile, 1 => [ 'pipe', 'w' ], 2 => STDERR ],
+ $pipes );
+
+ if ( !$proc ) {
+ return false;
+ }
+
+ $result = stream_get_contents( $pipes[1] );
+ proc_close( $proc );
+ fclose( $tempFile );
+ return $result;
+ }
+
+ protected function alternatingAligned( $expectedStr, $actualStr ) {
+ $expectedLines = explode( "\n", $expectedStr );
+ $actualLines = explode( "\n", $actualStr );
+ $maxLines = max( count( $expectedLines ), count( $actualLines ) );
+ $result = '';
+ for ( $i = 0; $i < $maxLines; $i++ ) {
+ if ( $i < count( $expectedLines ) ) {
+ $expectedLine = $expectedLines[$i];
+ $expectedChunks = str_split( $expectedLine, $this->termWidth - 3 );
+ } else {
+ $expectedChunks = [];
+ }
+
+ if ( $i < count( $actualLines ) ) {
+ $actualLine = $actualLines[$i];
+ $actualChunks = str_split( $actualLine, $this->termWidth - 3 );
+ } else {
+ $actualChunks = [];
+ }
+
+ $maxChunks = max( count( $expectedChunks ), count( $actualChunks ) );
+
+ for ( $j = 0; $j < $maxChunks; $j++ ) {
+ if ( isset( $expectedChunks[$j] ) ) {
+ $result .= "E: " . $expectedChunks[$j];
+ if ( $j === count( $expectedChunks ) - 1 ) {
+ $result .= "¶";
+ }
+ $result .= "\n";
+ } else {
+ $result .= "E:\n";
+ }
+ $result .= "\33[4m" . // underline
+ "A: ";
+ if ( isset( $actualChunks[$j] ) ) {
+ $result .= $actualChunks[$j];
+ if ( $j === count( $actualChunks ) - 1 ) {
+ $result .= "¶";
+ }
+ }
+ $result .= "\33[0m\n"; // reset
+ }
+ }
+ return $result;
+ }
+
+ protected function reload( $testInfo ) {
+ global $argv;
+ pcntl_exec( PHP_BINARY, [
+ $argv[0],
+ '--session-data',
+ json_encode( [
+ 'startFile' => $testInfo['file'],
+ 'startTest' => $testInfo['desc']
+ ] + $this->session ) ] );
+
+ print "pcntl_exec() failed\n";
+ return false;
+ }
+
+ protected function findTest( $file, $testInfo ) {
+ $initialPart = '';
+ for ( $i = 1; $i < $testInfo['line']; $i++ ) {
+ $line = fgets( $file );
+ if ( $line === false ) {
+ print "Error reading from file\n";
+ return false;
+ }
+ $initialPart .= $line;
+ }
+
+ $line = fgets( $file );
+ if ( !preg_match( '/^!!\s*test/', $line ) ) {
+ print "Test has moved, cannot edit\n";
+ return false;
+ }
+
+ $testPart = $line;
+
+ $desc = fgets( $file );
+ if ( trim( $desc ) !== $testInfo['desc'] ) {
+ print "Description does not match, cannot edit\n";
+ return false;
+ }
+ $testPart .= $desc;
+ return [ $initialPart, $testPart ];
+ }
+
+ protected function getOutputFileName( $inputFileName ) {
+ if ( is_writable( $inputFileName ) ) {
+ $outputFileName = $inputFileName;
+ } else {
+ $outputFileName = wfTempDir() . '/' . basename( $inputFileName );
+ print "Cannot write to input file, writing to $outputFileName instead\n";
+ }
+ return $outputFileName;
+ }
+
+ protected function editTest( $fileName, $deletions, $changes ) {
+ $text = file_get_contents( $fileName );
+ if ( $text === false ) {
+ print "Unable to open test file!";
+ return false;
+ }
+ $result = TestFileEditor::edit( $text, $deletions, $changes,
+ function ( $msg ) {
+ print "$msg\n";
+ }
+ );
+ if ( is_writable( $fileName ) ) {
+ file_put_contents( $fileName, $result );
+ print "Wrote updated file\n";
+ } else {
+ print "Cannot write updated file, here is a patch you can paste:\n\n";
+ print "--- {$fileName}\n" .
+ "+++ {$fileName}~\n" .
+ $this->unifiedDiff( $text, $result ) .
+ "\n";
+ }
+ }
+
+ protected function update( $testInfo, $result ) {
+ $this->editTest( $testInfo['file'],
+ [], // deletions
+ [ // changes
+ $testInfo['test'] => [
+ $testInfo['resultSection'] => [
+ 'op' => 'update',
+ 'value' => $result->actual . "\n"
+ ]
+ ]
+ ]
+ );
+ }
+
+ protected function deleteTest( $testInfo ) {
+ $this->editTest( $testInfo['file'],
+ [ $testInfo['test'] ], // deletions
+ [] // changes
+ );
+ }
+
+ protected function switchTidy( $testInfo ) {
+ $resultSection = $testInfo['resultSection'];
+ if ( in_array( $resultSection, [ 'html/php', 'html/*', 'html', 'result' ] ) ) {
+ $newSection = 'html+tidy';
+ } elseif ( in_array( $resultSection, [ 'html/php+tidy', 'html+tidy' ] ) ) {
+ $newSection = 'html';
+ } else {
+ print "Unrecognised result section name \"$resultSection\"";
+ return;
+ }
+
+ $this->editTest( $testInfo['file'],
+ [], // deletions
+ [ // changes
+ $testInfo['test'] => [
+ $resultSection => [
+ 'op' => 'rename',
+ 'value' => $newSection
+ ]
+ ]
+ ]
+ );
+ }
+
+ protected function deleteSubtest( $testInfo ) {
+ $this->editTest( $testInfo['file'],
+ [], // deletions
+ [ // changes
+ $testInfo['test'] => [
+ $testInfo['resultSection'] => [
+ 'op' => 'delete'
+ ]
+ ]
+ ]
+ );
+ }
+}
+
+$maintClass = 'ParserEditTests';
+require RUN_MAINTENANCE_IF_MAIN;
diff --git a/www/wiki/tests/parser/extraParserTests.txt b/www/wiki/tests/parser/extraParserTests.txt
new file mode 100644
index 00000000..50d1bc9e
--- /dev/null
+++ b/www/wiki/tests/parser/extraParserTests.txt
Binary files differ
diff --git a/www/wiki/tests/parser/fuzzTest.php b/www/wiki/tests/parser/fuzzTest.php
new file mode 100644
index 00000000..eb4181c7
--- /dev/null
+++ b/www/wiki/tests/parser/fuzzTest.php
@@ -0,0 +1,202 @@
+<?php
+
+use Wikimedia\ScopedCallback;
+
+require __DIR__ . '/../../maintenance/Maintenance.php';
+
+// Make RequestContext::resetMain() happy
+define( 'MW_PARSER_TEST', 1 );
+
+class ParserFuzzTest extends Maintenance {
+ private $parserTest;
+ private $maxFuzzTestLength = 300;
+ private $memoryLimit = 100;
+ private $seed;
+
+ function __construct() {
+ parent::__construct();
+ $this->addDescription( 'Run a fuzz test on the parser, until it segfaults ' .
+ 'or throws an exception' );
+ $this->addOption( 'file', 'Use the specified file as a dictionary, ' .
+ ' or leave blank to use parserTests.txt', false, true, true );
+
+ $this->addOption( 'seed', 'Start the fuzz test from the specified seed', false, true );
+ }
+
+ function finalSetup() {
+ self::requireTestsAutoloader();
+ TestSetup::applyInitialConfig();
+ }
+
+ function execute() {
+ $files = $this->getOption( 'file', [ __DIR__ . '/parserTests.txt' ] );
+ $this->seed = intval( $this->getOption( 'seed', 1 ) ) - 1;
+ $this->parserTest = new ParserTestRunner(
+ new MultiTestRecorder,
+ [] );
+ $this->fuzzTest( $files );
+ }
+
+ /**
+ * Run a fuzz test series
+ * Draw input from a set of test files
+ * @param array $filenames
+ */
+ function fuzzTest( $filenames ) {
+ $dict = $this->getFuzzInput( $filenames );
+ $dictSize = strlen( $dict );
+ $logMaxLength = log( $this->maxFuzzTestLength );
+
+ $teardown = $this->parserTest->staticSetup();
+ $teardown = $this->parserTest->setupDatabase( $teardown );
+ $teardown = $this->parserTest->setupUploads( $teardown );
+
+ $fakeTest = [
+ 'test' => '',
+ 'desc' => '',
+ 'input' => '',
+ 'result' => '',
+ 'options' => '',
+ 'config' => ''
+ ];
+
+ ini_set( 'memory_limit', $this->memoryLimit * 1048576 * 2 );
+
+ $numTotal = 0;
+ $numSuccess = 0;
+ $user = new User;
+ $opts = ParserOptions::newFromUser( $user );
+ $title = Title::makeTitle( NS_MAIN, 'Parser_test' );
+
+ while ( true ) {
+ // Generate test input
+ mt_srand( ++$this->seed );
+ $totalLength = mt_rand( 1, $this->maxFuzzTestLength );
+ $input = '';
+
+ while ( strlen( $input ) < $totalLength ) {
+ $logHairLength = mt_rand( 0, 1000000 ) / 1000000 * $logMaxLength;
+ $hairLength = min( intval( exp( $logHairLength ) ), $dictSize );
+ $offset = mt_rand( 0, $dictSize - $hairLength );
+ $input .= substr( $dict, $offset, $hairLength );
+ }
+
+ $perTestTeardown = $this->parserTest->perTestSetup( $fakeTest );
+ $parser = $this->parserTest->getParser();
+
+ // Run the test
+ try {
+ $parser->parse( $input, $title, $opts );
+ $fail = false;
+ } catch ( Exception $exception ) {
+ $fail = true;
+ }
+
+ if ( $fail ) {
+ echo "Test failed with seed {$this->seed}\n";
+ echo "Input:\n";
+ printf( "string(%d) \"%s\"\n\n", strlen( $input ), $input );
+ echo "$exception\n";
+ } else {
+ $numSuccess++;
+ }
+
+ $numTotal++;
+ ScopedCallback::consume( $perTestTeardown );
+
+ if ( $numTotal % 100 == 0 ) {
+ $usage = intval( memory_get_usage( true ) / $this->memoryLimit / 1048576 * 100 );
+ echo "{$this->seed}: $numSuccess/$numTotal (mem: $usage%)\n";
+ if ( $usage >= 100 ) {
+ echo "Out of memory:\n";
+ $memStats = $this->getMemoryBreakdown();
+
+ foreach ( $memStats as $name => $usage ) {
+ echo "$name: $usage\n";
+ }
+ if ( function_exists( 'hphpd_break' ) ) {
+ hphpd_break();
+ }
+ return;
+ }
+ }
+ }
+ }
+
+ /**
+ * Get a memory usage breakdown
+ * @return array
+ */
+ function getMemoryBreakdown() {
+ $memStats = [];
+
+ foreach ( $GLOBALS as $name => $value ) {
+ $memStats['$' . $name] = $this->guessVarSize( $value );
+ }
+
+ $classes = get_declared_classes();
+
+ foreach ( $classes as $class ) {
+ $rc = new ReflectionClass( $class );
+ $props = $rc->getStaticProperties();
+ $memStats[$class] = $this->guessVarSize( $props );
+ $methods = $rc->getMethods();
+
+ foreach ( $methods as $method ) {
+ $memStats[$class] += $this->guessVarSize( $method->getStaticVariables() );
+ }
+ }
+
+ $functions = get_defined_functions();
+
+ foreach ( $functions['user'] as $function ) {
+ $rf = new ReflectionFunction( $function );
+ $memStats["$function()"] = $this->guessVarSize( $rf->getStaticVariables() );
+ }
+
+ asort( $memStats );
+
+ return $memStats;
+ }
+
+ /**
+ * Estimate the size of the input variable
+ */
+ function guessVarSize( $var ) {
+ $length = 0;
+ try {
+ Wikimedia\suppressWarnings();
+ $length = strlen( serialize( $var ) );
+ Wikimedia\restoreWarnings();
+ } catch ( Exception $e ) {
+ }
+ return $length;
+ }
+
+ /**
+ * Get an input dictionary from a set of parser test files
+ * @param array $filenames
+ * @return string
+ */
+ function getFuzzInput( $filenames ) {
+ $dict = '';
+
+ foreach ( $filenames as $filename ) {
+ $contents = file_get_contents( $filename );
+ preg_match_all(
+ '/!!\s*(input|wikitext)\n(.*?)\n!!\s*(result|html|html\/\*|html\/php)/s',
+ $contents,
+ $matches
+ );
+
+ foreach ( $matches[1] as $match ) {
+ $dict .= $match . "\n";
+ }
+ }
+
+ return $dict;
+ }
+}
+
+$maintClass = 'ParserFuzzTest';
+require RUN_MAINTENANCE_IF_MAIN;
diff --git a/www/wiki/tests/parser/parserTests.php b/www/wiki/tests/parser/parserTests.php
new file mode 100644
index 00000000..6a423d5c
--- /dev/null
+++ b/www/wiki/tests/parser/parserTests.php
@@ -0,0 +1,199 @@
+<?php
+/**
+ * MediaWiki parser test suite
+ *
+ * Copyright © 2004 Brion Vibber <brion@pobox.com>
+ * https://www.mediawiki.org/
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ * @ingroup Testing
+ */
+
+// Some methods which are discouraged for normal code throw exceptions unless
+// we declare this is just a test.
+define( 'MW_PARSER_TEST', true );
+
+require __DIR__ . '/../../maintenance/Maintenance.php';
+
+use MediaWiki\MediaWikiServices;
+
+class ParserTestsMaintenance extends Maintenance {
+ function __construct() {
+ parent::__construct();
+ $this->addDescription( 'Run parser tests' );
+
+ $this->addOption( 'quick', 'Suppress diff output of failed tests' );
+ $this->addOption( 'quiet', 'Suppress notification of passed tests (shows only failed tests)' );
+ $this->addOption( 'show-output', 'Show expected and actual output' );
+ $this->addOption( 'color', '[=yes|no] Override terminal detection and force ' .
+ 'color output on or off. Use wgCommandLineDarkBg = true; if your term is dark',
+ false, true );
+ $this->addOption( 'regex', 'Only run tests whose descriptions which match given regex',
+ false, true );
+ $this->addOption( 'filter', 'Alias for --regex', false, true );
+ $this->addOption( 'file', 'Run test cases from a custom file instead of parserTests.txt',
+ false, true, false, true );
+ $this->addOption( 'record', 'Record tests in database' );
+ $this->addOption( 'compare', 'Compare with recorded results, without updating the database.' );
+ $this->addOption( 'setversion', 'When using --record, set the version string to use (useful' .
+ 'with "git rev-parse HEAD" to get the exact revision)',
+ false, true );
+ $this->addOption( 'keep-uploads', 'Re-use the same upload directory for each ' .
+ 'test, don\'t delete it' );
+ $this->addOption( 'file-backend', 'Use the file backend with the given name,' .
+ 'and upload files to it, instead of creating a mock file backend.', false, true );
+ $this->addOption( 'upload-dir', 'Specify the upload directory to use. Useful in ' .
+ 'conjunction with --keep-uploads. Causes a real (non-mock) file backend to ' .
+ 'be used.', false, true );
+ $this->addOption( 'run-disabled', 'run disabled tests' );
+ $this->addOption( 'run-parsoid', 'run parsoid tests (normally disabled)' );
+ $this->addOption( 'dwdiff', 'Use dwdiff to display diff output' );
+ $this->addOption( 'mark-ws', 'Mark whitespace in diffs by replacing it with symbols' );
+ $this->addOption( 'norm', 'Apply a comma-separated list of normalization functions to ' .
+ 'both the expected and actual output in order to resolve ' .
+ 'irrelevant differences. The accepted normalization functions ' .
+ 'are: removeTbody to remove <tbody> tags; and trimWhitespace ' .
+ 'to trim whitespace from the start and end of text nodes.',
+ false, true );
+ $this->addOption( 'use-tidy-config',
+ 'Use the wiki\'s Tidy configuration instead of known-good' .
+ 'defaults.' );
+ }
+
+ public function finalSetup() {
+ parent::finalSetup();
+ self::requireTestsAutoloader();
+ TestSetup::applyInitialConfig();
+ }
+
+ public function execute() {
+ global $wgDBtype;
+
+ // Cases of weird db corruption were encountered when running tests on earlyish
+ // versions of SQLite
+ if ( $wgDBtype == 'sqlite' ) {
+ $db = wfGetDB( DB_MASTER );
+ $version = $db->getServerVersion();
+ if ( version_compare( $version, '3.6' ) < 0 ) {
+ die( "Parser tests require SQLite version 3.6 or later, you have $version\n" );
+ }
+ }
+
+ // Print out software version to assist with locating regressions
+ $version = SpecialVersion::getVersion( 'nodb' );
+ echo "This is MediaWiki version {$version}.\n\n";
+
+ // Only colorize output if stdout is a terminal.
+ $color = !wfIsWindows() && Maintenance::posix_isatty( 1 );
+
+ if ( $this->hasOption( 'color' ) ) {
+ switch ( $this->getOption( 'color' ) ) {
+ case 'no':
+ $color = false;
+ break;
+ case 'yes':
+ default:
+ $color = true;
+ break;
+ }
+ }
+
+ $record = $this->hasOption( 'record' );
+ $compare = $this->hasOption( 'compare' );
+
+ $regex = $this->getOption( 'filter', $this->getOption( 'regex', false ) );
+ if ( $regex !== false ) {
+ $regex = "/$regex/i";
+
+ if ( $record ) {
+ echo "Warning: --record cannot be used with --regex, disabling --record\n";
+ $record = false;
+ }
+ }
+
+ $term = $color
+ ? new AnsiTermColorer()
+ : new DummyTermColorer();
+
+ $recorder = new MultiTestRecorder;
+
+ $recorder->addRecorder( new ParserTestPrinter(
+ $term,
+ [
+ 'showDiffs' => !$this->hasOption( 'quick' ),
+ 'showProgress' => !$this->hasOption( 'quiet' ),
+ 'showFailure' => !$this->hasOption( 'quiet' )
+ || ( !$record && !$compare ), // redundant output
+ 'showOutput' => $this->hasOption( 'show-output' ),
+ 'useDwdiff' => $this->hasOption( 'dwdiff' ),
+ 'markWhitespace' => $this->hasOption( 'mark-ws' ),
+ ]
+ ) );
+
+ $recorderLB = false;
+ if ( $record || $compare ) {
+ $lbFactory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory();
+ $recorderLB = $lbFactory->newMainLB();
+ // This connection will have the wiki's table prefix, not parsertest_
+ $recorderDB = $recorderLB->getConnection( DB_MASTER );
+
+ // Add recorder before previewer because recorder will create the
+ // DB table if it doesn't exist
+ if ( $record ) {
+ $recorder->addRecorder( new DbTestRecorder( $recorderDB ) );
+ }
+ $recorder->addRecorder( new DbTestPreviewer(
+ $recorderDB,
+ function ( $name ) use ( $regex ) {
+ // Filter reports of old tests by the filter regex
+ if ( $regex === false ) {
+ return true;
+ } else {
+ return (bool)preg_match( $regex, $name );
+ }
+ } ) );
+ }
+
+ // Default parser tests and any set from extensions or local config
+ $files = $this->getOption( 'file', ParserTestRunner::getParserTestFiles() );
+
+ $norm = $this->hasOption( 'norm' ) ? explode( ',', $this->getOption( 'norm' ) ) : [];
+
+ $tester = new ParserTestRunner( $recorder, [
+ 'norm' => $norm,
+ 'regex' => $regex,
+ 'keep-uploads' => $this->hasOption( 'keep-uploads' ),
+ 'run-disabled' => $this->hasOption( 'run-disabled' ),
+ 'run-parsoid' => $this->hasOption( 'run-parsoid' ),
+ 'use-tidy-config' => $this->hasOption( 'use-tidy-config' ),
+ 'file-backend' => $this->getOption( 'file-backend' ),
+ 'upload-dir' => $this->getOption( 'upload-dir' ),
+ ] );
+
+ $ok = $tester->runTestsFromFiles( $files );
+ if ( $recorderLB ) {
+ $recorderLB->closeAll();
+ }
+ if ( !$ok ) {
+ exit( 1 );
+ }
+ }
+}
+
+$maintClass = 'ParserTestsMaintenance';
+require_once RUN_MAINTENANCE_IF_MAIN;
diff --git a/www/wiki/tests/parser/parserTests.txt b/www/wiki/tests/parser/parserTests.txt
new file mode 100644
index 00000000..451c50f5
--- /dev/null
+++ b/www/wiki/tests/parser/parserTests.txt
@@ -0,0 +1,30829 @@
+# MediaWiki Parser test cases
+# Some taken from https://meta.wikimedia.org/wiki/Parser_testing
+# All (C) their respective authors and released under the GPL
+#
+# The syntax should be fairly self-explanatory.
+#
+# Currently supported test options:
+# One of the following three:
+#
+# (default) generate HTML output
+# pst apply pre-save transform
+# msg apply message transform
+#
+# Plus any combination of these:
+#
+# cat add category links
+# (ignored by Parsoid, since it emits <link>s)
+# ill add inter-language links
+# (ignored by Parsoid, since it emits <link>s)
+# subpage enable subpages (disabled by default)
+# title=[[XXX]] run test using article title XXX
+# language=XXX set content language to XXX for this test
+# variant=XXX set the variant of language for this test (eg zh-tw)
+# disabled do not run test
+# parsoid parsoid-specific options (not run by PHP parser unless
+# the test includes an html/php section)
+# php php-only test (not run by the parsoid parser unless
+# the test includes an html/parsoid section)
+# showtitle make the first line the title
+# showindicators make the first lines the page status indicators
+# comment run through Linker::formatComment() instead of main parser
+# local format section links in edit comment text as local links
+# notoc disable table of contents
+# thumbsize=NNN set the default thumb size to NNNpx for this test
+# wrap include the normal wrapper <div class="mw-parser-output"> (since 1.30)
+#
+# You can also set the following parser properties via test options:
+# wgEnableUploads, wgAllowExternalImages, wgMaxTocLevel,
+# wgLinkHolderBatchSize, wgRawHtml, wgInterwikiMagic,
+# wgEnableMagicLinks
+#
+# For testing purposes, temporary articles can created:
+# !!article / NAMESPACE:TITLE / !!text / ARTICLE TEXT / !!endarticle
+# where '/' denotes a newline.
+
+# This is the standard article assumed to exist.
+!! article
+Main Page
+!! text
+blah blah
+!! endarticle
+
+!!article
+Foo
+!!text
+FOO
+!!endarticle
+
+!!article
+Template:Foo
+!!text
+FOO
+!!endarticle
+
+!! article
+Template:Blank
+!! text
+!! endarticle
+
+!! article
+Template:pipe
+!! text
+|
+!! endarticle
+
+!! article
+Template:=
+!! text
+<nowiki>=</nowiki>
+!! endarticle
+
+!!article
+MediaWiki:bad image list
+!!text
+* [[File:Bad.jpg]] except [[Nasty page]]
+!!endarticle
+
+!! article
+Template:inner list
+!! text
+* item 1
+!! endarticle
+
+!! article
+Template:tbl-start
+!! text
+{|
+!! endarticle
+
+!! article
+Template:tbl-end
+!! text
+|}
+!! endarticle
+
+!! article
+Template:echo
+!! text
+{{{1}}}
+!! endarticle
+
+// For Serbian; localize Template namespace
+!! article
+Шаблон:Echo
+!! text
+{{{1}}}
+!! endarticle
+
+!! article
+Template:echo_with_span
+!! text
+<span>{{{1}}}</span>
+!! endarticle
+
+!! article
+Template:echo_with_div
+!! text
+<div>{{{1}}}</div>
+!! endarticle
+
+!! article
+Template:blank_param
+!! text
+{{{1}}}
+{{{}}}
+!! endarticle
+
+!! article
+Template:table_attribs
+!! text
+<noinclude>
+|</noinclude>style="color:red;"|Foo
+!! endarticle
+
+!! article
+Template:table_attribs_2
+!! text
+<noinclude>
+|</noinclude>style="color:red;"|Foo
+|Bar||Baz
+!! endarticle
+
+!! article
+Template:table_attribs_3
+!! text
+<noinclude>
+|</noinclude>style{{=}}"background:&#35;f9f9f9;"|Foo
+!! endarticle
+
+!! article
+Template:table_attribs_4
+!! text
+| style="background-color:#DC241f;" width="10px" |
+!! endarticle
+
+!! article
+Template:table_attribs_5
+!! text
+<noinclude>
+|</noinclude>style="color:red;"||Bar
+!! endarticle
+
+!! article
+Template:table_attribs_6
+!! text
+style="background: <nowiki>
+
+
+red;</nowiki>" |
+!! endarticle
+
+!! article
+Template:table_attribs_7
+!! text
+<noinclude>
+|</noinclude>style{{=}}"background:&#35;f9f9f9;"|Foo<ref>foo</ref>
+!! endarticle
+
+!! article
+Template:table_header_cells
+!! text
+{{table_attribs}}!!style='color:red;'|''Bar''||style='color:brown;'|''Foo'' and Baz
+!! endarticle
+
+!! article
+Template:table_cells
+!! text
+{{table_attribs}}||style='color:red;'|''Bar''||style='color:brown;'|''Foo'' and Baz
+!! endarticle
+
+!! article
+Template:PartialTable
+!! text
+{|
+|-
+!! endarticle
+
+!! article
+Template:image_attribs
+!! text
+<noinclude>
+[[File:foobar.jpg|</noinclude>right|Caption text<noinclude>]]</noinclude>
+!! endarticle
+
+## See T48811 for details
+!! article
+Template:mixed_attr_content_template
+!! text
+style="color:red;" title="T48811"
+|-
+|foo
+!! endarticle
+
+!! article
+Template:definition_list
+!! text
+one
+::two
+!! endarticle
+
+!! article
+A?b
+!! text
+Weirdo titles!
+!! endarticle
+
+!!article
+Template:Bullet
+!!text
+* Bar
+!!endarticle
+
+!!article
+Template:OpenTable
+!!text
+{|
+!!endarticle
+
+!!article
+Template:EmptyLITest
+!!text
+*a
+*
+*
+*b
+!!endarticle
+
+!!article
+Template:EmptyTRTest
+!!text
+{|
+|-
+|-
+|foo
+|-
+|-
+|bar
+|}
+!!endarticle
+
+!!article
+Template:EmptyTRWithHTMLAttrTest
+!!text
+<table>
+<tr align="center"></tr>
+<tr><td>foo</td></tr>
+<tr align="center"></tr>
+<tr><td>bar</td></tr>
+</table>
+!!endarticle
+
+!! article
+Template:With: Colon
+!! text
+Template with colon
+!! endarticle
+
+###
+### Basic tests
+###
+
+!! test
+Blank input
+!! wikitext
+!! html
+!! end
+
+!! test
+Simple paragraph
+!! wikitext
+This is a simple paragraph.
+!! html
+<p>This is a simple paragraph.
+</p>
+!! end
+
+!! test
+Paragraphs with extra newline spacing
+!! wikitext
+foo
+
+bar
+
+
+baz
+
+
+
+booz
+!! html
+<p>foo
+</p><p>bar
+</p><p><br />
+baz
+</p><p><br />
+</p><p>booz
+</p>
+!! end
+
+!! test
+Paragraphs with newline spacing with comment lines in between
+!! wikitext
+----
+a
+<!--foo-->
+b
+----
+a
+<!--foo--><!--More than 1 comment, still stripped-->
+b
+----
+a
+ <!--foo--> <!----> <!-- bar -->
+b
+----
+a
+<!--foo-->
+
+b
+----
+a
+
+<!--foo-->
+b
+----
+a
+<!--foo-->
+
+
+b
+----
+a
+
+
+<!--foo-->
+b
+----
+!! html
+<hr />
+<p>a
+b
+</p>
+<hr />
+<p>a
+b
+</p>
+<hr />
+<p>a
+b
+</p>
+<hr />
+<p>a
+</p><p>b
+</p>
+<hr />
+<p>a
+</p><p>b
+</p>
+<hr />
+<p>a
+</p><p><br />
+b
+</p>
+<hr />
+<p>a
+</p><p><br />
+b
+</p>
+<hr />
+
+!! end
+
+!! test
+Paragraphs with newline spacing with non-empty white-space lines in between
+!! wikitext
+----
+a
+
+b
+----
+a
+
+
+b
+----
+!! html
+<hr />
+<p>a
+</p><p>b
+</p>
+<hr />
+<p>a
+</p><p><br />
+b
+</p>
+<hr />
+
+!! end
+
+!! test
+Paragraphs with newline spacing with non-empty mixed comment and white-space lines in between
+!! wikitext
+----
+a
+ <!--foo-->
+b
+----
+a
+ <!--foo--><!--More than 1 comment doesn't disable stripping of this line!-->
+b
+----
+a
+
+<!--foo-->
+ <!--bar-->
+b
+----
+a
+
+ <!--foo-->
+ <!--bar-->
+
+b
+----
+!! html
+<hr />
+<p>a
+b
+</p>
+<hr />
+<p>a
+b
+</p>
+<hr />
+<p>a
+</p><p>b
+</p>
+<hr />
+<p>a
+</p><p><br />
+b
+</p>
+<hr />
+
+!! end
+
+!! test
+Extra newlines: More paragraphs with indented comment
+!! wikitext
+a
+
+ <!--boo-->
+
+b
+!! html
+<p>a
+</p><p><br />
+b
+</p>
+!!end
+
+!! test
+Extra newlines followed by heading
+!! wikitext
+a
+
+
+
+=b=
+[[a]]
+
+
+=b=
+!! html
+<p>a
+</p><p><br />
+</p>
+<h1><span class="mw-headline" id="b">b</span><span class="mw-editsection"><span class="mw-editsection-bracket">[</span><a href="/index.php?title=Parser_test&amp;action=edit&amp;section=1" title="Edit section: b">edit</a><span class="mw-editsection-bracket">]</span></span></h1>
+<p><a href="/index.php?title=A&amp;action=edit&amp;redlink=1" class="new" title="A (page does not exist)">a</a>
+</p><p><br />
+</p>
+<h1><span class="mw-headline" id="b_2">b</span><span class="mw-editsection"><span class="mw-editsection-bracket">[</span><a href="/index.php?title=Parser_test&amp;action=edit&amp;section=2" title="Edit section: b">edit</a><span class="mw-editsection-bracket">]</span></span></h1>
+
+!! end
+
+!! test
+Extra newlines between heading and content are swallowed
+!! wikitext
+=b=
+
+
+
+[[a]]
+!! html
+<h1><span class="mw-headline" id="b">b</span><span class="mw-editsection"><span class="mw-editsection-bracket">[</span><a href="/index.php?title=Parser_test&amp;action=edit&amp;section=1" title="Edit section: b">edit</a><span class="mw-editsection-bracket">]</span></span></h1>
+<p><a href="/index.php?title=A&amp;action=edit&amp;redlink=1" class="new" title="A (page does not exist)">a</a>
+</p>
+!! end
+
+!! test
+Heading with line break in nowiki
+!! options
+parsoid=wt2html
+!! config
+wgFragmentMode=[ 'html5', 'legacy' ]
+!! wikitext
+==A <nowiki>B
+C</nowiki>==
+!! html/php
+<h2><span id="A_B.0AC"></span><span class="mw-headline" id="A_B
+C">A B
+C</span><span class="mw-editsection"><span class="mw-editsection-bracket">[</span><a href="/index.php?title=Parser_test&amp;action=edit&amp;section=1" title="Edit section: A B&#10;C">edit</a><span class="mw-editsection-bracket">]</span></span></h2>
+
+!! html/parsoid
+<h2 id="A_B
+C"><span id="A_B.0AC" typeof="mw:FallbackId"></span>A <span typeof="mw:Nowiki">B
+C</span></h2>
+!! end
+
+!! test
+Parsing an URL
+!! wikitext
+http://fr.wikipedia.org/wiki/🍺
+<!-- EasterEgg we love beer, better be able be able to link to it -->
+!! html
+<p><a rel="nofollow" class="external free" href="http://fr.wikipedia.org/wiki/🍺">http://fr.wikipedia.org/wiki/🍺</a>
+</p>
+!! end
+
+!! test
+Simple list
+!! wikitext
+*Item 1
+*Item 2
+!! html
+<ul><li>Item 1</li>
+<li>Item 2</li></ul>
+
+!! end
+
+!! test
+Italics and bold
+!! wikitext
+*plain
+*plain''italic''plain
+*plain''italic''plain''italic''plain
+*plain'''bold'''plain
+*plain'''bold'''plain'''bold'''plain
+*plain''italic''plain'''bold'''plain
+*plain'''bold'''plain''italic''plain
+*plain''italic'''bold-italic'''italic''plain
+*plain'''bold''bold-italic''bold'''plain
+*plain'''''bold-italic'''italic''plain
+*plain'''''bold-italic''bold'''plain
+*plain''italic'''bold-italic'''''plain
+*plain'''bold''bold-italic'''''plain
+*plain l'''italic''plain
+*plain l''''bold''' plain
+!! html
+<ul><li>plain</li>
+<li>plain<i>italic</i>plain</li>
+<li>plain<i>italic</i>plain<i>italic</i>plain</li>
+<li>plain<b>bold</b>plain</li>
+<li>plain<b>bold</b>plain<b>bold</b>plain</li>
+<li>plain<i>italic</i>plain<b>bold</b>plain</li>
+<li>plain<b>bold</b>plain<i>italic</i>plain</li>
+<li>plain<i>italic<b>bold-italic</b>italic</i>plain</li>
+<li>plain<b>bold<i>bold-italic</i>bold</b>plain</li>
+<li>plain<i><b>bold-italic</b>italic</i>plain</li>
+<li>plain<b><i>bold-italic</i>bold</b>plain</li>
+<li>plain<i>italic<b>bold-italic</b></i>plain</li>
+<li>plain<b>bold<i>bold-italic</i></b>plain</li>
+<li>plain l'<i>italic</i>plain</li>
+<li>plain l'<b>bold</b> plain</li></ul>
+
+!! end
+
+# this example taken from the [[simple:Moon]] article (T49326)
+!! test
+Italics and possessives (1)
+!! wikitext
+obtained by ''[[Lunar Prospector]]'''s gamma-ray spectrometer
+!! html
+<p>obtained by <i><a href="/index.php?title=Lunar_Prospector&amp;action=edit&amp;redlink=1" class="new" title="Lunar Prospector (page does not exist)">Lunar Prospector</a>'</i>s gamma-ray spectrometer
+</p>
+!! end
+
+# this example taken from [[en:Flaming Pie]] (T51926)
+!! test
+Italics and possessives (2)
+!! wikitext
+'''''Flaming Pie''''' is ... released in 1997. In ''Flaming Pie'''s liner notes
+!! html
+<p><i><b>Flaming Pie</b></i> is ... released in 1997. In <i>Flaming Pie'</i>s liner notes
+</p>
+!! end
+
+# this example taken from [[en:Dictionary]] (T51926)
+!! test
+Italics and possessives (3)
+!! wikitext
+The first monolingual dictionary written in a Romance language was ''Sebastián Covarrubias''' ''Tesoro de la lengua castellana o española'', published in 1611 in Madrid. In 1612 the first edition of the ''Vocabolario dell'[[Accademia della Crusca]]'', for Italian, was published. In 1690 in Rotterdam was published, posthumously, the ''Dictionnaire Universel''.
+!! html
+<p>The first monolingual dictionary written in a Romance language was <i>Sebastián Covarrubias'</i> <i>Tesoro de la lengua castellana o española</i>, published in 1611 in Madrid. In 1612 the first edition of the <i>Vocabolario dell'<a href="/index.php?title=Accademia_della_Crusca&amp;action=edit&amp;redlink=1" class="new" title="Accademia della Crusca (page does not exist)">Accademia della Crusca</a></i>, for Italian, was published. In 1690 in Rotterdam was published, posthumously, the <i>Dictionnaire Universel</i>.
+</p>
+!! end
+
+
+###
+### 2-quote opening sequence tests
+###
+!! test
+Italics and bold: 2-quote opening sequence: (2,2)
+!! wikitext
+''foo''
+!! html
+<p><i>foo</i>
+</p>
+!!end
+
+!! test
+Italics and bold: 2-quote opening sequence: (2,3)
+!! wikitext
+''foo'''
+!! html/*
+<p><i>foo'</i>
+</p>
+!!end
+
+!! test
+Italics and bold: 2-quote opening sequence: (2,4)
+!! options
+parsoid=wt2html
+!! wikitext
+''foo''''
+!! html/*
+<p><i>foo''</i>
+</p>
+!!end
+
+# same html as previous, but wikitext adjusted to match parsoid html2wt
+!! test
+Italics and bold: 2-quote opening sequence: (2,4) w/ nowiki
+!! wikitext
+''foo<nowiki>''</nowiki>''
+!! html
+<p><i>foo''</i>
+</p>
+!! end
+
+# The PHP parser strips the empty tags out for giggles; parsoid doesn't.
+!! test
+Italics and bold: 2-quote opening sequence: (2,5)
+!! options
+parsoid=wt2html
+!! wikitext
+''foo'''''
+!! html/php
+<p><i>foo</i>
+</p>
+!! html/parsoid
+<p><i>foo</i><b></b>
+</p>
+!!end
+
+# same html as previous, but wikitext adjusted to match parsoid html2wt
+!! test
+Italics and bold: 2-quote opening sequence: (2,5+3) w/ nowiki
+!! wikitext
+''foo'''''<nowiki/>'''
+!! html/php
+<p><i>foo</i>
+</p>
+!! html/parsoid
+<p><i>foo</i><b></b>
+</p>
+!! end
+
+
+###
+### 3-quote opening sequence tests
+###
+
+!! test
+Italics and bold: 3-quote opening sequence: (3,2)
+!! wikitext
+'''foo''
+!! html/*
+<p>'<i>foo</i>
+</p>
+!!end
+
+!! test
+Italics and bold: 3-quote opening sequence: (3,3)
+!! wikitext
+'''foo'''
+!! html
+<p><b>foo</b>
+</p>
+!!end
+
+!! test
+Italics and bold: 3-quote opening sequence: (3,4)
+!! wikitext
+'''foo''''
+!! html/*
+<p><b>foo'</b>
+</p>
+!!end
+
+# The PHP parser strips the empty tags out for giggles; parsoid doesn't.
+!! test
+Italics and bold: 3-quote opening sequence: (3,5)
+!! options
+parsoid=wt2html
+!! wikitext
+'''foo'''''
+!! html/php
+<p><b>foo</b>
+</p>
+!! html/parsoid
+<p><b>foo</b><i></i>
+</p>
+!!end
+
+# same html as previous, but wikitext adjusted to match parsoid html2wt
+!! test
+Italics and bold: 3-quote opening sequence: (3,5+2) w/ nowiki
+!! wikitext
+'''foo'''''<nowiki/>''
+!! html/php
+<p><b>foo</b>
+</p>
+!! html/parsoid
+<p><b>foo</b><i></i>
+</p>
+!! end
+
+
+###
+### 4-quote opening sequence tests
+###
+
+!! test
+Italics and bold: 4-quote opening sequence: (4,2)
+!! options
+parsoid=wt2html
+!! wikitext
+''''foo''
+!! html/*
+<p>''<i>foo</i>
+</p>
+!!end
+
+# same html as previous, but wikitext adjusted to match parsoid html2wt
+!! test
+Italics and bold: 4-quote opening sequence: (4,2) w/ nowiki
+!! wikitext
+<nowiki>''</nowiki>''foo''
+!! html
+<p>''<i>foo</i>
+</p>
+!! end
+
+!! test
+Italics and bold: 4-quote opening sequence: (4,3)
+!! wikitext
+''''foo'''
+!! html/*
+<p>'<b>foo</b>
+</p>
+!!end
+
+!! test
+Italics and bold: 4-quote opening sequence: (4,4)
+!! options
+parsoid=wt2html
+!! wikitext
+''''foo''''
+!! html/*
+<p>'<b>foo'</b>
+</p>
+!!end
+
+# same html as previous, but wikitext adjusted to match parsoid html2wt
+!! test
+Italics and bold: 4-quote opening sequence: (4,4) w/ nowiki
+!! wikitext
+'<nowiki/>'''foo''''
+!! html
+<p>'<b>foo'</b>
+</p>
+!! end
+
+# The PHP parser strips the empty tags out for giggles; parsoid doesn't.
+!! test
+Italics and bold: 4-quote opening sequence: (4,5)
+!! options
+parsoid=wt2html
+!! wikitext
+''''foo'''''
+!! html/php
+<p>'<b>foo</b>
+</p>
+!! html/parsoid
+<p>'<b>foo</b><i></i>
+</p>
+!!end
+
+# same html as previous, but wikitext adjusted to match parsoid html2wt
+!! test
+Italics and bold: 4-quote opening sequence: (4,5+2) w/ nowiki
+!! wikitext
+'<nowiki/>'''foo'''''<nowiki/>''
+!! html/php
+<p>'<b>foo</b>
+</p>
+!! html/parsoid
+<p>'<b>foo</b><i></i>
+</p>
+!! end
+
+
+###
+### 5-quote opening sequence tests
+###
+
+!! test
+Italics and bold: 5-quote opening sequence: (5,2)
+!! options
+parsoid=wt2html
+!! wikitext
+'''''foo''
+!! html/*
+<p><b><i>foo</i></b>
+</p>
+!!end
+
+# same html as previous, but wikitext adjusted to match parsoid html2wt
+!! test
+Italics and bold: 5-quote opening sequence: (5,2+3)
+!! wikitext
+'''''foo'''''
+!! html/*
+<p><i><b>foo</b></i>
+</p>
+!! end
+
+!! test
+Italics and bold: 5-quote opening sequence: (5,3)
+!! options
+parsoid=wt2html
+!! wikitext
+'''''foo'''
+!! html/*
+<p><i><b>foo</b></i>
+</p>
+!!end
+
+# same html as previous, but wikitext adjusted to match parsoid html2wt
+!! test
+Italics and bold: 5-quote opening sequence: (5,3+2)
+!! wikitext
+'''''foo'''''
+!! html
+<p><i><b>foo</b></i>
+</p>
+!! end
+
+!! test
+Italics and bold: 5-quote opening sequence: (5,4)
+!! options
+parsoid=wt2html
+!! wikitext
+'''''foo''''
+!! html/*
+<p><i><b>foo'</b></i>
+</p>
+!!end
+
+!! test
+Italics and bold: 5-quote opening sequence: (5,5)
+!! wikitext
+'''''foo'''''
+!! html
+<p><i><b>foo</b></i>
+</p>
+!!end
+
+!! test
+Italics and bold: 5-quote opening sequence: (5,6)
+!! wikitext
+'''''foo''''''
+!! html/*
+<p><i><b>foo'</b></i>
+</p>
+!! end
+
+###
+### multiple quote sequences in a line
+###
+
+!! test
+Italics and bold: multiple quote sequences: (2,4,2)
+!! options
+parsoid=wt2html
+!! wikitext
+''foo''''bar''
+!! html/*
+<p><i>foo'<b>bar</b></i>
+</p>
+!! end
+
+# same html as previous, but wikitext adjusted to match parsoid html2wt
+!! test
+Italics and bold: multiple quote sequences: (2,4,2+3) w/ nowiki
+!! wikitext
+''foo'<nowiki/>'''bar'''''
+!! html
+<p><i>foo'<b>bar</b></i>
+</p>
+!! end
+
+!! test
+Italics and bold: multiple quote sequences: (2,4,3)
+!! options
+parsoid=wt2html
+!! wikitext
+''foo''''bar'''
+!! html/*
+<p><i>foo'<b>bar</b></i>
+</p>
+!! end
+
+# same html as previous, but wikitext adjusted to match parsoid html2wt
+!! test
+Italics and bold: multiple quote sequences: (2,4,3+2) w/ nowiki
+!! wikitext
+''foo'<nowiki/>'''bar'''''
+!! html
+<p><i>foo'<b>bar</b></i>
+</p>
+!! end
+
+!! test
+Italics and bold: multiple quote sequences: (2,4,4)
+!! options
+parsoid=wt2html
+!! wikitext
+''foo''''bar''''
+!! html/*
+<p><i>foo'<b>bar'</b></i>
+</p>
+!! end
+
+# same html as previous, but wikitext adjusted to match parsoid html2wt
+!! test
+Italics and bold: multiple quote sequences: (2,4,4+2) w/ nowiki
+!! wikitext
+''foo'<nowiki/>'''bar'<nowiki/>'''''
+!! html
+<p><i>foo'<b>bar'</b></i>
+</p>
+!! end
+
+# The PHP parser strips the empty tags out for giggles; parsoid doesn't.
+!! test
+Italics and bold: multiple quote sequences: (3,4,2)
+!! options
+parsoid=wt2html
+!! wikitext
+'''foo''''bar''
+!! html/php
+<p><b>foo'</b>bar
+</p>
+!! html/parsoid
+<p><b>foo'</b>bar<i></i>
+</p>
+!! end
+
+# same html as previous, but wikitext adjusted to match parsoid html2wt
+!! test
+Italics and bold: multiple quote sequences: (3,4,2+2) w/ nowiki
+!! wikitext
+'''foo''''bar''<nowiki/>''
+!! html/php
+<p><b>foo'</b>bar
+</p>
+!! html/parsoid
+<p><b>foo'</b>bar<i></i>
+</p>
+!! end
+
+# The PHP parser strips the empty tags out for giggles; parsoid doesn't.
+!! test
+Italics and bold: multiple quote sequences: (3,4,3)
+!! options
+parsoid=wt2html
+!! wikitext
+'''foo''''bar'''
+!! html/php
+<p><b>foo'</b>bar
+</p>
+!! html/parsoid
+<p><b>foo'</b>bar<b></b>
+</p>
+!! end
+
+# same html as previous, but wikitext adjusted to match parsoid html2wt
+!! test
+Italics and bold: multiple quote sequences: (3,4,3+3) w/ nowiki
+!! wikitext
+'''foo''''bar'''<nowiki/>'''
+!! html/php
+<p><b>foo'</b>bar
+</p>
+!! html/parsoid
+<p><b>foo'</b>bar<b></b>
+</p>
+!! end
+
+###
+### other quote tests
+###
+!! test
+Italics and bold: other quote tests: (2,3,5)
+!! wikitext
+''this is about '''foo's family'''''
+!! html
+<p><i>this is about <b>foo's family</b></i>
+</p>
+!!end
+
+
+!! test
+Italics and bold: other quote tests: (2,(3,3),2)
+!! wikitext
+''this is about '''foo's''' family''
+!! html
+<p><i>this is about <b>foo's</b> family</i>
+</p>
+!!end
+
+
+!! test
+Italics and bold: other quote tests: (3,2,3,2)
+!! options
+parsoid=wt2html
+!! wikitext
+'''this is about ''foo'''s family''
+!! html/*
+<p><b>this is about <i>foo</i></b><i>s family</i>
+</p>
+!!end
+
+
+# same html as previous, but wikitext adjusted to match parsoid html2wt
+!! test
+Italics and bold: other quote tests: (3,2,3+2+2,2)
+!! wikitext
+'''this is about ''foo'''''<nowiki/>''s family''
+!! html
+<p><b>this is about <i>foo</i></b><i>s family</i>
+</p>
+!! end
+
+
+!! test
+Italics and bold: other quote tests: (3,2,3,3)
+!! wikitext
+'''this is about ''foo'''s family'''
+!! html/*
+<p>'<i>this is about </i>foo<b>s family</b>
+</p>
+!!end
+
+
+!! test
+Italics and bold: other quote tests: (3,(2,2),3)
+!! wikitext
+'''this is about ''foo's'' family'''
+!! html
+<p><b>this is about <i>foo's</i> family</b>
+</p>
+!!end
+
+
+!! test
+Italicized possessive
+!! wikitext
+The ''[[Main Page]]'''s talk page.
+!! html/php
+<p>The <i><a href="/wiki/Main_Page" title="Main Page">Main Page</a>'</i>s talk page.
+</p>
+!! html/parsoid
+<p>The <i><a rel="mw:WikiLink" href="./Main_Page" title="Main Page">Main Page</a>'</i>s talk page.</p>
+!! end
+
+!! test
+Quote balancing context should be restricted to td/th cells on the same wikitext line
+!! options
+parsoid=wt2html,wt2wt
+!! wikitext
+{|
+!''a!!''b
+|''a||''b
+|}
+!! html+tidy
+<table>
+<tbody><tr>
+<th><i>a</i></th>
+<th><i>b</i>
+</th>
+<td><i>a</i></td>
+<td><i>b</i>
+</td></tr></tbody></table>
+!! end
+
+###
+### Non-html5 tags
+###
+
+!! test
+Non-html5 tags should be accepted
+!! wikitext
+<center>''foo''</center>
+<big>''foo''</big>
+<font>''foo''</font>
+<strike>''foo''</strike>
+<tt>''foo''</tt>
+!! html
+<center><i>foo</i></center>
+<p><big><i>foo</i></big>
+<font><i>foo</i></font>
+<strike><i>foo</i></strike>
+<tt><i>foo</i></tt>
+</p>
+!! end
+
+!! test
+<wbr> is valid wikitext (T54468)
+!! wikitext
+<wbr>
+!! html
+<p><wbr />
+</p>
+!! end
+
+# <strike> is HTML4, <s> is HTML4/5.
+!! test
+<s> or <strike> for strikethrough
+!! wikitext
+<strike>strike</strike>
+
+<s>s</s>
+!! html
+<p><strike>strike</strike>
+</p><p><s>s</s>
+</p>
+!! end
+
+## a not permitted
+## i,b,br omitted
+!! test
+Text-level semantic html elements in wikitext
+!! wikitext
+<em>text</em>
+<strong>text</strong>
+<small>text</small>
+<s>text</s>
+<cite>text</cite>
+<q>text</q>
+<dfn>text</dfn>
+<abbr>text</abbr>
+<data>text</data>
+<time>text</time>
+<code>text</code>
+<var>text</var>
+<samp>text</samp>
+<kbd>text</kbd>
+<sub>text</sub>
+<u>text</u>
+<mark>text</mark>
+<ruby><rb>明日</rb><rp>(</rp><rt>Ashita</rt><rp> </rp><rtc>あした</rtc><rp>)</rp></ruby>
+<bdi>text</bdi>
+<bdo>text</bdo>
+<span>text</span>
+<wbr />
+!! html
+<p><em>text</em>
+<strong>text</strong>
+<small>text</small>
+<s>text</s>
+<cite>text</cite>
+<q>text</q>
+<dfn>text</dfn>
+<abbr>text</abbr>
+<data>text</data>
+<time>text</time>
+<code>text</code>
+<var>text</var>
+<samp>text</samp>
+<kbd>text</kbd>
+<sub>text</sub>
+<u>text</u>
+<mark>text</mark>
+<ruby><rb>明日</rb><rp>(</rp><rt>Ashita</rt><rp> </rp><rtc>あした</rtc><rp>)</rp></ruby>
+<bdi>text</bdi>
+<bdo>text</bdo>
+<span>text</span>
+<wbr />
+</p>
+!! end
+
+# test cases taken from
+# https://www.w3.org/TR/html5/text-level-semantics.html#the-ruby-element
+!! test
+Ruby markup (W3C-style)
+!! wikitext
+;Mono-ruby for individual base characters
+:<ruby>日<rt>に</rt>本<rt>ほん</rt>語<rt>ご</rt></ruby>
+;Group ruby
+:<ruby>今日<rt>きょう</rt></ruby>
+;Jukugo ruby
+:<ruby>法<rb>華</rb><rb>経</rb><rt>ほ</rt><rt>け</rt><rt>きょう</rt></ruby>
+;Inline ruby
+:<ruby>東<rb>京</rb><rp>(</rp><rt>とう</rt><rt>きょう</rt><rp>)</rp></ruby>
+;Double-sided ruby
+:<ruby><rb>旧</rb><rb>金</rb><rb>山</rb><rt>jiù</rt><rt>jīn</rt><rt>shān</rt><rtc>San Francisco</rtc></ruby>
+
+<ruby>
+<rb>♥</rb><rtc><rt>Heart</rt></rtc><rtc lang="fr"><rt>Cœur</rt></rtc>
+<rb>☘</rb><rtc><rt>Shamrock</rt></rtc><rtc lang="fr"><rt>Trèfle</rt></rtc>
+<rb>✶</rb><rtc><rt>Star</rt></rtc><rtc lang="fr"><rt>Étoile</rt></rtc>
+</ruby>
+!! html
+<dl><dt>Mono-ruby for individual base characters</dt>
+<dd><ruby>日<rt>に</rt>本<rt>ほん</rt>語<rt>ご</rt></ruby></dd>
+<dt>Group ruby</dt>
+<dd><ruby>今日<rt>きょう</rt></ruby></dd>
+<dt>Jukugo ruby</dt>
+<dd><ruby>法<rb>華</rb><rb>経</rb><rt>ほ</rt><rt>け</rt><rt>きょう</rt></ruby></dd>
+<dt>Inline ruby</dt>
+<dd><ruby>東<rb>京</rb><rp>(</rp><rt>とう</rt><rt>きょう</rt><rp>)</rp></ruby></dd>
+<dt>Double-sided ruby</dt>
+<dd><ruby><rb>旧</rb><rb>金</rb><rb>山</rb><rt>jiù</rt><rt>jīn</rt><rt>shān</rt><rtc>San Francisco</rtc></ruby></dd></dl>
+<p><ruby>
+<rb>♥</rb><rtc><rt>Heart</rt></rtc><rtc lang="fr"><rt>Cœur</rt></rtc>
+<rb>☘</rb><rtc><rt>Shamrock</rt></rtc><rtc lang="fr"><rt>Trèfle</rt></rtc>
+<rb>✶</rb><rtc><rt>Star</rt></rtc><rtc lang="fr"><rt>Étoile</rt></rtc>
+</ruby>
+</p>
+!! end
+
+# The next two test different paths in the sanitizer.
+!! test
+Non-word characters don't terminate tag names (T19663, T42670, T54022)
+!! wikitext
+<blockquote|>a</blockquote>
+
+<b→> doesn't terminate </b→>
+
+<bä> doesn't terminate </bä>
+
+<boo> doesn't terminate </boo>
+
+<s.foo> doesn't terminate </s.foo>
+
+<sub-ID#1>
+!! html
+<p>&lt;blockquote|&gt;a&lt;/blockquote&gt;
+</p><p>&lt;b→&gt; doesn't terminate &lt;/b→&gt;
+</p><p>&lt;bä&gt; doesn't terminate &lt;/bä&gt;
+</p><p>&lt;boo&gt; doesn't terminate &lt;/boo&gt;
+</p><p>&lt;s.foo&gt; doesn't terminate &lt;/s.foo&gt;
+</p><p>&lt;sub-ID#1&gt;
+</p>
+!! end
+
+!! test
+Non-word characters don't terminate tag names
+!! wikitext
+<blockquote|>a</blockquote>
+
+<b→> doesn't terminate </b→>
+
+<bä> doesn't terminate </bä>
+
+<boo> doesn't terminate </boo>
+
+<s.foo> doesn't terminate </s.foo>
+
+<sub-ID#1>
+!! html+tidy
+<p>&lt;blockquote|&gt;a
+</p><p>&lt;b→&gt; doesn't terminate &lt;/b→&gt;
+</p><p>&lt;bä&gt; doesn't terminate &lt;/bä&gt;
+</p><p>&lt;boo&gt; doesn't terminate &lt;/boo&gt;
+</p><p>&lt;s.foo&gt; doesn't terminate &lt;/s.foo&gt;
+</p><p>&lt;sub-ID#1&gt;
+</p>
+!! end
+
+###
+### See tests/parser/parserTestsParserHook.php for the <tåg> extension)
+### This checks that HTML5 tags (with non-word characters in the tag
+### name) make it safely through the parser -- the Sanitizer will
+### munge them later, as it should.
+###
+!! test
+Non-word characters are valid in extension tags (T19663)
+!! wikitext
+<tåg>tåg</tåg>
+!! html/php
+<pre>
+'tåg'
+array (
+)
+</pre>
+
+!! html/parsoid
+<pre typeof="mw:Extension/tåg" data-mw='{"name":"tåg","attrs":{},"body":{"extsrc":"tåg"}}' data-parsoid='{}' about="#mwt2"></pre>
+!! end
+
+!! test
+Isolated close tags should be treated as literal text (T54760)
+!! options
+parsoid=wt2html
+!! wikitext
+</b>
+
+<s.foo>s</s>
+!! html/php+tidy
+<p class="mw-empty-elt">
+</p><p>&lt;s.foo&gt;s
+</p>
+!! html/parsoid
+<p>&lt;s.foo&gt;s</p>
+!! end
+
+###
+### Special characters
+###
+
+!! test
+Bare pipe character (T54363)
+!! wikitext
+|
+!! html
+<p>|
+</p>
+!! end
+
+!! test
+Bare pipe character from a template (T54363)
+!! wikitext
+{{pipe}}
+!! html
+<p>|
+</p>
+!! end
+
+###
+### <nowiki> test cases
+###
+
+!! test
+<nowiki> unordered list
+!! wikitext
+<nowiki>* This is not an unordered list item.</nowiki>
+!! html/php
+<p>* This is not an unordered list item.
+</p>
+!! html/parsoid
+<p><span typeof="mw:Nowiki">* This is not an unordered list item.</span></p>
+!! end
+
+!! test
+<nowiki> spacing
+!! wikitext
+<nowiki>Lorem ipsum dolor
+
+sed abit.
+ sed nullum.
+
+:and a colon
+</nowiki>
+!! html/php
+<p>Lorem ipsum dolor
+
+sed abit.
+ sed nullum.
+
+:and a colon
+
+</p>
+!! html/parsoid
+<p><span typeof="mw:Nowiki">Lorem ipsum dolor
+
+sed abit.
+ sed nullum.
+
+:and a colon
+</span></p>
+!! end
+
+!! test
+Don't parse <nowiki><span class="error"></nowiki> (T149622)
+!! wikitext
+<nowiki><span class="error"></nowiki>
+!! html/php
+<p>&lt;span class="error"&gt;
+</p>
+!! html/parsoid
+<p><span typeof="mw:Nowiki">&lt;span class="error"></span></p>
+!! end
+
+!! test
+nowiki 3
+!! wikitext
+:There is not nowiki.
+:There is <nowiki>nowiki</nowiki>.
+
+#There is not nowiki.
+#There is <nowiki>nowiki</nowiki>.
+
+*There is not nowiki.
+*There is <nowiki>nowiki</nowiki>.
+!! html/php
+<dl><dd>There is not nowiki.</dd>
+<dd>There is nowiki.</dd></dl>
+<ol><li>There is not nowiki.</li>
+<li>There is nowiki.</li></ol>
+<ul><li>There is not nowiki.</li>
+<li>There is nowiki.</li></ul>
+
+!! html/parsoid
+<dl><dd data-parsoid='{}'>There is not nowiki.</dd>
+<dd data-parsoid='{}'>There is <span typeof="mw:Nowiki">nowiki</span>.</dd></dl>
+
+<ol><li data-parsoid='{}'>There is not nowiki.</li>
+<li data-parsoid='{}'>There is <span typeof="mw:Nowiki">nowiki</span>.</li></ol>
+
+<ul><li data-parsoid='{}'>There is not nowiki.</li>
+<li data-parsoid='{}'>There is <span typeof="mw:Nowiki">nowiki</span>.</li></ul>
+!! end
+
+!! test
+Entities inside <nowiki>
+!! wikitext
+<nowiki>&lt;</nowiki>
+!! html/php
+<p>&lt;
+</p>
+!! html/parsoid
+<p><span typeof="mw:Nowiki"><span typeof="mw:Entity" data-parsoid='{"src":"&amp;lt;","srcContent":"&lt;"}'>&lt;</span></span></p>
+!! end
+
+!! test
+Entities inside template parameters
+!! wikitext
+{{echo|&ndash;}}
+!! html/php+tidy
+<p>&#8211;
+</p>
+!! html/parsoid
+<p><span typeof="mw:Transclusion mw:Entity" data-mw='{"parts":[{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"&amp;ndash;"}},"i":0}}]}'>&ndash;</span></p>
+!! end
+
+!! test
+Properly escape nowiki when combined with other wiki markup
+!! options
+parsoid=html2wt
+!! html/parsoid
+<p>* &lt;/nowiki&gt; tag</p>
+!! wikitext
+<nowiki>*</nowiki> <nowiki>&lt;/nowiki&gt;</nowiki> tag
+!! end
+
+!! test
+T93824: Put escaped HTML tags inside nowiki
+!! options
+parsoid=html2wt
+!! html/parsoid
+<p>&lt;h2&gt;foo&lt;/h2&gt;</p>
+!! wikitext
+<nowiki><h2>foo</h2></nowiki>
+!! end
+
+!! test
+T71950: 1. Put nowiki as close to cause as possible, even with non-quote escapable chars
+!! options
+parsoid=html2wt
+!! html/parsoid
+<p>This text: L'<a rel="mw:WikiLink" href="./Foo">Foo</a>
+This text: L''<a rel="mw:WikiLink" href="./Foo">Foo</a>
+This text: L'''<a rel="mw:WikiLink" href="./Foo">Foo</a>''</p>
+!! wikitext
+This text: L'[[Foo]]
+This text: L<nowiki>''</nowiki>[[Foo]]
+This text: L<nowiki>'''</nowiki>[[Foo]]<nowiki>''</nowiki>
+!! end
+
+# This test fails because wikitext whitespace is not normalized before comparing.
+!! test
+T71950: 2. Put nowiki as close to cause as possible, after ' :'
+!! options
+parsoid=html2wt
+!! html/parsoid
+<p>This text : L''<a rel="mw:WikiLink" href="./Foo">Foo</a>
+</p>
+!! wikitext
+This text : L<nowiki>''</nowiki>[[Foo]]
+!! end
+
+# This test and the next one are html2wt only as they test that incorrect wikitext
+# passed in template arguments gets escaped or wrapped in nowikis where required.
+!! test
+T71482: Use {{!}} instead of nowiki for single pipe in template argument
+!! options
+parsoid=html2wt
+!! html/parsoid
+<p><span typeof="mw:Transclusion" data-mw="{&quot;parts&quot;:[{&quot;template&quot;:{&quot;target&quot;:{&quot;wt&quot;:&quot;echo&quot;,&quot;href&quot;:&quot;./Template:Echo&quot;},&quot;params&quot;:{&quot;1&quot;:{&quot;wt&quot;:&quot;foo|bar&quot;}},&quot;i&quot;:0}}]}" about="#mwt1"></span>
+<span typeof="mw:Transclusion" data-mw="{&quot;parts&quot;:[{&quot;template&quot;:{&quot;target&quot;:{&quot;wt&quot;:&quot;echo&quot;,&quot;href&quot;:&quot;./Template:Echo&quot;},&quot;params&quot;:{&quot;1&quot;:{&quot;wt&quot;:&quot;foo|bar |[[&quot;}},&quot;i&quot;:0}}]}" about="#mwt2"></p>
+!! wikitext
+{{echo|foo{{!}}bar}}
+{{echo|<nowiki>foo|bar |[[</nowiki>}}
+!! end
+
+!! test
+T53961: Output correct nowikis in template arguments
+!! options
+parsoid=html2wt
+!! html/parsoid
+<p><span typeof="mw:Transclusion" data-mw="{&quot;parts&quot;:[{&quot;template&quot;:{&quot;target&quot;:{&quot;wt&quot;:&quot;echo&quot;,&quot;href&quot;:&quot;./Template:Echo&quot;},&quot;params&quot;:{&quot;1&quot;:{&quot;wt&quot;:&quot;a [ b&quot;}},&quot;i&quot;:0}}]}" about="#mwt1"></span>
+<span typeof="mw:Transclusion" data-mw="{&quot;parts&quot;:[{&quot;template&quot;:{&quot;target&quot;:{&quot;wt&quot;:&quot;echo&quot;,&quot;href&quot;:&quot;./Template:Echo&quot;},&quot;params&quot;:{&quot;1&quot;:{&quot;wt&quot;:&quot;a }} b&quot;}},&quot;i&quot;:0}}]}" about="#mwt2"></span>
+<span typeof="mw:Transclusion" data-mw="{&quot;parts&quot;:[{&quot;template&quot;:{&quot;target&quot;:{&quot;wt&quot;:&quot;echo&quot;,&quot;href&quot;:&quot;./Template:Echo&quot;},&quot;params&quot;:{&quot;1&quot;:{&quot;wt&quot;:&quot;a [[ b&quot;}},&quot;i&quot;:0}}]}" about="#mwt3"></span>
+<span typeof="mw:Transclusion" data-mw="{&quot;parts&quot;:[{&quot;template&quot;:{&quot;target&quot;:{&quot;wt&quot;:&quot;echo&quot;,&quot;href&quot;:&quot;./Template:Echo&quot;},&quot;params&quot;:{&quot;1&quot;:{&quot;wt&quot;:&quot;a | {{ ]]&quot;}},&quot;i&quot;:0}}]}" about="#mwt4"></span>
+<span typeof="mw:Transclusion" data-mw="{&quot;parts&quot;:[{&quot;template&quot;:{&quot;target&quot;:{&quot;wt&quot;:&quot;echo&quot;,&quot;href&quot;:&quot;./Template:Echo&quot;},&quot;params&quot;:{&quot;1&quot;:{&quot;wt&quot;:&quot;a }&quot;}},&quot;i&quot;:0}}]}" about="#mwt5"></span></p>
+!! wikitext
+{{echo|a [ b}}
+{{echo|<nowiki>a }} b</nowiki>}}
+{{echo|<nowiki>a [[ b</nowiki>}}
+{{echo|<nowiki>a | {{ ]]</nowiki>}}
+{{echo|a <nowiki>}</nowiki>}}
+!! end
+
+!! test
+Cases where "!!" needs nowiki protection
+!! options
+parsoid=html2wt
+!! html/parsoid
+<table>
+<tr><th>this needs protection !! here</th></tr>
+</table>
+
+<table>
+<tr><th>this does not need
+protection !! here</th></tr>
+</table>
+!! wikitext
+{|
+!<nowiki>this needs protection !! here</nowiki>
+|}
+
+{|
+!this does not need
+protection !! here
+|}
+!! end
+
+###
+### Comments
+###
+!! test
+Comments and Indent-Pre
+!! wikitext
+<!-- comment 1 --> asdf
+
+<!-- comment 1 --> asdf
+<!-- comment 2 -->
+
+<!-- comment 1 --> asdf
+<!-- comment 2 -->xyz
+
+<!-- comment 1 --> asdf
+<!-- comment 2 --> xyz
+!! html
+<pre>asdf
+</pre>
+<pre>asdf
+</pre>
+<pre>asdf
+</pre>
+<p>xyz
+</p>
+<pre>asdf
+xyz
+</pre>
+!! end
+
+!! test
+Comment test 2a
+!! wikitext
+asdf
+<!-- comment 1 -->
+jkl
+!! html
+<p>asdf
+jkl
+</p>
+!! end
+
+!! test
+Comment test 2b
+!! wikitext
+asdf
+<!-- comment 1 -->
+
+jkl
+!! html
+<p>asdf
+</p><p>jkl
+</p>
+!! end
+
+!! test
+Comment test 3
+!! wikitext
+asdf
+<!-- comment 1 -->
+<!-- comment 2 -->
+jkl
+!! html
+<p>asdf
+jkl
+</p>
+!! end
+
+!! test
+Comment test 4
+!! wikitext
+asdf<!-- comment 1 -->jkl
+!! html
+<p>asdfjkl
+</p>
+!! end
+
+!! test
+Comment spacing
+!! wikitext
+a
+ <!-- foo --> b <!-- bar -->
+c
+!! html
+<p>a
+</p>
+<pre> b
+</pre>
+<p>c
+</p>
+!! end
+
+!! test
+Comment whitespace
+!! wikitext
+<!-- returns a single newline, not nothing, since the newline after > is not stripped -->
+!! html
+
+!! end
+
+!! test
+Comment semantics and delimiters
+!! wikitext
+<!-- --><!----><!-----><!------>
+!! html/php
+
+!! html/parsoid
+<!-- --><!----><!--&#x2D;--><!--&#x2D;&#x2D;-->
+!! end
+
+!! test
+Comment semantics and delimiters, redux
+!! wikitext
+<!-- In SGML every "foo" here would actually show up in the text -- foo -- bar
+-- foo -- funky huh? ... -->
+!! html/php
+
+!! html/parsoid
+<!-- In SGML every "foo" here would actually show up in the text &#x2D;&#x2D; foo &#x2D;&#x2D; bar
+&#x2D;&#x2D; foo &#x2D;&#x2D; funky huh? ... -->
+!! end
+
+!! test
+Comment semantics and delimiters: directors cut
+!! wikitext
+<!-- ... However we like to keep things simple and somewhat XML-ish so we eat
+everything starting with < followed by !-- until the first -- and > we see,
+that wouldn't be valid XML however, since in XML -- has to terminate a comment
+-->-->
+!! html/php
+<p>--&gt;
+</p>
+!! html/parsoid
+<!-- ... However we like to keep things simple and somewhat XML&#x2D;ish so we eat
+everything starting with < followed by !&#x2D;&#x2D; until the first &#x2D;&#x2D; and &#x3E; we see,
+that wouldn't be valid XML however, since in XML &#x2D;&#x2D; has to terminate a comment
+--><p>--></p>
+!! end
+
+!! test
+Comment semantics: nesting
+!! wikitext
+<!--<!-- no, we're not going to do anything fancy here -->-->
+!! html/php
+<p>--&gt;
+</p>
+!! html/parsoid
+<!--<!&#x2D;&#x2D; no, we're not going to do anything fancy here --><p>--></p>
+!! end
+
+# Parsoid closes the unclosed comment, even if it means a slight
+# round-trip diff.
+!! test
+Comment semantics: unclosed comment at end
+!! options
+parsoid=wt2html,html2html
+!! wikitext
+<!--This comment will run out to the end of the document
+!! html/php
+
+!! html/parsoid
+<!--This comment will run out to the end of the document-->
+!! end
+
+!! test
+Comment semantics: normalize comments to play nice with XML and browsers
+!! wikitext
+<!-- Browsers --!> think this is closed -->
+<!--> This would normally be text -->
+<!---> As would this -->
+<!-- XML doesn't like trailing dashes -------->
+<!-- Nor doubled hyphens -- anywhere in the data -->
+But this is not a comment.
+!! html/php
+<p>But this is not a comment.
+</p>
+!! html/parsoid
+<!-- Browsers &#x2D;&#x2D;!&#x3E; think this is closed -->
+<!--&#x3E; This would normally be text -->
+<!--&#x2D;&#x3E; As would this -->
+<!-- XML doesn't like trailing dashes &#x2D;&#x2D;&#x2D;&#x2D;&#x2D;&#x2D;-->
+<!-- Nor doubled hyphens &#x2D;&#x2D; anywhere in the data -->
+<p>But this is not a comment.</p>
+!! end
+
+!! test
+Comment semantics: round-trip even text which contains encoded -->
+!! wikitext
+<!-- hello & goodbye - > --&gt; --&amp;gt; --&xx -->
+!! html/parsoid
+<!-- hello &#x26; goodbye &#x2D; &#x3E; &#x2D;&#x2D;&#x3E; &#x2D;&#x2D;&#x26;gt; &#x2D;&#x2D;&#x26;xx -->
+!! end
+
+!! test
+Comment in template title
+!! wikitext
+{{f<!---->oo}}
+!! html
+<p>FOO
+</p>
+!! end
+
+!! test
+Comment on its own line post-expand
+!! wikitext
+a
+{{blank}}<!---->
+b
+!! html
+<p>a
+</p><p>b
+</p>
+!! end
+
+!! test
+Comment on its own line post-expand with non-significant whitespace
+!! wikitext
+a
+ {{blank}} <!---->
+b
+!! html
+<p>a
+</p><p>b
+</p>
+!! end
+
+!! test
+Multiple comments should still parse as SOL-transparent
+!! options
+parsoid=wt2html,wt2wt
+!! wikitext
+<!--c1-->*a
+<!--c2--><!--c3--><!--c4-->*b
+!! html/php
+<ul><li>a</li>
+<li>b</li></ul>
+
+!! html/parsoid
+<!--c1--><ul>
+<li>a
+</li>
+<!--c2--><!--c3--><!--c4-->
+<li>b
+</li>
+</ul>
+!! end
+
+## Make sure ">" gets escaped in comments to avoid XSS
+!! test
+IE conditional comments
+!! wikitext
+<!--[if lt IE 9]>
+ <script>alert('hi');</script>
+<![endif]-->
+!! html/parsoid
+<!--[if lt IE 9]&#x3E;
+ <script&#x3E;alert('hi');</script&#x3E;
+<![endif]-->
+!! end
+
+###
+### paragraph wrapping tests
+###
+
+!! test
+No block tags
+!! wikitext
+a
+
+b
+!! html
+<p>a
+</p><p>b
+</p>
+!! end
+
+!! test
+Block tag on one line (<div>)
+!! wikitext
+a <div>foo</div>
+
+b
+!! html
+a <div>foo</div>
+<p>b
+</p>
+!! html+tidy
+<p>a </p><div>foo</div>
+<p>b
+</p>
+!! end
+
+# Remex wraps empty tag runs with p-tags.
+# Parsoid strips them out during p-wrapping.
+!! test
+No p-wrappable content
+!! wikitext
+<span><div>x</div></span>
+<span><s><div>x</div></s></span>
+<small><em></em></small><span><s><div>x</div></s></span>
+!! html/php+tidy
+<span><div>x</div></span>
+<span><s><div>x</div></s></span>
+<p><small><em></em></small></p><span><s><div>x</div></s></span>
+!! html/parsoid
+<span><div>x</div></span>
+<span><s><div>x</div></s></span>
+<small><em></em></small><span><s><div>x</div></s></span>
+!! end
+
+# T177612: Parsoid-only test
+!! test
+Transclusion meta tags shouldn't trip Parsoid's useless p-wrapper stripping code
+!! wikitext
+{{echo|<span><div>x</div></span>}}
+x
+!! html/parsoid
+<span about="#mwt1" typeof="mw:Transclusion" data-parsoid='{"stx":"html","pi":[[{"k":"1"}]]}' data-mw='{"parts":[{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"&lt;span>&lt;div>x&lt;/div>&lt;/span>"}},"i":0}}]}'><div>x</div></span>
+<p>x</p>
+!! end
+
+!! test
+Block tag on one line (<blockquote>)
+!! wikitext
+a <blockquote>foo</blockquote>
+
+b
+!! html
+a <blockquote>foo</blockquote>
+<p>b
+</p>
+!! html+tidy
+<p>a </p><blockquote><p>foo</p></blockquote>
+<p>b
+</p>
+!! end
+
+!! test
+Block tag on both lines (<div>)
+!! wikitext
+a <div>foo</div>
+
+b <div>foo</div>
+!! html
+a <div>foo</div>
+b <div>foo</div>
+
+!! html+tidy
+<p>a </p><div>foo</div><p>
+b </p><div>foo</div>
+!! end
+
+!! test
+Block tag on both lines (<blockquote>)
+!! wikitext
+a <blockquote>foo</blockquote>
+
+b <blockquote>foo</blockquote>
+!! html
+a <blockquote>foo</blockquote>
+b <blockquote>foo</blockquote>
+
+!! html+tidy
+<p>a </p><blockquote><p>foo</p></blockquote><p>
+b </p><blockquote><p>foo</p></blockquote>
+!! end
+
+!! test
+Multiple lines without block tags
+!! wikitext
+<div>foo</div> a
+b
+c
+d<!--foo--> e
+x <div>foo</div> z
+!! html
+<div>foo</div> a
+<p>b
+c
+d e
+</p>
+x <div>foo</div> z
+
+!! html+tidy
+<div>foo</div><p> a
+</p><p>b
+c
+d e
+</p><p>
+x </p><div>foo</div><p> z
+</p>
+!! end
+
+# The difference between Parsoid & Remex here
+# is because of Parsoid's Tidy-emulation code
+# for p-wrapping. We'll start work to remove this
+# emulation code in Parsoid sooner than later.
+# Remex wraps empty tag runs with p-tags.
+# Parsoid strips them out in a separate pass.
+!! test
+Empty lines between lines with block tags
+!! wikitext
+<div></div>
+
+
+<div></div>a
+
+b
+<div>a</div>b
+
+<div>b</div>d
+
+
+<div>e</div>
+!! html
+<div></div>
+<p><br />
+</p>
+<div></div>a
+<p>b
+</p>
+<div>a</div>b
+<div>b</div>d
+<p><br />
+</p>
+<div>e</div>
+
+!! html+tidy
+<div></div>
+<p><br />
+</p>
+<div></div><p>a
+</p><p>b
+</p>
+<div>a</div><p>b
+</p><div>b</div><p>d
+</p><p><br />
+</p>
+<div>e</div>
+!! html/parsoid
+<div data-parsoid='{"stx":"html"}'></div>
+
+<p><br /></p>
+<div data-parsoid='{"stx":"html"}'></div><p>a</p>
+
+<p>b</p>
+<div data-parsoid='{"stx":"html"}'>a</div><p>b</p>
+
+<div data-parsoid='{"stx":"html"}'>b</div><p>d</p>
+
+<p><br /></p>
+<div data-parsoid='{"stx":"html"}'>e</div>
+!! end
+
+!! test
+Unclosed HTML p-tags should be handled properly
+!! wikitext
+<div><p>foo</div>
+a
+
+b
+!! html/php+tidy
+<div><p>foo</p></div>
+<p>a
+</p><p>b
+</p>
+!! html/parsoid
+<div data-parsoid='{"stx":"html"}'><p data-parsoid='{"stx":"html", "autoInsertedEnd":true}'>foo</p></div>
+<p>a</p>
+<p>b</p>
+!! end
+
+## SSS FIXME: I can come up with other scenarios where this doesn't work because
+## of eager output of buffered tokens in the p-wrapper. But, I'm going to ignore
+## them for now.
+!! test
+1. P-wrapping should leave sol-transparent tags outside p-tags where possible
+!! options
+parsoid=wt2html
+!! wikitext
+a [[Category:A1]] [[Category:A2]]
+[[Category:A3]]
+[[Category:A4]]
+!! html/parsoid
+<p>a</p>
+<link rel="mw:PageProp/Category" href="./Category:A1"/> <link rel="mw:PageProp/Category" href="./Category:A2"/> <link rel="mw:PageProp/Category" href="./Category:A3"/> <link rel="mw:PageProp/Category" href="./Category:A4"/>
+!! end
+
+!! test
+2. P-wrapping should leave sol-transparent tags outside p-tags where possible
+!! options
+parsoid=wt2html
+!! wikitext
+[[Category:A1]]a
+!! html/parsoid
+<link rel="mw:PageProp/Category" href="./Category:A1"/><p>a</p>
+!! end
+
+!! test
+No paragraph necessary for SOL transparent template
+!! wikitext
+<span><div>foo</div></span>
+[[Category:Foo]]
+
+<span><div>foo</div></span>
+{{echo|[[Category:Foo]]}}
+!! html/php
+<span><div>foo</div></span>
+<span><div>foo</div></span>
+
+!! html/parsoid
+<span data-parsoid='{"stx":"html"}'><div data-parsoid='{"stx":"html"}'>foo</div></span>
+<link rel="mw:PageProp/Category" href="./Category:Foo"/>
+
+<span data-parsoid='{"stx":"html"}'><div data-parsoid='{"stx":"html"}'>foo</div></span>
+<link rel="mw:PageProp/Category" href="./Category:Foo" about="#mwt1" typeof="mw:Transclusion" data-mw='{"parts":[{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"[[Category:Foo]]"}},"i":0}}]}'/>
+!! end
+
+!! test
+Avoid expanding multiline sol transparent template ranges unnecessarily
+!! wikitext
+hi
+
+
+{{echo|<br/>
+}}
+
+[[Category:Ho]]
+!! html/php
+<p>hi
+</p><p><br />
+<br />
+</p>
+!! html/parsoid
+<p>hi</p>
+
+<p><br />
+<br about="#mwt1" typeof="mw:Transclusion" data-parsoid='{}' data-mw='{"parts":[{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"&lt;br/>\n"}},"i":0}}]}'/><span about="#mwt1">
+</span></p>
+
+<link rel="mw:PageProp/Category" href="./Category:Ho" />
+!! end
+
+###
+### Preformatted text
+###
+
+!! test
+Preformatted text
+!! wikitext
+ This is some
+ Preformatted text
+ With ''italic''
+ And '''bold'''
+ And a [[Main Page|link]]
+!! html
+<pre>This is some
+Preformatted text
+With <i>italic</i>
+And <b>bold</b>
+And a <a href="/wiki/Main_Page" title="Main Page">link</a>
+</pre>
+!! end
+
+!! test
+Tabs don't trigger preformatted text
+!! wikitext
+ This is not
+ preformatted text.
+ This is preformatted text.
+ So is this.
+!! html/php
+<p> This is not
+ preformatted text.
+</p>
+<pre>This is preformatted text.
+ So is this.
+</pre>
+!! html/parsoid
+<p> This is not
+ preformatted text.</p>
+<pre>This is preformatted text.
+ So is this.</pre>
+!! end
+
+!! test
+Space before tab needs nowiki pre protection
+!! options
+parsoid=html2wt
+!! html/parsoid
+<p> a</p>
+!! wikitext
+<nowiki> </nowiki> a
+!! end
+
+!! test
+Ident preformatting with inline content
+!! wikitext
+ a
+ ''b''
+!! html
+<pre>a
+<i>b</i>
+</pre>
+!! end
+
+!! test
+<pre> with <nowiki> inside (compatibility with 1.6 and earlier)
+!! wikitext
+<pre><nowiki>
+<b>
+<cite>
+<em>
+</nowiki></pre>
+!! html
+<pre>
+&lt;b&gt;
+&lt;cite&gt;
+&lt;em&gt;
+</pre>
+
+!! end
+
+!! test
+Regression with preformatted in <center>
+!! wikitext
+<center>
+ Blah
+</center>
+!! html
+<center>
+<pre>Blah
+</pre>
+</center>
+
+!! end
+
+!! test
+T54763: Preformatted in <blockquote>
+!! wikitext
+<blockquote>
+ Blah
+{|
+|
+ indented cell (no pre-wrapping!)
+|}
+</blockquote>
+!! html
+<blockquote>
+<p> Blah
+</p>
+<table>
+<tr>
+<td>
+<p> indented cell (no pre-wrapping!)
+</p>
+</td></tr></table>
+</blockquote>
+
+!! end
+
+!! test
+T53086: Double newlines in blockquotes should be turned into paragraphs
+!! wikitext
+<blockquote>
+Foo
+
+Bar
+</blockquote>
+!! html
+<blockquote>
+<p>Foo
+</p><p>Bar
+</p>
+</blockquote>
+
+!! end
+
+!! test
+T17491: <ins>/<del> in blockquote
+!! wikitext
+<blockquote>
+Foo <del>bar</del> <ins>baz</ins> quux
+</blockquote>
+!! html
+<blockquote>
+<p>Foo <del>bar</del> <ins>baz</ins> quux
+</p>
+</blockquote>
+
+!! html+tidy
+<blockquote>
+<p>Foo <del>bar</del> <ins>baz</ins> quux
+</p>
+</blockquote>
+!! end
+
+!! test
+T17491: <ins>/<del> in blockquote (2)
+!! wikitext
+<blockquote>Foo <del>bar</del> <ins>baz</ins> quux
+</blockquote>
+!! html
+<blockquote>Foo <del>bar</del> <ins>baz</ins> quux
+</blockquote>
+
+!! html+tidy
+<blockquote><p>Foo <del>bar</del> <ins>baz</ins> quux
+</p></blockquote>
+!! end
+
+!! test
+<pre> with attributes (T5202)
+!! wikitext
+<pre style="background: blue; color:white">Bluescreen of WikiDeath</pre>
+!! html
+<pre style="background: blue; color:white">Bluescreen of WikiDeath</pre>
+
+!! end
+
+!! test
+<pre> with width attribute (T5202)
+!! wikitext
+<pre width="8">Narrow screen goodies</pre>
+!! html
+<pre width="8">Narrow screen goodies</pre>
+
+!! end
+
+!! test
+<pre> with forbidden attribute (T5202)
+!! wikitext
+<pre width="8" onmouseover="alert(document.cookie)">Narrow screen goodies</pre>
+!! html
+<pre width="8">Narrow screen goodies</pre>
+
+!! end
+
+!! test
+Entities inside <pre>
+!! wikitext
+<pre>&lt;</pre>
+!! html
+<pre>&lt;</pre>
+
+!! end
+
+!! test
+<pre> with forbidden attribute values (T5202)
+!! wikitext
+<pre width="8" style="border-width: expression(alert(document.cookie))">Narrow screen goodies</pre>
+!! html
+<pre width="8" style="/* insecure input */">Narrow screen goodies</pre>
+
+!! end
+
+!! test
+<nowiki> inside <pre> (T15238)
+!! wikitext
+<pre>
+<nowiki>
+</pre>
+<pre>
+<nowiki></nowiki>
+</pre>
+<pre><nowiki><nowiki></nowiki>Foo<nowiki></nowiki></nowiki></pre>
+!! html
+<pre>
+&lt;nowiki&gt;
+</pre>
+<pre>
+
+</pre>
+<pre>&lt;nowiki&gt;Foo&lt;/nowiki&gt;</pre>
+
+!! end
+
+!! test
+<nowiki> inside of #tag:pre
+!! wikitext
+{{#tag:pre|Foo <nowiki>&rarr;bar</nowiki>}}
+!! html/php
+<pre>Foo &#8594;bar</pre>
+
+!! html/parsoid
+<pre about="#mwt1" typeof="mw:Transclusion" data-parsoid='{"pi":[[{"k":"1"}]]}' data-mw='{"parts":[{"template":{"target":{"wt":"#tag:pre","function":"tag"},"params":{"1":{"wt":"Foo &lt;nowiki>&amp;rarr;bar&lt;/nowiki>"}},"i":0}}]}'>Foo <span typeof="mw:Entity">→</span>bar</pre>
+!! end
+
+## Don't expect this to rt, Parsoid drops the unmatched closing pre tags that
+## aren't enclosed in nowikis.
+!! test
+<nowiki> and <pre> preference (first one wins)
+!! options
+parsoid=wt2html
+!! wikitext
+<pre>
+<nowiki>
+</pre>
+</nowiki>
+</pre>
+
+<nowiki>
+<pre>
+<nowiki>
+</pre>
+</nowiki>
+</pre>
+
+!! html/php
+<pre>
+&lt;nowiki&gt;
+</pre>
+<p>&lt;/nowiki&gt;
+&lt;/pre&gt;
+</p><p>
+&lt;pre&gt;
+&lt;nowiki&gt;
+&lt;/pre&gt;
+
+&lt;/pre&gt;
+</p>
+!! html/parsoid
+<pre typeof="mw:Extension/pre" about="#mwt2" data-mw='{"name":"pre","attrs":{},"body":{"extsrc":"\n&lt;nowiki>\n"}}'>&lt;nowiki>
+</pre>
+<p>&lt;/nowiki></p>
+
+
+<p><span typeof="mw:Nowiki">
+&lt;pre>
+&lt;nowiki>
+&lt;/pre>
+</span></p>
+!! end
+
+!! test
+</pre> inside nowiki
+!! wikitext
+<nowiki></pre></nowiki>
+!! html
+<p>&lt;/pre&gt;
+</p>
+!! end
+
+!! test
+Empty pre; pre inside other HTML tags (T56946)
+!! wikitext
+a
+
+<div><pre>
+foo
+</pre></div>
+<pre></pre>
+!! html/php+tidy
+<p>a
+</p>
+<div><pre>foo
+</pre></div>
+<pre></pre>
+!! html/parsoid
+<p>a</p>
+
+<div data-parsoid='{"stx":"html"}'><pre typeof="mw:Extension/pre" about="#mwt2" data-parsoid='{"stx":"html"}' data-mw='{"name":"pre","attrs":{},"body":{"extsrc":"\nfoo\n"}}'>foo
+</pre></div>
+<pre typeof="mw:Extension/pre" about="#mwt4" data-parsoid='{"stx":"html"}' data-mw='{"name":"pre","attrs":{},"body":{"extsrc":""}}'></pre>
+!! end
+
+!! test
+HTML pre followed by indent-pre
+!! wikitext
+<pre>foo</pre>
+ bar
+!! html
+<pre>foo</pre>
+<pre>bar
+</pre>
+!! end
+
+!! test
+Block tag pre
+!! wikitext
+<p><pre>foo</pre></p>
+!! html/php+tidy
+<p class="mw-empty-elt"></p><pre>foo</pre><p class="mw-empty-elt"></p>
+!! html/parsoid
+<p class='mw-empty-elt' data-parsoid='{"stx":"html","autoInsertedEnd":true}'></p><pre typeof="mw:Extension/pre" about="#mwt2" data-parsoid='{"stx":"html"}' data-mw='{"name":"pre","attrs":{},"body":{"extsrc":"foo"}}'>foo</pre><p class='mw-empty-elt' data-parsoid='{"autoInsertedStart":true,"stx":"html"}'></p>
+!! end
+
+!!test
+Templates: Indent-Pre: 1a. Templates that break a line should suppress <pre>
+!! wikitext
+ {{echo|}}
+!! html
+
+!!end
+
+!!test
+Templates: Indent-Pre: 1b. Templates that break a line should suppress <pre>
+!! wikitext
+ {{echo|
+foo}}
+!! html
+<p>foo
+</p>
+!!end
+
+!! test
+Templates: Indent-Pre: 1c: Wrapping should be based on expanded content
+!! wikitext
+ {{echo|a
+b}}
+!! html
+<pre>a
+</pre>
+<p>b
+</p>
+!!end
+
+!! test
+Templates: Indent-Pre: 1d: Wrapping should be based on expanded content
+!! wikitext
+ {{echo|a
+b
+c
+ d
+e
+}}
+!! html
+<pre>a
+</pre>
+<p>b
+c
+</p>
+<pre>d
+</pre>
+<p>e
+</p>
+!!end
+
+!!test
+Templates: Indent-Pre: 1e. Wrapping should be based on expanded content
+!! wikitext
+{{echo| foo}}
+
+{{echo| foo}}{{echo| bar}}
+
+{{echo| foo}}
+{{echo| bar}}
+
+{{echo|<!--cmt--> foo}}
+
+<!--cmt-->{{echo| foo}}
+
+{{echo|{{echo| }}bar}}
+!! html
+<pre>foo
+</pre>
+<pre>foo bar
+</pre>
+<pre>foo
+bar
+</pre>
+<pre>foo
+</pre>
+<pre>foo
+</pre>
+<pre>bar
+</pre>
+!!end
+
+!! test
+Templates: Indent-Pre: 1f: Wrapping should be based on expanded content
+!! wikitext
+{{echo| }}a
+
+{{echo|
+ }}a
+
+{{echo|
+ b}}
+
+{{echo|a
+ }}b
+
+{{echo|a
+}} b
+!! html
+<pre>a
+</pre>
+<p><br />
+</p>
+<pre>a
+</pre>
+<p><br />
+</p>
+<pre>b
+</pre>
+<p>a
+</p>
+<pre>b
+</pre>
+<p>a
+</p>
+<pre>b
+</pre>
+!!end
+
+## Hmm, should Parsoid rt this?
+!! test
+Pres with newline attributes
+!! options
+parsoid=wt2html,html2html
+!! wikitext
+<pre class="one
+two">hi</pre>
+!! html/php
+<pre class="one two">hi</pre>
+
+!! html/parsoid
+<pre typeof="mw:Extension/pre" about="#mwt2" class="one two" data-mw='{"name":"pre","attrs":{"class":"one two"},"body":{"extsrc":"hi"}}'>hi</pre>
+!! end
+
+!! test
+Things that look like <pre> tags aren't treated as such
+!! wikitext
+Barack Obama <President> of the United States
+<President></President>
+!! html
+<p>Barack Obama &lt;President&gt; of the United States
+&lt;President&gt;&lt;/President&gt;
+</p>
+!! end
+
+!! test
+Handle broken pre-like tags (T66025)
+!! options
+parsoid=wt2html
+!! wikitext
+{{echo|<pre <pre>x</pre>}}
+
+<table><pre </table>
+!! html/php
+<pre>x</pre>
+<table>&lt;pre </table>
+
+!! html/php+tidy
+<pre>x</pre>
+&lt;pre <table></table>
+!! html/parsoid
+<pre about="#mwt1" typeof="mw:Transclusion mw:Extension/pre" data-parsoid='{"a":{"&lt;pre":null},"sa":{"&lt;pre":""},"stx":"html","pi":[[{"k":"1"}]]}' data-mw='{"parts":[{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"&lt;pre &lt;pre>x&lt;/pre>"}},"i":0}}]}'>x</pre>
+
+
+<p>&lt;pre </p>
+
+<table></table>
+!! end
+
+!! test
+Parsoid: handle pre with space after attribute
+!! options
+parsoid=wt2html
+!! wikitext
+<pre style="width:50%;" >{{echo|foo}}</pre>
+!! html/php
+<pre style="width:50%;">{{echo|foo}}</pre>
+
+!! html/parsoid
+<pre typeof="mw:Extension/pre" about="#mwt2" style="width:50%;" data-mw='{"name":"pre","attrs":{"style":"width:50%;"},"body":{"extsrc":"{{echo|foo}}"}}'>{{echo|foo}}</pre>
+!! end
+
+# TODO / maybe: fix wt2wt for this
+!! test
+Parsoid: Don't paragraph-wrap fosterable content
+!! options
+parsoid=wt2html
+!! wikitext
+{|
+<td></td>
+<td></td>
+
+
+
+|}
+!! html
+<table>
+
+<tbody>
+<tr>
+<td></td>
+
+<td></td></tr>
+
+
+
+</tbody></table>
+!! end
+
+!! test
+Self-closed pre
+!! wikitext
+<pre />
+!! html/php
+<pre></pre>
+
+!! html/parsoid
+<pre typeof="mw:Extension/pre" about="#mwt2" data-mw='{"name":"pre","attrs":{},"body":null}'></pre>
+!! end
+
+!! test
+Parsoid: Don't paragraph-wrap fosterable content even if table syntax is unbalanced
+!! options
+parsoid=wt2html
+!! wikitext
+{|
+<td>
+<td>
+</td>
+
+
+
+|}
+!! html
+<table>
+
+<tbody>
+<tr>
+<td></td>
+
+<td>
+</td></tr>
+
+
+
+</tbody></table>
+!! end
+
+
+#--------------------------------------------------------------------
+# Transclusion parameter whitespace stripping tests
+# Behavior is different for positional and named parameters
+#--------------------------------------------------------------------
+!! test
+Templates: Strip leading and trailing whitespace from named-param values
+!! wikitext
+{{echo|1= a }}
+
+{{echo|1= {{echo|b}} }}
+
+{{echo| 1 =
+ c }}
+
+{{echo| 1 =
+* d
+}}
+!! html
+<p>a
+</p><p>b
+</p><p>c
+</p>
+<ul><li>d</li></ul>
+
+!! end
+
+!! test
+Templates: Don't strip whitespace from positional-param values
+!! wikitext
+{{echo|a }}
+
+{{echo|{{echo|b}} }}
+
+{{echo| c
+}}
+
+{{echo| {{echo|d}}
+}}
+
+{{echo|
+ e}}
+
+{{echo|
+*f}}
+
+{{echo|
+ }}g
+!! html
+<p>a
+</p><p>b
+</p>
+<pre>c
+</pre>
+<p><br />
+</p>
+<pre>d
+</pre>
+<p><br />
+</p>
+<pre>e
+</pre>
+<p><br />
+</p>
+<ul><li>f</li></ul>
+<p><br />
+</p>
+<pre>g
+</pre>
+!! end
+
+!! test
+Templates: Don't recognize targets split by newlines
+!! options
+parsoid=wt2html
+!! wikitext
+{{ech
+o|foo}}
+!! html/php
+<p>{{ech
+o|foo}}
+</p>
+!! html/parsoid
+<p>{{ech
+o|foo}}</p>
+!! end
+
+!! test
+Templates: Recognize targets when newlines and comments don't split the target
+!! options
+parsoid=wt2html
+!! wikitext
+{{
+ <!--X--> ech<!--X-->o<!--X-->
+ <!--X--> <!--X-->
+
+ |foo}}
+!! html/php
+<p>foo
+</p>
+!! html/parsoid
+<p typeof="mw:Transclusion" data-mw='{"parts":[{"template":{"target":{"wt":"ech&lt;!--X-->o&lt;!--X--> \n &lt;!--X--> &lt;!--X-->\n\n ","href":"./Template:Echo"},"params":{"1":{"wt":"foo"}},"i":0}}]}'>foo</p>
+!! end
+
+!! test
+Templates: Handle empty comment-and-ws-only lines correctly
+!! wikitext
+{{echo|foo
+<!--should be ignored-->
+ <!--should be ignored as well-->
+bar}}
+!! html/php
+<p>foo
+bar
+</p>
+!! html/parsoid
+<p typeof="mw:Transclusion" data-mw='{"parts":[{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"foo\n&lt;!--should be ignored-->\n &lt;!--should be ignored as well-->\nbar"}},"i":0}}]}'>foo <!--should be ignored--> <!--should be ignored as well--> bar</p>
+!! end
+
+!! test
+Templates: Handle comments in the target
+!! wikitext
+{{echo
+<!-- should be ignored -->
+|foo}}
+
+{{echo
+<!-- should be ignored and spaces on next line should not trip us up (T147742) -->
+ |foo}}
+
+{{echo<!-- should be ignored -->
+|foo}}
+
+{{echo<!-- should be ignored -->|foo}}
+
+{{<!-- should be ignored -->echo|foo}}
+!! html/php
+<p>foo
+</p><p>foo
+</p><p>foo
+</p><p>foo
+</p><p>foo
+</p>
+!! html/parsoid
+<p typeof="mw:Transclusion" data-mw='{"parts":[{"template":{"target":{"wt":"echo\n&lt;!-- should be ignored -->\n","href":"./Template:Echo"},"params":{"1":{"wt":"foo"}},"i":0}}]}'>foo</p>
+
+<p typeof="mw:Transclusion" data-mw='{"parts":[{"template":{"target":{"wt":"echo\n&lt;!-- should be ignored and spaces on next line should not trip us up (T147742) -->\n ","href":"./Template:Echo"},"params":{"1":{"wt":"foo"}},"i":0}}]}'>foo</p>
+
+<p typeof="mw:Transclusion" data-mw='{"parts":[{"template":{"target":{"wt":"echo&lt;!-- should be ignored -->\n","href":"./Template:Echo"},"params":{"1":{"wt":"foo"}},"i":0}}]}'>foo</p>
+
+<p typeof="mw:Transclusion" data-mw='{"parts":[{"template":{"target":{"wt":"echo&lt;!-- should be ignored -->","href":"./Template:Echo"},"params":{"1":{"wt":"foo"}},"i":0}}]}'>foo</p>
+
+<p typeof="mw:Transclusion" data-mw='{"parts":[{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"foo"}},"i":0}}]}'>foo</p>
+!! end
+
+!! test
+Templates: Handle comments in parameter names (T69657)
+!! wikitext
+{{echo|1
+<!-- should be ignored -->
+=foo}}
+
+{{echo|
+<!-- should be ignored -->
+1 = foo}}
+
+{{echo|1<!-- should be ignored -->=foo}}
+
+{{echo|<!-- should be ignored -->1=foo}}
+!! html/php
+<p>foo
+</p><p>foo
+</p><p>foo
+</p><p>foo
+</p>
+!! html/parsoid
+<p typeof="mw:Transclusion" data-mw='{"parts":[{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"foo","key":{"wt":"1\n&lt;!-- should be ignored -->"}}},"i":0}}]}'>foo</p>
+
+<p typeof="mw:Transclusion" data-mw='{"parts":[{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"foo","key":{"wt":"&lt;!-- should be ignored -->\n1"}}},"i":0}}]}'>foo</p>
+
+<p typeof="mw:Transclusion" data-mw='{"parts":[{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"foo","key":{"wt":"1&lt;!-- should be ignored -->"}}},"i":0}}]}'>foo</p>
+
+<p typeof="mw:Transclusion" data-mw='{"parts":[{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"foo","key":{"wt":"&lt;!-- should be ignored -->1"}}},"i":0}}]}'>foo</p>
+!! end
+
+!! test
+Templates: Other wikitext in parameter names (T69657)
+!! wikitext
+{{echo|''1''=foo}}
+!! html/php
+<p>{{{1}}}
+</p>
+!! html/parsoid
+<p typeof="mw:Transclusion" data-mw='{"parts":[{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"&#39;&#39;1&#39;&#39;":{"wt":"foo"}},"i":0}}]}'>{{{1}}}</p>
+!! end
+
+!! test
+Templates: With colons
+!! wikitext
+{{With: Colon}}
+!! html/php
+<p>Template with colon
+</p>
+!! html/parsoid
+<p about="#mwt1" typeof="mw:Transclusion" data-mw='{"parts":[{"template":{"target":{"wt":"With: Colon","href":"./Template:With:_Colon"},"params":{},"i":0}}]}'>Template with colon</p>
+!! end
+
+#--------------------------------------------------------------------
+# Transclusion parameter escaping tests
+#--------------------------------------------------------------------
+
+!! test
+Templates: Parsoid parameter escaping test 1
+!! wikitext
+{{echo|[foo]|{{echo|[bar]}}}}
+!! html/php+tidy
+<p>[foo]
+</p>
+!! html/parsoid
+<p about="#mwt1" typeof="mw:Transclusion"
+data-mw='{"parts":[{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"[foo]"},"2":{"wt":"{{echo|[bar]}}"}},"i":0}}]}'>[foo]</p>
+!! end
+
+!! test
+Parsoid: Pipes in external links in template parameter
+!! wikitext
+{{echo|[{{echo|http://example.com}} link]}}
+!! html/php+tidy
+<p><a rel="nofollow" class="external text" href="http://example.com">link</a>
+</p>
+!! html/parsoid
+<p><a rel="mw:ExtLink" class="external text" href="http://example.com" about="#mwt31" typeof="mw:Transclusion" data-mw='{"parts":[{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"[{{echo|http://example.com}} link]"}},"i":0}}]}'>link</a></p>
+!! end
+
+!! test
+Parsoid: pipe in transclusion parameter
+!! wikitext
+{{echo|http://foo.com/a&#124;b}}
+!! html/php+tidy
+<p><a rel="nofollow" class="external free" href="http://foo.com/a%7Cb">http://foo.com/a%7Cb</a>
+</p>
+!! html/parsoid
+<p><a rel="mw:ExtLink" class="external free" href="http://foo.com/a%7Cb" about="#mwt1" typeof="mw:Transclusion" data-mw='{"parts":[{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"http://foo.com/a&amp;#124;b"}},"i":0}}]}'>http://foo.com/a%7Cb</a></p>
+!! end
+
+!! test
+Parsoid: Pipe in external link target and content in template parameter
+!! options
+parsoid=html2wt,wt2wt
+!! wikitext
+{{echo|[http://foo.com/a&#124;b a&#124;b]}}
+!! html/php+tidy
+<p><a rel="nofollow" class="external text" href="http://foo.com/a%7Cb">a&#124;b</a>
+</p>
+!! html/parsoid
+<p><a rel="mw:ExtLink" href="http://foo.com/a|b" about="#mwt1"
+typeof="mw:Transclusion"
+data-mw='{"parts":[{"template":{"target":{"wt":"echo","href":"./Template:Echo"},
+"params":{"1":{"wt":"[http://foo.com/a|b a|b]"}},"i":0}}]}'>a|b</a></p>
+!! end
+
+!! test
+Parsoid: Pipe in template with nested template in external link target in template parameter (seriously)
+!! options
+parsoid
+!! wikitext
+{{echo|[{{fullurl:{{FULLPAGENAME}}|action=edit}} bar]}}
+!! html
+<p typeof="mw:Transclusion" data-mw='{"parts":[{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"[{{fullurl:{{FULLPAGENAME}}|action=edit}} bar]"}},"i":0}}]}'>[Main Page bar]</p>
+!! end
+
+!! test
+Templates: Don't escape already nowiki-escaped text in template parameters
+!! options
+parsoid=html2wt,wt2wt
+!! wikitext
+{{echo|foo<nowiki>|</nowiki>bar}}
+{{echo|<nowiki>&lt;div&gt;</nowiki>}}
+{{echo|<nowiki></nowiki>}}
+!! html/php+tidy
+<p>foo|bar
+&lt;div&gt;
+
+</p>
+!! html/parsoid
+<p><span about="#mwt1" typeof="mw:Transclusion" data-mw='{"parts":[{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"foo<nowiki>|</nowiki>bar"}},"i":0}}]}'}'>foo</span><span typeof="mw:Nowiki" about="#mwt1">|</span><span about="#mwt1">bar</span>
+<span typeof="mw:Transclusion mw:Nowiki" about="#mwt2" data-mw='{"parts":[{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"<nowiki>&amp;lt;div&amp;gt;</nowiki>"}},"i":0}}]}'><span typeof="mw:Entity">&lt;</span>div<span typeof="mw:Entity">&gt;</span></span>
+<span typeof="mw:Transclusion mw:Nowiki" about="#mwt3" data-mw='{"parts":[{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"<nowiki></nowiki>"}},"i":0}}]}'></span>
+</p>
+!! end
+
+## T54824
+!! test
+Templates: '=' char in nested transclusions should not trigger nowiki escapes or conversion to named param
+!! options
+parsoid=html2wt,wt2wt
+!! wikitext
+{{echo|{{echo|1=bar}}}}
+!! html/php+tidy
+<p>bar
+</p>
+!! html/parsoid
+<p about="#mwt1" typeof="mw:Transclusion" data-mw='{"parts":[{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"{{echo|1=bar}}"}},"i":0}}]}'>bar</p>
+!! end
+
+## T58733
+!! test
+Templates parameters with special tokenizing behavior dont get modified because of arg escaping
+!! wikitext
+{{echo|a : b}}
+!! html/php+tidy
+<p>a&#160;: b
+</p>
+!! html/parsoid
+<p about="#mwt1" typeof="mw:Transclusion" data-mw='{"parts":[{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"a : b"}},"i":0}}]}'>a<span typeof="mw:DisplaySpace mw:Placeholder" data-parsoid='{"isDisplayHack":true}'> </span>: b</p>
+!! end
+
+## T73412
+!! test
+Templates: Preserve blank parameter names
+!! wikitext
+{{echo|=foo}}
+!! html/php+tidy
+<p>{{{1}}}
+</p>
+!! html/parsoid
+<p about="#mwt1" typeof="mw:Transclusion" data-mw='{"parts":[{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"":{"wt":"foo"}},"i":0}}]}'>{{{1}}}</p>
+!! end
+
+!! test
+Templates: Preserve blank parameter names in other positions
+!! wikitext
+{{blank_param|bar|=foo}}
+!! html/php+tidy
+<p>bar
+foo
+</p>
+!! html/parsoid
+<p about="#mwt1" typeof="mw:Transclusion" data-parsoid='{"pi":[[{"k":"1"},{"k":"","named":true}]]}' data-mw='{"parts":[{"template":{"target":{"wt":"blank_param","href":"./Template:Blank_param"},"params":{"1":{"wt":"bar"},"":{"wt":"foo"}},"i":0}}]}'>bar
+foo</p>
+!! end
+
+###
+### Parsoid-centric tests for testing RT edge cases for pre
+###
+
+!!test
+1a. Indent-Pre and Comments
+!! wikitext
+ a
+<!--a-->
+c
+!! html
+<pre>a
+</pre>
+<p>c
+</p>
+!!end
+
+!!test
+1b. Indent-Pre and Comments
+!! wikitext
+ a
+ <!--a-->
+c
+!! html
+<pre>a
+</pre>
+<p>c
+</p>
+!!end
+
+!!test
+1c. Indent-Pre and Comments
+!! wikitext
+<!--a--> a
+
+ <!--a--> a
+!! html
+<pre> a
+</pre>
+<pre> a
+</pre>
+!!end
+
+!!test
+1d. Indent-Pre and Comments
+(Pre-handler currently cannot distinguish between comment/ws order and normalizes them to [comment,ws] order)
+!! wikitext
+<!--a--> a
+
+ <!--b-->b
+!! html
+<pre>a
+</pre>
+<pre>b
+</pre>
+!!end
+
+!!test
+2a. Indent-Pre and tables
+!! wikitext
+ {|
+ |-
+ !h1!!h2
+ |foo||bar
+ |}
+!! html
+<table>
+
+<tr>
+<th>h1</th>
+<th>h2
+</th>
+<td>foo</td>
+<td>bar
+</td></tr></table>
+
+!!end
+
+!!test
+2b. Indent-Pre and tables
+!! wikitext
+ {|
+ |-
+|foo
+|}
+!! html
+<table>
+
+<tr>
+<td>foo
+</td></tr></table>
+
+!!end
+
+!!test
+2c. Indent-Pre and tables (T44252)
+!! wikitext
+{|
+ |+foo
+ ! |bar
+|}
+!! html
+<table>
+<caption>foo
+</caption>
+<tr>
+<th>bar
+</th></tr></table>
+
+!!end
+
+!!test
+2d. Indent-Pre and tables
+!! wikitext
+ a
+ {|
+ |b
+ |}
+!! html/php
+<pre>a
+</pre>
+<table>
+<tr>
+<td>b
+</td></tr></table>
+
+!! html/parsoid
+<pre>a</pre>
+ <table>
+ <tbody><tr><td> b</td></tr>
+ </tbody></table>
+!!end
+
+!!test
+2e. Indent-Pre and table-line syntax
+!! wikitext
+ a
+ | b
+ | c
+!! html/php
+<pre>a
+| b
+| c
+</pre>
+!!end
+
+!!test
+2f. Indent-pre started by table-line syntax
+!! wikitext
+a
+ | b
+ | c
+!! html/php
+<p>a
+</p>
+<pre>| b
+| c
+</pre>
+!! html/parsoid
+<p>a</p>
+<pre>
+| b
+| c</pre>
+!!end
+
+!! test
+2g. Indented table markup mixed with indented pre content (proposed in T8200)
+!! wikitext
+ <table>
+ <tr>
+ <td>
+ Text that should be rendered preformatted
+ </td>
+ </tr>
+ </table>
+!! html
+ <table>
+ <tr>
+ <td>
+<pre>Text that should be rendered preformatted
+</pre>
+ </td>
+ </tr>
+ </table>
+
+!! end
+
+!!test
+3a. Indent-Pre and block tags (single-line html)
+!! wikitext
+ a <p> foo </p>
+ b <div> foo </div>
+ c <blockquote> foo </blockquote>
+ <span> foo </span>
+!! html
+ a <p> foo </p>
+ b <div> foo </div>
+ c <blockquote> foo </blockquote>
+<pre><span> foo </span>
+</pre>
+!! html/parsoid
+ <p>a </p><p data-parsoid='{"stx":"html"}'> foo </p>
+ <p>b </p><div data-parsoid='{"stx":"html"}'> foo </div>
+ <p>c </p><blockquote data-parsoid='{"stx":"html"}'> foo </blockquote>
+<pre><span> foo </span>
+</pre>
+!! html/php+tidy
+<p> a </p><p> foo </p><p>
+ b </p><div> foo </div><p>
+ c </p><blockquote><p> foo </p></blockquote>
+<pre><span> foo </span>
+</pre>
+!! end
+
+!!test
+3b. Indent-Pre and block tags (multi-line html)
+!! wikitext
+ a <span>foo</span>
+ b <div> foo </div>
+!! html
+<pre>a <span>foo</span>
+</pre>
+ b <div> foo </div>
+
+!! html/parsoid
+<pre>a <span data-parsoid='{"stx":"html"}'>foo</span></pre>
+ b <div data-parsoid='{"stx":"html"}'> foo </div>
+!! html/php+tidy
+<pre>a <span>foo</span>
+</pre><p>
+ b </p><div> foo </div>
+!!end
+
+!!test
+3c. Indent-Pre and block tags (pre-content on separate line)
+!! wikitext
+<p>
+ foo
+</p>
+
+<div>
+ foo
+</div>
+
+<center>
+ foo
+</center>
+
+<blockquote>
+ foo
+</blockquote>
+
+<blockquote>
+<pre>
+foo
+</pre>
+</blockquote>
+
+<table><tr><td>
+ foo
+</td></tr></table>
+
+<ul><li>
+ foo
+</li></ul>
+
+!! html
+<p>
+ foo
+</p>
+<div>
+<pre>foo
+</pre>
+</div>
+<center>
+<pre>foo
+</pre>
+</center>
+<blockquote>
+<p> foo
+</p>
+</blockquote>
+<blockquote>
+<pre>
+foo
+</pre>
+</blockquote>
+<table><tr><td>
+<pre>foo
+</pre>
+</td></tr></table>
+<ul><li>
+ foo
+</li></ul>
+
+!!end
+
+!! test
+4. Indent-Pre and extension tags
+!! wikitext
+ a <tag />
+!! html/php
+ a <pre>
+NULL
+array (
+)
+</pre>
+
+!! html/parsoid
+ a <pre typeof="mw:Extension/tag" about="#mwt2" data-parsoid='{}' data-mw='{"name":"tag","attrs":{},"body":null}'></pre>
+!! end
+
+!! test
+5. Indent-Pre and html pre
+!! wikitext
+ <pre class="123">hi</pre>
+!! html/php
+ <pre class="123">hi</pre>
+
+!! html/parsoid
+ <pre typeof="mw:Extension/pre" about="#mwt2" class="123" data-mw='{"name":"pre","attrs":{"class":"123"},"body":{"extsrc":"hi"}}'>hi</pre>
+!! end
+
+!!test
+Render paragraphs when indent-pre is suppressed in blocklevels
+!! wikitext
+<blockquote>
+ foo
+
+ bar
+</blockquote>
+!! html
+<blockquote>
+<p> foo
+</p><p> bar
+</p>
+</blockquote>
+
+!!end
+
+!!test
+4. Multiple spaces at start-of-line
+!! wikitext
+ <p> foo </p>
+ foo
+ {|
+|foo
+|}
+!! html
+ <p> foo </p>
+<pre> foo
+</pre>
+<table>
+<tr>
+<td>foo
+</td></tr></table>
+
+!!end
+
+## NOTE: the leading white-space chars on empty line are significant
+!! test
+5a. White-space in indent-pre
+!! wikitext
+ a<br />
+
+ b
+!! html
+<pre>a<br />
+
+b
+</pre>
+!! end
+
+## NOTE: the leading white-space chars on empty line are significant
+!! test
+5b. White-space in indent-pre
+!! wikitext
+ a
+
+ b
+
+
+ c
+!! html
+<pre>a
+
+b
+
+
+c
+</pre>
+!! end
+
+!! test
+5c. White-space in indent-pre
+!! wikitext
+ ''a''
+ ''b''
+ ''c''
+!! html
+<pre><i>a</i>
+ <i>b</i>
+ <i>c</i>
+</pre>
+!! end
+
+!! test
+6. Pre-blocks should extend across lines with leading WS even when there is no wrappable content
+!! wikitext
+ a
+
+ <!-- continue -->
+ b
+
+ c
+
+d
+!! html
+<pre>a
+
+b
+</pre>
+<pre>c
+
+</pre>
+<p>d
+</p>
+!! end
+
+!! test
+7a. Indent-pre and category links
+!! options
+parsoid=wt2html,wt2wt
+!! wikitext
+ [[Category:foo]] <!-- No pre-wrapping -->
+{{echo| [[Category:foo]]}} <!-- No pre-wrapping -->
+!! html/php+tidy
+!! html/parsoid
+ <link rel="mw:PageProp/Category" href="./Category:Foo"> <!-- No pre&#x2D;wrapping -->
+<span about="#mwt1" typeof="mw:Transclusion" data-mw='{"parts":[{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":" [[Category:foo]]"}},"i":0}}]}'> </span><link rel="mw:PageProp/Category" href="./Category:Foo" about="#mwt1"> <!-- No pre&#x2D;wrapping -->
+!! end
+
+## We used to, but no longer wt2wt this test since the default serializer
+## will normalize all categories to serialize on their own line.
+## This wikitext usage is going to be fairly uncommon in production and
+## selser will take care of preserving formatting in those scenarios.
+!! test
+7b. Indent-pre and category links
+!! options
+parsoid=wt2html
+!! wikitext
+ [[Category:foo]] a
+ [[Category:foo]] {{echo|b}}
+!! html/parsoid
+<pre><link rel="mw:PageProp/Category" href="./Category:Foo"> a
+ <link rel="mw:PageProp/Category" href="./Category:Foo"> <span about="#mwt1" typeof="mw:Transclusion" data-mw='{"parts":[{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"b"}},"i":0}}]}'>b</span></pre>
+!! end
+
+!! test
+Indent-Pre: Newlines in comments shouldn't affect sol state
+!! wikitext
+a <!--
+foo
+--> b
+!! html/php+tidy
+<p>a b
+</p>
+!! html/parsoid
+<p>a <!--
+foo
+--> b</p>
+!! end
+
+###
+### HTML-pre (some to spec PHP parser behavior and some Parsoid-RT-centric)
+###
+
+!!test
+HTML-pre: 1. embedded newlines
+!! wikitext
+<pre>foo</pre>
+
+<pre>
+foo
+</pre>
+
+<pre>
+
+foo
+</pre>
+
+<pre>
+
+
+foo
+</pre>
+!! html/php+tidy
+<pre>foo</pre>
+<pre>foo
+</pre>
+<pre>
+
+foo
+</pre>
+<pre>
+
+
+foo
+</pre>
+!! html/parsoid
+<pre typeof="mw:Extension/pre" about="#mwt2" data-mw='{"name":"pre","attrs":{},"body":{"extsrc":"foo"}}'>foo</pre>
+
+<pre typeof="mw:Extension/pre" about="#mwt4" data-mw='{"name":"pre","attrs":{},"body":{"extsrc":"\nfoo\n"}}'>foo
+</pre>
+
+<pre typeof="mw:Extension/pre" about="#mwt6" data-mw='{"name":"pre","attrs":{},"body":{"extsrc":"\n\nfoo\n"}}'>
+
+foo
+</pre>
+
+<pre typeof="mw:Extension/pre" about="#mwt8" data-mw='{"name":"pre","attrs":{},"body":{"extsrc":"\n\n\nfoo\n"}}'>
+
+
+foo
+</pre>
+!!end
+
+!! test
+HTML-pre: big spaces
+!! wikitext
+<pre>
+
+
+
+
+haha
+
+
+
+
+haha
+
+
+
+
+</pre>
+!! html/php+tidy
+<pre>
+
+
+
+
+haha
+
+
+
+
+haha
+
+
+
+
+</pre>
+!! html/parsoid
+<pre typeof="mw:Extension/pre" about="#mwt2" data-parsoid='{"stx":"html"}' data-mw='{"name":"pre","attrs":{},"body":{"extsrc":"\n\n\n\n\nhaha\n\n\n\n\nhaha\n\n\n\n\n"}}'>
+
+
+
+
+haha
+
+
+
+
+haha
+
+
+
+
+</pre>
+!! end
+
+!!test
+HTML-pre: 2: indented text
+!! wikitext
+<pre>
+ foo
+</pre>
+!! html
+<pre>
+ foo
+</pre>
+
+!!end
+
+!!test
+HTML-pre: 3: other wikitext
+!! wikitext
+<pre>
+* foo
+# bar
+= no-h =
+'' no-italic ''
+[[ NoLink ]]
+</pre>
+!! html/php
+<pre>
+* foo
+# bar
+= no-h =
+'' no-italic ''
+[[ NoLink ]]
+</pre>
+
+!! html/parsoid
+<pre typeof="mw:Extension/pre" about="#mwt2" data-mw='{"name":"pre","attrs":{},"body":{"extsrc":"\n* foo\n# bar\n= no-h =\n&#39;&#39; no-italic &#39;&#39;\n[[ NoLink ]]\n"}}'>* foo
+# bar
+= no-h =
+'' no-italic ''
+[[ NoLink ]]
+</pre>
+!!end
+
+###
+### Definition lists
+###
+!! test
+Simple definition
+!! wikitext
+;name :Definition
+!! html
+<dl><dt>name&#160;</dt>
+<dd>Definition</dd></dl>
+
+!! end
+
+!! test
+Definition list for indentation only
+!! wikitext
+:Indented text
+!! html
+<dl><dd>Indented text</dd></dl>
+
+!! end
+
+!! test
+Definition list with no space
+!! wikitext
+;name:Definition
+!! html
+<dl><dt>name</dt>
+<dd>Definition</dd></dl>
+
+!!end
+
+!! test
+Definition list with URL link
+!! wikitext
+;http://example.com/ :definition
+!! html
+<dl><dt><a rel="nofollow" class="external free" href="http://example.com/">http://example.com/</a>&#160;</dt>
+<dd>definition</dd></dl>
+
+!! end
+
+!! test
+Definition list with bracketed URL link
+!! wikitext
+;[http://www.example.com/ Example]:Something about it
+!! html
+<dl><dt><a rel="nofollow" class="external text" href="http://www.example.com/">Example</a></dt>
+<dd>Something about it</dd></dl>
+
+!! end
+
+!! test
+Definition list with wikilink containing colon
+!! wikitext
+; [[Help:FAQ]]:The least-read page on Wikipedia
+!! html
+<dl><dt><a href="/index.php?title=Help:FAQ&amp;action=edit&amp;redlink=1" class="new" title="Help:FAQ (page does not exist)">Help:FAQ</a></dt>
+<dd>The least-read page on Wikipedia</dd></dl>
+
+!! end
+
+# At Brion's and JeLuF's insistence... :)
+!! test
+Definition list with news link containing colon
+!! wikitext
+;news:alt.wikipedia.rox: This isn't even a real newsgroup!
+!! html/php
+<dl><dt><a rel="nofollow" class="external free" href="news:alt.wikipedia.rox">news:alt.wikipedia.rox</a></dt>
+<dd>This isn't even a real newsgroup!</dd></dl>
+
+!! html/parsoid
+<dl><dt> <a rel="mw:ExtLink" class="external free" href="news:alt.wikipedia.rox" data-parsoid='{"stx":"url"}'>news:alt.wikipedia.rox</a></dt><dd data-parsoid='{"stx":"row"}'>This isn't even a real newsgroup!</dd></dl>
+!! end
+
+!! test
+Malformed definition list with colon
+!! wikitext
+; news:alt.wikipedia.rox -- don't crash or enter an infinite loop
+!! html
+<dl><dt><a rel="nofollow" class="external free" href="news:alt.wikipedia.rox">news:alt.wikipedia.rox</a> -- don't crash or enter an infinite loop</dt></dl>
+
+!! end
+
+!! test
+Definition lists: colon in external link text
+!! wikitext
+;[http://www.wikipedia2.org/ Wikipedia :The Next Generation] :OK, I made that up
+!! html
+<dl><dt><a rel="nofollow" class="external text" href="http://www.wikipedia2.org/">Wikipedia&#160;:The Next Generation</a>&#160;</dt>
+<dd>OK, I made that up</dd></dl>
+
+!! end
+
+!! test
+Definition lists: colon in HTML attribute
+!! wikitext
+;<b style="display: inline">bold</b>
+!! html
+<dl><dt><b style="display: inline">bold</b></dt></dl>
+
+!! end
+
+!! test
+Definition lists: self-closed tag
+!! wikitext
+;one<br/>two :two-line fun
+!! html
+<dl><dt>one<br />two&#160;</dt>
+<dd>two-line fun</dd></dl>
+
+!! end
+
+!! test
+Definition lists: ignore colons inside tags
+!! wikitext
+;one <b>two : tag <i>fun:</i>:</b>:def
+!! html
+<dl><dt>one <b>two&#160;: tag <i>fun:</i>:</b></dt>
+<dd>def</dd></dl>
+
+!! end
+
+!! test
+Definition lists: excess closed tags
+!! wikitext
+;one</b>two :bad tag fun
+!! html/php+tidy
+<dl><dt>onetwo&#160;</dt>
+<dd>bad tag fun</dd></dl>
+!! html/parsoid
+<dl>
+<dt>onetwo</dt>
+<dd>bad tag fun</dd>
+</dl>
+!! end
+
+!! test
+T13748: Literal closing tags
+!! wikitext
+<dl>
+<dt>test 1</dt>
+<dd>test test test test test</dd>
+<dt>test 2</dt>
+<dd>test test test test test</dd>
+</dl>
+!! html
+<dl>
+<dt>test 1</dt>
+<dd>test test test test test</dd>
+<dt>test 2</dt>
+<dd>test test test test test</dd>
+</dl>
+
+!! end
+
+!! test
+Definition and unordered list using wiki syntax nested in unordered list using html tags.
+!! wikitext
+<ul><li>
+;term :description
+*unordered
+</li></ul>
+!! html
+<ul><li>
+<dl><dt>term&#160;</dt>
+<dd>description</dd></dl>
+<ul><li>unordered</li></ul>
+</li></ul>
+
+!! end
+
+!! test
+Definition list with empty definition and following paragraph
+!! wikitext
+;term:
+
+Paragraph text
+!! html
+<dl><dt>term</dt>
+<dd></dd></dl>
+<p>Paragraph text
+</p>
+!! end
+
+!! test
+Nested definition lists using html syntax
+!! wikitext
+<dl><dt>x</dt>
+<dd>a</dd>
+<dd>b</dd></dl>
+!! html
+<dl><dt>x</dt>
+<dd>a</dd>
+<dd>b</dd></dl>
+
+!! end
+
+!! test
+Definition Lists: No nesting: Multiple dd's
+!! wikitext
+;x
+:a
+:b
+!! html
+<dl><dt>x</dt>
+<dd>a</dd>
+<dd>b</dd></dl>
+
+!! end
+
+!! test
+Definition Lists: Indentation: Regular
+!! wikitext
+:i1
+::i2
+:::i3
+!! html
+<dl><dd>i1
+<dl><dd>i2
+<dl><dd>i3</dd></dl></dd></dl></dd></dl>
+
+!! end
+
+!! test
+Definition Lists: Indentation: Missing 1st level
+!! wikitext
+::i2
+:::i3
+!! html
+<dl><dd><dl><dd>i2
+<dl><dd>i3</dd></dl></dd></dl></dd></dl>
+
+!! end
+
+!! test
+Definition Lists: Indentation: Multi-level indent
+!! wikitext
+:::i3
+!! html
+<dl><dd><dl><dd><dl><dd>i3</dd></dl></dd></dl></dd></dl>
+
+!! end
+
+!! test
+Definition Lists: Hacky use to indent tables
+!! wikitext
+::{|
+|foo
+|bar
+|}
+this text
+should be left alone
+!! html
+<dl><dd><dl><dd><table>
+<tr>
+<td>foo
+</td>
+<td>bar
+</td></tr></table></dd></dl></dd></dl>
+<p>this text
+should be left alone
+</p>
+!! end
+
+!! test
+Definition Lists: Hacky use to indent tables (with content following table)
+!! wikitext
+:{|
+|foo
+|bar
+|} <!--c1--> this text should be part of the dl
+!! html/php+tidy
+<dl><dd><table>
+<tbody><tr>
+<td>foo
+</td>
+<td>bar
+</td></tr></tbody></table> this text should be part of the dl</dd></dl>
+!! html/parsoid
+<dl><dd><table>
+<tbody><tr>
+<td>foo
+</td>
+<td>bar
+</td></tr></tbody></table> <!--c1--> this text should be part of the dl</dd></dl>
+!! end
+
+!! test
+Definition Lists: Hacky use to indent tables, with comments (T65979)
+!! wikitext
+<!-- foo -->
+::{|
+|foo
+|bar
+|}<!-- bar -->
+this text
+should be left alone
+!! html/parsoid
+<!-- foo -->
+<dl><dd><dl><dd><table><tr>
+<td>foo</td>
+<td>bar</td>
+</tr></table><!-- bar --></dd></dl></dd></dl>
+<p>this text
+should be left alone</p>
+!! end
+
+!! test
+Definition Lists: Hacky use to indent tables, with comment before table
+!! wikitext
+::<!-- foo -->{|
+|foo
+|}
+!! html/parsoid
+<dl><dd><dl><dd><!-- foo --><table><tr>
+<td>foo</td>
+</tr></table></dd></dl></dd></dl>
+!! end
+
+# The trailing whitespace in this test is to catch a regression in
+# Parsoid after T54473.
+!! test
+Definition Lists: Hacky use to indent tables (WS-insensitive)
+!! wikitext
+: {|
+|a
+|}
+!! html/php
+<dl><dd><table>
+<tr>
+<td>a
+</td></tr></table></dd></dl>
+
+!! html/parsoid
+<dl><dd> <table>
+<tbody><tr><td>a</td></tr>
+</tbody></table> </dd></dl>
+!! end
+
+## The PHP parser treats : items (dd) without a corresponding ; item (dt)
+## as an empty dt item. It also ignores all but the last ";" when followed
+## by ":" later on. So, ";" are not ignored in ";;;t3" but are ignored in
+## ";;;t3 :d1". So, PHP parser behavior is a little inconsistent wrt multiple
+## ";"s.
+##
+## Ex: ";;t2 ::d2" is transformed into:
+##
+## <dl>
+## <dt>t2 </dt>
+## <dd>
+## <dl>
+## <dt></dt>
+## <dd>d2</dd>
+## </dl>
+## </dd>
+## </dl>
+##
+## But, Parsoid treats "; :" as a tight atomic unit and excess ":" as plain text
+## So, the same wikitext above (;;t2 ::d2) is transformed into:
+##
+## <dl>
+## <dt>
+## <dl>
+## <dt>t2 </dt>
+## <dd>:d2</dd>
+## </dl>
+## </dt>
+## </dl>
+##
+## All Parsoid only definition list tests have this difference.
+##
+## See also: https://phabricator.wikimedia.org/T8569
+## and https://lists.wikimedia.org/pipermail/wikitext-l/2011-November/000483.html
+
+!! test
+Table / list interaction: indented table with lists in table contents
+!! wikitext
+:{|
+|-
+|a
+
+*b
+|-
+|c
+
+*d
+|}
+!! html
+<dl><dd><table>
+
+<tr>
+<td>a
+<ul><li>b</li></ul>
+</td></tr>
+<tr>
+<td>c
+<ul><li>d</li></ul>
+</td></tr></table></dd></dl>
+
+!! end
+
+!!test
+Table / list interaction: lists nested in tables nested in indented lists
+!! wikitext
+:{|
+|
+:a
+:b
+|
+*c
+*d
+|}
+
+*e
+*f
+!! html
+<dl><dd><table>
+<tr>
+<td>
+<dl><dd>a</dd>
+<dd>b</dd></dl>
+</td>
+<td>
+<ul><li>c</li>
+<li>d</li></ul>
+</td></tr></table></dd></dl>
+<ul><li>e</li>
+<li>f</li></ul>
+
+!!end
+
+!! test
+Definition Lists: Nesting: Multi-level (Parsoid only)
+!! wikitext
+;t1 :d1
+;;t2 ::d2
+;;;t3 :::d3
+!! html/parsoid
+<dl>
+ <dt>t1 </dt>
+ <dd>d1</dd>
+ <dt>
+ <dl>
+ <dt>t2 </dt>
+ <dd>:d2</dd>
+ <dt>
+ <dl>
+ <dt>t3 </dt>
+ <dd>::d3</dd>
+ </dl>
+ </dt>
+ </dl>
+ </dt>
+</dl>
+
+
+!! end
+
+
+!! test
+Definition Lists: Nesting: Test 2
+!! wikitext
+;t1
+::d2
+!! html+tidy
+<dl><dt>t1</dt>
+<dd>
+<dl><dd>d2</dd></dl></dd></dl>
+!! end
+
+
+!! test
+Definition Lists: Nesting: Test 3
+!! wikitext
+:;t1
+::::d2
+!! html+tidy
+<dl><dd><dl><dt>t1</dt>
+<dd>
+<dl><dd><dl><dd>d2</dd></dl></dd></dl></dd></dl></dd></dl>
+!! end
+
+
+!! test
+Definition Lists: Nesting: Test 4
+!! wikitext
+::;t3
+:::d3
+!! html
+<dl><dd><dl><dd><dl><dt>t3</dt>
+<dd>d3</dd></dl></dd></dl></dd></dl>
+
+!! end
+
+
+## The Parsoid team believes the following three test exposes a
+## bug in the PHP parser. (Parsoid team thinks the PHP parser is
+## wrong to close the <dl> after the <dt> containing the <ul>.)
+## It also exposes a "misfeature" in tidy, which doesn't like
+## <dl> tags with a single <dt> child; it converts the <dt> into
+## a <dd> in that case. (Parsoid leaves the <dt> alone!)
+!! test
+Definition Lists: Mixed Lists: Test 1
+!! wikitext
+:;*foo
+::*bar
+:;baz
+!! html/php
+<dl><dd><dl><dt><ul><li>foo</li>
+<li>bar</li></ul></dt></dl>
+<dl><dt>baz</dt></dl></dd></dl>
+
+!! html/php+tidy
+<dl><dd><dl><dt><ul><li>foo</li>
+<li>bar</li></ul></dt></dl>
+<dl><dt>baz</dt></dl></dd></dl>
+!! html/parsoid
+<dl>
+<dd><dl>
+<dt><ul>
+<li>foo
+</li>
+</ul></dt>
+<dd><ul>
+<li>bar
+</li>
+</ul></dd>
+<dt>baz</dt>
+</dl></dd>
+</dl>
+!! end
+
+!! test
+Definition Lists: Mixed Lists: Test 2
+!! wikitext
+*:d1
+*:d2
+!! html
+<ul><li><dl><dd>d1</dd>
+<dd>d2</dd></dl></li></ul>
+
+!! end
+
+
+!! test
+Definition Lists: Mixed Lists: Test 3
+!! wikitext
+*:::d1
+*:::d2
+!! html
+<ul><li><dl><dd><dl><dd><dl><dd>d1</dd>
+<dd>d2</dd></dl></dd></dl></dd></dl></li></ul>
+
+!! end
+
+
+!! test
+Definition Lists: Mixed Lists: Test 4
+!! wikitext
+*;d1 :d2
+*;d3 :d4
+!! html
+<ul><li><dl><dt>d1&#160;</dt>
+<dd>d2</dd>
+<dt>d3&#160;</dt>
+<dd>d4</dd></dl></li></ul>
+
+!! end
+
+
+!! test
+Definition Lists: Mixed Lists: Test 5
+!! wikitext
+*:d1
+*::d2
+!! html
+<ul><li><dl><dd>d1
+<dl><dd>d2</dd></dl></dd></dl></li></ul>
+
+!! end
+
+
+!! test
+Definition Lists: Mixed Lists: Test 6
+!! wikitext
+#*:d1
+#*:::d3
+!! html
+<ol><li><ul><li><dl><dd>d1
+<dl><dd><dl><dd>d3</dd></dl></dd></dl></dd></dl></li></ul></li></ol>
+
+!! end
+
+
+!! test
+Definition Lists: Mixed Lists: Test 7
+!! wikitext
+:*d1
+:*d2
+!! html
+<dl><dd><ul><li>d1</li>
+<li>d2</li></ul></dd></dl>
+
+!! end
+
+
+!! test
+Definition Lists: Mixed Lists: Test 8
+!! wikitext
+:*d1
+::*d2
+!! html
+<dl><dd><ul><li>d1</li></ul>
+<dl><dd><ul><li>d2</li></ul></dd></dl></dd></dl>
+
+!! end
+
+
+!! test
+Definition Lists: Mixed Lists: Test 9
+!! wikitext
+*;foo :bar
+!! html
+<ul><li><dl><dt>foo&#160;</dt>
+<dd>bar</dd></dl></li></ul>
+
+!! end
+
+
+!! test
+Definition Lists: Mixed Lists: Test 10
+!! wikitext
+*#;foo :bar
+!! html
+<ul><li><ol><li><dl><dt>foo&#160;</dt>
+<dd>bar</dd></dl></li></ol></li></ul>
+
+!! end
+
+# The Parsoid team disagrees with the PHP parser's seemingly-random
+# rules regarding dd/dt on the next few tests. Parsoid is more
+# consistent, and recognizes the shared nesting and keeps the
+# still-open tags around until the nesting is complete.
+
+# This is a regression test for T175099
+!! test
+Definition Lists: Mixed Lists: Test 11
+!! wikitext
+;a
+:*b
+!! html/php
+<dl><dt>a</dt>
+<dd>
+<ul><li>b</li></ul></dd></dl>
+
+!! html/parsoid
+<dl><dt>a
+<dd><ul><li>b</li></ul></dd></dl>
+!! end
+
+# FIXME: Maybe get rid of this test?
+!! test
+Definition Lists: Mixed Lists: Test 12
+!! wikitext
+*#*#;*;;foo :bar
+*#*#;boo :baz
+!! html/php
+<ul><li><ol><li><ul><li><ol><li><dl><dt>foo&#160;</dt>
+<dd><ul><li><dl><dt><dl><dt>bar</dt></dl></dd></dl></li></ul></dd></dl>
+<dl><dt>boo&#160;</dt>
+<dd>baz</dd></dl></li></ol></li></ul></li></ol></li></ul>
+
+!! html/php+tidy
+<ul><li><ol><li><ul><li><ol><li><dl><dt>foo&#160;</dt>
+<dd><ul><li><dl><dt><dl><dt>bar</dt></dl></dt></dl></li></ul></dd></dl></li></ol></li></ul>
+<dl><dt>boo&#160;</dt>
+<dd>baz</dd></dl></li></ol></li></ul>
+!! html/parsoid
+<ul>
+<li>
+<ol>
+<li>
+<ul>
+<li>
+<ol>
+<li>
+<dl>
+<dt>
+<ul>
+<li>
+<dl>
+<dt>
+<dl>
+<dt>foo<span typeof="mw:Placeholder" data-parsoid='{"src":" "}'>&nbsp;</span></dt>
+<dd data-parsoid='{"stx":"row"}'>bar</dd>
+</dl></dt>
+</dl></li>
+</ul></dt>
+<dt>boo<span typeof="mw:Placeholder" data-parsoid='{"src":" "}'>&nbsp;</span></dt>
+<dd data-parsoid='{"stx":"row"}'>baz</dd>
+</dl></li>
+</ol></li>
+</ul></li>
+</ol></li>
+</ul>
+!! end
+
+# FIXME: Maybe get rid of this test?
+# From whitelist:
+# * The test is wrong, there are two colons where there should be :;
+# * The PHP parser is wrong to close the <dl> after the <dt> containing the <ul>.
+!! test
+Definition Lists: Weird Ones: Test 1
+!! wikitext
+*#;*::;;foo :bar (who uses this?)
+!! html/php+tidy
+<ul><li><ol><li><dl><dt>foo&#160;</dt>
+<dd><ul><li><dl><dd><dl><dd><dl><dt><dl><dt>bar (who uses this?)</dt></dl></dt></dl></dd></dl></dd></dl></li></ul></dd></dl></li></ol></li></ul>
+!! html/parsoid
+<ul>
+<li>
+<ol>
+<li>
+<dl>
+<dt>
+<ul>
+<li>
+<dl>
+<dd>
+<dl>
+<dd>
+<dl>
+<dt>
+<dl>
+<dt>foo<span typeof="mw:DisplaySpace mw:Placeholder" data-parsoid='{"src":" ","isDisplayHack":true}'> </span></dt>
+<dd data-parsoid='{"stx":"row"}'>bar (who uses this?)</dd>
+</dl></dt>
+</dl></dd>
+</dl></dd>
+</dl></li>
+</ul></dt>
+</dl></li>
+</ol></li>
+</ul>
+!! end
+
+!! test
+Definition Lists: colons occurring in tags
+!! wikitext
+;a:b
+;'''a:b'''
+;<i>a:b</i>
+;<span>a:b</span>
+;<div>a:b</div>
+;<div>a
+:b</div>
+;{{echo|a:b}}
+;{{echo|''a:b''}}
+;;;''a:b''
+!! html+tidy
+<dl><dt>a</dt>
+<dd>b</dd>
+<dt><b>a:b</b></dt>
+<dt><i>a:b</i></dt>
+<dt><span>a:b</span></dt>
+<dt><div>a:b</div></dt>
+<dt><div>a</div></dt>
+<dd>b</dd>
+<dt>a</dt>
+<dd>b</dd>
+<dt><i>a:b</i></dt></dl>
+<dl><dt><dl><dt><dl><dt><i>a:b</i></dt></dl></dt></dl></dt></dl>
+!! html/parsoid
+<dl><dt>a</dt><dd data-parsoid='{"stx":"row"}'>b</dd>
+<dt><b>a:b</b></dt>
+<dt><i data-parsoid='{"stx":"html"}'>a:b</i></dt>
+<dt><span data-parsoid='{"stx":"html"}'>a:b</span></dt>
+<dt><div data-parsoid='{"stx":"html"}'>a:b</div></dt>
+<dt><div data-parsoid='{"stx":"html","autoInsertedEnd":true}'>a</div></dt>
+<dd>b</dd>
+<dt><span about="#mwt1" typeof="mw:Transclusion" data-parsoid='{"pi":[[{"k":"1"}]]}' data-mw='{"parts":[{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"a:b"}},"i":0}}]}'>a:b</span></dt>
+<dt><i about="#mwt2" typeof="mw:Transclusion" data-parsoid='{"pi":[[{"k":"1"}]]}' data-mw='{"parts":[{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"&#39;&#39;a:b&#39;&#39;"}},"i":0}}]}'>a:b</i>
+<dl><dt><dl><dt><i>a:b</i></dt></dl></dt></dl></dt></dl>
+!! end
+
+# Parsoid's output differs here again because it shares
+# nesting between the two lists unlike the PHP parser.
+# Unsure which is more desirable.
+!! test
+Definition Lists: colons and tables 1
+!! wikitext
+:{|
+|x
+|}
+:{|
+|y
+|}
+!! html/php
+<dl><dd><table>
+<tr>
+<td>x
+</td></tr></table></dd></dl>
+<dl><dd><table>
+<tr>
+<td>y
+</td></tr></table></dd></dl>
+
+!! html/parsoid
+<dl><dd><table>
+<tr>
+<td>x
+</td></tr></table></dd>
+<dd><table>
+<tr>
+<td>y
+</td></tr></table></dd></dl>
+!! end
+
+# FIXME: Does this need a html/php section?
+!! test
+Definition Lists: template interaction
+!! wikitext
+::{{definition_list}}
+
+:one
+::{{definition_list}}
+:::two
+:::three
+::four
+!! html/parsoid
+<dl><dd><dl data-parsoid='{}'><dd about="#mwt1" typeof="mw:Transclusion" data-parsoid='{"pi":[[]]}' data-mw='{"parts":[":",{"template":{"target":{"wt":"definition_list","href":"./Template:Definition_list"},"params":{},"i":0}}]}'>one</dd><span about="#mwt1">
+</span><dd about="#mwt1">two</dd></dl></dd></dl>
+
+<dl><dd data-parsoid='{}'>one
+<dl><dd about="#mwt2" typeof="mw:Transclusion" data-parsoid='{"pi":[[]]}' data-mw='{"parts":["::",{"template":{"target":{"wt":"definition_list","href":"./Template:Definition_list"},"params":{},"i":0}},"\n:::two\n:::three"]}'>one</dd><span about="#mwt2">
+</span><dd about="#mwt2">two
+<dl><dd>two</dd>
+<dd>three</dd></dl></dd>
+<dd data-parsoid='{}'>four</dd></dl></dd></dl>
+!! end
+
+
+###
+### External links
+###
+!! test
+External links: non-bracketed
+!! wikitext
+Non-bracketed: http://example.com
+!! html
+<p>Non-bracketed: <a rel="nofollow" class="external free" href="http://example.com">http://example.com</a>
+</p>
+!! end
+
+# parsoid doesn't explicitly mark autonumbered links, see T55505
+!! test
+External links: numbered
+!! wikitext
+Numbered: [http://example.com]
+Numbered: [http://example.net]
+Numbered: [http://example.com]
+!! html/php
+<p>Numbered: <a rel="nofollow" class="external autonumber" href="http://example.com">[1]</a>
+Numbered: <a rel="nofollow" class="external autonumber" href="http://example.net">[2]</a>
+Numbered: <a rel="nofollow" class="external autonumber" href="http://example.com">[3]</a>
+</p>
+!! html/parsoid
+<p>Numbered: <a rel="mw:ExtLink" class="external autonumber" href="http://example.com"></a>
+Numbered: <a rel="mw:ExtLink" class="external autonumber" href="http://example.net"></a>
+Numbered: <a rel="mw:ExtLink" class="external autonumber" href="http://example.com"></a></p>
+!!end
+
+!! test
+External links: specified text
+!! wikitext
+Specified text: [http://example.com link]
+!! html
+<p>Specified text: <a rel="nofollow" class="external text" href="http://example.com">link</a>
+</p>
+!!end
+
+!! test
+External links: trail
+!! wikitext
+Linktrails should not work for external links: [http://example.com link]s
+!! html
+<p>Linktrails should not work for external links: <a rel="nofollow" class="external text" href="http://example.com">link</a>s
+</p>
+!! end
+
+!! test
+External links: dollar sign in URL
+!! wikitext
+http://example.com/1$2345
+!! html
+<p><a rel="nofollow" class="external free" href="http://example.com/1$2345">http://example.com/1$2345</a>
+</p>
+!! end
+
+# parsoid doesn't explicitly mark autonumbered links, see T55505
+!! test
+External links: dollar sign in URL (autonumber)
+!! wikitext
+[http://example.com/1$2345]
+!! html/php
+<p><a rel="nofollow" class="external autonumber" href="http://example.com/1$2345">[1]</a>
+</p>
+!! html/parsoid
+<p><a rel="mw:ExtLink" class="external autonumber" href="http://example.com/1$2345"></a></p>
+!!end
+
+!! test
+External links: open square bracket forbidden in URL (T6377)
+!! options
+parsoid=wt2html,wt2wt,html2html
+!! wikitext
+http://example.com/1[2345
+!! html/php
+<p><a rel="nofollow" class="external free" href="http://example.com/1">http://example.com/1</a>[2345
+</p>
+!! html/parsoid
+<p><a rel="mw:ExtLink" class="external free" href="http://example.com/1">http://example.com/1</a>[2345</p>
+!! end
+
+!! test
+External links: open square bracket forbidden in URL (named) (T6377)
+!! options
+parsoid=wt2html,html2html
+!! wikitext
+[http://example.com/1[2345]
+!! html/php
+<p><a rel="nofollow" class="external text" href="http://example.com/1">[2345</a>
+</p>
+!! html/parsoid
+<p><a rel="mw:ExtLink" class="external text" href="http://example.com/1">[2345</a></p>
+!!end
+
+# parsoid adds a space before the link name
+!! test
+External links: open square bracket forbidden in URL (named) (T6377)
+Parsoid variant.
+!! wikitext
+[http://example.com/1 [2345]
+!! html
+<p><a rel="nofollow" class="external text" href="http://example.com/1">[2345</a>
+</p>
+!!end
+
+!! test
+External links: nowiki in URL link text (T8230)
+!! wikitext
+[http://example.com/ <nowiki>''example site''</nowiki>]
+!! html
+<p><a rel="nofollow" class="external text" href="http://example.com/">''example site''</a>
+</p>
+!! end
+
+!! test
+External links: newline forbidden in text (T8230 regression check)
+!! wikitext
+[http://example.com/ first
+second]
+!! html
+<p>[<a rel="nofollow" class="external free" href="http://example.com/">http://example.com/</a> first
+second]
+</p>
+!!end
+
+!! test
+External links: Pipe char between url and text
+!! wikitext
+[http://example.com | link]
+!! html
+<p><a rel="nofollow" class="external text" href="http://example.com">| link</a>
+</p>
+!!end
+
+!! test
+External links: protocol-relative URL in brackets
+!! wikitext
+[//example.com/ Test]
+!! html
+<p><a rel="nofollow" class="external text" href="//example.com/">Test</a>
+</p>
+!! end
+
+# parsoid doesn't explicitly mark autonumbered links, see T55505
+!! test
+External links: protocol-relative URL in brackets without text
+!! wikitext
+[//example.com]
+!! html/php
+<p><a rel="nofollow" class="external autonumber" href="//example.com">[1]</a>
+</p>
+!! html/parsoid
+<p><a rel="mw:ExtLink" class="external autonumber" href="//example.com"></a></p>
+!! end
+
+!! test
+External links: protocol-relative URL in free text is left alone
+!! wikitext
+//example.com/Foo
+!! html
+<p>//example.com/Foo
+</p>
+!!end
+
+!! test
+External links: protocol-relative URL in the middle of a word is left alone (T32269)
+!! wikitext
+foo//example.com/Foo
+!! html
+<p>foo//example.com/Foo
+</p>
+!! end
+
+## html2wt and html2html will fail because we will prefer the :en: interwiki prefix over wikipedia:
+!! test
+External links: with no contents
+!! options
+parsoid=wt2html,wt2wt
+!! wikitext
+[http://en.wikipedia.org/wiki/Foo]
+
+[[wikipedia:Foo|Bar]]
+
+[[wikipedia:Foo|<span>Bar</span>]]
+!! html/php
+<p><a rel="nofollow" class="external autonumber" href="http://en.wikipedia.org/wiki/Foo">[1]</a>
+</p><p><a href="http://en.wikipedia.org/wiki/Foo" class="extiw" title="wikipedia:Foo">Bar</a>
+</p><p><a href="http://en.wikipedia.org/wiki/Foo" class="extiw" title="wikipedia:Foo"><span>Bar</span></a>
+</p>
+!! html/parsoid
+<p><a rel="mw:ExtLink" class="external autonumber" href="http://en.wikipedia.org/wiki/Foo"></a></p>
+<p><a rel="mw:WikiLink/Interwiki" href="http://en.wikipedia.org/wiki/Foo" title="wikipedia:Foo">Bar</a></p>
+<p><a rel="mw:WikiLink/Interwiki" href="http://en.wikipedia.org/wiki/Foo" title="wikipedia:Foo"><span>Bar</span></a></p>
+!! end
+
+!! test
+External links: Free with trailing punctuation
+!! wikitext
+http://example.com,
+http://example.com;
+http://example.com\
+http://example.com.
+http://example.com:
+http://example.com!
+http://example.com?
+http://example.com)
+http://example.com/url_with_(brackets)
+(http://example.com/url_without_brackets)
+http://example.com/url_with_entity&amp;
+http://example.com/url_with_entity&#x26;
+http://example.com/url_with_entity&#038;
+http://example.com/url_with_entity&nbsp;
+http://example.com/url_with_entity&#xA0;
+http://example.com/url_with_entity&#160;
+http://example.com/url_with_entity&lt;
+http://example.com/url_with_entity&#x3C;
+http://example.com/url_with_entity&#60;
+!! html/php
+<p><a rel="nofollow" class="external free" href="http://example.com">http://example.com</a>,
+<a rel="nofollow" class="external free" href="http://example.com">http://example.com</a>;
+<a rel="nofollow" class="external free" href="http://example.com">http://example.com</a>\
+<a rel="nofollow" class="external free" href="http://example.com">http://example.com</a>.
+<a rel="nofollow" class="external free" href="http://example.com">http://example.com</a>:
+<a rel="nofollow" class="external free" href="http://example.com">http://example.com</a>!
+<a rel="nofollow" class="external free" href="http://example.com">http://example.com</a>?
+<a rel="nofollow" class="external free" href="http://example.com">http://example.com</a>)
+<a rel="nofollow" class="external free" href="http://example.com/url_with_(brackets)">http://example.com/url_with_(brackets)</a>
+(<a rel="nofollow" class="external free" href="http://example.com/url_without_brackets">http://example.com/url_without_brackets</a>)
+<a rel="nofollow" class="external free" href="http://example.com/url_with_entity&amp;">http://example.com/url_with_entity&amp;</a>
+<a rel="nofollow" class="external free" href="http://example.com/url_with_entity&amp;">http://example.com/url_with_entity&amp;</a>
+<a rel="nofollow" class="external free" href="http://example.com/url_with_entity&amp;">http://example.com/url_with_entity&amp;</a>
+<a rel="nofollow" class="external free" href="http://example.com/url_with_entity">http://example.com/url_with_entity</a>&#160;
+<a rel="nofollow" class="external free" href="http://example.com/url_with_entity">http://example.com/url_with_entity</a>&#xa0;
+<a rel="nofollow" class="external free" href="http://example.com/url_with_entity">http://example.com/url_with_entity</a>&#160;
+<a rel="nofollow" class="external free" href="http://example.com/url_with_entity">http://example.com/url_with_entity</a>&lt;
+<a rel="nofollow" class="external free" href="http://example.com/url_with_entity">http://example.com/url_with_entity</a>&#x3c;
+<a rel="nofollow" class="external free" href="http://example.com/url_with_entity">http://example.com/url_with_entity</a>&#60;
+</p>
+!! html/parsoid
+<p><a rel="mw:ExtLink" class="external free" href="http://example.com">http://example.com</a>,
+<a rel="mw:ExtLink" class="external free" href="http://example.com">http://example.com</a>;
+<a rel="mw:ExtLink" class="external free" href="http://example.com">http://example.com</a>\
+<a rel="mw:ExtLink" class="external free" href="http://example.com">http://example.com</a>.
+<a rel="mw:ExtLink" class="external free" href="http://example.com">http://example.com</a>:
+<a rel="mw:ExtLink" class="external free" href="http://example.com">http://example.com</a>!
+<a rel="mw:ExtLink" class="external free" href="http://example.com">http://example.com</a>?
+<a rel="mw:ExtLink" class="external free" href="http://example.com">http://example.com</a>)
+<a rel="mw:ExtLink" class="external free" href="http://example.com/url_with_(brackets)">http://example.com/url_with_(brackets)</a>
+(<a rel="mw:ExtLink" class="external free" href="http://example.com/url_without_brackets">http://example.com/url_without_brackets</a>)
+<a rel="mw:ExtLink" class="external free" href="http://example.com/url_with_entity&amp;">http://example.com/url_with_entity&amp;</a>
+<a rel="mw:ExtLink" class="external free" href="http://example.com/url_with_entity&amp;">http://example.com/url_with_entity&amp;</a>
+<a rel="mw:ExtLink" class="external free" href="http://example.com/url_with_entity&amp;">http://example.com/url_with_entity&amp;</a>
+<a rel="mw:ExtLink" class="external free" href="http://example.com/url_with_entity">http://example.com/url_with_entity</a><span typeof="mw:Entity" data-parsoid='{"src":"&amp;nbsp;","srcContent":" "}'> </span>
+<a rel="mw:ExtLink" class="external free" href="http://example.com/url_with_entity">http://example.com/url_with_entity</a><span typeof="mw:Entity" data-parsoid='{"src":"&amp;#xA0;","srcContent":" "}'> </span>
+<a rel="mw:ExtLink" class="external free" href="http://example.com/url_with_entity">http://example.com/url_with_entity</a><span typeof="mw:Entity" data-parsoid='{"src":"&amp;#160;","srcContent":" "}'> </span>
+<a rel="mw:ExtLink" class="external free" href="http://example.com/url_with_entity">http://example.com/url_with_entity</a><span typeof="mw:Entity" data-parsoid='{"src":"&amp;lt;","srcContent":"&lt;"}'>&lt;</span>
+<a rel="mw:ExtLink" class="external free" href="http://example.com/url_with_entity">http://example.com/url_with_entity</a><span typeof="mw:Entity" data-parsoid='{"src":"&amp;#x3C;","srcContent":"&lt;"}'>&lt;</span>
+<a rel="mw:ExtLink" class="external free" href="http://example.com/url_with_entity">http://example.com/url_with_entity</a><span typeof="mw:Entity" data-parsoid='{"src":"&amp;#60;","srcContent":"&lt;"}'>&lt;</span></p>
+!! end
+
+!! test
+External links: tricky Parsoid html2html case
+!! options
+parsoid=wt2html,wt2wt,html2html
+!! wikitext
+http://example.com/url_with_entity&amp;amp;
+!! html/php
+<p><a rel="nofollow" class="external free" href="http://example.com/url_with_entity&amp;amp">http://example.com/url_with_entity&amp;amp</a>;
+</p>
+!! html/parsoid
+<p><a rel="mw:ExtLink" class="external free" href="http://example.com/url_with_entity&amp;amp">http://example.com/url_with_entity&amp;amp</a>;</p>
+!! end
+
+!! test
+External links: Free with trailing quotes (T113666)
+!! wikitext
+'''News:''' Stuff here
+
+news:'a'b''c''d e
+!! html/php
+<p><b>News:</b> Stuff here
+</p><p><a rel="nofollow" class="external free" href="news:&#39;a&#39;b">news:'a'b</a><i>c</i>d e
+</p>
+!! html/parsoid
+<p><b>News:</b> Stuff here</p>
+<p><a rel="mw:ExtLink" class="external free" href="news:'a'b">news:'a'b</a><i>c</i>d e</p>
+!! end
+
+!! test
+External links: with entity
+!! wikitext
+[http://&#x20;www.librarieswithoutborders.org Libraries without borders]
+!! html/php
+<p><a rel="nofollow" class="external text" href="http://+www.librarieswithoutborders.org">Libraries without borders</a>
+</p>
+!! html/parsoid
+<p><a rel="mw:ExtLink" class="external text" href="http://+www.librarieswithoutborders.org" data-parsoid='{"a":{"href":"http://+www.librarieswithoutborders.org"},"sa":{"href":"http://&amp;#x20;www.librarieswithoutborders.org"}}'>Libraries without borders</a></p>
+!! end
+
+!! test
+External links: Lone protocols are never linked (T105697)
+!! wikitext
+http://
+http://;
+(http://)
+bitcoin:
+bitcoin:;
+(bitcoin:)
+!! html
+<p>http://
+http://;
+(http://)
+bitcoin:
+bitcoin:;
+(bitcoin:)
+</p>
+!! end
+
+!! test
+External links: No preceding word characters allowed (T67278)
+!! wikitext
+NOPEhttp://example.com
+N0http://example.com
+ok:http://example.com
+ok-http://example.com
+!! html
+<p>NOPEhttp://example.com
+N0http://example.com
+ok:<a rel="nofollow" class="external free" href="http://example.com">http://example.com</a>
+ok-<a rel="nofollow" class="external free" href="http://example.com">http://example.com</a>
+</p>
+!! end
+
+!! test
+External links: nofollow domain exception
+!! wikitext
+A [https://no-nofollow.org/foobar link], and another [https://example.org link].
+!! html
+<p>A <a class="external text" href="https://no-nofollow.org/foobar">link</a>, and another <a rel="nofollow" class="external text" href="https://example.org">link</a>.
+</p>
+!!end
+
+!! test
+External image
+!! wikitext
+External image: http://meta.wikimedia.org/upload/f/f1/Ncwikicol.png
+!! html
+<p>External image: <img src="http://meta.wikimedia.org/upload/f/f1/Ncwikicol.png" alt="Ncwikicol.png"/>
+</p>
+!! end
+
+!! test
+External image from https
+!! wikitext
+External image from https: https://meta.wikimedia.org/upload/f/f1/Ncwikicol.png
+!! html
+<p>External image from https: <img src="https://meta.wikimedia.org/upload/f/f1/Ncwikicol.png" alt="Ncwikicol.png"/>
+</p>
+!! end
+
+!! test
+External image (when not allowed)
+!! options
+wgAllowExternalImages=0
+!! wikitext
+External image: http://meta.wikimedia.org/upload/f/f1/Ncwikicol.png
+!! html
+<p>External image: <a rel="nofollow" class="external free" href="http://meta.wikimedia.org/upload/f/f1/Ncwikicol.png">http://meta.wikimedia.org/upload/f/f1/Ncwikicol.png</a>
+</p>
+!! end
+
+!! test
+Link to non-http image, no img tag
+!! wikitext
+Link to non-http image, no img tag: ftp://example.com/test.jpg
+!! html
+<p>Link to non-http image, no img tag: <a rel="nofollow" class="external free" href="ftp://example.com/test.jpg">ftp://example.com/test.jpg</a>
+</p>
+!! end
+
+!! test
+External links: terminating separator
+!! wikitext
+Terminating separator: http://example.com/thing,
+!! html
+<p>Terminating separator: <a rel="nofollow" class="external free" href="http://example.com/thing">http://example.com/thing</a>,
+</p>
+!! end
+
+!! test
+External links: intervening separator
+!! wikitext
+Intervening separator: http://example.com/1,2,3
+!! html
+<p>Intervening separator: <a rel="nofollow" class="external free" href="http://example.com/1,2,3">http://example.com/1,2,3</a>
+</p>
+!! end
+
+!! test
+External links: old bug with URL in query
+!! wikitext
+Old bug with URL in query: [http://example.com/thing?url=http://example.com link]
+!! html
+<p>Old bug with URL in query: <a rel="nofollow" class="external text" href="http://example.com/thing?url=http://example.com">link</a>
+</p>
+!! end
+
+!! test
+External links: old URL-in-URL bug, mixed protocols
+!! wikitext
+And again with mixed protocols: [ftp://example.com?url=http://example.com link]
+!! html
+<p>And again with mixed protocols: <a rel="nofollow" class="external text" href="ftp://example.com?url=http://example.com">link</a>
+</p>
+!!end
+
+# Since Parsoid is starting to emit canonical wikitext for links,
+# [http://example.com http://example.com] will not RT back to that
+# form anymore.
+!! test
+External links: URL in text
+!! options
+parsoid=wt2html
+!! wikitext
+URL in text: [http://example.com http://example.com]
+!! html/php
+<p>URL in text: <a rel="nofollow" class="external text" href="http://example.com">http://example.com</a>
+</p>
+!! html/parsoid
+<p>URL in text: <a rel="mw:ExtLink" class="external free" href="http://example.com">http://example.com</a></p>
+!! end
+
+!! test
+External links: Clickable images
+!! wikitext
+ja-style clickable images: [http://example.com http://meta.wikimedia.org/upload/f/f1/Ncwikicol.png]
+!! html/php
+<p>ja-style clickable images: <a rel="nofollow" class="external text" href="http://example.com"><img src="http://meta.wikimedia.org/upload/f/f1/Ncwikicol.png" alt="Ncwikicol.png"/></a>
+</p>
+!! html/parsoid
+<p>ja-style clickable images: <a rel="mw:ExtLink" class="external text" href="http://example.com"><img src="http://meta.wikimedia.org/upload/f/f1/Ncwikicol.png" alt="Ncwikicol.png" data-parsoid='{"type":"extlink"}'/></a></p>
+!! end
+
+!! test
+External links: raw ampersand
+!! wikitext
+Old &amp; use: http://x&y
+!! html
+<p>Old &amp; use: <a rel="nofollow" class="external free" href="http://x&amp;y">http://x&amp;y</a>
+</p>
+!! end
+
+!! test
+External links: encoded ampersand
+!! wikitext
+Old &amp; use: http://x&amp;y
+!! html/php
+<p>Old &amp; use: <a rel="nofollow" class="external free" href="http://x&amp;y">http://x&amp;y</a>
+</p>
+!! html/parsoid
+<p>Old <span typeof="mw:Entity">&amp;</span> use: <a rel="mw:ExtLink" class="external free" href="http://x&amp;y">http://x&amp;y</a></p>
+!! end
+
+!! test
+External links: encoded equals (T8102)
+!! wikitext
+http://example.com/?foo&#61;bar
+!! html/php
+<p><a rel="nofollow" class="external free" href="http://example.com/?foo=bar">http://example.com/?foo=bar</a>
+</p>
+!! html/parsoid
+<p><a rel="mw:ExtLink" class="external free" href="http://example.com/?foo=bar">http://example.com/?foo=bar</a></p>
+!! end
+
+##
+## Note that parsoid doesn't explicit mark autonumbered links, nor
+## does it number them. As discussed in T55505, we can identify
+## autonumbered links via CSS.
+##
+
+!! test
+External links: [raw ampersand]
+!! wikitext
+Old &amp; use: [http://x&y]
+!! html/php
+<p>Old &amp; use: <a rel="nofollow" class="external autonumber" href="http://x&amp;y">[1]</a>
+</p>
+!! html/parsoid
+<p>Old <span typeof="mw:Entity">&amp;</span> use: <a rel="mw:ExtLink" class="external autonumber" href="http://x&amp;y"></a></p>
+!! end
+
+# note that parsoid html is identical to [raw ampersand] case; so html2wt
+# mode will return the [raw ampersand] wikitext
+!! test
+External links: [encoded ampersand]
+!! options
+parsoid=wt2html,wt2wt,html2html
+!! wikitext
+Old &amp; use: [http://x&amp;y]
+!! html/php
+<p>Old &amp; use: <a rel="nofollow" class="external autonumber" href="http://x&amp;y">[1]</a>
+</p>
+!! html/parsoid
+<p>Old <span typeof="mw:Entity">&amp;</span> use: <a rel="mw:ExtLink" class="external autonumber" href="http://x&amp;y"></a></p>
+!! end
+
+!! test
+External links: [raw equals]
+!! wikitext
+[http://example.com/?foo=bar]
+!! html/php
+<p><a rel="nofollow" class="external autonumber" href="http://example.com/?foo=bar">[1]</a>
+</p>
+!! html/parsoid
+<p><a rel="mw:ExtLink" class="external autonumber" href="http://example.com/?foo=bar"></a></p>
+!! end
+
+# note that parsoid html is identical to [raw equals] case; so html2wt
+# mode will return the [raw equals] wikitext
+!! test
+External links: [encoded equals] (T8102)
+!! options
+parsoid=wt2html,wt2wt,html2html
+!! wikitext
+[http://example.com/?foo&#61;bar]
+!! html/php
+<p><a rel="nofollow" class="external autonumber" href="http://example.com/?foo=bar">[1]</a>
+</p>
+!! html/parsoid
+<p><a rel="mw:ExtLink" class="external autonumber" href="http://example.com/?foo=bar"></a></p>
+!! end
+
+# xxx parsoid strips the IDN character, so the round-trip tests will
+# obviously fail and are disabled. --cscott
+!! test
+External links: [IDN ignored character reference in hostname; strip it right off]
+!! options
+parsoid=wt2html,wt2wt,html2html
+!! wikitext
+[http://e&zwnj;xample.com/]
+!! html/php
+<p><a rel="nofollow" class="external autonumber" href="http://example.com/">[1]</a>
+</p>
+!! html/parsoid
+<p><a rel="mw:ExtLink" class="external autonumber" href="http://example.com/"></a></p>
+!! end
+
+# FIXME: This test (the IDN characters in the text of a link) is an inconsistency.
+# Where an external link could easily circumvent the sanitization of the text of
+# a link like this (where an IDN-ignore character is in the URL somewhere), this
+# test demands a higher standard. That's a bit strange.
+#
+# Example:
+#
+# http://e‌xample.com -> [http://example.com|http://example.com]
+# [http://example.com|http://e‌xample.com] -> [http://example.com|http://e‌xample.com]
+#
+# The first example is sanitized, but the second is not. Any security benefits
+# from this production are trivial to circumvent. Either remove this test and
+# let the parser(s) do their thing unaccosted, or fix the inconsistency and change
+# the test accordingly.
+#
+# All our love,
+# The Parsoid team.
+# xxx parsoid strips the IDN character, so the round-trip tests will
+# obviously fail and are disabled. --cscott
+!! test
+External links: IDN ignored character reference in hostname; strip it right off
+!! options
+parsoid=wt2html,html2html
+!! wikitext
+http://e&zwnj;xample.com/
+!! html/php
+<p><a rel="nofollow" class="external free" href="http://example.com/">http://example.com/</a>
+</p>
+!! html/parsoid
+<p><a rel="mw:ExtLink" class="external free" href="http://example.com/">http://example.com/</a></p>
+!! end
+
+!! test
+External links: www.jpeg.org (T2554)
+!! wikitext
+http://www.jpeg.org
+!! html
+<p><a rel="nofollow" class="external free" href="http://www.jpeg.org">http://www.jpeg.org</a>
+</p>
+!! end
+
+# parsoid doesn't explicitly mark autonumbered links, see T55505
+!! test
+External links: URL within URL (T2002)
+!! wikitext
+[http://www.unausa.org/newindex.asp?place=http://www.unausa.org/programs/mun.asp]
+!! html/php
+<p><a rel="nofollow" class="external autonumber" href="http://www.unausa.org/newindex.asp?place=http://www.unausa.org/programs/mun.asp">[1]</a>
+</p>
+!! html/parsoid
+<p><a rel="mw:ExtLink" class="external autonumber" href="http://www.unausa.org/newindex.asp?place=http://www.unausa.org/programs/mun.asp"></a></p>
+!! end
+
+!! test
+T2361: URL inside bracketed URL
+!! wikitext
+[http://www.example.com/foo http://www.example.com/bar]
+!! html
+<p><a rel="nofollow" class="external text" href="http://www.example.com/foo">http://www.example.com/bar</a>
+</p>
+!! end
+
+!! test
+T2361: URL within URL, not bracketed
+!! wikitext
+http://www.example.com/foo?=http://www.example.com/bar
+!! html
+<p><a rel="nofollow" class="external free" href="http://www.example.com/foo?=http://www.example.com/bar">http://www.example.com/foo?=http://www.example.com/bar</a>
+</p>
+!! end
+
+!! test
+T2289: ">"-token in URL-tail
+!! wikitext
+http://www.example.com/<hello>
+!! html
+<p><a rel="nofollow" class="external free" href="http://www.example.com/">http://www.example.com/</a>&lt;hello&gt;
+</p>
+!!end
+
+!! test
+T2289: literal ">"-token in URL-tail
+!! wikitext
+http://www.example.com/<b>html</b>
+!! html/php
+<p><a rel="nofollow" class="external free" href="http://www.example.com/">http://www.example.com/</a><b>html</b>
+</p>
+!! html/parsoid
+<p><a rel="mw:ExtLink" class="external free" href="http://www.example.com/" data-parsoid='{"stx":"url"}'>http://www.example.com/</a><b data-parsoid='{"stx":"html"}'>html</b></p>
+!! end
+
+!! test
+T2289: ">"-token in bracketed URL
+!! wikitext
+[http://www.example.com/<hello> stuff]
+!! html
+<p><a rel="nofollow" class="external text" href="http://www.example.com/">&lt;hello&gt; stuff</a>
+</p>
+!!end
+
+!! test
+T2289: literal ">"-token in bracketed URL
+!! wikitext
+[http://www.example.com/<b>html</b> stuff]
+!! html
+<p><a rel="nofollow" class="external text" href="http://www.example.com/"><b>html</b> stuff</a>
+</p>
+!!end
+
+!! test
+T2289: literal double quote at end of URL
+!! wikitext
+http://www.example.com/"hello"
+!! html
+<p><a rel="nofollow" class="external free" href="http://www.example.com/">http://www.example.com/</a>"hello"
+</p>
+!!end
+
+!! test
+T2289: literal double quote in bracketed URL
+!! wikitext
+[http://www.example.com/"hello" stuff]
+!! html
+<p><a rel="nofollow" class="external text" href="http://www.example.com/">"hello" stuff</a>
+</p>
+!!end
+
+!! test
+External links: multiple legal whitespace is fine, Magnus. Don't break it please. (T7081)
+!! wikitext
+[http://www.example.com test]
+!! html
+<p><a rel="nofollow" class="external text" href="http://www.example.com">test</a>
+</p>
+!! end
+
+!! test
+External links: link text with spaces
+!! wikitext
+[http://www.example.com a b c]
+[http://www.example.com ''a'' ''b'']
+!! html
+<p><a rel="nofollow" class="external text" href="http://www.example.com">a b c</a>
+<a rel="nofollow" class="external text" href="http://www.example.com"><i>a</i> <i>b</i></a>
+</p>
+!! end
+
+# Note edge case difference between PHP and Parsoid here.
+!! test
+External links: wiki links within external link (T5695)
+!! options
+parsoid=wt2html,html2html
+!! wikitext
+[http://example.com [[wikilink]] embedded in ext link]
+
+[http://example.com test [[wikilink]] embedded in ext link]
+!! html/php
+<p><a rel="nofollow" class="external text" href="http://example.com"></a><a href="/index.php?title=Wikilink&amp;action=edit&amp;redlink=1" class="new" title="Wikilink (page does not exist)">wikilink</a><a rel="nofollow" class="external text" href="http://example.com"> embedded in ext link</a>
+</p><p><a rel="nofollow" class="external text" href="http://example.com">test </a><a href="/index.php?title=Wikilink&amp;action=edit&amp;redlink=1" class="new" title="Wikilink (page does not exist)">wikilink</a><a rel="nofollow" class="external text" href="http://example.com"> embedded in ext link</a>
+</p>
+!! html/parsoid
+<p><a rel="mw:ExtLink" class="external autonumber" href="http://example.com"></a><a rel="mw:WikiLink" href="./Wikilink" title="Wikilink">wikilink</a><span> embedded in ext link</span></p>
+<p><a rel="mw:ExtLink" class="external text" href="http://example.com">test </a><a rel="mw:WikiLink" href="./Wikilink" title="Wikilink">wikilink</a><span> embedded in ext link</span></p>
+!! end
+
+!! test
+T2787: Links with one slash after the url protocol are invalid
+!! wikitext
+http:/example.com
+
+[http:/example.com title]
+!! html
+<p>http:/example.com
+</p><p>[http:/example.com title]
+</p>
+!! end
+
+!! test
+Bracketed external links with template-generated invalid target
+!! wikitext
+[{{echo|http:/example.com}} title]
+!! html
+<p>[http:/example.com title]
+</p>
+!! end
+
+# wt2html only because Parsoid would want to add <nowiki>s coming from html
+!! test
+Broken wikilinks (but not external links) prevent templates from closing
+!! options
+parsoid=wt2html
+!! wikitext
+[http://example.com x
+
+{{echo|[http://example.com x}}
+
+[[Foo
+
+{{echo|[[Foo}}
+!! html/php
+<p>[<a rel="nofollow" class="external free" href="http://example.com">http://example.com</a> x
+</p><p>[<a rel="nofollow" class="external free" href="http://example.com">http://example.com</a> x
+</p><p>[[Foo
+</p><p>{{echo|[[Foo}}
+</p>
+!! html/parsoid
+<p>[<a rel="mw:ExtLink" class="external free" href="http://example.com">http://example.com</a> x</p>
+<p typeof="mw:Transclusion" data-mw='{"parts":[{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"[http://example.com x"}},"i":0}}]}'>[<a rel="mw:ExtLink" class="external free" href="http://example.com">http://example.com</a> x</p>
+<p>[[Foo</p>
+<p>{{echo|[[Foo}}</p>
+!! end
+
+!! test
+Wikilinks with embedded newlines are not broken
+!! wikitext
+{{echo|[[ Foo
+B
+C]]}}
+!! html/php
+<p>[[ Foo
+B
+C]]
+</p>
+!! html/parsoid
+<p typeof="mw:Transclusion" data-mw='{"parts":[{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"[[ Foo\nB\nC]]"}},"i":0}}]}'>[[ Foo B C]]</p>
+!! end
+
+!! test
+Broken templates
+!! options
+parsoid=wt2html
+!! wikitext
+{{echo|[[Foo|}}]]
+
+[[Foo|{{echo|]]}}
+!! html/php
+<p>{{echo|<a href="/wiki/Foo" title="Foo">}}</a>
+</p><p>[[Foo|]]
+</p>
+!! html/parsoid
+<p>{{echo|<a rel="mw:WikiLink" href="./Foo" title="Foo">}}</a></p>
+<p>[[Foo|<span typeof="mw:Transclusion" data-mw='{"parts":[{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"]]"}},"i":0}}]}'>]]</span></p>
+!! end
+
+!! test
+T4702: Mismatched <i>, <b> and <a> tags are invalid
+!! wikitext
+''[http://example.com text'']
+[http://example.com '''text]'''
+''Something [http://example.com in italic'']
+''Something [http://example.com mixed''''', even bold]'''
+'''''Now [http://example.com both''''']
+!! html
+<p><a rel="nofollow" class="external text" href="http://example.com"><i>text</i></a>
+<a rel="nofollow" class="external text" href="http://example.com"><b>text</b></a>
+<i>Something </i><a rel="nofollow" class="external text" href="http://example.com"><i>in italic</i></a>
+<i>Something </i><a rel="nofollow" class="external text" href="http://example.com"><i>mixed</i><b>, even bold</b></a>
+<i><b>Now </b></i><a rel="nofollow" class="external text" href="http://example.com"><i><b>both</b></i></a>
+</p>
+!! end
+
+
+!! test
+T6781: %26 in URL
+!! wikitext
+http://www.example.com/?title=AT%26T
+!! html/php
+<p><a rel="nofollow" class="external free" href="http://www.example.com/?title=AT%26T">http://www.example.com/?title=AT%26T</a>
+</p>
+!! html/parsoid
+<p><a rel="mw:ExtLink" class="external free" href="http://www.example.com/?title=AT%26T">http://www.example.com/?title=AT%26T</a></p>
+!! end
+
+# According to https://www.w3.org/TR/2011/WD-html5-20110525/Overview.html#parsing-urls a plain
+# % is actually legal in HTML5. Any change in output would need testing though.
+!! test
+T6781, T7267: %25 in URL
+!! wikitext
+http://www.example.com/?title=100%25_Bran
+!! html/php
+<p><a rel="nofollow" class="external free" href="http://www.example.com/?title=100%25_Bran">http://www.example.com/?title=100%25_Bran</a>
+</p>
+!! html/parsoid
+<p><a rel="mw:ExtLink" class="external free" href="http://www.example.com/?title=100%25_Bran">http://www.example.com/?title=100%25_Bran</a></p>
+!! end
+
+!! test
+T6781, T7267: %28, %29 in URL
+!! wikitext
+http://www.example.com/?title=Ben-Hur_%281959_film%29
+!! html/php
+<p><a rel="nofollow" class="external free" href="http://www.example.com/?title=Ben-Hur_%281959_film%29">http://www.example.com/?title=Ben-Hur_%281959_film%29</a>
+</p>
+!! html/parsoid
+<p><a rel="mw:ExtLink" class="external free" href="http://www.example.com/?title=Ben-Hur_%281959_film%29">http://www.example.com/?title=Ben-Hur_%281959_film%29</a></p>
+!! end
+
+
+!! test
+T6781: %26 in autonumber URL
+!! wikitext
+[http://www.example.com/?title=AT%26T]
+!! html/php
+<p><a rel="nofollow" class="external autonumber" href="http://www.example.com/?title=AT%26T">[1]</a>
+</p>
+!! html/parsoid
+<p><a rel="mw:ExtLink" class="external autonumber" href="http://www.example.com/?title=AT%26T"></a></p>
+!! end
+
+!! test
+T6781, T7267: %26 in autonumber URL
+!! wikitext
+[http://www.example.com/?title=100%25_Bran]
+!! html/php
+<p><a rel="nofollow" class="external autonumber" href="http://www.example.com/?title=100%25_Bran">[1]</a>
+</p>
+!! html/parsoid
+<p><a rel="mw:ExtLink" class="external autonumber" href="http://www.example.com/?title=100%25_Bran"></a></p>
+!! end
+
+!! test
+T6781, T7267: %28, %29 in autonumber URL
+!! wikitext
+[http://www.example.com/?title=Ben-Hur_%281959_film%29]
+!! html/php
+<p><a rel="nofollow" class="external autonumber" href="http://www.example.com/?title=Ben-Hur_%281959_film%29">[1]</a>
+</p>
+!! html/parsoid
+<p><a rel="mw:ExtLink" class="external autonumber" href="http://www.example.com/?title=Ben-Hur_%281959_film%29"></a></p>
+!! end
+
+
+!! test
+T6781: %26 in bracketed URL
+!! wikitext
+[http://www.example.com/?title=AT%26T link]
+!! html/php
+<p><a rel="nofollow" class="external text" href="http://www.example.com/?title=AT%26T">link</a>
+</p>
+!! html/parsoid
+<p><a rel="mw:ExtLink" class="external text" href="http://www.example.com/?title=AT%26T">link</a></p>
+!! end
+
+!! test
+T6781, T7267: %25 in bracketed URL
+!! wikitext
+[http://www.example.com/?title=100%25_Bran link]
+!! html
+<p><a rel="nofollow" class="external text" href="http://www.example.com/?title=100%25_Bran">link</a>
+</p>
+!! end
+
+!! test
+T6781, T7267: %28, %29 in bracketed URL
+!! wikitext
+[http://www.example.com/?title=Ben-Hur_%281959_film%29 link]
+!! html/php
+<p><a rel="nofollow" class="external text" href="http://www.example.com/?title=Ben-Hur_%281959_film%29">link</a>
+</p>
+!! html/parsoid
+<p><a rel="mw:ExtLink" class="external text" href="http://www.example.com/?title=Ben-Hur_%281959_film%29">link</a></p>
+!! end
+
+!! test
+External link containing a period in the anchor. (T65947)
+!! wikitext
+[//foo.org/bar#baz. bang]
+
+[//foo.org/bar. bang]
+!! html/php
+<p><a rel="nofollow" class="external text" href="//foo.org/bar#baz.">bang</a>
+</p><p><a rel="nofollow" class="external text" href="//foo.org/bar.">bang</a>
+</p>
+!! html/parsoid
+<p><a rel="mw:ExtLink" class="external text" href="//foo.org/bar#baz.">bang</a></p>
+<p><a rel="mw:ExtLink" class="external text" href="//foo.org/bar.">bang</a></p>
+!! end
+
+!! test
+External link containing a single quote. (T65947)
+!! wikitext
+[//foo.org/bar'baz]
+
+[//foo.org/bar'baz bang]
+!! html/php
+<p><a rel="nofollow" class="external autonumber" href="//foo.org/bar&#39;baz">[1]</a>
+</p><p><a rel="nofollow" class="external text" href="//foo.org/bar&#39;baz">bang</a>
+</p>
+!! html/parsoid
+<p><a rel="mw:ExtLink" class="external autonumber" href="//foo.org/bar'baz"></a></p>
+<p><a rel="mw:ExtLink" class="external text" href="//foo.org/bar'baz">bang</a></p>
+!! end
+
+!! test
+External link containing double-single-quotes in text '' (T6598 sanity check)
+!! wikitext
+Some [http://example.com/ pretty ''italics'' and stuff]!
+!! html
+<p>Some <a rel="nofollow" class="external text" href="http://example.com/">pretty <i>italics</i> and stuff</a>!
+</p>
+!! end
+
+!! test
+External link containing double-single-quotes in text embedded in italics (T6598 sanity check)
+!! wikitext
+''Some [http://example.com/ pretty ''italics'' and stuff]!''
+!! html
+<p><i>Some </i><a rel="nofollow" class="external text" href="http://example.com/"><i>pretty </i>italics<i> and stuff</i></a><i>!</i>
+</p>
+!! end
+
+# Don't add the html/php section since the output is broken and there isn't any reason to spec it
+!! test
+External link containing double-single-quotes with no space separating the url from text in italics
+!! wikitext
+[http://www.musee-picasso.fr/pages/page_id18528_u1l2.htm''La muerte de Casagemas'' (1901) en el sitio de [[Museo Picasso (París)|Museo Picasso]].]
+!! html/php+tidy
+<p><a rel="nofollow" class="external text" href="http://www.musee-picasso.fr/pages/page_id18528_u1l2.htm"><i>La muerte de Casagemas</i> (1901) en el sitio de </a><a href="/index.php?title=Museo_Picasso_(Par%C3%ADs)&amp;action=edit&amp;redlink=1" class="new" title="Museo Picasso (París) (page does not exist)">Museo Picasso</a>.
+</p>
+!! html/parsoid
+<p><a rel="mw:ExtLink" class="external text" href="http://www.musee-picasso.fr/pages/page_id18528_u1l2.htm"><i>La muerte de Casagemas</i> (1901) en el sitio de </a><a rel="mw:WikiLink" href="./Museo_Picasso_(París)" title="Museo Picasso (París)">Museo Picasso</a><span>.</span></p>
+!! end
+
+!! test
+External link with comments in link text
+!! wikitext
+[http://www.google.com Google <!-- comment -->]
+!! html/php
+<p><a rel="nofollow" class="external text" href="http://www.google.com">Google </a>
+</p>
+!! html/parsoid
+<p><a rel="mw:ExtLink" class="external text" href="http://www.google.com">Google <!-- comment --></a></p>
+!! end
+
+!! test
+External link to bare IPv4 address
+!! wikitext
+[http://192.168.0.1 Link]
+!! html/php
+<p><a rel="nofollow" class="external text" href="http://192.168.0.1">Link</a>
+</p>
+!! html/parsoid
+<p><a rel="mw:ExtLink" class="external text" href="http://192.168.0.1">Link</a></p>
+!! end
+
+!! test
+URL-encoding in URL functions (single parameter)
+!! wikitext
+{{localurl:Some page|amp=&}}
+!! html
+<p>/index.php?title=Some_page&amp;amp=&amp;
+</p>
+!! end
+
+!! test
+URL-encoding in URL functions (multiple parameters)
+!! wikitext
+{{localurl:Some page|q=?&amp=&}}
+!! html
+<p>/index.php?title=Some_page&amp;q=?&amp;amp=&amp;
+</p>
+!! end
+
+!! test
+Brackets in urls
+!! wikitext
+http://example.com/index.php?foozoid%5B%5D=bar
+
+http://example.com/index.php?foozoid&#x5B;&#x5D;=bar
+!! html/php
+<p><a rel="nofollow" class="external free" href="http://example.com/index.php?foozoid%5B%5D=bar">http://example.com/index.php?foozoid%5B%5D=bar</a>
+</p><p><a rel="nofollow" class="external free" href="http://example.com/index.php?foozoid%5B%5D=bar">http://example.com/index.php?foozoid%5B%5D=bar</a>
+</p>
+!! html/parsoid
+<p><a rel="mw:ExtLink" class="external free" href="http://example.com/index.php?foozoid%5B%5D=bar">http://example.com/index.php?foozoid%5B%5D=bar</a></p>
+
+<p><a rel="mw:ExtLink" class="external free" href="http://example.com/index.php?foozoid%5B%5D=bar" data-parsoid='{"stx":"url","a":{"href":"http://example.com/index.php?foozoid%5B%5D=bar"},"sa":{"href":"http://example.com/index.php?foozoid&amp;#x5B;&amp;#x5D;=bar"}}'>http://example.com/index.php?foozoid%5B%5D=bar</a></p>
+!! end
+
+!! test
+IPv6 urls, autolink format (T23261)
+!! wikitext
+http://[2404:130:0:1000::187:2]/index.php
+
+Examples from RFC 2373, section 2.2:
+
+*http://[1080::8:800:200C:417A]/unicast
+*http://[FF01::101]/multicast
+*http://[::1]/loopback
+*http://[::]/unspecified
+*http://[::13.1.68.3]/ipv4compat
+*http://[::FFFF:129.144.52.38]/ipv4compat
+
+Examples from RFC 2732, section 2:
+
+*http://[FEDC:BA98:7654:3210:FEDC:BA98:7654:3210]:80/index.html
+*http://[1080:0:0:0:8:800:200C:417A]/index.html
+*http://[3ffe:2a00:100:7031::1]
+*http://[1080::8:800:200C:417A]/foo
+*http://[::192.9.5.5]/ipng
+*http://[::FFFF:129.144.52.38]:80/index.html
+*http://[2010:836B:4179::836B:4179]
+!! html/php
+<p><a rel="nofollow" class="external free" href="http://[2404:130:0:1000::187:2]/index.php">http://[2404:130:0:1000::187:2]/index.php</a>
+</p><p>Examples from <a class="external mw-magiclink-rfc" rel="nofollow" href="https://tools.ietf.org/html/rfc2373">RFC 2373</a>, section 2.2:
+</p>
+<ul><li><a rel="nofollow" class="external free" href="http://[1080::8:800:200C:417A]/unicast">http://[1080::8:800:200C:417A]/unicast</a></li>
+<li><a rel="nofollow" class="external free" href="http://[FF01::101]/multicast">http://[FF01::101]/multicast</a></li>
+<li><a rel="nofollow" class="external free" href="http://[::1]/loopback">http://[::1]/loopback</a></li>
+<li><a rel="nofollow" class="external free" href="http://[::]/unspecified">http://[::]/unspecified</a></li>
+<li><a rel="nofollow" class="external free" href="http://[::13.1.68.3]/ipv4compat">http://[::13.1.68.3]/ipv4compat</a></li>
+<li><a rel="nofollow" class="external free" href="http://[::FFFF:129.144.52.38]/ipv4compat">http://[::FFFF:129.144.52.38]/ipv4compat</a></li></ul>
+<p>Examples from <a class="external mw-magiclink-rfc" rel="nofollow" href="https://tools.ietf.org/html/rfc2732">RFC 2732</a>, section 2:
+</p>
+<ul><li><a rel="nofollow" class="external free" href="http://[FEDC:BA98:7654:3210:FEDC:BA98:7654:3210]:80/index.html">http://[FEDC:BA98:7654:3210:FEDC:BA98:7654:3210]:80/index.html</a></li>
+<li><a rel="nofollow" class="external free" href="http://[1080:0:0:0:8:800:200C:417A]/index.html">http://[1080:0:0:0:8:800:200C:417A]/index.html</a></li>
+<li><a rel="nofollow" class="external free" href="http://[3ffe:2a00:100:7031::1]">http://[3ffe:2a00:100:7031::1]</a></li>
+<li><a rel="nofollow" class="external free" href="http://[1080::8:800:200C:417A]/foo">http://[1080::8:800:200C:417A]/foo</a></li>
+<li><a rel="nofollow" class="external free" href="http://[::192.9.5.5]/ipng">http://[::192.9.5.5]/ipng</a></li>
+<li><a rel="nofollow" class="external free" href="http://[::FFFF:129.144.52.38]:80/index.html">http://[::FFFF:129.144.52.38]:80/index.html</a></li>
+<li><a rel="nofollow" class="external free" href="http://[2010:836B:4179::836B:4179]">http://[2010:836B:4179::836B:4179]</a></li></ul>
+
+!! html/parsoid
+<p><a rel="mw:ExtLink" class="external free" href="http://[2404:130:0:1000::187:2]/index.php">http://[2404:130:0:1000::187:2]/index.php</a></p>
+
+<p>Examples from <a href="https://tools.ietf.org/html/rfc2373" rel="mw:ExtLink" class="external text">RFC 2373</a>, section 2.2:</p>
+<ul><li><a rel="mw:ExtLink" class="external free" href="http://[1080::8:800:200C:417A]/unicast">http://[1080::8:800:200C:417A]/unicast</a></li>
+<li><a rel="mw:ExtLink" class="external free" href="http://[FF01::101]/multicast">http://[FF01::101]/multicast</a></li>
+<li><a rel="mw:ExtLink" class="external free" href="http://[::1]/loopback">http://[::1]/loopback</a></li>
+<li><a rel="mw:ExtLink" class="external free" href="http://[::]/unspecified">http://[::]/unspecified</a></li>
+<li><a rel="mw:ExtLink" class="external free" href="http://[::13.1.68.3]/ipv4compat">http://[::13.1.68.3]/ipv4compat</a></li>
+<li><a rel="mw:ExtLink" class="external free" href="http://[::FFFF:129.144.52.38]/ipv4compat">http://[::FFFF:129.144.52.38]/ipv4compat</a></li></ul>
+
+<p>Examples from <a href="https://tools.ietf.org/html/rfc2732" rel="mw:ExtLink" class="external text">RFC 2732</a>, section 2:</p>
+<ul><li><a rel="mw:ExtLink" class="external free" href="http://[FEDC:BA98:7654:3210:FEDC:BA98:7654:3210]:80/index.html">http://[FEDC:BA98:7654:3210:FEDC:BA98:7654:3210]:80/index.html</a></li>
+<li><a rel="mw:ExtLink" class="external free" href="http://[1080:0:0:0:8:800:200C:417A]/index.html">http://[1080:0:0:0:8:800:200C:417A]/index.html</a></li>
+<li><a rel="mw:ExtLink" class="external free" href="http://[3ffe:2a00:100:7031::1]">http://[3ffe:2a00:100:7031::1]</a></li>
+<li><a rel="mw:ExtLink" class="external free" href="http://[1080::8:800:200C:417A]/foo">http://[1080::8:800:200C:417A]/foo</a></li>
+<li><a rel="mw:ExtLink" class="external free" href="http://[::192.9.5.5]/ipng">http://[::192.9.5.5]/ipng</a></li>
+<li><a rel="mw:ExtLink" class="external free" href="http://[::FFFF:129.144.52.38]:80/index.html">http://[::FFFF:129.144.52.38]:80/index.html</a></li>
+<li><a rel="mw:ExtLink" class="external free" href="http://[2010:836B:4179::836B:4179]">http://[2010:836B:4179::836B:4179]</a></li></ul>
+!! end
+
+!! test
+IPv6 urls, bracketed format (T23261)
+!! wikitext
+[http://[2404:130:0:1000::187:2]/index.php test]
+
+Examples from RFC 2373, section 2.2:
+
+*[http://[1080::8:800:200C:417A] unicast]
+*[http://[FF01::101] multicast]
+*[http://[::1]/ loopback]
+*[http://[::] unspecified]
+*[http://[::13.1.68.3] ipv4compat]
+*[http://[::FFFF:129.144.52.38] ipv4compat]
+
+Examples from RFC 2732, section 2:
+
+*[http://[FEDC:BA98:7654:3210:FEDC:BA98:7654:3210]:80/index.html 1]
+*[http://[1080:0:0:0:8:800:200C:417A]/index.html 2]
+*[http://[3ffe:2a00:100:7031::1] 3]
+*[http://[1080::8:800:200C:417A]/foo 4]
+*[http://[::192.9.5.5]/ipng 5]
+*[http://[::FFFF:129.144.52.38]:80/index.html 6]
+*[http://[2010:836B:4179::836B:4179] 7]
+!! html/php
+<p><a rel="nofollow" class="external text" href="http://[2404:130:0:1000::187:2]/index.php">test</a>
+</p><p>Examples from <a class="external mw-magiclink-rfc" rel="nofollow" href="https://tools.ietf.org/html/rfc2373">RFC 2373</a>, section 2.2:
+</p>
+<ul><li><a rel="nofollow" class="external text" href="http://[1080::8:800:200C:417A]">unicast</a></li>
+<li><a rel="nofollow" class="external text" href="http://[FF01::101]">multicast</a></li>
+<li><a rel="nofollow" class="external text" href="http://[::1]/">loopback</a></li>
+<li><a rel="nofollow" class="external text" href="http://[::]">unspecified</a></li>
+<li><a rel="nofollow" class="external text" href="http://[::13.1.68.3]">ipv4compat</a></li>
+<li><a rel="nofollow" class="external text" href="http://[::FFFF:129.144.52.38]">ipv4compat</a></li></ul>
+<p>Examples from <a class="external mw-magiclink-rfc" rel="nofollow" href="https://tools.ietf.org/html/rfc2732">RFC 2732</a>, section 2:
+</p>
+<ul><li><a rel="nofollow" class="external text" href="http://[FEDC:BA98:7654:3210:FEDC:BA98:7654:3210]:80/index.html">1</a></li>
+<li><a rel="nofollow" class="external text" href="http://[1080:0:0:0:8:800:200C:417A]/index.html">2</a></li>
+<li><a rel="nofollow" class="external text" href="http://[3ffe:2a00:100:7031::1]">3</a></li>
+<li><a