From 9441dde8bfb95277df073717ed7817dced40f948 Mon Sep 17 00:00:00 2001 From: Pierre Schmitz Date: Fri, 28 Mar 2014 05:41:12 +0100 Subject: Update to MediaWiki 1.22.5 --- tests/phpunit/Makefile | 91 + tests/phpunit/MediaWikiLangTestCase.php | 33 + tests/phpunit/MediaWikiPHPUnitCommand.php | 119 + tests/phpunit/MediaWikiPHPUnitTestListener.php | 114 + tests/phpunit/MediaWikiTestCase.php | 948 ++++++++ tests/phpunit/README | 53 + tests/phpunit/TODO | 10 + tests/phpunit/bootstrap.php | 15 + tests/phpunit/data/db/mysql/functions.sql | 12 + tests/phpunit/data/db/postgres/functions.sql | 12 + tests/phpunit/data/db/sqlite/tables-1.13.sql | 342 +++ tests/phpunit/data/db/sqlite/tables-1.15.sql | 454 ++++ tests/phpunit/data/db/sqlite/tables-1.16.sql | 478 ++++ tests/phpunit/data/db/sqlite/tables-1.17.sql | 511 +++++ tests/phpunit/data/db/sqlite/tables-1.18.sql | 530 +++++ .../data/less/common/test.common.mixins.less | 5 + tests/phpunit/data/less/module/dependency.less | 3 + tests/phpunit/data/less/module/styles.css | 6 + tests/phpunit/data/less/module/styles.less | 6 + tests/phpunit/data/media/1bit-png.png | Bin 0 -> 167 bytes tests/phpunit/data/media/80x60-2layers.xcf | Bin 0 -> 1162 bytes tests/phpunit/data/media/80x60-Greyscale.xcf | Bin 0 -> 667 bytes tests/phpunit/data/media/80x60-RGB.xcf | Bin 0 -> 677 bytes .../Animated_PNG_example_bouncing_beach_ball.png | Bin 0 -> 72209 bytes tests/phpunit/data/media/Gtk-media-play-ltr.svg | 35 + tests/phpunit/data/media/Png-native-test.png | Bin 0 -> 4665 bytes tests/phpunit/data/media/QA_icon.svg | 77 + tests/phpunit/data/media/README | 38 + tests/phpunit/data/media/Toll_Texas_1.svg | 150 ++ .../media/US_states_by_total_state_tax_revenue.svg | 248 +++ tests/phpunit/data/media/Wikimedia-logo.svg | 14 + .../data/media/Xmp-exif-multilingual_test.jpg | Bin 0 -> 12544 bytes tests/phpunit/data/media/animated-xmp.gif | Bin 0 -> 3864 bytes tests/phpunit/data/media/animated.gif | Bin 0 -> 497 bytes tests/phpunit/data/media/broken_exif_date.jpg | Bin 0 -> 3233 bytes tests/phpunit/data/media/exif-gps.jpg | Bin 0 -> 665 bytes tests/phpunit/data/media/exif-user-comment.jpg | Bin 0 -> 484 bytes tests/phpunit/data/media/greyscale-na-png.png | Bin 0 -> 365 bytes tests/phpunit/data/media/greyscale-png.png | Bin 0 -> 415 bytes tests/phpunit/data/media/iptc-invalid-psir.jpg | Bin 0 -> 9574 bytes tests/phpunit/data/media/iptc-timetest-invalid.jpg | Bin 0 -> 9573 bytes tests/phpunit/data/media/iptc-timetest.jpg | Bin 0 -> 9573 bytes tests/phpunit/data/media/jpeg-comment-binary.jpg | Bin 0 -> 448 bytes .../phpunit/data/media/jpeg-comment-iso8859-1.jpg | Bin 0 -> 447 bytes tests/phpunit/data/media/jpeg-comment-multiple.jpg | Bin 0 -> 431 bytes tests/phpunit/data/media/jpeg-comment-utf.jpg | Bin 0 -> 445 bytes tests/phpunit/data/media/jpeg-iptc-bad-hash.jpg | Bin 0 -> 499 bytes tests/phpunit/data/media/jpeg-iptc-good-hash.jpg | Bin 0 -> 499 bytes tests/phpunit/data/media/jpeg-padding-even.jpg | Bin 0 -> 450 bytes tests/phpunit/data/media/jpeg-padding-odd.jpg | Bin 0 -> 451 bytes tests/phpunit/data/media/jpeg-xmp-alt.jpg | Bin 0 -> 3255 bytes tests/phpunit/data/media/jpeg-xmp-psir.jpg | Bin 0 -> 3308 bytes tests/phpunit/data/media/jpeg-xmp-psir.xmp | 35 + tests/phpunit/data/media/landscape-plain.jpg | Bin 0 -> 38771 bytes tests/phpunit/data/media/nonanimated.gif | Bin 0 -> 200 bytes tests/phpunit/data/media/portrait-rotated.jpg | Bin 0 -> 38577 bytes tests/phpunit/data/media/rgb-na-png.png | Bin 0 -> 593 bytes tests/phpunit/data/media/rgb-png.png | Bin 0 -> 663 bytes tests/phpunit/data/media/test.jpg | Bin 0 -> 437 bytes tests/phpunit/data/media/test.tiff | Bin 0 -> 566 bytes tests/phpunit/data/media/xmp.png | Bin 0 -> 582 bytes tests/phpunit/data/xmp/1.result.php | 8 + tests/phpunit/data/xmp/1.xmp | 11 + tests/phpunit/data/xmp/2.result.php | 8 + tests/phpunit/data/xmp/2.xmp | 12 + tests/phpunit/data/xmp/3-invalid.result.php | 7 + tests/phpunit/data/xmp/3-invalid.xmp | 31 + tests/phpunit/data/xmp/3.result.php | 8 + tests/phpunit/data/xmp/3.xmp | 29 + tests/phpunit/data/xmp/4.result.php | 7 + tests/phpunit/data/xmp/4.xmp | 22 + tests/phpunit/data/xmp/5.result.php | 7 + tests/phpunit/data/xmp/5.xmp | 16 + tests/phpunit/data/xmp/6.result.php | 8 + tests/phpunit/data/xmp/6.xmp | 18 + tests/phpunit/data/xmp/7.result.php | 52 + tests/phpunit/data/xmp/7.xmp | 67 + tests/phpunit/data/xmp/README | 3 + tests/phpunit/data/xmp/bag-for-seq.result.php | 10 + tests/phpunit/data/xmp/bag-for-seq.xmp | 1 + tests/phpunit/data/xmp/flash.result.php | 8 + tests/phpunit/data/xmp/flash.xmp | 11 + tests/phpunit/data/xmp/gps.result.php | 11 + tests/phpunit/data/xmp/gps.xmp | 17 + .../data/xmp/invalid-child-not-struct.result.php | 7 + .../phpunit/data/xmp/invalid-child-not-struct.xmp | 12 + tests/phpunit/data/xmp/no-namespace.result.php | 7 + tests/phpunit/data/xmp/no-namespace.xmp | 11 + .../data/xmp/no-recognized-props.result.php | 2 + tests/phpunit/data/xmp/no-recognized-props.xmp | 8 + tests/phpunit/data/xmp/utf16BE.result.php | 12 + tests/phpunit/data/xmp/utf16BE.xmp | Bin 0 -> 930 bytes tests/phpunit/data/xmp/utf16LE.result.php | 12 + tests/phpunit/data/xmp/utf16LE.xmp | Bin 0 -> 930 bytes tests/phpunit/data/xmp/utf32BE.result.php | 12 + tests/phpunit/data/xmp/utf32BE.xmp | Bin 0 -> 1856 bytes tests/phpunit/data/xmp/utf32LE.result.php | 12 + tests/phpunit/data/xmp/utf32LE.xmp | Bin 0 -> 1856 bytes tests/phpunit/data/xmp/xmpExt.result.php | 8 + tests/phpunit/data/xmp/xmpExt.xmp | 13 + tests/phpunit/data/xmp/xmpExt2.xmp | 8 + tests/phpunit/data/zip/cd-gap.zip | Bin 0 -> 182 bytes tests/phpunit/data/zip/cd-truncated.zip | Bin 0 -> 171 bytes tests/phpunit/data/zip/class-trailing-null.zip | Bin 0 -> 173 bytes tests/phpunit/data/zip/class-trailing-slash.zip | Bin 0 -> 173 bytes tests/phpunit/data/zip/class.zip | Bin 0 -> 173 bytes tests/phpunit/data/zip/empty.zip | Bin 0 -> 22 bytes tests/phpunit/data/zip/looks-like-zip64.zip | Bin 0 -> 173 bytes tests/phpunit/data/zip/nosig.zip | Bin 0 -> 173 bytes tests/phpunit/data/zip/split.zip | Bin 0 -> 196 bytes tests/phpunit/data/zip/trail.zip | Bin 0 -> 181 bytes tests/phpunit/data/zip/wrong-cd-start-disk.zip | Bin 0 -> 173 bytes tests/phpunit/data/zip/wrong-central-entry-sig.zip | Bin 0 -> 173 bytes tests/phpunit/docs/ExportDemoTest.php | 39 + tests/phpunit/includes/ArticleTablesTest.php | 32 + tests/phpunit/includes/ArticleTest.php | 92 + tests/phpunit/includes/BlockTest.php | 354 +++ tests/phpunit/includes/CdbTest.php | 88 + tests/phpunit/includes/CollationTest.php | 111 + tests/phpunit/includes/DiffHistoryBlobTest.php | 44 + tests/phpunit/includes/EditPageTest.php | 491 +++++ tests/phpunit/includes/ExternalStoreTest.php | 81 + tests/phpunit/includes/ExtraParserTest.php | 157 ++ tests/phpunit/includes/FallbackTest.php | 73 + tests/phpunit/includes/FauxRequestTest.php | 15 + tests/phpunit/includes/FauxResponseTest.php | 74 + .../includes/FormOptionsInitializationTest.php | 84 + tests/phpunit/includes/FormOptionsTest.php | 91 + .../includes/GlobalFunctions/GlobalTest.php | 652 ++++++ .../includes/GlobalFunctions/GlobalWithDBTest.php | 31 + tests/phpunit/includes/GlobalFunctions/README | 2 + .../includes/GlobalFunctions/wfAssembleUrlTest.php | 111 + .../includes/GlobalFunctions/wfBCP47Test.php | 120 + .../includes/GlobalFunctions/wfBaseConvertTest.php | 182 ++ .../includes/GlobalFunctions/wfBaseNameTest.php | 36 + .../includes/GlobalFunctions/wfExpandUrlTest.php | 111 + .../includes/GlobalFunctions/wfGetCallerTest.php | 40 + .../includes/GlobalFunctions/wfParseUrlTest.php | 146 ++ .../GlobalFunctions/wfRemoveDotSegmentsTest.php | 91 + .../GlobalFunctions/wfShorthandToIntegerTest.php | 30 + .../includes/GlobalFunctions/wfTimestampTest.php | 134 ++ .../includes/GlobalFunctions/wfUrlencodeTest.php | 119 + tests/phpunit/includes/HTMLCheckMatrixTest.php | 102 + tests/phpunit/includes/HashRingTest.php | 53 + tests/phpunit/includes/HooksTest.php | 158 ++ tests/phpunit/includes/HtmlFormatterTest.php | 81 + tests/phpunit/includes/HtmlTest.php | 631 ++++++ tests/phpunit/includes/HttpTest.php | 216 ++ tests/phpunit/includes/IPTest.php | 595 +++++ tests/phpunit/includes/LanguageConverterTest.php | 148 ++ tests/phpunit/includes/LicensesTest.php | 22 + tests/phpunit/includes/LinkerTest.php | 71 + tests/phpunit/includes/LinksUpdateTest.php | 175 ++ tests/phpunit/includes/LocalFileTest.php | 107 + tests/phpunit/includes/LocalisationCacheTest.php | 31 + tests/phpunit/includes/MWExceptionHandlerTest.php | 73 + tests/phpunit/includes/MWFunctionTest.php | 30 + tests/phpunit/includes/MWNamespaceTest.php | 571 +++++ tests/phpunit/includes/MessageTest.php | 130 ++ tests/phpunit/includes/OutputPageTest.php | 133 ++ tests/phpunit/includes/PathRouterTest.php | 263 +++ tests/phpunit/includes/PreferencesTest.php | 91 + tests/phpunit/includes/RecentChangeTest.php | 280 +++ tests/phpunit/includes/RequestContextTest.php | 73 + tests/phpunit/includes/ResourceLoaderTest.php | 149 ++ tests/phpunit/includes/RevisionStorageTest.php | 546 +++++ .../RevisionStorageTest_ContentHandlerUseDB.php | 81 + tests/phpunit/includes/RevisionTest.php | 481 ++++ tests/phpunit/includes/SampleTest.php | 105 + tests/phpunit/includes/SanitizerTest.php | 302 +++ .../includes/SanitizerValidateEmailTest.php | 103 + tests/phpunit/includes/SiteConfigurationTest.php | 335 +++ tests/phpunit/includes/StringUtilsTest.php | 147 ++ tests/phpunit/includes/TemplateCategoriesTest.php | 53 + tests/phpunit/includes/TestUser.php | 59 + tests/phpunit/includes/TimeAdjustTest.php | 41 + tests/phpunit/includes/TimestampTest.php | 304 +++ tests/phpunit/includes/TitleMethodsTest.php | 300 +++ tests/phpunit/includes/TitlePermissionTest.php | 742 +++++++ tests/phpunit/includes/TitleTest.php | 485 ++++ tests/phpunit/includes/UIDGeneratorTest.php | 98 + tests/phpunit/includes/UserMailerTest.php | 14 + tests/phpunit/includes/UserTest.php | 237 ++ tests/phpunit/includes/WebRequestTest.php | 310 +++ tests/phpunit/includes/WikiPageTest.php | 1074 +++++++++ .../includes/WikiPageTest_ContentHandlerUseDB.php | 53 + tests/phpunit/includes/XmlJsTest.php | 24 + tests/phpunit/includes/XmlSelectTest.php | 173 ++ tests/phpunit/includes/XmlTest.php | 402 ++++ tests/phpunit/includes/XmlTypeCheckTest.php | 30 + tests/phpunit/includes/ZipDirectoryReaderTest.php | 85 + .../includes/api/ApiAccountCreationTest.php | 159 ++ tests/phpunit/includes/api/ApiBlockTest.php | 95 + tests/phpunit/includes/api/ApiEditPageTest.php | 417 ++++ tests/phpunit/includes/api/ApiOptionsTest.php | 420 ++++ tests/phpunit/includes/api/ApiParseTest.php | 29 + tests/phpunit/includes/api/ApiPurgeTest.php | 40 + tests/phpunit/includes/api/ApiTest.php | 259 +++ tests/phpunit/includes/api/ApiTestCase.php | 253 +++ tests/phpunit/includes/api/ApiTestCaseUpload.php | 149 ++ tests/phpunit/includes/api/ApiUploadTest.php | 561 +++++ tests/phpunit/includes/api/ApiWatchTest.php | 148 ++ .../phpunit/includes/api/PrefixUniquenessTest.php | 25 + .../phpunit/includes/api/RandomImageGenerator.php | 468 ++++ .../includes/api/format/ApiFormatPhpTest.php | 17 + .../includes/api/format/ApiFormatTestBase.php | 22 + .../phpunit/includes/api/generateRandomImages.php | 46 + .../includes/api/query/ApiQueryBasicTest.php | 395 ++++ .../includes/api/query/ApiQueryContinue2Test.php | 68 + .../includes/api/query/ApiQueryContinueTest.php | 313 +++ .../api/query/ApiQueryContinueTestBase.php | 209 ++ .../includes/api/query/ApiQueryRevisionsTest.php | 39 + tests/phpunit/includes/api/query/ApiQueryTest.php | 66 + .../includes/api/query/ApiQueryTestBase.php | 150 ++ tests/phpunit/includes/api/words.txt | 1000 +++++++++ tests/phpunit/includes/cache/GenderCacheTest.php | 104 + tests/phpunit/includes/cache/MessageCacheTest.php | 128 ++ .../phpunit/includes/cache/ProcessCacheLRUTest.php | 237 ++ .../includes/content/ContentHandlerTest.php | 451 ++++ tests/phpunit/includes/content/CssContentTest.php | 87 + .../includes/content/JavaScriptContentTest.php | 287 +++ tests/phpunit/includes/content/TextContentTest.php | 458 ++++ .../content/WikitextContentHandlerTest.php | 227 ++ .../includes/content/WikitextContentTest.php | 404 ++++ .../phpunit/includes/db/DatabaseMysqlBaseTest.php | 209 ++ tests/phpunit/includes/db/DatabaseSQLTest.php | 721 ++++++ tests/phpunit/includes/db/DatabaseSqliteTest.php | 421 ++++ tests/phpunit/includes/db/DatabaseTest.php | 234 ++ tests/phpunit/includes/db/DatabaseTestHelper.php | 166 ++ tests/phpunit/includes/db/ORMRowTest.php | 226 ++ tests/phpunit/includes/db/ORMTableTest.php | 146 ++ tests/phpunit/includes/db/TestORMRowTest.php | 215 ++ tests/phpunit/includes/debug/MWDebugTest.php | 72 + .../includes/filebackend/FileBackendTest.php | 2306 ++++++++++++++++++++ tests/phpunit/includes/filerepo/FileRepoTest.php | 55 + tests/phpunit/includes/filerepo/StoreBatchTest.php | 134 ++ .../includes/installer/InstallDocFormatterTest.php | 65 + .../includes/installer/OracleInstallerTest.php | 48 + tests/phpunit/includes/jobqueue/JobQueueTest.php | 329 +++ tests/phpunit/includes/json/FormatJsonTest.php | 161 ++ tests/phpunit/includes/libs/CSSJanusTest.php | 606 +++++ tests/phpunit/includes/libs/CSSMinTest.php | 133 ++ .../includes/libs/GenericArrayObjectTest.php | 263 +++ tests/phpunit/includes/libs/IEUrlExtensionTest.php | 126 ++ .../includes/libs/JavaScriptMinifierTest.php | 170 ++ .../phpunit/includes/logging/LogFormatterTest.php | 207 ++ tests/phpunit/includes/logging/LogTests.i18n.php | 15 + .../includes/media/BitmapMetadataHandlerTest.php | 167 ++ tests/phpunit/includes/media/BitmapScalingTest.php | 137 ++ tests/phpunit/includes/media/ExifBitmapTest.php | 139 ++ tests/phpunit/includes/media/ExifRotationTest.php | 248 +++ tests/phpunit/includes/media/ExifTest.php | 46 + tests/phpunit/includes/media/FakeDimensionFile.php | 28 + .../phpunit/includes/media/FormatMetadataTest.php | 59 + .../includes/media/GIFMetadataExtractorTest.php | 107 + tests/phpunit/includes/media/GIFTest.php | 120 + tests/phpunit/includes/media/IPTCTest.php | 81 + .../includes/media/JpegMetadataExtractorTest.php | 109 + tests/phpunit/includes/media/JpegTest.php | 33 + tests/phpunit/includes/media/MediaHandlerTest.php | 53 + .../includes/media/PNGMetadataExtractorTest.php | 155 ++ tests/phpunit/includes/media/PNGTest.php | 123 ++ .../includes/media/SVGMetadataExtractorTest.php | 112 + tests/phpunit/includes/media/TiffTest.php | 39 + tests/phpunit/includes/media/XMPTest.php | 166 ++ tests/phpunit/includes/media/XMPValidateTest.php | 46 + tests/phpunit/includes/normal/CleanUpTest.php | 409 ++++ .../phpunit/includes/objectcache/BagOStuffTest.php | 149 ++ .../phpunit/includes/parser/MagicVariableTest.php | 242 ++ .../includes/parser/MediaWikiParserTest.php | 120 + tests/phpunit/includes/parser/NewParserTest.php | 969 ++++++++ .../phpunit/includes/parser/ParserMethodsTest.php | 95 + tests/phpunit/includes/parser/ParserOutputTest.php | 59 + .../phpunit/includes/parser/ParserPreloadTest.php | 80 + tests/phpunit/includes/parser/PreprocessorTest.php | 239 ++ tests/phpunit/includes/parser/TagHooksTest.php | 82 + tests/phpunit/includes/parser/TidyTest.php | 44 + tests/phpunit/includes/search/SearchEngineTest.php | 176 ++ tests/phpunit/includes/search/SearchUpdateTest.php | 74 + tests/phpunit/includes/site/MediaWikiSiteTest.php | 90 + tests/phpunit/includes/site/SiteListTest.php | 197 ++ tests/phpunit/includes/site/SiteSQLStoreTest.php | 134 ++ tests/phpunit/includes/site/SiteTest.php | 296 +++ tests/phpunit/includes/site/TestSites.php | 100 + .../includes/specials/QueryAllSpecialPagesTest.php | 79 + .../includes/specials/SpecialPreferencesTest.php | 60 + .../includes/specials/SpecialRecentchangesTest.php | 125 ++ .../includes/specials/SpecialSearchTest.php | 139 ++ tests/phpunit/includes/upload/UploadBaseTest.php | 147 ++ .../phpunit/includes/upload/UploadFromUrlTest.php | 350 +++ tests/phpunit/includes/upload/UploadStashTest.php | 76 + tests/phpunit/install-phpunit.sh | 37 + tests/phpunit/languages/LanguageAmTest.php | 35 + tests/phpunit/languages/LanguageArTest.php | 87 + tests/phpunit/languages/LanguageBeTest.php | 42 + tests/phpunit/languages/LanguageBe_taraskTest.php | 95 + tests/phpunit/languages/LanguageBhoTest.php | 35 + tests/phpunit/languages/LanguageBsTest.php | 42 + .../phpunit/languages/LanguageClassesTestCase.php | 74 + tests/phpunit/languages/LanguageCsTest.php | 41 + tests/phpunit/languages/LanguageCuTest.php | 42 + tests/phpunit/languages/LanguageCyTest.php | 43 + tests/phpunit/languages/LanguageDsbTest.php | 41 + tests/phpunit/languages/LanguageFrTest.php | 35 + tests/phpunit/languages/LanguageGaTest.php | 35 + tests/phpunit/languages/LanguageGdTest.php | 53 + tests/phpunit/languages/LanguageGvTest.php | 44 + tests/phpunit/languages/LanguageHeTest.php | 132 ++ tests/phpunit/languages/LanguageHiTest.php | 35 + tests/phpunit/languages/LanguageHrTest.php | 42 + tests/phpunit/languages/LanguageHsbTest.php | 41 + tests/phpunit/languages/LanguageHuTest.php | 35 + tests/phpunit/languages/LanguageHyTest.php | 35 + tests/phpunit/languages/LanguageKshTest.php | 35 + tests/phpunit/languages/LanguageLnTest.php | 35 + tests/phpunit/languages/LanguageLtTest.php | 63 + tests/phpunit/languages/LanguageLvTest.php | 44 + tests/phpunit/languages/LanguageMgTest.php | 36 + tests/phpunit/languages/LanguageMkTest.php | 40 + tests/phpunit/languages/LanguageMlTest.php | 38 + tests/phpunit/languages/LanguageMoTest.php | 45 + tests/phpunit/languages/LanguageMtTest.php | 77 + tests/phpunit/languages/LanguageNlTest.php | 24 + tests/phpunit/languages/LanguageNsoTest.php | 34 + tests/phpunit/languages/LanguagePlTest.php | 77 + tests/phpunit/languages/LanguageRoTest.php | 45 + tests/phpunit/languages/LanguageRuTest.php | 105 + tests/phpunit/languages/LanguageSeTest.php | 53 + tests/phpunit/languages/LanguageSgsTest.php | 71 + tests/phpunit/languages/LanguageShTest.php | 42 + tests/phpunit/languages/LanguageSkTest.php | 42 + tests/phpunit/languages/LanguageSlTest.php | 44 + tests/phpunit/languages/LanguageSmaTest.php | 53 + tests/phpunit/languages/LanguageSrTest.php | 246 +++ tests/phpunit/languages/LanguageTest.php | 1567 +++++++++++++ tests/phpunit/languages/LanguageTiTest.php | 34 + tests/phpunit/languages/LanguageTlTest.php | 34 + tests/phpunit/languages/LanguageTrTest.php | 59 + tests/phpunit/languages/LanguageUkTest.php | 72 + tests/phpunit/languages/LanguageUzTest.php | 121 + tests/phpunit/languages/LanguageWaTest.php | 34 + .../utils/CLDRPluralRuleEvaluatorTest.php | 145 ++ tests/phpunit/maintenance/DumpTestCase.php | 379 ++++ tests/phpunit/maintenance/MaintenanceTest.php | 816 +++++++ tests/phpunit/maintenance/backupPrefetchTest.php | 275 +++ tests/phpunit/maintenance/backupTextPassTest.php | 581 +++++ tests/phpunit/maintenance/backup_LogTest.php | 228 ++ tests/phpunit/maintenance/backup_PageTest.php | 404 ++++ tests/phpunit/maintenance/fetchTextTest.php | 238 ++ tests/phpunit/maintenance/getSlaveServerTest.php | 67 + tests/phpunit/mocks/filebackend/MockFSFile.php | 69 + .../phpunit/mocks/filebackend/MockFileBackend.php | 122 ++ tests/phpunit/mocks/media/MockBitmapHandler.php | 92 + tests/phpunit/phpunit.php | 119 + tests/phpunit/run-tests.bat | 1 + tests/phpunit/skins/SideBarTest.php | 223 ++ tests/phpunit/structure/AutoLoaderTest.php | 56 + tests/phpunit/structure/ResourcesTest.php | 131 ++ tests/phpunit/structure/StructureTest.php | 63 + tests/phpunit/suite.xml | 51 + tests/phpunit/suites/ExtensionsParserTestSuite.php | 8 + tests/phpunit/suites/ExtensionsTestSuite.php | 33 + tests/phpunit/suites/UploadFromUrlTestSuite.php | 207 ++ 363 files changed, 49070 insertions(+) create mode 100644 tests/phpunit/Makefile create mode 100644 tests/phpunit/MediaWikiLangTestCase.php create mode 100644 tests/phpunit/MediaWikiPHPUnitCommand.php create mode 100644 tests/phpunit/MediaWikiPHPUnitTestListener.php create mode 100644 tests/phpunit/MediaWikiTestCase.php create mode 100644 tests/phpunit/README create mode 100644 tests/phpunit/TODO create mode 100644 tests/phpunit/bootstrap.php create mode 100644 tests/phpunit/data/db/mysql/functions.sql create mode 100644 tests/phpunit/data/db/postgres/functions.sql create mode 100644 tests/phpunit/data/db/sqlite/tables-1.13.sql create mode 100644 tests/phpunit/data/db/sqlite/tables-1.15.sql create mode 100644 tests/phpunit/data/db/sqlite/tables-1.16.sql create mode 100644 tests/phpunit/data/db/sqlite/tables-1.17.sql create mode 100644 tests/phpunit/data/db/sqlite/tables-1.18.sql create mode 100644 tests/phpunit/data/less/common/test.common.mixins.less create mode 100644 tests/phpunit/data/less/module/dependency.less create mode 100644 tests/phpunit/data/less/module/styles.css create mode 100644 tests/phpunit/data/less/module/styles.less create mode 100644 tests/phpunit/data/media/1bit-png.png create mode 100644 tests/phpunit/data/media/80x60-2layers.xcf create mode 100644 tests/phpunit/data/media/80x60-Greyscale.xcf create mode 100644 tests/phpunit/data/media/80x60-RGB.xcf create mode 100644 tests/phpunit/data/media/Animated_PNG_example_bouncing_beach_ball.png create mode 100644 tests/phpunit/data/media/Gtk-media-play-ltr.svg create mode 100644 tests/phpunit/data/media/Png-native-test.png create mode 100644 tests/phpunit/data/media/QA_icon.svg create mode 100644 tests/phpunit/data/media/README create mode 100644 tests/phpunit/data/media/Toll_Texas_1.svg create mode 100644 tests/phpunit/data/media/US_states_by_total_state_tax_revenue.svg create mode 100644 tests/phpunit/data/media/Wikimedia-logo.svg create mode 100644 tests/phpunit/data/media/Xmp-exif-multilingual_test.jpg create mode 100644 tests/phpunit/data/media/animated-xmp.gif create mode 100644 tests/phpunit/data/media/animated.gif create mode 100644 tests/phpunit/data/media/broken_exif_date.jpg create mode 100644 tests/phpunit/data/media/exif-gps.jpg create mode 100644 tests/phpunit/data/media/exif-user-comment.jpg create mode 100644 tests/phpunit/data/media/greyscale-na-png.png create mode 100644 tests/phpunit/data/media/greyscale-png.png create mode 100644 tests/phpunit/data/media/iptc-invalid-psir.jpg create mode 100644 tests/phpunit/data/media/iptc-timetest-invalid.jpg create mode 100644 tests/phpunit/data/media/iptc-timetest.jpg create mode 100644 tests/phpunit/data/media/jpeg-comment-binary.jpg create mode 100644 tests/phpunit/data/media/jpeg-comment-iso8859-1.jpg create mode 100644 tests/phpunit/data/media/jpeg-comment-multiple.jpg create mode 100644 tests/phpunit/data/media/jpeg-comment-utf.jpg create mode 100644 tests/phpunit/data/media/jpeg-iptc-bad-hash.jpg create mode 100644 tests/phpunit/data/media/jpeg-iptc-good-hash.jpg create mode 100644 tests/phpunit/data/media/jpeg-padding-even.jpg create mode 100644 tests/phpunit/data/media/jpeg-padding-odd.jpg create mode 100644 tests/phpunit/data/media/jpeg-xmp-alt.jpg create mode 100644 tests/phpunit/data/media/jpeg-xmp-psir.jpg create mode 100644 tests/phpunit/data/media/jpeg-xmp-psir.xmp create mode 100644 tests/phpunit/data/media/landscape-plain.jpg create mode 100644 tests/phpunit/data/media/nonanimated.gif create mode 100644 tests/phpunit/data/media/portrait-rotated.jpg create mode 100644 tests/phpunit/data/media/rgb-na-png.png create mode 100644 tests/phpunit/data/media/rgb-png.png create mode 100644 tests/phpunit/data/media/test.jpg create mode 100644 tests/phpunit/data/media/test.tiff create mode 100644 tests/phpunit/data/media/xmp.png create mode 100644 tests/phpunit/data/xmp/1.result.php create mode 100644 tests/phpunit/data/xmp/1.xmp create mode 100644 tests/phpunit/data/xmp/2.result.php create mode 100644 tests/phpunit/data/xmp/2.xmp create mode 100644 tests/phpunit/data/xmp/3-invalid.result.php create mode 100644 tests/phpunit/data/xmp/3-invalid.xmp create mode 100644 tests/phpunit/data/xmp/3.result.php create mode 100644 tests/phpunit/data/xmp/3.xmp create mode 100644 tests/phpunit/data/xmp/4.result.php create mode 100644 tests/phpunit/data/xmp/4.xmp create mode 100644 tests/phpunit/data/xmp/5.result.php create mode 100644 tests/phpunit/data/xmp/5.xmp create mode 100644 tests/phpunit/data/xmp/6.result.php create mode 100644 tests/phpunit/data/xmp/6.xmp create mode 100644 tests/phpunit/data/xmp/7.result.php create mode 100644 tests/phpunit/data/xmp/7.xmp create mode 100644 tests/phpunit/data/xmp/README create mode 100644 tests/phpunit/data/xmp/bag-for-seq.result.php create mode 100644 tests/phpunit/data/xmp/bag-for-seq.xmp create mode 100644 tests/phpunit/data/xmp/flash.result.php create mode 100644 tests/phpunit/data/xmp/flash.xmp create mode 100644 tests/phpunit/data/xmp/gps.result.php create mode 100644 tests/phpunit/data/xmp/gps.xmp create mode 100644 tests/phpunit/data/xmp/invalid-child-not-struct.result.php create mode 100644 tests/phpunit/data/xmp/invalid-child-not-struct.xmp create mode 100644 tests/phpunit/data/xmp/no-namespace.result.php create mode 100644 tests/phpunit/data/xmp/no-namespace.xmp create mode 100644 tests/phpunit/data/xmp/no-recognized-props.result.php create mode 100644 tests/phpunit/data/xmp/no-recognized-props.xmp create mode 100644 tests/phpunit/data/xmp/utf16BE.result.php create mode 100644 tests/phpunit/data/xmp/utf16BE.xmp create mode 100644 tests/phpunit/data/xmp/utf16LE.result.php create mode 100644 tests/phpunit/data/xmp/utf16LE.xmp create mode 100644 tests/phpunit/data/xmp/utf32BE.result.php create mode 100644 tests/phpunit/data/xmp/utf32BE.xmp create mode 100644 tests/phpunit/data/xmp/utf32LE.result.php create mode 100644 tests/phpunit/data/xmp/utf32LE.xmp create mode 100644 tests/phpunit/data/xmp/xmpExt.result.php create mode 100644 tests/phpunit/data/xmp/xmpExt.xmp create mode 100644 tests/phpunit/data/xmp/xmpExt2.xmp create mode 100644 tests/phpunit/data/zip/cd-gap.zip create mode 100644 tests/phpunit/data/zip/cd-truncated.zip create mode 100644 tests/phpunit/data/zip/class-trailing-null.zip create mode 100644 tests/phpunit/data/zip/class-trailing-slash.zip create mode 100644 tests/phpunit/data/zip/class.zip create mode 100644 tests/phpunit/data/zip/empty.zip create mode 100644 tests/phpunit/data/zip/looks-like-zip64.zip create mode 100644 tests/phpunit/data/zip/nosig.zip create mode 100644 tests/phpunit/data/zip/split.zip create mode 100644 tests/phpunit/data/zip/trail.zip create mode 100644 tests/phpunit/data/zip/wrong-cd-start-disk.zip create mode 100644 tests/phpunit/data/zip/wrong-central-entry-sig.zip create mode 100644 tests/phpunit/docs/ExportDemoTest.php create mode 100644 tests/phpunit/includes/ArticleTablesTest.php create mode 100644 tests/phpunit/includes/ArticleTest.php create mode 100644 tests/phpunit/includes/BlockTest.php create mode 100644 tests/phpunit/includes/CdbTest.php create mode 100644 tests/phpunit/includes/CollationTest.php create mode 100644 tests/phpunit/includes/DiffHistoryBlobTest.php create mode 100644 tests/phpunit/includes/EditPageTest.php create mode 100644 tests/phpunit/includes/ExternalStoreTest.php create mode 100644 tests/phpunit/includes/ExtraParserTest.php create mode 100644 tests/phpunit/includes/FallbackTest.php create mode 100644 tests/phpunit/includes/FauxRequestTest.php create mode 100644 tests/phpunit/includes/FauxResponseTest.php create mode 100644 tests/phpunit/includes/FormOptionsInitializationTest.php create mode 100644 tests/phpunit/includes/FormOptionsTest.php create mode 100644 tests/phpunit/includes/GlobalFunctions/GlobalTest.php create mode 100644 tests/phpunit/includes/GlobalFunctions/GlobalWithDBTest.php create mode 100644 tests/phpunit/includes/GlobalFunctions/README create mode 100644 tests/phpunit/includes/GlobalFunctions/wfAssembleUrlTest.php create mode 100644 tests/phpunit/includes/GlobalFunctions/wfBCP47Test.php create mode 100644 tests/phpunit/includes/GlobalFunctions/wfBaseConvertTest.php create mode 100644 tests/phpunit/includes/GlobalFunctions/wfBaseNameTest.php create mode 100644 tests/phpunit/includes/GlobalFunctions/wfExpandUrlTest.php create mode 100644 tests/phpunit/includes/GlobalFunctions/wfGetCallerTest.php create mode 100644 tests/phpunit/includes/GlobalFunctions/wfParseUrlTest.php create mode 100644 tests/phpunit/includes/GlobalFunctions/wfRemoveDotSegmentsTest.php create mode 100644 tests/phpunit/includes/GlobalFunctions/wfShorthandToIntegerTest.php create mode 100644 tests/phpunit/includes/GlobalFunctions/wfTimestampTest.php create mode 100644 tests/phpunit/includes/GlobalFunctions/wfUrlencodeTest.php create mode 100644 tests/phpunit/includes/HTMLCheckMatrixTest.php create mode 100644 tests/phpunit/includes/HashRingTest.php create mode 100644 tests/phpunit/includes/HooksTest.php create mode 100644 tests/phpunit/includes/HtmlFormatterTest.php create mode 100644 tests/phpunit/includes/HtmlTest.php create mode 100644 tests/phpunit/includes/HttpTest.php create mode 100644 tests/phpunit/includes/IPTest.php create mode 100644 tests/phpunit/includes/LanguageConverterTest.php create mode 100644 tests/phpunit/includes/LicensesTest.php create mode 100644 tests/phpunit/includes/LinkerTest.php create mode 100644 tests/phpunit/includes/LinksUpdateTest.php create mode 100644 tests/phpunit/includes/LocalFileTest.php create mode 100644 tests/phpunit/includes/LocalisationCacheTest.php create mode 100644 tests/phpunit/includes/MWExceptionHandlerTest.php create mode 100644 tests/phpunit/includes/MWFunctionTest.php create mode 100644 tests/phpunit/includes/MWNamespaceTest.php create mode 100644 tests/phpunit/includes/MessageTest.php create mode 100644 tests/phpunit/includes/OutputPageTest.php create mode 100644 tests/phpunit/includes/PathRouterTest.php create mode 100644 tests/phpunit/includes/PreferencesTest.php create mode 100644 tests/phpunit/includes/RecentChangeTest.php create mode 100644 tests/phpunit/includes/RequestContextTest.php create mode 100644 tests/phpunit/includes/ResourceLoaderTest.php create mode 100644 tests/phpunit/includes/RevisionStorageTest.php create mode 100644 tests/phpunit/includes/RevisionStorageTest_ContentHandlerUseDB.php create mode 100644 tests/phpunit/includes/RevisionTest.php create mode 100644 tests/phpunit/includes/SampleTest.php create mode 100644 tests/phpunit/includes/SanitizerTest.php create mode 100644 tests/phpunit/includes/SanitizerValidateEmailTest.php create mode 100644 tests/phpunit/includes/SiteConfigurationTest.php create mode 100644 tests/phpunit/includes/StringUtilsTest.php create mode 100644 tests/phpunit/includes/TemplateCategoriesTest.php create mode 100644 tests/phpunit/includes/TestUser.php create mode 100644 tests/phpunit/includes/TimeAdjustTest.php create mode 100644 tests/phpunit/includes/TimestampTest.php create mode 100644 tests/phpunit/includes/TitleMethodsTest.php create mode 100644 tests/phpunit/includes/TitlePermissionTest.php create mode 100644 tests/phpunit/includes/TitleTest.php create mode 100644 tests/phpunit/includes/UIDGeneratorTest.php create mode 100644 tests/phpunit/includes/UserMailerTest.php create mode 100644 tests/phpunit/includes/UserTest.php create mode 100644 tests/phpunit/includes/WebRequestTest.php create mode 100644 tests/phpunit/includes/WikiPageTest.php create mode 100644 tests/phpunit/includes/WikiPageTest_ContentHandlerUseDB.php create mode 100644 tests/phpunit/includes/XmlJsTest.php create mode 100644 tests/phpunit/includes/XmlSelectTest.php create mode 100644 tests/phpunit/includes/XmlTest.php create mode 100644 tests/phpunit/includes/XmlTypeCheckTest.php create mode 100644 tests/phpunit/includes/ZipDirectoryReaderTest.php create mode 100644 tests/phpunit/includes/api/ApiAccountCreationTest.php create mode 100644 tests/phpunit/includes/api/ApiBlockTest.php create mode 100644 tests/phpunit/includes/api/ApiEditPageTest.php create mode 100644 tests/phpunit/includes/api/ApiOptionsTest.php create mode 100644 tests/phpunit/includes/api/ApiParseTest.php create mode 100644 tests/phpunit/includes/api/ApiPurgeTest.php create mode 100644 tests/phpunit/includes/api/ApiTest.php create mode 100644 tests/phpunit/includes/api/ApiTestCase.php create mode 100644 tests/phpunit/includes/api/ApiTestCaseUpload.php create mode 100644 tests/phpunit/includes/api/ApiUploadTest.php create mode 100644 tests/phpunit/includes/api/ApiWatchTest.php create mode 100644 tests/phpunit/includes/api/PrefixUniquenessTest.php create mode 100644 tests/phpunit/includes/api/RandomImageGenerator.php create mode 100644 tests/phpunit/includes/api/format/ApiFormatPhpTest.php create mode 100644 tests/phpunit/includes/api/format/ApiFormatTestBase.php create mode 100644 tests/phpunit/includes/api/generateRandomImages.php create mode 100644 tests/phpunit/includes/api/query/ApiQueryBasicTest.php create mode 100644 tests/phpunit/includes/api/query/ApiQueryContinue2Test.php create mode 100644 tests/phpunit/includes/api/query/ApiQueryContinueTest.php create mode 100644 tests/phpunit/includes/api/query/ApiQueryContinueTestBase.php create mode 100644 tests/phpunit/includes/api/query/ApiQueryRevisionsTest.php create mode 100644 tests/phpunit/includes/api/query/ApiQueryTest.php create mode 100644 tests/phpunit/includes/api/query/ApiQueryTestBase.php create mode 100644 tests/phpunit/includes/api/words.txt create mode 100644 tests/phpunit/includes/cache/GenderCacheTest.php create mode 100644 tests/phpunit/includes/cache/MessageCacheTest.php create mode 100644 tests/phpunit/includes/cache/ProcessCacheLRUTest.php create mode 100644 tests/phpunit/includes/content/ContentHandlerTest.php create mode 100644 tests/phpunit/includes/content/CssContentTest.php create mode 100644 tests/phpunit/includes/content/JavaScriptContentTest.php create mode 100644 tests/phpunit/includes/content/TextContentTest.php create mode 100644 tests/phpunit/includes/content/WikitextContentHandlerTest.php create mode 100644 tests/phpunit/includes/content/WikitextContentTest.php create mode 100644 tests/phpunit/includes/db/DatabaseMysqlBaseTest.php create mode 100644 tests/phpunit/includes/db/DatabaseSQLTest.php create mode 100644 tests/phpunit/includes/db/DatabaseSqliteTest.php create mode 100644 tests/phpunit/includes/db/DatabaseTest.php create mode 100644 tests/phpunit/includes/db/DatabaseTestHelper.php create mode 100644 tests/phpunit/includes/db/ORMRowTest.php create mode 100644 tests/phpunit/includes/db/ORMTableTest.php create mode 100644 tests/phpunit/includes/db/TestORMRowTest.php create mode 100644 tests/phpunit/includes/debug/MWDebugTest.php create mode 100644 tests/phpunit/includes/filebackend/FileBackendTest.php create mode 100644 tests/phpunit/includes/filerepo/FileRepoTest.php create mode 100644 tests/phpunit/includes/filerepo/StoreBatchTest.php create mode 100644 tests/phpunit/includes/installer/InstallDocFormatterTest.php create mode 100644 tests/phpunit/includes/installer/OracleInstallerTest.php create mode 100644 tests/phpunit/includes/jobqueue/JobQueueTest.php create mode 100644 tests/phpunit/includes/json/FormatJsonTest.php create mode 100644 tests/phpunit/includes/libs/CSSJanusTest.php create mode 100644 tests/phpunit/includes/libs/CSSMinTest.php create mode 100644 tests/phpunit/includes/libs/GenericArrayObjectTest.php create mode 100644 tests/phpunit/includes/libs/IEUrlExtensionTest.php create mode 100644 tests/phpunit/includes/libs/JavaScriptMinifierTest.php create mode 100644 tests/phpunit/includes/logging/LogFormatterTest.php create mode 100644 tests/phpunit/includes/logging/LogTests.i18n.php create mode 100644 tests/phpunit/includes/media/BitmapMetadataHandlerTest.php create mode 100644 tests/phpunit/includes/media/BitmapScalingTest.php create mode 100644 tests/phpunit/includes/media/ExifBitmapTest.php create mode 100644 tests/phpunit/includes/media/ExifRotationTest.php create mode 100644 tests/phpunit/includes/media/ExifTest.php create mode 100644 tests/phpunit/includes/media/FakeDimensionFile.php create mode 100644 tests/phpunit/includes/media/FormatMetadataTest.php create mode 100644 tests/phpunit/includes/media/GIFMetadataExtractorTest.php create mode 100644 tests/phpunit/includes/media/GIFTest.php create mode 100644 tests/phpunit/includes/media/IPTCTest.php create mode 100644 tests/phpunit/includes/media/JpegMetadataExtractorTest.php create mode 100644 tests/phpunit/includes/media/JpegTest.php create mode 100644 tests/phpunit/includes/media/MediaHandlerTest.php create mode 100644 tests/phpunit/includes/media/PNGMetadataExtractorTest.php create mode 100644 tests/phpunit/includes/media/PNGTest.php create mode 100644 tests/phpunit/includes/media/SVGMetadataExtractorTest.php create mode 100644 tests/phpunit/includes/media/TiffTest.php create mode 100644 tests/phpunit/includes/media/XMPTest.php create mode 100644 tests/phpunit/includes/media/XMPValidateTest.php create mode 100644 tests/phpunit/includes/normal/CleanUpTest.php create mode 100644 tests/phpunit/includes/objectcache/BagOStuffTest.php create mode 100644 tests/phpunit/includes/parser/MagicVariableTest.php create mode 100644 tests/phpunit/includes/parser/MediaWikiParserTest.php create mode 100644 tests/phpunit/includes/parser/NewParserTest.php create mode 100644 tests/phpunit/includes/parser/ParserMethodsTest.php create mode 100644 tests/phpunit/includes/parser/ParserOutputTest.php create mode 100644 tests/phpunit/includes/parser/ParserPreloadTest.php create mode 100644 tests/phpunit/includes/parser/PreprocessorTest.php create mode 100644 tests/phpunit/includes/parser/TagHooksTest.php create mode 100644 tests/phpunit/includes/parser/TidyTest.php create mode 100644 tests/phpunit/includes/search/SearchEngineTest.php create mode 100644 tests/phpunit/includes/search/SearchUpdateTest.php create mode 100644 tests/phpunit/includes/site/MediaWikiSiteTest.php create mode 100644 tests/phpunit/includes/site/SiteListTest.php create mode 100644 tests/phpunit/includes/site/SiteSQLStoreTest.php create mode 100644 tests/phpunit/includes/site/SiteTest.php create mode 100644 tests/phpunit/includes/site/TestSites.php create mode 100644 tests/phpunit/includes/specials/QueryAllSpecialPagesTest.php create mode 100644 tests/phpunit/includes/specials/SpecialPreferencesTest.php create mode 100644 tests/phpunit/includes/specials/SpecialRecentchangesTest.php create mode 100644 tests/phpunit/includes/specials/SpecialSearchTest.php create mode 100644 tests/phpunit/includes/upload/UploadBaseTest.php create mode 100644 tests/phpunit/includes/upload/UploadFromUrlTest.php create mode 100644 tests/phpunit/includes/upload/UploadStashTest.php create mode 100644 tests/phpunit/install-phpunit.sh create mode 100644 tests/phpunit/languages/LanguageAmTest.php create mode 100644 tests/phpunit/languages/LanguageArTest.php create mode 100644 tests/phpunit/languages/LanguageBeTest.php create mode 100644 tests/phpunit/languages/LanguageBe_taraskTest.php create mode 100644 tests/phpunit/languages/LanguageBhoTest.php create mode 100644 tests/phpunit/languages/LanguageBsTest.php create mode 100644 tests/phpunit/languages/LanguageClassesTestCase.php create mode 100644 tests/phpunit/languages/LanguageCsTest.php create mode 100644 tests/phpunit/languages/LanguageCuTest.php create mode 100644 tests/phpunit/languages/LanguageCyTest.php create mode 100644 tests/phpunit/languages/LanguageDsbTest.php create mode 100644 tests/phpunit/languages/LanguageFrTest.php create mode 100644 tests/phpunit/languages/LanguageGaTest.php create mode 100644 tests/phpunit/languages/LanguageGdTest.php create mode 100644 tests/phpunit/languages/LanguageGvTest.php create mode 100644 tests/phpunit/languages/LanguageHeTest.php create mode 100644 tests/phpunit/languages/LanguageHiTest.php create mode 100644 tests/phpunit/languages/LanguageHrTest.php create mode 100644 tests/phpunit/languages/LanguageHsbTest.php create mode 100644 tests/phpunit/languages/LanguageHuTest.php create mode 100644 tests/phpunit/languages/LanguageHyTest.php create mode 100644 tests/phpunit/languages/LanguageKshTest.php create mode 100644 tests/phpunit/languages/LanguageLnTest.php create mode 100644 tests/phpunit/languages/LanguageLtTest.php create mode 100644 tests/phpunit/languages/LanguageLvTest.php create mode 100644 tests/phpunit/languages/LanguageMgTest.php create mode 100644 tests/phpunit/languages/LanguageMkTest.php create mode 100644 tests/phpunit/languages/LanguageMlTest.php create mode 100644 tests/phpunit/languages/LanguageMoTest.php create mode 100644 tests/phpunit/languages/LanguageMtTest.php create mode 100644 tests/phpunit/languages/LanguageNlTest.php create mode 100644 tests/phpunit/languages/LanguageNsoTest.php create mode 100644 tests/phpunit/languages/LanguagePlTest.php create mode 100644 tests/phpunit/languages/LanguageRoTest.php create mode 100644 tests/phpunit/languages/LanguageRuTest.php create mode 100644 tests/phpunit/languages/LanguageSeTest.php create mode 100644 tests/phpunit/languages/LanguageSgsTest.php create mode 100644 tests/phpunit/languages/LanguageShTest.php create mode 100644 tests/phpunit/languages/LanguageSkTest.php create mode 100644 tests/phpunit/languages/LanguageSlTest.php create mode 100644 tests/phpunit/languages/LanguageSmaTest.php create mode 100644 tests/phpunit/languages/LanguageSrTest.php create mode 100644 tests/phpunit/languages/LanguageTest.php create mode 100644 tests/phpunit/languages/LanguageTiTest.php create mode 100644 tests/phpunit/languages/LanguageTlTest.php create mode 100644 tests/phpunit/languages/LanguageTrTest.php create mode 100644 tests/phpunit/languages/LanguageUkTest.php create mode 100644 tests/phpunit/languages/LanguageUzTest.php create mode 100644 tests/phpunit/languages/LanguageWaTest.php create mode 100644 tests/phpunit/languages/utils/CLDRPluralRuleEvaluatorTest.php create mode 100644 tests/phpunit/maintenance/DumpTestCase.php create mode 100644 tests/phpunit/maintenance/MaintenanceTest.php create mode 100644 tests/phpunit/maintenance/backupPrefetchTest.php create mode 100644 tests/phpunit/maintenance/backupTextPassTest.php create mode 100644 tests/phpunit/maintenance/backup_LogTest.php create mode 100644 tests/phpunit/maintenance/backup_PageTest.php create mode 100644 tests/phpunit/maintenance/fetchTextTest.php create mode 100644 tests/phpunit/maintenance/getSlaveServerTest.php create mode 100644 tests/phpunit/mocks/filebackend/MockFSFile.php create mode 100644 tests/phpunit/mocks/filebackend/MockFileBackend.php create mode 100644 tests/phpunit/mocks/media/MockBitmapHandler.php create mode 100644 tests/phpunit/phpunit.php create mode 100644 tests/phpunit/run-tests.bat create mode 100644 tests/phpunit/skins/SideBarTest.php create mode 100644 tests/phpunit/structure/AutoLoaderTest.php create mode 100644 tests/phpunit/structure/ResourcesTest.php create mode 100644 tests/phpunit/structure/StructureTest.php create mode 100644 tests/phpunit/suite.xml create mode 100644 tests/phpunit/suites/ExtensionsParserTestSuite.php create mode 100644 tests/phpunit/suites/ExtensionsTestSuite.php create mode 100644 tests/phpunit/suites/UploadFromUrlTestSuite.php (limited to 'tests/phpunit') diff --git a/tests/phpunit/Makefile b/tests/phpunit/Makefile new file mode 100644 index 00000000..c3e2a303 --- /dev/null +++ b/tests/phpunit/Makefile @@ -0,0 +1,91 @@ +.PHONY: help test phpunit install coverage warning destructive parser noparser safe databaseless list-groups +.DEFAULT: warning + +SHELL = /bin/sh +CONFIG_FILE = ${PWD}/suite.xml +PHP = php +PU = ${PHP} phpunit.php --configuration ${CONFIG_FILE} ${FLAGS} + +all test: warning + +warning: + @echo "Run 'make help' to get usage" + @echo "" + @echo "WARNING -- some tests are DESTRUCTIVE and will alter your wiki." + @echo "DO NOT RUN THESE TESTS on a production wiki." + @echo "" + @echo "Until the default tests are made non-destructive, you can run" + @echo "the destructive tests like so:" + @echo "" + @echo " make destructive" + @echo "" + @echo "Some tests are expected to be safe, you can run them with" + @echo "" + @echo " make safe" + @echo "" + @echo "You are recommended to run the tests with read-only credentials." + @echo "" + @echo "If you don't have a database running, you can still run" + @echo "" + @echo " make databaseless" + @echo "" + +destructive: phpunit + +phpunit: + ${PU} + +install: + ./install-phpunit.sh + +tap: + ${PU} --tap + +coverage: + ${PU} --coverage-html ../../docs/code-coverage + +parser: + ${PU} --group Parser +parserfuzz: + @echo "******************************************************************" + @echo "* This WILL kill your computer by eating all memory AND all swap *" + @echo "* *" + @echo "* If you are on a production machine. ABORT NOW!! *" + @echo "* Press control+C to stop *" + @echo "* *" + @echo "******************************************************************" + ${PU} --group Parser,ParserFuzz +noparser: + ${PU} --exclude-group Parser,Broken,ParserFuzz,Stub + +safe: + ${PU} --exclude-group Broken,ParserFuzz,Destructive,Stub + +databaseless: + ${PU} --exclude-group Broken,ParserFuzz,Destructive,Database,Stub + +database: + ${PU} --exclude-group Broken,ParserFuzz,Destructive,Stub --group Database + +list-groups: + ${PU} --list-groups + +help: + # Usage: + # make [OPTION=value] + # + # Targets: + # phpunit (default) Run all the tests with phpunit + # install Install PHPUnit from phpunit.de + # tap Run the tests individually through Test::Harness's prove(1) + # help You're looking at it! + # coverage Run the tests and generates an HTML code coverage report + # You will need the Xdebug PHP extension for the later. + # [no]parser Skip or only run Parser tests + # + # list-groups List availabe Tests groups. + # + # Options: + # CONFIG_FILE Path to a PHPUnit configuration file (default: suite.xml) + # FLAGS Additional flags to pass to PHPUnit + # PHP Path to php diff --git a/tests/phpunit/MediaWikiLangTestCase.php b/tests/phpunit/MediaWikiLangTestCase.php new file mode 100644 index 00000000..1131385f --- /dev/null +++ b/tests/phpunit/MediaWikiLangTestCase.php @@ -0,0 +1,33 @@ +getCode() ) { + throw new MWException( "Error in MediaWikiLangTestCase::setUp(): " . + "\$wgLanguageCode ('$wgLanguageCode') is different from " . + "\$wgContLang->getCode() (" . $wgContLang->getCode() . ")" ); + } + + // HACK: Call getLanguage() so the real $wgContLang is cached as the user language + // rather than our fake one. This is to avoid breaking other, unrelated tests. + RequestContext::getMain()->getLanguage(); + + $langCode = 'en'; # For mainpage to be 'Main Page' + $langObj = Language::factory( $langCode ); + + $this->setMwGlobals( array( + 'wgLanguageCode' => $langCode, + 'wgLang' => $langObj, + 'wgContLang' => $langObj, + ) ); + + MessageCache::singleton()->disable(); + } +} diff --git a/tests/phpunit/MediaWikiPHPUnitCommand.php b/tests/phpunit/MediaWikiPHPUnitCommand.php new file mode 100644 index 00000000..042956a9 --- /dev/null +++ b/tests/phpunit/MediaWikiPHPUnitCommand.php @@ -0,0 +1,119 @@ + false, + 'file=' => false, + 'use-filebackend=' => false, + 'use-bagostuff=' => false, + 'use-jobqueue=' => false, + 'keep-uploads' => false, + 'use-normal-tables' => false, + 'reuse-db' => false, + 'wiki=' => false, + 'debug-tests' => false, + ); + + public function __construct() { + foreach ( self::$additionalOptions as $option => $default ) { + $this->longOptions[$option] = $option . 'Handler'; + } + } + + protected function handleArguments( array $argv ) { + parent::handleArguments( $argv ); + + if ( !isset( $this->arguments['listeners'] ) ) { + $this->arguments['listeners'] = array(); + } + + foreach ( $this->options[0] as $option ) { + switch ( $option[0] ) { + case '--debug-tests': + $this->arguments['listeners'][] = new MediaWikiPHPUnitTestListener( 'PHPUnitCommand' ); + break; + } + } + } + + public static function main( $exit = true ) { + $command = new self; + + if ( wfIsWindows() ) { + # Windows does not come anymore with ANSI.SYS loaded by default + # PHPUnit uses the suite.xml parameters to enable/disable colors + # which can be then forced to be enabled with --colors. + # The below code inject a parameter just like if the user called + # phpunit with a --no-color option (which does not exist). It + # overrides the suite.xml setting. + # Probably fix bug 29226 + $command->arguments['colors'] = false; + } + + # Makes MediaWiki PHPUnit directory includable so the PHPUnit will + # be able to resolve relative files inclusion such as suites/* + # PHPUnit uses stream_resolve_include_path() internally + # See bug 32022 + set_include_path( + __DIR__ + . PATH_SEPARATOR + . get_include_path() + ); + + $command->run( $_SERVER['argv'], $exit ); + } + + public function __call( $func, $args ) { + + if ( substr( $func, -7 ) == 'Handler' ) { + if ( is_null( $args[0] ) ) { + $args[0] = true; + } //Booleans + self::$additionalOptions[substr( $func, 0, -7 )] = $args[0]; + } + } + + public function run( array $argv, $exit = true ) { + wfProfileIn( __METHOD__ ); + + $ret = parent::run( $argv, false ); + + wfProfileOut( __METHOD__ ); + + // Return to real wiki db, so profiling data is preserved + MediaWikiTestCase::teardownTestDB(); + + // Log profiling data, e.g. in the database or UDP + wfLogProfilingData(); + + if ( $exit ) { + exit( $ret ); + } else { + return $ret; + } + } + + public function showHelp() { + parent::showHelp(); + + print <<logChannel = $logChannel; + } + + protected function getTestName( PHPUnit_Framework_Test $test ) { + $name = get_class( $test ); + + if ( $test instanceof PHPUnit_Framework_TestCase ) { + $name .= '::' . $test->getName( true ); + } + + return $name; + } + + protected function getErrorName( Exception $exception ) { + $name = get_class( $exception ); + $name = "[$name] " . $exception->getMessage(); + + return $name; + } + + /** + * An error occurred. + * + * @param PHPUnit_Framework_Test $test + * @param Exception $e + * @param float $time + */ + public function addError( PHPUnit_Framework_Test $test, Exception $e, $time ) { + wfDebugLog( $this->logChannel, 'ERROR in ' . $this->getTestName( $test ) . ': ' . $this->getErrorName( $e ) ); + } + + /** + * A failure occurred. + * + * @param PHPUnit_Framework_Test $test + * @param PHPUnit_Framework_AssertionFailedError $e + * @param float $time + */ + public function addFailure( PHPUnit_Framework_Test $test, PHPUnit_Framework_AssertionFailedError $e, $time ) { + wfDebugLog( $this->logChannel, 'FAILURE in ' . $this->getTestName( $test ) . ': ' . $this->getErrorName( $e ) ); + } + + /** + * Incomplete test. + * + * @param PHPUnit_Framework_Test $test + * @param Exception $e + * @param float $time + */ + public function addIncompleteTest( PHPUnit_Framework_Test $test, Exception $e, $time ) { + wfDebugLog( $this->logChannel, 'Incomplete test ' . $this->getTestName( $test ) . ': ' . $this->getErrorName( $e ) ); + } + + /** + * Skipped test. + * + * @param PHPUnit_Framework_Test $test + * @param Exception $e + * @param float $time + * + * @since Method available since Release 3.0.0 + */ + public function addSkippedTest( PHPUnit_Framework_Test $test, Exception $e, $time ) { + wfDebugLog( $this->logChannel, 'Skipped test ' . $this->getTestName( $test ) . ': ' . $this->getErrorName( $e ) ); + } + + /** + * A test suite started. + * + * @param PHPUnit_Framework_TestSuite $suite + * @since Method available since Release 2.2.0 + */ + public function startTestSuite( PHPUnit_Framework_TestSuite $suite ) { + wfDebugLog( $this->logChannel, 'START suite ' . $suite->getName() ); + } + + /** + * A test suite ended. + * + * @param PHPUnit_Framework_TestSuite $suite + * @since Method available since Release 2.2.0 + */ + public function endTestSuite( PHPUnit_Framework_TestSuite $suite ) { + wfDebugLog( $this->logChannel, 'END suite ' . $suite->getName() ); + } + + /** + * A test started. + * + * @param PHPUnit_Framework_Test $test + */ + public function startTest( PHPUnit_Framework_Test $test ) { + wfDebugLog( $this->logChannel, 'Start test ' . $this->getTestName( $test ) ); + } + + /** + * A test ended. + * + * @param PHPUnit_Framework_Test $test + * @param float $time + */ + public function endTest( PHPUnit_Framework_Test $test, $time ) { + wfDebugLog( $this->logChannel, 'End test ' . $this->getTestName( $test ) ); + } +} diff --git a/tests/phpunit/MediaWikiTestCase.php b/tests/phpunit/MediaWikiTestCase.php new file mode 100644 index 00000000..6ce78b56 --- /dev/null +++ b/tests/phpunit/MediaWikiTestCase.php @@ -0,0 +1,948 @@ +backupGlobals = false; + $this->backupStaticAttributes = false; + } + + function run( PHPUnit_Framework_TestResult $result = null ) { + /* Some functions require some kind of caching, and will end up using the db, + * which we can't allow, as that would open a new connection for mysql. + * Replace with a HashBag. They would not be going to persist anyway. + */ + ObjectCache::$instances[CACHE_DB] = new HashBagOStuff; + + $needsResetDB = false; + $logName = get_class( $this ) . '::' . $this->getName( false ); + + if ( $this->needsDB() ) { + // set up a DB connection for this test to use + + self::$useTemporaryTables = !$this->getCliArg( 'use-normal-tables' ); + self::$reuseDB = $this->getCliArg( 'reuse-db' ); + + $this->db = wfGetDB( DB_MASTER ); + + $this->checkDbIsSupported(); + + if ( !self::$dbSetup ) { + wfProfileIn( $logName . ' (clone-db)' ); + + // switch to a temporary clone of the database + self::setupTestDB( $this->db, $this->dbPrefix() ); + + if ( ( $this->db->getType() == 'oracle' || !self::$useTemporaryTables ) && self::$reuseDB ) { + $this->resetDB(); + } + + wfProfileOut( $logName . ' (clone-db)' ); + } + + wfProfileIn( $logName . ' (prepare-db)' ); + $this->addCoreDBData(); + $this->addDBData(); + wfProfileOut( $logName . ' (prepare-db)' ); + + $needsResetDB = true; + } + + wfProfileIn( $logName ); + parent::run( $result ); + wfProfileOut( $logName ); + + if ( $needsResetDB ) { + wfProfileIn( $logName . ' (reset-db)' ); + $this->resetDB(); + wfProfileOut( $logName . ' (reset-db)' ); + } + } + + function usesTemporaryTables() { + return self::$useTemporaryTables; + } + + /** + * obtains a new temporary file name + * + * The obtained filename is enlisted to be removed upon tearDown + * + * @return string: absolute name of the temporary file + */ + protected function getNewTempFile() { + $fname = tempnam( wfTempDir(), 'MW_PHPUnit_' . get_class( $this ) . '_' ); + $this->tmpfiles[] = $fname; + + return $fname; + } + + /** + * obtains a new temporary directory + * + * The obtained directory is enlisted to be removed (recursively with all its contained + * files) upon tearDown. + * + * @return string: absolute name of the temporary directory + */ + protected function getNewTempDirectory() { + // Starting of with a temporary /file/. + $fname = $this->getNewTempFile(); + + // Converting the temporary /file/ to a /directory/ + // + // The following is not atomic, but at least we now have a single place, + // where temporary directory creation is bundled and can be improved + unlink( $fname ); + $this->assertTrue( wfMkdirParents( $fname ) ); + + return $fname; + } + + /** + * setUp and tearDown should (where significant) + * happen in reverse order. + */ + protected function setUp() { + wfProfileIn( __METHOD__ ); + parent::setUp(); + $this->called['setUp'] = 1; + + /* + // @todo global variables to restore for *every* test + array( + 'wgLang', + 'wgContLang', + 'wgLanguageCode', + 'wgUser', + 'wgTitle', + ); + */ + + // Cleaning up temporary files + foreach ( $this->tmpfiles as $fname ) { + if ( is_file( $fname ) || ( is_link( $fname ) ) ) { + unlink( $fname ); + } elseif ( is_dir( $fname ) ) { + wfRecursiveRemoveDir( $fname ); + } + } + + if ( $this->needsDB() && $this->db ) { + // Clean up open transactions + while ( $this->db->trxLevel() > 0 ) { + $this->db->rollback(); + } + + // don't ignore DB errors + $this->db->ignoreErrors( false ); + } + + wfProfileOut( __METHOD__ ); + } + + protected function tearDown() { + wfProfileIn( __METHOD__ ); + + // Cleaning up temporary files + foreach ( $this->tmpfiles as $fname ) { + if ( is_file( $fname ) || ( is_link( $fname ) ) ) { + unlink( $fname ); + } elseif ( is_dir( $fname ) ) { + wfRecursiveRemoveDir( $fname ); + } + } + + if ( $this->needsDB() && $this->db ) { + // Clean up open transactions + while ( $this->db->trxLevel() > 0 ) { + $this->db->rollback(); + } + + // don't ignore DB errors + $this->db->ignoreErrors( false ); + } + + // Restore mw globals + foreach ( $this->mwGlobals as $key => $value ) { + $GLOBALS[$key] = $value; + } + $this->mwGlobals = array(); + + parent::tearDown(); + wfProfileOut( __METHOD__ ); + } + + /** + * Make sure MediaWikiTestCase extending classes have called their + * parent setUp method + */ + final public function testMediaWikiTestCaseParentSetupCalled() { + $this->assertArrayHasKey( 'setUp', $this->called, + get_called_class() . "::setUp() must call parent::setUp()" + ); + } + + /** + * Individual test functions may override globals (either directly or through this + * setMwGlobals() function), however one must call this method at least once for + * each key within the setUp(). + * That way the key is added to the array of globals that will be reset afterwards + * in the tearDown(). And, equally important, that way all other tests are executed + * with the same settings (instead of using the unreliable local settings for most + * tests and fix it only for some tests). + * + * @example + * + * protected function setUp() { + * $this->setMwGlobals( 'wgRestrictStuff', true ); + * } + * + * function testFoo() {} + * + * function testBar() {} + * $this->assertTrue( self::getX()->doStuff() ); + * + * $this->setMwGlobals( 'wgRestrictStuff', false ); + * $this->assertTrue( self::getX()->doStuff() ); + * } + * + * function testQuux() {} + * + * + * @param array|string $pairs Key to the global variable, or an array + * of key/value pairs. + * @param mixed $value Value to set the global to (ignored + * if an array is given as first argument). + */ + protected function setMwGlobals( $pairs, $value = null ) { + + // Normalize (string, value) to an array + if ( is_string( $pairs ) ) { + $pairs = array( $pairs => $value ); + } + + foreach ( $pairs as $key => $value ) { + // NOTE: make sure we only save the global once or a second call to + // setMwGlobals() on the same global would override the original + // value. + if ( !array_key_exists( $key, $this->mwGlobals ) ) { + $this->mwGlobals[$key] = $GLOBALS[$key]; + } + + // Override the global + $GLOBALS[$key] = $value; + } + } + + /** + * Merges the given values into a MW global array variable. + * Useful for setting some entries in a configuration array, instead of + * setting the entire array. + * + * @param String $name The name of the global, as in wgFooBar + * @param Array $values The array containing the entries to set in that global + * + * @throws MWException if the designated global is not an array. + */ + protected function mergeMwGlobalArrayValue( $name, $values ) { + if ( !isset( $GLOBALS[$name] ) ) { + $merged = $values; + } else { + if ( !is_array( $GLOBALS[$name] ) ) { + throw new MWException( "MW global $name is not an array." ); + } + + // NOTE: do not use array_merge, it screws up for numeric keys. + $merged = $GLOBALS[$name]; + foreach ( $values as $k => $v ) { + $merged[$k] = $v; + } + } + + $this->setMwGlobals( $name, $merged ); + } + + function dbPrefix() { + return $this->db->getType() == 'oracle' ? self::ORA_DB_PREFIX : self::DB_PREFIX; + } + + function needsDB() { + # if the test says it uses database tables, it needs the database + if ( $this->tablesUsed ) { + return true; + } + + # if the test says it belongs to the Database group, it needs the database + $rc = new ReflectionClass( $this ); + if ( preg_match( '/@group +Database/im', $rc->getDocComment() ) ) { + return true; + } + + return false; + } + + /** + * Stub. If a test needs to add additional data to the database, it should + * implement this method and do so + */ + function addDBData() { + } + + private function addCoreDBData() { + # disabled for performance + #$this->tablesUsed[] = 'page'; + #$this->tablesUsed[] = 'revision'; + + if ( $this->db->getType() == 'oracle' ) { + + # Insert 0 user to prevent FK violations + # Anonymous user + $this->db->insert( 'user', array( + 'user_id' => 0, + 'user_name' => 'Anonymous' ), __METHOD__, array( 'IGNORE' ) ); + + # Insert 0 page to prevent FK violations + # Blank page + $this->db->insert( 'page', array( + 'page_id' => 0, + 'page_namespace' => 0, + 'page_title' => ' ', + 'page_restrictions' => null, + 'page_counter' => 0, + 'page_is_redirect' => 0, + 'page_is_new' => 0, + 'page_random' => 0, + 'page_touched' => $this->db->timestamp(), + 'page_latest' => 0, + 'page_len' => 0 ), __METHOD__, array( 'IGNORE' ) ); + } + + User::resetIdByNameCache(); + + //Make sysop user + $user = User::newFromName( 'UTSysop' ); + + if ( $user->idForName() == 0 ) { + $user->addToDatabase(); + $user->setPassword( 'UTSysopPassword' ); + + $user->addGroup( 'sysop' ); + $user->addGroup( 'bureaucrat' ); + $user->saveSettings(); + } + + //Make 1 page with 1 revision + $page = WikiPage::factory( Title::newFromText( 'UTPage' ) ); + if ( !$page->getId() == 0 ) { + $page->doEditContent( + new WikitextContent( 'UTContent' ), + 'UTPageSummary', + EDIT_NEW, + false, + User::newFromName( 'UTSysop' ) ); + } + } + + /** + * Restores MediaWiki to using the table set (table prefix) it was using before + * setupTestDB() was called. Useful if we need to perform database operations + * after the test run has finished (such as saving logs or profiling info). + */ + public static function teardownTestDB() { + if ( !self::$dbSetup ) { + return; + } + + CloneDatabase::changePrefix( self::$oldTablePrefix ); + + self::$oldTablePrefix = false; + self::$dbSetup = false; + } + + /** + * Creates an empty skeleton of the wiki database by cloning its structure + * to equivalent tables using the given $prefix. Then sets MediaWiki to + * use the new set of tables (aka schema) instead of the original set. + * + * This is used to generate a dummy table set, typically consisting of temporary + * tables, that will be used by tests instead of the original wiki database tables. + * + * @note: the original table prefix is stored in self::$oldTablePrefix. This is used + * by teardownTestDB() to return the wiki to using the original table set. + * + * @note: this method only works when first called. Subsequent calls have no effect, + * even if using different parameters. + * + * @param DatabaseBase $db The database connection + * @param String $prefix The prefix to use for the new table set (aka schema). + * + * @throws MWException if the database table prefix is already $prefix + */ + public static function setupTestDB( DatabaseBase $db, $prefix ) { + global $wgDBprefix; + if ( $wgDBprefix === $prefix ) { + throw new MWException( 'Cannot run unit tests, the database prefix is already "' . $prefix . '"' ); + } + + if ( self::$dbSetup ) { + return; + } + + $tablesCloned = self::listTables( $db ); + $dbClone = new CloneDatabase( $db, $tablesCloned, $prefix ); + $dbClone->useTemporaryTables( self::$useTemporaryTables ); + + self::$dbSetup = true; + self::$oldTablePrefix = $wgDBprefix; + + if ( ( $db->getType() == 'oracle' || !self::$useTemporaryTables ) && self::$reuseDB ) { + CloneDatabase::changePrefix( $prefix ); + + return; + } else { + $dbClone->cloneTableStructure(); + } + + if ( $db->getType() == 'oracle' ) { + $db->query( 'BEGIN FILL_WIKI_INFO; END;' ); + } + } + + /** + * Empty all tables so they can be repopulated for tests + */ + private function resetDB() { + if ( $this->db ) { + if ( $this->db->getType() == 'oracle' ) { + if ( self::$useTemporaryTables ) { + wfGetLB()->closeAll(); + $this->db = wfGetDB( DB_MASTER ); + } else { + foreach ( $this->tablesUsed as $tbl ) { + if ( $tbl == 'interwiki' ) { + continue; + } + $this->db->query( 'TRUNCATE TABLE ' . $this->db->tableName( $tbl ), __METHOD__ ); + } + } + } else { + foreach ( $this->tablesUsed as $tbl ) { + if ( $tbl == 'interwiki' || $tbl == 'user' ) { + continue; + } + $this->db->delete( $tbl, '*', __METHOD__ ); + } + } + } + } + + function __call( $func, $args ) { + static $compatibility = array( + 'assertInternalType' => 'assertType', + 'assertNotInternalType' => 'assertNotType', + 'assertInstanceOf' => 'assertType', + 'assertEmpty' => 'assertEmpty2', + ); + + if ( method_exists( $this->suite, $func ) ) { + return call_user_func_array( array( $this->suite, $func ), $args ); + } elseif ( isset( $compatibility[$func] ) ) { + return call_user_func_array( array( $this, $compatibility[$func] ), $args ); + } else { + throw new MWException( "Called non-existant $func method on " + . get_class( $this ) ); + } + } + + private function assertEmpty2( $value, $msg ) { + return $this->assertTrue( $value == '', $msg ); + } + + private static function unprefixTable( $tableName ) { + global $wgDBprefix; + + return substr( $tableName, strlen( $wgDBprefix ) ); + } + + private static function isNotUnittest( $table ) { + return strpos( $table, 'unittest_' ) !== 0; + } + + public static function listTables( $db ) { + global $wgDBprefix; + + $tables = $db->listTables( $wgDBprefix, __METHOD__ ); + + if ( $db->getType() === 'mysql' ) { + # bug 43571: cannot clone VIEWs under MySQL + $views = $db->listViews( $wgDBprefix, __METHOD__ ); + $tables = array_diff( $tables, $views ); + } + $tables = array_map( array( __CLASS__, 'unprefixTable' ), $tables ); + + // Don't duplicate test tables from the previous fataled run + $tables = array_filter( $tables, array( __CLASS__, 'isNotUnittest' ) ); + + if ( $db->getType() == 'sqlite' ) { + $tables = array_flip( $tables ); + // these are subtables of searchindex and don't need to be duped/dropped separately + unset( $tables['searchindex_content'] ); + unset( $tables['searchindex_segdir'] ); + unset( $tables['searchindex_segments'] ); + $tables = array_flip( $tables ); + } + + return $tables; + } + + protected function checkDbIsSupported() { + if ( !in_array( $this->db->getType(), $this->supportedDBs ) ) { + throw new MWException( $this->db->getType() . " is not currently supported for unit testing." ); + } + } + + public function getCliArg( $offset ) { + + if ( isset( MediaWikiPHPUnitCommand::$additionalOptions[$offset] ) ) { + return MediaWikiPHPUnitCommand::$additionalOptions[$offset]; + } + } + + public function setCliArg( $offset, $value ) { + + MediaWikiPHPUnitCommand::$additionalOptions[$offset] = $value; + } + + /** + * Don't throw a warning if $function is deprecated and called later + * + * @param $function String + * @return null + */ + function hideDeprecated( $function ) { + wfSuppressWarnings(); + wfDeprecated( $function ); + wfRestoreWarnings(); + } + + /** + * Asserts that the given database query yields the rows given by $expectedRows. + * The expected rows should be given as indexed (not associative) arrays, with + * the values given in the order of the columns in the $fields parameter. + * Note that the rows are sorted by the columns given in $fields. + * + * @since 1.20 + * + * @param $table String|Array the table(s) to query + * @param $fields String|Array the columns to include in the result (and to sort by) + * @param $condition String|Array "where" condition(s) + * @param $expectedRows Array - an array of arrays giving the expected rows. + * + * @throws MWException if this test cases's needsDB() method doesn't return true. + * Test cases can use "@group Database" to enable database test support, + * or list the tables under testing in $this->tablesUsed, or override the + * needsDB() method. + */ + protected function assertSelect( $table, $fields, $condition, array $expectedRows ) { + if ( !$this->needsDB() ) { + throw new MWException( 'When testing database state, the test cases\'s needDB()' . + ' method should return true. Use @group Database or $this->tablesUsed.' ); + } + + $db = wfGetDB( DB_SLAVE ); + + $res = $db->select( $table, $fields, $condition, wfGetCaller(), array( 'ORDER BY' => $fields ) ); + $this->assertNotEmpty( $res, "query failed: " . $db->lastError() ); + + $i = 0; + + foreach ( $expectedRows as $expected ) { + $r = $res->fetchRow(); + self::stripStringKeys( $r ); + + $i += 1; + $this->assertNotEmpty( $r, "row #$i missing" ); + + $this->assertEquals( $expected, $r, "row #$i mismatches" ); + } + + $r = $res->fetchRow(); + self::stripStringKeys( $r ); + + $this->assertFalse( $r, "found extra row (after #$i)" ); + } + + /** + * Utility method taking an array of elements and wrapping + * each element in it's own array. Useful for data providers + * that only return a single argument. + * + * @since 1.20 + * + * @param array $elements + * + * @return array + */ + protected function arrayWrap( array $elements ) { + return array_map( + function ( $element ) { + return array( $element ); + }, + $elements + ); + } + + /** + * Assert that two arrays are equal. By default this means that both arrays need to hold + * the same set of values. Using additional arguments, order and associated key can also + * be set as relevant. + * + * @since 1.20 + * + * @param array $expected + * @param array $actual + * @param boolean $ordered If the order of the values should match + * @param boolean $named If the keys should match + */ + protected function assertArrayEquals( array $expected, array $actual, $ordered = false, $named = false ) { + if ( !$ordered ) { + $this->objectAssociativeSort( $expected ); + $this->objectAssociativeSort( $actual ); + } + + if ( !$named ) { + $expected = array_values( $expected ); + $actual = array_values( $actual ); + } + + call_user_func_array( + array( $this, 'assertEquals' ), + array_merge( array( $expected, $actual ), array_slice( func_get_args(), 4 ) ) + ); + } + + /** + * Put each HTML element on its own line and then equals() the results + * + * Use for nicely formatting of PHPUnit diff output when comparing very + * simple HTML + * + * @since 1.20 + * + * @param String $expected HTML on oneline + * @param String $actual HTML on oneline + * @param String $msg Optional message + */ + protected function assertHTMLEquals( $expected, $actual, $msg = '' ) { + $expected = str_replace( '>', ">\n", $expected ); + $actual = str_replace( '>', ">\n", $actual ); + + $this->assertEquals( $expected, $actual, $msg ); + } + + /** + * Does an associative sort that works for objects. + * + * @since 1.20 + * + * @param array $array + */ + protected function objectAssociativeSort( array &$array ) { + uasort( + $array, + function ( $a, $b ) { + return serialize( $a ) > serialize( $b ) ? 1 : -1; + } + ); + } + + /** + * Utility function for eliminating all string keys from an array. + * Useful to turn a database result row as returned by fetchRow() into + * a pure indexed array. + * + * @since 1.20 + * + * @param $r mixed the array to remove string keys from. + */ + protected static function stripStringKeys( &$r ) { + if ( !is_array( $r ) ) { + return; + } + + foreach ( $r as $k => $v ) { + if ( is_string( $k ) ) { + unset( $r[$k] ); + } + } + } + + /** + * Asserts that the provided variable is of the specified + * internal type or equals the $value argument. This is useful + * for testing return types of functions that return a certain + * type or *value* when not set or on error. + * + * @since 1.20 + * + * @param string $type + * @param mixed $actual + * @param mixed $value + * @param string $message + */ + protected function assertTypeOrValue( $type, $actual, $value = false, $message = '' ) { + if ( $actual === $value ) { + $this->assertTrue( true, $message ); + } else { + $this->assertType( $type, $actual, $message ); + } + } + + /** + * Asserts the type of the provided value. This can be either + * in internal type such as boolean or integer, or a class or + * interface the value extends or implements. + * + * @since 1.20 + * + * @param string $type + * @param mixed $actual + * @param string $message + */ + protected function assertType( $type, $actual, $message = '' ) { + if ( class_exists( $type ) || interface_exists( $type ) ) { + $this->assertInstanceOf( $type, $actual, $message ); + } else { + $this->assertInternalType( $type, $actual, $message ); + } + } + + /** + * Returns true if the given namespace defaults to Wikitext + * according to $wgNamespaceContentModels + * + * @param int $ns The namespace ID to check + * + * @return bool + * @since 1.21 + */ + protected function isWikitextNS( $ns ) { + global $wgNamespaceContentModels; + + if ( isset( $wgNamespaceContentModels[$ns] ) ) { + return $wgNamespaceContentModels[$ns] === CONTENT_MODEL_WIKITEXT; + } + + return true; + } + + /** + * Returns the ID of a namespace that defaults to Wikitext. + * Throws an MWException if there is none. + * + * @return int the ID of the wikitext Namespace + * @since 1.21 + */ + protected function getDefaultWikitextNS() { + global $wgNamespaceContentModels; + + static $wikitextNS = null; // this is not going to change + if ( $wikitextNS !== null ) { + return $wikitextNS; + } + + // quickly short out on most common case: + if ( !isset( $wgNamespaceContentModels[NS_MAIN] ) ) { + return NS_MAIN; + } + + // NOTE: prefer content namespaces + $namespaces = array_unique( array_merge( + MWNamespace::getContentNamespaces(), + array( NS_MAIN, NS_HELP, NS_PROJECT ), // prefer these + MWNamespace::getValidNamespaces() + ) ); + + $namespaces = array_diff( $namespaces, array( + NS_FILE, NS_CATEGORY, NS_MEDIAWIKI, NS_USER // don't mess with magic namespaces + ) ); + + $talk = array_filter( $namespaces, function ( $ns ) { + return MWNamespace::isTalk( $ns ); + } ); + + // prefer non-talk pages + $namespaces = array_diff( $namespaces, $talk ); + $namespaces = array_merge( $namespaces, $talk ); + + // check default content model of each namespace + foreach ( $namespaces as $ns ) { + if ( !isset( $wgNamespaceContentModels[$ns] ) || + $wgNamespaceContentModels[$ns] === CONTENT_MODEL_WIKITEXT + ) { + + $wikitextNS = $ns; + + return $wikitextNS; + } + } + + // give up + // @todo Inside a test, we could skip the test as incomplete. + // But frequently, this is used in fixture setup. + throw new MWException( "No namespace defaults to wikitext!" ); + } + + /** + * Check, if $wgDiff3 is set and ready to merge + * Will mark the calling test as skipped, if not ready + * + * @since 1.21 + */ + protected function checkHasDiff3() { + global $wgDiff3; + + # This check may also protect against code injection in + # case of broken installations. + wfSuppressWarnings(); + $haveDiff3 = $wgDiff3 && file_exists( $wgDiff3 ); + wfRestoreWarnings(); + + if ( !$haveDiff3 ) { + $this->markTestSkipped( "Skip test, since diff3 is not configured" ); + } + } + + /** + * Check whether we have the 'gzip' commandline utility, will skip + * the test whenever "gzip -V" fails. + * + * Result is cached at the process level. + * + * @return bool + * + * @since 1.21 + */ + protected function checkHasGzip() { + static $haveGzip; + + if ( $haveGzip === null ) { + $retval = null; + wfShellExec( 'gzip -V', $retval ); + $haveGzip = ( $retval === 0 ); + } + + if ( !$haveGzip ) { + $this->markTestSkipped( "Skip test, requires the gzip utility in PATH" ); + } + + return $haveGzip; + } + + /** + * Check if $extName is a loaded PHP extension, will skip the + * test whenever it is not loaded. + * + * @since 1.21 + */ + protected function checkPHPExtension( $extName ) { + $loaded = extension_loaded( $extName ); + if ( !$loaded ) { + $this->markTestSkipped( "PHP extension '$extName' is not loaded, skipping." ); + } + + return $loaded; + } + + /** + * Asserts that an exception of the specified type occurs when running + * the provided code. + * + * @since 1.21 + * @deprecated since 1.22 Use setExpectedException + * + * @param callable $code + * @param string $expected + * @param string $message + */ + protected function assertException( $code, $expected = 'Exception', $message = '' ) { + $pokemons = null; + + try { + call_user_func( $code ); + } catch ( Exception $pokemons ) { + // Gotta Catch 'Em All! + } + + if ( $message === '' ) { + $message = 'An exception of type "' . $expected . '" should have been thrown'; + } + + $this->assertInstanceOf( $expected, $pokemons, $message ); + } +} diff --git a/tests/phpunit/README b/tests/phpunit/README new file mode 100644 index 00000000..0a32ba17 --- /dev/null +++ b/tests/phpunit/README @@ -0,0 +1,53 @@ +== MediaWiki PHPUnit Tests == + +The unit tests for MediaWiki are implemented using the PHPUnit testing +framework and require PHPUnit to run. + + +=== WARNING === + +Some of the unit tests are DESTRUCTIVE and WILL ALTER YOUR WIKI'S CONTENTS. + +DO NOT RUN THESE TESTS ON A PRODUCTION SYSTEM OR ON ANY SYSTEM WHERE YOU NEED +TO RETAIN YOUR DATA. + + +== Installation == + +If PHPUnit is not installed, follow the installation instructions in the +PHPUnit Manual at: + + http://www.phpunit.de/manual/current/en/installation.html + +- or - + +On Unix-like operating systems, run: + + make install + + +== Running tests == + +The tests are run from your operating system's command line. + +Ensure that you are in the tests/phpunit directory of your MediaWiki +installation. + + +On Unix-like operating systems, the tests runs are controlled with a makefile. +Run command: + + make help + +for a full list of options for running tests. + + +On Windows-family operating systems, run the 'run-tests.bat' batch file. + + +=== Writing tests === + +A guide to writing unit tests for MediaWiki can be found at: + + http://mediawiki.org/wiki/Unit_Testing + diff --git a/tests/phpunit/TODO b/tests/phpunit/TODO new file mode 100644 index 00000000..b2fa7fb6 --- /dev/null +++ b/tests/phpunit/TODO @@ -0,0 +1,10 @@ +== Things To Do == +* Most of the tests are named poorly; naming should describe a use case in story-like language, not simply identify the +unit under test. An example would be the difference between testCalculate and testAddingIntegersTogetherWorks. +* Many of the tests make multiple assertions, and are thus not unitary tests. By using data-providers and more use-case +oriented test selection nearly all of these cases can be easily resolved. +* Some of the test files are either incorrectly named or in the wrong folder. Tests should be organized in a mirrored +structure to the source they are testing, and named the same, with the exception of the word "Test" at the end. +* Shared set-up code or base classes are present, but usually named improperly or appear to be poorly factored. Support +code should share as much of the same naming as the code it's supporting, and test and test-case depenencies should be +considered to resolve other shared needs. diff --git a/tests/phpunit/bootstrap.php b/tests/phpunit/bootstrap.php new file mode 100644 index 00000000..d929b79d --- /dev/null +++ b/tests/phpunit/bootstrap.php @@ -0,0 +1,15 @@ + + +]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tests/phpunit/data/media/Png-native-test.png b/tests/phpunit/data/media/Png-native-test.png new file mode 100644 index 00000000..a0b81ca9 Binary files /dev/null and b/tests/phpunit/data/media/Png-native-test.png differ diff --git a/tests/phpunit/data/media/QA_icon.svg b/tests/phpunit/data/media/QA_icon.svg new file mode 100644 index 00000000..6b5d86e4 --- /dev/null +++ b/tests/phpunit/data/media/QA_icon.svg @@ -0,0 +1,77 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ? + + + + + + + + \ No newline at end of file diff --git a/tests/phpunit/data/media/README b/tests/phpunit/data/media/README new file mode 100644 index 00000000..fe3bc682 --- /dev/null +++ b/tests/phpunit/data/media/README @@ -0,0 +1,38 @@ +This directory contains media files for use with the +tests in includes/media directory. + +Image credits: + +QA_icon.svg: +http://es.wikipedia.org/wiki/Archivo:QA_icon.svg +GNU Lesser General Public License +~~helix84 (16.4.2007), Philverney (6.12.2005) David Vignoni + +Gtk-media-play-ltr.svg +http://commons.wikimedia.org/wiki/File:Gtk-media-play-ltr.svg +GNU Lesser General Public License +http://ftp.gnome.org/pub/GNOME/sources/gnome-themes-extras/0.9/gnome-themes-extras-0.9.0.tar.gz +David Vignoni + +US_states_by_total_state_tax_revenue.svg +http://commons.wikimedia.org/wiki/File:US_states_by_total_state_tax_revenue.svg +CC-BY 3.0 +TastyCakes on English Wikipedia + +greyscale-na-png.png, rgb-png.png, Xmp-exif-multilingual_test.jpg +greyscale-png.png, 1bit-png.png, Png-native-test.png, rgb-na-png.png, +test.tiff, test.jpg, jpeg-comment-multiple.jpg, jpeg-comment-utf.jpg, +jpeg-comment-iso8859-1.jpg, jpeg-comment-binary.jpg, jpeg-xmp-psir.jpg, +jpeg-xmp-alt.jpg, animated.gif, exif-user-comment.jpg, animated-xmp.gif, +iptc-timetest-invalid.jpg, jpeg-iptc-bad-hash.jpg, iptc-timetest.jpg, +xmp.png, nonanimated.gif, exif-gps.jpg, jpeg-xmp-psir.xmp, jpeg-iptc-good-hash.jpg, +jpeg-padding-even.jpg, jpeg-padding-odd.jpg +Are all by Bawolff. I don't think they contain enough originality to +claim copyright, but on the off chance they do, feel free to use them +however you feel fit, without restriction. + +Animated_PNG_example_bouncing_beach_ball.png +http://commons.wikimedia.org/wiki/File:Animated_PNG_example_bouncing_beach_ball.png (originally http://www.treebuilder.de/default.asp?file=89031.xml ) +Public Domain +Holger Will + diff --git a/tests/phpunit/data/media/Toll_Texas_1.svg b/tests/phpunit/data/media/Toll_Texas_1.svg new file mode 100644 index 00000000..73004e3e --- /dev/null +++ b/tests/phpunit/data/media/Toll_Texas_1.svg @@ -0,0 +1,150 @@ + + + + +]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/phpunit/data/media/US_states_by_total_state_tax_revenue.svg b/tests/phpunit/data/media/US_states_by_total_state_tax_revenue.svg new file mode 100644 index 00000000..9afea859 --- /dev/null +++ b/tests/phpunit/data/media/US_states_by_total_state_tax_revenue.svg @@ -0,0 +1,248 @@ + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tests/phpunit/data/media/Wikimedia-logo.svg b/tests/phpunit/data/media/Wikimedia-logo.svg new file mode 100644 index 00000000..1e17acbe --- /dev/null +++ b/tests/phpunit/data/media/Wikimedia-logo.svg @@ -0,0 +1,14 @@ + + + \ No newline at end of file diff --git a/tests/phpunit/data/media/Xmp-exif-multilingual_test.jpg b/tests/phpunit/data/media/Xmp-exif-multilingual_test.jpg new file mode 100644 index 00000000..f7b23025 Binary files /dev/null and b/tests/phpunit/data/media/Xmp-exif-multilingual_test.jpg differ diff --git a/tests/phpunit/data/media/animated-xmp.gif b/tests/phpunit/data/media/animated-xmp.gif new file mode 100644 index 00000000..fcba079d Binary files /dev/null and b/tests/phpunit/data/media/animated-xmp.gif differ diff --git a/tests/phpunit/data/media/animated.gif b/tests/phpunit/data/media/animated.gif new file mode 100644 index 00000000..a8f248b3 Binary files /dev/null and b/tests/phpunit/data/media/animated.gif differ diff --git a/tests/phpunit/data/media/broken_exif_date.jpg b/tests/phpunit/data/media/broken_exif_date.jpg new file mode 100644 index 00000000..82f62f57 Binary files /dev/null and b/tests/phpunit/data/media/broken_exif_date.jpg differ diff --git a/tests/phpunit/data/media/exif-gps.jpg b/tests/phpunit/data/media/exif-gps.jpg new file mode 100644 index 00000000..40137340 Binary files /dev/null and b/tests/phpunit/data/media/exif-gps.jpg differ diff --git a/tests/phpunit/data/media/exif-user-comment.jpg b/tests/phpunit/data/media/exif-user-comment.jpg new file mode 100644 index 00000000..9f23966a Binary files /dev/null and b/tests/phpunit/data/media/exif-user-comment.jpg differ diff --git a/tests/phpunit/data/media/greyscale-na-png.png b/tests/phpunit/data/media/greyscale-na-png.png new file mode 100644 index 00000000..4a4b7452 Binary files /dev/null and b/tests/phpunit/data/media/greyscale-na-png.png differ diff --git a/tests/phpunit/data/media/greyscale-png.png b/tests/phpunit/data/media/greyscale-png.png new file mode 100644 index 00000000..340a67b4 Binary files /dev/null and b/tests/phpunit/data/media/greyscale-png.png differ diff --git a/tests/phpunit/data/media/iptc-invalid-psir.jpg b/tests/phpunit/data/media/iptc-invalid-psir.jpg new file mode 100644 index 00000000..01b9acf3 Binary files /dev/null and b/tests/phpunit/data/media/iptc-invalid-psir.jpg differ diff --git a/tests/phpunit/data/media/iptc-timetest-invalid.jpg b/tests/phpunit/data/media/iptc-timetest-invalid.jpg new file mode 100644 index 00000000..b03e192a Binary files /dev/null and b/tests/phpunit/data/media/iptc-timetest-invalid.jpg differ diff --git a/tests/phpunit/data/media/iptc-timetest.jpg b/tests/phpunit/data/media/iptc-timetest.jpg new file mode 100644 index 00000000..db9932ba Binary files /dev/null and b/tests/phpunit/data/media/iptc-timetest.jpg differ diff --git a/tests/phpunit/data/media/jpeg-comment-binary.jpg b/tests/phpunit/data/media/jpeg-comment-binary.jpg new file mode 100644 index 00000000..b467fe43 Binary files /dev/null and b/tests/phpunit/data/media/jpeg-comment-binary.jpg differ diff --git a/tests/phpunit/data/media/jpeg-comment-iso8859-1.jpg b/tests/phpunit/data/media/jpeg-comment-iso8859-1.jpg new file mode 100644 index 00000000..d9ffbac1 Binary files /dev/null and b/tests/phpunit/data/media/jpeg-comment-iso8859-1.jpg differ diff --git a/tests/phpunit/data/media/jpeg-comment-multiple.jpg b/tests/phpunit/data/media/jpeg-comment-multiple.jpg new file mode 100644 index 00000000..363c7385 Binary files /dev/null and b/tests/phpunit/data/media/jpeg-comment-multiple.jpg differ diff --git a/tests/phpunit/data/media/jpeg-comment-utf.jpg b/tests/phpunit/data/media/jpeg-comment-utf.jpg new file mode 100644 index 00000000..d6d35b4b Binary files /dev/null and b/tests/phpunit/data/media/jpeg-comment-utf.jpg differ diff --git a/tests/phpunit/data/media/jpeg-iptc-bad-hash.jpg b/tests/phpunit/data/media/jpeg-iptc-bad-hash.jpg new file mode 100644 index 00000000..6464c5b8 Binary files /dev/null and b/tests/phpunit/data/media/jpeg-iptc-bad-hash.jpg differ diff --git a/tests/phpunit/data/media/jpeg-iptc-good-hash.jpg b/tests/phpunit/data/media/jpeg-iptc-good-hash.jpg new file mode 100644 index 00000000..ef970854 Binary files /dev/null and b/tests/phpunit/data/media/jpeg-iptc-good-hash.jpg differ diff --git a/tests/phpunit/data/media/jpeg-padding-even.jpg b/tests/phpunit/data/media/jpeg-padding-even.jpg new file mode 100644 index 00000000..c83c66bd Binary files /dev/null and b/tests/phpunit/data/media/jpeg-padding-even.jpg differ diff --git a/tests/phpunit/data/media/jpeg-padding-odd.jpg b/tests/phpunit/data/media/jpeg-padding-odd.jpg new file mode 100644 index 00000000..25b93308 Binary files /dev/null and b/tests/phpunit/data/media/jpeg-padding-odd.jpg differ diff --git a/tests/phpunit/data/media/jpeg-xmp-alt.jpg b/tests/phpunit/data/media/jpeg-xmp-alt.jpg new file mode 100644 index 00000000..0e2c3f63 Binary files /dev/null and b/tests/phpunit/data/media/jpeg-xmp-alt.jpg differ diff --git a/tests/phpunit/data/media/jpeg-xmp-psir.jpg b/tests/phpunit/data/media/jpeg-xmp-psir.jpg new file mode 100644 index 00000000..4d19fcbe Binary files /dev/null and b/tests/phpunit/data/media/jpeg-xmp-psir.jpg differ diff --git a/tests/phpunit/data/media/jpeg-xmp-psir.xmp b/tests/phpunit/data/media/jpeg-xmp-psir.xmp new file mode 100644 index 00000000..fee6ee18 --- /dev/null +++ b/tests/phpunit/data/media/jpeg-xmp-psir.xmp @@ -0,0 +1,35 @@ + + + + + + jpeg-xmp-psir.jpg + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tests/phpunit/data/media/landscape-plain.jpg b/tests/phpunit/data/media/landscape-plain.jpg new file mode 100644 index 00000000..cf296555 Binary files /dev/null and b/tests/phpunit/data/media/landscape-plain.jpg differ diff --git a/tests/phpunit/data/media/nonanimated.gif b/tests/phpunit/data/media/nonanimated.gif new file mode 100644 index 00000000..9e52a7f0 Binary files /dev/null and b/tests/phpunit/data/media/nonanimated.gif differ diff --git a/tests/phpunit/data/media/portrait-rotated.jpg b/tests/phpunit/data/media/portrait-rotated.jpg new file mode 100644 index 00000000..445feaed Binary files /dev/null and b/tests/phpunit/data/media/portrait-rotated.jpg differ diff --git a/tests/phpunit/data/media/rgb-na-png.png b/tests/phpunit/data/media/rgb-na-png.png new file mode 100644 index 00000000..2f2a5ca0 Binary files /dev/null and b/tests/phpunit/data/media/rgb-na-png.png differ diff --git a/tests/phpunit/data/media/rgb-png.png b/tests/phpunit/data/media/rgb-png.png new file mode 100644 index 00000000..6f40cc92 Binary files /dev/null and b/tests/phpunit/data/media/rgb-png.png differ diff --git a/tests/phpunit/data/media/test.jpg b/tests/phpunit/data/media/test.jpg new file mode 100644 index 00000000..cb084253 Binary files /dev/null and b/tests/phpunit/data/media/test.jpg differ diff --git a/tests/phpunit/data/media/test.tiff b/tests/phpunit/data/media/test.tiff new file mode 100644 index 00000000..6a36f760 Binary files /dev/null and b/tests/phpunit/data/media/test.tiff differ diff --git a/tests/phpunit/data/media/xmp.png b/tests/phpunit/data/media/xmp.png new file mode 100644 index 00000000..6b9f7a87 Binary files /dev/null and b/tests/phpunit/data/media/xmp.png differ diff --git a/tests/phpunit/data/xmp/1.result.php b/tests/phpunit/data/xmp/1.result.php new file mode 100644 index 00000000..beead1bd --- /dev/null +++ b/tests/phpunit/data/xmp/1.result.php @@ -0,0 +1,8 @@ + + array( + 'DigitalZoomRatio' => '0/10', + 'Flash' => '9' + ) +); diff --git a/tests/phpunit/data/xmp/1.xmp b/tests/phpunit/data/xmp/1.xmp new file mode 100644 index 00000000..66e15427 --- /dev/null +++ b/tests/phpunit/data/xmp/1.xmp @@ -0,0 +1,11 @@ + + + + +True 0 1 False False + + diff --git a/tests/phpunit/data/xmp/2.result.php b/tests/phpunit/data/xmp/2.result.php new file mode 100644 index 00000000..beead1bd --- /dev/null +++ b/tests/phpunit/data/xmp/2.result.php @@ -0,0 +1,8 @@ + + array( + 'DigitalZoomRatio' => '0/10', + 'Flash' => '9' + ) +); diff --git a/tests/phpunit/data/xmp/2.xmp b/tests/phpunit/data/xmp/2.xmp new file mode 100644 index 00000000..0fa6a894 --- /dev/null +++ b/tests/phpunit/data/xmp/2.xmp @@ -0,0 +1,12 @@ + + + + + +True 1 False False + + diff --git a/tests/phpunit/data/xmp/3-invalid.result.php b/tests/phpunit/data/xmp/3-invalid.result.php new file mode 100644 index 00000000..5741b2c9 --- /dev/null +++ b/tests/phpunit/data/xmp/3-invalid.result.php @@ -0,0 +1,7 @@ + + array( + 'DigitalZoomRatio' => '0/10', + ) +); diff --git a/tests/phpunit/data/xmp/3-invalid.xmp b/tests/phpunit/data/xmp/3-invalid.xmp new file mode 100644 index 00000000..2425e254 --- /dev/null +++ b/tests/phpunit/data/xmp/3-invalid.xmp @@ -0,0 +1,31 @@ + + + + + + + + +0/10 + +fred + + + + + + + + +1 +False + + False False + + diff --git a/tests/phpunit/data/xmp/3.result.php b/tests/phpunit/data/xmp/3.result.php new file mode 100644 index 00000000..beead1bd --- /dev/null +++ b/tests/phpunit/data/xmp/3.result.php @@ -0,0 +1,8 @@ + + array( + 'DigitalZoomRatio' => '0/10', + 'Flash' => '9' + ) +); diff --git a/tests/phpunit/data/xmp/3.xmp b/tests/phpunit/data/xmp/3.xmp new file mode 100644 index 00000000..2cf19883 --- /dev/null +++ b/tests/phpunit/data/xmp/3.xmp @@ -0,0 +1,29 @@ + + + + + + + +0/10 + +fred + + + + + + + +True + +1 +False + + False False + + diff --git a/tests/phpunit/data/xmp/4.result.php b/tests/phpunit/data/xmp/4.result.php new file mode 100644 index 00000000..5741b2c9 --- /dev/null +++ b/tests/phpunit/data/xmp/4.result.php @@ -0,0 +1,7 @@ + + array( + 'DigitalZoomRatio' => '0/10', + ) +); diff --git a/tests/phpunit/data/xmp/4.xmp b/tests/phpunit/data/xmp/4.xmp new file mode 100644 index 00000000..29eb614b --- /dev/null +++ b/tests/phpunit/data/xmp/4.xmp @@ -0,0 +1,22 @@ + + + + + + + +0/10 + + +True 0 1 False False + + + + + + diff --git a/tests/phpunit/data/xmp/5.result.php b/tests/phpunit/data/xmp/5.result.php new file mode 100644 index 00000000..5741b2c9 --- /dev/null +++ b/tests/phpunit/data/xmp/5.result.php @@ -0,0 +1,7 @@ + + array( + 'DigitalZoomRatio' => '0/10', + ) +); diff --git a/tests/phpunit/data/xmp/5.xmp b/tests/phpunit/data/xmp/5.xmp new file mode 100644 index 00000000..3cc61d68 --- /dev/null +++ b/tests/phpunit/data/xmp/5.xmp @@ -0,0 +1,16 @@ + + + + + + +True 0 1 False False + + + + + + diff --git a/tests/phpunit/data/xmp/6.result.php b/tests/phpunit/data/xmp/6.result.php new file mode 100644 index 00000000..beead1bd --- /dev/null +++ b/tests/phpunit/data/xmp/6.result.php @@ -0,0 +1,8 @@ + + array( + 'DigitalZoomRatio' => '0/10', + 'Flash' => '9' + ) +); diff --git a/tests/phpunit/data/xmp/6.xmp b/tests/phpunit/data/xmp/6.xmp new file mode 100644 index 00000000..f435ab23 --- /dev/null +++ b/tests/phpunit/data/xmp/6.xmp @@ -0,0 +1,18 @@ + + + + +0/10 + + + + + +True 0 1 False False + + diff --git a/tests/phpunit/data/xmp/7.result.php b/tests/phpunit/data/xmp/7.result.php new file mode 100644 index 00000000..115cdc92 --- /dev/null +++ b/tests/phpunit/data/xmp/7.result.php @@ -0,0 +1,52 @@ + + array( + 'CameraOwnerName' => 'Me!', + ), + 'xmp-general' => + array( + 'LicenseUrl' => 'http://creativecommons.com/cc-by-2.9', + 'ImageDescription' => + array( + 'x-default' => 'Test image for the cc: xmp: xmpRights: namespaces in xmp', + '_type' => 'lang', + ), + 'ObjectName' => + array( + 'x-default' => 'xmp core/xmp rights/cc ns test', + '_type' => 'lang', + ), + 'DateTimeDigitized' => '2005:04:03', + 'Software' => 'The one true editor: Vi (ok i used gimp)', + 'Identifier' => + array( + 0 => 'http://example.com/identifierurl', + 1 => 'urn:sha1:342524abcdef', + '_type' => 'ul', + ), + 'Label' => 'Test image', + 'DateTimeMetadata' => '2011:05:12', + 'DateTime' => '2007:03:04 06:34:10', + 'Nickname' => 'My little xmp test image', + 'Rating' => '5', + 'RightsCertificate' => 'http://example.com/rights-certificate/', + 'Copyrighted' => 'True', + 'CopyrightOwner' => + array( + 0 => 'Bawolff is copyright owner', + '_type' => 'ul', + ), + 'UsageTerms' => + array( + 'x-default' => 'do whatever you want', + 'en-gb' => 'Do whatever you want in british english', + '_type' => 'lang', + ), + 'WebStatement' => 'http://example.com/web_statement', + ), + 'xmp-deprecated' => + array( + 'Identifier' => 'http://example.com/identifierurl/wrong', + ), +); diff --git a/tests/phpunit/data/xmp/7.xmp b/tests/phpunit/data/xmp/7.xmp new file mode 100644 index 00000000..e18e13d9 --- /dev/null +++ b/tests/phpunit/data/xmp/7.xmp @@ -0,0 +1,67 @@ + + + + + + Me! + + + + http://creativecommons.com/cc-by-2.9 + + + + + + Test image for the cc: xmp: xmpRights: namespaces in xmp + + + http://example.com/identifierurl/wrong + + + xmp core/xmp rights/cc ns test + + + + + + 2005-04-03 + The one true editor: Vi (ok i used gimp) + + + http://example.com/identifierurl + + urn:sha1:342524abcdef + + + Test image + 2011-05-12 + 2007-03-04T12:34:10-06:00 + My little xmp test image + 7 + + + + http://example.com/rights-certificate/ + True + + + Bawolff is copyright owner + + + + + do whatever you want + Do whatever you want in british english + + + http://example.com/web_statement + + + + diff --git a/tests/phpunit/data/xmp/README b/tests/phpunit/data/xmp/README new file mode 100644 index 00000000..bd949176 --- /dev/null +++ b/tests/phpunit/data/xmp/README @@ -0,0 +1,3 @@ +This directory contains a bunch of XMP files +as well as a bunch of php files containing what the +parsed version of the XMP looks like. diff --git a/tests/phpunit/data/xmp/bag-for-seq.result.php b/tests/phpunit/data/xmp/bag-for-seq.result.php new file mode 100644 index 00000000..b5244f88 --- /dev/null +++ b/tests/phpunit/data/xmp/bag-for-seq.result.php @@ -0,0 +1,10 @@ + array( + 'Artist' => array( + '_type' => 'ul', + 0 => 'The author', + ) + ) +); diff --git a/tests/phpunit/data/xmp/bag-for-seq.xmp b/tests/phpunit/data/xmp/bag-for-seq.xmp new file mode 100644 index 00000000..c6ed5b7c --- /dev/null +++ b/tests/phpunit/data/xmp/bag-for-seq.xmp @@ -0,0 +1 @@ + The author diff --git a/tests/phpunit/data/xmp/flash.result.php b/tests/phpunit/data/xmp/flash.result.php new file mode 100644 index 00000000..018c0ac1 --- /dev/null +++ b/tests/phpunit/data/xmp/flash.result.php @@ -0,0 +1,8 @@ + + array( + 'DigitalZoomRatio' => '0/10', + 'Flash' => '127' + ) +); diff --git a/tests/phpunit/data/xmp/flash.xmp b/tests/phpunit/data/xmp/flash.xmp new file mode 100644 index 00000000..b1373cc2 --- /dev/null +++ b/tests/phpunit/data/xmp/flash.xmp @@ -0,0 +1,11 @@ + + + + +True 3 3 True True + + diff --git a/tests/phpunit/data/xmp/gps.result.php b/tests/phpunit/data/xmp/gps.result.php new file mode 100644 index 00000000..8ea9c68c --- /dev/null +++ b/tests/phpunit/data/xmp/gps.result.php @@ -0,0 +1,11 @@ + + array( + 'GPSAltitude' => -3.14159265301, + 'GPSDOP' => '5/1', + 'GPSLatitude' => 88.51805555, + 'GPSLongitude' => -21.12356945, + 'GPSVersionID' => '2.2.0.0' + ) +); diff --git a/tests/phpunit/data/xmp/gps.xmp b/tests/phpunit/data/xmp/gps.xmp new file mode 100644 index 00000000..e52d2c8a --- /dev/null +++ b/tests/phpunit/data/xmp/gps.xmp @@ -0,0 +1,17 @@ + + + + + + 103993/33102 + 1 + 5/1 + 88,31.083333N + 21,7.414167W + 2.2.0.0 + + + + + diff --git a/tests/phpunit/data/xmp/invalid-child-not-struct.result.php b/tests/phpunit/data/xmp/invalid-child-not-struct.result.php new file mode 100644 index 00000000..5741b2c9 --- /dev/null +++ b/tests/phpunit/data/xmp/invalid-child-not-struct.result.php @@ -0,0 +1,7 @@ + + array( + 'DigitalZoomRatio' => '0/10', + ) +); diff --git a/tests/phpunit/data/xmp/invalid-child-not-struct.xmp b/tests/phpunit/data/xmp/invalid-child-not-struct.xmp new file mode 100644 index 00000000..6aa0c10b --- /dev/null +++ b/tests/phpunit/data/xmp/invalid-child-not-struct.xmp @@ -0,0 +1,12 @@ + + + +True 0 1 False False + + + + diff --git a/tests/phpunit/data/xmp/no-namespace.result.php b/tests/phpunit/data/xmp/no-namespace.result.php new file mode 100644 index 00000000..3ff69201 --- /dev/null +++ b/tests/phpunit/data/xmp/no-namespace.result.php @@ -0,0 +1,7 @@ + + array( + 'FNumber' => '28/10', + ) +); diff --git a/tests/phpunit/data/xmp/no-namespace.xmp b/tests/phpunit/data/xmp/no-namespace.xmp new file mode 100644 index 00000000..7d6cdb2f --- /dev/null +++ b/tests/phpunit/data/xmp/no-namespace.xmp @@ -0,0 +1,11 @@ + + + + + + diff --git a/tests/phpunit/data/xmp/no-recognized-props.result.php b/tests/phpunit/data/xmp/no-recognized-props.result.php new file mode 100644 index 00000000..b3ca9f5a --- /dev/null +++ b/tests/phpunit/data/xmp/no-recognized-props.result.php @@ -0,0 +1,2 @@ + + + + + diff --git a/tests/phpunit/data/xmp/utf16BE.result.php b/tests/phpunit/data/xmp/utf16BE.result.php new file mode 100644 index 00000000..ac7ea506 --- /dev/null +++ b/tests/phpunit/data/xmp/utf16BE.result.php @@ -0,0 +1,12 @@ + + array( + 'DigitalZoomRatio' => '0/10', + ), + 'xmp-general' => + array( + 'Label' => '􊯍' + ), +); diff --git a/tests/phpunit/data/xmp/utf16BE.xmp b/tests/phpunit/data/xmp/utf16BE.xmp new file mode 100644 index 00000000..0cf60d60 Binary files /dev/null and b/tests/phpunit/data/xmp/utf16BE.xmp differ diff --git a/tests/phpunit/data/xmp/utf16LE.result.php b/tests/phpunit/data/xmp/utf16LE.result.php new file mode 100644 index 00000000..ac7ea506 --- /dev/null +++ b/tests/phpunit/data/xmp/utf16LE.result.php @@ -0,0 +1,12 @@ + + array( + 'DigitalZoomRatio' => '0/10', + ), + 'xmp-general' => + array( + 'Label' => '􊯍' + ), +); diff --git a/tests/phpunit/data/xmp/utf16LE.xmp b/tests/phpunit/data/xmp/utf16LE.xmp new file mode 100644 index 00000000..66d71f4c Binary files /dev/null and b/tests/phpunit/data/xmp/utf16LE.xmp differ diff --git a/tests/phpunit/data/xmp/utf32BE.result.php b/tests/phpunit/data/xmp/utf32BE.result.php new file mode 100644 index 00000000..ac7ea506 --- /dev/null +++ b/tests/phpunit/data/xmp/utf32BE.result.php @@ -0,0 +1,12 @@ + + array( + 'DigitalZoomRatio' => '0/10', + ), + 'xmp-general' => + array( + 'Label' => '􊯍' + ), +); diff --git a/tests/phpunit/data/xmp/utf32BE.xmp b/tests/phpunit/data/xmp/utf32BE.xmp new file mode 100644 index 00000000..06afdf92 Binary files /dev/null and b/tests/phpunit/data/xmp/utf32BE.xmp differ diff --git a/tests/phpunit/data/xmp/utf32LE.result.php b/tests/phpunit/data/xmp/utf32LE.result.php new file mode 100644 index 00000000..ac7ea506 --- /dev/null +++ b/tests/phpunit/data/xmp/utf32LE.result.php @@ -0,0 +1,12 @@ + + array( + 'DigitalZoomRatio' => '0/10', + ), + 'xmp-general' => + array( + 'Label' => '􊯍' + ), +); diff --git a/tests/phpunit/data/xmp/utf32LE.xmp b/tests/phpunit/data/xmp/utf32LE.xmp new file mode 100644 index 00000000..bf2097fe Binary files /dev/null and b/tests/phpunit/data/xmp/utf32LE.xmp differ diff --git a/tests/phpunit/data/xmp/xmpExt.result.php b/tests/phpunit/data/xmp/xmpExt.result.php new file mode 100644 index 00000000..beead1bd --- /dev/null +++ b/tests/phpunit/data/xmp/xmpExt.result.php @@ -0,0 +1,8 @@ + + array( + 'DigitalZoomRatio' => '0/10', + 'Flash' => '9' + ) +); diff --git a/tests/phpunit/data/xmp/xmpExt.xmp b/tests/phpunit/data/xmp/xmpExt.xmp new file mode 100644 index 00000000..da0383f8 --- /dev/null +++ b/tests/phpunit/data/xmp/xmpExt.xmp @@ -0,0 +1,13 @@ + + + + +True 0 1 False False + + diff --git a/tests/phpunit/data/xmp/xmpExt2.xmp b/tests/phpunit/data/xmp/xmpExt2.xmp new file mode 100644 index 00000000..060abb2c --- /dev/null +++ b/tests/phpunit/data/xmp/xmpExt2.xmp @@ -0,0 +1,8 @@ + + + + + diff --git a/tests/phpunit/data/zip/cd-gap.zip b/tests/phpunit/data/zip/cd-gap.zip new file mode 100644 index 00000000..b5ae6ccd Binary files /dev/null and b/tests/phpunit/data/zip/cd-gap.zip differ diff --git a/tests/phpunit/data/zip/cd-truncated.zip b/tests/phpunit/data/zip/cd-truncated.zip new file mode 100644 index 00000000..4d40d7d4 Binary files /dev/null and b/tests/phpunit/data/zip/cd-truncated.zip differ diff --git a/tests/phpunit/data/zip/class-trailing-null.zip b/tests/phpunit/data/zip/class-trailing-null.zip new file mode 100644 index 00000000..31dcf3d8 Binary files /dev/null and b/tests/phpunit/data/zip/class-trailing-null.zip differ diff --git a/tests/phpunit/data/zip/class-trailing-slash.zip b/tests/phpunit/data/zip/class-trailing-slash.zip new file mode 100644 index 00000000..9eb1f037 Binary files /dev/null and b/tests/phpunit/data/zip/class-trailing-slash.zip differ diff --git a/tests/phpunit/data/zip/class.zip b/tests/phpunit/data/zip/class.zip new file mode 100644 index 00000000..98a625b7 Binary files /dev/null and b/tests/phpunit/data/zip/class.zip differ diff --git a/tests/phpunit/data/zip/empty.zip b/tests/phpunit/data/zip/empty.zip new file mode 100644 index 00000000..15cb0ecb Binary files /dev/null and b/tests/phpunit/data/zip/empty.zip differ diff --git a/tests/phpunit/data/zip/looks-like-zip64.zip b/tests/phpunit/data/zip/looks-like-zip64.zip new file mode 100644 index 00000000..7428cddd Binary files /dev/null and b/tests/phpunit/data/zip/looks-like-zip64.zip differ diff --git a/tests/phpunit/data/zip/nosig.zip b/tests/phpunit/data/zip/nosig.zip new file mode 100644 index 00000000..a22c73a4 Binary files /dev/null and b/tests/phpunit/data/zip/nosig.zip differ diff --git a/tests/phpunit/data/zip/split.zip b/tests/phpunit/data/zip/split.zip new file mode 100644 index 00000000..6984ae6d Binary files /dev/null and b/tests/phpunit/data/zip/split.zip differ diff --git a/tests/phpunit/data/zip/trail.zip b/tests/phpunit/data/zip/trail.zip new file mode 100644 index 00000000..50bcea12 Binary files /dev/null and b/tests/phpunit/data/zip/trail.zip differ diff --git a/tests/phpunit/data/zip/wrong-cd-start-disk.zip b/tests/phpunit/data/zip/wrong-cd-start-disk.zip new file mode 100644 index 00000000..59b45938 Binary files /dev/null and b/tests/phpunit/data/zip/wrong-cd-start-disk.zip differ diff --git a/tests/phpunit/data/zip/wrong-central-entry-sig.zip b/tests/phpunit/data/zip/wrong-central-entry-sig.zip new file mode 100644 index 00000000..05329b43 Binary files /dev/null and b/tests/phpunit/data/zip/wrong-central-entry-sig.zip differ diff --git a/tests/phpunit/docs/ExportDemoTest.php b/tests/phpunit/docs/ExportDemoTest.php new file mode 100644 index 00000000..b09487a6 --- /dev/null +++ b/tests/phpunit/docs/ExportDemoTest.php @@ -0,0 +1,39 @@ +validateXmlFileAgainstXsd( "../../docs/export-demo.xml" ); + } + + /** + * Validates a xml file against the xsd. + * + * The validation is slow, because php has to read the xsd on each call. + * + * @param $fname string: name of file to validate + */ + protected function validateXmlFileAgainstXsd( $fname ) { + $version = WikiExporter::schemaVersion(); + + $dom = new DomDocument(); + $dom->load( $fname ); + + // Ensure, the demo is for the current version + $this->assertEquals( $dom->documentElement->getAttribute( 'version' ), $version, 'export-demo.xml should have the current version' ); + + try { + $this->assertTrue( $dom->schemaValidate( "../../docs/export-" . $version . ".xsd" ), + "schemaValidate has found an error" ); + } catch ( Exception $e ) { + $this->fail( "xml not valid against xsd: " . $e->getMessage() ); + } + } +} diff --git a/tests/phpunit/includes/ArticleTablesTest.php b/tests/phpunit/includes/ArticleTablesTest.php new file mode 100644 index 00000000..469d1d19 --- /dev/null +++ b/tests/phpunit/includes/ArticleTablesTest.php @@ -0,0 +1,32 @@ +mRights = array( 'createpage', 'edit', 'purge' ); + $wgLanguageCode = 'es'; + $wgContLang = Language::factory( 'es' ); + + $wgLang = Language::factory( 'fr' ); + $page->doEditContent( new WikitextContent( '{{:{{int:history}}}}' ), 'Test code for bug 14404', 0, false, $user ); + $templates1 = $title->getTemplateLinksFrom(); + + $wgLang = Language::factory( 'de' ); + $page = WikiPage::factory( $title ); // In order to force the rerendering of the same wikitext + + // We need an edit, a purge is not enough to regenerate the tables + $page->doEditContent( new WikitextContent( '{{:{{int:history}}}}' ), 'Test code for bug 14404', EDIT_UPDATE, false, $user ); + $templates2 = $title->getTemplateLinksFrom(); + + $this->assertEquals( $templates1, $templates2 ); + $this->assertEquals( $templates1[0]->getFullText(), 'Historial' ); + } +} diff --git a/tests/phpunit/includes/ArticleTest.php b/tests/phpunit/includes/ArticleTest.php new file mode 100644 index 00000000..b4d6dca6 --- /dev/null +++ b/tests/phpunit/includes/ArticleTest.php @@ -0,0 +1,92 @@ +title = Title::makeTitle( NS_MAIN, 'SomePage' ); + $this->article = new Article( $this->title ); + } + + /** cleanup title object and its article object */ + protected function tearDown() { + parent::tearDown(); + $this->title = null; + $this->article = null; + } + + public function testImplementsGetMagic() { + $this->assertEquals( false, $this->article->mLatest, "Article __get magic" ); + } + + /** + * @depends testImplementsGetMagic + */ + public function testImplementsSetMagic() { + $this->article->mLatest = 2; + $this->assertEquals( 2, $this->article->mLatest, "Article __set magic" ); + } + + /** + * @depends testImplementsSetMagic + */ + public function testImplementsCallMagic() { + $this->article->mLatest = 33; + $this->article->mDataLoaded = true; + $this->assertEquals( 33, $this->article->getLatest(), "Article __call magic" ); + } + + public function testGetOrSetOnNewProperty() { + $this->article->ext_someNewProperty = 12; + $this->assertEquals( 12, $this->article->ext_someNewProperty, + "Article get/set magic on new field" ); + + $this->article->ext_someNewProperty = -8; + $this->assertEquals( -8, $this->article->ext_someNewProperty, + "Article get/set magic on update to new field" ); + } + + /** + * Checks for the existence of the backwards compatibility static functions (forwarders to WikiPage class) + */ + public function testStaticFunctions() { + $this->hideDeprecated( 'Article::getAutosummary' ); + $this->hideDeprecated( 'WikiPage::getAutosummary' ); + $this->hideDeprecated( 'CategoryPage::getAutosummary' ); // Inherited from Article + + $this->assertEquals( WikiPage::selectFields(), Article::selectFields(), + "Article static functions" ); + $this->assertEquals( true, is_callable( "Article::onArticleCreate" ), + "Article static functions" ); + $this->assertEquals( true, is_callable( "Article::onArticleDelete" ), + "Article static functions" ); + $this->assertEquals( true, is_callable( "ImagePage::onArticleEdit" ), + "Article static functions" ); + $this->assertTrue( is_string( CategoryPage::getAutosummary( '', '', 0 ) ), + "Article static functions" ); + } + + public function testWikiPageFactory() { + $title = Title::makeTitle( NS_FILE, 'Someimage.png' ); + $page = WikiPage::factory( $title ); + $this->assertEquals( 'WikiFilePage', get_class( $page ) ); + + $title = Title::makeTitle( NS_CATEGORY, 'SomeCategory' ); + $page = WikiPage::factory( $title ); + $this->assertEquals( 'WikiCategoryPage', get_class( $page ) ); + + $title = Title::makeTitle( NS_MAIN, 'SomePage' ); + $page = WikiPage::factory( $title ); + $this->assertEquals( 'WikiPage', get_class( $page ) ); + } +} diff --git a/tests/phpunit/includes/BlockTest.php b/tests/phpunit/includes/BlockTest.php new file mode 100644 index 00000000..21de0985 --- /dev/null +++ b/tests/phpunit/includes/BlockTest.php @@ -0,0 +1,354 @@ +setMwGlobals( array( + 'wgLanguageCode' => 'en', + 'wgContLang' => Language::factory( 'en' ) + ) ); + } + + function addDBData() { + + $user = User::newFromName( 'UTBlockee' ); + if ( $user->getID() == 0 ) { + $user->addToDatabase(); + $user->setPassword( 'UTBlockeePassword' ); + + $user->saveSettings(); + } + + // Delete the last round's block if it's still there + $oldBlock = Block::newFromTarget( 'UTBlockee' ); + if ( $oldBlock ) { + // An old block will prevent our new one from saving. + $oldBlock->delete(); + } + + $this->block = new Block( 'UTBlockee', $user->getID(), 0, + 'Parce que', 0, false, time() + 100500 + ); + $this->madeAt = wfTimestamp( TS_MW ); + + $this->block->insert(); + // save up ID for use in assertion. Since ID is an autoincrement, + // its value might change depending on the order the tests are run. + // ApiBlockTest insert its own blocks! + $newBlockId = $this->block->getId(); + if ( $newBlockId ) { + $this->blockId = $newBlockId; + } else { + throw new MWException( "Failed to insert block for BlockTest; old leftover block remaining?" ); + } + + $this->addXffBlocks(); + } + + /** + * debug function : dump the ipblocks table + */ + function dumpBlocks() { + $v = $this->db->select( 'ipblocks', '*' ); + print "Got " . $v->numRows() . " rows. Full dump follow:\n"; + foreach ( $v as $row ) { + print_r( $row ); + } + } + + public function testInitializerFunctionsReturnCorrectBlock() { + // $this->dumpBlocks(); + + $this->assertTrue( $this->block->equals( Block::newFromTarget( 'UTBlockee' ) ), "newFromTarget() returns the same block as the one that was made" ); + + $this->assertTrue( $this->block->equals( Block::newFromID( $this->blockId ) ), "newFromID() returns the same block as the one that was made" ); + } + + /** + * per bug 26425 + */ + public function testBug26425BlockTimestampDefaultsToTime() { + // delta to stop one-off errors when things happen to go over a second mark. + $delta = abs( $this->madeAt - $this->block->mTimestamp ); + $this->assertLessThan( 2, $delta, "If no timestamp is specified, the block is recorded as time()" ); + } + + /** + * This is the method previously used to load block info in CheckUser etc + * passing an empty value (empty string, null, etc) as the ip parameter bypasses IP lookup checks. + * + * This stopped working with r84475 and friends: regression being fixed for bug 29116. + * + * @dataProvider provideBug29116Data + */ + public function testBug29116LoadWithEmptyIp( $vagueTarget ) { + $this->hideDeprecated( 'Block::load' ); + + $uid = User::idFromName( 'UTBlockee' ); + $this->assertTrue( ( $uid > 0 ), 'Must be able to look up the target user during tests' ); + + $block = new Block(); + $ok = $block->load( $vagueTarget, $uid ); + $this->assertTrue( $ok, "Block->load() with empty IP and user ID '$uid' should return a block" ); + + $this->assertTrue( $this->block->equals( $block ), "Block->load() returns the same block as the one that was made when given empty ip param " . var_export( $vagueTarget, true ) ); + } + + /** + * CheckUser since being changed to use Block::newFromTarget started failing + * because the new function didn't accept empty strings like Block::load() + * had. Regression bug 29116. + * + * @dataProvider provideBug29116Data + */ + public function testBug29116NewFromTargetWithEmptyIp( $vagueTarget ) { + $block = Block::newFromTarget( 'UTBlockee', $vagueTarget ); + $this->assertTrue( $this->block->equals( $block ), "newFromTarget() returns the same block as the one that was made when given empty vagueTarget param " . var_export( $vagueTarget, true ) ); + } + + public static function provideBug29116Data() { + return array( + array( null ), + array( '' ), + array( false ) + ); + } + + public function testBlockedUserCanNotCreateAccount() { + $username = 'BlockedUserToCreateAccountWith'; + $u = User::newFromName( $username ); + $u->setPassword( 'NotRandomPass' ); + $u->addToDatabase(); + unset( $u ); + + // Sanity check + $this->assertNull( + Block::newFromTarget( $username ), + "$username should not be blocked" + ); + + // Reload user + $u = User::newFromName( $username ); + $this->assertFalse( + $u->isBlockedFromCreateAccount(), + "Our sandbox user should be able to create account before being blocked" + ); + + // Foreign perspective (blockee not on current wiki)... + $block = new Block( + /* $address */ $username, + /* $user */ 14146, + /* $by */ 0, + /* $reason */ 'crosswiki block...', + /* $timestamp */ wfTimestampNow(), + /* $auto */ false, + /* $expiry */ $this->db->getInfinity(), + /* anonOnly */ false, + /* $createAccount */ true, + /* $enableAutoblock */ true, + /* $hideName (ipb_deleted) */ true, + /* $blockEmail */ true, + /* $allowUsertalk */ false, + /* $byName */ 'MetaWikiUser' + ); + $block->insert(); + + // Reload block from DB + $userBlock = Block::newFromTarget( $username ); + $this->assertTrue( + (bool)$block->prevents( 'createaccount' ), + "Block object in DB should prevents 'createaccount'" + ); + + $this->assertInstanceOf( + 'Block', + $userBlock, + "'$username' block block object should be existent" + ); + + // Reload user + $u = User::newFromName( $username ); + $this->assertTrue( + (bool)$u->isBlockedFromCreateAccount(), + "Our sandbox user '$username' should NOT be able to create account" + ); + } + + public function testCrappyCrossWikiBlocks() { + // Delete the last round's block if it's still there + $oldBlock = Block::newFromTarget( 'UserOnForeignWiki' ); + if ( $oldBlock ) { + // An old block will prevent our new one from saving. + $oldBlock->delete(); + } + + // Foreign perspective (blockee not on current wiki)... + $block = new Block( + /* $address */ 'UserOnForeignWiki', + /* $user */ 14146, + /* $by */ 0, + /* $reason */ 'crosswiki block...', + /* $timestamp */ wfTimestampNow(), + /* $auto */ false, + /* $expiry */ $this->db->getInfinity(), + /* anonOnly */ false, + /* $createAccount */ true, + /* $enableAutoblock */ true, + /* $hideName (ipb_deleted) */ true, + /* $blockEmail */ true, + /* $allowUsertalk */ false, + /* $byName */ 'MetaWikiUser' + ); + + $res = $block->insert( $this->db ); + $this->assertTrue( (bool)$res['id'], 'Block succeeded' ); + + // Local perspective (blockee on current wiki)... + $user = User::newFromName( 'UserOnForeignWiki' ); + $user->addToDatabase(); + // Set user ID to match the test value + $this->db->update( 'user', array( 'user_id' => 14146 ), array( 'user_id' => $user->getId() ) ); + $user = null; // clear + + $block = Block::newFromID( $res['id'] ); + $this->assertEquals( 'UserOnForeignWiki', $block->getTarget()->getName(), 'Correct blockee name' ); + $this->assertEquals( '14146', $block->getTarget()->getId(), 'Correct blockee id' ); + $this->assertEquals( 'MetaWikiUser', $block->getBlocker(), 'Correct blocker name' ); + $this->assertEquals( 'MetaWikiUser', $block->getByName(), 'Correct blocker name' ); + $this->assertEquals( 0, $block->getBy(), 'Correct blocker id' ); + } + + protected function addXffBlocks() { + static $inited = false; + + if ( $inited ) { + return; + } + + $inited = true; + + $blockList = array( + array( 'target' => '70.2.0.0/16', + 'type' => Block::TYPE_RANGE, + 'desc' => 'Range Hardblock', + 'ACDisable' => false, + 'isHardblock' => true, + 'isAutoBlocking' => false, + ), + array( 'target' => '2001:4860:4001::/48', + 'type' => Block::TYPE_RANGE, + 'desc' => 'Range6 Hardblock', + 'ACDisable' => false, + 'isHardblock' => true, + 'isAutoBlocking' => false, + ), + array( 'target' => '60.2.0.0/16', + 'type' => Block::TYPE_RANGE, + 'desc' => 'Range Softblock with AC Disabled', + 'ACDisable' => true, + 'isHardblock' => false, + 'isAutoBlocking' => false, + ), + array( 'target' => '50.2.0.0/16', + 'type' => Block::TYPE_RANGE, + 'desc' => 'Range Softblock', + 'ACDisable' => false, + 'isHardblock' => false, + 'isAutoBlocking' => false, + ), + array( 'target' => '50.1.1.1', + 'type' => Block::TYPE_IP, + 'desc' => 'Exact Softblock', + 'ACDisable' => false, + 'isHardblock' => false, + 'isAutoBlocking' => false, + ), + ); + + foreach ( $blockList as $insBlock ) { + $target = $insBlock['target']; + + if ( $insBlock['type'] === Block::TYPE_IP ) { + $target = User::newFromName( IP::sanitizeIP( $target ), false )->getName(); + } elseif ( $insBlock['type'] === Block::TYPE_RANGE ) { + $target = IP::sanitizeRange( $target ); + } + + $block = new Block(); + $block->setTarget( $target ); + $block->setBlocker( 'testblocker@global' ); + $block->mReason = $insBlock['desc']; + $block->mExpiry = 'infinity'; + $block->prevents( 'createaccount', $insBlock['ACDisable'] ); + $block->isHardblock( $insBlock['isHardblock'] ); + $block->isAutoblocking( $insBlock['isAutoBlocking'] ); + $block->insert(); + } + } + + public static function providerXff() { + return array( + array( 'xff' => '1.2.3.4, 70.2.1.1, 60.2.1.1, 2.3.4.5', + 'count' => 2, + 'result' => 'Range Hardblock' + ), + array( 'xff' => '1.2.3.4, 50.2.1.1, 60.2.1.1, 2.3.4.5', + 'count' => 2, + 'result' => 'Range Softblock with AC Disabled' + ), + array( 'xff' => '1.2.3.4, 70.2.1.1, 50.1.1.1, 2.3.4.5', + 'count' => 2, + 'result' => 'Exact Softblock' + ), + array( 'xff' => '1.2.3.4, 70.2.1.1, 50.2.1.1, 50.1.1.1, 2.3.4.5', + 'count' => 3, + 'result' => 'Exact Softblock' + ), + array( 'xff' => '1.2.3.4, 70.2.1.1, 50.2.1.1, 2.3.4.5', + 'count' => 2, + 'result' => 'Range Hardblock' + ), + array( 'xff' => '1.2.3.4, 70.2.1.1, 60.2.1.1, 2.3.4.5', + 'count' => 2, + 'result' => 'Range Hardblock' + ), + array( 'xff' => '50.2.1.1, 60.2.1.1, 2.3.4.5', + 'count' => 2, + 'result' => 'Range Softblock with AC Disabled' + ), + array( 'xff' => '1.2.3.4, 50.1.1.1, 60.2.1.1, 2.3.4.5', + 'count' => 2, + 'result' => 'Exact Softblock' + ), + array( 'xff' => '1.2.3.4, <$A_BUNCH-OF{INVALID}TEXT\>, 60.2.1.1, 2.3.4.5', + 'count' => 1, + 'result' => 'Range Softblock with AC Disabled' + ), + array( 'xff' => '1.2.3.4, 50.2.1.1, 2001:4860:4001:802::1003, 2.3.4.5', + 'count' => 2, + 'result' => 'Range6 Hardblock' + ), + ); + } + + /** + * @dataProvider providerXff + */ + public function testBlocksOnXff( $xff, $exCount, $exResult ) { + $list = array_map( 'trim', explode( ',', $xff ) ); + $xffblocks = Block::getBlocksForIPList( $list, true ); + $this->assertEquals( $exCount, count( $xffblocks ), 'Number of blocks for ' . $xff ); + $block = Block::chooseBlock( $xffblocks, $list ); + $this->assertEquals( $exResult, $block->mReason, 'Correct block type for XFF header ' . $xff ); + } +} diff --git a/tests/phpunit/includes/CdbTest.php b/tests/phpunit/includes/CdbTest.php new file mode 100644 index 00000000..e3d9da7c --- /dev/null +++ b/tests/phpunit/includes/CdbTest.php @@ -0,0 +1,88 @@ +markTestSkipped( 'Native CDB support is not available' ); + } + } + + /** + * @group medium + */ + public function testCdb() { + $dir = wfTempDir(); + if ( !is_writable( $dir ) ) { + $this->markTestSkipped( "Temp dir isn't writable" ); + } + + $phpcdbfile = $this->getNewTempFile(); + $dbacdbfile = $this->getNewTempFile(); + + $w1 = new CdbWriter_PHP( $phpcdbfile ); + $w2 = new CdbWriter_DBA( $dbacdbfile ); + + $data = array(); + for ( $i = 0; $i < 1000; $i++ ) { + $key = $this->randomString(); + $value = $this->randomString(); + $w1->set( $key, $value ); + $w2->set( $key, $value ); + + if ( !isset( $data[$key] ) ) { + $data[$key] = $value; + } + } + + $w1->close(); + $w2->close(); + + $this->assertEquals( + md5_file( $phpcdbfile ), + md5_file( $dbacdbfile ), + 'same hash' + ); + + $r1 = new CdbReader_PHP( $phpcdbfile ); + $r2 = new CdbReader_DBA( $dbacdbfile ); + + foreach ( $data as $key => $value ) { + if ( $key === '' ) { + // Known bug + continue; + } + $v1 = $r1->get( $key ); + $v2 = $r2->get( $key ); + + $v1 = $v1 === false ? '(not found)' : $v1; + $v2 = $v2 === false ? '(not found)' : $v2; + + # cdbAssert( 'Mismatch', $key, $v1, $v2 ); + $this->cdbAssert( "PHP error", $key, $v1, $value ); + $this->cdbAssert( "DBA error", $key, $v2, $value ); + } + } + + private function randomString() { + $len = mt_rand( 0, 10 ); + $s = ''; + for ( $j = 0; $j < $len; $j++ ) { + $s .= chr( mt_rand( 0, 255 ) ); + } + + return $s; + } + + private function cdbAssert( $msg, $key, $v1, $v2 ) { + $this->assertEquals( + $v2, + $v1, + $msg . ', k=' . bin2hex( $key ) + ); + } +} diff --git a/tests/phpunit/includes/CollationTest.php b/tests/phpunit/includes/CollationTest.php new file mode 100644 index 00000000..43bb3941 --- /dev/null +++ b/tests/phpunit/includes/CollationTest.php @@ -0,0 +1,111 @@ +markTestSkipped( 'These tests require intl extension' ); + } + } + + /** + * Test to make sure, that if you + * have "X" and "XY", the binary + * sortkey also has "X" being a + * prefix of "XY". Our collation + * code makes this assumption. + * + * @param $lang String Language code for collator + * @param $base String Base string + * @param $extended String String containing base as a prefix. + * + * @dataProvider prefixDataProvider + */ + public function testIsPrefix( $lang, $base, $extended ) { + $cp = Collator::create( $lang ); + $cp->setStrength( Collator::PRIMARY ); + $baseBin = $cp->getSortKey( $base ); + // Remove sortkey terminator + $baseBin = rtrim( $baseBin, "\0" ); + $extendedBin = $cp->getSortKey( $extended ); + $this->assertStringStartsWith( $baseBin, $extendedBin, "$base is not a prefix of $extended" ); + } + + function prefixDataProvider() { + return array( + array( 'en', 'A', 'AA' ), + array( 'en', 'A', 'AAA' ), + array( 'en', 'Д', 'ДЂ' ), + array( 'en', 'Д', 'ДA' ), + // 'Ʒ' should expand to 'Z ' (note space). + array( 'fi', 'Z', 'Ʒ' ), + // 'Þ' should expand to 'th' + array( 'sv', 't', 'Þ' ), + // Javanese is a limited use alphabet, so should have 3 bytes + // per character, so do some tests with it. + array( 'en', 'ꦲ', 'ꦲꦤ' ), + array( 'en', 'ꦲ', 'ꦲД' ), + array( 'en', 'A', 'Aꦲ' ), + ); + } + + /** + * Opposite of testIsPrefix + * + * @dataProvider notPrefixDataProvider + */ + public function testNotIsPrefix( $lang, $base, $extended ) { + $cp = Collator::create( $lang ); + $cp->setStrength( Collator::PRIMARY ); + $baseBin = $cp->getSortKey( $base ); + // Remove sortkey terminator + $baseBin = rtrim( $baseBin, "\0" ); + $extendedBin = $cp->getSortKey( $extended ); + $this->assertStringStartsNotWith( $baseBin, $extendedBin, "$base is a prefix of $extended" ); + } + + function notPrefixDataProvider() { + return array( + array( 'en', 'A', 'B' ), + array( 'en', 'AC', 'ABC' ), + array( 'en', 'Z', 'Ʒ' ), + array( 'en', 'A', 'ꦲ' ), + ); + } + + /** + * Test correct first letter is fetched. + * + * @param $collation String Collation name (aka uca-en) + * @param $string String String to get first letter of + * @param $firstLetter String Expected first letter. + * + * @dataProvider firstLetterProvider + */ + public function testGetFirstLetter( $collation, $string, $firstLetter ) { + $col = Collation::factory( $collation ); + $this->assertEquals( $firstLetter, $col->getFirstLetter( $string ) ); + } + + function firstLetterProvider() { + return array( + array( 'uppercase', 'Abc', 'A' ), + array( 'uppercase', 'abc', 'A' ), + array( 'identity', 'abc', 'a' ), + array( 'uca-en', 'abc', 'A' ), + array( 'uca-en', ' ', ' ' ), + array( 'uca-en', 'Êveryone', 'E' ), + array( 'uca-vi', 'Êveryone', 'Ê' ), + // Make sure thorn is not a first letter. + array( 'uca-sv', 'The', 'T' ), + array( 'uca-sv', 'Å', 'Å' ), + array( 'uca-hu', 'dzsdo', 'Dzs' ), + array( 'uca-hu', 'dzdso', 'Dz' ), + array( 'uca-hu', 'CSD', 'Cs' ), + array( 'uca-root', 'CSD', 'C' ), + array( 'uca-fi', 'Ǥ', 'G' ), + array( 'uca-fi', 'Ŧ', 'T' ), + array( 'uca-fi', 'Ʒ', 'Z' ), + array( 'uca-fi', 'Ŋ', 'N' ), + ); + } +} diff --git a/tests/phpunit/includes/DiffHistoryBlobTest.php b/tests/phpunit/includes/DiffHistoryBlobTest.php new file mode 100644 index 00000000..a4d5b91a --- /dev/null +++ b/tests/phpunit/includes/DiffHistoryBlobTest.php @@ -0,0 +1,44 @@ +markTestSkipped( 'The xdiff extension is not available' ); + + return; + } + if ( !function_exists( 'xdiff_string_rabdiff' ) ) { + $this->markTestSkipped( 'The version of xdiff extension is lower than 1.5.0' ); + + return; + } + if ( !extension_loaded( 'hash' ) ) { + $this->markTestSkipped( 'The hash extension is not available' ); + + return; + } + parent::setUp(); + } + + /** + * Test for DiffHistoryBlob::xdiffAdler32() + * @dataProvider provideXdiffAdler32 + */ + public function testXdiffAdler32( $input ) { + $xdiffHash = substr( xdiff_string_rabdiff( $input, '' ), 0, 4 ); + $dhb = new DiffHistoryBlob; + $myHash = $dhb->xdiffAdler32( $input ); + $this->assertSame( bin2hex( $xdiffHash ), bin2hex( $myHash ), + "Hash of " . addcslashes( $input, "\0..\37!@\@\177..\377" ) ); + } + + public static function provideXdiffAdler32() { + return array( + array( '', 'Empty string' ), + array( "\0", 'Null' ), + array( "\0\0\0", "Several nulls" ), + array( "Hello", "An ASCII string" ), + array( str_repeat( "x", 6000 ), "A string larger than xdiff's NMAX (5552)" ) + ); + } +} diff --git a/tests/phpunit/includes/EditPageTest.php b/tests/phpunit/includes/EditPageTest.php new file mode 100644 index 00000000..87272a4c --- /dev/null +++ b/tests/phpunit/includes/EditPageTest.php @@ -0,0 +1,491 @@ +assertEquals( $title, $extracted ); + } + + public static function provideExtractSectionTitle() { + return array( + array( + "== Test ==\n\nJust a test section.", + "Test" + ), + array( + "An initial section, no header.", + false + ), + array( + "An initial section with a fake heder (bug 32617)\n\n== Test == ??\nwtf", + false + ), + array( + "== Section ==\nfollowed by a fake == Non-section == ??\nnoooo", + "Section" + ), + array( + "== Section== \t\r\n followed by whitespace (bug 35051)", + 'Section', + ), + ); + } + + protected function forceRevisionDate( WikiPage $page, $timestamp ) { + $dbw = wfGetDB( DB_MASTER ); + + $dbw->update( 'revision', + array( 'rev_timestamp' => $dbw->timestamp( $timestamp ) ), + array( 'rev_id' => $page->getLatest() ) ); + + $page->clear(); + } + + /** + * User input text is passed to rtrim() by edit page. This is a simple + * wrapper around assertEquals() which calls rrtrim() to normalize the + * expected and actual texts. + */ + function assertEditedTextEquals( $expected, $actual, $msg = '' ) { + return $this->assertEquals( rtrim( $expected ), rtrim( $actual ), $msg ); + } + + /** + * Performs an edit and checks the result. + * + * @param String|Title $title The title of the page to edit + * @param String|null $baseText Some text to create the page with before attempting the edit. + * @param User|String|null $user The user to perform the edit as. + * @param array $edit An array of request parameters used to define the edit to perform. + * Some well known fields are: + * * wpTextbox1: the text to submit + * * wpSummary: the edit summary + * * wpEditToken: the edit token (will be inserted if not provided) + * * wpEdittime: timestamp of the edit's base revision (will be inserted if not provided) + * * wpStarttime: timestamp when the edit started (will be inserted if not provided) + * * wpSectionTitle: the section to edit + * * wpMinorEdit: mark as minor edit + * * wpWatchthis: whether to watch the page + * @param int|null $expectedCode The expected result code (EditPage::AS_XXX constants). + * Set to null to skip the check. Defaults to EditPage::AS_OK. + * @param String|null $expectedText The text expected to be on the page after the edit. + * Set to null to skip the check. + * @param String|null $message An optional message to show along with any error message. + * + * @return WikiPage The page that was just edited, useful for getting the edit's rev_id, etc. + */ + protected function assertEdit( $title, $baseText, $user = null, array $edit, + $expectedCode = EditPage::AS_OK, $expectedText = null, $message = null + ) { + if ( is_string( $title ) ) { + $ns = $this->getDefaultWikitextNS(); + $title = Title::newFromText( $title, $ns ); + } + + if ( is_string( $user ) ) { + $user = User::newFromName( $user ); + + if ( $user->getId() === 0 ) { + $user->addToDatabase(); + } + } + + $page = WikiPage::factory( $title ); + + if ( $baseText !== null ) { + $content = ContentHandler::makeContent( $baseText, $title ); + $page->doEditContent( $content, "base text for test" ); + $this->forceRevisionDate( $page, '20120101000000' ); + + //sanity check + $page->clear(); + $currentText = ContentHandler::getContentText( $page->getContent() ); + + # EditPage rtrim() the user input, so we alter our expected text + # to reflect that. + $this->assertEditedTextEquals( $baseText, $currentText ); + } + + if ( $user == null ) { + $user = $GLOBALS['wgUser']; + } else { + $this->setMwGlobals( 'wgUser', $user ); + } + + if ( !isset( $edit['wpEditToken'] ) ) { + $edit['wpEditToken'] = $user->getEditToken(); + } + + if ( !isset( $edit['wpEdittime'] ) ) { + $edit['wpEdittime'] = $page->exists() ? $page->getTimestamp() : ''; + } + + if ( !isset( $edit['wpStarttime'] ) ) { + $edit['wpStarttime'] = wfTimestampNow(); + } + + $req = new FauxRequest( $edit, true ); // session ?? + + $ep = new EditPage( new Article( $title ) ); + $ep->setContextTitle( $title ); + $ep->importFormData( $req ); + + $bot = isset( $edit['bot'] ) ? (bool)$edit['bot'] : false; + + // this is where the edit happens! + // Note: don't want to use EditPage::AttemptSave, because it messes with $wgOut + // and throws exceptions like PermissionsError + $status = $ep->internalAttemptSave( $result, $bot ); + + if ( $expectedCode !== null ) { + // check edit code + $this->assertEquals( $expectedCode, $status->value, + "Expected result code mismatch. $message" ); + } + + $page = WikiPage::factory( $title ); + + if ( $expectedText !== null ) { + // check resulting page text + $content = $page->getContent(); + $text = ContentHandler::getContentText( $content ); + + # EditPage rtrim() the user input, so we alter our expected text + # to reflect that. + $this->assertEditedTextEquals( $expectedText, $text, + "Expected article text mismatch. $message" ); + } + + return $page; + } + + public function testCreatePage() { + $this->assertEdit( + 'EditPageTest_testCreatePage', + null, + null, + array( + 'wpTextbox1' => "Hello World!", + ), + EditPage::AS_SUCCESS_NEW_ARTICLE, + "Hello World!", + "expected article being created" + )->doDeleteArticleReal( 'EditPageTest_testCreatePage' ); + + $this->assertEdit( + 'EditPageTest_testCreatePage', + null, + null, + array( + 'wpTextbox1' => "", + ), + EditPage::AS_BLANK_ARTICLE, + null, + "expected article not being created if empty" + ); + + + $this->assertEdit( + 'MediaWiki:January', + null, + 'UTSysop', + array( + 'wpTextbox1' => "Not January", + ), + EditPage::AS_SUCCESS_NEW_ARTICLE, + "Not January", + "expected MediaWiki: page being created" + )->doDeleteArticleReal( 'EditPageTest_testCreatePage' ); + + $this->assertEdit( + 'MediaWiki:EditPageTest_testCreatePage', + null, + 'UTSysop', + array( + 'wpTextbox1' => "", + ), + EditPage::AS_BLANK_ARTICLE, + null, + "expected not-registered MediaWiki: page not being created if empty" + ); + + $this->assertEdit( + 'MediaWiki:January', + null, + 'UTSysop', + array( + 'wpTextbox1' => "", + ), + EditPage::AS_SUCCESS_NEW_ARTICLE, + "", + "expected registered MediaWiki: page being created even if empty" + )->doDeleteArticleReal( 'EditPageTest_testCreatePage' ); + + $this->assertEdit( + 'MediaWiki:Ipb-default-expiry', + null, + 'UTSysop', + array( + 'wpTextbox1' => "", + ), + EditPage::AS_BLANK_ARTICLE, + "", + "expected registered MediaWiki: page whose default content is empty not being created if empty" + ); + + $this->assertEdit( + 'MediaWiki:January', + null, + 'UTSysop', + array( + 'wpTextbox1' => "January", + ), + EditPage::AS_BLANK_ARTICLE, + null, + "expected MediaWiki: page not being created if text equals default message" + ); + } + + public function testUpdatePage() { + $text = "one"; + $edit = array( + 'wpTextbox1' => $text, + 'wpSummary' => 'first update', + ); + + $page = $this->assertEdit( 'EditPageTest_testUpdatePage', "zero", null, $edit, + EditPage::AS_SUCCESS_UPDATE, $text, + "expected successfull update with given text" ); + + $this->forceRevisionDate( $page, '20120101000000' ); + + $text = "two"; + $edit = array( + 'wpTextbox1' => $text, + 'wpSummary' => 'second update', + ); + + $this->assertEdit( 'EditPageTest_testUpdatePage', null, null, $edit, + EditPage::AS_SUCCESS_UPDATE, $text, + "expected successfull update with given text" ); + } + + public static function provideSectionEdit() { + $text = 'Intro + +== one == +first section. + +== two == +second section. +'; + + $sectionOne = '== one == +hello +'; + + $newSection = '== new section == + +hello +'; + + $textWithNewSectionOne = preg_replace( + '/== one ==.*== two ==/ms', + "$sectionOne\n== two ==", $text + ); + + $textWithNewSectionAdded = "$text\n$newSection"; + + return array( + array( #0 + $text, + '', + 'hello', + 'replace all', + 'hello' + ), + + array( #1 + $text, + '1', + $sectionOne, + 'replace first section', + $textWithNewSectionOne, + ), + + array( #2 + $text, + 'new', + 'hello', + 'new section', + $textWithNewSectionAdded, + ), + ); + } + + /** + * @dataProvider provideSectionEdit + */ + public function testSectionEdit( $base, $section, $text, $summary, $expected ) { + $edit = array( + 'wpTextbox1' => $text, + 'wpSummary' => $summary, + 'wpSection' => $section, + ); + + $this->assertEdit( 'EditPageTest_testSectionEdit', $base, null, $edit, + EditPage::AS_SUCCESS_UPDATE, $expected, + "expected successfull update of section" ); + } + + public static function provideAutoMerge() { + $tests = array(); + + $tests[] = array( #0: plain conflict + "Elmo", # base edit user + "one\n\ntwo\n\nthree\n", + array( #adam's edit + 'wpStarttime' => 1, + 'wpTextbox1' => "ONE\n\ntwo\n\nthree\n", + ), + array( #berta's edit + 'wpStarttime' => 2, + 'wpTextbox1' => "(one)\n\ntwo\n\nthree\n", + ), + EditPage::AS_CONFLICT_DETECTED, # expected code + "ONE\n\ntwo\n\nthree\n", # expected text + 'expected edit conflict', # message + ); + + $tests[] = array( #1: successful merge + "Elmo", # base edit user + "one\n\ntwo\n\nthree\n", + array( #adam's edit + 'wpStarttime' => 1, + 'wpTextbox1' => "ONE\n\ntwo\n\nthree\n", + ), + array( #berta's edit + 'wpStarttime' => 2, + 'wpTextbox1' => "one\n\ntwo\n\nTHREE\n", + ), + EditPage::AS_SUCCESS_UPDATE, # expected code + "ONE\n\ntwo\n\nTHREE\n", # expected text + 'expected automatic merge', # message + ); + + $text = "Intro\n\n"; + $text .= "== first section ==\n\n"; + $text .= "one\n\ntwo\n\nthree\n\n"; + $text .= "== second section ==\n\n"; + $text .= "four\n\nfive\n\nsix\n\n"; + + // extract the first section. + $section = preg_replace( '/.*(== first section ==.*)== second section ==.*/sm', '$1', $text ); + + // generate expected text after merge + $expected = str_replace( 'one', 'ONE', str_replace( 'three', 'THREE', $text ) ); + + $tests[] = array( #2: merge in section + "Elmo", # base edit user + $text, + array( #adam's edit + 'wpStarttime' => 1, + 'wpTextbox1' => str_replace( 'one', 'ONE', $section ), + 'wpSection' => '1' + ), + array( #berta's edit + 'wpStarttime' => 2, + 'wpTextbox1' => str_replace( 'three', 'THREE', $section ), + 'wpSection' => '1' + ), + EditPage::AS_SUCCESS_UPDATE, # expected code + $expected, # expected text + 'expected automatic section merge', # message + ); + + // see whether it makes a difference who did the base edit + $testsWithAdam = array_map( function ( $test ) { + $test[0] = 'Adam'; // change base edit user + return $test; + }, $tests ); + + $testsWithBerta = array_map( function ( $test ) { + $test[0] = 'Berta'; // change base edit user + return $test; + }, $tests ); + + return array_merge( $tests, $testsWithAdam, $testsWithBerta ); + } + + /** + * @dataProvider provideAutoMerge + */ + public function testAutoMerge( $baseUser, $text, $adamsEdit, $bertasEdit, + $expectedCode, $expectedText, $message = null + ) { + $this->checkHasDiff3(); + + //create page + $ns = $this->getDefaultWikitextNS(); + $title = Title::newFromText( 'EditPageTest_testAutoMerge', $ns ); + $page = WikiPage::factory( $title ); + + if ( $page->exists() ) { + $page->doDeleteArticle( "clean slate for testing" ); + } + + $baseEdit = array( + 'wpTextbox1' => $text, + ); + + $page = $this->assertEdit( 'EditPageTest_testAutoMerge', null, + $baseUser, $baseEdit, null, null, __METHOD__ ); + + $this->forceRevisionDate( $page, '20120101000000' ); + + $edittime = $page->getTimestamp(); + + // start timestamps for conflict detection + if ( !isset( $adamsEdit['wpStarttime'] ) ) { + $adamsEdit['wpStarttime'] = 1; + } + + if ( !isset( $bertasEdit['wpStarttime'] ) ) { + $bertasEdit['wpStarttime'] = 2; + } + + $starttime = wfTimestampNow(); + $adamsTime = wfTimestamp( TS_MW, (int)wfTimestamp( TS_UNIX, $starttime ) + (int)$adamsEdit['wpStarttime'] ); + $bertasTime = wfTimestamp( TS_MW, (int)wfTimestamp( TS_UNIX, $starttime ) + (int)$bertasEdit['wpStarttime'] ); + + $adamsEdit['wpStarttime'] = $adamsTime; + $bertasEdit['wpStarttime'] = $bertasTime; + + $adamsEdit['wpSummary'] = 'Adam\'s edit'; + $bertasEdit['wpSummary'] = 'Bertas\'s edit'; + + $adamsEdit['wpEdittime'] = $edittime; + $bertasEdit['wpEdittime'] = $edittime; + + // first edit + $this->assertEdit( 'EditPageTest_testAutoMerge', null, 'Adam', $adamsEdit, + EditPage::AS_SUCCESS_UPDATE, null, "expected successfull update" ); + + // second edit + $this->assertEdit( 'EditPageTest_testAutoMerge', null, 'Berta', $bertasEdit, + $expectedCode, $expectedText, $message ); + } +} diff --git a/tests/phpunit/includes/ExternalStoreTest.php b/tests/phpunit/includes/ExternalStoreTest.php new file mode 100644 index 00000000..fcffcbc2 --- /dev/null +++ b/tests/phpunit/includes/ExternalStoreTest.php @@ -0,0 +1,81 @@ +setMwGlobals( 'wgExternalStores', false ); + + $this->assertFalse( + ExternalStore::fetchFromURL( 'FOO://cluster1/200' ), + 'Deny if wgExternalStores is not set to a non-empty array' + ); + + $this->setMwGlobals( 'wgExternalStores', array( 'FOO' ) ); + + $this->assertEquals( + ExternalStore::fetchFromURL( 'FOO://cluster1/200' ), + 'Hello', + 'Allow FOO://cluster1/200' + ); + $this->assertEquals( + ExternalStore::fetchFromURL( 'FOO://cluster1/300/0' ), + 'Hello', + 'Allow FOO://cluster1/300/0' + ); + # Assertions for r68900 + $this->assertFalse( + ExternalStore::fetchFromURL( 'ftp.example.org' ), + 'Deny domain ftp.example.org' + ); + $this->assertFalse( + ExternalStore::fetchFromURL( '/example.txt' ), + 'Deny path /example.txt' + ); + $this->assertFalse( + ExternalStore::fetchFromURL( 'http://' ), + 'Deny protocol http://' + ); + } +} + +class ExternalStoreFOO { + + protected $data = array( + 'cluster1' => array( + '200' => 'Hello', + '300' => array( + 'Hello', 'World', + ), + ), + ); + + /** + * Fetch data from given URL + * @param $url String: an url of the form FOO://cluster/id or FOO://cluster/id/itemid. + * @return mixed + */ + function fetchFromURL( $url ) { + // Based on ExternalStoreDB + $path = explode( '/', $url ); + $cluster = $path[2]; + $id = $path[3]; + if ( isset( $path[4] ) ) { + $itemID = $path[4]; + } else { + $itemID = false; + } + + if ( !isset( $this->data[$cluster][$id] ) ) { + return null; + } + + if ( $itemID !== false && is_array( $this->data[$cluster][$id] ) && isset( $this->data[$cluster][$id][$itemID] ) ) { + return $this->data[$cluster][$id][$itemID]; + } + + return $this->data[$cluster][$id]; + } +} diff --git a/tests/phpunit/includes/ExtraParserTest.php b/tests/phpunit/includes/ExtraParserTest.php new file mode 100644 index 00000000..6c67beb1 --- /dev/null +++ b/tests/phpunit/includes/ExtraParserTest.php @@ -0,0 +1,157 @@ +setMwGlobals( array( + 'wgShowDBErrorBacktrace' => true, + 'wgLanguageCode' => 'en', + 'wgContLang' => $contLang, + 'wgLang' => Language::factory( 'en' ), + 'wgMemc' => new EmptyBagOStuff, + 'wgAlwaysUseTidy' => false, + 'wgCleanSignatures' => true, + ) ); + + $this->options = ParserOptions::newFromUserAndLang( new User, $contLang ); + $this->options->setTemplateCallback( array( __CLASS__, 'statelessFetchTemplate' ) ); + $this->parser = new Parser; + + MagicWord::clearCache(); + } + + // Bug 8689 - Long numeric lines kill the parser + public function testBug8689() { + global $wgUser; + $longLine = '1.' . str_repeat( '1234567890', 100000 ) . "\n"; + + $t = Title::newFromText( 'Unit test' ); + $options = ParserOptions::newFromUser( $wgUser ); + $this->assertEquals( "

$longLine

", + $this->parser->parse( $longLine, $t, $options )->getText() ); + } + + /* Test the parser entry points */ + public function testParse() { + $title = Title::newFromText( __FUNCTION__ ); + $parserOutput = $this->parser->parse( "Test\n{{Foo}}\n{{Bar}}", $title, $this->options ); + $this->assertEquals( "

Test\nContent of Template:Foo\nContent of Template:Bar\n

", $parserOutput->getText() ); + } + + public function testPreSaveTransform() { + global $wgUser; + $title = Title::newFromText( __FUNCTION__ ); + $outputText = $this->parser->preSaveTransform( "Test\r\n{{subst:Foo}}\n{{Bar}}", $title, $wgUser, $this->options ); + + $this->assertEquals( "Test\nContent of ''Template:Foo''\n{{Bar}}", $outputText ); + } + + public function testPreprocess() { + $title = Title::newFromText( __FUNCTION__ ); + $outputText = $this->parser->preprocess( "Test\n{{Foo}}\n{{Bar}}", $title, $this->options ); + + $this->assertEquals( "Test\nContent of ''Template:Foo''\nContent of ''Template:Bar''", $outputText ); + } + + /** + * cleanSig() makes all templates substs and removes tildes + */ + public function testCleanSig() { + $title = Title::newFromText( __FUNCTION__ ); + $outputText = $this->parser->cleanSig( "{{Foo}} ~~~~" ); + + $this->assertEquals( "{{SUBST:Foo}} ", $outputText ); + } + + /** + * cleanSig() should do nothing if disabled + */ + public function testCleanSigDisabled() { + $this->setMwGlobals( 'wgCleanSignatures', false ); + + $title = Title::newFromText( __FUNCTION__ ); + $outputText = $this->parser->cleanSig( "{{Foo}} ~~~~" ); + + $this->assertEquals( "{{Foo}} ~~~~", $outputText ); + } + + /** + * cleanSigInSig() just removes tildes + * @dataProvider provideStringsForCleanSigInSig + */ + public function testCleanSigInSig( $in, $out ) { + $this->assertEquals( Parser::cleanSigInSig( $in ), $out ); + } + + public static function provideStringsForCleanSigInSig() { + return array( + array( "{{Foo}} ~~~~", "{{Foo}} " ), + array( "~~~", "" ), + array( "~~~~~", "" ), + ); + } + + public function testGetSection() { + $outputText2 = $this->parser->getSection( "Section 0\n== Heading 1 ==\nSection 1\n=== Heading 2 ===\nSection 2\n== Heading 3 ==\nSection 3\n", 2 ); + $outputText1 = $this->parser->getSection( "Section 0\n== Heading 1 ==\nSection 1\n=== Heading 2 ===\nSection 2\n== Heading 3 ==\nSection 3\n", 1 ); + + $this->assertEquals( "=== Heading 2 ===\nSection 2", $outputText2 ); + $this->assertEquals( "== Heading 1 ==\nSection 1\n=== Heading 2 ===\nSection 2", $outputText1 ); + } + + public function testReplaceSection() { + $outputText = $this->parser->replaceSection( "Section 0\n== Heading 1 ==\nSection 1\n=== Heading 2 ===\nSection 2\n== Heading 3 ==\nSection 3\n", 1, "New section 1" ); + + $this->assertEquals( "Section 0\nNew section 1\n\n== Heading 3 ==\nSection 3", $outputText ); + } + + /** + * Templates and comments are not affected, but noinclude/onlyinclude is. + */ + public function testGetPreloadText() { + $title = Title::newFromText( __FUNCTION__ ); + $outputText = $this->parser->getPreloadText( "{{Foo}} censored information ", $title, $this->options ); + + $this->assertEquals( "{{Foo}} information ", $outputText ); + } + + static function statelessFetchTemplate( $title, $parser = false ) { + $text = "Content of ''" . $title->getFullText() . "''"; + $deps = array(); + + return array( + 'text' => $text, + 'finalTitle' => $title, + 'deps' => $deps ); + } + + /** + * @group Database + */ + public function testTrackingCategory() { + $title = Title::newFromText( __FUNCTION__ ); + $catName = wfMessage( 'broken-file-category' )->inContentLanguage()->text(); + $cat = Title::makeTitleSafe( NS_CATEGORY, $catName ); + $expected = array( $cat->getDBkey() ); + $parserOutput = $this->parser->parse( "[[file:nonexistent]]", $title, $this->options ); + $result = $parserOutput->getCategoryLinks(); + $this->assertEquals( $expected, $result ); + } + + /** + * @group Database + */ + public function testTrackingCategorySpecial() { + // Special pages shouldn't have tracking cats. + $title = SpecialPage::getTitleFor( 'Contributions' ); + $parserOutput = $this->parser->parse( "[[file:nonexistent]]", $title, $this->options ); + $result = $parserOutput->getCategoryLinks(); + $this->assertEmpty( $result ); + } +} diff --git a/tests/phpunit/includes/FallbackTest.php b/tests/phpunit/includes/FallbackTest.php new file mode 100644 index 00000000..f408f471 --- /dev/null +++ b/tests/phpunit/includes/FallbackTest.php @@ -0,0 +1,73 @@ +markTestSkipped( "The mb_string functions must be installed to test the fallback functions" ); + } + + $sampleUTF = "Östergötland_coat_of_arms.png"; + + //mb_substr + $substr_params = array( + array( 0, 0 ), + array( 5, -4 ), + array( 33 ), + array( 100, -5 ), + array( -8, 10 ), + array( 1, 1 ), + array( 2, -1 ) + ); + + foreach ( $substr_params as $param_set ) { + $old_param_set = $param_set; + array_unshift( $param_set, $sampleUTF ); + + $this->assertEquals( + call_user_func_array( 'mb_substr', $param_set ), + call_user_func_array( 'Fallback::mb_substr', $param_set ), + 'Fallback mb_substr with params ' . implode( ', ', $old_param_set ) + ); + } + + //mb_strlen + $this->assertEquals( + mb_strlen( $sampleUTF ), + Fallback::mb_strlen( $sampleUTF ), + 'Fallback mb_strlen' + ); + + //mb_str(r?)pos + $strpos_params = array( + //array( 'ter' ), + //array( 'Ö' ), + //array( 'Ö', 3 ), + //array( 'oat_', 100 ), + //array( 'c', -10 ), + //Broken for now + ); + + foreach ( $strpos_params as $param_set ) { + $old_param_set = $param_set; + array_unshift( $param_set, $sampleUTF ); + + $this->assertEquals( + call_user_func_array( 'mb_strpos', $param_set ), + call_user_func_array( 'Fallback::mb_strpos', $param_set ), + 'Fallback mb_strpos with params ' . implode( ', ', $old_param_set ) + ); + + $this->assertEquals( + call_user_func_array( 'mb_strrpos', $param_set ), + call_user_func_array( 'Fallback::mb_strrpos', $param_set ), + 'Fallback mb_strrpos with params ' . implode( ', ', $old_param_set ) + ); + } + } + +} \ No newline at end of file diff --git a/tests/phpunit/includes/FauxRequestTest.php b/tests/phpunit/includes/FauxRequestTest.php new file mode 100644 index 00000000..9f3aa11d --- /dev/null +++ b/tests/phpunit/includes/FauxRequestTest.php @@ -0,0 +1,15 @@ +setHeader( 'Content-Type', $value ); + + $this->assertEquals( $request->getHeader( 'Content-Type' ), $value ); + $this->assertEquals( $request->getHeader( 'CONTENT-TYPE' ), $value ); + $this->assertEquals( $request->getHeader( 'content-type' ), $value ); + } +} diff --git a/tests/phpunit/includes/FauxResponseTest.php b/tests/phpunit/includes/FauxResponseTest.php new file mode 100644 index 00000000..f9ba1b3b --- /dev/null +++ b/tests/phpunit/includes/FauxResponseTest.php @@ -0,0 +1,74 @@ +response = new FauxResponse; + } + + public function testCookie() { + $this->assertEquals( null, $this->response->getcookie( 'key' ), 'Non-existing cookie' ); + $this->response->setcookie( 'key', 'val' ); + $this->assertEquals( 'val', $this->response->getcookie( 'key' ), 'Existing cookie' ); + } + + public function testHeader() { + $this->assertEquals( null, $this->response->getheader( 'Location' ), 'Non-existing header' ); + + $this->response->header( 'Location: http://localhost/' ); + $this->assertEquals( 'http://localhost/', $this->response->getheader( 'Location' ), 'Set header' ); + + $this->response->header( 'Location: http://127.0.0.1/' ); + $this->assertEquals( 'http://127.0.0.1/', $this->response->getheader( 'Location' ), 'Same header' ); + + $this->response->header( 'Location: http://127.0.0.2/', false ); + $this->assertEquals( 'http://127.0.0.1/', $this->response->getheader( 'Location' ), 'Same header with override disabled' ); + + $this->response->header( 'Location: http://localhost/' ); + $this->assertEquals( 'http://localhost/', $this->response->getheader( 'LOCATION' ), 'Get header case insensitive' ); + } + + public function testResponseCode() { + $this->response->header( 'HTTP/1.1 200' ); + $this->assertEquals( 200, $this->response->getStatusCode(), 'Header with no message' ); + + $this->response->header( 'HTTP/1.x 201' ); + $this->assertEquals( 201, $this->response->getStatusCode(), 'Header with no message and protocol 1.x' ); + + $this->response->header( 'HTTP/1.1 202 OK' ); + $this->assertEquals( 202, $this->response->getStatusCode(), 'Normal header' ); + + $this->response->header( 'HTTP/1.x 203 OK' ); + $this->assertEquals( 203, $this->response->getStatusCode(), 'Normal header with no message and protocol 1.x' ); + + $this->response->header( 'HTTP/1.x 204 OK', false, 205 ); + $this->assertEquals( 205, $this->response->getStatusCode(), 'Third parameter overrides the HTTP/... header' ); + + $this->response->header( 'Location: http://localhost/', false, 206 ); + $this->assertEquals( 206, $this->response->getStatusCode(), 'Third parameter with another header' ); + } +} diff --git a/tests/phpunit/includes/FormOptionsInitializationTest.php b/tests/phpunit/includes/FormOptionsInitializationTest.php new file mode 100644 index 00000000..fb2304dc --- /dev/null +++ b/tests/phpunit/includes/FormOptionsInitializationTest.php @@ -0,0 +1,84 @@ +options; + } +} + +/** + * Test class for FormOptions initialization + * Ensure the FormOptions::add() does what we want it to do. + * + * Generated by PHPUnit on 2011-02-28 at 20:46:27. + * + * Copyright © 2011, Antoine Musso + * + * @author Antoine Musso + */ +class FormOptionsInitializationTest extends MediaWikiTestCase { + /** + * @var FormOptions + */ + protected $object; + + + /** + * A new fresh and empty FormOptions object to test initialization + * with. + */ + protected function setUp() { + parent::setUp(); + $this->object = new FormOptionsExposed(); + } + + public function testAddStringOption() { + $this->object->add( 'foo', 'string value' ); + $this->assertEquals( + array( + 'foo' => array( + 'default' => 'string value', + 'consumed' => false, + 'type' => FormOptions::STRING, + 'value' => null, + ) + ), + $this->object->getOptions() + ); + } + + public function testAddIntegers() { + $this->object->add( 'one', 1 ); + $this->object->add( 'negone', -1 ); + $this->assertEquals( + array( + 'negone' => array( + 'default' => -1, + 'value' => null, + 'consumed' => false, + 'type' => FormOptions::INT, + ), + 'one' => array( + 'default' => 1, + 'value' => null, + 'consumed' => false, + 'type' => FormOptions::INT, + ) + ), + $this->object->getOptions() + ); + } +} diff --git a/tests/phpunit/includes/FormOptionsTest.php b/tests/phpunit/includes/FormOptionsTest.php new file mode 100644 index 00000000..0a13cfec --- /dev/null +++ b/tests/phpunit/includes/FormOptionsTest.php @@ -0,0 +1,91 @@ +object = new FormOptions; + $this->object->add( 'string1', 'string one' ); + $this->object->add( 'string2', 'string two' ); + $this->object->add( 'integer', 0 ); + $this->object->add( 'intnull', 0, FormOptions::INTNULL ); + } + + /** Helpers for testGuessType() */ + /* @{ */ + private function assertGuessBoolean( $data ) { + $this->guess( FormOptions::BOOL, $data ); + } + private function assertGuessInt( $data ) { + $this->guess( FormOptions::INT, $data ); + } + private function assertGuessString( $data ) { + $this->guess( FormOptions::STRING, $data ); + } + + /** Generic helper */ + private function guess( $expected, $data ) { + $this->assertEquals( + $expected, + FormOptions::guessType( $data ) + ); + } + /* @} */ + + /** + * Reuse helpers above assertGuessBoolean assertGuessInt assertGuessString + */ + public function testGuessTypeDetection() { + $this->assertGuessBoolean( true ); + $this->assertGuessBoolean( false ); + + $this->assertGuessInt( 0 ); + $this->assertGuessInt( -5 ); + $this->assertGuessInt( 5 ); + $this->assertGuessInt( 0x0F ); + + $this->assertGuessString( 'true' ); + $this->assertGuessString( 'false' ); + $this->assertGuessString( '5' ); + $this->assertGuessString( '0' ); + } + + /** + * @expectedException MWException + */ + public function testGuessTypeOnArrayThrowException() { + $this->object->guessType( array( 'foo' ) ); + } + /** + * @expectedException MWException + */ + public function testGuessTypeOnNullThrowException() { + $this->object->guessType( null ); + } +} diff --git a/tests/phpunit/includes/GlobalFunctions/GlobalTest.php b/tests/phpunit/includes/GlobalFunctions/GlobalTest.php new file mode 100644 index 00000000..6154df1d --- /dev/null +++ b/tests/phpunit/includes/GlobalFunctions/GlobalTest.php @@ -0,0 +1,652 @@ +setMwGlobals( array( + 'wgReadOnlyFile' => $readOnlyFile, + 'wgUrlProtocols' => array( + 'http://', + 'https://', + 'mailto:', + '//', + 'file://', # Non-default + ), + ) ); + } + + protected function tearDown() { + global $wgReadOnlyFile; + + if ( file_exists( $wgReadOnlyFile ) ) { + unlink( $wgReadOnlyFile ); + } + + parent::tearDown(); + } + + /** + * @dataProvider provideForWfArrayDiff2 + * @covers ::wfArrayDiff2 + */ + public function testWfArrayDiff2( $a, $b, $expected ) { + $this->assertEquals( + wfArrayDiff2( $a, $b ), $expected + ); + } + + // @todo Provide more tests + public static function provideForWfArrayDiff2() { + // $a $b $expected + return array( + array( + array( 'a', 'b' ), + array( 'a', 'b' ), + array(), + ), + array( + array( array( 'a' ), array( 'a', 'b', 'c' ) ), + array( array( 'a' ), array( 'a', 'b' ) ), + array( 1 => array( 'a', 'b', 'c' ) ), + ), + ); + } + + /** + * @covers ::wfRandom + */ + public function testRandom() { + # This could hypothetically fail, but it shouldn't ;) + $this->assertFalse( + wfRandom() == wfRandom() ); + } + + /** + * @covers ::wfUrlencode + */ + public function testUrlencode() { + $this->assertEquals( + "%E7%89%B9%E5%88%A5:Contributions/Foobar", + wfUrlencode( "\xE7\x89\xB9\xE5\x88\xA5:Contributions/Foobar" ) ); + } + + /** + * @covers ::wfExpandIRI + */ + public function testExpandIRI() { + $this->assertEquals( + "https://te.wikibooks.org/wiki/ఉబుంటు_వాడుకరి_మార్గదర్శని", + wfExpandIRI( "https://te.wikibooks.org/wiki/%E0%B0%89%E0%B0%AC%E0%B1%81%E0%B0%82%E0%B0%9F%E0%B1%81_%E0%B0%B5%E0%B0%BE%E0%B0%A1%E0%B1%81%E0%B0%95%E0%B0%B0%E0%B0%BF_%E0%B0%AE%E0%B0%BE%E0%B0%B0%E0%B1%8D%E0%B0%97%E0%B0%A6%E0%B0%B0%E0%B1%8D%E0%B0%B6%E0%B0%A8%E0%B0%BF" ) ); + } + + /** + * @covers ::wfReadOnly + */ + public function testReadOnlyEmpty() { + global $wgReadOnly; + $wgReadOnly = null; + + $this->assertFalse( wfReadOnly() ); + $this->assertFalse( wfReadOnly() ); + } + + /** + * @covers ::wfReadOnly + */ + public function testReadOnlySet() { + global $wgReadOnly, $wgReadOnlyFile; + + $f = fopen( $wgReadOnlyFile, "wt" ); + fwrite( $f, 'Message' ); + fclose( $f ); + $wgReadOnly = null; # Check on $wgReadOnlyFile + + $this->assertTrue( wfReadOnly() ); + $this->assertTrue( wfReadOnly() ); # Check cached + + unlink( $wgReadOnlyFile ); + $wgReadOnly = null; # Clean cache + + $this->assertFalse( wfReadOnly() ); + $this->assertFalse( wfReadOnly() ); + } + + public static function provideArrayToCGI() { + return array( + array( array(), '' ), // empty + array( array( 'foo' => 'bar' ), 'foo=bar' ), // string test + array( array( 'foo' => '' ), 'foo=' ), // empty string test + array( array( 'foo' => 1 ), 'foo=1' ), // number test + array( array( 'foo' => true ), 'foo=1' ), // true test + array( array( 'foo' => false ), '' ), // false test + array( array( 'foo' => null ), '' ), // null test + array( array( 'foo' => 'A&B=5+6@!"\'' ), 'foo=A%26B%3D5%2B6%40%21%22%27' ), // urlencoding test + array( array( 'foo' => 'bar', 'baz' => 'is', 'asdf' => 'qwerty' ), 'foo=bar&baz=is&asdf=qwerty' ), // multi-item test + array( array( 'foo' => array( 'bar' => 'baz' ) ), 'foo%5Bbar%5D=baz' ), + array( array( 'foo' => array( 'bar' => 'baz', 'qwerty' => 'asdf' ) ), 'foo%5Bbar%5D=baz&foo%5Bqwerty%5D=asdf' ), + array( array( 'foo' => array( 'bar', 'baz' ) ), 'foo%5B0%5D=bar&foo%5B1%5D=baz' ), + array( array( 'foo' => array( 'bar' => array( 'bar' => 'baz' ) ) ), 'foo%5Bbar%5D%5Bbar%5D=baz' ), + ); + } + + /** + * @dataProvider provideArrayToCGI + * @covers ::wfArrayToCgi + */ + public function testArrayToCGI( $array, $result ) { + $this->assertEquals( $result, wfArrayToCgi( $array ) ); + } + + + /** + * @covers ::testWfArrayDiff2 + */ + public function testArrayToCGI2() { + $this->assertEquals( + "baz=bar&foo=bar", + wfArrayToCgi( + array( 'baz' => 'bar' ), + array( 'foo' => 'bar', 'baz' => 'overridden value' ) ) ); + } + + public static function provideCgiToArray() { + return array( + array( '', array() ), // empty + array( 'foo=bar', array( 'foo' => 'bar' ) ), // string + array( 'foo=', array( 'foo' => '' ) ), // empty string + array( 'foo', array( 'foo' => '' ) ), // missing = + array( 'foo=bar&qwerty=asdf', array( 'foo' => 'bar', 'qwerty' => 'asdf' ) ), // multiple value + array( 'foo=A%26B%3D5%2B6%40%21%22%27', array( 'foo' => 'A&B=5+6@!"\'' ) ), // urldecoding test + array( 'foo%5Bbar%5D=baz', array( 'foo' => array( 'bar' => 'baz' ) ) ), + array( 'foo%5Bbar%5D=baz&foo%5Bqwerty%5D=asdf', array( 'foo' => array( 'bar' => 'baz', 'qwerty' => 'asdf' ) ) ), + array( 'foo%5B0%5D=bar&foo%5B1%5D=baz', array( 'foo' => array( 0 => 'bar', 1 => 'baz' ) ) ), + array( 'foo%5Bbar%5D%5Bbar%5D=baz', array( 'foo' => array( 'bar' => array( 'bar' => 'baz' ) ) ) ), + ); + } + + /** + * @dataProvider provideCgiToArray + * @covers ::wfCgiToArray + */ + public function testCgiToArray( $cgi, $result ) { + $this->assertEquals( $result, wfCgiToArray( $cgi ) ); + } + + public static function provideCgiRoundTrip() { + return array( + array( '' ), + array( 'foo=bar' ), + array( 'foo=' ), + array( 'foo=bar&baz=biz' ), + array( 'foo=A%26B%3D5%2B6%40%21%22%27' ), + array( 'foo%5Bbar%5D=baz' ), + array( 'foo%5B0%5D=bar&foo%5B1%5D=baz' ), + array( 'foo%5Bbar%5D%5Bbar%5D=baz' ), + ); + } + + /** + * @dataProvider provideCgiRoundTrip + * @covers ::wfArrayToCgi + */ + public function testCgiRoundTrip( $cgi ) { + $this->assertEquals( $cgi, wfArrayToCgi( wfCgiToArray( $cgi ) ) ); + } + + /** + * @covers ::mimeTypeMatch + */ + public function testMimeTypeMatch() { + $this->assertEquals( + 'text/html', + mimeTypeMatch( 'text/html', + array( 'application/xhtml+xml' => 1.0, + 'text/html' => 0.7, + 'text/plain' => 0.3 ) ) ); + $this->assertEquals( + 'text/*', + mimeTypeMatch( 'text/html', + array( 'image/*' => 1.0, + 'text/*' => 0.5 ) ) ); + $this->assertEquals( + '*/*', + mimeTypeMatch( 'text/html', + array( '*/*' => 1.0 ) ) ); + $this->assertNull( + mimeTypeMatch( 'text/html', + array( 'image/png' => 1.0, + 'image/svg+xml' => 0.5 ) ) ); + } + + /** + * @covers ::wfNegotiateType + */ + public function testNegotiateType() { + $this->assertEquals( + 'text/html', + wfNegotiateType( + array( 'application/xhtml+xml' => 1.0, + 'text/html' => 0.7, + 'text/plain' => 0.5, + 'text/*' => 0.2 ), + array( 'text/html' => 1.0 ) ) ); + $this->assertEquals( + 'application/xhtml+xml', + wfNegotiateType( + array( 'application/xhtml+xml' => 1.0, + 'text/html' => 0.7, + 'text/plain' => 0.5, + 'text/*' => 0.2 ), + array( 'application/xhtml+xml' => 1.0, + 'text/html' => 0.5 ) ) ); + $this->assertEquals( + 'text/html', + wfNegotiateType( + array( 'text/html' => 1.0, + 'text/plain' => 0.5, + 'text/*' => 0.5, + 'application/xhtml+xml' => 0.2 ), + array( 'application/xhtml+xml' => 1.0, + 'text/html' => 0.5 ) ) ); + $this->assertEquals( + 'text/html', + wfNegotiateType( + array( 'text/*' => 1.0, + 'image/*' => 0.7, + '*/*' => 0.3 ), + array( 'application/xhtml+xml' => 1.0, + 'text/html' => 0.5 ) ) ); + $this->assertNull( + wfNegotiateType( + array( 'text/*' => 1.0 ), + array( 'application/xhtml+xml' => 1.0 ) ) ); + } + + /** + * @covers ::wfDebug + * @covers ::wfDebugMem + */ + public function testDebugFunctionTest() { + + global $wgDebugLogFile, $wgDebugTimestamps; + + $old_log_file = $wgDebugLogFile; + $wgDebugLogFile = tempnam( wfTempDir(), 'mw-' ); + # @todo FIXME: $wgDebugTimestamps should be tested + $old_wgDebugTimestamps = $wgDebugTimestamps; + $wgDebugTimestamps = false; + + wfDebug( "This is a normal string" ); + $this->assertEquals( "This is a normal string", file_get_contents( $wgDebugLogFile ) ); + unlink( $wgDebugLogFile ); + + wfDebug( "This is nöt an ASCII string" ); + $this->assertEquals( "This is nöt an ASCII string", file_get_contents( $wgDebugLogFile ) ); + unlink( $wgDebugLogFile ); + + wfDebug( "\00305This has böth UTF and control chars\003" ); + $this->assertEquals( " 05This has böth UTF and control chars ", file_get_contents( $wgDebugLogFile ) ); + unlink( $wgDebugLogFile ); + + wfDebugMem(); + $this->assertGreaterThan( 5000, preg_replace( '/\D/', '', file_get_contents( $wgDebugLogFile ) ) ); + unlink( $wgDebugLogFile ); + + wfDebugMem( true ); + $this->assertGreaterThan( 5000000, preg_replace( '/\D/', '', file_get_contents( $wgDebugLogFile ) ) ); + unlink( $wgDebugLogFile ); + + $wgDebugLogFile = $old_log_file; + $wgDebugTimestamps = $old_wgDebugTimestamps; + } + + /** + * @covers ::wfClientAcceptsGzip + */ + public function testClientAcceptsGzipTest() { + + $settings = array( + 'gzip' => true, + 'bzip' => false, + '*' => false, + 'compress, gzip' => true, + 'gzip;q=1.0' => true, + 'foozip' => false, + 'foo*zip' => false, + 'gzip;q=abcde' => true, //is this REALLY valid? + 'gzip;q=12345678.9' => true, + ' gzip' => true, + ); + + if ( isset( $_SERVER['HTTP_ACCEPT_ENCODING'] ) ) { + $old_server_setting = $_SERVER['HTTP_ACCEPT_ENCODING']; + } + + foreach ( $settings as $encoding => $expect ) { + $_SERVER['HTTP_ACCEPT_ENCODING'] = $encoding; + + $this->assertEquals( $expect, wfClientAcceptsGzip( true ), + "'$encoding' => " . wfBoolToStr( $expect ) ); + } + + if ( isset( $old_server_setting ) ) { + $_SERVER['HTTP_ACCEPT_ENCODING'] = $old_server_setting; + } + } + + /** + * @covers ::swap + */ + public function testSwapVarsTest() { + $var1 = 1; + $var2 = 2; + + $this->assertEquals( $var1, 1, 'var1 is set originally' ); + $this->assertEquals( $var2, 2, 'var1 is set originally' ); + + swap( $var1, $var2 ); + + $this->assertEquals( $var1, 2, 'var1 is swapped' ); + $this->assertEquals( $var2, 1, 'var2 is swapped' ); + } + + /** + * @covers ::wfPercent + */ + public function testWfPercentTest() { + + $pcts = array( + array( 6 / 7, '0.86%', 2, false ), + array( 3 / 3, '1%' ), + array( 22 / 7, '3.14286%', 5 ), + array( 3 / 6, '0.5%' ), + array( 1 / 3, '0%', 0 ), + array( 10 / 3, '0%', -1 ), + array( 3 / 4 / 5, '0.1%', 1 ), + array( 6 / 7 * 8, '6.8571428571%', 10 ), + ); + + foreach ( $pcts as $pct ) { + if ( !isset( $pct[2] ) ) { + $pct[2] = 2; + } + if ( !isset( $pct[3] ) ) { + $pct[3] = true; + } + + $this->assertEquals( wfPercent( $pct[0], $pct[2], $pct[3] ), $pct[1], $pct[1] ); + } + } + + /** + * test @see wfShorthandToInteger() + * @dataProvider provideShorthand + * @covers ::wfShorthandToInteger + */ + public function testWfShorthandToInteger( $shorthand, $expected ) { + $this->assertEquals( $expected, + wfShorthandToInteger( $shorthand ) + ); + } + + /** array( shorthand, expected integer ) */ + public static function provideShorthand() { + return array( + # Null, empty ... + array( '', -1 ), + array( ' ', -1 ), + array( null, -1 ), + + # Failures returns 0 :( + array( 'ABCDEFG', 0 ), + array( 'Ak', 0 ), + + # Int, strings with spaces + array( 1, 1 ), + array( ' 1 ', 1 ), + array( 1023, 1023 ), + array( ' 1023 ', 1023 ), + + # kilo, Mega, Giga + array( '1k', 1024 ), + array( '1K', 1024 ), + array( '1m', 1024 * 1024 ), + array( '1M', 1024 * 1024 ), + array( '1g', 1024 * 1024 * 1024 ), + array( '1G', 1024 * 1024 * 1024 ), + + # Negatives + array( -1, -1 ), + array( -500, -500 ), + array( '-500', -500 ), + array( '-1k', -1024 ), + + # Zeroes + array( '0', 0 ), + array( '0k', 0 ), + array( '0M', 0 ), + array( '0G', 0 ), + array( '-0', 0 ), + array( '-0k', 0 ), + array( '-0M', 0 ), + array( '-0G', 0 ), + ); + } + + /** + * @param String $old: Text as it was in the database + * @param String $mine: Text submitted while user was editing + * @param String $yours: Text submitted by the user + * @param Boolean $expectedMergeResult Whether the merge should be a success + * @param String $expectedText: Text after merge has been completed + * + * @dataProvider provideMerge() + * @group medium + * @covers ::wfMerge + */ + public function testMerge( $old, $mine, $yours, $expectedMergeResult, $expectedText ) { + $this->checkHasDiff3(); + + $mergedText = null; + $isMerged = wfMerge( $old, $mine, $yours, $mergedText ); + + $msg = 'Merge should be a '; + $msg .= $expectedMergeResult ? 'success' : 'failure'; + $this->assertEquals( $expectedMergeResult, $isMerged, $msg ); + + if ( $isMerged ) { + // Verify the merged text + $this->assertEquals( $expectedText, $mergedText, + 'is merged text as expected?' ); + } + } + + public static function provideMerge() { + $EXPECT_MERGE_SUCCESS = true; + $EXPECT_MERGE_FAILURE = false; + + return array( + // #0: clean merge + array( + // old: + "one one one\n" . // trimmed + "\n" . + "two two two", + + // mine: + "one one one ONE ONE\n" . + "\n" . + "two two two\n", // with tailing whitespace + + // yours: + "one one one\n" . + "\n" . + "two two TWO TWO", // trimmed + + // ok: + $EXPECT_MERGE_SUCCESS, + + // result: + "one one one ONE ONE\n" . + "\n" . + "two two TWO TWO\n", // note: will always end in a newline + ), + + // #1: conflict, fail + array( + // old: + "one one one", // trimmed + + // mine: + "one one one ONE ONE\n" . + "\n" . + "bla bla\n" . + "\n", // with tailing whitespace + + // yours: + "one one one\n" . + "\n" . + "two two", // trimmed + + $EXPECT_MERGE_FAILURE, + + // result: + null, + ), + ); + } + + /** + * @dataProvider provideMakeUrlIndexes() + * @covers ::wfMakeUrlIndexes + */ + public function testMakeUrlIndexes( $url, $expected ) { + $index = wfMakeUrlIndexes( $url ); + $this->assertEquals( $expected, $index, "wfMakeUrlIndexes(\"$url\")" ); + } + + public static function provideMakeUrlIndexes() { + return array( + array( + // just a regular :) + 'https://bugzilla.wikimedia.org/show_bug.cgi?id=28627', + array( 'https://org.wikimedia.bugzilla./show_bug.cgi?id=28627' ) + ), + array( + // mailtos are handled special + // is this really right though? that final . probably belongs earlier? + 'mailto:wiki@wikimedia.org', + array( 'mailto:org.wikimedia@wiki.' ) + ), + + // file URL cases per bug 28627... + array( + // three slashes: local filesystem path Unix-style + 'file:///whatever/you/like.txt', + array( 'file://./whatever/you/like.txt' ) + ), + array( + // three slashes: local filesystem path Windows-style + 'file:///c:/whatever/you/like.txt', + array( 'file://./c:/whatever/you/like.txt' ) + ), + array( + // two slashes: UNC filesystem path Windows-style + 'file://intranet/whatever/you/like.txt', + array( 'file://intranet./whatever/you/like.txt' ) + ), + // Multiple-slash cases that can sorta work on Mozilla + // if you hack it just right are kinda pathological, + // and unreliable cross-platform or on IE which means they're + // unlikely to appear on intranets. + // + // Those will survive the algorithm but with results that + // are less consistent. + + // protocol-relative URL cases per bug 29854... + array( + '//bugzilla.wikimedia.org/show_bug.cgi?id=28627', + array( + 'http://org.wikimedia.bugzilla./show_bug.cgi?id=28627', + 'https://org.wikimedia.bugzilla./show_bug.cgi?id=28627' + ) + ), + ); + } + + /** + * @dataProvider provideWfMatchesDomainList + * @covers ::wfMatchesDomainList + */ + public function testWfMatchesDomainList( $url, $domains, $expected, $description ) { + $actual = wfMatchesDomainList( $url, $domains ); + $this->assertEquals( $expected, $actual, $description ); + } + + public static function provideWfMatchesDomainList() { + $a = array(); + $protocols = array( 'HTTP' => 'http:', 'HTTPS' => 'https:', 'protocol-relative' => '' ); + foreach ( $protocols as $pDesc => $p ) { + $a = array_merge( $a, array( + array( "$p//www.example.com", array(), false, "No matches for empty domains array, $pDesc URL" ), + array( "$p//www.example.com", array( 'www.example.com' ), true, "Exact match in domains array, $pDesc URL" ), + array( "$p//www.example.com", array( 'example.com' ), true, "Match without subdomain in domains array, $pDesc URL" ), + array( "$p//www.example2.com", array( 'www.example.com', 'www.example2.com', 'www.example3.com' ), true, "Exact match with other domains in array, $pDesc URL" ), + array( "$p//www.example2.com", array( 'example.com', 'example2.com', 'example3,com' ), true, "Match without subdomain with other domains in array, $pDesc URL" ), + array( "$p//www.example4.com", array( 'example.com', 'example2.com', 'example3,com' ), false, "Domain not in array, $pDesc URL" ), + array( "$p//nds-nl.wikipedia.org", array( 'nl.wikipedia.org' ), false, "Non-matching substring of domain, $pDesc URL" ), + ) ); + } + + return $a; + } + + /** + * @covers ::wfMkdirParents + */ + public function testWfMkdirParents() { + // Should not return true if file exists instead of directory + $fname = $this->getNewTempFile(); + wfSuppressWarnings(); + $ok = wfMkdirParents( $fname ); + wfRestoreWarnings(); + $this->assertFalse( $ok ); + } + + /** + * @dataProvider provideWfShellMaintenanceCmdList + * @covers ::wfShellMaintenanceCmd + */ + public function testWfShellMaintenanceCmd( $script, $parameters, $options, $expected, $description ) { + if ( wfIsWindows() ) { + // Approximation that's good enough for our purposes just now + $expected = str_replace( "'", '"', $expected ); + } + $actual = wfShellMaintenanceCmd( $script, $parameters, $options ); + $this->assertEquals( $expected, $actual, $description ); + } + + public static function provideWfShellMaintenanceCmdList() { + global $wgPhpCli; + + return array( + array( 'eval.php', array( '--help', '--test' ), array(), + "'$wgPhpCli' 'eval.php' '--help' '--test'", + "Called eval.php --help --test" ), + array( 'eval.php', array( '--help', '--test space' ), array( 'php' => 'php5' ), + "'php5' 'eval.php' '--help' '--test space'", + "Called eval.php --help --test with php option" ), + array( 'eval.php', array( '--help', '--test', 'X' ), array( 'wrapper' => 'MWScript.php' ), + "'$wgPhpCli' 'MWScript.php' 'eval.php' '--help' '--test' 'X'", + "Called eval.php --help --test with wrapper option" ), + array( 'eval.php', array( '--help', '--test', 'y' ), array( 'php' => 'php5', 'wrapper' => 'MWScript.php' ), + "'php5' 'MWScript.php' 'eval.php' '--help' '--test' 'y'", + "Called eval.php --help --test with wrapper and php option" ), + ); + } + /* @TODO many more! */ +} diff --git a/tests/phpunit/includes/GlobalFunctions/GlobalWithDBTest.php b/tests/phpunit/includes/GlobalFunctions/GlobalWithDBTest.php new file mode 100644 index 00000000..cf891e7b --- /dev/null +++ b/tests/phpunit/includes/GlobalFunctions/GlobalWithDBTest.php @@ -0,0 +1,31 @@ +assertEquals( $expected, wfIsBadImage( $name, $title, $blacklist ), $desc ); + } + + public static function provideWfIsBadImageList() { + $blacklist = '* [[File:Bad.jpg]] except [[Nasty page]]'; + + return array( + array( 'Bad.jpg', false, $blacklist, true, + 'Called on a bad image' ), + array( 'Bad.jpg', Title::makeTitle( NS_MAIN, 'A page' ), $blacklist, true, + 'Called on a bad image' ), + array( 'NotBad.jpg', false, $blacklist, false, + 'Called on a non-bad image' ), + array( 'Bad.jpg', Title::makeTitle( NS_MAIN, 'Nasty page' ), $blacklist, false, + 'Called on a bad image but is on a whitelisted page' ), + array( 'File:Bad.jpg', false, $blacklist, false, + 'Called on a bad image with File:' ), + ); + } +} diff --git a/tests/phpunit/includes/GlobalFunctions/README b/tests/phpunit/includes/GlobalFunctions/README new file mode 100644 index 00000000..0042bdac --- /dev/null +++ b/tests/phpunit/includes/GlobalFunctions/README @@ -0,0 +1,2 @@ +This directory hold tests for includes/GlobalFunctions.php file +which is a pile of functions. diff --git a/tests/phpunit/includes/GlobalFunctions/wfAssembleUrlTest.php b/tests/phpunit/includes/GlobalFunctions/wfAssembleUrlTest.php new file mode 100644 index 00000000..9bb74873 --- /dev/null +++ b/tests/phpunit/includes/GlobalFunctions/wfAssembleUrlTest.php @@ -0,0 +1,111 @@ +assertEquals( + $output, + wfAssembleUrl( $parts ), + "Testing $partsDump assembles to $output" + ); + } + + /** + * Provider of URL parts for testing wfAssembleUrl() + * + * @return array + */ + public static function provideURLParts() { + $schemes = array( + '' => array(), + '//' => array( + 'delimiter' => '//', + ), + 'http://' => array( + 'scheme' => 'http', + 'delimiter' => '://', + ), + ); + + $hosts = array( + '' => array(), + 'example.com' => array( + 'host' => 'example.com', + ), + 'example.com:123' => array( + 'host' => 'example.com', + 'port' => 123, + ), + 'id@example.com' => array( + 'user' => 'id', + 'host' => 'example.com', + ), + 'id@example.com:123' => array( + 'user' => 'id', + 'host' => 'example.com', + 'port' => 123, + ), + 'id:key@example.com' => array( + 'user' => 'id', + 'pass' => 'key', + 'host' => 'example.com', + ), + 'id:key@example.com:123' => array( + 'user' => 'id', + 'pass' => 'key', + 'host' => 'example.com', + 'port' => 123, + ), + ); + + $cases = array(); + foreach ( $schemes as $scheme => $schemeParts ) { + foreach ( $hosts as $host => $hostParts ) { + foreach ( array( '', '/path' ) as $path ) { + foreach ( array( '', 'query' ) as $query ) { + foreach ( array( '', 'fragment' ) as $fragment ) { + $parts = array_merge( + $schemeParts, + $hostParts + ); + $url = $scheme . + $host . + $path; + + if ( $path ) { + $parts['path'] = $path; + } + if ( $query ) { + $parts['query'] = $query; + $url .= '?' . $query; + } + if ( $fragment ) { + $parts['fragment'] = $fragment; + $url .= '#' . $fragment; + } + + $cases[] = array( + $parts, + $url, + ); + } + } + } + } + } + + $complexURL = 'http://id:key@example.org:321' . + '/over/there?name=ferret&foo=bar#nose'; + $cases[] = array( + wfParseUrl( $complexURL ), + $complexURL, + ); + + return $cases; + } +} diff --git a/tests/phpunit/includes/GlobalFunctions/wfBCP47Test.php b/tests/phpunit/includes/GlobalFunctions/wfBCP47Test.php new file mode 100644 index 00000000..a01c0d49 --- /dev/null +++ b/tests/phpunit/includes/GlobalFunctions/wfBCP47Test.php @@ -0,0 +1,120 @@ +assertEquals( $expected, wfBCP47( $code ), + "Applying BCP47 standard to lower case '$code'" + ); + + $code = strtoupper( $code ); + $this->assertEquals( $expected, wfBCP47( $code ), + "Applying BCP47 standard to upper case '$code'" + ); + } + + /** + * Array format is ($code, $expected) + */ + public static function provideLanguageCodes() { + return array( + // Extracted from BCP47 (list not exhaustive) + # 2.1.1 + array( 'en-ca-x-ca', 'en-CA-x-ca' ), + array( 'sgn-be-fr', 'sgn-BE-FR' ), + array( 'az-latn-x-latn', 'az-Latn-x-latn' ), + # 2.2 + array( 'sr-Latn-RS', 'sr-Latn-RS' ), + array( 'az-arab-ir', 'az-Arab-IR' ), + + # 2.2.5 + array( 'sl-nedis', 'sl-nedis' ), + array( 'de-ch-1996', 'de-CH-1996' ), + + # 2.2.6 + array( + 'en-latn-gb-boont-r-extended-sequence-x-private', + 'en-Latn-GB-boont-r-extended-sequence-x-private' + ), + + // Examples from BCP47 Appendix A + # Simple language subtag: + array( 'DE', 'de' ), + array( 'fR', 'fr' ), + array( 'ja', 'ja' ), + + # Language subtag plus script subtag: + array( 'zh-hans', 'zh-Hans' ), + array( 'sr-cyrl', 'sr-Cyrl' ), + array( 'sr-latn', 'sr-Latn' ), + + # Extended language subtags and their primary language subtag + # counterparts: + array( 'zh-cmn-hans-cn', 'zh-cmn-Hans-CN' ), + array( 'cmn-hans-cn', 'cmn-Hans-CN' ), + array( 'zh-yue-hk', 'zh-yue-HK' ), + array( 'yue-hk', 'yue-HK' ), + + # Language-Script-Region: + array( 'zh-hans-cn', 'zh-Hans-CN' ), + array( 'sr-latn-RS', 'sr-Latn-RS' ), + + # Language-Variant: + array( 'sl-rozaj', 'sl-rozaj' ), + array( 'sl-rozaj-biske', 'sl-rozaj-biske' ), + array( 'sl-nedis', 'sl-nedis' ), + + # Language-Region-Variant: + array( 'de-ch-1901', 'de-CH-1901' ), + array( 'sl-it-nedis', 'sl-IT-nedis' ), + + # Language-Script-Region-Variant: + array( 'hy-latn-it-arevela', 'hy-Latn-IT-arevela' ), + + # Language-Region: + array( 'de-de', 'de-DE' ), + array( 'en-us', 'en-US' ), + array( 'es-419', 'es-419' ), + + # Private use subtags: + array( 'de-ch-x-phonebk', 'de-CH-x-phonebk' ), + array( 'az-arab-x-aze-derbend', 'az-Arab-x-aze-derbend' ), + /** + * Previous test does not reflect the BCP which states: + * az-Arab-x-AZE-derbend + * AZE being private, it should be lower case, hence the test above + * should probably be: + #array( 'az-arab-x-aze-derbend', 'az-Arab-x-AZE-derbend' ), + */ + + # Private use registry values: + array( 'x-whatever', 'x-whatever' ), + array( 'qaa-qaaa-qm-x-southern', 'qaa-Qaaa-QM-x-southern' ), + array( 'de-qaaa', 'de-Qaaa' ), + array( 'sr-latn-qm', 'sr-Latn-QM' ), + array( 'sr-qaaa-rs', 'sr-Qaaa-RS' ), + + # Tags that use extensions + array( 'en-us-u-islamcal', 'en-US-u-islamcal' ), + array( 'zh-cn-a-myext-x-private', 'zh-CN-a-myext-x-private' ), + array( 'en-a-myext-b-another', 'en-a-myext-b-another' ), + + # Invalid: + // de-419-DE + // a-DE + // ar-a-aaa-b-bbb-a-ccc + ); + } +} diff --git a/tests/phpunit/includes/GlobalFunctions/wfBaseConvertTest.php b/tests/phpunit/includes/GlobalFunctions/wfBaseConvertTest.php new file mode 100644 index 00000000..7da804e6 --- /dev/null +++ b/tests/phpunit/includes/GlobalFunctions/wfBaseConvertTest.php @@ -0,0 +1,182 @@ +assertSame( $base2, wfBaseConvert( $base3, '3', '2' ) ); + $this->assertSame( $base2, wfBaseConvert( $base5, '5', '2' ) ); + $this->assertSame( $base2, wfBaseConvert( $base8, '8', '2' ) ); + $this->assertSame( $base2, wfBaseConvert( $base10, '10', '2' ) ); + $this->assertSame( $base2, wfBaseConvert( $base16, '16', '2' ) ); + $this->assertSame( $base2, wfBaseConvert( $base36, '36', '2' ) ); + } + + /** + * @dataProvider provideSingleDigitConversions + */ + public function testDigitToBase3( $base2, $base3, $base5, $base8, $base10, $base16, $base36 ) { + $this->assertSame( $base3, wfBaseConvert( $base2, '2', '3' ) ); + $this->assertSame( $base3, wfBaseConvert( $base5, '5', '3' ) ); + $this->assertSame( $base3, wfBaseConvert( $base8, '8', '3' ) ); + $this->assertSame( $base3, wfBaseConvert( $base10, '10', '3' ) ); + $this->assertSame( $base3, wfBaseConvert( $base16, '16', '3' ) ); + $this->assertSame( $base3, wfBaseConvert( $base36, '36', '3' ) ); + } + + /** + * @dataProvider provideSingleDigitConversions + */ + public function testDigitToBase5( $base2, $base3, $base5, $base8, $base10, $base16, $base36 ) { + $this->assertSame( $base5, wfBaseConvert( $base2, '2', '5' ) ); + $this->assertSame( $base5, wfBaseConvert( $base3, '3', '5' ) ); + $this->assertSame( $base5, wfBaseConvert( $base8, '8', '5' ) ); + $this->assertSame( $base5, wfBaseConvert( $base10, '10', '5' ) ); + $this->assertSame( $base5, wfBaseConvert( $base16, '16', '5' ) ); + $this->assertSame( $base5, wfBaseConvert( $base36, '36', '5' ) ); + } + + /** + * @dataProvider provideSingleDigitConversions + */ + public function testDigitToBase8( $base2, $base3, $base5, $base8, $base10, $base16, $base36 ) { + $this->assertSame( $base8, wfBaseConvert( $base2, '2', '8' ) ); + $this->assertSame( $base8, wfBaseConvert( $base3, '3', '8' ) ); + $this->assertSame( $base8, wfBaseConvert( $base5, '5', '8' ) ); + $this->assertSame( $base8, wfBaseConvert( $base10, '10', '8' ) ); + $this->assertSame( $base8, wfBaseConvert( $base16, '16', '8' ) ); + $this->assertSame( $base8, wfBaseConvert( $base36, '36', '8' ) ); + } + + /** + * @dataProvider provideSingleDigitConversions + */ + public function testDigitToBase10( $base2, $base3, $base5, $base8, $base10, $base16, $base36 ) { + $this->assertSame( $base10, wfBaseConvert( $base2, '2', '10' ) ); + $this->assertSame( $base10, wfBaseConvert( $base3, '3', '10' ) ); + $this->assertSame( $base10, wfBaseConvert( $base5, '5', '10' ) ); + $this->assertSame( $base10, wfBaseConvert( $base8, '8', '10' ) ); + $this->assertSame( $base10, wfBaseConvert( $base16, '16', '10' ) ); + $this->assertSame( $base10, wfBaseConvert( $base36, '36', '10' ) ); + } + + /** + * @dataProvider provideSingleDigitConversions + */ + public function testDigitToBase16( $base2, $base3, $base5, $base8, $base10, $base16, $base36 ) { + $this->assertSame( $base16, wfBaseConvert( $base2, '2', '16' ) ); + $this->assertSame( $base16, wfBaseConvert( $base3, '3', '16' ) ); + $this->assertSame( $base16, wfBaseConvert( $base5, '5', '16' ) ); + $this->assertSame( $base16, wfBaseConvert( $base8, '8', '16' ) ); + $this->assertSame( $base16, wfBaseConvert( $base10, '10', '16' ) ); + $this->assertSame( $base16, wfBaseConvert( $base36, '36', '16' ) ); + } + + /** + * @dataProvider provideSingleDigitConversions + */ + public function testDigitToBase36( $base2, $base3, $base5, $base8, $base10, $base16, $base36 ) { + $this->assertSame( $base36, wfBaseConvert( $base2, '2', '36' ) ); + $this->assertSame( $base36, wfBaseConvert( $base3, '3', '36' ) ); + $this->assertSame( $base36, wfBaseConvert( $base5, '5', '36' ) ); + $this->assertSame( $base36, wfBaseConvert( $base8, '8', '36' ) ); + $this->assertSame( $base36, wfBaseConvert( $base10, '10', '36' ) ); + $this->assertSame( $base36, wfBaseConvert( $base16, '16', '36' ) ); + } + + public function testLargeNumber() { + $this->assertSame( '1100110001111010000000101110100', wfBaseConvert( 'sd89ys', 36, 2 ) ); + $this->assertSame( '11102112120221201101', wfBaseConvert( 'sd89ys', 36, 3 ) ); + $this->assertSame( '12003102232400', wfBaseConvert( 'sd89ys', 36, 5 ) ); + $this->assertSame( '14617200564', wfBaseConvert( 'sd89ys', 36, 8 ) ); + $this->assertSame( '1715274100', wfBaseConvert( 'sd89ys', 36, 10 ) ); + $this->assertSame( '663d0174', wfBaseConvert( 'sd89ys', 36, 16 ) ); + } + + public static function provideNumbers() { + $x = array(); + $chars = '0123456789abcdefghijklmnopqrstuvwxyz'; + for ( $i = 0; $i < 50; $i++ ) { + $base = mt_rand( 2, 36 ); + $len = mt_rand( 10, 100 ); + + $str = ''; + for ( $j = 0; $j < $len; $j++ ) { + $str .= $chars[mt_rand( 0, $base - 1 )]; + } + + $x[] = array( $base, $str ); + } + + return $x; + } + + /** + * @dataProvider provideNumbers + */ + public function testIdentity( $base, $number ) { + $this->assertSame( $number, wfBaseConvert( $number, $base, $base, strlen( $number ) ) ); + } + + public function testInvalid() { + $this->assertFalse( wfBaseConvert( '101', 1, 15 ) ); + $this->assertFalse( wfBaseConvert( '101', 15, 1 ) ); + $this->assertFalse( wfBaseConvert( '101', 37, 15 ) ); + $this->assertFalse( wfBaseConvert( '101', 15, 37 ) ); + $this->assertFalse( wfBaseConvert( 'abcde', 10, 11 ) ); + $this->assertFalse( wfBaseConvert( '12930', 2, 10 ) ); + $this->assertFalse( wfBaseConvert( '101', 'abc', 15 ) ); + $this->assertFalse( wfBaseConvert( '101', 15, 'abc' ) ); + } + + public function testPadding() { + $number = "10101010101"; + $this->assertSame( strlen( $number ) + 5, strlen( wfBaseConvert( $number, 2, 2, strlen( $number ) + 5 ) ) ); + $this->assertSame( strlen( $number ), strlen( wfBaseConvert( $number, 2, 2, strlen( $number ) - 5 ) ) ); + } +} diff --git a/tests/phpunit/includes/GlobalFunctions/wfBaseNameTest.php b/tests/phpunit/includes/GlobalFunctions/wfBaseNameTest.php new file mode 100644 index 00000000..8c548040 --- /dev/null +++ b/tests/phpunit/includes/GlobalFunctions/wfBaseNameTest.php @@ -0,0 +1,36 @@ +assertEquals( $basename, wfBaseName( $fullpath ), + "wfBaseName('$fullpath') => '$basename'" ); + } + + public static function providePaths() { + return array( + array( '', '' ), + array( '/', '' ), + array( '\\', '' ), + array( '//', '' ), + array( '\\\\', '' ), + array( 'a', 'a' ), + array( 'aaaa', 'aaaa' ), + array( '/a', 'a' ), + array( '\\a', 'a' ), + array( '/aaaa', 'aaaa' ), + array( '\\aaaa', 'aaaa' ), + array( '/aaaa/', 'aaaa' ), + array( '\\aaaa\\', 'aaaa' ), + array( '\\aaaa\\', 'aaaa' ), + array( '/mnt/upload3/wikipedia/en/thumb/8/8b/Zork_Grand_Inquisitor_box_cover.jpg/93px-Zork_Grand_Inquisitor_box_cover.jpg', + '93px-Zork_Grand_Inquisitor_box_cover.jpg' ), + array( 'C:\\Progra~1\\Wikime~1\\Wikipe~1\\VIEWER.EXE', 'VIEWER.EXE' ), + array( 'Östergötland_coat_of_arms.png', 'Östergötland_coat_of_arms.png' ), + ); + } +} diff --git a/tests/phpunit/includes/GlobalFunctions/wfExpandUrlTest.php b/tests/phpunit/includes/GlobalFunctions/wfExpandUrlTest.php new file mode 100644 index 00000000..41230a1e --- /dev/null +++ b/tests/phpunit/includes/GlobalFunctions/wfExpandUrlTest.php @@ -0,0 +1,111 @@ +setMwGlobals( array( + 'wgServer' => $server, + 'wgCanonicalServer' => $canServer, + ) ); + + // Fake $_SERVER['HTTPS'] if needed + if ( $httpsMode ) { + $_SERVER['HTTPS'] = 'on'; + } else { + unset( $_SERVER['HTTPS'] ); + } + + $this->assertEquals( $fullUrl, wfExpandUrl( $shortUrl, $defaultProto ), $message ); + } + + /** + * Provider of URL examples for testing wfExpandUrl() + * + * @return array + */ + public static function provideExpandableUrls() { + $modes = array( 'http', 'https' ); + $servers = array( + 'http' => 'http://example.com', + 'https' => 'https://example.com', + 'protocol-relative' => '//example.com' + ); + $defaultProtos = array( + 'http' => PROTO_HTTP, + 'https' => PROTO_HTTPS, + 'protocol-relative' => PROTO_RELATIVE, + 'current' => PROTO_CURRENT, + 'canonical' => PROTO_CANONICAL + ); + + $retval = array(); + foreach ( $modes as $mode ) { + $httpsMode = $mode == 'https'; + foreach ( $servers as $serverDesc => $server ) { + foreach ( $modes as $canServerMode ) { + $canServer = "$canServerMode://example2.com"; + foreach ( $defaultProtos as $protoDesc => $defaultProto ) { + $retval[] = array( + 'http://example.com', 'http://example.com', + $defaultProto, $server, $canServer, $httpsMode, + "Testing fully qualified http URLs (no need to expand) ' . + '(defaultProto: $protoDesc , wgServer: $server, wgCanonicalServer: $canServer, current request protocol: $mode )" + ); + $retval[] = array( + 'https://example.com', 'https://example.com', + $defaultProto, $server, $canServer, $httpsMode, + "Testing fully qualified https URLs (no need to expand) ' . + '(defaultProto: $protoDesc , wgServer: $server, wgCanonicalServer: $canServer, current request protocol: $mode )" + ); + # Would be nice to support this, see fixme on wfExpandUrl() + $retval[] = array( + "wiki/FooBar", 'wiki/FooBar', + $defaultProto, $server, $canServer, $httpsMode, + "Test non-expandable relative URLs ' . + '(defaultProto: $protoDesc , wgServer: $server, wgCanonicalServer: $canServer, current request protocol: $mode )" + ); + + // Determine expected protocol + if ( $protoDesc == 'protocol-relative' ) { + $p = ''; + } elseif ( $protoDesc == 'current' ) { + $p = "$mode:"; + } elseif ( $protoDesc == 'canonical' ) { + $p = "$canServerMode:"; + } else { + $p = $protoDesc . ':'; + } + // Determine expected server name + if ( $protoDesc == 'canonical' ) { + $srv = $canServer; + } elseif ( $serverDesc == 'protocol-relative' ) { + $srv = $p . $server; + } else { + $srv = $server; + } + + $retval[] = array( + "$p//wikipedia.org", '//wikipedia.org', + $defaultProto, $server, $canServer, $httpsMode, + "Test protocol-relative URL ' . + '(defaultProto: $protoDesc, wgServer: $server, wgCanonicalServer: $canServer, current request protocol: $mode )" + ); + $retval[] = array( + "$srv/wiki/FooBar", '/wiki/FooBar', + $defaultProto, $server, $canServer, $httpsMode, + "Testing expanding URL beginning with / ' . + '(defaultProto: $protoDesc , wgServer: $server, wgCanonicalServer: $canServer, current request protocol: $mode )" + ); + } + } + } + } + + return $retval; + } +} diff --git a/tests/phpunit/includes/GlobalFunctions/wfGetCallerTest.php b/tests/phpunit/includes/GlobalFunctions/wfGetCallerTest.php new file mode 100644 index 00000000..62296245 --- /dev/null +++ b/tests/phpunit/includes/GlobalFunctions/wfGetCallerTest.php @@ -0,0 +1,40 @@ +assertEquals( __METHOD__, wfGetCaller( 1 ) ); + } + + function callerOne() { + return wfGetCaller(); + } + + public function testOne() { + $this->assertEquals( 'WfGetCallerTest::testOne', self::callerOne() ); + } + + function intermediateFunction( $level = 2, $n = 0 ) { + if ( $n > 0 ) { + return self::intermediateFunction( $level, $n - 1 ); + } + + return wfGetCaller( $level ); + } + + public function testTwo() { + $this->assertEquals( 'WfGetCallerTest::testTwo', self::intermediateFunction() ); + } + + public function testN() { + $this->assertEquals( 'WfGetCallerTest::testN', self::intermediateFunction( 2, 0 ) ); + $this->assertEquals( 'WfGetCallerTest::intermediateFunction', self::intermediateFunction( 1, 0 ) ); + + for ( $i = 0; $i < 10; $i++ ) { + $this->assertEquals( 'WfGetCallerTest::intermediateFunction', self::intermediateFunction( $i + 1, $i ) ); + } + } +} diff --git a/tests/phpunit/includes/GlobalFunctions/wfParseUrlTest.php b/tests/phpunit/includes/GlobalFunctions/wfParseUrlTest.php new file mode 100644 index 00000000..5032dc11 --- /dev/null +++ b/tests/phpunit/includes/GlobalFunctions/wfParseUrlTest.php @@ -0,0 +1,146 @@ +setMwGlobals( 'wgUrlProtocols', array( + '//', 'http://', 'file://', 'mailto:', + ) ); + } + + /** + * @dataProvider provideURLs + */ + public function testWfParseUrl( $url, $parts ) { + $partsDump = var_export( $parts, true ); + $this->assertEquals( + $parts, + wfParseUrl( $url ), + "Testing $url parses to $partsDump" + ); + } + + /** + * Provider of URLs for testing wfParseUrl() + * + * @return array + */ + public static function provideURLs() { + return array( + array( + '//example.org', + array( + 'scheme' => '', + 'delimiter' => '//', + 'host' => 'example.org', + ) + ), + array( + 'http://example.org', + array( + 'scheme' => 'http', + 'delimiter' => '://', + 'host' => 'example.org', + ) + ), + array( + 'http://id:key@example.org:123/path?foo=bar#baz', + array( + 'scheme' => 'http', + 'delimiter' => '://', + 'user' => 'id', + 'pass' => 'key', + 'host' => 'example.org', + 'port' => 123, + 'path' => '/path', + 'query' => 'foo=bar', + 'fragment' => 'baz', + ) + ), + array( + 'file://example.org/etc/php.ini', + array( + 'scheme' => 'file', + 'delimiter' => '://', + 'host' => 'example.org', + 'path' => '/etc/php.ini', + ) + ), + array( + 'file:///etc/php.ini', + array( + 'scheme' => 'file', + 'delimiter' => '://', + 'host' => '', + 'path' => '/etc/php.ini', + ) + ), + array( + 'file:///c:/', + array( + 'scheme' => 'file', + 'delimiter' => '://', + 'host' => '', + 'path' => '/c:/', + ) + ), + array( + 'mailto:id@example.org', + array( + 'scheme' => 'mailto', + 'delimiter' => ':', + 'host' => 'id@example.org', + 'path' => '', + ) + ), + array( + 'mailto:id@example.org?subject=Foo', + array( + 'scheme' => 'mailto', + 'delimiter' => ':', + 'host' => 'id@example.org', + 'path' => '', + 'query' => 'subject=Foo', + ) + ), + array( + 'mailto:?subject=Foo', + array( + 'scheme' => 'mailto', + 'delimiter' => ':', + 'host' => '', + 'path' => '', + 'query' => 'subject=Foo', + ) + ), + array( + 'invalid://test/', + false + ), + ); + } +} diff --git a/tests/phpunit/includes/GlobalFunctions/wfRemoveDotSegmentsTest.php b/tests/phpunit/includes/GlobalFunctions/wfRemoveDotSegmentsTest.php new file mode 100644 index 00000000..238a2c9c --- /dev/null +++ b/tests/phpunit/includes/GlobalFunctions/wfRemoveDotSegmentsTest.php @@ -0,0 +1,91 @@ +assertEquals( + $outputPath, + wfRemoveDotSegments( $inputPath ), + "Testing $inputPath expands to $outputPath" + ); + } + + /** + * Provider of URL paths for testing wfRemoveDotSegments() + * + * @return array + */ + public static function providePaths() { + return array( + array( '/a/b/c/./../../g', '/a/g' ), + array( 'mid/content=5/../6', 'mid/6' ), + array( '/a//../b', '/a/b' ), + array( '/.../a', '/.../a' ), + array( '.../a', '.../a' ), + array( '', '' ), + array( '/', '/' ), + array( '//', '//' ), + array( '.', '' ), + array( '..', '' ), + array( '...', '...' ), + array( '/.', '/' ), + array( '/..', '/' ), + array( './', '' ), + array( '../', '' ), + array( './a', 'a' ), + array( '../a', 'a' ), + array( '../../a', 'a' ), + array( '.././a', 'a' ), + array( './../a', 'a' ), + array( '././a', 'a' ), + array( '../../', '' ), + array( '.././', '' ), + array( './../', '' ), + array( '././', '' ), + array( '../..', '' ), + array( '../.', '' ), + array( './..', '' ), + array( './.', '' ), + array( '/../../a', '/a' ), + array( '/.././a', '/a' ), + array( '/./../a', '/a' ), + array( '/././a', '/a' ), + array( '/../../', '/' ), + array( '/.././', '/' ), + array( '/./../', '/' ), + array( '/././', '/' ), + array( '/../..', '/' ), + array( '/../.', '/' ), + array( '/./..', '/' ), + array( '/./.', '/' ), + array( 'b/../../a', '/a' ), + array( 'b/.././a', '/a' ), + array( 'b/./../a', '/a' ), + array( 'b/././a', 'b/a' ), + array( 'b/../../', '/' ), + array( 'b/.././', '/' ), + array( 'b/./../', '/' ), + array( 'b/././', 'b/' ), + array( 'b/../..', '/' ), + array( 'b/../.', '/' ), + array( 'b/./..', '/' ), + array( 'b/./.', 'b/' ), + array( '/b/../../a', '/a' ), + array( '/b/.././a', '/a' ), + array( '/b/./../a', '/a' ), + array( '/b/././a', '/b/a' ), + array( '/b/../../', '/' ), + array( '/b/.././', '/' ), + array( '/b/./../', '/' ), + array( '/b/././', '/b/' ), + array( '/b/../..', '/' ), + array( '/b/../.', '/' ), + array( '/b/./..', '/' ), + array( '/b/./.', '/b/' ), + ); + } +} diff --git a/tests/phpunit/includes/GlobalFunctions/wfShorthandToIntegerTest.php b/tests/phpunit/includes/GlobalFunctions/wfShorthandToIntegerTest.php new file mode 100644 index 00000000..aadec87f --- /dev/null +++ b/tests/phpunit/includes/GlobalFunctions/wfShorthandToIntegerTest.php @@ -0,0 +1,30 @@ +assertEquals( + wfShorthandToInteger( $input ), + $output, + $description + ); + } + + public static function provideABunchOfShorthands() { + return array( + array( '', -1, 'Empty string' ), + array( ' ', -1, 'String of spaces' ), + array( '1G', 1024 * 1024 * 1024, 'One gig uppercased' ), + array( '1g', 1024 * 1024 * 1024, 'One gig lowercased' ), + array( '1M', 1024 * 1024, 'One meg uppercased' ), + array( '1m', 1024 * 1024, 'One meg lowercased' ), + array( '1K', 1024, 'One kb uppercased' ), + array( '1k', 1024, 'One kb lowercased' ), + ); + } +} diff --git a/tests/phpunit/includes/GlobalFunctions/wfTimestampTest.php b/tests/phpunit/includes/GlobalFunctions/wfTimestampTest.php new file mode 100644 index 00000000..5998f186 --- /dev/null +++ b/tests/phpunit/includes/GlobalFunctions/wfTimestampTest.php @@ -0,0 +1,134 @@ +assertEquals( $output, wfTimestamp( $format, $input ), $desc ); + } + + public static function provideNormalTimestamps() { + $t = gmmktime( 12, 34, 56, 1, 15, 2001 ); + + return array( + // TS_UNIX + array( $t, TS_MW, '20010115123456', 'TS_UNIX to TS_MW' ), + array( -30281104, TS_MW, '19690115123456', 'Negative TS_UNIX to TS_MW' ), + array( $t, TS_UNIX, 979562096, 'TS_UNIX to TS_UNIX' ), + array( $t, TS_DB, '2001-01-15 12:34:56', 'TS_UNIX to TS_DB' ), + + array( $t, TS_ISO_8601_BASIC, '20010115T123456Z', 'TS_ISO_8601_BASIC to TS_DB' ), + + // TS_MW + array( '20010115123456', TS_MW, '20010115123456', 'TS_MW to TS_MW' ), + array( '20010115123456', TS_UNIX, 979562096, 'TS_MW to TS_UNIX' ), + array( '20010115123456', TS_DB, '2001-01-15 12:34:56', 'TS_MW to TS_DB' ), + array( '20010115123456', TS_ISO_8601_BASIC, '20010115T123456Z', 'TS_MW to TS_ISO_8601_BASIC' ), + + // TS_DB + array( '2001-01-15 12:34:56', TS_MW, '20010115123456', 'TS_DB to TS_MW' ), + array( '2001-01-15 12:34:56', TS_UNIX, 979562096, 'TS_DB to TS_UNIX' ), + array( '2001-01-15 12:34:56', TS_DB, '2001-01-15 12:34:56', 'TS_DB to TS_DB' ), + array( '2001-01-15 12:34:56', TS_ISO_8601_BASIC, '20010115T123456Z', 'TS_DB to TS_ISO_8601_BASIC' ), + + # rfc2822 section 3.3 + array( '20010115123456', TS_RFC2822, 'Mon, 15 Jan 2001 12:34:56 GMT', 'TS_MW to TS_RFC2822' ), + array( 'Mon, 15 Jan 2001 12:34:56 GMT', TS_MW, '20010115123456', 'TS_RFC2822 to TS_MW' ), + array( ' Mon, 15 Jan 2001 12:34:56 GMT', TS_MW, '20010115123456', 'TS_RFC2822 with leading space to TS_MW' ), + array( '15 Jan 2001 12:34:56 GMT', TS_MW, '20010115123456', 'TS_RFC2822 without optional day-of-week to TS_MW' ), + + # FWS = ([*WSP CRLF] 1*WSP) / obs-FWS ; Folding white space + # obs-FWS = 1*WSP *(CRLF 1*WSP) ; Section 4.2 + array( 'Mon, 15 Jan 2001 12:34:56 GMT', TS_MW, '20010115123456', 'TS_RFC2822 to TS_MW' ), + + # WSP = SP / HTAB ; rfc2234 + array( "Mon, 15 Jan\x092001 12:34:56 GMT", TS_MW, '20010115123456', 'TS_RFC2822 with HTAB to TS_MW' ), + array( "Mon, 15 Jan\x09 \x09 2001 12:34:56 GMT", TS_MW, '20010115123456', 'TS_RFC2822 with HTAB and SP to TS_MW' ), + array( 'Sun, 6 Nov 94 08:49:37 GMT', TS_MW, '19941106084937', 'TS_RFC2822 with obsolete year to TS_MW' ), + ); + } + + /** + * This test checks wfTimestamp() with values outside. + * It needs PHP 64 bits or PHP > 5.1. + * See r74778 and bug 25451 + * @dataProvider provideOldTimestamps + */ + public function testOldTimestamps( $input, $format, $output, $desc ) { + $this->assertEquals( $output, wfTimestamp( $format, $input ), $desc ); + } + + public static function provideOldTimestamps() { + return array( + array( '19011213204554', TS_RFC2822, 'Fri, 13 Dec 1901 20:45:54 GMT', 'Earliest time according to php documentation' ), + array( '20380119031407', TS_RFC2822, 'Tue, 19 Jan 2038 03:14:07 GMT', 'Latest 32 bit time' ), + array( '19011213204552', TS_UNIX, '-2147483648', 'Earliest 32 bit unix time' ), + array( '20380119031407', TS_UNIX, '2147483647', 'Latest 32 bit unix time' ), + array( '19011213204552', TS_RFC2822, 'Fri, 13 Dec 1901 20:45:52 GMT', 'Earliest 32 bit time' ), + array( '19011213204551', TS_RFC2822, 'Fri, 13 Dec 1901 20:45:51 GMT', 'Earliest 32 bit time - 1' ), + array( '20380119031408', TS_RFC2822, 'Tue, 19 Jan 2038 03:14:08 GMT', 'Latest 32 bit time + 1' ), + array( '19011212000000', TS_MW, '19011212000000', 'Convert to itself r74778#c10645' ), + array( '19011213204551', TS_UNIX, '-2147483649', 'Earliest 32 bit unix time - 1' ), + array( '20380119031408', TS_UNIX, '2147483648', 'Latest 32 bit unix time + 1' ), + array( '-2147483649', TS_MW, '19011213204551', '1901 negative unix time to MediaWiki' ), + array( '-5331871504', TS_MW, '18010115123456', '1801 negative unix time to MediaWiki' ), + array( '0117-08-09 12:34:56', TS_RFC2822, 'Tue, 09 Aug 0117 12:34:56 GMT', 'Death of Roman Emperor [[Trajan]]' ), + + /* @todo FIXME: 00 to 101 years are taken as being in [1970-2069] */ + array( '-58979923200', TS_RFC2822, 'Sun, 01 Jan 0101 00:00:00 GMT', '1/1/101' ), + array( '-62135596800', TS_RFC2822, 'Mon, 01 Jan 0001 00:00:00 GMT', 'Year 1' ), + + /* It is not clear if we should generate a year 0 or not + * We are completely off RFC2822 requirement of year being + * 1900 or later. + */ + array( '-62142076800', TS_RFC2822, 'Wed, 18 Oct 0000 00:00:00 GMT', 'ISO 8601:2004 [[year 0]], also called [[1 BC]]' ), + ); + } + + /** + * The Resource Loader uses wfTimestamp() to convert timestamps + * from If-Modified-Since header. Thus it must be able to parse all + * rfc2616 date formats + * @see http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.3.1 + * @dataProvider provideHttpDates + */ + public function testHttpDate( $input, $output, $desc ) { + $this->assertEquals( $output, wfTimestamp( TS_MW, $input ), $desc ); + } + + public static function provideHttpDates() { + return array( + array( 'Sun, 06 Nov 1994 08:49:37 GMT', '19941106084937', 'RFC 822 date' ), + array( 'Sunday, 06-Nov-94 08:49:37 GMT', '19941106084937', 'RFC 850 date' ), + array( 'Sun Nov 6 08:49:37 1994', '19941106084937', "ANSI C's asctime() format" ), + // See http://www.squid-cache.org/mail-archive/squid-users/200307/0122.html and r77171 + array( 'Mon, 22 Nov 2010 14:12:42 GMT; length=52626', '20101122141242', 'Netscape extension to HTTP/1.0' ), + ); + } + + /** + * There are a number of assumptions in our codebase where wfTimestamp() + * should give the current date but it is not given a 0 there. See r71751 CR + */ + public function testTimestampParameter() { + $now = wfTimestamp( TS_UNIX ); + // We check that wfTimestamp doesn't return false (error) and use a LessThan assert + // for the cases where the test is run in a second boundary. + + $zero = wfTimestamp( TS_UNIX, 0 ); + $this->assertNotEquals( false, $zero ); + $this->assertLessThan( 5, $zero - $now ); + + $empty = wfTimestamp( TS_UNIX, '' ); + $this->assertNotEquals( false, $empty ); + $this->assertLessThan( 5, $empty - $now ); + + $null = wfTimestamp( TS_UNIX, null ); + $this->assertNotEquals( false, $null ); + $this->assertLessThan( 5, $null - $now ); + } +} diff --git a/tests/phpunit/includes/GlobalFunctions/wfUrlencodeTest.php b/tests/phpunit/includes/GlobalFunctions/wfUrlencodeTest.php new file mode 100644 index 00000000..ce6c82c5 --- /dev/null +++ b/tests/phpunit/includes/GlobalFunctions/wfUrlencodeTest.php @@ -0,0 +1,119 @@ +verifyEncodingFor( 'Apache', $input, $expected ); + } + + /** + * @dataProvider provideURLS + */ + public function testEncodingUrlWithMicrosoftIis7( $input, $expected ) { + $this->verifyEncodingFor( 'Microsoft-IIS/7', $input, $expected ); + } + + #### HELPERS ############################################################# + + /** + * Internal helper that actually run the test. + * Called by the public methods testEncodingUrlWith...() + * + */ + private function verifyEncodingFor( $server, $input, $expectations ) { + $expected = $this->extractExpect( $server, $expectations ); + + // save up global + $old = isset( $_SERVER['SERVER_SOFTWARE'] ) + ? $_SERVER['SERVER_SOFTWARE'] + : null; + $_SERVER['SERVER_SOFTWARE'] = $server; + wfUrlencode( null ); + + // do the requested test + $this->assertEquals( + $expected, + wfUrlencode( $input ), + "Encoding '$input' for server '$server' should be '$expected'" + ); + + // restore global + if ( $old === null ) { + unset( $_SERVER['SERVER_SOFTWARE'] ); + } else { + $_SERVER['SERVER_SOFTWARE'] = $old; + } + wfUrlencode( null ); + } + + /** + * Interprets the provider array. Return expected value depending + * the HTTP server name. + */ + private function extractExpect( $server, $expectations ) { + if ( is_string( $expectations ) ) { + return $expectations; + } elseif ( is_array( $expectations ) ) { + if ( !array_key_exists( $server, $expectations ) ) { + throw new MWException( __METHOD__ . " expectation does not have any value for server name $server. Check the provider array.\n" ); + } else { + return $expectations[$server]; + } + } else { + throw new MWException( __METHOD__ . " given invalid expectation for '$server'. Should be a string or an array( => ).\n" ); + } + } + + #### PROVIDERS ########################################################### + + /** + * Format is either: + * array( 'input', 'expected' ); + * Or: + * array( 'input', + * array( 'Apache', 'expected' ), + * array( 'Microsoft-IIS/7', 'expected' ), + * ), + * If you want to add other HTTP server name, you will have to add a new + * testing method much like the testEncodingUrlWith() method above. + */ + public static function provideURLS() { + return array( + ### RFC 1738 chars + // + is not safe + array( '+', '%2B' ), + // & and = not safe in queries + array( '&', '%26' ), + array( '=', '%3D' ), + + array( ':', array( + 'Apache' => ':', + 'Microsoft-IIS/7' => '%3A', + ) ), + + // remaining chars do not need encoding + array( + ';@$-_.!*', + ';@$-_.!*', + ), + + ### Other tests + // slash remain unchanged. %2F seems to break things + array( '/', '/' ), + + // Other 'funnies' chars + array( '[]', '%5B%5D' ), + array( '<>', '%3C%3E' ), + + // Apostrophe is encoded + array( '\'', '%27' ), + ); + } +} diff --git a/tests/phpunit/includes/HTMLCheckMatrixTest.php b/tests/phpunit/includes/HTMLCheckMatrixTest.php new file mode 100644 index 00000000..5bbafd37 --- /dev/null +++ b/tests/phpunit/includes/HTMLCheckMatrixTest.php @@ -0,0 +1,102 @@ + array( 'r1', 'r2' ), + 'columns' => array( 'c1', 'c2' ), + 'fieldname' => 'test', + ); + + public function testPlainInstantiation() { + try { + $form = new HTMLCheckMatrix( array() ); + } catch ( MWException $e ) { + $this->assertInstanceOf( 'HTMLFormFieldRequiredOptionsException', $e ); + return; + } + + $this->fail( 'Expected MWException indicating missing parameters but none was thrown.' ); + } + + public function testInstantiationWithMinimumRequiredParameters() { + $form = new HTMLCheckMatrix( self::$defaultOptions ); + $this->assertTrue( true ); // form instantiation must throw exception on failure + } + + public function testValidateCallsUserDefinedValidationCallback() { + $called = false; + $field = new HTMLCheckMatrix( self::$defaultOptions + array( + 'validation-callback' => function() use ( &$called ) { + $called = true; + return false; + }, + ) ); + $this->assertEquals( false, $this->validate( $field, array() ) ); + $this->assertTrue( $called ); + } + + public function testValidateRequiresArrayInput() { + $field = new HTMLCheckMatrix( self::$defaultOptions ); + $this->assertEquals( false, $this->validate( $field, null ) ); + $this->assertEquals( false, $this->validate( $field, true ) ); + $this->assertEquals( false, $this->validate( $field, 'abc' ) ); + $this->assertEquals( false, $this->validate( $field, new stdClass ) ); + $this->assertEquals( true, $this->validate( $field, array() ) ); + } + + public function testValidateAllowsOnlyKnownTags() { + $field = new HTMLCheckMatrix( self::$defaultOptions ); + $this->assertInternalType( 'string', $this->validate( $field, array( 'foo' ) ) ); + } + + public function testValidateAcceptsPartialTagList() { + $field = new HTMLCheckMatrix( self::$defaultOptions ); + $this->assertTrue( $this->validate( $field, array() ) ); + $this->assertTrue( $this->validate( $field, array( 'c1-r1' ) ) ); + $this->assertTrue( $this->validate( $field, array( 'c1-r1', 'c1-r2', 'c2-r1', 'c2-r2' ) ) ); + } + + /** + * This form object actually has no visibility into what happens later on, but essentially + * if the data submitted by the user passes validate the following is run: + * foreach ( $field->filterDataForSubmit( $data ) as $k => $v ) { + * $user->setOption( $k, $v ); + * } + */ + public function testValuesForcedOnRemainOn() { + $field = new HTMLCheckMatrix( self::$defaultOptions + array( + 'force-options-on' => array( 'c2-r1' ), + ) ); + $expected = array( + 'c1-r1' => false, + 'c1-r2' => false, + 'c2-r1' => true, + 'c2-r2' => false, + ); + $this->assertEquals( $expected, $field->filterDataForSubmit( array() ) ); + } + + public function testValuesForcedOffRemainOff() { + $field = new HTMLCheckMatrix( self::$defaultOptions + array( + 'force-options-off' => array( 'c1-r2', 'c2-r2' ), + ) ); + $expected = array( + 'c1-r1' => true, + 'c1-r2' => false, + 'c2-r1' => true, + 'c2-r2' => false, + ); + // array_keys on the result simulates submitting all fields checked + $this->assertEquals( $expected, $field->filterDataForSubmit( array_keys( $expected ) ) ); + } + + protected function validate( HTMLFormField $field, $submitted ) { + return $field->validate( + $submitted, + array( self::$defaultOptions['fieldname'] => $submitted ) + ); + } +} diff --git a/tests/phpunit/includes/HashRingTest.php b/tests/phpunit/includes/HashRingTest.php new file mode 100644 index 00000000..65f13696 --- /dev/null +++ b/tests/phpunit/includes/HashRingTest.php @@ -0,0 +1,53 @@ + 1, 's2' => 1, 's3' => 2, 's4' => 2, 's5' => 2, 's6' => 3 ) ); + + $locations = array(); + for ( $i = 0; $i < 20; $i++ ) { + $locations[ "hello$i"] = $ring->getLocation( "hello$i" ); + } + $expectedLocations = array( + "hello0" => "s5", + "hello1" => "s6", + "hello2" => "s2", + "hello3" => "s5", + "hello4" => "s6", + "hello5" => "s4", + "hello6" => "s5", + "hello7" => "s4", + "hello8" => "s5", + "hello9" => "s5", + "hello10" => "s3", + "hello11" => "s6", + "hello12" => "s1", + "hello13" => "s3", + "hello14" => "s3", + "hello15" => "s5", + "hello16" => "s4", + "hello17" => "s6", + "hello18" => "s6", + "hello19" => "s3" + ); + + $this->assertEquals( $expectedLocations, $locations, 'Items placed at proper locations' ); + + $locations = array(); + for ( $i = 0; $i < 5; $i++ ) { + $locations[ "hello$i"] = $ring->getLocations( "hello$i", 2 ); + } + + $expectedLocations = array( + "hello0" => array( "s5", "s6" ), + "hello1" => array( "s6", "s4" ), + "hello2" => array( "s2", "s1" ), + "hello3" => array( "s5", "s6" ), + "hello4" => array( "s6", "s4" ), + ); + $this->assertEquals( $expectedLocations, $locations, 'Items placed at proper locations' ); + } +} diff --git a/tests/phpunit/includes/HooksTest.php b/tests/phpunit/includes/HooksTest.php new file mode 100644 index 00000000..81dd4870 --- /dev/null +++ b/tests/phpunit/includes/HooksTest.php @@ -0,0 +1,158 @@ +assertSame( $expectedFoo, $foo, $msg ); + $this->assertSame( $expectedBar, $bar, $msg ); + } + + /** + * @dataProvider provideHooks + */ + public function testNewStyleHooks( $msg, $hook, $expectedFoo, $expectedBar ) { + $foo = $bar = 'original'; + + Hooks::register( 'MediaWikiHooksTest001', $hook ); + Hooks::run( 'MediaWikiHooksTest001', array( &$foo, &$bar ) ); + + $this->assertSame( $expectedFoo, $foo, $msg ); + $this->assertSame( $expectedBar, $bar, $msg ); + } + + public function testNewStyleHookInteraction() { + global $wgHooks; + + $a = new NothingClass(); + $b = new NothingClass(); + + $wgHooks['MediaWikiHooksTest001'][] = $a; + $this->assertTrue( Hooks::isRegistered( 'MediaWikiHooksTest001' ), 'Hook registered via $wgHooks should be noticed by Hooks::isRegistered' ); + + Hooks::register( 'MediaWikiHooksTest001', $b ); + $this->assertEquals( 2, count( Hooks::getHandlers( 'MediaWikiHooksTest001' ) ), 'Hooks::getHandlers() should return hooks registered via wgHooks as well as Hooks::register' ); + + $foo = 'quux'; + $bar = 'qaax'; + + Hooks::run( 'MediaWikiHooksTest001', array( &$foo, &$bar ) ); + $this->assertEquals( 1, $a->calls, 'Hooks::run() should run hooks registered via wgHooks as well as Hooks::register' ); + $this->assertEquals( 1, $b->calls, 'Hooks::run() should run hooks registered via wgHooks as well as Hooks::register' ); + } + + /** + * @expectedException MWException + */ + public function testUncallableFunction() { + Hooks::register( 'MediaWikiHooksTest001', 'ThisFunctionDoesntExist' ); + Hooks::run( 'MediaWikiHooksTest001', array() ); + } + + public function testFalseReturn() { + Hooks::register( 'MediaWikiHooksTest001', function ( &$foo ) { + return false; + } ); + Hooks::register( 'MediaWikiHooksTest001', function ( &$foo ) { + $foo = 'test'; + + return true; + } ); + $foo = 'original'; + Hooks::run( 'MediaWikiHooksTest001', array( &$foo ) ); + $this->assertSame( 'original', $foo, 'Hooks continued processing after a false return.' ); + } + + /** + * @expectedException FatalError + */ + public function testFatalError() { + Hooks::register( 'MediaWikiHooksTest001', function () { + return 'test'; + } ); + Hooks::run( 'MediaWikiHooksTest001', array() ); + } +} + +function NothingFunction( &$foo, &$bar ) { + $foo = 'changed-func'; + + return true; +} + +function NothingFunctionData( $data, &$foo, &$bar ) { + $foo = $data; + + return true; +} + +class NothingClass { + public $calls = 0; + + public static function someStatic( &$foo, &$bar ) { + $foo = 'changed-static'; + + return true; + } + + public function someNonStatic( &$foo, &$bar ) { + $this->calls++; + $foo = 'changed-nonstatic'; + $bar = 'changed-nonstatic'; + + return true; + } + + public function onMediaWikiHooksTest001( &$foo, &$bar ) { + $this->calls++; + $foo = 'changed-onevent'; + + return true; + } + + public function someNonStaticWithData( $data, &$foo, &$bar ) { + $this->calls++; + $foo = $data; + + return true; + } +} diff --git a/tests/phpunit/includes/HtmlFormatterTest.php b/tests/phpunit/includes/HtmlFormatterTest.php new file mode 100644 index 00000000..a37df74f --- /dev/null +++ b/tests/phpunit/includes/HtmlFormatterTest.php @@ -0,0 +1,81 @@ +filterContent(); + $html = $formatter->getText(); + $this->assertEquals( self::normalize( $expected ), self::normalize( $html ) ); + } + + private static function normalize( $s ) { + return str_replace( "\n", '', + str_replace( "\r", '', $s ) // "yay" to Windows! + ); + } + + public function getHtmlData() { + $removeImages = function( HtmlFormatter $f ) { + $f->setRemoveMedia(); + }; + $removeTags = function( HtmlFormatter $f ) { + $f->remove( array( 'table', '.foo', '#bar', 'div.baz' ) ); + }; + $flattenSomeStuff = function( HtmlFormatter $f ) { + $f->flatten( array( 's', 'div' ) ); + }; + $flattenEverything = function( HtmlFormatter $f ) { + $f->flattenAllTags(); + }; + return array( + // remove images if asked + array( + 'Blah', + '', + $removeImages, + ), + // basic tag removal + array( + '
foo
foo
foo
bar +foobar
test
+baz', + + '
test
+baz', + $removeTags, + ), + // don't flatten tags that start like chosen ones + array( + '
foo bar
', + 'foo bar', + $flattenSomeStuff, + ), + // total flattening + array( + '
bar2
', + 'bar2', + $flattenEverything, + ), + // UTF-8 preservation and security + array( + '<Тест!> &<&&&&', + '<Тест!> &<&&&&', + ), + // https://bugzilla.wikimedia.org/show_bug.cgi?id=53086 + array( + 'Foo[1] Bar', + 'Foo[1] Bar', + ), + ); + } +} diff --git a/tests/phpunit/includes/HtmlTest.php b/tests/phpunit/includes/HtmlTest.php new file mode 100644 index 00000000..1c62d032 --- /dev/null +++ b/tests/phpunit/includes/HtmlTest.php @@ -0,0 +1,631 @@ +setNamespaces( array( + -2 => 'Media', + -1 => 'Special', + 0 => '', + 1 => 'Talk', + 2 => 'User', + 3 => 'User_talk', + 4 => 'MyWiki', + 5 => 'MyWiki_Talk', + 6 => 'File', + 7 => 'File_talk', + 8 => 'MediaWiki', + 9 => 'MediaWiki_talk', + 10 => 'Template', + 11 => 'Template_talk', + 14 => 'Category', + 15 => 'Category_talk', + 100 => 'Custom', + 101 => 'Custom_talk', + ) ); + + $this->setMwGlobals( array( + 'wgLanguageCode' => $langCode, + 'wgContLang' => $langObj, + 'wgLang' => $langObj, + 'wgWellFormedXml' => false, + ) ); + } + + public function testElementBasics() { + $this->assertEquals( + '', + Html::element( 'img', null, '' ), + 'No close tag for short-tag elements' + ); + + $this->assertEquals( + '', + Html::element( 'element', null, null ), + 'Close tag for empty element (null, null)' + ); + + $this->assertEquals( + '', + Html::element( 'element', array(), '' ), + 'Close tag for empty element (array, string)' + ); + + $this->setMwGlobals( 'wgWellFormedXml', true ); + + $this->assertEquals( + '', + Html::element( 'img', null, '' ), + 'Self-closing tag for short-tag elements (wgWellFormedXml = true)' + ); + } + + public function dataXmlMimeType() { + return array( + // ( $mimetype, $isXmlMimeType ) + # HTML is not an XML MimeType + array( 'text/html', false ), + # XML is an XML MimeType + array( 'text/xml', true ), + array( 'application/xml', true ), + # XHTML is an XML MimeType + array( 'application/xhtml+xml', true ), + # Make sure other +xml MimeTypes are supported + # SVG is another random MimeType even though we don't use it + array( 'image/svg+xml', true ), + # Complete random other MimeTypes are not XML + array( 'text/plain', false ), + ); + } + + /** + * @dataProvider dataXmlMimeType + */ + public function testXmlMimeType( $mimetype, $isXmlMimeType ) { + $this->assertEquals( $isXmlMimeType, Html::isXmlMimeType( $mimetype ) ); + } + + public function testExpandAttributesSkipsNullAndFalse() { + + ### EMPTY ######## + $this->assertEmpty( + Html::expandAttributes( array( 'foo' => null ) ), + 'skip keys with null value' + ); + $this->assertEmpty( + Html::expandAttributes( array( 'foo' => false ) ), + 'skip keys with false value' + ); + $this->assertNotEmpty( + Html::expandAttributes( array( 'foo' => '' ) ), + 'keep keys with an empty string' + ); + } + + public function testExpandAttributesForBooleans() { + $this->assertEquals( + '', + Html::expandAttributes( array( 'selected' => false ) ), + 'Boolean attributes do not generates output when value is false' + ); + $this->assertEquals( + '', + Html::expandAttributes( array( 'selected' => null ) ), + 'Boolean attributes do not generates output when value is null' + ); + + $this->assertEquals( + ' selected', + Html::expandAttributes( array( 'selected' => true ) ), + 'Boolean attributes have no value when value is true' + ); + $this->assertEquals( + ' selected', + Html::expandAttributes( array( 'selected' ) ), + 'Boolean attributes have no value when value is true (passed as numerical array)' + ); + + $this->setMwGlobals( 'wgWellFormedXml', true ); + + $this->assertEquals( + ' selected=""', + Html::expandAttributes( array( 'selected' => true ) ), + 'Boolean attributes have empty string value when value is true (wgWellFormedXml)' + ); + } + + /** + * Test for Html::expandAttributes() + * Please note it output a string prefixed with a space! + */ + public function testExpandAttributesVariousExpansions() { + ### NOT EMPTY #### + $this->assertEquals( + ' empty_string=""', + Html::expandAttributes( array( 'empty_string' => '' ) ), + 'Empty string is always quoted' + ); + $this->assertEquals( + ' key=value', + Html::expandAttributes( array( 'key' => 'value' ) ), + 'Simple string value needs no quotes' + ); + $this->assertEquals( + ' one=1', + Html::expandAttributes( array( 'one' => 1 ) ), + 'Number 1 value needs no quotes' + ); + $this->assertEquals( + ' zero=0', + Html::expandAttributes( array( 'zero' => 0 ) ), + 'Number 0 value needs no quotes' + ); + + $this->setMwGlobals( 'wgWellFormedXml', true ); + + $this->assertEquals( + ' empty_string=""', + Html::expandAttributes( array( 'empty_string' => '' ) ), + 'Attribute values are always quoted (wgWellFormedXml): Empty string' + ); + $this->assertEquals( + ' key="value"', + Html::expandAttributes( array( 'key' => 'value' ) ), + 'Attribute values are always quoted (wgWellFormedXml): Simple string' + ); + $this->assertEquals( + ' one="1"', + Html::expandAttributes( array( 'one' => 1 ) ), + 'Attribute values are always quoted (wgWellFormedXml): Number 1' + ); + $this->assertEquals( + ' zero="0"', + Html::expandAttributes( array( 'zero' => 0 ) ), + 'Attribute values are always quoted (wgWellFormedXml): Number 0' + ); + } + + /** + * Html::expandAttributes has special features for HTML + * attributes that use space separated lists and also + * allows arrays to be used as values. + */ + public function testExpandAttributesListValueAttributes() { + ### STRING VALUES + $this->assertEquals( + ' class="redundant spaces here"', + Html::expandAttributes( array( 'class' => ' redundant spaces here ' ) ), + 'Normalization should strip redundant spaces' + ); + $this->assertEquals( + ' class="foo bar"', + Html::expandAttributes( array( 'class' => 'foo bar foo bar bar' ) ), + 'Normalization should remove duplicates in string-lists' + ); + ### "EMPTY" ARRAY VALUES + $this->assertEquals( + ' class=""', + Html::expandAttributes( array( 'class' => array() ) ), + 'Value with an empty array' + ); + $this->assertEquals( + ' class=""', + Html::expandAttributes( array( 'class' => array( null, '', ' ', ' ' ) ) ), + 'Array with null, empty string and spaces' + ); + ### NON-EMPTY ARRAY VALUES + $this->assertEquals( + ' class="foo bar"', + Html::expandAttributes( array( 'class' => array( + 'foo', + 'bar', + 'foo', + 'bar', + 'bar', + ) ) ), + 'Normalization should remove duplicates in the array' + ); + $this->assertEquals( + ' class="foo bar"', + Html::expandAttributes( array( 'class' => array( + 'foo bar', + 'bar foo', + 'foo', + 'bar bar', + ) ) ), + 'Normalization should remove duplicates in string-lists in the array' + ); + } + + /** + * Test feature added by r96188, let pass attributes values as + * a PHP array. Restricted to class,rel, accesskey. + */ + public function testExpandAttributesSpaceSeparatedAttributesWithBoolean() { + $this->assertEquals( + ' class="booltrue one"', + Html::expandAttributes( array( 'class' => array( + 'booltrue' => true, + 'one' => 1, + + # Method use isset() internally, make sure we do discard + # attributes values which have been assigned well known values + 'emptystring' => '', + 'boolfalse' => false, + 'zero' => 0, + 'null' => null, + ) ) ) + ); + } + + /** + * How do we handle duplicate keys in HTML attributes expansion? + * We could pass a "class" the values: 'GREEN' and array( 'GREEN' => false ) + * The later will take precedence. + * + * Feature added by r96188 + */ + public function testValueIsAuthoritativeInSpaceSeparatedAttributesArrays() { + $this->assertEquals( + ' class=""', + Html::expandAttributes( array( 'class' => array( + 'GREEN', + 'GREEN' => false, + 'GREEN', + ) ) ) + ); + } + + public function testNamespaceSelector() { + $this->assertEquals( + '', + Html::namespaceSelector(), + 'Basic namespace selector without custom options' + ); + + $this->assertEquals( + ' ' . + '', + Html::namespaceSelector( + array( 'selected' => '2', 'all' => 'all', 'label' => 'Select a namespace:' ), + array( 'name' => 'wpNamespace', 'id' => 'mw-test-namespace' ) + ), + 'Basic namespace selector with custom values' + ); + + $this->assertEquals( + ' ' . + '', + Html::namespaceSelector( + array( 'label' => 'Select a namespace:' ) + ), + 'Basic namespace selector with a custom label but no id attribtue for the ' . "\n" . + '' . "\n" . + '' . "\n" . + '' . "\n" . + '' . "\n" . + '' . "\n" . + '' . "\n" . + '' . "\n" . + '' . "\n" . + '' . "\n" . + '' . "\n" . + '' . "\n" . + '', + Html::namespaceSelector( + array( 'exclude' => array( 0, 1, 3, 100, 101 ) ) + ), + 'Namespace selector namespace filtering.' + ); + } + + public function testCanDisableANamespaces() { + $this->assertEquals( + '', + Html::namespaceSelector( array( + 'disable' => array( 0, 1, 2, 3, 4 ) + ) ), + 'Namespace selector namespace disabling' + ); + } + + /** + * @dataProvider provideHtml5InputTypes + */ + public function testHtmlElementAcceptsNewHtml5TypesInHtml5Mode( $HTML5InputType ) { + $this->assertEquals( + '', + Html::element( 'input', array( 'type' => $HTML5InputType ) ), + 'In HTML5, HTML::element() should accept type="' . $HTML5InputType . '"' + ); + } + + /** + * List of input element types values introduced by HTML5 + * Full list at http://www.w3.org/TR/html-markup/input.html + */ + public static function provideHtml5InputTypes() { + $types = array( + 'datetime', + 'datetime-local', + 'date', + 'month', + 'time', + 'week', + 'number', + 'range', + 'email', + 'url', + 'search', + 'tel', + 'color', + ); + $cases = array(); + foreach ( $types as $type ) { + $cases[] = array( $type ); + } + + return $cases; + } + + /** + * Test out Html::element drops or enforces default value + * @covers Html::dropDefaults + * @dataProvider provideElementsWithAttributesHavingDefaultValues + */ + public function testDropDefaults( $expected, $element, $attribs, $message = '' ) { + $this->assertEquals( $expected, Html::element( $element, $attribs ), $message ); + } + + public static function provideElementsWithAttributesHavingDefaultValues() { + # Use cases in a concise format: + # , , [, ] + # Will be mapped to Html::element() + $cases = array(); + + ### Generic cases, match $attribDefault static array + $cases[] = array( '', + 'area', array( 'shape' => 'rect' ) + ); + + $cases[] = array( '', + 'button', array( 'formaction' => 'GET' ) + ); + $cases[] = array( '', + 'button', array( 'formenctype' => 'application/x-www-form-urlencoded' ) + ); + + $cases[] = array( '', + 'canvas', array( 'height' => '150' ) + ); + $cases[] = array( '', + 'canvas', array( 'width' => '300' ) + ); + # Also check with numeric values + $cases[] = array( '', + 'canvas', array( 'height' => 150 ) + ); + $cases[] = array( '', + 'canvas', array( 'width' => 300 ) + ); + + $cases[] = array( '', + 'command', array( 'type' => 'command' ) + ); + + $cases[] = array( '
', + 'form', array( 'action' => 'GET' ) + ); + $cases[] = array( '
', + 'form', array( 'autocomplete' => 'on' ) + ); + $cases[] = array( '
', + 'form', array( 'enctype' => 'application/x-www-form-urlencoded' ) + ); + + $cases[] = array( '', + 'input', array( 'formaction' => 'GET' ) + ); + $cases[] = array( '', + 'input', array( 'type' => 'text' ) + ); + + $cases[] = array( '', + 'keygen', array( 'keytype' => 'rsa' ) + ); + + $cases[] = array( '', + 'link', array( 'media' => 'all' ) + ); + + $cases[] = array( '', + 'menu', array( 'type' => 'list' ) + ); + + $cases[] = array( '', + 'script', array( 'type' => 'text/javascript' ) + ); + + $cases[] = array( '', + 'style', array( 'media' => 'all' ) + ); + $cases[] = array( '', + 'style', array( 'type' => 'text/css' ) + ); + + $cases[] = array( '', + 'textarea', array( 'wrap' => 'soft' ) + ); + + ### SPECIFIC CASES + + # + $cases[] = array( '', + 'link', array( 'type' => 'text/css' ) + ); + + # specific handling + $cases[] = array( '', + 'input', array( 'type' => 'checkbox', 'value' => 'on' ), + 'Default value "on" is stripped of checkboxes', + ); + $cases[] = array( '', + 'input', array( 'type' => 'radio', 'value' => 'on' ), + 'Default value "on" is stripped of radio buttons', + ); + $cases[] = array( '', + 'input', array( 'type' => 'submit', 'value' => 'Submit' ), + 'Default value "Submit" is kept on submit buttons (for possible l10n issues)', + ); + $cases[] = array( '', + 'input', array( 'type' => 'color', 'value' => '' ), + ); + $cases[] = array( '', + 'input', array( 'type' => 'range', 'value' => '' ), + ); + + # ', + 'button', array( 'type' => 'submit' ), + 'According to standard the default type is "submit". Depending on compatibility mode IE might use "button", instead.', + ); + + # ', + 'select', array( 'size' => '4', 'multiple' => true ), + ); + # .. with numeric value + $cases[] = array( '', + 'select', array( 'size' => 4, 'multiple' => true ), + ); + $cases[] = array( '', + 'select', array( 'size' => '1', 'multiple' => false ), + ); + # .. with numeric value + $cases[] = array( '', + 'select', array( 'size' => 1, 'multiple' => false ), + ); + + # Passing an array as value + $cases[] = array( '', + 'a', array( 'class' => array( 'css-class-one', 'css-class-two' ) ), + "dropDefaults accepts values given as an array" + ); + + # FIXME: doDropDefault should remove defaults given in an array + # Expected should be '' + $cases[] = array( '', + 'a', array( 'class' => array( '', '' ) ), + "dropDefaults accepts values given as an array" + ); + + # Craft the Html elements + $ret = array(); + foreach ( $cases as $case ) { + $ret[] = array( + $case[0], + $case[1], $case[2], + isset( $case[3] ) ? $case[3] : '' + ); + } + + return $ret; + } + + public function testFormValidationBlacklist() { + $this->assertEmpty( + Html::expandAttributes( array( 'min' => 1, 'max' => 100, 'pattern' => 'abc', 'required' => true, 'step' => 2 ) ), + 'Blacklist form validation attributes.' + ); + $this->assertEquals( + ' step=any', + Html::expandAttributes( array( 'min' => 1, 'max' => 100, 'pattern' => 'abc', 'required' => true, 'step' => 'any' ) ), + 'Allow special case "step=any".' + ); + } +} diff --git a/tests/phpunit/includes/HttpTest.php b/tests/phpunit/includes/HttpTest.php new file mode 100644 index 00000000..11d8ed60 --- /dev/null +++ b/tests/phpunit/includes/HttpTest.php @@ -0,0 +1,216 @@ +assertEquals( $expected, $ok, $msg ); + } + + public static function cookieDomains() { + return array( + array( false, "org" ), + array( false, ".org" ), + array( true, "wikipedia.org" ), + array( true, ".wikipedia.org" ), + array( false, "co.uk" ), + array( false, ".co.uk" ), + array( false, "gov.uk" ), + array( false, ".gov.uk" ), + array( true, "supermarket.uk" ), + array( false, "uk" ), + array( false, ".uk" ), + array( false, "127.0.0." ), + array( false, "127." ), + array( false, "127.0.0.1." ), + array( true, "127.0.0.1" ), + array( false, "333.0.0.1" ), + array( true, "example.com" ), + array( false, "example.com." ), + array( true, ".example.com" ), + + array( true, ".example.com", "www.example.com" ), + array( false, "example.com", "www.example.com" ), + array( true, "127.0.0.1", "127.0.0.1" ), + array( false, "127.0.0.1", "localhost" ), + ); + } + + /** + * Test Http::isValidURI() + * @bug 27854 : Http::isValidURI is too lax + * @dataProvider provideURI + * @covers Http::isValidURI + */ + public function testIsValidUri( $expect, $URI, $message = '' ) { + $this->assertEquals( + $expect, + (bool)Http::isValidURI( $URI ), + $message + ); + } + + /** + * Feeds URI to test a long regular expression in Http::isValidURI + */ + public static function provideURI() { + /** Format: 'boolean expectation', 'URI to test', 'Optional message' */ + return array( + array( false, '¿non sens before!! http://a', 'Allow anything before URI' ), + + # (http|https) - only two schemes allowed + array( true, 'http://www.example.org/' ), + array( true, 'https://www.example.org/' ), + array( true, 'http://www.example.org', 'URI without directory' ), + array( true, 'http://a', 'Short name' ), + array( true, 'http://étoile', 'Allow UTF-8 in hostname' ), # 'étoile' is french for 'star' + array( false, '\\host\directory', 'CIFS share' ), + array( false, 'gopher://host/dir', 'Reject gopher scheme' ), + array( false, 'telnet://host', 'Reject telnet scheme' ), + + # :\/\/ - double slashes + array( false, 'http//example.org', 'Reject missing colon in protocol' ), + array( false, 'http:/example.org', 'Reject missing slash in protocol' ), + array( false, 'http:example.org', 'Must have two slashes' ), + # Following fail since hostname can be made of anything + array( false, 'http:///example.org', 'Must have exactly two slashes, not three' ), + + # (\w+:{0,1}\w*@)? - optional user:pass + array( true, 'http://user@host', 'Username provided' ), + array( true, 'http://user:@host', 'Username provided, no password' ), + array( true, 'http://user:pass@host', 'Username and password provided' ), + + # (\S+) - host part is made of anything not whitespaces + array( false, 'http://!"èèè¿¿¿~~\'', 'hostname is made of any non whitespace' ), + array( false, 'http://exam:ple.org/', 'hostname can not use colons!' ), + + # (:[0-9]+)? - port number + array( true, 'http://example.org:80/' ), + array( true, 'https://example.org:80/' ), + array( true, 'http://example.org:443/' ), + array( true, 'https://example.org:443/' ), + + # Part after the hostname is / or / with something else + array( true, 'http://example/#' ), + array( true, 'http://example/!' ), + array( true, 'http://example/:' ), + array( true, 'http://example/.' ), + array( true, 'http://example/?' ), + array( true, 'http://example/+' ), + array( true, 'http://example/=' ), + array( true, 'http://example/&' ), + array( true, 'http://example/%' ), + array( true, 'http://example/@' ), + array( true, 'http://example/-' ), + array( true, 'http://example//' ), + array( true, 'http://example/&' ), + + # Fragment + array( true, 'http://exam#ple.org', ), # This one is valid, really! + array( true, 'http://example.org:80#anchor' ), + array( true, 'http://example.org/?id#anchor' ), + array( true, 'http://example.org/?#anchor' ), + + array( false, 'http://a ¿non !!sens after', 'Allow anything after URI' ), + ); + } + + /** + * Warning: + * + * These tests are for code that makes use of an artifact of how CURL + * handles header reporting on redirect pages, and will need to be + * rewritten when bug 29232 is taken care of (high-level handling of + * HTTP redirects). + */ + public function testRelativeRedirections() { + $h = MWHttpRequestTester::factory( 'http://oldsite/file.ext' ); + + # Forge a Location header + $h->setRespHeaders( 'location', array( + 'http://newsite/file.ext', + '/newfile.ext', + ) + ); + # Verify we correctly fix the Location + $this->assertEquals( + 'http://newsite/newfile.ext', + $h->getFinalUrl(), + "Relative file path Location: interpreted as full URL" + ); + + $h->setRespHeaders( 'location', array( + 'https://oldsite/file.ext' + ) + ); + $this->assertEquals( + 'https://oldsite/file.ext', + $h->getFinalUrl(), + "Location to the HTTPS version of the site" + ); + + $h->setRespHeaders( 'location', array( + '/anotherfile.ext', + 'http://anotherfile/hoster.ext', + 'https://anotherfile/hoster.ext' + ) + ); + $this->assertEquals( + 'https://anotherfile/hoster.ext', + $h->getFinalUrl( "Relative file path Location: should keep the latest host and scheme!" ) + ); + } +} + +/** + * Class to let us overwrite MWHttpRequest respHeaders variable + */ +class MWHttpRequestTester extends MWHttpRequest { + + // function derived from the MWHttpRequest factory function but + // returns appropriate tester class here + public static function factory( $url, $options = null ) { + if ( !Http::$httpEngine ) { + Http::$httpEngine = function_exists( 'curl_init' ) ? 'curl' : 'php'; + } elseif ( Http::$httpEngine == 'curl' && !function_exists( 'curl_init' ) ) { + throw new MWException( __METHOD__ . ': curl (http://php.net/curl) is not installed, but' . + 'Http::$httpEngine is set to "curl"' ); + } + + switch ( Http::$httpEngine ) { + case 'curl': + return new CurlHttpRequestTester( $url, $options ); + case 'php': + if ( !wfIniGetBool( 'allow_url_fopen' ) ) { + throw new MWException( __METHOD__ . ': allow_url_fopen needs to be enabled for pure PHP' . + ' http requests to work. If possible, curl should be used instead. See http://php.net/curl.' ); + } + + return new PhpHttpRequestTester( $url, $options ); + default: + } + } +} + +class CurlHttpRequestTester extends CurlHttpRequest { + function setRespHeaders( $name, $value ) { + $this->respHeaders[$name] = $value; + } +} + +class PhpHttpRequestTester extends PhpHttpRequest { + function setRespHeaders( $name, $value ) { + $this->respHeaders[$name] = $value; + } +} diff --git a/tests/phpunit/includes/IPTest.php b/tests/phpunit/includes/IPTest.php new file mode 100644 index 00000000..c074eea6 --- /dev/null +++ b/tests/phpunit/includes/IPTest.php @@ -0,0 +1,595 @@ +assertFalse( IP::isIPAddress( false ), 'Boolean false is not an IP' ); + $this->assertFalse( IP::isIPAddress( true ), 'Boolean true is not an IP' ); + $this->assertFalse( IP::isIPAddress( "" ), 'Empty string is not an IP' ); + $this->assertFalse( IP::isIPAddress( 'abc' ), 'Garbage IP string' ); + $this->assertFalse( IP::isIPAddress( ':' ), 'Single ":" is not an IP' ); + $this->assertFalse( IP::isIPAddress( '2001:0DB8::A:1::1' ), 'IPv6 with a double :: occurrence' ); + $this->assertFalse( IP::isIPAddress( '2001:0DB8::A:1::' ), 'IPv6 with a double :: occurrence, last at end' ); + $this->assertFalse( IP::isIPAddress( '::2001:0DB8::5:1' ), 'IPv6 with a double :: occurrence, firt at beginning' ); + $this->assertFalse( IP::isIPAddress( '124.24.52' ), 'IPv4 not enough quads' ); + $this->assertFalse( IP::isIPAddress( '24.324.52.13' ), 'IPv4 out of range' ); + $this->assertFalse( IP::isIPAddress( '.24.52.13' ), 'IPv4 starts with period' ); + $this->assertFalse( IP::isIPAddress( 'fc:100:300' ), 'IPv6 with only 3 words' ); + + $this->assertTrue( IP::isIPAddress( '::' ), 'RFC 4291 IPv6 Unspecified Address' ); + $this->assertTrue( IP::isIPAddress( '::1' ), 'RFC 4291 IPv6 Loopback Address' ); + $this->assertTrue( IP::isIPAddress( '74.24.52.13/20', 'IPv4 range' ) ); + $this->assertTrue( IP::isIPAddress( 'fc:100:a:d:1:e:ac:0/24' ), 'IPv6 range' ); + $this->assertTrue( IP::isIPAddress( 'fc::100:a:d:1:e:ac/96' ), 'IPv6 range with "::"' ); + + $validIPs = array( 'fc:100::', 'fc:100:a:d:1:e:ac::', 'fc::100', '::fc:100:a:d:1:e:ac', + '::fc', 'fc::100:a:d:1:e:ac', 'fc:100:a:d:1:e:ac:0', '124.24.52.13', '1.24.52.13' ); + foreach ( $validIPs as $ip ) { + $this->assertTrue( IP::isIPAddress( $ip ), "$ip is a valid IP address" ); + } + } + + /** + * @covers IP::isIPv6 + */ + public function testisIPv6() { + $this->assertFalse( IP::isIPv6( ':fc:100::' ), 'IPv6 starting with lone ":"' ); + $this->assertFalse( IP::isIPv6( 'fc:100:::' ), 'IPv6 ending with a ":::"' ); + $this->assertFalse( IP::isIPv6( 'fc:300' ), 'IPv6 with only 2 words' ); + $this->assertFalse( IP::isIPv6( 'fc:100:300' ), 'IPv6 with only 3 words' ); + + $this->assertTrue( IP::isIPv6( 'fc:100::' ) ); + $this->assertTrue( IP::isIPv6( 'fc:100:a::' ) ); + $this->assertTrue( IP::isIPv6( 'fc:100:a:d::' ) ); + $this->assertTrue( IP::isIPv6( 'fc:100:a:d:1::' ) ); + $this->assertTrue( IP::isIPv6( 'fc:100:a:d:1:e::' ) ); + $this->assertTrue( IP::isIPv6( 'fc:100:a:d:1:e:ac::' ) ); + + $this->assertFalse( IP::isIPv6( 'fc:100:a:d:1:e:ac:0::' ), 'IPv6 with 8 words ending with "::"' ); + $this->assertFalse( IP::isIPv6( 'fc:100:a:d:1:e:ac:0:1::' ), 'IPv6 with 9 words ending with "::"' ); + + $this->assertFalse( IP::isIPv6( ':::' ) ); + $this->assertFalse( IP::isIPv6( '::0:' ), 'IPv6 ending in a lone ":"' ); + + $this->assertTrue( IP::isIPv6( '::' ), 'IPv6 zero address' ); + $this->assertTrue( IP::isIPv6( '::0' ) ); + $this->assertTrue( IP::isIPv6( '::fc' ) ); + $this->assertTrue( IP::isIPv6( '::fc:100' ) ); + $this->assertTrue( IP::isIPv6( '::fc:100:a' ) ); + $this->assertTrue( IP::isIPv6( '::fc:100:a:d' ) ); + $this->assertTrue( IP::isIPv6( '::fc:100:a:d:1' ) ); + $this->assertTrue( IP::isIPv6( '::fc:100:a:d:1:e' ) ); + $this->assertTrue( IP::isIPv6( '::fc:100:a:d:1:e:ac' ) ); + + $this->assertFalse( IP::isIPv6( '::fc:100:a:d:1:e:ac:0' ), 'IPv6 with "::" and 8 words' ); + $this->assertFalse( IP::isIPv6( '::fc:100:a:d:1:e:ac:0:1' ), 'IPv6 with 9 words' ); + + $this->assertFalse( IP::isIPv6( ':fc::100' ), 'IPv6 starting with lone ":"' ); + $this->assertFalse( IP::isIPv6( 'fc::100:' ), 'IPv6 ending with lone ":"' ); + $this->assertFalse( IP::isIPv6( 'fc:::100' ), 'IPv6 with ":::" in the middle' ); + + $this->assertTrue( IP::isIPv6( 'fc::100' ), 'IPv6 with "::" and 2 words' ); + $this->assertTrue( IP::isIPv6( 'fc::100:a' ), 'IPv6 with "::" and 3 words' ); + $this->assertTrue( IP::isIPv6( 'fc::100:a:d', 'IPv6 with "::" and 4 words' ) ); + $this->assertTrue( IP::isIPv6( 'fc::100:a:d:1' ), 'IPv6 with "::" and 5 words' ); + $this->assertTrue( IP::isIPv6( 'fc::100:a:d:1:e' ), 'IPv6 with "::" and 6 words' ); + $this->assertTrue( IP::isIPv6( 'fc::100:a:d:1:e:ac' ), 'IPv6 with "::" and 7 words' ); + $this->assertTrue( IP::isIPv6( '2001::df' ), 'IPv6 with "::" and 2 words' ); + $this->assertTrue( IP::isIPv6( '2001:5c0:1400:a::df' ), 'IPv6 with "::" and 5 words' ); + $this->assertTrue( IP::isIPv6( '2001:5c0:1400:a::df:2' ), 'IPv6 with "::" and 6 words' ); + + $this->assertFalse( IP::isIPv6( 'fc::100:a:d:1:e:ac:0' ), 'IPv6 with "::" and 8 words' ); + $this->assertFalse( IP::isIPv6( 'fc::100:a:d:1:e:ac:0:1' ), 'IPv6 with 9 words' ); + + $this->assertTrue( IP::isIPv6( 'fc:100:a:d:1:e:ac:0' ) ); + } + + /** + * @covers IP::isIPv4 + */ + public function testisIPv4() { + $this->assertFalse( IP::isIPv4( false ), 'Boolean false is not an IP' ); + $this->assertFalse( IP::isIPv4( true ), 'Boolean true is not an IP' ); + $this->assertFalse( IP::isIPv4( "" ), 'Empty string is not an IP' ); + $this->assertFalse( IP::isIPv4( 'abc' ) ); + $this->assertFalse( IP::isIPv4( ':' ) ); + $this->assertFalse( IP::isIPv4( '124.24.52' ), 'IPv4 not enough quads' ); + $this->assertFalse( IP::isIPv4( '24.324.52.13' ), 'IPv4 out of range' ); + $this->assertFalse( IP::isIPv4( '.24.52.13' ), 'IPv4 starts with period' ); + + $this->assertTrue( IP::isIPv4( '124.24.52.13' ) ); + $this->assertTrue( IP::isIPv4( '1.24.52.13' ) ); + $this->assertTrue( IP::isIPv4( '74.24.52.13/20', 'IPv4 range' ) ); + } + + /** + * @covers IP::isValid + */ + public function testValidIPs() { + foreach ( range( 0, 255 ) as $i ) { + $a = sprintf( "%03d", $i ); + $b = sprintf( "%02d", $i ); + $c = sprintf( "%01d", $i ); + foreach ( array_unique( array( $a, $b, $c ) ) as $f ) { + $ip = "$f.$f.$f.$f"; + $this->assertTrue( IP::isValid( $ip ), "$ip is a valid IPv4 address" ); + } + } + foreach ( range( 0x0, 0xFFFF, 0xF ) as $i ) { + $a = sprintf( "%04x", $i ); + $b = sprintf( "%03x", $i ); + $c = sprintf( "%02x", $i ); + foreach ( array_unique( array( $a, $b, $c ) ) as $f ) { + $ip = "$f:$f:$f:$f:$f:$f:$f:$f"; + $this->assertTrue( IP::isValid( $ip ), "$ip is a valid IPv6 address" ); + } + } + // test with some abbreviations + $this->assertFalse( IP::isValid( ':fc:100::' ), 'IPv6 starting with lone ":"' ); + $this->assertFalse( IP::isValid( 'fc:100:::' ), 'IPv6 ending with a ":::"' ); + $this->assertFalse( IP::isValid( 'fc:300' ), 'IPv6 with only 2 words' ); + $this->assertFalse( IP::isValid( 'fc:100:300' ), 'IPv6 with only 3 words' ); + + $this->assertTrue( IP::isValid( 'fc:100::' ) ); + $this->assertTrue( IP::isValid( 'fc:100:a:d:1:e::' ) ); + $this->assertTrue( IP::isValid( 'fc:100:a:d:1:e:ac::' ) ); + + $this->assertTrue( IP::isValid( 'fc::100' ), 'IPv6 with "::" and 2 words' ); + $this->assertTrue( IP::isValid( 'fc::100:a' ), 'IPv6 with "::" and 3 words' ); + $this->assertTrue( IP::isValid( '2001::df' ), 'IPv6 with "::" and 2 words' ); + $this->assertTrue( IP::isValid( '2001:5c0:1400:a::df' ), 'IPv6 with "::" and 5 words' ); + $this->assertTrue( IP::isValid( '2001:5c0:1400:a::df:2' ), 'IPv6 with "::" and 6 words' ); + $this->assertTrue( IP::isValid( 'fc::100:a:d:1' ), 'IPv6 with "::" and 5 words' ); + $this->assertTrue( IP::isValid( 'fc::100:a:d:1:e:ac' ), 'IPv6 with "::" and 7 words' ); + + $this->assertFalse( IP::isValid( 'fc:100:a:d:1:e:ac:0::' ), 'IPv6 with 8 words ending with "::"' ); + $this->assertFalse( IP::isValid( 'fc:100:a:d:1:e:ac:0:1::' ), 'IPv6 with 9 words ending with "::"' ); + } + + /** + * @covers IP::isValid + */ + public function testInvalidIPs() { + // Out of range... + foreach ( range( 256, 999 ) as $i ) { + $a = sprintf( "%03d", $i ); + $b = sprintf( "%02d", $i ); + $c = sprintf( "%01d", $i ); + foreach ( array_unique( array( $a, $b, $c ) ) as $f ) { + $ip = "$f.$f.$f.$f"; + $this->assertFalse( IP::isValid( $ip ), "$ip is not a valid IPv4 address" ); + } + } + foreach ( range( 'g', 'z' ) as $i ) { + $a = sprintf( "%04s", $i ); + $b = sprintf( "%03s", $i ); + $c = sprintf( "%02s", $i ); + foreach ( array_unique( array( $a, $b, $c ) ) as $f ) { + $ip = "$f:$f:$f:$f:$f:$f:$f:$f"; + $this->assertFalse( IP::isValid( $ip ), "$ip is not a valid IPv6 address" ); + } + } + // Have CIDR + $ipCIDRs = array( + '212.35.31.121/32', + '212.35.31.121/18', + '212.35.31.121/24', + '::ff:d:321:5/96', + 'ff::d3:321:5/116', + 'c:ff:12:1:ea:d:321:5/120', + ); + foreach ( $ipCIDRs as $i ) { + $this->assertFalse( IP::isValid( $i ), + "$i is an invalid IP address because it is a block" ); + } + // Incomplete/garbage + $invalid = array( + 'www.xn--var-xla.net', + '216.17.184.G', + '216.17.184.1.', + '216.17.184', + '216.17.184.', + '256.17.184.1' + ); + foreach ( $invalid as $i ) { + $this->assertFalse( IP::isValid( $i ), "$i is an invalid IP address" ); + } + } + + /** + * @covers IP::isValidBlock + */ + public function testValidBlocks() { + $valid = array( + '116.17.184.5/32', + '0.17.184.5/30', + '16.17.184.1/24', + '30.242.52.14/1', + '10.232.52.13/8', + '30.242.52.14/0', + '::e:f:2001/96', + '::c:f:2001/128', + '::10:f:2001/70', + '::fe:f:2001/1', + '::6d:f:2001/8', + '::fe:f:2001/0', + ); + foreach ( $valid as $i ) { + $this->assertTrue( IP::isValidBlock( $i ), "$i is a valid IP block" ); + } + } + + /** + * @covers IP::isValidBlock + */ + public function testInvalidBlocks() { + $invalid = array( + '116.17.184.5/33', + '0.17.184.5/130', + '16.17.184.1/-1', + '10.232.52.13/*', + '7.232.52.13/ab', + '11.232.52.13/', + '::e:f:2001/129', + '::c:f:2001/228', + '::10:f:2001/-1', + '::6d:f:2001/*', + '::86:f:2001/ab', + '::23:f:2001/', + ); + foreach ( $invalid as $i ) { + $this->assertFalse( IP::isValidBlock( $i ), "$i is not a valid IP block" ); + } + } + + /** + * Improve IP::sanitizeIP() code coverage + * @todo Most probably incomplete + */ + public function testSanitizeIP() { + $this->assertNull( IP::sanitizeIP( '' ) ); + $this->assertNull( IP::sanitizeIP( ' ' ) ); + } + + /** + * @covers IP::toUnsigned + * @dataProvider provideToUnsigned + */ + public function testToUnsigned( $expected, $input ) { + $result = IP::toUnsigned( $input ); + $this->assertTrue( $result === false || is_string( $result ) || is_int( $result ) ); + $this->assertEquals( $expected, $result ); + } + + /** + * Provider for IP::testToUnsigned() + */ + public static function provideToUnsigned() { + return array( + array( 1, '0.0.0.1' ), + array( 16909060, '1.2.3.4' ), + array( 2130706433, '127.0.0.1' ), + array( '2147483648', '128.0.0.0' ), + array( '3735931646', '222.173.202.254' ), + array( pow( 2, 32 ) - 1, '255.255.255.255' ), + array( false, 'IN.VA.LI.D' ), + array( 1, '::1' ), + array( '42540766452641154071740215577757643572', '2001:0db8:85a3:0000:0000:8a2e:0370:7334' ), + array( '42540766452641154071740215577757643572', '2001:db8:85a3::8a2e:0370:7334' ), + array( false, 'IN:VA::LI:D' ), + array( false, ':::1' ) + ); + } + + /** + * @covers IP::toHex + * @dataProvider provideToHex + */ + public function testToHex( $expected, $input ) { + $result = IP::toHex( $input ); + $this->assertTrue( $result === false || is_string( $result ) ); + $this->assertEquals( $expected, $result ); + } + + /** + * Provider for IP::testToHex() + */ + public static function provideToHex() { + return array( + array( '00000001', '0.0.0.1' ), + array( '01020304', '1.2.3.4' ), + array( '7F000001', '127.0.0.1' ), + array( '80000000', '128.0.0.0' ), + array( 'DEADCAFE', '222.173.202.254' ), + array( 'FFFFFFFF', '255.255.255.255' ), + array( false, 'IN.VA.LI.D' ), + array( 'v6-00000000000000000000000000000001', '::1' ), + array( 'v6-20010DB885A3000000008A2E03707334', '2001:0db8:85a3:0000:0000:8a2e:0370:7334' ), + array( 'v6-20010DB885A3000000008A2E03707334', '2001:db8:85a3::8a2e:0370:7334' ), + array( false, 'IN:VA::LI:D' ), + array( false, ':::1' ) + ); + } + + /** + * @covers IP::isPublic + */ + public function testPrivateIPs() { + $private = array( 'fc00::3', 'fc00::ff', '::1', '10.0.0.1', '172.16.0.1', '192.168.0.1' ); + foreach ( $private as $p ) { + $this->assertFalse( IP::isPublic( $p ), "$p is not a public IP address" ); + } + $public = array( '2001:5c0:1000:a::133', 'fc::3', '00FC::' ); + foreach ( $public as $p ) { + $this->assertTrue( IP::isPublic( $p ), "$p is a public IP address" ); + } + } + + // Private wrapper used to test CIDR Parsing. + private function assertFalseCIDR( $CIDR, $msg = '' ) { + $ff = array( false, false ); + $this->assertEquals( $ff, IP::parseCIDR( $CIDR ), $msg ); + } + + // Private wrapper to test network shifting using only dot notation + private function assertNet( $expected, $CIDR ) { + $parse = IP::parseCIDR( $CIDR ); + $this->assertEquals( $expected, long2ip( $parse[0] ), "network shifting $CIDR" ); + } + + /** + * @covers IP::hexToQuad + */ + public function testHexToQuad() { + $this->assertEquals( '0.0.0.1', IP::hexToQuad( '00000001' ) ); + $this->assertEquals( '255.0.0.0', IP::hexToQuad( 'FF000000' ) ); + $this->assertEquals( '255.255.255.255', IP::hexToQuad( 'FFFFFFFF' ) ); + $this->assertEquals( '10.188.222.255', IP::hexToQuad( '0ABCDEFF' ) ); + // hex not left-padded... + $this->assertEquals( '0.0.0.0', IP::hexToQuad( '0' ) ); + $this->assertEquals( '0.0.0.1', IP::hexToQuad( '1' ) ); + $this->assertEquals( '0.0.0.255', IP::hexToQuad( 'FF' ) ); + $this->assertEquals( '0.0.255.0', IP::hexToQuad( 'FF00' ) ); + } + + /** + * @covers IP::hexToOctet + */ + public function testHexToOctet() { + $this->assertEquals( '0:0:0:0:0:0:0:1', + IP::hexToOctet( '00000000000000000000000000000001' ) ); + $this->assertEquals( '0:0:0:0:0:0:FF:3', + IP::hexToOctet( '00000000000000000000000000FF0003' ) ); + $this->assertEquals( '0:0:0:0:0:0:FF00:6', + IP::hexToOctet( '000000000000000000000000FF000006' ) ); + $this->assertEquals( '0:0:0:0:0:0:FCCF:FAFF', + IP::hexToOctet( '000000000000000000000000FCCFFAFF' ) ); + $this->assertEquals( 'FFFF:FFFF:FFFF:FFFF:FFFF:FFFF:FFFF:FFFF', + IP::hexToOctet( 'FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF' ) ); + // hex not left-padded... + $this->assertEquals( '0:0:0:0:0:0:0:0', IP::hexToOctet( '0' ) ); + $this->assertEquals( '0:0:0:0:0:0:0:1', IP::hexToOctet( '1' ) ); + $this->assertEquals( '0:0:0:0:0:0:0:FF', IP::hexToOctet( 'FF' ) ); + $this->assertEquals( '0:0:0:0:0:0:0:FFD0', IP::hexToOctet( 'FFD0' ) ); + $this->assertEquals( '0:0:0:0:0:0:FA00:0', IP::hexToOctet( 'FA000000' ) ); + $this->assertEquals( '0:0:0:0:0:0:FCCF:FAFF', IP::hexToOctet( 'FCCFFAFF' ) ); + } + + /** + * IP::parseCIDR() returns an array containing a signed IP address + * representing the network mask and the bit mask. + * @covers IP::parseCIDR + */ + public function testCIDRParsing() { + $this->assertFalseCIDR( '192.0.2.0', "missing mask" ); + $this->assertFalseCIDR( '192.0.2.0/', "missing bitmask" ); + + // Verify if statement + $this->assertFalseCIDR( '256.0.0.0/32', "invalid net" ); + $this->assertFalseCIDR( '192.0.2.0/AA', "mask not numeric" ); + $this->assertFalseCIDR( '192.0.2.0/-1', "mask < 0" ); + $this->assertFalseCIDR( '192.0.2.0/33', "mask > 32" ); + + // Check internal logic + # 0 mask always result in array(0,0) + $this->assertEquals( array( 0, 0 ), IP::parseCIDR( '192.0.0.2/0' ) ); + $this->assertEquals( array( 0, 0 ), IP::parseCIDR( '0.0.0.0/0' ) ); + $this->assertEquals( array( 0, 0 ), IP::parseCIDR( '255.255.255.255/0' ) ); + + // @todo FIXME: Add more tests. + + # This part test network shifting + $this->assertNet( '192.0.0.0', '192.0.0.2/24' ); + $this->assertNet( '192.168.5.0', '192.168.5.13/24' ); + $this->assertNet( '10.0.0.160', '10.0.0.161/28' ); + $this->assertNet( '10.0.0.0', '10.0.0.3/28' ); + $this->assertNet( '10.0.0.0', '10.0.0.3/30' ); + $this->assertNet( '10.0.0.4', '10.0.0.4/30' ); + $this->assertNet( '172.17.32.0', '172.17.35.48/21' ); + $this->assertNet( '10.128.0.0', '10.135.0.0/9' ); + $this->assertNet( '134.0.0.0', '134.0.5.1/8' ); + } + + /** + * @covers IP::canonicalize + */ + public function testIPCanonicalizeOnValidIp() { + $this->assertEquals( '192.0.2.152', IP::canonicalize( '192.0.2.152' ), + 'Canonicalization of a valid IP returns it unchanged' ); + } + + /** + * @covers IP::canonicalize + */ + public function testIPCanonicalizeMappedAddress() { + $this->assertEquals( + '192.0.2.152', + IP::canonicalize( '::ffff:192.0.2.152' ) + ); + $this->assertEquals( + '192.0.2.152', + IP::canonicalize( '::192.0.2.152' ) + ); + } + + /** + * Issues there are most probably from IP::toHex() or IP::parseRange() + * @covers IP::isInRange + * @dataProvider provideIPsAndRanges + */ + public function testIPIsInRange( $expected, $addr, $range, $message = '' ) { + $this->assertEquals( + $expected, + IP::isInRange( $addr, $range ), + $message + ); + } + + /** Provider for testIPIsInRange() */ + public static function provideIPsAndRanges() { + # Format: (expected boolean, address, range, optional message) + return array( + # IPv4 + array( true, '192.0.2.0', '192.0.2.0/24', 'Network address' ), + array( true, '192.0.2.77', '192.0.2.0/24', 'Simple address' ), + array( true, '192.0.2.255', '192.0.2.0/24', 'Broadcast address' ), + + array( false, '0.0.0.0', '192.0.2.0/24' ), + array( false, '255.255.255', '192.0.2.0/24' ), + + # IPv6 + array( false, '::1', '2001:DB8::/32' ), + array( false, '::', '2001:DB8::/32' ), + array( false, 'FE80::1', '2001:DB8::/32' ), + + array( true, '2001:DB8::', '2001:DB8::/32' ), + array( true, '2001:0DB8::', '2001:DB8::/32' ), + array( true, '2001:DB8::1', '2001:DB8::/32' ), + array( true, '2001:0DB8::1', '2001:DB8::/32' ), + array( true, '2001:0DB8:FFFF:FFFF:FFFF:FFFF:FFFF:FFFF', + '2001:DB8::/32' ), + + array( false, '2001:0DB8:F::', '2001:DB8::/96' ), + ); + } + + /** + * Test for IP::splitHostAndPort(). + * @dataProvider provideSplitHostAndPort + */ + public function testSplitHostAndPort( $expected, $input, $description ) { + $this->assertEquals( $expected, IP::splitHostAndPort( $input ), $description ); + } + + /** + * Provider for IP::splitHostAndPort() + */ + public static function provideSplitHostAndPort() { + return array( + array( false, '[', 'Unclosed square bracket' ), + array( false, '[::', 'Unclosed square bracket 2' ), + array( array( '::', false ), '::', 'Bare IPv6 0' ), + array( array( '::1', false ), '::1', 'Bare IPv6 1' ), + array( array( '::', false ), '[::]', 'Bracketed IPv6 0' ), + array( array( '::1', false ), '[::1]', 'Bracketed IPv6 1' ), + array( array( '::1', 80 ), '[::1]:80', 'Bracketed IPv6 with port' ), + array( false, '::x', 'Double colon but no IPv6' ), + array( array( 'x', 80 ), 'x:80', 'Hostname and port' ), + array( false, 'x:x', 'Hostname and invalid port' ), + array( array( 'x', false ), 'x', 'Plain hostname' ) + ); + } + + /** + * Test for IP::combineHostAndPort() + * @dataProvider provideCombineHostAndPort + */ + public function testCombineHostAndPort( $expected, $input, $description ) { + list( $host, $port, $defaultPort ) = $input; + $this->assertEquals( + $expected, + IP::combineHostAndPort( $host, $port, $defaultPort ), + $description ); + } + + /** + * Provider for IP::combineHostAndPort() + */ + public static function provideCombineHostAndPort() { + return array( + array( '[::1]', array( '::1', 2, 2 ), 'IPv6 default port' ), + array( '[::1]:2', array( '::1', 2, 3 ), 'IPv6 non-default port' ), + array( 'x', array( 'x', 2, 2 ), 'Normal default port' ), + array( 'x:2', array( 'x', 2, 3 ), 'Normal non-default port' ), + ); + } + + /** + * Test for IP::sanitizeRange() + * @dataProvider provideIPCIDRs + */ + public function testSanitizeRange( $input, $expected, $description ) { + $this->assertEquals( $expected, IP::sanitizeRange( $input ), $description ); + } + + /** + * Provider for IP::testSanitizeRange() + */ + public static function provideIPCIDRs() { + return array( + array( '35.56.31.252/16', '35.56.0.0/16', 'IPv4 range' ), + array( '135.16.21.252/24', '135.16.21.0/24', 'IPv4 range' ), + array( '5.36.71.252/32', '5.36.71.252/32', 'IPv4 silly range' ), + array( '5.36.71.252', '5.36.71.252', 'IPv4 non-range' ), + array( '0:1:2:3:4:c5:f6:7/96', '0:1:2:3:4:C5:0:0/96', 'IPv6 range' ), + array( '0:1:2:3:4:5:6:7/120', '0:1:2:3:4:5:6:0/120', 'IPv6 range' ), + array( '0:e1:2:3:4:5:e6:7/128', '0:E1:2:3:4:5:E6:7/128', 'IPv6 silly range' ), + array( '0:c1:A2:3:4:5:c6:7', '0:C1:A2:3:4:5:C6:7', 'IPv6 non range' ), + ); + } + + /** + * Test for IP::prettifyIP() + * @dataProvider provideIPsToPrettify + */ + public function testPrettifyIP( $ip, $prettified ) { + $this->assertEquals( $prettified, IP::prettifyIP( $ip ), "Prettify of $ip" ); + } + + /** + * Provider for IP::testPrettifyIP() + */ + public static function provideIPsToPrettify() { + return array( + array( '0:0:0:0:0:0:0:0', '::' ), + array( '0:0:0::0:0:0', '::' ), + array( '0:0:0:1:0:0:0:0', '0:0:0:1::' ), + array( '0:0::f', '::f' ), + array( '0::0:0:0:33:fef:b', '::33:fef:b' ), + array( '3f:535:0:0:0:0:e:fbb', '3f:535::e:fbb' ), + array( '0:0:fef:0:0:0:e:fbb', '0:0:fef::e:fbb' ), + array( 'abbc:2004::0:0:0:0', 'abbc:2004::' ), + array( 'cebc:2004:f:0:0:0:0:0', 'cebc:2004:f::' ), + array( '0:0:0:0:0:0:0:0/16', '::/16' ), + array( '0:0:0::0:0:0/64', '::/64' ), + array( '0:0::f/52', '::f/52' ), + array( '::0:0:33:fef:b/52', '::33:fef:b/52' ), + array( '3f:535:0:0:0:0:e:fbb/48', '3f:535::e:fbb/48' ), + array( '0:0:fef:0:0:0:e:fbb/96', '0:0:fef::e:fbb/96' ), + array( 'abbc:2004:0:0::0:0/40', 'abbc:2004::/40' ), + array( 'aebc:2004:f:0:0:0:0:0/80', 'aebc:2004:f::/80' ), + ); + } +} diff --git a/tests/phpunit/includes/LanguageConverterTest.php b/tests/phpunit/includes/LanguageConverterTest.php new file mode 100644 index 00000000..7c2134b9 --- /dev/null +++ b/tests/phpunit/includes/LanguageConverterTest.php @@ -0,0 +1,148 @@ +setMwGlobals( array( + 'wgContLang' => Language::factory( 'tg' ), + 'wgLanguageCode' => 'tg', + 'wgDefaultLanguageVariant' => false, + 'wgMemc' => new EmptyBagOStuff, + 'wgRequest' => new FauxRequest( array() ), + 'wgUser' => new User, + ) ); + + $this->lang = new LanguageToTest(); + $this->lc = new TestConverter( + $this->lang, 'tg', + array( 'tg', 'tg-latn' ) + ); + } + + protected function tearDown() { + unset( $this->lc ); + unset( $this->lang ); + + parent::tearDown(); + } + + public function testGetPreferredVariantDefaults() { + $this->assertEquals( 'tg', $this->lc->getPreferredVariant() ); + } + + public function testGetPreferredVariantHeaders() { + global $wgRequest; + $wgRequest->setHeader( 'Accept-Language', 'tg-latn' ); + + $this->assertEquals( 'tg-latn', $this->lc->getPreferredVariant() ); + } + + public function testGetPreferredVariantHeaderWeight() { + global $wgRequest; + $wgRequest->setHeader( 'Accept-Language', 'tg;q=1' ); + + $this->assertEquals( 'tg', $this->lc->getPreferredVariant() ); + } + + public function testGetPreferredVariantHeaderWeight2() { + global $wgRequest; + $wgRequest->setHeader( 'Accept-Language', 'tg-latn;q=1' ); + + $this->assertEquals( 'tg-latn', $this->lc->getPreferredVariant() ); + } + + public function testGetPreferredVariantHeaderMulti() { + global $wgRequest; + $wgRequest->setHeader( 'Accept-Language', 'en, tg-latn;q=1' ); + + $this->assertEquals( 'tg-latn', $this->lc->getPreferredVariant() ); + } + + public function testGetPreferredVariantUserOption() { + global $wgUser; + + $wgUser = new User; + $wgUser->load(); // from 'defaults' + $wgUser->mId = 1; + $wgUser->mDataLoaded = true; + $wgUser->mOptionsLoaded = true; + $wgUser->setOption( 'variant', 'tg-latn' ); + + $this->assertEquals( 'tg-latn', $this->lc->getPreferredVariant() ); + } + + public function testGetPreferredVariantUserOptionForForeignLanguage() { + global $wgContLang, $wgUser; + + $wgContLang = Language::factory( 'en' ); + $wgUser = new User; + $wgUser->load(); // from 'defaults' + $wgUser->mId = 1; + $wgUser->mDataLoaded = true; + $wgUser->mOptionsLoaded = true; + $wgUser->setOption( 'variant-tg', 'tg-latn' ); + + $this->assertEquals( 'tg-latn', $this->lc->getPreferredVariant() ); + } + + public function testGetPreferredVariantHeaderUserVsUrl() { + global $wgContLang, $wgRequest, $wgUser; + + $wgContLang = Language::factory( 'tg-latn' ); + $wgRequest->setVal( 'variant', 'tg' ); + $wgUser = User::newFromId( "admin" ); + $wgUser->setId( 1 ); + $wgUser->mFrom = 'defaults'; + $wgUser->mOptionsLoaded = true; + // The user's data is ignored because the variant is set in the URL. + $wgUser->setOption( 'variant', 'tg-latn' ); + $this->assertEquals( 'tg', $this->lc->getPreferredVariant() ); + } + + + public function testGetPreferredVariantDefaultLanguageVariant() { + global $wgDefaultLanguageVariant; + + $wgDefaultLanguageVariant = 'tg-latn'; + $this->assertEquals( 'tg-latn', $this->lc->getPreferredVariant() ); + } + + public function testGetPreferredVariantDefaultLanguageVsUrlVariant() { + global $wgDefaultLanguageVariant, $wgRequest, $wgContLang; + + $wgContLang = Language::factory( 'tg-latn' ); + $wgDefaultLanguageVariant = 'tg'; + $wgRequest->setVal( 'variant', null ); + $this->assertEquals( 'tg', $this->lc->getPreferredVariant() ); + } +} + +/** + * Test converter (from Tajiki to latin orthography) + */ +class TestConverter extends LanguageConverter { + private $table = array( + 'б' => 'b', + 'в' => 'v', + 'г' => 'g', + ); + + function loadDefaultTables() { + $this->mTables = array( + 'tg-latn' => new ReplacementArray( $this->table ), + 'tg' => new ReplacementArray() + ); + } +} + +class LanguageToTest extends Language { + function __construct() { + parent::__construct(); + $variants = array( 'tg', 'tg-latn' ); + $this->mConverter = new TestConverter( $this, 'tg', $variants ); + } +} diff --git a/tests/phpunit/includes/LicensesTest.php b/tests/phpunit/includes/LicensesTest.php new file mode 100644 index 00000000..478a2ffc --- /dev/null +++ b/tests/phpunit/includes/LicensesTest.php @@ -0,0 +1,22 @@ + 'FooField', + 'type' => 'select', + 'section' => 'description', + 'id' => 'wpLicense', + 'label' => 'A label text', # Note can't test label-message because $wgOut is not defined + 'name' => 'AnotherName', + 'licenses' => $str, + ) ); + $this->assertThat( $lc, $this->isInstanceOf( 'Licenses' ) ); + } +} diff --git a/tests/phpunit/includes/LinkerTest.php b/tests/phpunit/includes/LinkerTest.php new file mode 100644 index 00000000..b605f08f --- /dev/null +++ b/tests/phpunit/includes/LinkerTest.php @@ -0,0 +1,71 @@ +setMwGlobals( array( + 'wgArticlePath' => '/wiki/$1', + 'wgWellFormedXml' => true, + ) ); + + $this->assertEquals( $expected, + Linker::userLink( $userId, $userName, $altUserName, $msg ) + ); + } + + public static function provideCasesForUserLink() { + # Format: + # - expected + # - userid + # - username + # - optional altUserName + # - optional message + return array( + + ### ANONYMOUS USER ######################################## + array( + 'JohnDoe', + 0, 'JohnDoe', false, + ), + array( + '::1', + 0, '::1', false, + 'Anonymous with pretty IPv6' + ), + array( + '::1', + 0, '0:0:0:0:0:0:0:1', false, + 'Anonymous with almost pretty IPv6' + ), + array( + '::1', + 0, '0000:0000:0000:0000:0000:0000:0000:0001', false, + 'Anonymous with full IPv6' + ), + array( + 'AlternativeUsername', + 0, '::1', 'AlternativeUsername', + 'Anonymous with pretty IPv6 and an alternative username' + ), + + # IPV4 + array( + '127.0.0.1', + 0, '127.0.0.1', false, + 'Anonymous with IPv4' + ), + array( + 'AlternativeUsername', + 0, '127.0.0.1', 'AlternativeUsername', + 'Anonymous with IPv4 and an alternative username' + ), + + ### Regular user ########################################## + # TODO! + ); + } +} diff --git a/tests/phpunit/includes/LinksUpdateTest.php b/tests/phpunit/includes/LinksUpdateTest.php new file mode 100644 index 00000000..5ade250e --- /dev/null +++ b/tests/phpunit/includes/LinksUpdateTest.php @@ -0,0 +1,175 @@ +tablesUsed = array_merge( $this->tablesUsed, + array( + 'interwiki', + 'page_props', + 'pagelinks', + 'categorylinks', + 'langlinks', + 'externallinks', + 'imagelinks', + 'templatelinks', + 'iwlinks' + ) + ); + } + + protected function setUp() { + parent::setUp(); + $dbw = wfGetDB( DB_MASTER ); + $dbw->replace( + 'interwiki', + array( 'iw_prefix' ), + array( + 'iw_prefix' => 'linksupdatetest', + 'iw_url' => 'http://testing.com/wiki/$1', + 'iw_api' => 'http://testing.com/w/api.php', + 'iw_local' => 0, + 'iw_trans' => 0, + 'iw_wikiid' => 'linksupdatetest', + ) + ); + } + + protected function makeTitleAndParserOutput( $name, $id ) { + $t = Title::newFromText( $name ); + $t->mArticleID = $id; # XXX: this is fugly + + $po = new ParserOutput(); + $po->setTitleText( $t->getPrefixedText() ); + + return array( $t, $po ); + } + + public function testUpdate_pagelinks() { + list( $t, $po ) = $this->makeTitleAndParserOutput( "Testing", 111 ); + + $po->addLink( Title::newFromText( "Foo" ) ); + $po->addLink( Title::newFromText( "Special:Foo" ) ); // special namespace should be ignored + $po->addLink( Title::newFromText( "linksupdatetest:Foo" ) ); // interwiki link should be ignored + $po->addLink( Title::newFromText( "#Foo" ) ); // hash link should be ignored + + $update = $this->assertLinksUpdate( $t, $po, 'pagelinks', 'pl_namespace, pl_title', 'pl_from = 111', array( + array( NS_MAIN, 'Foo' ), + ) ); + $this->assertArrayEquals( array( + Title::makeTitle( NS_MAIN, 'Foo' ), // newFromText doesn't yield the same internal state.... + ), $update->getAddedLinks() ); + + $po = new ParserOutput(); + $po->setTitleText( $t->getPrefixedText() ); + + $po->addLink( Title::newFromText( "Bar" ) ); + $po->addLink( Title::newFromText( "Talk:Bar" ) ); + + $update = $this->assertLinksUpdate( $t, $po, 'pagelinks', 'pl_namespace, pl_title', 'pl_from = 111', array( + array( NS_MAIN, 'Bar' ), + array( NS_TALK, 'Bar' ), + ) ); + $this->assertArrayEquals( array( + Title::makeTitle( NS_MAIN, 'Bar' ), + Title::makeTitle( NS_TALK, 'Bar' ), + ), $update->getAddedLinks() ); + $this->assertArrayEquals( array( + Title::makeTitle( NS_MAIN, 'Foo' ), + ), $update->getRemovedLinks() ); + } + + public function testUpdate_externallinks() { + list( $t, $po ) = $this->makeTitleAndParserOutput( "Testing", 111 ); + + $po->addExternalLink( "http://testing.com/wiki/Foo" ); + + $this->assertLinksUpdate( $t, $po, 'externallinks', 'el_to, el_index', 'el_from = 111', array( + array( 'http://testing.com/wiki/Foo', 'http://com.testing./wiki/Foo' ), + ) ); + } + + public function testUpdate_categorylinks() { + $this->setMwGlobals( 'wgCategoryCollation', 'uppercase' ); + + list( $t, $po ) = $this->makeTitleAndParserOutput( "Testing", 111 ); + + $po->addCategory( "Foo", "FOO" ); + + $this->assertLinksUpdate( $t, $po, 'categorylinks', 'cl_to, cl_sortkey', 'cl_from = 111', array( + array( 'Foo', "FOO\nTESTING" ), + ) ); + } + + public function testUpdate_iwlinks() { + list( $t, $po ) = $this->makeTitleAndParserOutput( "Testing", 111 ); + + $target = Title::makeTitleSafe( NS_MAIN, "Foo", '', 'linksupdatetest' ); + $po->addInterwikiLink( $target ); + + $this->assertLinksUpdate( $t, $po, 'iwlinks', 'iwl_prefix, iwl_title', 'iwl_from = 111', array( + array( 'linksupdatetest', 'Foo' ), + ) ); + } + + public function testUpdate_templatelinks() { + list( $t, $po ) = $this->makeTitleAndParserOutput( "Testing", 111 ); + + $po->addTemplate( Title::newFromText( "Template:Foo" ), 23, 42 ); + + $this->assertLinksUpdate( $t, $po, 'templatelinks', 'tl_namespace, tl_title', 'tl_from = 111', array( + array( NS_TEMPLATE, 'Foo' ), + ) ); + } + + public function testUpdate_imagelinks() { + list( $t, $po ) = $this->makeTitleAndParserOutput( "Testing", 111 ); + + $po->addImage( "Foo.png" ); + + $this->assertLinksUpdate( $t, $po, 'imagelinks', 'il_to', 'il_from = 111', array( + array( 'Foo.png' ), + ) ); + } + + public function testUpdate_langlinks() { + list( $t, $po ) = $this->makeTitleAndParserOutput( "Testing", 111 ); + + $po->addLanguageLink( Title::newFromText( "en:Foo" )->getFullText() ); + + $this->assertLinksUpdate( $t, $po, 'langlinks', 'll_lang, ll_title', 'll_from = 111', array( + array( 'En', 'Foo' ), + ) ); + } + + public function testUpdate_page_props() { + list( $t, $po ) = $this->makeTitleAndParserOutput( "Testing", 111 ); + + $po->setProperty( "foo", "bar" ); + + $this->assertLinksUpdate( $t, $po, 'page_props', 'pp_propname, pp_value', 'pp_page = 111', array( + array( 'foo', 'bar' ), + ) ); + } + + // @todo test recursive, too! + + protected function assertLinksUpdate( Title $title, ParserOutput $parserOutput, $table, $fields, $condition, array $expectedRows ) { + $update = new LinksUpdate( $title, $parserOutput ); + + //NOTE: make sure LinksUpdate does not generate warnings when called inside a transaction. + $update->beginTransaction(); + $update->doUpdate(); + $update->commitTransaction(); + + $this->assertSelect( $table, $fields, $condition, $expectedRows ); + return $update; + } +} diff --git a/tests/phpunit/includes/LocalFileTest.php b/tests/phpunit/includes/LocalFileTest.php new file mode 100644 index 00000000..2501c783 --- /dev/null +++ b/tests/phpunit/includes/LocalFileTest.php @@ -0,0 +1,107 @@ +setMwGlobals( 'wgCapitalLinks', true ); + + $info = array( + 'name' => 'test', + 'directory' => '/testdir', + 'url' => '/testurl', + 'hashLevels' => 2, + 'transformVia404' => false, + 'backend' => new FSFileBackend( array( + 'name' => 'local-backend', + 'lockManager' => 'fsLockManager', + 'containerPaths' => array( + 'cont1' => "/testdir/local-backend/tempimages/cont1", + 'cont2' => "/testdir/local-backend/tempimages/cont2" + ) + ) ) + ); + $this->repo_hl0 = new LocalRepo( array( 'hashLevels' => 0 ) + $info ); + $this->repo_hl2 = new LocalRepo( array( 'hashLevels' => 2 ) + $info ); + $this->repo_lc = new LocalRepo( array( 'initialCapital' => false ) + $info ); + $this->file_hl0 = $this->repo_hl0->newFile( 'test!' ); + $this->file_hl2 = $this->repo_hl2->newFile( 'test!' ); + $this->file_lc = $this->repo_lc->newFile( 'test!' ); + } + + public function testGetHashPath() { + $this->assertEquals( '', $this->file_hl0->getHashPath() ); + $this->assertEquals( 'a/a2/', $this->file_hl2->getHashPath() ); + $this->assertEquals( 'c/c4/', $this->file_lc->getHashPath() ); + } + + public function testGetRel() { + $this->assertEquals( 'Test!', $this->file_hl0->getRel() ); + $this->assertEquals( 'a/a2/Test!', $this->file_hl2->getRel() ); + $this->assertEquals( 'c/c4/test!', $this->file_lc->getRel() ); + } + + public function testGetUrlRel() { + $this->assertEquals( 'Test%21', $this->file_hl0->getUrlRel() ); + $this->assertEquals( 'a/a2/Test%21', $this->file_hl2->getUrlRel() ); + $this->assertEquals( 'c/c4/test%21', $this->file_lc->getUrlRel() ); + } + + public function testGetArchivePath() { + $this->assertEquals( 'mwstore://local-backend/test-public/archive', $this->file_hl0->getArchivePath() ); + $this->assertEquals( 'mwstore://local-backend/test-public/archive/a/a2', $this->file_hl2->getArchivePath() ); + $this->assertEquals( 'mwstore://local-backend/test-public/archive/!', $this->file_hl0->getArchivePath( '!' ) ); + $this->assertEquals( 'mwstore://local-backend/test-public/archive/a/a2/!', $this->file_hl2->getArchivePath( '!' ) ); + } + + public function testGetThumbPath() { + $this->assertEquals( 'mwstore://local-backend/test-thumb/Test!', $this->file_hl0->getThumbPath() ); + $this->assertEquals( 'mwstore://local-backend/test-thumb/a/a2/Test!', $this->file_hl2->getThumbPath() ); + $this->assertEquals( 'mwstore://local-backend/test-thumb/Test!/x', $this->file_hl0->getThumbPath( 'x' ) ); + $this->assertEquals( 'mwstore://local-backend/test-thumb/a/a2/Test!/x', $this->file_hl2->getThumbPath( 'x' ) ); + } + + public function testGetArchiveUrl() { + $this->assertEquals( '/testurl/archive', $this->file_hl0->getArchiveUrl() ); + $this->assertEquals( '/testurl/archive/a/a2', $this->file_hl2->getArchiveUrl() ); + $this->assertEquals( '/testurl/archive/%21', $this->file_hl0->getArchiveUrl( '!' ) ); + $this->assertEquals( '/testurl/archive/a/a2/%21', $this->file_hl2->getArchiveUrl( '!' ) ); + } + + public function testGetThumbUrl() { + $this->assertEquals( '/testurl/thumb/Test%21', $this->file_hl0->getThumbUrl() ); + $this->assertEquals( '/testurl/thumb/a/a2/Test%21', $this->file_hl2->getThumbUrl() ); + $this->assertEquals( '/testurl/thumb/Test%21/x', $this->file_hl0->getThumbUrl( 'x' ) ); + $this->assertEquals( '/testurl/thumb/a/a2/Test%21/x', $this->file_hl2->getThumbUrl( 'x' ) ); + } + + public function testGetArchiveVirtualUrl() { + $this->assertEquals( 'mwrepo://test/public/archive', $this->file_hl0->getArchiveVirtualUrl() ); + $this->assertEquals( 'mwrepo://test/public/archive/a/a2', $this->file_hl2->getArchiveVirtualUrl() ); + $this->assertEquals( 'mwrepo://test/public/archive/%21', $this->file_hl0->getArchiveVirtualUrl( '!' ) ); + $this->assertEquals( 'mwrepo://test/public/archive/a/a2/%21', $this->file_hl2->getArchiveVirtualUrl( '!' ) ); + } + + public function testGetThumbVirtualUrl() { + $this->assertEquals( 'mwrepo://test/thumb/Test%21', $this->file_hl0->getThumbVirtualUrl() ); + $this->assertEquals( 'mwrepo://test/thumb/a/a2/Test%21', $this->file_hl2->getThumbVirtualUrl() ); + $this->assertEquals( 'mwrepo://test/thumb/Test%21/%21', $this->file_hl0->getThumbVirtualUrl( '!' ) ); + $this->assertEquals( 'mwrepo://test/thumb/a/a2/Test%21/%21', $this->file_hl2->getThumbVirtualUrl( '!' ) ); + } + + public function testGetUrl() { + $this->assertEquals( '/testurl/Test%21', $this->file_hl0->getUrl() ); + $this->assertEquals( '/testurl/a/a2/Test%21', $this->file_hl2->getUrl() ); + } + + public function testWfLocalFile() { + $file = wfLocalFile( "File:Some_file_that_probably_doesn't exist.png" ); + $this->assertThat( $file, $this->isInstanceOf( 'LocalFile' ), 'wfLocalFile() returns LocalFile for valid Titles' ); + } +} diff --git a/tests/phpunit/includes/LocalisationCacheTest.php b/tests/phpunit/includes/LocalisationCacheTest.php new file mode 100644 index 00000000..b34847aa --- /dev/null +++ b/tests/phpunit/includes/LocalisationCacheTest.php @@ -0,0 +1,31 @@ +assertEquals( + $cache->getItem( 'ar', 'pluralRules' ), + $cache->getItem( 'arz', 'pluralRules' ), + 'arz plural rules (undefined) fallback to ar (defined)' + ); + + $this->assertEquals( + $cache->getItem( 'ar', 'compiledPluralRules' ), + $cache->getItem( 'arz', 'compiledPluralRules' ), + 'arz compiled plural rules (undefined) fallback to ar (defined)' + ); + + $this->assertNotEquals( + $cache->getItem( 'ksh', 'pluralRules' ), + $cache->getItem( 'de', 'pluralRules' ), + 'ksh plural rules (defined) dont fallback to de (defined)' + ); + + $this->assertNotEquals( + $cache->getItem( 'ksh', 'compiledPluralRules' ), + $cache->getItem( 'de', 'compiledPluralRules' ), + 'ksh compiled plural rules (defined) dont fallback to de (defined)' + ); + } +} diff --git a/tests/phpunit/includes/MWExceptionHandlerTest.php b/tests/phpunit/includes/MWExceptionHandlerTest.php new file mode 100644 index 00000000..987dfa83 --- /dev/null +++ b/tests/phpunit/includes/MWExceptionHandlerTest.php @@ -0,0 +1,73 @@ +getTrace(); + $hasObject = false; + $hasArray = false; + foreach ( $trace as $frame ) { + if ( ! isset( $frame['args'] ) ) { + continue; + } + foreach ( $frame['args'] as $arg ) { + $hasObject = $hasObject || is_object( $arg ); + $hasArray = $hasArray || is_array( $arg ); + } + + if( $hasObject && $hasArray ) { + break; + } + } + $this->assertTrue( $hasObject, + "The stacktrace must have a function having an object has parameter" ); + $this->assertTrue( $hasArray, + "The stacktrace must have a function having an array has parameter" ); + + # Now we redact the trace.. and make sure no function arguments are + # arrays or objects. + $redacted = MWExceptionHandler::getRedactedTrace( $e ); + + foreach ( $redacted as $frame ) { + if ( ! isset( $frame['args'] ) ) { + continue; + } + foreach ( $frame['args'] as $arg ) { + $this->assertNotInternalType( 'array', $arg); + $this->assertNotInternalType( 'object', $arg); + } + } + } + + /** + * Helper function for testExpandArgumentsInCall + * + * Pass it an object and an array :-) + * + * @throws Exception + */ + protected static function helperThrowAnException( $a, $b ) { + throw new Exception(); + } +} diff --git a/tests/phpunit/includes/MWFunctionTest.php b/tests/phpunit/includes/MWFunctionTest.php new file mode 100644 index 00000000..d86f2c9b --- /dev/null +++ b/tests/phpunit/includes/MWFunctionTest.php @@ -0,0 +1,30 @@ +assertEquals( + MWFunction::newObj( 'MWBlankClass', $args )->args, + $newObject->args + ); + } +} + +class MWBlankClass { + + public $args = array(); + + function __construct( $arg1, $arg2, $arg3, $arg4 ) { + $this->args = array( $arg1, $arg2, $arg3, $arg4 ); + } +} + +class ExampleObject { +} diff --git a/tests/phpunit/includes/MWNamespaceTest.php b/tests/phpunit/includes/MWNamespaceTest.php new file mode 100644 index 00000000..10e9db61 --- /dev/null +++ b/tests/phpunit/includes/MWNamespaceTest.php @@ -0,0 +1,571 @@ +setMwGlobals( array( + 'wgContentNamespaces' => array( NS_MAIN ), + 'wgNamespacesWithSubpages' => array( + NS_TALK => true, + NS_USER => true, + NS_USER_TALK => true, + ), + 'wgCapitalLinks' => true, + 'wgCapitalLinkOverrides' => array(), + 'wgNonincludableNamespaces' => array(), + ) ); + } + +#### START OF TESTS ######################################################### + + /** + * @todo Write more texts, handle $wgAllowImageMoving setting + */ + public function testIsMovable() { + $this->assertFalse( MWNamespace::isMovable( NS_CATEGORY ) ); + # @todo FIXME: Write more tests!! + } + + /** + * Please make sure to change testIsTalk() if you change the assertions below + */ + public function testIsSubject() { + // Special namespaces + $this->assertIsSubject( NS_MEDIA ); + $this->assertIsSubject( NS_SPECIAL ); + + // Subject pages + $this->assertIsSubject( NS_MAIN ); + $this->assertIsSubject( NS_USER ); + $this->assertIsSubject( 100 ); # user defined + + // Talk pages + $this->assertIsNotSubject( NS_TALK ); + $this->assertIsNotSubject( NS_USER_TALK ); + $this->assertIsNotSubject( 101 ); # user defined + } + + /** + * Reverse of testIsSubject(). + * Please update testIsSubject() if you change assertions below + */ + public function testIsTalk() { + // Special namespaces + $this->assertIsNotTalk( NS_MEDIA ); + $this->assertIsNotTalk( NS_SPECIAL ); + + // Subject pages + $this->assertIsNotTalk( NS_MAIN ); + $this->assertIsNotTalk( NS_USER ); + $this->assertIsNotTalk( 100 ); # user defined + + // Talk pages + $this->assertIsTalk( NS_TALK ); + $this->assertIsTalk( NS_USER_TALK ); + $this->assertIsTalk( 101 ); # user defined + } + + /** + */ + public function testGetSubject() { + // Special namespaces are their own subjects + $this->assertEquals( NS_MEDIA, MWNamespace::getSubject( NS_MEDIA ) ); + $this->assertEquals( NS_SPECIAL, MWNamespace::getSubject( NS_SPECIAL ) ); + + $this->assertEquals( NS_MAIN, MWNamespace::getSubject( NS_TALK ) ); + $this->assertEquals( NS_USER, MWNamespace::getSubject( NS_USER_TALK ) ); + } + + /** + * Regular getTalk() calls + * Namespaces without a talk page (NS_MEDIA, NS_SPECIAL) are tested in + * the function testGetTalkExceptions() + */ + public function testGetTalk() { + $this->assertEquals( NS_TALK, MWNamespace::getTalk( NS_MAIN ) ); + $this->assertEquals( NS_TALK, MWNamespace::getTalk( NS_TALK ) ); + $this->assertEquals( NS_USER_TALK, MWNamespace::getTalk( NS_USER ) ); + $this->assertEquals( NS_USER_TALK, MWNamespace::getTalk( NS_USER_TALK ) ); + } + + /** + * Exceptions with getTalk() + * NS_MEDIA does not have talk pages. MediaWiki raise an exception for them. + * @expectedException MWException + */ + public function testGetTalkExceptionsForNsMedia() { + $this->assertNull( MWNamespace::getTalk( NS_MEDIA ) ); + } + + /** + * Exceptions with getTalk() + * NS_SPECIAL does not have talk pages. MediaWiki raise an exception for them. + * @expectedException MWException + */ + public function testGetTalkExceptionsForNsSpecial() { + $this->assertNull( MWNamespace::getTalk( NS_SPECIAL ) ); + } + + /** + * Regular getAssociated() calls + * Namespaces without an associated page (NS_MEDIA, NS_SPECIAL) are tested in + * the function testGetAssociatedExceptions() + */ + public function testGetAssociated() { + $this->assertEquals( NS_TALK, MWNamespace::getAssociated( NS_MAIN ) ); + $this->assertEquals( NS_MAIN, MWNamespace::getAssociated( NS_TALK ) ); + } + + ### Exceptions with getAssociated() + ### NS_MEDIA and NS_SPECIAL do not have talk pages. MediaWiki raises + ### an exception for them. + /** + * @expectedException MWException + */ + public function testGetAssociatedExceptionsForNsMedia() { + $this->assertNull( MWNamespace::getAssociated( NS_MEDIA ) ); + } + + /** + * @expectedException MWException + */ + public function testGetAssociatedExceptionsForNsSpecial() { + $this->assertNull( MWNamespace::getAssociated( NS_SPECIAL ) ); + } + + /** + * @todo Implement testExists(). + */ + /* + public function testExists() { + // Remove the following lines when you implement this test. + $this->markTestIncomplete( + 'This test has not been implemented yet. Rely on $wgCanonicalNamespaces.' + ); + } + */ + + /** + * Test MWNamespace::equals + * Note if we add a namespace registration system with keys like 'MAIN' + * we should add tests here for equivilance on things like 'MAIN' == 0 + * and 'MAIN' == NS_MAIN. + */ + public function testEquals() { + $this->assertTrue( MWNamespace::equals( NS_MAIN, NS_MAIN ) ); + $this->assertTrue( MWNamespace::equals( NS_MAIN, 0 ) ); // In case we make NS_MAIN 'MAIN' + $this->assertTrue( MWNamespace::equals( NS_USER, NS_USER ) ); + $this->assertTrue( MWNamespace::equals( NS_USER, 2 ) ); + $this->assertTrue( MWNamespace::equals( NS_USER_TALK, NS_USER_TALK ) ); + $this->assertTrue( MWNamespace::equals( NS_SPECIAL, NS_SPECIAL ) ); + $this->assertFalse( MWNamespace::equals( NS_MAIN, NS_TALK ) ); + $this->assertFalse( MWNamespace::equals( NS_USER, NS_USER_TALK ) ); + $this->assertFalse( MWNamespace::equals( NS_PROJECT, NS_TEMPLATE ) ); + } + + /** + * Test MWNamespace::subjectEquals + */ + public function testSubjectEquals() { + $this->assertSameSubject( NS_MAIN, NS_MAIN ); + $this->assertSameSubject( NS_MAIN, 0 ); // In case we make NS_MAIN 'MAIN' + $this->assertSameSubject( NS_USER, NS_USER ); + $this->assertSameSubject( NS_USER, 2 ); + $this->assertSameSubject( NS_USER_TALK, NS_USER_TALK ); + $this->assertSameSubject( NS_SPECIAL, NS_SPECIAL ); + $this->assertSameSubject( NS_MAIN, NS_TALK ); + $this->assertSameSubject( NS_USER, NS_USER_TALK ); + + $this->assertDifferentSubject( NS_PROJECT, NS_TEMPLATE ); + $this->assertDifferentSubject( NS_SPECIAL, NS_MAIN ); + } + + public function testSpecialAndMediaAreDifferentSubjects() { + $this->assertDifferentSubject( + NS_MEDIA, NS_SPECIAL, + "NS_MEDIA and NS_SPECIAL are different subject namespaces" + ); + $this->assertDifferentSubject( + NS_SPECIAL, NS_MEDIA, + "NS_SPECIAL and NS_MEDIA are different subject namespaces" + ); + } + + /** + * @todo Implement testGetCanonicalNamespaces(). + */ + /* + public function testGetCanonicalNamespaces() { + // Remove the following lines when you implement this test. + $this->markTestIncomplete( + 'This test has not been implemented yet. Rely on $wgCanonicalNamespaces.' + ); + } + */ + /** + * @todo Implement testGetCanonicalName(). + */ + /* + public function testGetCanonicalName() { + // Remove the following lines when you implement this test. + $this->markTestIncomplete( + 'This test has not been implemented yet. Rely on $wgCanonicalNamespaces.' + ); + } + */ + /** + * @todo Implement testGetCanonicalIndex(). + */ + /* + public function testGetCanonicalIndex() { + // Remove the following lines when you implement this test. + $this->markTestIncomplete( + 'This test has not been implemented yet. Rely on $wgCanonicalNamespaces.' + ); + } + */ + + /** + * @todo Implement testGetValidNamespaces(). + */ + /* + public function testGetValidNamespaces() { + // Remove the following lines when you implement this test. + $this->markTestIncomplete( + 'This test has not been implemented yet. Rely on $wgCanonicalNamespaces.' + ); + } + */ + + /** + */ + public function testCanTalk() { + $this->assertCanNotTalk( NS_MEDIA ); + $this->assertCanNotTalk( NS_SPECIAL ); + + $this->assertCanTalk( NS_MAIN ); + $this->assertCanTalk( NS_TALK ); + $this->assertCanTalk( NS_USER ); + $this->assertCanTalk( NS_USER_TALK ); + + // User defined namespaces + $this->assertCanTalk( 100 ); + $this->assertCanTalk( 101 ); + } + + /** + */ + public function testIsContent() { + // NS_MAIN is a content namespace per DefaultSettings.php + // and per function definition. + + $this->assertIsContent( NS_MAIN ); + + // Other namespaces which are not expected to be content + + $this->assertIsNotContent( NS_MEDIA ); + $this->assertIsNotContent( NS_SPECIAL ); + $this->assertIsNotContent( NS_TALK ); + $this->assertIsNotContent( NS_USER ); + $this->assertIsNotContent( NS_CATEGORY ); + $this->assertIsNotContent( 100 ); + } + + /** + * Similar to testIsContent() but alters the $wgContentNamespaces + * global variable. + */ + public function testIsContentAdvanced() { + global $wgContentNamespaces; + + // Test that user defined namespace #252 is not content + $this->assertIsNotContent( 252 ); + + // Bless namespace # 252 as a content namespace + $wgContentNamespaces[] = 252; + + $this->assertIsContent( 252 ); + + // Makes sure NS_MAIN was not impacted + $this->assertIsContent( NS_MAIN ); + } + + public function testIsWatchable() { + // Specials namespaces are not watchable + $this->assertIsNotWatchable( NS_MEDIA ); + $this->assertIsNotWatchable( NS_SPECIAL ); + + // Core defined namespaces are watchables + $this->assertIsWatchable( NS_MAIN ); + $this->assertIsWatchable( NS_TALK ); + + // Additional, user defined namespaces are watchables + $this->assertIsWatchable( 100 ); + $this->assertIsWatchable( 101 ); + } + + public function testHasSubpages() { + global $wgNamespacesWithSubpages; + + // Special namespaces: + $this->assertHasNotSubpages( NS_MEDIA ); + $this->assertHasNotSubpages( NS_SPECIAL ); + + // Namespaces without subpages + $this->assertHasNotSubpages( NS_MAIN ); + + $wgNamespacesWithSubpages[NS_MAIN] = true; + $this->assertHasSubpages( NS_MAIN ); + + $wgNamespacesWithSubpages[NS_MAIN] = false; + $this->assertHasNotSubpages( NS_MAIN ); + + // Some namespaces with subpages + $this->assertHasSubpages( NS_TALK ); + $this->assertHasSubpages( NS_USER ); + $this->assertHasSubpages( NS_USER_TALK ); + } + + /** + */ + public function testGetContentNamespaces() { + global $wgContentNamespaces; + + $this->assertEquals( + array( NS_MAIN ), + MWNamespace::getContentNamespaces(), + '$wgContentNamespaces is an array with only NS_MAIN by default' + ); + + # test !is_array( $wgcontentNamespaces ) + $wgContentNamespaces = ''; + $this->assertEquals( array( NS_MAIN ), MWNamespace::getContentNamespaces() ); + + $wgContentNamespaces = false; + $this->assertEquals( array( NS_MAIN ), MWNamespace::getContentNamespaces() ); + + $wgContentNamespaces = null; + $this->assertEquals( array( NS_MAIN ), MWNamespace::getContentNamespaces() ); + + $wgContentNamespaces = 5; + $this->assertEquals( array( NS_MAIN ), MWNamespace::getContentNamespaces() ); + + # test $wgContentNamespaces === array() + $wgContentNamespaces = array(); + $this->assertEquals( array( NS_MAIN ), MWNamespace::getContentNamespaces() ); + + # test !in_array( NS_MAIN, $wgContentNamespaces ) + $wgContentNamespaces = array( NS_USER, NS_CATEGORY ); + $this->assertEquals( + array( NS_MAIN, NS_USER, NS_CATEGORY ), + MWNamespace::getContentNamespaces(), + 'NS_MAIN is forced in $wgContentNamespaces even if unwanted' + ); + + # test other cases, return $wgcontentNamespaces as is + $wgContentNamespaces = array( NS_MAIN ); + $this->assertEquals( + array( NS_MAIN ), + MWNamespace::getContentNamespaces() + ); + + $wgContentNamespaces = array( NS_MAIN, NS_USER, NS_CATEGORY ); + $this->assertEquals( + array( NS_MAIN, NS_USER, NS_CATEGORY ), + MWNamespace::getContentNamespaces() + ); + } + + /** + */ + public function testGetSubjectNamespaces() { + $subjectsNS = MWNamespace::getSubjectNamespaces(); + $this->assertContains( NS_MAIN, $subjectsNS, + "Talk namespaces should have NS_MAIN" ); + $this->assertNotContains( NS_TALK, $subjectsNS, + "Talk namespaces should have NS_TALK" ); + + $this->assertNotContains( NS_MEDIA, $subjectsNS, + "Talk namespaces should not have NS_MEDIA" ); + $this->assertNotContains( NS_SPECIAL, $subjectsNS, + "Talk namespaces should not have NS_SPECIAL" ); + } + + /** + */ + public function testGetTalkNamespaces() { + $talkNS = MWNamespace::getTalkNamespaces(); + $this->assertContains( NS_TALK, $talkNS, + "Subject namespaces should have NS_TALK" ); + $this->assertNotContains( NS_MAIN, $talkNS, + "Subject namespaces should not have NS_MAIN" ); + + $this->assertNotContains( NS_MEDIA, $talkNS, + "Subject namespaces should not have NS_MEDIA" ); + $this->assertNotContains( NS_SPECIAL, $talkNS, + "Subject namespaces should not have NS_SPECIAL" ); + } + + /** + * Some namespaces are always capitalized per code definition + * in MWNamespace::$alwaysCapitalizedNamespaces + */ + public function testIsCapitalizedHardcodedAssertions() { + // NS_MEDIA and NS_FILE are treated the same + $this->assertEquals( + MWNamespace::isCapitalized( NS_MEDIA ), + MWNamespace::isCapitalized( NS_FILE ), + 'NS_MEDIA and NS_FILE have same capitalization rendering' + ); + + // Boths are capitalized by default + $this->assertIsCapitalized( NS_MEDIA ); + $this->assertIsCapitalized( NS_FILE ); + + // Always capitalized namespaces + // @see MWNamespace::$alwaysCapitalizedNamespaces + $this->assertIsCapitalized( NS_SPECIAL ); + $this->assertIsCapitalized( NS_USER ); + $this->assertIsCapitalized( NS_MEDIAWIKI ); + } + + /** + * Follows up for testIsCapitalizedHardcodedAssertions() but alter the + * global $wgCapitalLink setting to have extended coverage. + * + * MWNamespace::isCapitalized() rely on two global settings: + * $wgCapitalLinkOverrides = array(); by default + * $wgCapitalLinks = true; by default + * This function test $wgCapitalLinks + * + * Global setting correctness is tested against the NS_PROJECT and + * NS_PROJECT_TALK namespaces since they are not hardcoded nor specials + */ + public function testIsCapitalizedWithWgCapitalLinks() { + global $wgCapitalLinks; + + $this->assertIsCapitalized( NS_PROJECT ); + $this->assertIsCapitalized( NS_PROJECT_TALK ); + + $wgCapitalLinks = false; + + // hardcoded namespaces (see above function) are still capitalized: + $this->assertIsCapitalized( NS_SPECIAL ); + $this->assertIsCapitalized( NS_USER ); + $this->assertIsCapitalized( NS_MEDIAWIKI ); + + // setting is correctly applied + $this->assertIsNotCapitalized( NS_PROJECT ); + $this->assertIsNotCapitalized( NS_PROJECT_TALK ); + } + + /** + * Counter part for MWNamespace::testIsCapitalizedWithWgCapitalLinks() now + * testing the $wgCapitalLinkOverrides global. + * + * @todo split groups of assertions in autonomous testing functions + */ + public function testIsCapitalizedWithWgCapitalLinkOverrides() { + global $wgCapitalLinkOverrides; + + // Test default settings + $this->assertIsCapitalized( NS_PROJECT ); + $this->assertIsCapitalized( NS_PROJECT_TALK ); + + // hardcoded namespaces (see above function) are capitalized: + $this->assertIsCapitalized( NS_SPECIAL ); + $this->assertIsCapitalized( NS_USER ); + $this->assertIsCapitalized( NS_MEDIAWIKI ); + + // Hardcoded namespaces remains capitalized + $wgCapitalLinkOverrides[NS_SPECIAL] = false; + $wgCapitalLinkOverrides[NS_USER] = false; + $wgCapitalLinkOverrides[NS_MEDIAWIKI] = false; + + $this->assertIsCapitalized( NS_SPECIAL ); + $this->assertIsCapitalized( NS_USER ); + $this->assertIsCapitalized( NS_MEDIAWIKI ); + + $wgCapitalLinkOverrides[NS_PROJECT] = false; + $this->assertIsNotCapitalized( NS_PROJECT ); + + $wgCapitalLinkOverrides[NS_PROJECT] = true; + $this->assertIsCapitalized( NS_PROJECT ); + + unset( $wgCapitalLinkOverrides[NS_PROJECT] ); + $this->assertIsCapitalized( NS_PROJECT ); + } + + public function testHasGenderDistinction() { + // Namespaces with gender distinctions + $this->assertTrue( MWNamespace::hasGenderDistinction( NS_USER ) ); + $this->assertTrue( MWNamespace::hasGenderDistinction( NS_USER_TALK ) ); + + // Other ones, "genderless" + $this->assertFalse( MWNamespace::hasGenderDistinction( NS_MEDIA ) ); + $this->assertFalse( MWNamespace::hasGenderDistinction( NS_SPECIAL ) ); + $this->assertFalse( MWNamespace::hasGenderDistinction( NS_MAIN ) ); + $this->assertFalse( MWNamespace::hasGenderDistinction( NS_TALK ) ); + } + + public function testIsNonincludable() { + global $wgNonincludableNamespaces; + + $wgNonincludableNamespaces = array( NS_USER ); + + $this->assertTrue( MWNamespace::isNonincludable( NS_USER ) ); + $this->assertFalse( MWNamespace::isNonincludable( NS_TEMPLATE ) ); + } + + ####### HELPERS ########################################################### + function __call( $method, $args ) { + // Call the real method if it exists + if ( method_exists( $this, $method ) ) { + return $this->$method( $args ); + } + + if ( preg_match( '/^assert(Has|Is|Can)(Not|)(Subject|Talk|Watchable|Content|Subpages|Capitalized)$/', $method, $m ) ) { + # Interprets arguments: + $ns = $args[0]; + $msg = isset( $args[1] ) ? $args[1] : " dummy message"; + + # Forge the namespace constant name: + if ( $ns === 0 ) { + $ns_name = "NS_MAIN"; + } else { + $ns_name = "NS_" . strtoupper( MWNamespace::getCanonicalName( $ns ) ); + } + # ... and the MWNamespace method name + $nsMethod = strtolower( $m[1] ) . $m[3]; + + $expect = ( $m[2] === '' ); + $expect_name = $expect ? 'TRUE' : 'FALSE'; + + return $this->assertEquals( $expect, + MWNamespace::$nsMethod( $ns, $msg ), + "MWNamespace::$nsMethod( $ns_name ) should returns $expect_name" + ); + } + + throw new Exception( __METHOD__ . " could not find a method named $method\n" ); + } + + function assertSameSubject( $ns1, $ns2, $msg = '' ) { + $this->assertTrue( MWNamespace::subjectEquals( $ns1, $ns2, $msg ) ); + } + + function assertDifferentSubject( $ns1, $ns2, $msg = '' ) { + $this->assertFalse( MWNamespace::subjectEquals( $ns1, $ns2, $msg ) ); + } +} diff --git a/tests/phpunit/includes/MessageTest.php b/tests/phpunit/includes/MessageTest.php new file mode 100644 index 00000000..1e18f975 --- /dev/null +++ b/tests/phpunit/includes/MessageTest.php @@ -0,0 +1,130 @@ +setMwGlobals( array( + 'wgLang' => Language::factory( 'en' ), + 'wgForceUIMsgAsContentMsg' => array(), + ) ); + } + + public function testExists() { + $this->assertTrue( wfMessage( 'mainpage' )->exists() ); + $this->assertTrue( wfMessage( 'mainpage' )->params( array() )->exists() ); + $this->assertTrue( wfMessage( 'mainpage' )->rawParams( 'foo', 123 )->exists() ); + $this->assertFalse( wfMessage( 'i-dont-exist-evar' )->exists() ); + $this->assertFalse( wfMessage( 'i-dont-exist-evar' )->params( array() )->exists() ); + $this->assertFalse( wfMessage( 'i-dont-exist-evar' )->rawParams( 'foo', 123 )->exists() ); + } + + public function testKey() { + $this->assertInstanceOf( 'Message', wfMessage( 'mainpage' ) ); + $this->assertInstanceOf( 'Message', wfMessage( 'i-dont-exist-evar' ) ); + $this->assertEquals( 'Main Page', wfMessage( 'mainpage' )->text() ); + $this->assertEquals( '<i-dont-exist-evar>', wfMessage( 'i-dont-exist-evar' )->text() ); + $this->assertEquals( '', wfMessage( 'i-dont-exist-evar' )->plain() ); + $this->assertEquals( '<i-dont-exist-evar>', wfMessage( 'i-dont-exist-evar' )->escaped() ); + } + + public function testInLanguage() { + $this->assertEquals( 'Main Page', wfMessage( 'mainpage' )->inLanguage( 'en' )->text() ); + $this->assertEquals( 'Заглавная страница', wfMessage( 'mainpage' )->inLanguage( 'ru' )->text() ); + $this->assertEquals( 'Main Page', wfMessage( 'mainpage' )->inLanguage( Language::factory( 'en' ) )->text() ); + $this->assertEquals( 'Заглавная страница', wfMessage( 'mainpage' )->inLanguage( Language::factory( 'ru' ) )->text() ); + } + + public function testMessageParams() { + $this->assertEquals( 'Return to $1.', wfMessage( 'returnto' )->text() ); + $this->assertEquals( 'Return to $1.', wfMessage( 'returnto', array() )->text() ); + $this->assertEquals( 'You have foo (bar).', wfMessage( 'youhavenewmessages', 'foo', 'bar' )->text() ); + $this->assertEquals( 'You have foo (bar).', wfMessage( 'youhavenewmessages', array( 'foo', 'bar' ) )->text() ); + } + + public function testMessageParamSubstitution() { + $this->assertEquals( '(Заглавная страница)', wfMessage( 'parentheses', 'Заглавная страница' )->plain() ); + $this->assertEquals( '(Заглавная страница $1)', wfMessage( 'parentheses', 'Заглавная страница $1' )->plain() ); + $this->assertEquals( '(Заглавная страница)', wfMessage( 'parentheses' )->rawParams( 'Заглавная страница' )->plain() ); + $this->assertEquals( '(Заглавная страница $1)', wfMessage( 'parentheses' )->rawParams( 'Заглавная страница $1' )->plain() ); + } + + public function testDeliciouslyManyParams() { + $msg = new RawMessage( '$1$2$3$4$5$6$7$8$9$10$11$12' ); + // One less than above has placeholders + $params = array( 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k' ); + $this->assertEquals( 'abcdefghijka2', $msg->params( $params )->plain(), 'Params > 9 are replaced correctly' ); + } + + /** + * FIXME: This should not need database, but Language#formatExpiry does (bug 55912) + * @group Database + */ + public function testMessageParamTypes() { + $lang = Language::factory( 'en' ); + + $msg = new RawMessage( '$1' ); + $this->assertEquals( + $lang->formatNum( 123456.789 ), + $msg->inLanguage( $lang )->numParams( 123456.789 )->plain(), + 'numParams is handled correctly' + ); + + $msg = new RawMessage( '$1' ); + $this->assertEquals( + $lang->formatDuration( 1234 ), + $msg->inLanguage( $lang )->durationParams( 1234 )->plain(), + 'durationParams is handled correctly' + ); + + $msg = new RawMessage( '$1' ); + $this->assertEquals( + $lang->formatExpiry( wfTimestampNow() ), + $msg->inLanguage( $lang )->expiryParams( wfTimestampNow() )->plain(), + 'expiryParams is handled correctly' + ); + + $msg = new RawMessage( '$1' ); + $this->assertEquals( + $lang->formatTimePeriod( 1234 ), + $msg->inLanguage( $lang )->timeperiodParams( 1234 )->plain(), + 'timeperiodParams is handled correctly' + ); + + $msg = new RawMessage( '$1' ); + $this->assertEquals( + $lang->formatSize( 123456 ), + $msg->inLanguage( $lang )->sizeParams( 123456 )->plain(), + 'sizeParams is handled correctly' + ); + + $msg = new RawMessage( '$1' ); + $this->assertEquals( + $lang->formatBitrate( 123456 ), + $msg->inLanguage( $lang )->bitrateParams( 123456 )->plain(), + 'bitrateParams is handled correctly' + ); + } + + public function testInContentLanguageDisabled() { + $this->setMwGlobals( 'wgLang', Language::factory( 'fr' ) ); + + $this->assertEquals( 'Main Page', wfMessage( 'mainpage' )->inContentLanguage()->plain(), 'ForceUIMsg disabled' ); + } + + public function testInContentLanguageEnabled() { + $this->setMwGlobals( array( + 'wgLang' => Language::factory( 'fr' ), + 'wgForceUIMsgAsContentMsg' => array( 'mainpage' ), + ) ); + + $this->assertEquals( 'Accueil', wfMessage( 'mainpage' )->inContentLanguage()->plain(), 'ForceUIMsg enabled' ); + } + + /** + * @expectedException MWException + */ + public function testInLanguageThrows() { + wfMessage( 'foo' )->inLanguage( 123 ); + } +} diff --git a/tests/phpunit/includes/OutputPageTest.php b/tests/phpunit/includes/OutputPageTest.php new file mode 100644 index 00000000..56bb0fce --- /dev/null +++ b/tests/phpunit/includes/OutputPageTest.php @@ -0,0 +1,133 @@ +setMWGlobals( array( + 'wgRequest' => $fauxRequest, + ) ); + + $actualReturn = OutputPage::transformCssMedia( $args['media'] ); + $this->assertSame( $args['expectedReturn'], $actualReturn, $args['message'] ); + } + + /** + * Tests print requests + */ + public function testPrintRequests() { + $this->assertTransformCssMediaCase( array( + 'printableQuery' => '1', + 'media' => 'screen', + 'expectedReturn' => null, + 'message' => 'On printable request, screen returns null' + ) ); + + $this->assertTransformCssMediaCase( array( + 'printableQuery' => '1', + 'media' => self::SCREEN_MEDIA_QUERY, + 'expectedReturn' => null, + 'message' => 'On printable request, screen media query returns null' + ) ); + + $this->assertTransformCssMediaCase( array( + 'printableQuery' => '1', + 'media' => self::SCREEN_ONLY_MEDIA_QUERY, + 'expectedReturn' => null, + 'message' => 'On printable request, screen media query with only returns null' + ) ); + + $this->assertTransformCssMediaCase( array( + 'printableQuery' => '1', + 'media' => 'print', + 'expectedReturn' => '', + 'message' => 'On printable request, media print returns empty string' + ) ); + } + + /** + * Tests screen requests, without either query parameter set + */ + public function testScreenRequests() { + $this->assertTransformCssMediaCase( array( + 'media' => 'screen', + 'expectedReturn' => 'screen', + 'message' => 'On screen request, screen media type is preserved' + ) ); + + $this->assertTransformCssMediaCase( array( + 'media' => 'handheld', + 'expectedReturn' => 'handheld', + 'message' => 'On screen request, handheld media type is preserved' + ) ); + + $this->assertTransformCssMediaCase( array( + 'media' => self::SCREEN_MEDIA_QUERY, + 'expectedReturn' => self::SCREEN_MEDIA_QUERY, + 'message' => 'On screen request, screen media query is preserved.' + ) ); + + $this->assertTransformCssMediaCase( array( + 'media' => self::SCREEN_ONLY_MEDIA_QUERY, + 'expectedReturn' => self::SCREEN_ONLY_MEDIA_QUERY, + 'message' => 'On screen request, screen media query with only is preserved.' + ) ); + + $this->assertTransformCssMediaCase( array( + 'media' => 'print', + 'expectedReturn' => 'print', + 'message' => 'On screen request, print media type is preserved' + ) ); + } + + /** + * Tests handheld behavior + */ + public function testHandheld() { + $this->assertTransformCssMediaCase( array( + 'handheldQuery' => '1', + 'media' => 'handheld', + 'expectedReturn' => '', + 'message' => 'On request with handheld querystring and media is handheld, returns empty string' + ) ); + + $this->assertTransformCssMediaCase( array( + 'handheldQuery' => '1', + 'media' => 'screen', + 'expectedReturn' => null, + 'message' => 'On request with handheld querystring and media is screen, returns null' + ) ); + } +} diff --git a/tests/phpunit/includes/PathRouterTest.php b/tests/phpunit/includes/PathRouterTest.php new file mode 100644 index 00000000..adfb215a --- /dev/null +++ b/tests/phpunit/includes/PathRouterTest.php @@ -0,0 +1,263 @@ +add( "/wiki/$1" ); + $this->basicRouter = $router; + } + + /** + * Test basic path parsing + */ + public function testBasic() { + $matches = $this->basicRouter->parse( "/wiki/Foo" ); + $this->assertEquals( $matches, array( 'title' => "Foo" ) ); + } + + /** + * Test loose path auto-$1 + */ + public function testLoose() { + $router = new PathRouter; + $router->add( "/" ); # Should be the same as "/$1" + $matches = $router->parse( "/Foo" ); + $this->assertEquals( $matches, array( 'title' => "Foo" ) ); + + $router = new PathRouter; + $router->add( "/wiki" ); # Should be the same as /wiki/$1 + $matches = $router->parse( "/wiki/Foo" ); + $this->assertEquals( $matches, array( 'title' => "Foo" ) ); + + $router = new PathRouter; + $router->add( "/wiki/" ); # Should be the same as /wiki/$1 + $matches = $router->parse( "/wiki/Foo" ); + $this->assertEquals( $matches, array( 'title' => "Foo" ) ); + } + + /** + * Test to ensure that path is based on specifity, not order + */ + public function testOrder() { + $router = new PathRouter; + $router->add( "/$1" ); + $router->add( "/a/$1" ); + $router->add( "/b/$1" ); + $matches = $router->parse( "/a/Foo" ); + $this->assertEquals( $matches, array( 'title' => "Foo" ) ); + + $router = new PathRouter; + $router->add( "/b/$1" ); + $router->add( "/a/$1" ); + $router->add( "/$1" ); + $matches = $router->parse( "/a/Foo" ); + $this->assertEquals( $matches, array( 'title' => "Foo" ) ); + } + + /** + * Test the handling of key based arrays with a url parameter + */ + public function testKeyParameter() { + $router = new PathRouter; + $router->add( array( 'edit' => "/edit/$1" ), array( 'action' => '$key' ) ); + $matches = $router->parse( "/edit/Foo" ); + $this->assertEquals( $matches, array( 'title' => "Foo", 'action' => 'edit' ) ); + } + + /** + * Test the handling of $2 inside paths + */ + public function testAdditionalParameter() { + // Basic $2 + $router = new PathRouter; + $router->add( '/$2/$1', array( 'test' => '$2' ) ); + $matches = $router->parse( "/asdf/Foo" ); + $this->assertEquals( $matches, array( 'title' => "Foo", 'test' => 'asdf' ) ); + } + + /** + * Test additional restricted value parameter + */ + public function testRestrictedValue() { + $router = new PathRouter; + $router->add( '/$2/$1', + array( 'test' => '$2' ), + array( '$2' => array( 'a', 'b' ) ) + ); + $router->add( '/$2/$1', + array( 'test2' => '$2' ), + array( '$2' => 'c' ) + ); + $router->add( '/$1' ); + + $matches = $router->parse( "/asdf/Foo" ); + $this->assertEquals( $matches, array( 'title' => "asdf/Foo" ) ); + + $matches = $router->parse( "/a/Foo" ); + $this->assertEquals( $matches, array( 'title' => "Foo", 'test' => 'a' ) ); + + $matches = $router->parse( "/c/Foo" ); + $this->assertEquals( $matches, array( 'title' => "Foo", 'test2' => 'c' ) ); + } + + public function callbackForTest( &$matches, $data ) { + $matches['x'] = $data['$1']; + $matches['foo'] = $data['foo']; + } + + public function testCallback() { + $router = new PathRouter; + $router->add( "/$1", + array( 'a' => 'b', 'data:foo' => 'bar' ), + array( 'callback' => array( $this, 'callbackForTest' ) ) + ); + $matches = $router->parse( '/Foo' ); + $this->assertEquals( $matches, array( + 'title' => "Foo", + 'x' => 'Foo', + 'a' => 'b', + 'foo' => 'bar' + ) ); + } + + /** + * Test to ensure that matches are not made if a parameter expects nonexistent input + */ + public function testFail() { + $router = new PathRouter; + $router->add( "/wiki/$1", array( 'title' => "$1$2" ) ); + $matches = $router->parse( "/wiki/A" ); + $this->assertEquals( array(), $matches ); + } + + /** + * Test to ensure weight of paths is handled correctly + */ + public function testWeight() { + $router = new PathRouter; + $router->addStrict( "/Bar", array( 'ping' => 'pong' ) ); + $router->add( "/asdf-$1", array( 'title' => 'qwerty-$1' ) ); + $router->add( "/$1" ); + $router->add( "/qwerty-$1", array( 'title' => 'asdf-$1' ) ); + $router->addStrict( "/Baz", array( 'marco' => 'polo' ) ); + $router->add( "/a/$1" ); + $router->add( "/asdf/$1" ); + $router->add( "/$2/$1", array( 'unrestricted' => '$2' ) ); + $router->add( array( 'qwerty' => "/qwerty/$1" ), array( 'qwerty' => '$key' ) ); + $router->add( "/$2/$1", array( 'restricted-to-y' => '$2' ), array( '$2' => 'y' ) ); + + foreach ( + array( + '/Foo' => array( 'title' => 'Foo' ), + '/Bar' => array( 'ping' => 'pong' ), + '/Baz' => array( 'marco' => 'polo' ), + '/asdf-foo' => array( 'title' => 'qwerty-foo' ), + '/qwerty-bar' => array( 'title' => 'asdf-bar' ), + '/a/Foo' => array( 'title' => 'Foo' ), + '/asdf/Foo' => array( 'title' => 'Foo' ), + '/qwerty/Foo' => array( 'title' => 'Foo', 'qwerty' => 'qwerty' ), + '/baz/Foo' => array( 'title' => 'Foo', 'unrestricted' => 'baz' ), + '/y/Foo' => array( 'title' => 'Foo', 'restricted-to-y' => 'y' ), + ) as $path => $result + ) { + $this->assertEquals( $router->parse( $path ), $result ); + } + } + + /** + * Make sure the router handles titles like Special:Recentchanges correctly + */ + public function testSpecial() { + $matches = $this->basicRouter->parse( "/wiki/Special:Recentchanges" ); + $this->assertEquals( $matches, array( 'title' => "Special:Recentchanges" ) ); + } + + /** + * Make sure the router decodes urlencoding properly + */ + public function testUrlencoding() { + $matches = $this->basicRouter->parse( "/wiki/Title_With%20Space" ); + $this->assertEquals( $matches, array( 'title' => "Title_With Space" ) ); + } + + public static function provideRegexpChars() { + return array( + array( "$" ), + array( "$1" ), + array( "\\" ), + array( "\\$1" ), + ); + } + + /** + * Make sure the router doesn't break on special characters like $ used in regexp replacements + * @dataProvider provideRegexpChars + */ + public function testRegexpChars( $char ) { + $matches = $this->basicRouter->parse( "/wiki/$char" ); + $this->assertEquals( $matches, array( 'title' => "$char" ) ); + } + + /** + * Make sure the router handles characters like +&() properly + */ + public function testCharacters() { + $matches = $this->basicRouter->parse( "/wiki/Plus+And&Dollar\\Stuff();[]{}*" ); + $this->assertEquals( $matches, array( 'title' => "Plus+And&Dollar\\Stuff();[]{}*" ) ); + } + + /** + * Make sure the router handles unicode characters correctly + * @depends testSpecial + * @depends testUrlencoding + * @depends testCharacters + */ + public function testUnicode() { + $matches = $this->basicRouter->parse( "/wiki/Spécial:Modifications_récentes" ); + $this->assertEquals( $matches, array( 'title' => "Spécial:Modifications_récentes" ) ); + + $matches = $this->basicRouter->parse( "/wiki/Sp%C3%A9cial:Modifications_r%C3%A9centes" ); + $this->assertEquals( $matches, array( 'title' => "Spécial:Modifications_récentes" ) ); + } + + /** + * Ensure the router doesn't choke on long paths. + */ + public function testLength() { + $matches = $this->basicRouter->parse( "/wiki/Lorem_ipsum_dolor_sit_amet,_consectetur_adipisicing_elit,_sed_do_eiusmod_tempor_incididunt_ut_labore_et_dolore_magna_aliqua._Ut_enim_ad_minim_veniam,_quis_nostrud_exercitation_ullamco_laboris_nisi_ut_aliquip_ex_ea_commodo_consequat._Duis_aute_irure_dolor_in_reprehenderit_in_voluptate_velit_esse_cillum_dolore_eu_fugiat_nulla_pariatur._Excepteur_sint_occaecat_cupidatat_non_proident,_sunt_in_culpa_qui_officia_deserunt_mollit_anim_id_est_laborum." ); + $this->assertEquals( $matches, array( 'title' => "Lorem_ipsum_dolor_sit_amet,_consectetur_adipisicing_elit,_sed_do_eiusmod_tempor_incididunt_ut_labore_et_dolore_magna_aliqua._Ut_enim_ad_minim_veniam,_quis_nostrud_exercitation_ullamco_laboris_nisi_ut_aliquip_ex_ea_commodo_consequat._Duis_aute_irure_dolor_in_reprehenderit_in_voluptate_velit_esse_cillum_dolore_eu_fugiat_nulla_pariatur._Excepteur_sint_occaecat_cupidatat_non_proident,_sunt_in_culpa_qui_officia_deserunt_mollit_anim_id_est_laborum." ) ); + } + + + /** + * Ensure that the php passed site of parameter values are not urldecoded + */ + public function testPatternUrlencoding() { + $router = new PathRouter; + $router->add( "/wiki/$1", array( 'title' => '%20:$1' ) ); + $matches = $router->parse( "/wiki/Foo" ); + $this->assertEquals( $matches, array( 'title' => '%20:Foo' ) ); + } + + /** + * Ensure that raw parameter values do not have any variable replacements or urldecoding + */ + public function testRawParamValue() { + $router = new PathRouter; + $router->add( "/wiki/$1", array( 'title' => array( 'value' => 'bar%20$1' ) ) ); + $matches = $router->parse( "/wiki/Foo" ); + $this->assertEquals( $matches, array( 'title' => 'bar%20$1' ) ); + } +} diff --git a/tests/phpunit/includes/PreferencesTest.php b/tests/phpunit/includes/PreferencesTest.php new file mode 100644 index 00000000..3dec2da0 --- /dev/null +++ b/tests/phpunit/includes/PreferencesTest.php @@ -0,0 +1,91 @@ +prefUsers['noemail'] = new User; + + $this->prefUsers['notauth'] = new User; + $this->prefUsers['notauth'] + ->setEmail( 'noauth@example.org' ); + + $this->prefUsers['auth'] = new User; + $this->prefUsers['auth'] + ->setEmail( 'noauth@example.org' ); + $this->prefUsers['auth'] + ->setEmailAuthenticationTimestamp( 1330946623 ); + + $this->context = new RequestContext; + $this->context->setTitle( Title::newFromText( 'PreferencesTest' ) ); + } + + protected function setUp() { + parent::setUp(); + + $this->setMwGlobals( array( + 'wgEnableEmail' => true, + 'wgEmailAuthentication' => true, + ) ); + } + + /** + * Placeholder to verify bug 34302 + * @covers Preferences::profilePreferences + */ + public function testEmailFieldsWhenUserHasNoEmail() { + $prefs = $this->prefsFor( 'noemail' ); + $this->assertArrayHasKey( 'cssclass', + $prefs['emailaddress'] + ); + $this->assertEquals( 'mw-email-none', $prefs['emailaddress']['cssclass'] ); + } + + /** + * Placeholder to verify bug 34302 + * @covers Preferences::profilePreferences + */ + public function testEmailFieldsWhenUserEmailNotAuthenticated() { + $prefs = $this->prefsFor( 'notauth' ); + $this->assertArrayHasKey( 'cssclass', + $prefs['emailaddress'] + ); + $this->assertEquals( 'mw-email-not-authenticated', $prefs['emailaddress']['cssclass'] ); + } + + /** + * Placeholder to verify bug 34302 + * @covers Preferences::profilePreferences + */ + public function testEmailFieldsWhenUserEmailIsAuthenticated() { + $prefs = $this->prefsFor( 'auth' ); + $this->assertArrayHasKey( 'cssclass', + $prefs['emailaddress'] + ); + $this->assertEquals( 'mw-email-authenticated', $prefs['emailaddress']['cssclass'] ); + } + + /** Helper */ + protected function prefsFor( $user_key ) { + $preferences = array(); + Preferences::profilePreferences( + $this->prefUsers[$user_key] + , $this->context + , $preferences + ); + + return $preferences; + } +} diff --git a/tests/phpunit/includes/RecentChangeTest.php b/tests/phpunit/includes/RecentChangeTest.php new file mode 100644 index 00000000..cfa3e777 --- /dev/null +++ b/tests/phpunit/includes/RecentChangeTest.php @@ -0,0 +1,280 @@ +title = Title::newFromText( 'SomeTitle' ); + $this->target = Title::newFromText( 'TestTarget' ); + $this->user = User::newFromName( 'UserName' ); + + $this->user_comment = ''; + $this->context = RequestContext::newExtraneousContext( $this->title ); + } + + /** + * The testIrcMsgForAction* tests are supposed to cover the hacky + * LogFormatter::getIRCActionText / bug 34508 + * + * Third parties bots listen to those messages. They are clever enough + * to fetch the i18n messages from the wiki and then analyze the IRC feed + * to reverse engineer the $1, $2 messages. + * One thing bots can not detect is when MediaWiki change the meaning of + * a message like what happened when we deployed 1.19. $1 became the user + * performing the action which broke basically all bots around. + * + * Should cover the following log actions (which are most commonly used by bots): + * - block/block + * - block/unblock + * - delete/delete + * - delete/restore + * - newusers/create + * - newusers/create2 + * - newusers/autocreate + * - move/move + * - move/move_redir + * - protect/protect + * - protect/modifyprotect + * - protect/unprotect + * - upload/upload + * + * As well as the following Auto Edit Summaries: + * - blank + * - replace + * - rollback + * - undo + */ + + /** + * @covers LogFormatter::getIRCActionText + */ + public function testIrcMsgForLogTypeBlock() { + $sep = $this->context->msg( 'colon-separator' )->text(); + + # block/block + $this->assertIRCComment( + $this->context->msg( 'blocklogentry', 'SomeTitle' )->plain() . $sep . $this->user_comment, + 'block', 'block', + array(), + $this->user_comment + ); + # block/unblock + $this->assertIRCComment( + $this->context->msg( 'unblocklogentry', 'SomeTitle' )->plain() . $sep . $this->user_comment, + 'block', 'unblock', + array(), + $this->user_comment + ); + } + + /** + * @covers LogFormatter::getIRCActionText + */ + public function testIrcMsgForLogTypeDelete() { + $sep = $this->context->msg( 'colon-separator' )->text(); + + # delete/delete + $this->assertIRCComment( + $this->context->msg( 'deletedarticle', 'SomeTitle' )->plain() . $sep . $this->user_comment, + 'delete', 'delete', + array(), + $this->user_comment + ); + + # delete/restore + $this->assertIRCComment( + $this->context->msg( 'undeletedarticle', 'SomeTitle' )->plain() . $sep . $this->user_comment, + 'delete', 'restore', + array(), + $this->user_comment + ); + } + + /** + * @covers LogFormatter::getIRCActionText + */ + public function testIrcMsgForLogTypeNewusers() { + $this->assertIRCComment( + 'New user account', + 'newusers', 'newusers', + array() + ); + $this->assertIRCComment( + 'New user account', + 'newusers', 'create', + array() + ); + $this->assertIRCComment( + 'created new account SomeTitle', + 'newusers', 'create2', + array() + ); + $this->assertIRCComment( + 'Account created automatically', + 'newusers', 'autocreate', + array() + ); + } + + /** + * @covers LogFormatter::getIRCActionText + */ + public function testIrcMsgForLogTypeMove() { + $move_params = array( + '4::target' => $this->target->getPrefixedText(), + '5::noredir' => 0, + ); + $sep = $this->context->msg( 'colon-separator' )->text(); + + # move/move + $this->assertIRCComment( + $this->context->msg( '1movedto2', 'SomeTitle', 'TestTarget' )->plain() . $sep . $this->user_comment, + 'move', 'move', + $move_params, + $this->user_comment + ); + + # move/move_redir + $this->assertIRCComment( + $this->context->msg( '1movedto2_redir', 'SomeTitle', 'TestTarget' )->plain() . $sep . $this->user_comment, + 'move', 'move_redir', + $move_params, + $this->user_comment + ); + } + + /** + * @covers LogFormatter::getIRCActionText + */ + public function testIrcMsgForLogTypePatrol() { + # patrol/patrol + $this->assertIRCComment( + $this->context->msg( 'patrol-log-line', 'revision 777', '[[SomeTitle]]', '' )->plain(), + 'patrol', 'patrol', + array( + '4::curid' => '777', + '5::previd' => '666', + '6::auto' => 0, + ) + ); + } + + /** + * @covers LogFormatter::getIRCActionText + */ + public function testIrcMsgForLogTypeProtect() { + $protectParams = array( + '[edit=sysop] (indefinite) ‎[move=sysop] (indefinite)' + ); + $sep = $this->context->msg( 'colon-separator' )->text(); + + # protect/protect + $this->assertIRCComment( + $this->context->msg( 'protectedarticle', 'SomeTitle ' . $protectParams[0] )->plain() . $sep . $this->user_comment, + 'protect', 'protect', + $protectParams, + $this->user_comment + ); + + # protect/unprotect + $this->assertIRCComment( + $this->context->msg( 'unprotectedarticle', 'SomeTitle' )->plain() . $sep . $this->user_comment, + 'protect', 'unprotect', + array(), + $this->user_comment + ); + + # protect/modify + $this->assertIRCComment( + $this->context->msg( 'modifiedarticleprotection', 'SomeTitle ' . $protectParams[0] )->plain() . $sep . $this->user_comment, + 'protect', 'modify', + $protectParams, + $this->user_comment + ); + } + + /** + * @covers LogFormatter::getIRCActionText + */ + public function testIrcMsgForLogTypeUpload() { + $sep = $this->context->msg( 'colon-separator' )->text(); + + # upload/upload + $this->assertIRCComment( + $this->context->msg( 'uploadedimage', 'SomeTitle' )->plain() . $sep . $this->user_comment, + 'upload', 'upload', + array(), + $this->user_comment + ); + + # upload/overwrite + $this->assertIRCComment( + $this->context->msg( 'overwroteimage', 'SomeTitle' )->plain() . $sep . $this->user_comment, + 'upload', 'overwrite', + array(), + $this->user_comment + ); + } + + /** + * @todo Emulate these edits somehow and extract + * raw edit summary from RecentChange object + * -- + */ + /* + public function testIrcMsgForBlankingAES() { + // $this->context->msg( 'autosumm-blank', .. ); + } + + public function testIrcMsgForReplaceAES() { + // $this->context->msg( 'autosumm-replace', .. ); + } + + public function testIrcMsgForRollbackAES() { + // $this->context->msg( 'revertpage', .. ); + } + + public function testIrcMsgForUndoAES() { + // $this->context->msg( 'undo-summary', .. ); + } + */ + + /** + * @param $expected String Expected IRC text without colors codes + * @param $type String Log type (move, delete, suppress, patrol ...) + * @param $action String A log type action + * @param $params + * @param $comment String (optional) A comment for the log action + * @param $msg String (optional) A message for PHPUnit :-) + */ + protected function assertIRCComment( $expected, $type, $action, $params, $comment = null, $msg = '' ) { + + $logEntry = new ManualLogEntry( $type, $action ); + $logEntry->setPerformer( $this->user ); + $logEntry->setTarget( $this->title ); + if ( $comment !== null ) { + $logEntry->setComment( $comment ); + } + $logEntry->setParameters( $params ); + + $formatter = LogFormatter::newFromEntry( $logEntry ); + $formatter->setContext( $this->context ); + + // Apply the same transformation as done in IRCColourfulRCFeedFormatter::getLine for rc_comment + $ircRcComment = IRCColourfulRCFeedFormatter::cleanupForIRC( $formatter->getIRCActionComment() ); + + $this->assertEquals( + $expected, + $ircRcComment, + $msg + ); + } +} diff --git a/tests/phpunit/includes/RequestContextTest.php b/tests/phpunit/includes/RequestContextTest.php new file mode 100644 index 00000000..1776b5d5 --- /dev/null +++ b/tests/phpunit/includes/RequestContextTest.php @@ -0,0 +1,73 @@ +setTitle( $curTitle ); + $this->assertTrue( $curTitle->equals( $context->getWikiPage()->getTitle() ), + "When a title is first set WikiPage should be created on-demand for that title." ); + + $curTitle = Title::newFromText( "B" ); + $context->setWikiPage( WikiPage::factory( $curTitle ) ); + $this->assertTrue( $curTitle->equals( $context->getTitle() ), + "Title must be updated when a new WikiPage is provided." ); + + $curTitle = Title::newFromText( "C" ); + $context->setTitle( $curTitle ); + $this->assertTrue( $curTitle->equals( $context->getWikiPage()->getTitle() ), + "When a title is updated the WikiPage should be purged and recreated on-demand with the new title." ); + } + + /** + * @covers RequestContext::importScopedSession + */ + public function testImportScopedSession() { + $context = RequestContext::getMain(); + + $oInfo = $context->exportSession(); + $this->assertEquals( '127.0.0.1', $oInfo['ip'], "Correct initial IP address." ); + $this->assertEquals( 0, $oInfo['userId'], "Correct initial user ID." ); + + $user = User::newFromName( 'UnitTestContextUser' ); + $user->addToDatabase(); + + $sinfo = array( + 'sessionId' => 'd612ee607c87e749ef14da4983a702cd', + 'userId' => $user->getId(), + 'ip' => '192.0.2.0', + 'headers' => array( 'USER-AGENT' => 'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:18.0) Gecko/20100101 Firefox/18.0' ) + ); + $sc = RequestContext::importScopedSession( $sinfo ); // load new context + + $info = $context->exportSession(); + $this->assertEquals( $sinfo['ip'], $info['ip'], "Correct IP address." ); + $this->assertEquals( $sinfo['headers'], $info['headers'], "Correct headers." ); + $this->assertEquals( $sinfo['sessionId'], $info['sessionId'], "Correct session ID." ); + $this->assertEquals( $sinfo['userId'], $info['userId'], "Correct user ID." ); + $this->assertEquals( $sinfo['ip'], $context->getRequest()->getIP(), "Correct context IP address." ); + $this->assertEquals( $sinfo['headers'], $context->getRequest()->getAllHeaders(), "Correct context headers." ); + $this->assertEquals( $sinfo['sessionId'], session_id(), "Correct context session ID." ); + $this->assertEquals( true, $context->getUser()->isLoggedIn(), "Correct context user." ); + $this->assertEquals( $sinfo['userId'], $context->getUser()->getId(), "Correct context user ID." ); + $this->assertEquals( 'UnitTestContextUser', $context->getUser()->getName(), "Correct context user name." ); + + unset( $sc ); // restore previous context + + $info = $context->exportSession(); + $this->assertEquals( $oInfo['ip'], $info['ip'], "Correct initial IP address." ); + $this->assertEquals( $oInfo['headers'], $info['headers'], "Correct initial headers." ); + $this->assertEquals( $oInfo['sessionId'], $info['sessionId'], "Correct initial session ID." ); + $this->assertEquals( $oInfo['userId'], $info['userId'], "Correct initial user ID." ); + } +} diff --git a/tests/phpunit/includes/ResourceLoaderTest.php b/tests/phpunit/includes/ResourceLoaderTest.php new file mode 100644 index 00000000..ca8b2b6e --- /dev/null +++ b/tests/phpunit/includes/ResourceLoaderTest.php @@ -0,0 +1,149 @@ +setMwGlobals( array( + 'wgResourceLoaderLESSFunctions' => array( + 'test-sum' => function ( $frame, $less ) { + $sum = 0; + foreach ( $frame[2] as $arg ) { + $sum += (int)$arg[1]; + } + return $sum; + }, + ), + 'wgResourceLoaderLESSImportPaths' => array( + dirname( __DIR__ ) . '/data/less/common', + ), + 'wgResourceLoaderLESSVars' => array( + 'foo' => '2px', + 'Foo' => '#eeeeee', + 'bar' => 5, + ), + ) ); + } + + /* Hook Methods */ + + /** + * ResourceLoaderRegisterModules hook + */ + public static function resourceLoaderRegisterModules( &$resourceLoader ) { + self::$resourceLoaderRegisterModulesHook = true; + + return true; + } + + /* Provider Methods */ + public static function provideValidModules() { + return array( + array( 'TEST.validModule1', new ResourceLoaderTestModule() ), + ); + } + + public static function provideResourceLoaderContext() { + $resourceLoader = new ResourceLoader(); + $request = new FauxRequest(); + return array( + array( new ResourceLoaderContext( $resourceLoader, $request ) ), + ); + } + + /* Test Methods */ + + /** + * Ensures that the ResourceLoaderRegisterModules hook is called when a new ResourceLoader object is constructed + * @covers ResourceLoader::__construct + */ + public function testCreatingNewResourceLoaderCallsRegistrationHook() { + self::$resourceLoaderRegisterModulesHook = false; + $resourceLoader = new ResourceLoader(); + $this->assertTrue( self::$resourceLoaderRegisterModulesHook ); + + return $resourceLoader; + } + + /** + * @dataProvider provideValidModules + * @depends testCreatingNewResourceLoaderCallsRegistrationHook + * @covers ResourceLoader::register + * @covers ResourceLoader::getModule + */ + public function testRegisteredValidModulesAreAccessible( + $name, ResourceLoaderModule $module, ResourceLoader $resourceLoader + ) { + $resourceLoader->register( $name, $module ); + $this->assertEquals( $module, $resourceLoader->getModule( $name ) ); + } + + /** + * @dataProvider provideResourceLoaderContext + * @covers ResourceLoaderFileModule::compileLessFile + */ + public function testLessFileCompilation( $context ) { + $basePath = __DIR__ . '/../data/less/module'; + $module = new ResourceLoaderFileModule( array( + 'localBasePath' => $basePath, + 'styles' => array( 'styles.less' ), + ) ); + $styles = $module->getStyles( $context ); + $this->assertStringEqualsFile( $basePath . '/styles.css', $styles['all'] ); + } + + /** + * @dataProvider providePackedModules + * @covers ResourceLoader::makePackedModulesString + */ + public function testMakePackedModulesString( $desc, $modules, $packed ) { + $this->assertEquals( $packed, ResourceLoader::makePackedModulesString( $modules ), $desc ); + } + + /** + * @dataProvider providePackedModules + * @covers ResourceLoaderContext::expandModuleNames + */ + public function testexpandModuleNames( $desc, $modules, $packed ) { + $this->assertEquals( $modules, ResourceLoaderContext::expandModuleNames( $packed ), $desc ); + } + + public static function providePackedModules() { + return array( + array( + 'Example from makePackedModulesString doc comment', + array( 'foo.bar', 'foo.baz', 'bar.baz', 'bar.quux' ), + 'foo.bar,baz|bar.baz,quux', + ), + array( + 'Example from expandModuleNames doc comment', + array( 'jquery.foo', 'jquery.bar', 'jquery.ui.baz', 'jquery.ui.quux' ), + 'jquery.foo,bar|jquery.ui.baz,quux', + ), + array( + 'Regression fixed in r88706 with dotless names', + array( 'foo', 'bar', 'baz' ), + 'foo,bar,baz', + ), + array( + 'Prefixless modules after a prefixed module', + array( 'single.module', 'foobar', 'foobaz' ), + 'single.module|foobar,foobaz', + ), + ); + } +} + +/* Stubs */ + +class ResourceLoaderTestModule extends ResourceLoaderModule { +} + +/* Hooks */ +global $wgHooks; +$wgHooks['ResourceLoaderRegisterModules'][] = 'ResourceLoaderTest::resourceLoaderRegisterModules'; diff --git a/tests/phpunit/includes/RevisionStorageTest.php b/tests/phpunit/includes/RevisionStorageTest.php new file mode 100644 index 00000000..e17c7b0f --- /dev/null +++ b/tests/phpunit/includes/RevisionStorageTest.php @@ -0,0 +1,546 @@ +tablesUsed = array_merge( $this->tablesUsed, + array( 'page', + 'revision', + 'text', + + 'recentchanges', + 'logging', + + 'page_props', + 'pagelinks', + 'categorylinks', + 'langlinks', + 'externallinks', + 'imagelinks', + 'templatelinks', + 'iwlinks' ) ); + } + + protected function setUp() { + global $wgExtraNamespaces, $wgNamespaceContentModels, $wgContentHandlers, $wgContLang; + + parent::setUp(); + + $wgExtraNamespaces[12312] = 'Dummy'; + $wgExtraNamespaces[12313] = 'Dummy_talk'; + + $wgNamespaceContentModels[12312] = 'DUMMY'; + $wgContentHandlers['DUMMY'] = 'DummyContentHandlerForTesting'; + + MWNamespace::getCanonicalNamespaces( true ); # reset namespace cache + $wgContLang->resetNamespaces(); # reset namespace cache + if ( !$this->the_page ) { + $this->the_page = $this->createPage( 'RevisionStorageTest_the_page', "just a dummy page", CONTENT_MODEL_WIKITEXT ); + } + } + + public function tearDown() { + global $wgExtraNamespaces, $wgNamespaceContentModels, $wgContentHandlers, $wgContLang; + + parent::tearDown(); + + unset( $wgExtraNamespaces[12312] ); + unset( $wgExtraNamespaces[12313] ); + + unset( $wgNamespaceContentModels[12312] ); + unset( $wgContentHandlers['DUMMY'] ); + + MWNamespace::getCanonicalNamespaces( true ); # reset namespace cache + $wgContLang->resetNamespaces(); # reset namespace cache + } + + protected function makeRevision( $props = null ) { + if ( $props === null ) { + $props = array(); + } + + if ( !isset( $props['content'] ) && !isset( $props['text'] ) ) { + $props['text'] = 'Lorem Ipsum'; + } + + if ( !isset( $props['comment'] ) ) { + $props['comment'] = 'just a test'; + } + + if ( !isset( $props['page'] ) ) { + $props['page'] = $this->the_page->getId(); + } + + $rev = new Revision( $props ); + + $dbw = wfgetDB( DB_MASTER ); + $rev->insertOn( $dbw ); + + return $rev; + } + + protected function createPage( $page, $text, $model = null ) { + if ( is_string( $page ) ) { + if ( !preg_match( '/:/', $page ) && + ( $model === null || $model === CONTENT_MODEL_WIKITEXT ) + ) { + $ns = $this->getDefaultWikitextNS(); + $page = MWNamespace::getCanonicalName( $ns ) . ':' . $page; + } + + $page = Title::newFromText( $page ); + } + + if ( $page instanceof Title ) { + $page = new WikiPage( $page ); + } + + if ( $page->exists() ) { + $page->doDeleteArticle( "done" ); + } + + $content = ContentHandler::makeContent( $text, $page->getTitle(), $model ); + $page->doEditContent( $content, "testing", EDIT_NEW ); + + return $page; + } + + protected function assertRevEquals( Revision $orig, Revision $rev = null ) { + $this->assertNotNull( $rev, 'missing revision' ); + + $this->assertEquals( $orig->getId(), $rev->getId() ); + $this->assertEquals( $orig->getPage(), $rev->getPage() ); + $this->assertEquals( $orig->getTimestamp(), $rev->getTimestamp() ); + $this->assertEquals( $orig->getUser(), $rev->getUser() ); + $this->assertEquals( $orig->getContentModel(), $rev->getContentModel() ); + $this->assertEquals( $orig->getContentFormat(), $rev->getContentFormat() ); + $this->assertEquals( $orig->getSha1(), $rev->getSha1() ); + } + + /** + * @covers Revision::__construct + */ + public function testConstructFromRow() { + $orig = $this->makeRevision(); + + $dbr = wfgetDB( DB_SLAVE ); + $res = $dbr->select( 'revision', '*', array( 'rev_id' => $orig->getId() ) ); + $this->assertTrue( is_object( $res ), 'query failed' ); + + $row = $res->fetchObject(); + $res->free(); + + $rev = new Revision( $row ); + + $this->assertRevEquals( $orig, $rev ); + } + + /** + * @covers Revision::newFromRow + */ + public function testNewFromRow() { + $orig = $this->makeRevision(); + + $dbr = wfgetDB( DB_SLAVE ); + $res = $dbr->select( 'revision', '*', array( 'rev_id' => $orig->getId() ) ); + $this->assertTrue( is_object( $res ), 'query failed' ); + + $row = $res->fetchObject(); + $res->free(); + + $rev = Revision::newFromRow( $row ); + + $this->assertRevEquals( $orig, $rev ); + } + + + /** + * @covers Revision::newFromArchiveRow + */ + public function testNewFromArchiveRow() { + $page = $this->createPage( 'RevisionStorageTest_testNewFromArchiveRow', 'Lorem Ipsum', CONTENT_MODEL_WIKITEXT ); + $orig = $page->getRevision(); + $page->doDeleteArticle( 'test Revision::newFromArchiveRow' ); + + $dbr = wfgetDB( DB_SLAVE ); + $res = $dbr->select( 'archive', '*', array( 'ar_rev_id' => $orig->getId() ) ); + $this->assertTrue( is_object( $res ), 'query failed' ); + + $row = $res->fetchObject(); + $res->free(); + + $rev = Revision::newFromArchiveRow( $row ); + + $this->assertRevEquals( $orig, $rev ); + } + + /** + * @covers Revision::newFromId + */ + public function testNewFromId() { + $orig = $this->makeRevision(); + + $rev = Revision::newFromId( $orig->getId() ); + + $this->assertRevEquals( $orig, $rev ); + } + + /** + * @covers Revision::fetchRevision + */ + public function testFetchRevision() { + $page = $this->createPage( 'RevisionStorageTest_testFetchRevision', 'one', CONTENT_MODEL_WIKITEXT ); + $id1 = $page->getRevision()->getId(); + + $page->doEditContent( new WikitextContent( 'two' ), 'second rev' ); + $id2 = $page->getRevision()->getId(); + + $res = Revision::fetchRevision( $page->getTitle() ); + + #note: order is unspecified + $rows = array(); + while ( ( $row = $res->fetchObject() ) ) { + $rows[$row->rev_id] = $row; + } + + $row = $res->fetchObject(); + $this->assertEquals( 1, count( $rows ), 'expected exactly one revision' ); + $this->assertArrayHasKey( $id2, $rows, 'missing revision with id ' . $id2 ); + } + + /** + * @covers Revision::selectFields + */ + public function testSelectFields() { + global $wgContentHandlerUseDB; + + $fields = Revision::selectFields(); + + $this->assertTrue( in_array( 'rev_id', $fields ), 'missing rev_id in list of fields' ); + $this->assertTrue( in_array( 'rev_page', $fields ), 'missing rev_page in list of fields' ); + $this->assertTrue( in_array( 'rev_timestamp', $fields ), 'missing rev_timestamp in list of fields' ); + $this->assertTrue( in_array( 'rev_user', $fields ), 'missing rev_user in list of fields' ); + + if ( $wgContentHandlerUseDB ) { + $this->assertTrue( in_array( 'rev_content_model', $fields ), + 'missing rev_content_model in list of fields' ); + $this->assertTrue( in_array( 'rev_content_format', $fields ), + 'missing rev_content_format in list of fields' ); + } + } + + /** + * @covers Revision::getPage + */ + public function testGetPage() { + $page = $this->the_page; + + $orig = $this->makeRevision( array( 'page' => $page->getId() ) ); + $rev = Revision::newFromId( $orig->getId() ); + + $this->assertEquals( $page->getId(), $rev->getPage() ); + } + + /** + * @covers Revision::getText + */ + public function testGetText() { + $this->hideDeprecated( 'Revision::getText' ); + + $orig = $this->makeRevision( array( 'text' => 'hello hello.' ) ); + $rev = Revision::newFromId( $orig->getId() ); + + $this->assertEquals( 'hello hello.', $rev->getText() ); + } + + /** + * @covers Revision::getContent + */ + public function testGetContent_failure() { + $rev = new Revision( array( + 'page' => $this->the_page->getId(), + 'content_model' => $this->the_page->getContentModel(), + 'text_id' => 123456789, // not in the test DB + ) ); + + $this->assertNull( $rev->getContent(), + "getContent() should return null if the revision's text blob could not be loaded." ); + + //NOTE: check this twice, once for lazy initialization, and once with the cached value. + $this->assertNull( $rev->getContent(), + "getContent() should return null if the revision's text blob could not be loaded." ); + } + + /** + * @covers Revision::getContent + */ + public function testGetContent() { + $orig = $this->makeRevision( array( 'text' => 'hello hello.' ) ); + $rev = Revision::newFromId( $orig->getId() ); + + $this->assertEquals( 'hello hello.', $rev->getContent()->getNativeData() ); + } + + /** + * @covers Revision::revText + */ + public function testRevText() { + $this->hideDeprecated( 'Revision::revText' ); + $orig = $this->makeRevision( array( 'text' => 'hello hello rev.' ) ); + $rev = Revision::newFromId( $orig->getId() ); + + $this->assertEquals( 'hello hello rev.', $rev->revText() ); + } + + /** + * @covers Revision::getRawText + */ + public function testGetRawText() { + $this->hideDeprecated( 'Revision::getRawText' ); + + $orig = $this->makeRevision( array( 'text' => 'hello hello raw.' ) ); + $rev = Revision::newFromId( $orig->getId() ); + + $this->assertEquals( 'hello hello raw.', $rev->getRawText() ); + } + + /** + * @covers Revision::getContentModel + */ + public function testGetContentModel() { + global $wgContentHandlerUseDB; + + if ( !$wgContentHandlerUseDB ) { + $this->markTestSkipped( '$wgContentHandlerUseDB is disabled' ); + } + + $orig = $this->makeRevision( array( 'text' => 'hello hello.', + 'content_model' => CONTENT_MODEL_JAVASCRIPT ) ); + $rev = Revision::newFromId( $orig->getId() ); + + $this->assertEquals( CONTENT_MODEL_JAVASCRIPT, $rev->getContentModel() ); + } + + /** + * @covers Revision::getContentFormat + */ + public function testGetContentFormat() { + global $wgContentHandlerUseDB; + + if ( !$wgContentHandlerUseDB ) { + $this->markTestSkipped( '$wgContentHandlerUseDB is disabled' ); + } + + $orig = $this->makeRevision( array( + 'text' => 'hello hello.', + 'content_model' => CONTENT_MODEL_JAVASCRIPT, + 'content_format' => CONTENT_FORMAT_JAVASCRIPT + ) ); + $rev = Revision::newFromId( $orig->getId() ); + + $this->assertEquals( CONTENT_FORMAT_JAVASCRIPT, $rev->getContentFormat() ); + } + + /** + * @covers Revision::isCurrent + */ + public function testIsCurrent() { + $page = $this->createPage( 'RevisionStorageTest_testIsCurrent', 'Lorem Ipsum', CONTENT_MODEL_WIKITEXT ); + $rev1 = $page->getRevision(); + + # @todo find out if this should be true + # $this->assertTrue( $rev1->isCurrent() ); + + $rev1x = Revision::newFromId( $rev1->getId() ); + $this->assertTrue( $rev1x->isCurrent() ); + + $page->doEditContent( ContentHandler::makeContent( 'Bla bla', $page->getTitle(), CONTENT_MODEL_WIKITEXT ), 'second rev' ); + $rev2 = $page->getRevision(); + + # @todo find out if this should be true + # $this->assertTrue( $rev2->isCurrent() ); + + $rev1x = Revision::newFromId( $rev1->getId() ); + $this->assertFalse( $rev1x->isCurrent() ); + + $rev2x = Revision::newFromId( $rev2->getId() ); + $this->assertTrue( $rev2x->isCurrent() ); + } + + /** + * @covers Revision::getPrevious + */ + public function testGetPrevious() { + $page = $this->createPage( 'RevisionStorageTest_testGetPrevious', 'Lorem Ipsum testGetPrevious', CONTENT_MODEL_WIKITEXT ); + $rev1 = $page->getRevision(); + + $this->assertNull( $rev1->getPrevious() ); + + $page->doEditContent( ContentHandler::makeContent( 'Bla bla', $page->getTitle(), CONTENT_MODEL_WIKITEXT ), + 'second rev testGetPrevious' ); + $rev2 = $page->getRevision(); + + $this->assertNotNull( $rev2->getPrevious() ); + $this->assertEquals( $rev1->getId(), $rev2->getPrevious()->getId() ); + } + + /** + * @covers Revision::getNext + */ + public function testGetNext() { + $page = $this->createPage( 'RevisionStorageTest_testGetNext', 'Lorem Ipsum testGetNext', CONTENT_MODEL_WIKITEXT ); + $rev1 = $page->getRevision(); + + $this->assertNull( $rev1->getNext() ); + + $page->doEditContent( ContentHandler::makeContent( 'Bla bla', $page->getTitle(), CONTENT_MODEL_WIKITEXT ), + 'second rev testGetNext' ); + $rev2 = $page->getRevision(); + + $this->assertNotNull( $rev1->getNext() ); + $this->assertEquals( $rev2->getId(), $rev1->getNext()->getId() ); + } + + /** + * @covers Revision::newNullRevision + */ + public function testNewNullRevision() { + $page = $this->createPage( 'RevisionStorageTest_testNewNullRevision', 'some testing text', CONTENT_MODEL_WIKITEXT ); + $orig = $page->getRevision(); + + $dbw = wfGetDB( DB_MASTER ); + $rev = Revision::newNullRevision( $dbw, $page->getId(), 'a null revision', false ); + + $this->assertNotEquals( $orig->getId(), $rev->getId(), + 'new null revision shold have a different id from the original revision' ); + $this->assertEquals( $orig->getTextId(), $rev->getTextId(), + 'new null revision shold have the same text id as the original revision' ); + $this->assertEquals( 'some testing text', $rev->getContent()->getNativeData() ); + } + + public static function provideUserWasLastToEdit() { + return array( + array( #0 + 3, true, # actually the last edit + ), + array( #1 + 2, true, # not the current edit, but still by this user + ), + array( #2 + 1, false, # edit by another user + ), + array( #3 + 0, false, # first edit, by this user, but another user edited in the mean time + ), + ); + } + + /** + * @dataProvider provideUserWasLastToEdit + */ + public function testUserWasLastToEdit( $sinceIdx, $expectedLast ) { + $userA = User::newFromName( "RevisionStorageTest_userA" ); + $userB = User::newFromName( "RevisionStorageTest_userB" ); + + if ( $userA->getId() === 0 ) { + $userA = User::createNew( $userA->getName() ); + } + + if ( $userB->getId() === 0 ) { + $userB = User::createNew( $userB->getName() ); + } + + $ns = $this->getDefaultWikitextNS(); + + $dbw = wfGetDB( DB_MASTER ); + $revisions = array(); + + // create revisions ----------------------------- + $page = WikiPage::factory( Title::newFromText( + 'RevisionStorageTest_testUserWasLastToEdit', $ns ) ); + + # zero + $revisions[0] = new Revision( array( + 'page' => $page->getId(), + 'title' => $page->getTitle(), // we need the title to determine the page's default content model + 'timestamp' => '20120101000000', + 'user' => $userA->getId(), + 'text' => 'zero', + 'content_model' => CONTENT_MODEL_WIKITEXT, + 'summary' => 'edit zero' + ) ); + $revisions[0]->insertOn( $dbw ); + + # one + $revisions[1] = new Revision( array( + 'page' => $page->getId(), + 'title' => $page->getTitle(), // still need the title, because $page->getId() is 0 (there's no entry in the page table) + 'timestamp' => '20120101000100', + 'user' => $userA->getId(), + 'text' => 'one', + 'content_model' => CONTENT_MODEL_WIKITEXT, + 'summary' => 'edit one' + ) ); + $revisions[1]->insertOn( $dbw ); + + # two + $revisions[2] = new Revision( array( + 'page' => $page->getId(), + 'title' => $page->getTitle(), + 'timestamp' => '20120101000200', + 'user' => $userB->getId(), + 'text' => 'two', + 'content_model' => CONTENT_MODEL_WIKITEXT, + 'summary' => 'edit two' + ) ); + $revisions[2]->insertOn( $dbw ); + + # three + $revisions[3] = new Revision( array( + 'page' => $page->getId(), + 'title' => $page->getTitle(), + 'timestamp' => '20120101000300', + 'user' => $userA->getId(), + 'text' => 'three', + 'content_model' => CONTENT_MODEL_WIKITEXT, + 'summary' => 'edit three' + ) ); + $revisions[3]->insertOn( $dbw ); + + # four + $revisions[4] = new Revision( array( + 'page' => $page->getId(), + 'title' => $page->getTitle(), + 'timestamp' => '20120101000200', + 'user' => $userA->getId(), + 'text' => 'zero', + 'content_model' => CONTENT_MODEL_WIKITEXT, + 'summary' => 'edit four' + ) ); + $revisions[4]->insertOn( $dbw ); + + // test it --------------------------------- + $since = $revisions[$sinceIdx]->getTimestamp(); + + $wasLast = Revision::userWasLastToEdit( $dbw, $page->getId(), $userA->getId(), $since ); + + $this->assertEquals( $expectedLast, $wasLast ); + } +} diff --git a/tests/phpunit/includes/RevisionStorageTest_ContentHandlerUseDB.php b/tests/phpunit/includes/RevisionStorageTest_ContentHandlerUseDB.php new file mode 100644 index 00000000..4e83e355 --- /dev/null +++ b/tests/phpunit/includes/RevisionStorageTest_ContentHandlerUseDB.php @@ -0,0 +1,81 @@ +setMwGlobals( 'wgContentHandlerUseDB', false ); + + $dbw = wfGetDB( DB_MASTER ); + + $page_table = $dbw->tableName( 'page' ); + $revision_table = $dbw->tableName( 'revision' ); + $archive_table = $dbw->tableName( 'archive' ); + + if ( $dbw->fieldExists( $page_table, 'page_content_model' ) ) { + $dbw->query( "alter table $page_table drop column page_content_model" ); + $dbw->query( "alter table $revision_table drop column rev_content_model" ); + $dbw->query( "alter table $revision_table drop column rev_content_format" ); + $dbw->query( "alter table $archive_table drop column ar_content_model" ); + $dbw->query( "alter table $archive_table drop column ar_content_format" ); + } + + parent::setUp(); + } + + /** + * @covers Revision::selectFields + */ + public function testSelectFields() { + $fields = Revision::selectFields(); + + $this->assertTrue( in_array( 'rev_id', $fields ), 'missing rev_id in list of fields' ); + $this->assertTrue( in_array( 'rev_page', $fields ), 'missing rev_page in list of fields' ); + $this->assertTrue( in_array( 'rev_timestamp', $fields ), 'missing rev_timestamp in list of fields' ); + $this->assertTrue( in_array( 'rev_user', $fields ), 'missing rev_user in list of fields' ); + + $this->assertFalse( in_array( 'rev_content_model', $fields ), 'missing rev_content_model in list of fields' ); + $this->assertFalse( in_array( 'rev_content_format', $fields ), 'missing rev_content_format in list of fields' ); + } + + /** + * @covers Revision::getContentModel + */ + public function testGetContentModel() { + try { + $this->makeRevision( array( 'text' => 'hello hello.', + 'content_model' => CONTENT_MODEL_JAVASCRIPT ) ); + + $this->fail( "Creating JavaScript content on a wikitext page should fail with " + . "\$wgContentHandlerUseDB disabled" ); + } catch ( MWException $ex ) { + $this->assertTrue( true ); // ok + } + } + + + /** + * @covers Revision::getContentFormat + */ + public function testGetContentFormat() { + try { + // @todo change this to test failure on using a non-standard (but supported) format + // for a content model supported in the given location. As of 1.21, there are + // no alternative formats for any of the standard content models that could be + // used for this though. + + $this->makeRevision( array( 'text' => 'hello hello.', + 'content_model' => CONTENT_MODEL_JAVASCRIPT, + 'content_format' => 'text/javascript' ) ); + + $this->fail( "Creating JavaScript content on a wikitext page should fail with " + . "\$wgContentHandlerUseDB disabled" ); + } catch ( MWException $ex ) { + $this->assertTrue( true ); // ok + } + } +} diff --git a/tests/phpunit/includes/RevisionTest.php b/tests/phpunit/includes/RevisionTest.php new file mode 100644 index 00000000..b5819ff6 --- /dev/null +++ b/tests/phpunit/includes/RevisionTest.php @@ -0,0 +1,481 @@ +setMwGlobals( array( + 'wgContLang' => Language::factory( 'en' ), + 'wgLanguageCode' => 'en', + 'wgLegacyEncoding' => false, + 'wgCompressRevisions' => false, + + 'wgContentHandlerTextFallback' => 'ignore', + ) ); + + $this->mergeMwGlobalArrayValue( + 'wgExtraNamespaces', + array( + 12312 => 'Dummy', + 12313 => 'Dummy_talk', + ) + ); + + $this->mergeMwGlobalArrayValue( + 'wgNamespaceContentModels', + array( + 12312 => 'testing', + ) + ); + + $this->mergeMwGlobalArrayValue( + 'wgContentHandlers', + array( + 'testing' => 'DummyContentHandlerForTesting', + 'RevisionTestModifyableContent' => 'RevisionTestModifyableContentHandler', + ) + ); + + MWNamespace::getCanonicalNamespaces( true ); # reset namespace cache + $wgContLang->resetNamespaces(); # reset namespace cache + } + + function tearDown() { + global $wgContLang; + + MWNamespace::getCanonicalNamespaces( true ); # reset namespace cache + $wgContLang->resetNamespaces(); # reset namespace cache + + parent::tearDown(); + } + + /** + * @covers Revision::getRevisionText + */ + public function testGetRevisionText() { + $row = new stdClass; + $row->old_flags = ''; + $row->old_text = 'This is a bunch of revision text.'; + $this->assertEquals( + 'This is a bunch of revision text.', + Revision::getRevisionText( $row ) ); + } + + /** + * @covers Revision::getRevisionText + */ + public function testGetRevisionTextGzip() { + $this->checkPHPExtension( 'zlib' ); + + $row = new stdClass; + $row->old_flags = 'gzip'; + $row->old_text = gzdeflate( 'This is a bunch of revision text.' ); + $this->assertEquals( + 'This is a bunch of revision text.', + Revision::getRevisionText( $row ) ); + } + + /** + * @covers Revision::getRevisionText + */ + public function testGetRevisionTextUtf8Native() { + $row = new stdClass; + $row->old_flags = 'utf-8'; + $row->old_text = "Wiki est l'\xc3\xa9cole superieur !"; + $GLOBALS['wgLegacyEncoding'] = 'iso-8859-1'; + $this->assertEquals( + "Wiki est l'\xc3\xa9cole superieur !", + Revision::getRevisionText( $row ) ); + } + + /** + * @covers Revision::getRevisionText + */ + public function testGetRevisionTextUtf8Legacy() { + $row = new stdClass; + $row->old_flags = ''; + $row->old_text = "Wiki est l'\xe9cole superieur !"; + $GLOBALS['wgLegacyEncoding'] = 'iso-8859-1'; + $this->assertEquals( + "Wiki est l'\xc3\xa9cole superieur !", + Revision::getRevisionText( $row ) ); + } + + /** + * @covers Revision::getRevisionText + */ + public function testGetRevisionTextUtf8NativeGzip() { + $this->checkPHPExtension( 'zlib' ); + + $row = new stdClass; + $row->old_flags = 'gzip,utf-8'; + $row->old_text = gzdeflate( "Wiki est l'\xc3\xa9cole superieur !" ); + $GLOBALS['wgLegacyEncoding'] = 'iso-8859-1'; + $this->assertEquals( + "Wiki est l'\xc3\xa9cole superieur !", + Revision::getRevisionText( $row ) ); + } + + /** + * @covers Revision::getRevisionText + */ + public function testGetRevisionTextUtf8LegacyGzip() { + $this->checkPHPExtension( 'zlib' ); + + $row = new stdClass; + $row->old_flags = 'gzip'; + $row->old_text = gzdeflate( "Wiki est l'\xe9cole superieur !" ); + $GLOBALS['wgLegacyEncoding'] = 'iso-8859-1'; + $this->assertEquals( + "Wiki est l'\xc3\xa9cole superieur !", + Revision::getRevisionText( $row ) ); + } + + /** + * @covers Revision::compressRevisionText + */ + public function testCompressRevisionTextUtf8() { + $row = new stdClass; + $row->old_text = "Wiki est l'\xc3\xa9cole superieur !"; + $row->old_flags = Revision::compressRevisionText( $row->old_text ); + $this->assertTrue( false !== strpos( $row->old_flags, 'utf-8' ), + "Flags should contain 'utf-8'" ); + $this->assertFalse( false !== strpos( $row->old_flags, 'gzip' ), + "Flags should not contain 'gzip'" ); + $this->assertEquals( "Wiki est l'\xc3\xa9cole superieur !", + $row->old_text, "Direct check" ); + $this->assertEquals( "Wiki est l'\xc3\xa9cole superieur !", + Revision::getRevisionText( $row ), "getRevisionText" ); + } + + /** + * @covers Revision::compressRevisionText + */ + public function testCompressRevisionTextUtf8Gzip() { + $this->checkPHPExtension( 'zlib' ); + $this->setMwGlobals( 'wgCompressRevisions', true ); + + $row = new stdClass; + $row->old_text = "Wiki est l'\xc3\xa9cole superieur !"; + $row->old_flags = Revision::compressRevisionText( $row->old_text ); + $this->assertTrue( false !== strpos( $row->old_flags, 'utf-8' ), + "Flags should contain 'utf-8'" ); + $this->assertTrue( false !== strpos( $row->old_flags, 'gzip' ), + "Flags should contain 'gzip'" ); + $this->assertEquals( "Wiki est l'\xc3\xa9cole superieur !", + gzinflate( $row->old_text ), "Direct check" ); + $this->assertEquals( "Wiki est l'\xc3\xa9cole superieur !", + Revision::getRevisionText( $row ), "getRevisionText" ); + } + + # ================================================================================================================= + + /** + * @param string $text + * @param string $title + * @param string $model + * @param null $format + * + * @return Revision + */ + function newTestRevision( $text, $title = "Test", $model = CONTENT_MODEL_WIKITEXT, $format = null ) { + if ( is_string( $title ) ) { + $title = Title::newFromText( $title ); + } + + $content = ContentHandler::makeContent( $text, $title, $model, $format ); + + $rev = new Revision( + array( + 'id' => 42, + 'page' => 23, + 'title' => $title, + + 'content' => $content, + 'length' => $content->getSize(), + 'comment' => "testing", + 'minor_edit' => false, + + 'content_format' => $format, + ) + ); + + return $rev; + } + + function dataGetContentModel() { + //NOTE: we expect the help namespace to always contain wikitext + return array( + array( 'hello world', 'Help:Hello', null, null, CONTENT_MODEL_WIKITEXT ), + array( 'hello world', 'User:hello/there.css', null, null, CONTENT_MODEL_CSS ), + array( serialize( 'hello world' ), 'Dummy:Hello', null, null, "testing" ), + ); + } + + /** + * @group Database + * @dataProvider dataGetContentModel + * @covers Revision::getContentModel + */ + public function testGetContentModel( $text, $title, $model, $format, $expectedModel ) { + $rev = $this->newTestRevision( $text, $title, $model, $format ); + + $this->assertEquals( $expectedModel, $rev->getContentModel() ); + } + + function dataGetContentFormat() { + //NOTE: we expect the help namespace to always contain wikitext + return array( + array( 'hello world', 'Help:Hello', null, null, CONTENT_FORMAT_WIKITEXT ), + array( 'hello world', 'Help:Hello', CONTENT_MODEL_CSS, null, CONTENT_FORMAT_CSS ), + array( 'hello world', 'User:hello/there.css', null, null, CONTENT_FORMAT_CSS ), + array( serialize( 'hello world' ), 'Dummy:Hello', null, null, "testing" ), + ); + } + + /** + * @group Database + * @dataProvider dataGetContentFormat + * @covers Revision::getContentFormat + */ + public function testGetContentFormat( $text, $title, $model, $format, $expectedFormat ) { + $rev = $this->newTestRevision( $text, $title, $model, $format ); + + $this->assertEquals( $expectedFormat, $rev->getContentFormat() ); + } + + function dataGetContentHandler() { + //NOTE: we expect the help namespace to always contain wikitext + return array( + array( 'hello world', 'Help:Hello', null, null, 'WikitextContentHandler' ), + array( 'hello world', 'User:hello/there.css', null, null, 'CssContentHandler' ), + array( serialize( 'hello world' ), 'Dummy:Hello', null, null, 'DummyContentHandlerForTesting' ), + ); + } + + /** + * @group Database + * @dataProvider dataGetContentHandler + * @covers Revision::getContentHandler + */ + public function testGetContentHandler( $text, $title, $model, $format, $expectedClass ) { + $rev = $this->newTestRevision( $text, $title, $model, $format ); + + $this->assertEquals( $expectedClass, get_class( $rev->getContentHandler() ) ); + } + + function dataGetContent() { + //NOTE: we expect the help namespace to always contain wikitext + return array( + array( 'hello world', 'Help:Hello', null, null, Revision::FOR_PUBLIC, 'hello world' ), + array( serialize( 'hello world' ), 'Hello', "testing", null, Revision::FOR_PUBLIC, serialize( 'hello world' ) ), + array( serialize( 'hello world' ), 'Dummy:Hello', null, null, Revision::FOR_PUBLIC, serialize( 'hello world' ) ), + ); + } + + /** + * @group Database + * @dataProvider dataGetContent + * @covers Revision::getContent + */ + public function testGetContent( $text, $title, $model, $format, $audience, $expectedSerialization ) { + $rev = $this->newTestRevision( $text, $title, $model, $format ); + $content = $rev->getContent( $audience ); + + $this->assertEquals( $expectedSerialization, is_null( $content ) ? null : $content->serialize( $format ) ); + } + + function dataGetText() { + //NOTE: we expect the help namespace to always contain wikitext + return array( + array( 'hello world', 'Help:Hello', null, null, Revision::FOR_PUBLIC, 'hello world' ), + array( serialize( 'hello world' ), 'Hello', "testing", null, Revision::FOR_PUBLIC, null ), + array( serialize( 'hello world' ), 'Dummy:Hello', null, null, Revision::FOR_PUBLIC, null ), + ); + } + + /** + * @group Database + * @dataProvider dataGetText + * @covers Revision::getText + */ + public function testGetText( $text, $title, $model, $format, $audience, $expectedText ) { + $this->hideDeprecated( 'Revision::getText' ); + + $rev = $this->newTestRevision( $text, $title, $model, $format ); + + $this->assertEquals( $expectedText, $rev->getText( $audience ) ); + } + + /** + * @group Database + * @dataProvider dataGetText + * @covers Revision::getRawText + */ + public function testGetRawText( $text, $title, $model, $format, $audience, $expectedText ) { + $this->hideDeprecated( 'Revision::getRawText' ); + + $rev = $this->newTestRevision( $text, $title, $model, $format ); + + $this->assertEquals( $expectedText, $rev->getRawText( $audience ) ); + } + + + public function dataGetSize() { + return array( + array( "hello world.", CONTENT_MODEL_WIKITEXT, 12 ), + array( serialize( "hello world." ), "testing", 12 ), + ); + } + + /** + * @covers Revision::getSize + * @group Database + * @dataProvider dataGetSize + */ + public function testGetSize( $text, $model, $expected_size ) { + $rev = $this->newTestRevision( $text, 'RevisionTest_testGetSize', $model ); + $this->assertEquals( $expected_size, $rev->getSize() ); + } + + public function dataGetSha1() { + return array( + array( "hello world.", CONTENT_MODEL_WIKITEXT, Revision::base36Sha1( "hello world." ) ), + array( serialize( "hello world." ), "testing", Revision::base36Sha1( serialize( "hello world." ) ) ), + ); + } + + /** + * @covers Revision::getSha1 + * @group Database + * @dataProvider dataGetSha1 + */ + public function testGetSha1( $text, $model, $expected_hash ) { + $rev = $this->newTestRevision( $text, 'RevisionTest_testGetSha1', $model ); + $this->assertEquals( $expected_hash, $rev->getSha1() ); + } + + /** + * @covers Revision::__construct + */ + public function testConstructWithText() { + $this->hideDeprecated( "Revision::getText" ); + + $rev = new Revision( array( + 'text' => 'hello world.', + 'content_model' => CONTENT_MODEL_JAVASCRIPT + ) ); + + $this->assertNotNull( $rev->getText(), 'no content text' ); + $this->assertNotNull( $rev->getContent(), 'no content object available' ); + $this->assertEquals( CONTENT_MODEL_JAVASCRIPT, $rev->getContent()->getModel() ); + $this->assertEquals( CONTENT_MODEL_JAVASCRIPT, $rev->getContentModel() ); + } + + /** + * @covers Revision::__construct + */ + public function testConstructWithContent() { + $this->hideDeprecated( "Revision::getText" ); + + $title = Title::newFromText( 'RevisionTest_testConstructWithContent' ); + + $rev = new Revision( array( + 'content' => ContentHandler::makeContent( 'hello world.', $title, CONTENT_MODEL_JAVASCRIPT ), + ) ); + + $this->assertNotNull( $rev->getText(), 'no content text' ); + $this->assertNotNull( $rev->getContent(), 'no content object available' ); + $this->assertEquals( CONTENT_MODEL_JAVASCRIPT, $rev->getContent()->getModel() ); + $this->assertEquals( CONTENT_MODEL_JAVASCRIPT, $rev->getContentModel() ); + } + + /** + * Tests whether $rev->getContent() returns a clone when needed. + * + * @group Database + * @covers Revision::getContent + */ + public function testGetContentClone() { + $content = new RevisionTestModifyableContent( "foo" ); + + $rev = new Revision( + array( + 'id' => 42, + 'page' => 23, + 'title' => Title::newFromText( "testGetContentClone_dummy" ), + + 'content' => $content, + 'length' => $content->getSize(), + 'comment' => "testing", + 'minor_edit' => false, + ) + ); + + $content = $rev->getContent( Revision::RAW ); + $content->setText( "bar" ); + + $content2 = $rev->getContent( Revision::RAW ); + $this->assertNotSame( $content, $content2, "expected a clone" ); // content is mutable, expect clone + $this->assertEquals( "foo", $content2->getText() ); // clone should contain the original text + + $content2->setText( "bla bla" ); + $this->assertEquals( "bar", $content->getText() ); // clones should be independent + } + + + /** + * Tests whether $rev->getContent() returns the same object repeatedly if appropriate. + * + * @group Database + * @covers Revision::getContent + */ + public function testGetContentUncloned() { + $rev = $this->newTestRevision( "hello", "testGetContentUncloned_dummy", CONTENT_MODEL_WIKITEXT ); + $content = $rev->getContent( Revision::RAW ); + $content2 = $rev->getContent( Revision::RAW ); + + // for immutable content like wikitext, this should be the same object + $this->assertSame( $content, $content2 ); + } +} + +class RevisionTestModifyableContent extends TextContent { + public function __construct( $text ) { + parent::__construct( $text, "RevisionTestModifyableContent" ); + } + + public function copy() { + return new RevisionTestModifyableContent( $this->mText ); + } + + public function getText() { + return $this->mText; + } + + public function setText( $text ) { + $this->mText = $text; + } +} + +class RevisionTestModifyableContentHandler extends TextContentHandler { + + public function __construct() { + parent::__construct( "RevisionTestModifyableContent", array( CONTENT_FORMAT_TEXT ) ); + } + + public function unserializeContent( $text, $format = null ) { + $this->checkFormat( $format ); + + return new RevisionTestModifyableContent( $text ); + } + + public function makeEmptyContent() { + return new RevisionTestModifyableContent( '' ); + } +} diff --git a/tests/phpunit/includes/SampleTest.php b/tests/phpunit/includes/SampleTest.php new file mode 100644 index 00000000..8516a4ce --- /dev/null +++ b/tests/phpunit/includes/SampleTest.php @@ -0,0 +1,105 @@ +setMwGlobals( array( + 'wgContLang' => Language::factory( 'en' ), + 'wgLanguageCode' => 'en', + ) ); + } + + /** + * Anything cleanup you need to do should go here. + */ + protected function tearDown() { + parent::tearDown(); + } + + /** + * Name tests so that PHPUnit can turn them into sentences when + * they run. While MediaWiki isn't strictly an Agile Programming + * project, you are encouraged to use the naming described under + * "Agile Documentation" at + * http://www.phpunit.de/manual/3.4/en/other-uses-for-tests.html + */ + public function testTitleObjectStringConversion() { + $title = Title::newFromText( "text" ); + $this->assertInstanceOf( 'Title', $title, "Title creation" ); + $this->assertEquals( "Text", $title, "Automatic string conversion" ); + + $title = Title::newFromText( "text", NS_MEDIA ); + $this->assertEquals( "Media:Text", $title, "Title creation with namespace" ); + } + + /** + * If you want to run a the same test with a variety of data. use a data provider. + * see: http://www.phpunit.de/manual/3.4/en/writing-tests-for-phpunit.html + * + * Note: Data providers are always called statically and outside setUp/tearDown! + */ + public static function provideTitles() { + return array( + array( 'Text', NS_MEDIA, 'Media:Text' ), + array( 'Text', null, 'Text' ), + array( 'text', null, 'Text' ), + array( 'Text', NS_USER, 'User:Text' ), + array( 'Photo.jpg', NS_FILE, 'File:Photo.jpg' ) + ); + } + + /** + * @dataProvider provideTitles + * See http://www.phpunit.de/manual/3.4/en/appendixes.annotations.html#appendixes.annotations.dataProvider + */ + public function testCreateBasicListOfTitles( $titleName, $ns, $text ) { + $title = Title::newFromText( $titleName, $ns ); + $this->assertEquals( $text, "$title", "see if '$titleName' matches '$text'" ); + } + + public function testSetUpMainPageTitleForNextTest() { + $title = Title::newMainPage(); + $this->assertEquals( "Main Page", "$title", "Test initial creation of a title" ); + + return $title; + } + + /** + * Instead of putting a bunch of tests in a single test method, + * you should put only one or two tests in each test method. This + * way, the test method names can remain descriptive. + * + * If you want to make tests depend on data created in another + * method, you can create dependencies feed whatever you return + * from the dependant method (e.g. testInitialCreation in this + * example) as arguments to the next method (e.g. $title in + * testTitleDepends is whatever testInitialCreatiion returned.) + */ + + /** + * @depends testSetUpMainPageTitleForNextTest + * See http://www.phpunit.de/manual/3.4/en/appendixes.annotations.html#appendixes.annotations.depends + */ + public function testCheckMainPageTitleIsConsideredLocal( $title ) { + $this->assertTrue( $title->isLocal() ); + } + + /** + * @expectedException MWException object + * See http://www.phpunit.de/manual/3.4/en/appendixes.annotations.html#appendixes.annotations.expectedException + */ + public function testTitleObjectFromObject() { + $title = Title::newFromText( Title::newFromText( "test" ) ); + $this->assertEquals( "Test", $title->isLocal() ); + } +} diff --git a/tests/phpunit/includes/SanitizerTest.php b/tests/phpunit/includes/SanitizerTest.php new file mode 100644 index 00000000..81246d33 --- /dev/null +++ b/tests/phpunit/includes/SanitizerTest.php @@ -0,0 +1,302 @@ +assertEquals( + "\xc3\xa9cole", + Sanitizer::decodeCharReferences( 'école' ), + 'decode named entities' + ); + } + + /** + * @covers Sanitizer::decodeCharReferences + */ + public function testDecodeNumericEntities() { + $this->assertEquals( + "\xc4\x88io bonas dans l'\xc3\xa9cole!", + Sanitizer::decodeCharReferences( "Ĉio bonas dans l'école!" ), + 'decode numeric entities' + ); + } + + /** + * @covers Sanitizer::decodeCharReferences + */ + public function testDecodeMixedEntities() { + $this->assertEquals( + "\xc4\x88io bonas dans l'\xc3\xa9cole!", + Sanitizer::decodeCharReferences( "Ĉio bonas dans l'école!" ), + 'decode mixed numeric/named entities' + ); + } + + /** + * @covers Sanitizer::decodeCharReferences + */ + public function testDecodeMixedComplexEntities() { + $this->assertEquals( + "\xc4\x88io bonas dans l'\xc3\xa9cole! (mais pas Ĉio dans l'école)", + Sanitizer::decodeCharReferences( + "Ĉio bonas dans l'école! (mais pas &#x108;io dans l'&eacute;cole)" + ), + 'decode mixed complex entities' + ); + } + + /** + * @covers Sanitizer::decodeCharReferences + */ + public function testInvalidAmpersand() { + $this->assertEquals( + 'a & b', + Sanitizer::decodeCharReferences( 'a & b' ), + 'Invalid ampersand' + ); + } + + /** + * @covers Sanitizer::decodeCharReferences + */ + public function testInvalidEntities() { + $this->assertEquals( + '&foo;', + Sanitizer::decodeCharReferences( '&foo;' ), + 'Invalid named entity' + ); + } + + /** + * @covers Sanitizer::decodeCharReferences + */ + public function testInvalidNumberedEntities() { + $this->assertEquals( UTF8_REPLACEMENT, Sanitizer::decodeCharReferences( "�" ), 'Invalid numbered entity' ); + } + + /** + * @covers Sanitizer::removeHTMLtags + * @dataProvider provideHtml5Tags + * + * @param String $tag Name of an HTML5 element (ie: 'video') + * @param Boolean $escaped Wheter sanitizer let the tag in or escape it (ie: '<video>') + */ + public function testRemovehtmltagsOnHtml5Tags( $tag, $escaped ) { + $this->setMwGlobals( array( + 'wgUseTidy' => false + ) ); + + if ( $escaped ) { + $this->assertEquals( "<$tag>", + Sanitizer::removeHTMLtags( "<$tag>" ) + ); + } else { + $this->assertEquals( "<$tag>\n", + Sanitizer::removeHTMLtags( "<$tag>" ) + ); + } + } + + /** + * Provide HTML5 tags + */ + public static function provideHtml5Tags() { + $ESCAPED = true; # We want tag to be escaped + $VERBATIM = false; # We want to keep the tag + return array( + array( 'data', $VERBATIM ), + array( 'mark', $VERBATIM ), + array( 'time', $VERBATIM ), + array( 'video', $ESCAPED ), + ); + } + + function dataRemoveHTMLtags() { + return array( + // former testSelfClosingTag + array( + '
Hello world
', + '
Hello world
', + 'Self-closing closing div' + ), + // Make sure special nested HTML5 semantics are not broken + // http://www.whatwg.org/html/text-level-semantics.html#the-kbd-element + array( + 'Shift+F3', + 'Shift+F3', + 'Nested .' + ), + // http://www.whatwg.org/html/text-level-semantics.html#the-sub-and-sup-elements + array( + 'xi, yi', + 'xi, yi', + 'Nested .' + ), + // http://www.whatwg.org/html/text-level-semantics.html#the-dfn-element + array( + 'GDO', + 'GDO', + ' inside ', + ), + ); + } + + /** + * @dataProvider dataRemoveHTMLtags + * @covers Sanitizer::removeHTMLtags + */ + public function testRemoveHTMLtags( $input, $output, $msg = null ) { + $GLOBALS['wgUseTidy'] = false; + $this->assertEquals( $output, Sanitizer::removeHTMLtags( $input ), $msg ); + } + + /** + * @dataProvider provideTagAttributesToDecode + * @covers Sanitizer::decodeTagAttributes + */ + public function testDecodeTagAttributes( $expected, $attributes, $message = '' ) { + $this->assertEquals( $expected, + Sanitizer::decodeTagAttributes( $attributes ), + $message + ); + } + + public static function provideTagAttributesToDecode() { + return array( + array( array( 'foo' => 'bar' ), 'foo=bar', 'Unquoted attribute' ), + array( array( 'foo' => 'bar' ), ' foo = bar ', 'Spaced attribute' ), + array( array( 'foo' => 'bar' ), 'foo="bar"', 'Double-quoted attribute' ), + array( array( 'foo' => 'bar' ), 'foo=\'bar\'', 'Single-quoted attribute' ), + array( array( 'foo' => 'bar', 'baz' => 'foo' ), 'foo=\'bar\' baz="foo"', 'Several attributes' ), + array( array( 'foo' => 'bar', 'baz' => 'foo' ), 'foo=\'bar\' baz="foo"', 'Several attributes' ), + array( array( 'foo' => 'bar', 'baz' => 'foo' ), 'foo=\'bar\' baz="foo"', 'Several attributes' ), + array( array( ':foo' => 'bar' ), ':foo=\'bar\'', 'Leading :' ), + array( array( '_foo' => 'bar' ), '_foo=\'bar\'', 'Leading _' ), + array( array( 'foo' => 'bar' ), 'Foo=\'bar\'', 'Leading capital' ), + array( array( 'foo' => 'BAR' ), 'FOO=BAR', 'Attribute keys are normalized to lowercase' ), + + # Invalid beginning + array( array(), '-foo=bar', 'Leading - is forbidden' ), + array( array(), '.foo=bar', 'Leading . is forbidden' ), + array( array( 'foo-bar' => 'bar' ), 'foo-bar=bar', 'A - is allowed inside the attribute' ), + array( array( 'foo-' => 'bar' ), 'foo-=bar', 'A - is allowed inside the attribute' ), + array( array( 'foo.bar' => 'baz' ), 'foo.bar=baz', 'A . is allowed inside the attribute' ), + array( array( 'foo.' => 'baz' ), 'foo.=baz', 'A . is allowed as last character' ), + array( array( 'foo6' => 'baz' ), 'foo6=baz', 'Numbers are allowed' ), + + # This bit is more relaxed than XML rules, but some extensions use + # it, like ProofreadPage (see bug 27539) + array( array( '1foo' => 'baz' ), '1foo=baz', 'Leading numbers are allowed' ), + array( array(), 'foo$=baz', 'Symbols are not allowed' ), + array( array(), 'foo@=baz', 'Symbols are not allowed' ), + array( array(), 'foo~=baz', 'Symbols are not allowed' ), + array( array( 'foo' => '1[#^`*%w/(' ), 'foo=1[#^`*%w/(', 'All kind of characters are allowed as values' ), + array( array( 'foo' => '1[#^`*%\'w/(' ), 'foo="1[#^`*%\'w/("', 'Double quotes are allowed if quoted by single quotes' ), + array( array( 'foo' => '1[#^`*%"w/(' ), 'foo=\'1[#^`*%"w/(\'', 'Single quotes are allowed if quoted by double quotes' ), + array( array( 'foo' => '&"' ), 'foo=&"', 'Special chars can be provided as entities' ), + array( array( 'foo' => '&foobar;' ), 'foo=&foobar;', 'Entity-like items are accepted' ), + ); + } + + /** + * @dataProvider provideDeprecatedAttributes + * @covers Sanitizer::fixTagAttributes + */ + public function testDeprecatedAttributesUnaltered( $inputAttr, $inputEl, $message = '' ) { + $this->assertEquals( " $inputAttr", + Sanitizer::fixTagAttributes( $inputAttr, $inputEl ), + $message + ); + } + + public static function provideDeprecatedAttributes() { + /** array( , , [message] ) */ + return array( + array( 'clear="left"', 'br' ), + array( 'clear="all"', 'br' ), + array( 'width="100"', 'td' ), + array( 'nowrap="true"', 'td' ), + array( 'nowrap=""', 'td' ), + array( 'align="right"', 'td' ), + array( 'align="center"', 'table' ), + array( 'align="left"', 'tr' ), + array( 'align="center"', 'div' ), + array( 'align="left"', 'h1' ), + array( 'align="left"', 'span' ), + ); + } + + /** + * @dataProvider provideCssCommentsFixtures + * @covers Sanitizer::checkCss + */ + public function testCssCommentsChecking( $expected, $css, $message = '' ) { + $this->assertEquals( $expected, + Sanitizer::checkCss( $css ), + $message + ); + } + + public static function provideCssCommentsFixtures() { + /** array( , , [message] ) */ + return array( + // Valid comments spanning entire input + array( '/**/', '/**/' ), + array( '/* comment */', '/* comment */' ), + // Weird stuff + array( ' ', '/****/' ), + array( ' ', '/* /* */' ), + array( 'display: block;', "display:/* foo */block;" ), + array( 'display: block;', "display:\\2f\\2a foo \\2a\\2f block;", + 'Backslash-escaped comments must be stripped (bug 28450)' ), + array( '', '/* unfinished comment structure', + 'Remove anything after a comment-start token' ), + array( '', "\\2f\\2a unifinished comment'", + 'Remove anything after a backslash-escaped comment-start token' ), + array( '/* insecure input */', 'filter: progid:DXImageTransform.Microsoft.AlphaImageLoader(src=\'asdf.png\',sizingMethod=\'scale\');' ), + array( '/* insecure input */', '-ms-filter: "progid:DXImageTransform.Microsoft.AlphaImageLoader(src=\'asdf.png\',sizingMethod=\'scale\')";' ), + array( '/* insecure input */', 'width: expression(1+1);' ), + array( '/* insecure input */', 'background-image: image(asdf.png);' ), + array( '/* insecure input */', 'background-image: -webkit-image(asdf.png);' ), + array( '/* insecure input */', 'background-image: -moz-image(asdf.png);' ), + array( '/* insecure input */', 'background-image: image-set("asdf.png" 1x, "asdf.png" 2x);' ), + array( '/* insecure input */', 'background-image: -webkit-image-set("asdf.png" 1x, "asdf.png" 2x);' ), + array( '/* insecure input */', 'background-image: -moz-image-set("asdf.png" 1x, "asdf.png" 2x);' ), + ); + } + + /** + * Test for support or lack of support for specific attributes in the attribute whitelist. + */ + public static function provideAttributeSupport() { + /** array( , , ) */ + return array( + array( 'div', ' role="presentation"', ' role="presentation"', 'Support for WAI-ARIA\'s role="presentation".' ), + array( 'div', ' role="main"', '', "Other WAI-ARIA roles are currently not supported." ), + ); + } + + /** + * @dataProvider provideAttributeSupport + * @covers Sanitizer::fixTagAttributes + */ + public function testAttributeSupport( $tag, $attributes, $expected, $message ) { + $this->assertEquals( $expected, + Sanitizer::fixTagAttributes( $attributes, $tag ), + $message + ); + } +} diff --git a/tests/phpunit/includes/SanitizerValidateEmailTest.php b/tests/phpunit/includes/SanitizerValidateEmailTest.php new file mode 100644 index 00000000..f13e8382 --- /dev/null +++ b/tests/phpunit/includes/SanitizerValidateEmailTest.php @@ -0,0 +1,103 @@ +assertEquals( + $expected, + Sanitizer::validateEmail( $addr ), + $msg + ); + } + + private function valid( $addr, $msg = '' ) { + $this->checkEmail( $addr, true, $msg ); + } + + private function invalid( $addr, $msg = '' ) { + $this->checkEmail( $addr, false, $msg ); + } + + public function testEmailWellKnownUserAtHostDotTldAreValid() { + $this->valid( 'user@example.com' ); + $this->valid( 'user@example.museum' ); + } + + public function testEmailWithUpperCaseCharactersAreValid() { + $this->valid( 'USER@example.com' ); + $this->valid( 'user@EXAMPLE.COM' ); + $this->valid( 'user@Example.com' ); + $this->valid( 'USER@eXAMPLE.com' ); + } + + public function testEmailWithAPlusInUserName() { + $this->valid( 'user+sub@example.com' ); + $this->valid( 'user+@example.com' ); + } + + public function testEmailDoesNotNeedATopLevelDomain() { + $this->valid( "user@localhost" ); + $this->valid( "FooBar@localdomain" ); + $this->valid( "nobody@mycompany" ); + } + + public function testEmailWithWhiteSpacesBeforeOrAfterAreInvalids() { + $this->invalid( " user@host.com" ); + $this->invalid( "user@host.com " ); + $this->invalid( "\tuser@host.com" ); + $this->invalid( "user@host.com\t" ); + } + + public function testEmailWithWhiteSpacesAreInvalids() { + $this->invalid( "User user@host" ); + $this->invalid( "first last@mycompany" ); + $this->invalid( "firstlast@my company" ); + } + + /** + * bug 26948 : comma were matched by an incorrect regexp range + */ + public function testEmailWithCommasAreInvalids() { + $this->invalid( "user,foo@example.org" ); + $this->invalid( "userfoo@ex,ample.org" ); + } + + public function testEmailWithHyphens() { + $this->valid( "user-foo@example.org" ); + $this->valid( "userfoo@ex-ample.org" ); + } + + public function testEmailDomainCanNotBeginWithDot() { + $this->invalid( "user@." ); + $this->invalid( "user@.localdomain" ); + $this->invalid( "user@localdomain." ); + $this->valid( "user.@localdomain" ); + $this->valid( ".@localdomain" ); + $this->invalid( ".@a............" ); + } + + public function testEmailWithFunnyCharacters() { + $this->valid( "\$user!ex{this}@123.com" ); + } + + public function testEmailTopLevelDomainCanBeNumerical() { + $this->valid( "user@example.1234" ); + } + + public function testEmailWithoutAtSignIsInvalid() { + $this->invalid( 'useràexample.com' ); + } + + public function testEmailWithOneCharacterDomainIsValid() { + $this->valid( 'user@a' ); + } +} diff --git a/tests/phpunit/includes/SiteConfigurationTest.php b/tests/phpunit/includes/SiteConfigurationTest.php new file mode 100644 index 00000000..053d8a7d --- /dev/null +++ b/tests/phpunit/includes/SiteConfigurationTest.php @@ -0,0 +1,335 @@ +suffixes as $suffix ) { + if ( substr( $wiki, -strlen( $suffix ) ) == $suffix ) { + $site = $suffix; + $lang = substr( $wiki, 0, -strlen( $suffix ) ); + break; + } + } + + return array( + 'suffix' => $site, + 'lang' => $lang, + 'params' => array( + 'lang' => $lang, + 'site' => $site, + 'wiki' => $wiki, + ), + 'tags' => array( 'tag' ), + ); +} + +class SiteConfigurationTest extends MediaWikiTestCase { + + /** + * @var SiteConfiguration + */ + protected $mConf; + + protected function setUp() { + parent::setUp(); + + $this->mConf = new SiteConfiguration; + + $this->mConf->suffixes = array( 'wikipedia' => 'wiki' ); + $this->mConf->wikis = array( 'enwiki', 'dewiki', 'frwiki' ); + $this->mConf->settings = array( + 'simple' => array( + 'wiki' => 'wiki', + 'tag' => 'tag', + 'enwiki' => 'enwiki', + 'dewiki' => 'dewiki', + 'frwiki' => 'frwiki', + ), + + 'fallback' => array( + 'default' => 'default', + 'wiki' => 'wiki', + 'tag' => 'tag', + ), + + 'params' => array( + 'default' => '$lang $site $wiki', + ), + + '+global' => array( + 'wiki' => array( + 'wiki' => 'wiki', + ), + 'tag' => array( + 'tag' => 'tag', + ), + 'enwiki' => array( + 'enwiki' => 'enwiki', + ), + 'dewiki' => array( + 'dewiki' => 'dewiki', + ), + 'frwiki' => array( + 'frwiki' => 'frwiki', + ), + ), + + 'merge' => array( + '+wiki' => array( + 'wiki' => 'wiki', + ), + '+tag' => array( + 'tag' => 'tag', + ), + 'default' => array( + 'default' => 'default', + ), + '+enwiki' => array( + 'enwiki' => 'enwiki', + ), + '+dewiki' => array( + 'dewiki' => 'dewiki', + ), + '+frwiki' => array( + 'frwiki' => 'frwiki', + ), + ), + ); + + $GLOBALS['global'] = array( 'global' => 'global' ); + } + + /** + * @covers SiteConfiguration::siteFromDB + */ + public function testSiteFromDb() { + $this->assertEquals( + array( 'wikipedia', 'en' ), + $this->mConf->siteFromDB( 'enwiki' ), + 'siteFromDB()' + ); + $this->assertEquals( + array( 'wikipedia', '' ), + $this->mConf->siteFromDB( 'wiki' ), + 'siteFromDB() on a suffix' + ); + $this->assertEquals( + array( null, null ), + $this->mConf->siteFromDB( 'wikien' ), + 'siteFromDB() on a non-existing wiki' + ); + + $this->mConf->suffixes = array( 'wiki', '' ); + $this->assertEquals( + array( '', 'wikien' ), + $this->mConf->siteFromDB( 'wikien' ), + 'siteFromDB() on a non-existing wiki (2)' + ); + } + + /** + * @covers SiteConfiguration::getLocalDatabases + */ + public function testGetLocalDatabases() { + $this->assertEquals( + array( 'enwiki', 'dewiki', 'frwiki' ), + $this->mConf->getLocalDatabases(), + 'getLocalDatabases()' + ); + } + + /** + * @covers SiteConfiguration::get + */ + public function testGetConfVariables() { + $this->assertEquals( + 'enwiki', + $this->mConf->get( 'simple', 'enwiki', 'wiki' ), + 'get(): simple setting on an existing wiki' + ); + $this->assertEquals( + 'dewiki', + $this->mConf->get( 'simple', 'dewiki', 'wiki' ), + 'get(): simple setting on an existing wiki (2)' + ); + $this->assertEquals( + 'frwiki', + $this->mConf->get( 'simple', 'frwiki', 'wiki' ), + 'get(): simple setting on an existing wiki (3)' + ); + $this->assertEquals( + 'wiki', + $this->mConf->get( 'simple', 'wiki', 'wiki' ), + 'get(): simple setting on an suffix' + ); + $this->assertEquals( + 'wiki', + $this->mConf->get( 'simple', 'eswiki', 'wiki' ), + 'get(): simple setting on an non-existing wiki' + ); + + $this->assertEquals( + 'wiki', + $this->mConf->get( 'fallback', 'enwiki', 'wiki' ), + 'get(): fallback setting on an existing wiki' + ); + $this->assertEquals( + 'tag', + $this->mConf->get( 'fallback', 'dewiki', 'wiki', array(), array( 'tag' ) ), + 'get(): fallback setting on an existing wiki (with wiki tag)' + ); + $this->assertEquals( + 'wiki', + $this->mConf->get( 'fallback', 'wiki', 'wiki' ), + 'get(): fallback setting on an suffix' + ); + $this->assertEquals( + 'wiki', + $this->mConf->get( 'fallback', 'wiki', 'wiki', array(), array( 'tag' ) ), + 'get(): fallback setting on an suffix (with wiki tag)' + ); + $this->assertEquals( + 'wiki', + $this->mConf->get( 'fallback', 'eswiki', 'wiki' ), + 'get(): fallback setting on an non-existing wiki' + ); + $this->assertEquals( + 'tag', + $this->mConf->get( 'fallback', 'eswiki', 'wiki', array(), array( 'tag' ) ), + 'get(): fallback setting on an non-existing wiki (with wiki tag)' + ); + + $common = array( 'wiki' => 'wiki', 'default' => 'default' ); + $commonTag = array( 'tag' => 'tag', 'wiki' => 'wiki', 'default' => 'default' ); + $this->assertEquals( + array( 'enwiki' => 'enwiki' ) + $common, + $this->mConf->get( 'merge', 'enwiki', 'wiki' ), + 'get(): merging setting on an existing wiki' + ); + $this->assertEquals( + array( 'enwiki' => 'enwiki' ) + $commonTag, + $this->mConf->get( 'merge', 'enwiki', 'wiki', array(), array( 'tag' ) ), + 'get(): merging setting on an existing wiki (with tag)' + ); + $this->assertEquals( + array( 'dewiki' => 'dewiki' ) + $common, + $this->mConf->get( 'merge', 'dewiki', 'wiki' ), + 'get(): merging setting on an existing wiki (2)' + ); + $this->assertEquals( + array( 'dewiki' => 'dewiki' ) + $commonTag, + $this->mConf->get( 'merge', 'dewiki', 'wiki', array(), array( 'tag' ) ), + 'get(): merging setting on an existing wiki (2) (with tag)' + ); + $this->assertEquals( + array( 'frwiki' => 'frwiki' ) + $common, + $this->mConf->get( 'merge', 'frwiki', 'wiki' ), + 'get(): merging setting on an existing wiki (3)' + ); + $this->assertEquals( + array( 'frwiki' => 'frwiki' ) + $commonTag, + $this->mConf->get( 'merge', 'frwiki', 'wiki', array(), array( 'tag' ) ), + 'get(): merging setting on an existing wiki (3) (with tag)' + ); + $this->assertEquals( + array( 'wiki' => 'wiki' ) + $common, + $this->mConf->get( 'merge', 'wiki', 'wiki' ), + 'get(): merging setting on an suffix' + ); + $this->assertEquals( + array( 'wiki' => 'wiki' ) + $commonTag, + $this->mConf->get( 'merge', 'wiki', 'wiki', array(), array( 'tag' ) ), + 'get(): merging setting on an suffix (with tag)' + ); + $this->assertEquals( + $common, + $this->mConf->get( 'merge', 'eswiki', 'wiki' ), + 'get(): merging setting on an non-existing wiki' + ); + $this->assertEquals( + $commonTag, + $this->mConf->get( 'merge', 'eswiki', 'wiki', array(), array( 'tag' ) ), + 'get(): merging setting on an non-existing wiki (with tag)' + ); + } + + /** + * @covers SiteConfiguration::siteFromDB + */ + public function testSiteFromDbWithCallback() { + $this->mConf->siteParamsCallback = 'getSiteParams'; + + $this->assertEquals( + array( 'wiki', 'en' ), + $this->mConf->siteFromDB( 'enwiki' ), + 'siteFromDB() with callback' + ); + $this->assertEquals( + array( 'wiki', '' ), + $this->mConf->siteFromDB( 'wiki' ), + 'siteFromDB() with callback on a suffix' + ); + $this->assertEquals( + array( null, null ), + $this->mConf->siteFromDB( 'wikien' ), + 'siteFromDB() with callback on a non-existing wiki' + ); + } + + /** + * @covers SiteConfiguration::get + */ + public function testParameterReplacement() { + $this->mConf->siteParamsCallback = 'getSiteParams'; + + $this->assertEquals( + 'en wiki enwiki', + $this->mConf->get( 'params', 'enwiki', 'wiki' ), + 'get(): parameter replacement on an existing wiki' + ); + $this->assertEquals( + 'de wiki dewiki', + $this->mConf->get( 'params', 'dewiki', 'wiki' ), + 'get(): parameter replacement on an existing wiki (2)' + ); + $this->assertEquals( + 'fr wiki frwiki', + $this->mConf->get( 'params', 'frwiki', 'wiki' ), + 'get(): parameter replacement on an existing wiki (3)' + ); + $this->assertEquals( + ' wiki wiki', + $this->mConf->get( 'params', 'wiki', 'wiki' ), + 'get(): parameter replacement on an suffix' + ); + $this->assertEquals( + 'es wiki eswiki', + $this->mConf->get( 'params', 'eswiki', 'wiki' ), + 'get(): parameter replacement on an non-existing wiki' + ); + } + + /** + * @covers SiteConfiguration::getAll + */ + public function testGetAllGlobals() { + $this->mConf->siteParamsCallback = 'getSiteParams'; + + $getall = array( + 'simple' => 'enwiki', + 'fallback' => 'tag', + 'params' => 'en wiki enwiki', + 'global' => array( 'enwiki' => 'enwiki' ) + $GLOBALS['global'], + 'merge' => array( 'enwiki' => 'enwiki', 'tag' => 'tag', 'wiki' => 'wiki', 'default' => 'default' ), + ); + $this->assertEquals( $getall, $this->mConf->getAll( 'enwiki' ), 'getAll()' ); + + $this->mConf->extractAllGlobals( 'enwiki', 'wiki' ); + + $this->assertEquals( $getall['simple'], $GLOBALS['simple'], 'extractAllGlobals(): simple setting' ); + $this->assertEquals( $getall['fallback'], $GLOBALS['fallback'], 'extractAllGlobals(): fallback setting' ); + $this->assertEquals( $getall['params'], $GLOBALS['params'], 'extractAllGlobals(): parameter replacement' ); + $this->assertEquals( $getall['global'], $GLOBALS['global'], 'extractAllGlobals(): merging with global' ); + $this->assertEquals( $getall['merge'], $GLOBALS['merge'], 'extractAllGlobals(): merging setting' ); + } +} diff --git a/tests/phpunit/includes/StringUtilsTest.php b/tests/phpunit/includes/StringUtilsTest.php new file mode 100644 index 00000000..89759e5c --- /dev/null +++ b/tests/phpunit/includes/StringUtilsTest.php @@ -0,0 +1,147 @@ +markTestSkipped( 'Test requires the mbstring PHP extension' ); + } + $this->assertEquals( $expected, + StringUtils::isUtf8( $string ), + 'Testing string "' . $this->escaped( $string ) . '" with mb_check_encoding' + ); + } + + /** + * This tests StringUtils::isUtf8 making sure we use the pure PHP + * implementation used as a fallback when mb_check_encoding() is + * not available. + * + * @covers StringUtils::isUtf8 + * @dataProvider provideStringsForIsUtf8Check + */ + public function testIsUtf8WithPhpFallbackImplementation( $expected, $string ) { + $this->assertEquals( $expected, + StringUtils::isUtf8( $string, /** disable mbstring: */true ), + 'Testing string "' . $this->escaped( $string ) . '" with pure PHP implementation' + ); + } + + /** + * Print high range characters as an hexadecimal + */ + function escaped( $string ) { + $escaped = ''; + $length = strlen( $string ); + for ( $i = 0; $i < $length; $i++ ) { + $char = $string[$i]; + $val = ord( $char ); + if ( $val > 127 ) { + $escaped .= '\x' . dechex( $val ); + } else { + $escaped .= $char; + } + } + + return $escaped; + } + + /** + * See also "UTF-8 decoder capability and stress test" by + * Markus Kuhn: + * http://www.cl.cam.ac.uk/~mgk25/ucs/examples/UTF-8-test.txt + */ + public static function provideStringsForIsUtf8Check() { + // Expected return values for StringUtils::isUtf8() + $PASS = true; + $FAIL = false; + + return array( + 'some ASCII' => array( $PASS, 'Some ASCII' ), + 'euro sign' => array( $PASS, "Euro sign €" ), + + 'first possible sequence 1 byte' => array( $PASS, "\x00" ), + 'first possible sequence 2 bytes' => array( $PASS, "\xc2\x80" ), + 'first possible sequence 3 bytes' => array( $PASS, "\xe0\xa0\x80" ), + 'first possible sequence 4 bytes' => array( $PASS, "\xf0\x90\x80\x80" ), + 'first possible sequence 5 bytes' => array( $FAIL, "\xf8\x88\x80\x80\x80" ), + 'first possible sequence 6 bytes' => array( $FAIL, "\xfc\x84\x80\x80\x80\x80" ), + + 'last possible sequence 1 byte' => array( $PASS, "\x7f" ), + 'last possible sequence 2 bytes' => array( $PASS, "\xdf\xbf" ), + 'last possible sequence 3 bytes' => array( $PASS, "\xef\xbf\xbf" ), + 'last possible sequence 4 bytes (U+1FFFFF)' => array( $FAIL, "\xf7\xbf\xbf\xbf" ), + 'last possible sequence 5 bytes' => array( $FAIL, "\xfb\xbf\xbf\xbf\xbf" ), + 'last possible sequence 6 bytes' => array( $FAIL, "\xfd\xbf\xbf\xbf\xbf\xbf" ), + + 'boundary 1' => array( $PASS, "\xed\x9f\xbf" ), + 'boundary 2' => array( $PASS, "\xee\x80\x80" ), + 'boundary 3' => array( $PASS, "\xef\xbf\xbd" ), + 'boundary 4' => array( $PASS, "\xf2\x80\x80\x80" ), + 'boundary 5 (U+FFFFF)' => array( $PASS, "\xf3\xbf\xbf\xbf" ), + 'boundary 6 (U+100000)' => array( $PASS, "\xf4\x80\x80\x80" ), + 'boundary 7 (U+10FFFF)' => array( $PASS, "\xf4\x8f\xbf\xbf" ), + 'boundary 8 (U+110000)' => array( $FAIL, "\xf4\x90\x80\x80" ), + + 'malformed 1' => array( $FAIL, "\x80" ), + 'malformed 2' => array( $FAIL, "\xbf" ), + 'malformed 3' => array( $FAIL, "\x80\xbf" ), + 'malformed 4' => array( $FAIL, "\x80\xbf\x80" ), + 'malformed 5' => array( $FAIL, "\x80\xbf\x80\xbf" ), + 'malformed 6' => array( $FAIL, "\x80\xbf\x80\xbf\x80" ), + 'malformed 7' => array( $FAIL, "\x80\xbf\x80\xbf\x80\xbf" ), + 'malformed 8' => array( $FAIL, "\x80\xbf\x80\xbf\x80\xbf\x80" ), + + 'last byte missing 1' => array( $FAIL, "\xc0" ), + 'last byte missing 2' => array( $FAIL, "\xe0\x80" ), + 'last byte missing 3' => array( $FAIL, "\xf0\x80\x80" ), + 'last byte missing 4' => array( $FAIL, "\xf8\x80\x80\x80" ), + 'last byte missing 5' => array( $FAIL, "\xfc\x80\x80\x80\x80" ), + 'last byte missing 6' => array( $FAIL, "\xdf" ), + 'last byte missing 7' => array( $FAIL, "\xef\xbf" ), + 'last byte missing 8' => array( $FAIL, "\xf7\xbf\xbf" ), + 'last byte missing 9' => array( $FAIL, "\xfb\xbf\xbf\xbf" ), + 'last byte missing 10' => array( $FAIL, "\xfd\xbf\xbf\xbf\xbf" ), + + 'extra continuation byte 1' => array( $FAIL, "e\xaf" ), + 'extra continuation byte 2' => array( $FAIL, "\xc3\x89\xaf" ), + 'extra continuation byte 3' => array( $FAIL, "\xef\xbc\xa5\xaf" ), + 'extra continuation byte 4' => array( $FAIL, "\xf0\x9d\x99\xb4\xaf" ), + + 'impossible bytes 1' => array( $FAIL, "\xfe" ), + 'impossible bytes 2' => array( $FAIL, "\xff" ), + 'impossible bytes 3' => array( $FAIL, "\xfe\xfe\xff\xff" ), + + 'overlong sequences 1' => array( $FAIL, "\xc0\xaf" ), + 'overlong sequences 2' => array( $FAIL, "\xc1\xaf" ), + 'overlong sequences 3' => array( $FAIL, "\xe0\x80\xaf" ), + 'overlong sequences 4' => array( $FAIL, "\xf0\x80\x80\xaf" ), + 'overlong sequences 5' => array( $FAIL, "\xf8\x80\x80\x80\xaf" ), + 'overlong sequences 6' => array( $FAIL, "\xfc\x80\x80\x80\x80\xaf" ), + + 'maximum overlong sequences 1' => array( $FAIL, "\xc1\xbf" ), + 'maximum overlong sequences 2' => array( $FAIL, "\xe0\x9f\xbf" ), + 'maximum overlong sequences 3' => array( $FAIL, "\xf0\x8f\xbf\xbf" ), + 'maximum overlong sequences 4' => array( $FAIL, "\xf8\x87\xbf\xbf" ), + 'maximum overlong sequences 5' => array( $FAIL, "\xfc\x83\xbf\xbf\xbf\xbf" ), + + 'surrogates 1 (U+D799)' => array( $PASS, "\xed\x9f\xbf" ), + 'surrogates 2 (U+E000)' => array( $PASS, "\xee\x80\x80" ), + 'surrogates 3 (U+D800)' => array( $FAIL, "\xed\xa0\x80" ), + 'surrogates 4 (U+DBFF)' => array( $FAIL, "\xed\xaf\xbf" ), + 'surrogates 5 (U+DC00)' => array( $FAIL, "\xed\xb0\x80" ), + 'surrogates 6 (U+DFFF)' => array( $FAIL, "\xed\xbf\xbf" ), + 'surrogates 7 (U+D800 U+DC00)' => array( $FAIL, "\xed\xa0\x80\xed\xb0\x80" ), + + 'noncharacters 1' => array( $PASS, "\xef\xbf\xbe" ), + 'noncharacters 2' => array( $PASS, "\xef\xbf\xbf" ), + ); + } +} diff --git a/tests/phpunit/includes/TemplateCategoriesTest.php b/tests/phpunit/includes/TemplateCategoriesTest.php new file mode 100644 index 00000000..fb63a564 --- /dev/null +++ b/tests/phpunit/includes/TemplateCategoriesTest.php @@ -0,0 +1,53 @@ +mRights = array( 'createpage', 'edit', 'purge' ); + + $page->doEditContent( + new WikitextContent( '{{Categorising template}}' ), + 'Create a page with a template', + 0, + false, + $user + ); + + $this->assertEquals( + array() + , $title->getParentCategories() + ); + + $template = WikiPage::factory( Title::newFromText( 'Template:Categorising template' ) ); + + $template->doEditContent( + new WikitextContent( '[[Category:Solved bugs]]' ), + 'Add a category through a template', + 0, + false, + $user + ); + + // Run the job queue + JobQueueGroup::destroySingletons(); + $jobs = new RunJobs; + $jobs->loadParamsAndArgs( null, array( 'quiet' => true ), null ); + $jobs->execute(); + + $this->assertEquals( + array( 'Category:Solved_bugs' => $title->getPrefixedText() ) + , $title->getParentCategories() + ); + } +} diff --git a/tests/phpunit/includes/TestUser.php b/tests/phpunit/includes/TestUser.php new file mode 100644 index 00000000..23e65031 --- /dev/null +++ b/tests/phpunit/includes/TestUser.php @@ -0,0 +1,59 @@ +username = $username; + $this->realname = $realname; + $this->email = $email; + $this->groups = $groups; + + // don't allow user to hardcode or select passwords -- people sometimes run tests + // on live wikis. Sometimes we create sysop users in these tests. A sysop user with + // a known password would be a Bad Thing. + $this->password = User::randomPassword(); + + $this->user = User::newFromName( $this->username ); + $this->user->load(); + + // In an ideal world we'd have a new wiki (or mock data store) for every single test. + // But for now, we just need to create or update the user with the desired properties. + // we particularly need the new password, since we just generated it randomly. + // In core MediaWiki, there is no functionality to delete users, so this is the best we can do. + if ( !$this->user->getID() ) { + // create the user + $this->user = User::createNew( + $this->username, array( + "email" => $this->email, + "real_name" => $this->realname + ) + ); + if ( !$this->user ) { + throw new Exception( "error creating user" ); + } + } + + // update the user to use the new random password and other details + $this->user->setPassword( $this->password ); + $this->user->setEmail( $this->email ); + $this->user->setRealName( $this->realname ); + // remove all groups, replace with any groups specified + foreach ( $this->user->getGroups() as $group ) { + $this->user->removeGroup( $group ); + } + if ( count( $this->groups ) ) { + foreach ( $this->groups as $group ) { + $this->user->addGroup( $group ); + } + } + $this->user->saveSettings(); + } +} diff --git a/tests/phpunit/includes/TimeAdjustTest.php b/tests/phpunit/includes/TimeAdjustTest.php new file mode 100644 index 00000000..0b368c25 --- /dev/null +++ b/tests/phpunit/includes/TimeAdjustTest.php @@ -0,0 +1,41 @@ +iniSet( 'precision', 15 ); + } + + /** + * Test offset usage for a given Language::userAdjust + * @dataProvider dataUserAdjust + * @covers Language::userAdjust + */ + public function testUserAdjust( $date, $localTZoffset, $expected ) { + global $wgContLang; + + $this->setMwGlobals( 'wgLocalTZoffset', $localTZoffset ); + + $this->assertEquals( + strval( $expected ), + strval( $wgContLang->userAdjust( $date, '' ) ), + "User adjust {$date} by {$localTZoffset} minutes should give {$expected}" + ); + } + + public static function dataUserAdjust() { + return array( + array( 20061231235959, 0, 20061231235959 ), + array( 20061231235959, 5, 20070101000459 ), + array( 20061231235959, 15, 20070101001459 ), + array( 20061231235959, 60, 20070101005959 ), + array( 20061231235959, 90, 20070101012959 ), + array( 20061231235959, 120, 20070101015959 ), + array( 20061231235959, 540, 20070101085959 ), + array( 20061231235959, -5, 20061231235459 ), + array( 20061231235959, -30, 20061231232959 ), + array( 20061231235959, -60, 20061231225959 ), + ); + } +} diff --git a/tests/phpunit/includes/TimestampTest.php b/tests/phpunit/includes/TimestampTest.php new file mode 100644 index 00000000..53388392 --- /dev/null +++ b/tests/phpunit/includes/TimestampTest.php @@ -0,0 +1,304 @@ +setLanguage( Language::factory( 'en' ) ); + } + + /** + * Test parsing of valid timestamps and outputing to MW format. + * @dataProvider provideValidTimestamps + * @covers MWTimestamp::getTimestamp + */ + public function testValidParse( $format, $original, $expected ) { + $timestamp = new MWTimestamp( $original ); + $this->assertEquals( $expected, $timestamp->getTimestamp( TS_MW ) ); + } + + /** + * Test outputting valid timestamps to different formats. + * @dataProvider provideValidTimestamps + * @covers MWTimestamp::getTimestamp + */ + public function testValidOutput( $format, $expected, $original ) { + $timestamp = new MWTimestamp( $original ); + $this->assertEquals( $expected, (string)$timestamp->getTimestamp( $format ) ); + } + + /** + * Test an invalid timestamp. + * @expectedException TimestampException + * @covers MWTimestamp + */ + public function testInvalidParse() { + new MWTimestamp( "This is not a timestamp." ); + } + + /** + * Test requesting an invalid output format. + * @expectedException TimestampException + * @covers MWTimestamp::getTimestamp + */ + public function testInvalidOutput() { + $timestamp = new MWTimestamp( '1343761268' ); + $timestamp->getTimestamp( 98 ); + } + + /** + * Returns a list of valid timestamps in the format: + * array( type, timestamp_of_type, timestamp_in_MW ) + */ + public static function provideValidTimestamps() { + return array( + // Various formats + array( TS_UNIX, '1343761268', '20120731190108' ), + array( TS_MW, '20120731190108', '20120731190108' ), + array( TS_DB, '2012-07-31 19:01:08', '20120731190108' ), + array( TS_ISO_8601, '2012-07-31T19:01:08Z', '20120731190108' ), + array( TS_ISO_8601_BASIC, '20120731T190108Z', '20120731190108' ), + array( TS_EXIF, '2012:07:31 19:01:08', '20120731190108' ), + array( TS_RFC2822, 'Tue, 31 Jul 2012 19:01:08 GMT', '20120731190108' ), + array( TS_ORACLE, '31-07-2012 19:01:08.000000', '20120731190108' ), + array( TS_POSTGRES, '2012-07-31 19:01:08 GMT', '20120731190108' ), + // Some extremes and weird values + array( TS_ISO_8601, '9999-12-31T23:59:59Z', '99991231235959' ), + array( TS_UNIX, '-62135596801', '00001231235959' ) + ); + } + + /** + * @dataProvider provideHumanTimestampTests + * @covers MWTimestamp::getHumanTimestamp + */ + public function testHumanTimestamp( + $tsTime, // The timestamp to format + $currentTime, // The time to consider "now" + $timeCorrection, // The time offset to use + $dateFormat, // The date preference to use + $expectedOutput, // The expected output + $desc // Description + ) { + $user = $this->getMock( 'User' ); + $user->expects( $this->any() ) + ->method( 'getOption' ) + ->with( 'timecorrection' ) + ->will( $this->returnValue( $timeCorrection ) ); + + $user->expects( $this->any() ) + ->method( 'getDatePreference' ) + ->will( $this->returnValue( $dateFormat ) ); + + $tsTime = new MWTimestamp( $tsTime ); + $currentTime = new MWTimestamp( $currentTime ); + + $this->assertEquals( + $expectedOutput, + $tsTime->getHumanTimestamp( $currentTime, $user ), + $desc + ); + } + + public static function provideHumanTimestampTests() { + return array( + array( + '20111231170000', + '20120101000000', + 'Offset|0', + 'mdy', + 'Yesterday at 17:00', + '"Yesterday" across years', + ), + array( + '20120717190900', + '20120717190929', + 'Offset|0', + 'mdy', + 'just now', + '"Just now"', + ), + array( + '20120717190900', + '20120717191530', + 'Offset|0', + 'mdy', + '6 minutes ago', + 'X minutes ago', + ), + array( + '20121006173100', + '20121006173200', + 'Offset|0', + 'mdy', + '1 minute ago', + '"1 minute ago"', + ), + array( + '20120617190900', + '20120717190900', + 'Offset|0', + 'mdy', + 'June 17', + 'Another month' + ), + array( + '19910130151500', + '20120716193700', + 'Offset|0', + 'mdy', + '15:15, January 30, 1991', + 'Different year', + ), + array( + '20120101050000', + '20120101080000', + 'Offset|-360', + 'mdy', + 'Yesterday at 23:00', + '"Yesterday" across years with time correction', + ), + array( + '20120714184300', + '20120716184300', + 'Offset|-420', + 'mdy', + 'Saturday at 11:43', + 'Recent weekday with time correction', + ), + array( + '20120714184300', + '20120715040000', + 'Offset|-420', + 'mdy', + '11:43', + 'Today at another time with time correction', + ), + array( + '20120617190900', + '20120717190900', + 'Offset|0', + 'dmy', + '17 June', + 'Another month with dmy' + ), + array( + '20120617190900', + '20120717190900', + 'Offset|0', + 'ISO 8601', + '06-17', + 'Another month with ISO-8601' + ), + array( + '19910130151500', + '20120716193700', + 'Offset|0', + 'ISO 8601', + '1991-01-30T15:15:00', + 'Different year with ISO-8601', + ), + ); + } + + /** + * @dataProvider provideRelativeTimestampTests + * @covers MWTimestamp::getRelativeTimestamp + */ + public function testRelativeTimestamp( + $tsTime, // The timestamp to format + $currentTime, // The time to consider "now" + $timeCorrection, // The time offset to use + $dateFormat, // The date preference to use + $expectedOutput, // The expected output + $desc // Description + ) { + $user = $this->getMock( 'User' ); + $user->expects( $this->any() ) + ->method( 'getOption' ) + ->with( 'timecorrection' ) + ->will( $this->returnValue( $timeCorrection ) ); + + $tsTime = new MWTimestamp( $tsTime ); + $currentTime = new MWTimestamp( $currentTime ); + + $this->assertEquals( + $expectedOutput, + $tsTime->getRelativeTimestamp( $currentTime, $user ), + $desc + ); + } + + public static function provideRelativeTimestampTests() { + return array( + array( + '20111231170000', + '20120101000000', + 'Offset|0', + 'mdy', + '7 hours ago', + '"Yesterday" across years', + ), + array( + '20120717190900', + '20120717190929', + 'Offset|0', + 'mdy', + '29 seconds ago', + '"Just now"', + ), + array( + '20120717190900', + '20120717191530', + 'Offset|0', + 'mdy', + '6 minutes and 30 seconds ago', + 'Combination of multiple units', + ), + array( + '20121006173100', + '20121006173200', + 'Offset|0', + 'mdy', + '1 minute ago', + '"1 minute ago"', + ), + array( + '19910130151500', + '20120716193700', + 'Offset|0', + 'mdy', + '2 decades, 1 year, 168 days, 2 hours, 8 minutes and 48 seconds ago', + 'A long time ago', + ), + array( + '20120101050000', + '20120101080000', + 'Offset|-360', + 'mdy', + '3 hours ago', + '"Yesterday" across years with time correction', + ), + array( + '20120714184300', + '20120716184300', + 'Offset|-420', + 'mdy', + '2 days ago', + 'Recent weekday with time correction', + ), + array( + '20120714184300', + '20120715040000', + 'Offset|-420', + 'mdy', + '9 hours and 17 minutes ago', + 'Today at another time with time correction', + ), + ); + } +} diff --git a/tests/phpunit/includes/TitleMethodsTest.php b/tests/phpunit/includes/TitleMethodsTest.php new file mode 100644 index 00000000..3079d73a --- /dev/null +++ b/tests/phpunit/includes/TitleMethodsTest.php @@ -0,0 +1,300 @@ +mergeMwGlobalArrayValue( + 'wgExtraNamespaces', + array( + 12302 => 'TEST-JS', + 12303 => 'TEST-JS_TALK', + ) + ); + + $this->mergeMwGlobalArrayValue( + 'wgNamespaceContentModels', + array( + 12302 => CONTENT_MODEL_JAVASCRIPT, + ) + ); + + MWNamespace::getCanonicalNamespaces( true ); # reset namespace cache + $wgContLang->resetNamespaces(); # reset namespace cache + } + + public function tearDown() { + global $wgContLang; + + parent::tearDown(); + + MWNamespace::getCanonicalNamespaces( true ); # reset namespace cache + $wgContLang->resetNamespaces(); # reset namespace cache + } + + public static function provideEquals() { + return array( + array( 'Main Page', 'Main Page', true ), + array( 'Main Page', 'Not The Main Page', false ), + array( 'Main Page', 'Project:Main Page', false ), + array( 'File:Example.png', 'Image:Example.png', true ), + array( 'Special:Version', 'Special:Version', true ), + array( 'Special:Version', 'Special:Recentchanges', false ), + array( 'Special:Version', 'Main Page', false ), + ); + } + + /** + * @dataProvider provideEquals + * @covers Title::equals + */ + public function testEquals( $titleA, $titleB, $expectedBool ) { + $titleA = Title::newFromText( $titleA ); + $titleB = Title::newFromText( $titleB ); + + $this->assertEquals( $expectedBool, $titleA->equals( $titleB ) ); + $this->assertEquals( $expectedBool, $titleB->equals( $titleA ) ); + } + + public static function provideInNamespace() { + return array( + array( 'Main Page', NS_MAIN, true ), + array( 'Main Page', NS_TALK, false ), + array( 'Main Page', NS_USER, false ), + array( 'User:Foo', NS_USER, true ), + array( 'User:Foo', NS_USER_TALK, false ), + array( 'User:Foo', NS_TEMPLATE, false ), + array( 'User_talk:Foo', NS_USER_TALK, true ), + array( 'User_talk:Foo', NS_USER, false ), + ); + } + + /** + * @dataProvider provideInNamespace + * @covers Title::inNamespace + */ + public function testInNamespace( $title, $ns, $expectedBool ) { + $title = Title::newFromText( $title ); + $this->assertEquals( $expectedBool, $title->inNamespace( $ns ) ); + } + + /** + * @covers Title::inNamespaces + */ + public function testInNamespaces() { + $mainpage = Title::newFromText( 'Main Page' ); + $this->assertTrue( $mainpage->inNamespaces( NS_MAIN, NS_USER ) ); + $this->assertTrue( $mainpage->inNamespaces( array( NS_MAIN, NS_USER ) ) ); + $this->assertTrue( $mainpage->inNamespaces( array( NS_USER, NS_MAIN ) ) ); + $this->assertFalse( $mainpage->inNamespaces( array( NS_PROJECT, NS_TEMPLATE ) ) ); + } + + public static function provideHasSubjectNamespace() { + return array( + array( 'Main Page', NS_MAIN, true ), + array( 'Main Page', NS_TALK, true ), + array( 'Main Page', NS_USER, false ), + array( 'User:Foo', NS_USER, true ), + array( 'User:Foo', NS_USER_TALK, true ), + array( 'User:Foo', NS_TEMPLATE, false ), + array( 'User_talk:Foo', NS_USER_TALK, true ), + array( 'User_talk:Foo', NS_USER, true ), + ); + } + + /** + * @dataProvider provideHasSubjectNamespace + * @covers Title::hasSubjectNamespace + */ + public function testHasSubjectNamespace( $title, $ns, $expectedBool ) { + $title = Title::newFromText( $title ); + $this->assertEquals( $expectedBool, $title->hasSubjectNamespace( $ns ) ); + } + + public function dataGetContentModel() { + return array( + array( 'Help:Foo', CONTENT_MODEL_WIKITEXT ), + array( 'Help:Foo.js', CONTENT_MODEL_WIKITEXT ), + array( 'Help:Foo/bar.js', CONTENT_MODEL_WIKITEXT ), + array( 'User:Foo', CONTENT_MODEL_WIKITEXT ), + array( 'User:Foo.js', CONTENT_MODEL_WIKITEXT ), + array( 'User:Foo/bar.js', CONTENT_MODEL_JAVASCRIPT ), + array( 'User:Foo/bar.css', CONTENT_MODEL_CSS ), + array( 'User talk:Foo/bar.css', CONTENT_MODEL_WIKITEXT ), + array( 'User:Foo/bar.js.xxx', CONTENT_MODEL_WIKITEXT ), + array( 'User:Foo/bar.xxx', CONTENT_MODEL_WIKITEXT ), + array( 'MediaWiki:Foo.js', CONTENT_MODEL_JAVASCRIPT ), + array( 'MediaWiki:Foo.css', CONTENT_MODEL_CSS ), + array( 'MediaWiki:Foo/bar.css', CONTENT_MODEL_CSS ), + array( 'MediaWiki:Foo.JS', CONTENT_MODEL_WIKITEXT ), + array( 'MediaWiki:Foo.CSS', CONTENT_MODEL_WIKITEXT ), + array( 'MediaWiki:Foo.css.xxx', CONTENT_MODEL_WIKITEXT ), + array( 'TEST-JS:Foo', CONTENT_MODEL_JAVASCRIPT ), + array( 'TEST-JS:Foo.js', CONTENT_MODEL_JAVASCRIPT ), + array( 'TEST-JS:Foo/bar.js', CONTENT_MODEL_JAVASCRIPT ), + array( 'TEST-JS_TALK:Foo.js', CONTENT_MODEL_WIKITEXT ), + ); + } + + /** + * @dataProvider dataGetContentModel + * @covers Title::getContentModel + */ + public function testGetContentModel( $title, $expectedModelId ) { + $title = Title::newFromText( $title ); + $this->assertEquals( $expectedModelId, $title->getContentModel() ); + } + + /** + * @dataProvider dataGetContentModel + * @covers Title::hasContentModel + */ + public function testHasContentModel( $title, $expectedModelId ) { + $title = Title::newFromText( $title ); + $this->assertTrue( $title->hasContentModel( $expectedModelId ) ); + } + + public static function provideIsCssOrJsPage() { + return array( + array( 'Help:Foo', false ), + array( 'Help:Foo.js', false ), + array( 'Help:Foo/bar.js', false ), + array( 'User:Foo', false ), + array( 'User:Foo.js', false ), + array( 'User:Foo/bar.js', false ), + array( 'User:Foo/bar.css', false ), + array( 'User talk:Foo/bar.css', false ), + array( 'User:Foo/bar.js.xxx', false ), + array( 'User:Foo/bar.xxx', false ), + array( 'MediaWiki:Foo.js', true ), + array( 'MediaWiki:Foo.css', true ), + array( 'MediaWiki:Foo.JS', false ), + array( 'MediaWiki:Foo.CSS', false ), + array( 'MediaWiki:Foo.css.xxx', false ), + array( 'TEST-JS:Foo', false ), + array( 'TEST-JS:Foo.js', false ), + ); + } + + /** + * @dataProvider provideIsCssOrJsPage + * @covers Title::isCssOrJsPage + */ + public function testIsCssOrJsPage( $title, $expectedBool ) { + $title = Title::newFromText( $title ); + $this->assertEquals( $expectedBool, $title->isCssOrJsPage() ); + } + + public static function provideIsCssJsSubpage() { + return array( + array( 'Help:Foo', false ), + array( 'Help:Foo.js', false ), + array( 'Help:Foo/bar.js', false ), + array( 'User:Foo', false ), + array( 'User:Foo.js', false ), + array( 'User:Foo/bar.js', true ), + array( 'User:Foo/bar.css', true ), + array( 'User talk:Foo/bar.css', false ), + array( 'User:Foo/bar.js.xxx', false ), + array( 'User:Foo/bar.xxx', false ), + array( 'MediaWiki:Foo.js', false ), + array( 'User:Foo/bar.JS', false ), + array( 'User:Foo/bar.CSS', false ), + array( 'TEST-JS:Foo', false ), + array( 'TEST-JS:Foo.js', false ), + ); + } + + /** + * @dataProvider provideIsCssJsSubpage + * @covers Title::isCssJsSubpage + */ + public function testIsCssJsSubpage( $title, $expectedBool ) { + $title = Title::newFromText( $title ); + $this->assertEquals( $expectedBool, $title->isCssJsSubpage() ); + } + + public static function provideIsCssSubpage() { + return array( + array( 'Help:Foo', false ), + array( 'Help:Foo.css', false ), + array( 'User:Foo', false ), + array( 'User:Foo.js', false ), + array( 'User:Foo.css', false ), + array( 'User:Foo/bar.js', false ), + array( 'User:Foo/bar.css', true ), + ); + } + + /** + * @dataProvider provideIsCssSubpage + * @covers Title::isCssSubpage + */ + public function testIsCssSubpage( $title, $expectedBool ) { + $title = Title::newFromText( $title ); + $this->assertEquals( $expectedBool, $title->isCssSubpage() ); + } + + public static function provideIsJsSubpage() { + return array( + array( 'Help:Foo', false ), + array( 'Help:Foo.css', false ), + array( 'User:Foo', false ), + array( 'User:Foo.js', false ), + array( 'User:Foo.css', false ), + array( 'User:Foo/bar.js', true ), + array( 'User:Foo/bar.css', false ), + ); + } + + /** + * @dataProvider provideIsJsSubpage + * @covers Title::isJsSubpage + */ + public function testIsJsSubpage( $title, $expectedBool ) { + $title = Title::newFromText( $title ); + $this->assertEquals( $expectedBool, $title->isJsSubpage() ); + } + + public static function provideIsWikitextPage() { + return array( + array( 'Help:Foo', true ), + array( 'Help:Foo.js', true ), + array( 'Help:Foo/bar.js', true ), + array( 'User:Foo', true ), + array( 'User:Foo.js', true ), + array( 'User:Foo/bar.js', false ), + array( 'User:Foo/bar.css', false ), + array( 'User talk:Foo/bar.css', true ), + array( 'User:Foo/bar.js.xxx', true ), + array( 'User:Foo/bar.xxx', true ), + array( 'MediaWiki:Foo.js', false ), + array( 'MediaWiki:Foo.css', false ), + array( 'MediaWiki:Foo/bar.css', false ), + array( 'User:Foo/bar.JS', true ), + array( 'User:Foo/bar.CSS', true ), + array( 'TEST-JS:Foo', false ), + array( 'TEST-JS:Foo.js', false ), + array( 'TEST-JS_TALK:Foo.js', true ), + ); + } + + /** + * @dataProvider provideIsWikitextPage + * @covers Title::isWikitextPage + */ + public function testIsWikitextPage( $title, $expectedBool ) { + $title = Title::newFromText( $title ); + $this->assertEquals( $expectedBool, $title->isWikitextPage() ); + } +} diff --git a/tests/phpunit/includes/TitlePermissionTest.php b/tests/phpunit/includes/TitlePermissionTest.php new file mode 100644 index 00000000..f15c1772 --- /dev/null +++ b/tests/phpunit/includes/TitlePermissionTest.php @@ -0,0 +1,742 @@ +setMwGlobals( array( + 'wgMemc' => new EmptyBagOStuff, + 'wgContLang' => $langObj, + 'wgLanguageCode' => 'en', + 'wgLang' => $langObj, + 'wgLocaltimezone' => $localZone, + 'wgLocalTZoffset' => $localOffset, + 'wgNamespaceProtection' => array( + NS_MEDIAWIKI => 'editinterface', + ), + ) ); + + $this->userName = 'Useruser'; + $this->altUserName = 'Altuseruser'; + date_default_timezone_set( $localZone ); + + $this->title = Title::makeTitle( NS_MAIN, "Main Page" ); + if ( !isset( $this->userUser ) || !( $this->userUser instanceOf User ) ) { + $this->userUser = User::newFromName( $this->userName ); + + if ( !$this->userUser->getID() ) { + $this->userUser = User::createNew( $this->userName, array( + "email" => "test@example.com", + "real_name" => "Test User" ) ); + $this->userUser->load(); + } + + $this->altUser = User::newFromName( $this->altUserName ); + if ( !$this->altUser->getID() ) { + $this->altUser = User::createNew( $this->altUserName, array( + "email" => "alttest@example.com", + "real_name" => "Test User Alt" ) ); + $this->altUser->load(); + } + + $this->anonUser = User::newFromId( 0 ); + + $this->user = $this->userUser; + } + } + + protected function setUserPerm( $perm ) { + // Setting member variables is evil!!! + + if ( is_array( $perm ) ) { + $this->user->mRights = $perm; + } else { + $this->user->mRights = array( $perm ); + } + } + + protected function setTitle( $ns, $title = "Main_Page" ) { + $this->title = Title::makeTitle( $ns, $title ); + } + + protected function setUser( $userName = null ) { + if ( $userName === 'anon' ) { + $this->user = $this->anonUser; + } elseif ( $userName === null || $userName === $this->userName ) { + $this->user = $this->userUser; + } else { + $this->user = $this->altUser; + } + } + + /** + * @todo This test method should be split up into separate test methods and + * data providers + */ + public function testQuickPermissions() { + global $wgContLang; + $prefix = $wgContLang->getFormattedNsText( NS_PROJECT ); + + $this->setUser( 'anon' ); + $this->setTitle( NS_TALK ); + $this->setUserPerm( "createtalk" ); + $res = $this->title->getUserPermissionsErrors( 'create', $this->user ); + $this->assertEquals( array(), $res ); + + $this->setTitle( NS_TALK ); + $this->setUserPerm( "createpage" ); + $res = $this->title->getUserPermissionsErrors( 'create', $this->user ); + $this->assertEquals( array( array( "nocreatetext" ) ), $res ); + + $this->setTitle( NS_TALK ); + $this->setUserPerm( "" ); + $res = $this->title->getUserPermissionsErrors( 'create', $this->user ); + $this->assertEquals( array( array( 'nocreatetext' ) ), $res ); + + $this->setTitle( NS_MAIN ); + $this->setUserPerm( "createpage" ); + $res = $this->title->getUserPermissionsErrors( 'create', $this->user ); + $this->assertEquals( array(), $res ); + + $this->setTitle( NS_MAIN ); + $this->setUserPerm( "createtalk" ); + $res = $this->title->getUserPermissionsErrors( 'create', $this->user ); + $this->assertEquals( array( array( 'nocreatetext' ) ), $res ); + + $this->setUser( $this->userName ); + $this->setTitle( NS_TALK ); + $this->setUserPerm( "createtalk" ); + $res = $this->title->getUserPermissionsErrors( 'create', $this->user ); + $this->assertEquals( array(), $res ); + + $this->setTitle( NS_TALK ); + $this->setUserPerm( "createpage" ); + $res = $this->title->getUserPermissionsErrors( 'create', $this->user ); + $this->assertEquals( array( array( 'nocreate-loggedin' ) ), $res ); + + $this->setTitle( NS_TALK ); + $this->setUserPerm( "" ); + $res = $this->title->getUserPermissionsErrors( 'create', $this->user ); + $this->assertEquals( array( array( 'nocreate-loggedin' ) ), $res ); + + $this->setTitle( NS_MAIN ); + $this->setUserPerm( "createpage" ); + $res = $this->title->getUserPermissionsErrors( 'create', $this->user ); + $this->assertEquals( array(), $res ); + + $this->setTitle( NS_MAIN ); + $this->setUserPerm( "createtalk" ); + $res = $this->title->getUserPermissionsErrors( 'create', $this->user ); + $this->assertEquals( array( array( 'nocreate-loggedin' ) ), $res ); + + $this->setTitle( NS_MAIN ); + $this->setUserPerm( "" ); + $res = $this->title->getUserPermissionsErrors( 'create', $this->user ); + $this->assertEquals( array( array( 'nocreate-loggedin' ) ), $res ); + + $this->setUser( 'anon' ); + $this->setTitle( NS_USER, $this->userName . '' ); + $this->setUserPerm( "" ); + $res = $this->title->getUserPermissionsErrors( 'move', $this->user ); + $this->assertEquals( array( array( 'cant-move-user-page' ), array( 'movenologintext' ) ), $res ); + + $this->setTitle( NS_USER, $this->userName . '/subpage' ); + $this->setUserPerm( "" ); + $res = $this->title->getUserPermissionsErrors( 'move', $this->user ); + $this->assertEquals( array( array( 'movenologintext' ) ), $res ); + + $this->setTitle( NS_USER, $this->userName . '' ); + $this->setUserPerm( "move-rootuserpages" ); + $res = $this->title->getUserPermissionsErrors( 'move', $this->user ); + $this->assertEquals( array( array( 'movenologintext' ) ), $res ); + + $this->setTitle( NS_USER, $this->userName . '/subpage' ); + $this->setUserPerm( "move-rootuserpages" ); + $res = $this->title->getUserPermissionsErrors( 'move', $this->user ); + $this->assertEquals( array( array( 'movenologintext' ) ), $res ); + + $this->setTitle( NS_USER, $this->userName . '' ); + $this->setUserPerm( "" ); + $res = $this->title->getUserPermissionsErrors( 'move', $this->user ); + $this->assertEquals( array( array( 'cant-move-user-page' ), array( 'movenologintext' ) ), $res ); + + $this->setTitle( NS_USER, $this->userName . '/subpage' ); + $this->setUserPerm( "" ); + $res = $this->title->getUserPermissionsErrors( 'move', $this->user ); + $this->assertEquals( array( array( 'movenologintext' ) ), $res ); + + $this->setTitle( NS_USER, $this->userName . '' ); + $this->setUserPerm( "move-rootuserpages" ); + $res = $this->title->getUserPermissionsErrors( 'move', $this->user ); + $this->assertEquals( array( array( 'movenologintext' ) ), $res ); + + $this->setTitle( NS_USER, $this->userName . '/subpage' ); + $this->setUserPerm( "move-rootuserpages" ); + $res = $this->title->getUserPermissionsErrors( 'move', $this->user ); + $this->assertEquals( array( array( 'movenologintext' ) ), $res ); + + $this->setUser( $this->userName ); + $this->setTitle( NS_FILE, "img.png" ); + $this->setUserPerm( "" ); + $res = $this->title->getUserPermissionsErrors( 'move', $this->user ); + $this->assertEquals( array( array( 'movenotallowedfile' ), array( 'movenotallowed' ) ), $res ); + + $this->setTitle( NS_FILE, "img.png" ); + $this->setUserPerm( "movefile" ); + $res = $this->title->getUserPermissionsErrors( 'move', $this->user ); + $this->assertEquals( array( array( 'movenotallowed' ) ), $res ); + + $this->setUser( 'anon' ); + $this->setTitle( NS_FILE, "img.png" ); + $this->setUserPerm( "" ); + $res = $this->title->getUserPermissionsErrors( 'move', $this->user ); + $this->assertEquals( array( array( 'movenotallowedfile' ), array( 'movenologintext' ) ), $res ); + + $this->setTitle( NS_FILE, "img.png" ); + $this->setUserPerm( "movefile" ); + $res = $this->title->getUserPermissionsErrors( 'move', $this->user ); + $this->assertEquals( array( array( 'movenologintext' ) ), $res ); + + $this->setUser( $this->userName ); + $this->setUserPerm( "move" ); + $this->runGroupPermissions( 'move', array( array( 'movenotallowedfile' ) ) ); + + $this->setUserPerm( "" ); + $this->runGroupPermissions( 'move', array( array( 'movenotallowedfile' ), array( 'movenotallowed' ) ) ); + + $this->setUser( 'anon' ); + $this->setUserPerm( "move" ); + $this->runGroupPermissions( 'move', array( array( 'movenotallowedfile' ) ) ); + + $this->setUserPerm( "" ); + $this->runGroupPermissions( 'move', array( array( 'movenotallowedfile' ), array( 'movenotallowed' ) ), + array( array( 'movenotallowedfile' ), array( 'movenologintext' ) ) ); + + if ( $this->isWikitextNS( NS_MAIN ) ) { + //NOTE: some content models don't allow moving + // @todo find a Wikitext namespace for testing + + $this->setTitle( NS_MAIN ); + $this->setUser( 'anon' ); + $this->setUserPerm( "move" ); + $this->runGroupPermissions( 'move', array() ); + + $this->setUserPerm( "" ); + $this->runGroupPermissions( 'move', array( array( 'movenotallowed' ) ), + array( array( 'movenologintext' ) ) ); + + $this->setUser( $this->userName ); + $this->setUserPerm( "" ); + $this->runGroupPermissions( 'move', array( array( 'movenotallowed' ) ) ); + + $this->setUserPerm( "move" ); + $this->runGroupPermissions( 'move', array() ); + + $this->setUser( 'anon' ); + $this->setUserPerm( 'move' ); + $res = $this->title->getUserPermissionsErrors( 'move-target', $this->user ); + $this->assertEquals( array(), $res ); + + $this->setUserPerm( '' ); + $res = $this->title->getUserPermissionsErrors( 'move-target', $this->user ); + $this->assertEquals( array( array( 'movenotallowed' ) ), $res ); + } + + $this->setTitle( NS_USER ); + $this->setUser( $this->userName ); + $this->setUserPerm( array( "move", "move-rootuserpages" ) ); + $res = $this->title->getUserPermissionsErrors( 'move-target', $this->user ); + $this->assertEquals( array(), $res ); + + $this->setUserPerm( "move" ); + $res = $this->title->getUserPermissionsErrors( 'move-target', $this->user ); + $this->assertEquals( array( array( 'cant-move-to-user-page' ) ), $res ); + + $this->setUser( 'anon' ); + $this->setUserPerm( array( "move", "move-rootuserpages" ) ); + $res = $this->title->getUserPermissionsErrors( 'move-target', $this->user ); + $this->assertEquals( array(), $res ); + + $this->setTitle( NS_USER, "User/subpage" ); + $this->setUserPerm( array( "move", "move-rootuserpages" ) ); + $res = $this->title->getUserPermissionsErrors( 'move-target', $this->user ); + $this->assertEquals( array(), $res ); + + $this->setUserPerm( "move" ); + $res = $this->title->getUserPermissionsErrors( 'move-target', $this->user ); + $this->assertEquals( array(), $res ); + + $this->setUser( 'anon' ); + $check = array( 'edit' => array( array( array( 'badaccess-groups', "*, [[$prefix:Users|Users]]", 2 ) ), + array( array( 'badaccess-group0' ) ), + array(), true ), + 'protect' => array( array( array( 'badaccess-groups', "[[$prefix:Administrators|Administrators]]", 1 ), array( 'protect-cantedit' ) ), + array( array( 'badaccess-group0' ), array( 'protect-cantedit' ) ), + array( array( 'protect-cantedit' ) ), false ), + '' => array( array(), array(), array(), true ) ); + + foreach ( array( "edit", "protect", "" ) as $action ) { + $this->setUserPerm( null ); + $this->assertEquals( $check[$action][0], + $this->title->getUserPermissionsErrors( $action, $this->user, true ) ); + + global $wgGroupPermissions; + $old = $wgGroupPermissions; + $wgGroupPermissions = array(); + + $this->assertEquals( $check[$action][1], + $this->title->getUserPermissionsErrors( $action, $this->user, true ) ); + $wgGroupPermissions = $old; + + $this->setUserPerm( $action ); + $this->assertEquals( $check[$action][2], + $this->title->getUserPermissionsErrors( $action, $this->user, true ) ); + + $this->setUserPerm( $action ); + $this->assertEquals( $check[$action][3], + $this->title->userCan( $action, $this->user, true ) ); + $this->assertEquals( $check[$action][3], + $this->title->quickUserCan( $action, $this->user ) ); + # count( User::getGroupsWithPermissions( $action ) ) < 1 + } + } + + protected function runGroupPermissions( $action, $result, $result2 = null ) { + global $wgGroupPermissions; + + if ( $result2 === null ) { + $result2 = $result; + } + + $wgGroupPermissions['autoconfirmed']['move'] = false; + $wgGroupPermissions['user']['move'] = false; + $res = $this->title->getUserPermissionsErrors( $action, $this->user ); + $this->assertEquals( $result, $res ); + + $wgGroupPermissions['autoconfirmed']['move'] = true; + $wgGroupPermissions['user']['move'] = false; + $res = $this->title->getUserPermissionsErrors( $action, $this->user ); + $this->assertEquals( $result2, $res ); + + $wgGroupPermissions['autoconfirmed']['move'] = true; + $wgGroupPermissions['user']['move'] = true; + $res = $this->title->getUserPermissionsErrors( $action, $this->user ); + $this->assertEquals( $result2, $res ); + + $wgGroupPermissions['autoconfirmed']['move'] = false; + $wgGroupPermissions['user']['move'] = true; + $res = $this->title->getUserPermissionsErrors( $action, $this->user ); + $this->assertEquals( $result2, $res ); + } + + /** + * @todo This test method should be split up into separate test methods and + * data providers + */ + public function testSpecialsAndNSPermissions() { + global $wgNamespaceProtection; + $this->setUser( $this->userName ); + + $this->setTitle( NS_SPECIAL ); + + $this->assertEquals( array( array( 'badaccess-group0' ), array( 'ns-specialprotected' ) ), + $this->title->getUserPermissionsErrors( 'bogus', $this->user ) ); + + $this->setTitle( NS_MAIN ); + $this->setUserPerm( 'bogus' ); + $this->assertEquals( array(), + $this->title->getUserPermissionsErrors( 'bogus', $this->user ) ); + + $this->setTitle( NS_MAIN ); + $this->setUserPerm( '' ); + $this->assertEquals( array( array( 'badaccess-group0' ) ), + $this->title->getUserPermissionsErrors( 'bogus', $this->user ) ); + + $wgNamespaceProtection[NS_USER] = array( 'bogus' ); + + $this->setTitle( NS_USER ); + $this->setUserPerm( '' ); + $this->assertEquals( array( array( 'badaccess-group0' ), array( 'namespaceprotected', 'User' ) ), + $this->title->getUserPermissionsErrors( 'bogus', $this->user ) ); + + $this->setTitle( NS_MEDIAWIKI ); + $this->setUserPerm( 'bogus' ); + $this->assertEquals( array( array( 'protectedinterface' ) ), + $this->title->getUserPermissionsErrors( 'bogus', $this->user ) ); + + $this->setTitle( NS_MEDIAWIKI ); + $this->setUserPerm( 'bogus' ); + $this->assertEquals( array( array( 'protectedinterface' ) ), + $this->title->getUserPermissionsErrors( 'bogus', $this->user ) ); + + $wgNamespaceProtection = null; + + $this->setUserPerm( 'bogus' ); + $this->assertEquals( array(), + $this->title->getUserPermissionsErrors( 'bogus', $this->user ) ); + $this->assertEquals( true, + $this->title->userCan( 'bogus', $this->user ) ); + + $this->setUserPerm( '' ); + $this->assertEquals( array( array( 'badaccess-group0' ) ), + $this->title->getUserPermissionsErrors( 'bogus', $this->user ) ); + $this->assertEquals( false, + $this->title->userCan( 'bogus', $this->user ) ); + } + + /** + * @todo This test method should be split up into separate test methods and + * data providers + */ + public function testCssAndJavascriptPermissions() { + $this->setUser( $this->userName ); + + $this->setTitle( NS_USER, $this->userName . '/test.js' ); + $this->runCSSandJSPermissions( + array( array( 'badaccess-group0' ), array( 'mycustomjsprotected' ) ), + array( array( 'badaccess-group0' ), array( 'mycustomjsprotected' ) ), + array( array( 'badaccess-group0' ) ), + array( array( 'badaccess-group0' ), array( 'mycustomjsprotected' ) ), + array( array( 'badaccess-group0' ) ) + ); + + $this->setTitle( NS_USER, $this->userName . '/test.css' ); + $this->runCSSandJSPermissions( + array( array( 'badaccess-group0' ), array( 'mycustomcssprotected' ) ), + array( array( 'badaccess-group0' ) ), + array( array( 'badaccess-group0' ), array( 'mycustomcssprotected' ) ), + array( array( 'badaccess-group0' ) ), + array( array( 'badaccess-group0' ), array( 'mycustomcssprotected' ) ) + ); + + $this->setTitle( NS_USER, $this->altUserName . '/test.js' ); + $this->runCSSandJSPermissions( + array( array( 'badaccess-group0' ), array( 'customjsprotected' ) ), + array( array( 'badaccess-group0' ), array( 'customjsprotected' ) ), + array( array( 'badaccess-group0' ), array( 'customjsprotected' ) ), + array( array( 'badaccess-group0' ), array( 'customjsprotected' ) ), + array( array( 'badaccess-group0' ) ) + ); + + $this->setTitle( NS_USER, $this->altUserName . '/test.css' ); + $this->runCSSandJSPermissions( + array( array( 'badaccess-group0' ), array( 'customcssprotected' ) ), + array( array( 'badaccess-group0' ), array( 'customcssprotected' ) ), + array( array( 'badaccess-group0' ), array( 'customcssprotected' ) ), + array( array( 'badaccess-group0' ) ), + array( array( 'badaccess-group0' ), array( 'customcssprotected' ) ) + ); + + $this->setTitle( NS_USER, $this->altUserName . '/tempo' ); + $this->runCSSandJSPermissions( + array( array( 'badaccess-group0' ) ), + array( array( 'badaccess-group0' ) ), + array( array( 'badaccess-group0' ) ), + array( array( 'badaccess-group0' ) ), + array( array( 'badaccess-group0' ) ) + ); + } + + protected function runCSSandJSPermissions( $result0, $result1, $result2, $result3, $result4 ) { + $this->setUserPerm( '' ); + $this->assertEquals( $result0, + $this->title->getUserPermissionsErrors( 'bogus', + $this->user ) ); + + $this->setUserPerm( 'editmyusercss' ); + $this->assertEquals( $result1, + $this->title->getUserPermissionsErrors( 'bogus', + $this->user ) ); + + $this->setUserPerm( 'editmyuserjs' ); + $this->assertEquals( $result2, + $this->title->getUserPermissionsErrors( 'bogus', + $this->user ) ); + + $this->setUserPerm( 'editusercss' ); + $this->assertEquals( $result3, + $this->title->getUserPermissionsErrors( 'bogus', + $this->user ) ); + + $this->setUserPerm( 'edituserjs' ); + $this->assertEquals( $result4, + $this->title->getUserPermissionsErrors( 'bogus', + $this->user ) ); + + $this->setUserPerm( 'editusercssjs' ); + $this->assertEquals( array( array( 'badaccess-group0' ) ), + $this->title->getUserPermissionsErrors( 'bogus', + $this->user ) ); + + $this->setUserPerm( array( 'edituserjs', 'editusercss' ) ); + $this->assertEquals( array( array( 'badaccess-group0' ) ), + $this->title->getUserPermissionsErrors( 'bogus', + $this->user ) ); + } + + /** + * @todo This test method should be split up into separate test methods and + * data providers + */ + public function testPageRestrictions() { + global $wgContLang; + + $prefix = $wgContLang->getFormattedNsText( NS_PROJECT ); + + $this->setTitle( NS_MAIN ); + $this->title->mRestrictionsLoaded = true; + $this->setUserPerm( "edit" ); + $this->title->mRestrictions = array( "bogus" => array( 'bogus', "sysop", "protect", "" ) ); + + $this->assertEquals( array(), + $this->title->getUserPermissionsErrors( 'edit', + $this->user ) ); + + $this->assertEquals( true, + $this->title->quickUserCan( 'edit', $this->user ) ); + $this->title->mRestrictions = array( "edit" => array( 'bogus', "sysop", "protect", "" ), + "bogus" => array( 'bogus', "sysop", "protect", "" ) ); + + $this->assertEquals( array( array( 'badaccess-group0' ), + array( 'protectedpagetext', 'bogus' ), + array( 'protectedpagetext', 'editprotected' ), + array( 'protectedpagetext', 'protect' ) ), + $this->title->getUserPermissionsErrors( 'bogus', + $this->user ) ); + $this->assertEquals( array( array( 'protectedpagetext', 'bogus' ), + array( 'protectedpagetext', 'editprotected' ), + array( 'protectedpagetext', 'protect' ) ), + $this->title->getUserPermissionsErrors( 'edit', + $this->user ) ); + $this->setUserPerm( "" ); + $this->assertEquals( array( array( 'badaccess-group0' ), + array( 'protectedpagetext', 'bogus' ), + array( 'protectedpagetext', 'editprotected' ), + array( 'protectedpagetext', 'protect' ) ), + $this->title->getUserPermissionsErrors( 'bogus', + $this->user ) ); + $this->assertEquals( array( array( 'badaccess-groups', "*, [[$prefix:Users|Users]]", 2 ), + array( 'protectedpagetext', 'bogus' ), + array( 'protectedpagetext', 'editprotected' ), + array( 'protectedpagetext', 'protect' ) ), + $this->title->getUserPermissionsErrors( 'edit', + $this->user ) ); + $this->setUserPerm( array( "edit", "editprotected" ) ); + $this->assertEquals( array( array( 'badaccess-group0' ), + array( 'protectedpagetext', 'bogus' ), + array( 'protectedpagetext', 'protect' ) ), + $this->title->getUserPermissionsErrors( 'bogus', + $this->user ) ); + $this->assertEquals( array( + array( 'protectedpagetext', 'bogus' ), + array( 'protectedpagetext', 'protect' ) ), + $this->title->getUserPermissionsErrors( 'edit', + $this->user ) ); + + $this->title->mCascadeRestriction = true; + $this->setUserPerm( "edit" ); + $this->assertEquals( false, + $this->title->quickUserCan( 'bogus', $this->user ) ); + $this->assertEquals( false, + $this->title->quickUserCan( 'edit', $this->user ) ); + $this->assertEquals( array( array( 'badaccess-group0' ), + array( 'protectedpagetext', 'bogus' ), + array( 'protectedpagetext', 'editprotected' ), + array( 'protectedpagetext', 'protect' ) ), + $this->title->getUserPermissionsErrors( 'bogus', + $this->user ) ); + $this->assertEquals( array( array( 'protectedpagetext', 'bogus' ), + array( 'protectedpagetext', 'editprotected' ), + array( 'protectedpagetext', 'protect' ) ), + $this->title->getUserPermissionsErrors( 'edit', + $this->user ) ); + + $this->setUserPerm( array( "edit", "editprotected" ) ); + $this->assertEquals( false, + $this->title->quickUserCan( 'bogus', $this->user ) ); + $this->assertEquals( false, + $this->title->quickUserCan( 'edit', $this->user ) ); + $this->assertEquals( array( array( 'badaccess-group0' ), + array( 'protectedpagetext', 'bogus' ), + array( 'protectedpagetext', 'protect' ), + array( 'protectedpagetext', 'protect' ) ), + $this->title->getUserPermissionsErrors( 'bogus', + $this->user ) ); + $this->assertEquals( array( array( 'protectedpagetext', 'bogus' ), + array( 'protectedpagetext', 'protect' ), + array( 'protectedpagetext', 'protect' ) ), + $this->title->getUserPermissionsErrors( 'edit', + $this->user ) ); + } + + public function testCascadingSourcesRestrictions() { + $this->setTitle( NS_MAIN, "test page" ); + $this->setUserPerm( array( "edit", "bogus" ) ); + + $this->title->mCascadeSources = array( Title::makeTitle( NS_MAIN, "Bogus" ), Title::makeTitle( NS_MAIN, "UnBogus" ) ); + $this->title->mCascadingRestrictions = array( "bogus" => array( 'bogus', "sysop", "protect", "" ) ); + + $this->assertEquals( false, + $this->title->userCan( 'bogus', $this->user ) ); + $this->assertEquals( array( array( "cascadeprotected", 2, "* [[:Bogus]]\n* [[:UnBogus]]\n" ), + array( "cascadeprotected", 2, "* [[:Bogus]]\n* [[:UnBogus]]\n" ), + array( "cascadeprotected", 2, "* [[:Bogus]]\n* [[:UnBogus]]\n" ) ), + $this->title->getUserPermissionsErrors( 'bogus', $this->user ) ); + + $this->assertEquals( true, + $this->title->userCan( 'edit', $this->user ) ); + $this->assertEquals( array(), + $this->title->getUserPermissionsErrors( 'edit', $this->user ) ); + } + + /** + * @todo This test method should be split up into separate test methods and + * data providers + */ + public function testActionPermissions() { + $this->setUserPerm( array( "createpage" ) ); + $this->setTitle( NS_MAIN, "test page" ); + $this->title->mTitleProtection['pt_create_perm'] = ''; + $this->title->mTitleProtection['pt_user'] = $this->user->getID(); + $this->title->mTitleProtection['pt_expiry'] = wfGetDB( DB_SLAVE )->getInfinity(); + $this->title->mTitleProtection['pt_reason'] = 'test'; + $this->title->mCascadeRestriction = false; + + $this->assertEquals( array( array( 'titleprotected', 'Useruser', 'test' ) ), + $this->title->getUserPermissionsErrors( 'create', $this->user ) ); + $this->assertEquals( false, + $this->title->userCan( 'create', $this->user ) ); + + $this->title->mTitleProtection['pt_create_perm'] = 'sysop'; + $this->setUserPerm( array( 'createpage', 'protect' ) ); + $this->assertEquals( array( array( 'titleprotected', 'Useruser', 'test' ) ), + $this->title->getUserPermissionsErrors( 'create', $this->user ) ); + $this->assertEquals( false, + $this->title->userCan( 'create', $this->user ) ); + + $this->setUserPerm( array( 'createpage', 'editprotected' ) ); + $this->assertEquals( array(), + $this->title->getUserPermissionsErrors( 'create', $this->user ) ); + $this->assertEquals( true, + $this->title->userCan( 'create', $this->user ) ); + + $this->setUserPerm( array( 'createpage' ) ); + $this->assertEquals( array( array( 'titleprotected', 'Useruser', 'test' ) ), + $this->title->getUserPermissionsErrors( 'create', $this->user ) ); + $this->assertEquals( false, + $this->title->userCan( 'create', $this->user ) ); + + $this->setTitle( NS_MEDIA, "test page" ); + $this->setUserPerm( array( "move" ) ); + $this->assertEquals( false, + $this->title->userCan( 'move', $this->user ) ); + $this->assertEquals( array( array( 'immobile-source-namespace', 'Media' ) ), + $this->title->getUserPermissionsErrors( 'move', $this->user ) ); + + $this->setTitle( NS_HELP, "test page" ); + $this->assertEquals( array(), + $this->title->getUserPermissionsErrors( 'move', $this->user ) ); + $this->assertEquals( true, + $this->title->userCan( 'move', $this->user ) ); + + $this->title->mInterwiki = "no"; + $this->assertEquals( array( array( 'immobile-source-page' ) ), + $this->title->getUserPermissionsErrors( 'move', $this->user ) ); + $this->assertEquals( false, + $this->title->userCan( 'move', $this->user ) ); + + $this->setTitle( NS_MEDIA, "test page" ); + $this->assertEquals( false, + $this->title->userCan( 'move-target', $this->user ) ); + $this->assertEquals( array( array( 'immobile-target-namespace', 'Media' ) ), + $this->title->getUserPermissionsErrors( 'move-target', $this->user ) ); + + $this->setTitle( NS_HELP, "test page" ); + $this->assertEquals( array(), + $this->title->getUserPermissionsErrors( 'move-target', $this->user ) ); + $this->assertEquals( true, + $this->title->userCan( 'move-target', $this->user ) ); + + $this->title->mInterwiki = "no"; + $this->assertEquals( array( array( 'immobile-target-page' ) ), + $this->title->getUserPermissionsErrors( 'move-target', $this->user ) ); + $this->assertEquals( false, + $this->title->userCan( 'move-target', $this->user ) ); + } + + public function testUserBlock() { + global $wgEmailConfirmToEdit, $wgEmailAuthentication; + $wgEmailConfirmToEdit = true; + $wgEmailAuthentication = true; + + $this->setUserPerm( array( "createpage", "move" ) ); + $this->setTitle( NS_HELP, "test page" ); + + # $short + $this->assertEquals( array( array( 'confirmedittext' ) ), + $this->title->getUserPermissionsErrors( 'move-target', $this->user ) ); + $wgEmailConfirmToEdit = false; + $this->assertEquals( true, $this->title->userCan( 'move-target', $this->user ) ); + + # $wgEmailConfirmToEdit && !$user->isEmailConfirmed() && $action != 'createaccount' + $this->assertEquals( array(), + $this->title->getUserPermissionsErrors( 'move-target', + $this->user ) ); + + global $wgLang; + $prev = time(); + $now = time() + 120; + $this->user->mBlockedby = $this->user->getId(); + $this->user->mBlock = new Block( '127.0.8.1', 0, $this->user->getId(), + 'no reason given', $prev + 3600, 1, 0 ); + $this->user->mBlock->mTimestamp = 0; + $this->assertEquals( array( array( 'autoblockedtext', + '[[User:Useruser|Useruser]]', 'no reason given', '127.0.0.1', + 'Useruser', null, 'infinite', '127.0.8.1', + $wgLang->timeanddate( wfTimestamp( TS_MW, $prev ), true ) ) ), + $this->title->getUserPermissionsErrors( 'move-target', + $this->user ) ); + + $this->assertEquals( false, $this->title->userCan( 'move-target', $this->user ) ); + // quickUserCan should ignore user blocks + $this->assertEquals( true, $this->title->quickUserCan( 'move-target', $this->user ) ); + + global $wgLocalTZoffset; + $wgLocalTZoffset = -60; + $this->user->mBlockedby = $this->user->getName(); + $this->user->mBlock = new Block( '127.0.8.1', 0, $this->user->getId(), + 'no reason given', $now, 0, 10 ); + $this->assertEquals( array( array( 'blockedtext', + '[[User:Useruser|Useruser]]', 'no reason given', '127.0.0.1', + 'Useruser', null, '23:00, 31 December 1969', '127.0.8.1', + $wgLang->timeanddate( wfTimestamp( TS_MW, $now ), true ) ) ), + $this->title->getUserPermissionsErrors( 'move-target', $this->user ) ); + # $action != 'read' && $action != 'createaccount' && $user->isBlockedFrom( $this ) + # $user->blockedFor() == '' + # $user->mBlock->mExpiry == 'infinity' + } +} diff --git a/tests/phpunit/includes/TitleTest.php b/tests/phpunit/includes/TitleTest.php new file mode 100644 index 00000000..6bfe5453 --- /dev/null +++ b/tests/phpunit/includes/TitleTest.php @@ -0,0 +1,485 @@ +setMwGlobals( array( + 'wgLanguageCode' => 'en', + 'wgContLang' => Language::factory( 'en' ), + // User language + 'wgLang' => Language::factory( 'en' ), + 'wgAllowUserJs' => false, + 'wgDefaultLanguageVariant' => false, + ) ); + } + + /** + * @covers Title::legalChars + */ + public function testLegalChars() { + $titlechars = Title::legalChars(); + + foreach ( range( 1, 255 ) as $num ) { + $chr = chr( $num ); + if ( strpos( "#[]{}<>|", $chr ) !== false || preg_match( "/[\\x00-\\x1f\\x7f]/", $chr ) ) { + $this->assertFalse( (bool)preg_match( "/[$titlechars]/", $chr ), "chr($num) = $chr is not a valid titlechar" ); + } else { + $this->assertTrue( (bool)preg_match( "/[$titlechars]/", $chr ), "chr($num) = $chr is a valid titlechar" ); + } + } + } + + /** + * See also mediawiki.Title.test.js + * @covers Title::secureAndSplit + * @todo This method should be split into 2 separate tests each with a provider + */ + public function testSecureAndSplit() { + // Valid + foreach ( array( + 'Sandbox', + 'A "B"', + 'A \'B\'', + '.com', + '~', + '"', + '\'', + 'Talk:Sandbox', + 'Talk:Foo:Sandbox', + 'File:Example.svg', + 'File_talk:Example.svg', + 'Foo/.../Sandbox', + 'Sandbox/...', + 'A~~', + // Length is 256 total, but only title part matters + 'Category:' . str_repeat( 'x', 248 ), + str_repeat( 'x', 252 ) + ) as $text ) { + $this->assertInstanceOf( 'Title', Title::newFromText( $text ), "Valid: $text" ); + } + + // Invalid + foreach ( array( + '', + '__ __', + ' __ ', + // Bad characters forbidden regardless of wgLegalTitleChars + 'A [ B', + 'A ] B', + 'A { B', + 'A } B', + 'A < B', + 'A > B', + 'A | B', + // URL encoding + 'A%20B', + 'A%23B', + 'A%2523B', + // XML/HTML character entity references + // Note: Commented out because they are not marked invalid by the PHP test as + // Title::newFromText runs Sanitizer::decodeCharReferencesAndNormalize first. + //'A é B', + //'A é B', + //'A é B', + // Subject of NS_TALK does not roundtrip to NS_MAIN + 'Talk:File:Example.svg', + // Directory navigation + '.', + '..', + './Sandbox', + '../Sandbox', + 'Foo/./Sandbox', + 'Foo/../Sandbox', + 'Sandbox/.', + 'Sandbox/..', + // Tilde + 'A ~~~ Name', + 'A ~~~~ Signature', + 'A ~~~~~ Timestamp', + str_repeat( 'x', 256 ), + // Namespace prefix without actual title + // ':', // bug 54044 + 'Talk:', + 'Category: ', + 'Category: #bar' + ) as $text ) { + $this->assertNull( Title::newFromText( $text ), "Invalid: $text" ); + } + } + + public static function provideConvertByteClassToUnicodeClass() { + return array( + array( + ' %!"$&\'()*,\\-.\\/0-9:;=?@A-Z\\\\^_`a-z~\\x80-\\xFF+', + ' %!"$&\'()*,\\-./0-9:;=?@A-Z\\\\\\^_`a-z~+\\u0080-\\uFFFF', + ), + array( + 'QWERTYf-\\xFF+', + 'QWERTYf-\\x7F+\\u0080-\\uFFFF', + ), + array( + 'QWERTY\\x66-\\xFD+', + 'QWERTYf-\\x7F+\\u0080-\\uFFFF', + ), + array( + 'QWERTYf-y+', + 'QWERTYf-y+', + ), + array( + 'QWERTYf-\\x80+', + 'QWERTYf-\\x7F+\\u0080-\\uFFFF', + ), + array( + 'QWERTY\\x66-\\x80+\\x23', + 'QWERTYf-\\x7F+#\\u0080-\\uFFFF', + ), + array( + 'QWERTY\\x66-\\x80+\\xD3', + 'QWERTYf-\\x7F+\\u0080-\\uFFFF', + ), + array( + '\\\\\\x99', + '\\\\\\u0080-\\uFFFF', + ), + array( + '-\\x99', + '\\-\\u0080-\\uFFFF', + ), + array( + 'QWERTY\\-\\x99', + 'QWERTY\\-\\u0080-\\uFFFF', + ), + array( + '\\\\x99', + '\\\\x99', + ), + array( + 'A-\\x9F', + 'A-\\x7F\\u0080-\\uFFFF', + ), + array( + '\\x66-\\x77QWERTY\\x88-\\x91FXZ', + 'f-wQWERTYFXZ\\u0080-\\uFFFF', + ), + array( + '\\x66-\\x99QWERTY\\xAA-\\xEEFXZ', + 'f-\\x7FQWERTYFXZ\\u0080-\\uFFFF', + ), + ); + } + + /** + * @dataProvider provideConvertByteClassToUnicodeClass + * @covers Title::convertByteClassToUnicodeClass + */ + public function testConvertByteClassToUnicodeClass( $byteClass, $unicodeClass ) { + $this->assertEquals( $unicodeClass, Title::convertByteClassToUnicodeClass( $byteClass ) ); + } + + /** + * @dataProvider provideBug31100 + * @covers Title::fixSpecialName + */ + public function testBug31100FixSpecialName( $text, $expectedParam ) { + $title = Title::newFromText( $text ); + $fixed = $title->fixSpecialName(); + $stuff = explode( '/', $fixed->getDBkey(), 2 ); + if ( count( $stuff ) == 2 ) { + $par = $stuff[1]; + } else { + $par = null; + } + $this->assertEquals( $expectedParam, $par, "Bug 31100 regression check: Title->fixSpecialName() should preserve parameter" ); + } + + public static function provideBug31100() { + return array( + array( 'Special:Version', null ), + array( 'Special:Version/', '' ), + array( 'Special:Version/param', 'param' ), + ); + } + + /** + * Auth-less test of Title::isValidMoveOperation + * + * @group Database + * @param string $source + * @param string $target + * @param array|string|bool $expected Required error + * @dataProvider provideTestIsValidMoveOperation + * @covers Title::isValidMoveOperation + */ + public function testIsValidMoveOperation( $source, $target, $expected ) { + $title = Title::newFromText( $source ); + $nt = Title::newFromText( $target ); + $errors = $title->isValidMoveOperation( $nt, false ); + if ( $expected === true ) { + $this->assertTrue( $errors ); + } else { + $errors = $this->flattenErrorsArray( $errors ); + foreach ( (array)$expected as $error ) { + $this->assertContains( $error, $errors ); + } + } + } + + /** + * Provides test parameter values for testIsValidMoveOperation() + */ + public function dataTestIsValidMoveOperation() { + return array( + array( 'Test', 'Test', 'selfmove' ), + array( 'File:Test.jpg', 'Page', 'imagenocrossnamespace' ) + ); + } + + /** + * Auth-less test of Title::userCan + * + * @param array $whitelistRegexp + * @param string $source + * @param string $action + * @param array|string|bool $expected Required error + * + * @covers Title::checkReadPermissions + * @dataProvider dataWgWhitelistReadRegexp + */ + public function testWgWhitelistReadRegexp( $whitelistRegexp, $source, $action, $expected ) { + // $wgWhitelistReadRegexp must be an array. Since the provided test cases + // usually have only one regex, it is more concise to write the lonely regex + // as a string. Thus we cast to an array() to honor $wgWhitelistReadRegexp + // type requisite. + if ( is_string( $whitelistRegexp ) ) { + $whitelistRegexp = array( $whitelistRegexp ); + } + + $title = Title::newFromDBkey( $source ); + + global $wgGroupPermissions; + $oldPermissions = $wgGroupPermissions; + // Disallow all so we can ensure our regex works + $wgGroupPermissions = array(); + $wgGroupPermissions['*']['read'] = false; + + global $wgWhitelistRead; + $oldWhitelist = $wgWhitelistRead; + // Undo any LocalSettings explicite whitelists so they won't cause a + // failing test to succeed. Set it to some random non sense just + // to make sure we properly test Title::checkReadPermissions() + $wgWhitelistRead = array( 'some random non sense title' ); + + global $wgWhitelistReadRegexp; + $oldWhitelistRegexp = $wgWhitelistReadRegexp; + $wgWhitelistReadRegexp = $whitelistRegexp; + + // Just use $wgUser which in test is a user object for '127.0.0.1' + global $wgUser; + // Invalidate user rights cache to take in account $wgGroupPermissions + // change above. + $wgUser->clearInstanceCache(); + $errors = $title->userCan( $action, $wgUser ); + + // Restore globals + $wgGroupPermissions = $oldPermissions; + $wgWhitelistRead = $oldWhitelist; + $wgWhitelistReadRegexp = $oldWhitelistRegexp; + + if ( is_bool( $expected ) ) { + # Forge the assertion message depending on the assertion expectation + $allowableness = $expected + ? " should be allowed" + : " should NOT be allowed"; + $this->assertEquals( $expected, $errors, "User action '$action' on [[$source]] $allowableness." ); + } else { + $errors = $this->flattenErrorsArray( $errors ); + foreach ( (array)$expected as $error ) { + $this->assertContains( $error, $errors ); + } + } + } + + /** + * Provides test parameter values for testWgWhitelistReadRegexp() + */ + public function dataWgWhitelistReadRegexp() { + $ALLOWED = true; + $DISALLOWED = false; + + return array( + // Everything, if this doesn't work, we're really in trouble + array( '/.*/', 'Main_Page', 'read', $ALLOWED ), + array( '/.*/', 'Main_Page', 'edit', $DISALLOWED ), + + // We validate against the title name, not the db key + array( '/^Main_Page$/', 'Main_Page', 'read', $DISALLOWED ), + // Main page + array( '/^Main/', 'Main_Page', 'read', $ALLOWED ), + array( '/^Main.*/', 'Main_Page', 'read', $ALLOWED ), + // With spaces + array( '/Mic\sCheck/', 'Mic Check', 'read', $ALLOWED ), + // Unicode multibyte + // ...without unicode modifier + array( '/Unicode Test . Yes/', 'Unicode Test Ñ Yes', 'read', $DISALLOWED ), + // ...with unicode modifier + array( '/Unicode Test . Yes/u', 'Unicode Test Ñ Yes', 'read', $ALLOWED ), + // Case insensitive + array( '/MiC ChEcK/', 'mic check', 'read', $DISALLOWED ), + array( '/MiC ChEcK/i', 'mic check', 'read', $ALLOWED ), + + // From DefaultSettings.php: + array( "@^UsEr.*@i", 'User is banned', 'read', $ALLOWED ), + array( "@^UsEr.*@i", 'User:John Doe', 'read', $ALLOWED ), + + // With namespaces: + array( '/^Special:NewPages$/', 'Special:NewPages', 'read', $ALLOWED ), + array( null, 'Special:Newpages', 'read', $DISALLOWED ), + + ); + } + + public function flattenErrorsArray( $errors ) { + $result = array(); + foreach ( $errors as $error ) { + $result[] = $error[0]; + } + + return $result; + } + + public static function provideTestIsValidMoveOperation() { + return array( + array( 'Test', 'Test', 'selfmove' ), + array( 'File:Test.jpg', 'Page', 'imagenocrossnamespace' ) + ); + } + + /** + * @dataProvider provideGetPageViewLanguage + * @covers Title::getPageViewLanguage + */ + public function testGetPageViewLanguage( $expected, $titleText, $contLang, $lang, $variant, $msg = '' ) { + global $wgLanguageCode, $wgContLang, $wgLang, $wgDefaultLanguageVariant, $wgAllowUserJs; + + // Setup environnement for this test + $wgLanguageCode = $contLang; + $wgContLang = Language::factory( $contLang ); + $wgLang = Language::factory( $lang ); + $wgDefaultLanguageVariant = $variant; + $wgAllowUserJs = true; + + $title = Title::newFromText( $titleText ); + $this->assertInstanceOf( 'Title', $title, + "Test must be passed a valid title text, you gave '$titleText'" + ); + $this->assertEquals( $expected, + $title->getPageViewLanguage()->getCode(), + $msg + ); + } + + public static function provideGetPageViewLanguage() { + # Format: + # - expected + # - Title name + # - wgContLang (expected in most case) + # - wgLang (on some specific pages) + # - wgDefaultLanguageVariant + # - Optional message + return array( + array( 'fr', 'Help:I_need_somebody', 'fr', 'fr', false ), + array( 'es', 'Help:I_need_somebody', 'es', 'zh-tw', false ), + array( 'zh', 'Help:I_need_somebody', 'zh', 'zh-tw', false ), + + array( 'es', 'Help:I_need_somebody', 'es', 'zh-tw', 'zh-cn' ), + array( 'es', 'MediaWiki:About', 'es', 'zh-tw', 'zh-cn' ), + array( 'es', 'MediaWiki:About/', 'es', 'zh-tw', 'zh-cn' ), + array( 'de', 'MediaWiki:About/de', 'es', 'zh-tw', 'zh-cn' ), + array( 'en', 'MediaWiki:Common.js', 'es', 'zh-tw', 'zh-cn' ), + array( 'en', 'MediaWiki:Common.css', 'es', 'zh-tw', 'zh-cn' ), + array( 'en', 'User:JohnDoe/Common.js', 'es', 'zh-tw', 'zh-cn' ), + array( 'en', 'User:JohnDoe/Monobook.css', 'es', 'zh-tw', 'zh-cn' ), + + array( 'zh-cn', 'Help:I_need_somebody', 'zh', 'zh-tw', 'zh-cn' ), + array( 'zh', 'MediaWiki:About', 'zh', 'zh-tw', 'zh-cn' ), + array( 'zh', 'MediaWiki:About/', 'zh', 'zh-tw', 'zh-cn' ), + array( 'de', 'MediaWiki:About/de', 'zh', 'zh-tw', 'zh-cn' ), + array( 'zh-cn', 'MediaWiki:About/zh-cn', 'zh', 'zh-tw', 'zh-cn' ), + array( 'zh-tw', 'MediaWiki:About/zh-tw', 'zh', 'zh-tw', 'zh-cn' ), + array( 'en', 'MediaWiki:Common.js', 'zh', 'zh-tw', 'zh-cn' ), + array( 'en', 'MediaWiki:Common.css', 'zh', 'zh-tw', 'zh-cn' ), + array( 'en', 'User:JohnDoe/Common.js', 'zh', 'zh-tw', 'zh-cn' ), + array( 'en', 'User:JohnDoe/Monobook.css', 'zh', 'zh-tw', 'zh-cn' ), + + array( 'zh-tw', 'Special:NewPages', 'es', 'zh-tw', 'zh-cn' ), + array( 'zh-tw', 'Special:NewPages', 'zh', 'zh-tw', 'zh-cn' ), + + ); + } + + /** + * @dataProvider provideBaseTitleCases + * @covers Title::getBaseText + */ + public function testGetBaseText( $title, $expected, $msg = '' ) { + $title = Title::newFromText( $title ); + $this->assertEquals( $expected, + $title->getBaseText(), + $msg + ); + } + + public static function provideBaseTitleCases() { + return array( + # Title, expected base, optional message + array( 'User:John_Doe/subOne/subTwo', 'John Doe/subOne' ), + array( 'User:Foo/Bar/Baz', 'Foo/Bar' ), + ); + } + + /** + * @dataProvider provideRootTitleCases + * @covers Title::getRootText + */ + public function testGetRootText( $title, $expected, $msg = '' ) { + $title = Title::newFromText( $title ); + $this->assertEquals( $expected, + $title->getRootText(), + $msg + ); + } + + public static function provideRootTitleCases() { + return array( + # Title, expected base, optional message + array( 'User:John_Doe/subOne/subTwo', 'John Doe' ), + array( 'User:Foo/Bar/Baz', 'Foo' ), + ); + } + + /** + * @todo Handle $wgNamespacesWithSubpages cases + * @dataProvider provideSubpageTitleCases + * @covers Title::getSubpageText + */ + public function testGetSubpageText( $title, $expected, $msg = '' ) { + $title = Title::newFromText( $title ); + $this->assertEquals( $expected, + $title->getSubpageText(), + $msg + ); + } + + public static function provideSubpageTitleCases() { + return array( + # Title, expected base, optional message + array( 'User:John_Doe/subOne/subTwo', 'subTwo' ), + array( 'User:John_Doe/subOne', 'subOne' ), + ); + } +} diff --git a/tests/phpunit/includes/UIDGeneratorTest.php b/tests/phpunit/includes/UIDGeneratorTest.php new file mode 100644 index 00000000..8f78ae51 --- /dev/null +++ b/tests/phpunit/includes/UIDGeneratorTest.php @@ -0,0 +1,98 @@ +assertEquals( true, ctype_digit( $id ), "UID made of digit characters" ); + $this->assertLessThanOrEqual( $digitlen, strlen( $id ), + "UID has the right number of digits" ); + $this->assertLessThanOrEqual( $bits, strlen( wfBaseConvert( $id, 10, 2 ) ), + "UID has the right number of bits" ); + + $ids = array(); + for ( $i = 0; $i < 300; $i++ ) { + $ids[] = call_user_func( array( 'UIDGenerator', $method ) ); + } + + $lastId = array_shift( $ids ); + if ( $hostbits ) { + $lastHost = substr( wfBaseConvert( $lastId, 10, 2, $bits ), -$hostbits ); + } + + $this->assertArrayEquals( array_unique( $ids ), $ids, "All generated IDs are unique." ); + + foreach ( $ids as $id ) { + $id_bin = wfBaseConvert( $id, 10, 2 ); + $lastId_bin = wfBaseConvert( $lastId, 10, 2 ); + + $this->assertGreaterThanOrEqual( + substr( $id_bin, 0, $tbits ), + substr( $lastId_bin, 0, $tbits ), + "New ID timestamp ($id_bin) >= prior one ($lastId_bin)." ); + + if ( $hostbits ) { + $this->assertEquals( + substr( $id_bin, 0, -$hostbits ), + substr( $lastId_bin, 0, -$hostbits ), + "Host ID of ($id_bin) is same as prior one ($lastId_bin)." ); + } + + $lastId = $id; + } + } + + /** + * array( method, length, bits, hostbits ) + * NOTE: When adding a new method name here please update the covers tags for the tests! + */ + public static function provider_testTimestampedUID() { + return array( + array( 'newTimestampedUID128', 39, 128, 46, 48 ), + array( 'newTimestampedUID128', 39, 128, 46, 48 ), + array( 'newTimestampedUID88', 27, 88, 46, 32 ), + ); + } + + /** + * @covers UIDGenerator::newUUIDv4 + */ + public function testUUIDv4() { + for ( $i = 0; $i < 100; $i++ ) { + $id = UIDGenerator::newUUIDv4(); + $this->assertEquals( true, + preg_match( '!^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$!', $id ), + "UID $id has the right format" ); + } + } + + /** + * @covers UIDGenerator::newRawUUIDv4 + */ + public function testRawUUIDv4() { + for ( $i = 0; $i < 100; $i++ ) { + $id = UIDGenerator::newRawUUIDv4(); + $this->assertEquals( true, + preg_match( '!^[0-9a-f]{12}4[0-9a-f]{3}[89ab][0-9a-f]{15}$!', $id ), + "UID $id has the right format" ); + } + } + + /** + * @covers UIDGenerator::newRawUUIDv4 + */ + public function testRawUUIDv4QuickRand() { + for ( $i = 0; $i < 100; $i++ ) { + $id = UIDGenerator::newRawUUIDv4( UIDGenerator::QUICK_RAND ); + $this->assertEquals( true, + preg_match( '!^[0-9a-f]{12}4[0-9a-f]{3}[89ab][0-9a-f]{15}$!', $id ), + "UID $id has the right format" ); + } + } + +} diff --git a/tests/phpunit/includes/UserMailerTest.php b/tests/phpunit/includes/UserMailerTest.php new file mode 100644 index 00000000..278edfaa --- /dev/null +++ b/tests/phpunit/includes/UserMailerTest.php @@ -0,0 +1,14 @@ +assertEquals( + "=?UTF-8?Q?=C4=88u=20legebla=3F?=", + UserMailer::quotedPrintable( "\xc4\x88u legebla?", "UTF-8" ) ); + } + +} \ No newline at end of file diff --git a/tests/phpunit/includes/UserTest.php b/tests/phpunit/includes/UserTest.php new file mode 100644 index 00000000..ff33e825 --- /dev/null +++ b/tests/phpunit/includes/UserTest.php @@ -0,0 +1,237 @@ +setMwGlobals( array( + 'wgGroupPermissions' => array(), + 'wgRevokePermissions' => array(), + ) ); + + $this->setUpPermissionGlobals(); + + $this->user = new User; + $this->user->addGroup( 'unittesters' ); + } + + private function setUpPermissionGlobals() { + global $wgGroupPermissions, $wgRevokePermissions; + + # Data for regular $wgGroupPermissions test + $wgGroupPermissions['unittesters'] = array( + 'test' => true, + 'runtest' => true, + 'writetest' => false, + 'nukeworld' => false, + ); + $wgGroupPermissions['testwriters'] = array( + 'test' => true, + 'writetest' => true, + 'modifytest' => true, + ); + + # Data for regular $wgRevokePermissions test + $wgRevokePermissions['formertesters'] = array( + 'runtest' => true, + ); + + # For the options test + $wgGroupPermissions['*'] = array( + 'editmyoptions' => true, + ); + } + + /** + * @covers User::getGroupPermissions + */ + public function testGroupPermissions() { + $rights = User::getGroupPermissions( array( 'unittesters' ) ); + $this->assertContains( 'runtest', $rights ); + $this->assertNotContains( 'writetest', $rights ); + $this->assertNotContains( 'modifytest', $rights ); + $this->assertNotContains( 'nukeworld', $rights ); + + $rights = User::getGroupPermissions( array( 'unittesters', 'testwriters' ) ); + $this->assertContains( 'runtest', $rights ); + $this->assertContains( 'writetest', $rights ); + $this->assertContains( 'modifytest', $rights ); + $this->assertNotContains( 'nukeworld', $rights ); + } + + /** + * @covers User::getGroupPermissions + */ + public function testRevokePermissions() { + $rights = User::getGroupPermissions( array( 'unittesters', 'formertesters' ) ); + $this->assertNotContains( 'runtest', $rights ); + $this->assertNotContains( 'writetest', $rights ); + $this->assertNotContains( 'modifytest', $rights ); + $this->assertNotContains( 'nukeworld', $rights ); + } + + /** + * @covers User::getRights + */ + public function testUserPermissions() { + $rights = $this->user->getRights(); + $this->assertContains( 'runtest', $rights ); + $this->assertNotContains( 'writetest', $rights ); + $this->assertNotContains( 'modifytest', $rights ); + $this->assertNotContains( 'nukeworld', $rights ); + } + + /** + * @dataProvider provideGetGroupsWithPermission + * @covers User::getGroupsWithPermission + */ + public function testGetGroupsWithPermission( $expected, $right ) { + $result = User::getGroupsWithPermission( $right ); + sort( $result ); + sort( $expected ); + + $this->assertEquals( $expected, $result, "Groups with permission $right" ); + } + + public static function provideGetGroupsWithPermission() { + return array( + array( + array( 'unittesters', 'testwriters' ), + 'test' + ), + array( + array( 'unittesters' ), + 'runtest' + ), + array( + array( 'testwriters' ), + 'writetest' + ), + array( + array( 'testwriters' ), + 'modifytest' + ), + ); + } + + /** + * @dataProvider provideUserNames + * @covers User::isValidUserName + */ + public function testIsValidUserName( $username, $result, $message ) { + $this->assertEquals( $this->user->isValidUserName( $username ), $result, $message ); + } + + public static function provideUserNames() { + return array( + array( '', false, 'Empty string' ), + array( ' ', false, 'Blank space' ), + array( 'abcd', false, 'Starts with small letter' ), + array( 'Ab/cd', false, 'Contains slash' ), + array( 'Ab cd', true, 'Whitespace' ), + array( '192.168.1.1', false, 'IP' ), + array( 'User:Abcd', false, 'Reserved Namespace' ), + array( '12abcd232', true, 'Starts with Numbers' ), + array( '?abcd', true, 'Start with ? mark' ), + array( '#abcd', false, 'Start with #' ), + array( 'Abcdകഖഗഘ', true, ' Mixed scripts' ), + array( 'ജോസ്‌തോമസ്', false, 'ZWNJ- Format control character' ), + array( 'Ab cd', false, ' Ideographic space' ), + ); + } + + /** + * Test, if for all rights a right- message exist, + * which is used on Special:ListGroupRights as help text + * Extensions and core + */ + public function testAllRightsWithMessage() { + //Getting all user rights, for core: User::$mCoreRights, for extensions: $wgAvailableRights + $allRights = User::getAllRights(); + $allMessageKeys = Language::getMessageKeysFor( 'en' ); + + $rightsWithMessage = array(); + foreach ( $allMessageKeys as $message ) { + // === 0: must be at beginning of string (position 0) + if ( strpos( $message, 'right-' ) === 0 ) { + $rightsWithMessage[] = substr( $message, strlen( 'right-' ) ); + } + } + + sort( $allRights ); + sort( $rightsWithMessage ); + + $this->assertEquals( + $allRights, + $rightsWithMessage, + 'Each user rights (core/extensions) has a corresponding right- message.' + ); + } + + /** + * Test User::editCount + * @group medium + * @covers User::getEditCount + */ + public function testEditCount() { + $user = User::newFromName( 'UnitTestUser' ); + $user->loadDefaults(); + $user->addToDatabase(); + + // let the user have a few (3) edits + $page = WikiPage::factory( Title::newFromText( 'Help:UserTest_EditCount' ) ); + for ( $i = 0; $i < 3; $i++ ) { + $page->doEdit( (string)$i, 'test', 0, false, $user ); + } + + $user->clearInstanceCache(); + $this->assertEquals( 3, $user->getEditCount(), 'After three edits, the user edit count should be 3' ); + + // increase the edit count and clear the cache + $user->incEditCount(); + + $user->clearInstanceCache(); + $this->assertEquals( 4, $user->getEditCount(), 'After increasing the edit count manually, the user edit count should be 4' ); + } + + /** + * Test changing user options. + * @covers User::setOption + * @covers User::getOption + */ + public function testOptions() { + $user = User::newFromName( 'UnitTestUser' ); + $user->addToDatabase(); + + $user->setOption( 'someoption', 'test' ); + $user->setOption( 'cols', 200 ); + $user->saveSettings(); + + $user = User::newFromName( 'UnitTestUser' ); + $this->assertEquals( 'test', $user->getOption( 'someoption' ) ); + $this->assertEquals( 200, $user->getOption( 'cols' ) ); + } + + /** + * Bug 37963 + * Make sure defaults are loaded when setOption is called. + * @covers User::loadOptions + */ + public function testAnonOptions() { + global $wgDefaultUserOptions; + $this->user->setOption( 'someoption', 'test' ); + $this->assertEquals( $wgDefaultUserOptions['cols'], $this->user->getOption( 'cols' ) ); + $this->assertEquals( 'test', $this->user->getOption( 'someoption' ) ); + } +} diff --git a/tests/phpunit/includes/WebRequestTest.php b/tests/phpunit/includes/WebRequestTest.php new file mode 100644 index 00000000..f8ed14b6 --- /dev/null +++ b/tests/phpunit/includes/WebRequestTest.php @@ -0,0 +1,310 @@ +oldServer = $_SERVER; + } + + protected function tearDown() { + $_SERVER = $this->oldServer; + + parent::tearDown(); + } + + /** + * @dataProvider provideDetectServer + * @covers WebRequest::detectServer + */ + public function testDetectServer( $expected, $input, $description ) { + $_SERVER = $input; + $result = WebRequest::detectServer(); + $this->assertEquals( $expected, $result, $description ); + } + + public static function provideDetectServer() { + return array( + array( + 'http://x', + array( + 'HTTP_HOST' => 'x' + ), + 'Host header' + ), + array( + 'https://x', + array( + 'HTTP_HOST' => 'x', + 'HTTPS' => 'on', + ), + 'Host header with secure' + ), + array( + 'http://x', + array( + 'HTTP_HOST' => 'x', + 'SERVER_PORT' => 80, + ), + 'Default SERVER_PORT', + ), + array( + 'http://x', + array( + 'HTTP_HOST' => 'x', + 'HTTPS' => 'off', + ), + 'Secure off' + ), + array( + 'http://y', + array( + 'SERVER_NAME' => 'y', + ), + 'Server name' + ), + array( + 'http://x', + array( + 'HTTP_HOST' => 'x', + 'SERVER_NAME' => 'y', + ), + 'Host server name precedence' + ), + array( + 'http://[::1]:81', + array( + 'HTTP_HOST' => '[::1]', + 'SERVER_NAME' => '::1', + 'SERVER_PORT' => '81', + ), + 'Apache bug 26005' + ), + array( + 'http://localhost', + array( + 'SERVER_NAME' => '[2001' + ), + 'Kind of like lighttpd per commit message in MW r83847', + ), + array( + 'http://[2a01:e35:2eb4:1::2]:777', + array( + 'SERVER_NAME' => '[2a01:e35:2eb4:1::2]:777' + ), + 'Possible lighttpd environment per bug 14977 comment 13', + ), + ); + } + + /** + * @dataProvider provideGetIP + * @covers WebRequest::getIP + */ + public function testGetIP( $expected, $input, $squid, $xffList, $private, $description ) { + $_SERVER = $input; + $this->setMwGlobals( array( + 'wgSquidServersNoPurge' => $squid, + 'wgUsePrivateIPs' => $private, + 'wgHooks' => array( + 'IsTrustedProxy' => array( + function( &$ip, &$trusted ) use ( $xffList ) { + $trusted = $trusted || in_array( $ip, $xffList ); + return true; + } + ) + ) + ) ); + + $request = new WebRequest(); + $result = $request->getIP(); + $this->assertEquals( $expected, $result, $description ); + } + + public static function provideGetIP() { + return array( + array( + '127.0.0.1', + array( + 'REMOTE_ADDR' => '127.0.0.1' + ), + array(), + array(), + false, + 'Simple IPv4' + ), + array( + '::1', + array( + 'REMOTE_ADDR' => '::1' + ), + array(), + array(), + false, + 'Simple IPv6' + ), + array( + '12.0.0.1', + array( + 'REMOTE_ADDR' => 'abcd:0001:002:03:4:555:6666:7777', + 'HTTP_X_FORWARDED_FOR' => '12.0.0.1, abcd:0001:002:03:4:555:6666:7777', + ), + array( 'ABCD:1:2:3:4:555:6666:7777' ), + array(), + false, + 'IPv6 normalisation' + ), + array( + '12.0.0.3', + array( + 'REMOTE_ADDR' => '12.0.0.1', + 'HTTP_X_FORWARDED_FOR' => '12.0.0.3, 12.0.0.2' + ), + array( '12.0.0.1', '12.0.0.2' ), + array(), + false, + 'With X-Forwaded-For' + ), + array( + '12.0.0.1', + array( + 'REMOTE_ADDR' => '12.0.0.1', + 'HTTP_X_FORWARDED_FOR' => '12.0.0.3, 12.0.0.2' + ), + array(), + array(), + false, + 'With X-Forwaded-For and disallowed server' + ), + array( + '12.0.0.2', + array( + 'REMOTE_ADDR' => '12.0.0.1', + 'HTTP_X_FORWARDED_FOR' => '12.0.0.3, 12.0.0.2' + ), + array( '12.0.0.1' ), + array(), + false, + 'With multiple X-Forwaded-For and only one allowed server' + ), + array( + '10.0.0.3', + array( + 'REMOTE_ADDR' => '12.0.0.2', + 'HTTP_X_FORWARDED_FOR' => '10.0.0.4, 10.0.0.3, 12.0.0.2' + ), + array( '12.0.0.1', '12.0.0.2' ), + array(), + false, + 'With X-Forwaded-For and private IP (from cache proxy)' + ), + array( + '10.0.0.4', + array( + 'REMOTE_ADDR' => '12.0.0.2', + 'HTTP_X_FORWARDED_FOR' => '10.0.0.4, 10.0.0.3, 12.0.0.2' + ), + array( '12.0.0.1', '12.0.0.2', '10.0.0.3' ), + array(), + true, + 'With X-Forwaded-For and private IP (allowed)' + ), + array( + '10.0.0.4', + array( + 'REMOTE_ADDR' => '12.0.0.2', + 'HTTP_X_FORWARDED_FOR' => '10.0.0.4, 10.0.0.3, 12.0.0.2' + ), + array( '12.0.0.1', '12.0.0.2' ), + array( '10.0.0.3' ), + true, + 'With X-Forwaded-For and private IP (allowed)' + ), + array( + '10.0.0.3', + array( + 'REMOTE_ADDR' => '12.0.0.2', + 'HTTP_X_FORWARDED_FOR' => '10.0.0.4, 10.0.0.3, 12.0.0.2' + ), + array( '12.0.0.1', '12.0.0.2' ), + array( '10.0.0.3' ), + false, + 'With X-Forwaded-For and private IP (disallowed)' + ), + array( + '12.0.0.3', + array( + 'REMOTE_ADDR' => '12.0.0.1', + 'HTTP_X_FORWARDED_FOR' => '12.0.0.3, 12.0.0.2' + ), + array(), + array( '12.0.0.1', '12.0.0.2' ), + false, + 'With X-Forwaded-For' + ), + array( + '12.0.0.2', + array( + 'REMOTE_ADDR' => '12.0.0.1', + 'HTTP_X_FORWARDED_FOR' => '12.0.0.3, 12.0.0.2' + ), + array(), + array( '12.0.0.1' ), + false, + 'With multiple X-Forwaded-For and only one allowed server' + ), + array( + '12.0.0.2', + array( + 'REMOTE_ADDR' => '12.0.0.2', + 'HTTP_X_FORWARDED_FOR' => '10.0.0.3, 12.0.0.2' + ), + array(), + array( '12.0.0.2' ), + false, + 'With X-Forwaded-For and private IP and hook (disallowed)' + ), + ); + } + + /** + * @expectedException MWException + * @covers WebRequest::getIP + */ + public function testGetIpLackOfRemoteAddrThrowAnException() { + $request = new WebRequest(); + # Next call throw an exception about lacking an IP + $request->getIP(); + } + + public static function provideLanguageData() { + return array( + array( '', array(), 'Empty Accept-Language header' ), + array( 'en', array( 'en' => 1 ), 'One language' ), + array( 'en, ar', array( 'en' => 1, 'ar' => 1 ), 'Two languages listed in appearance order.' ), + array( 'zh-cn,zh-tw', array( 'zh-cn' => 1, 'zh-tw' => 1 ), 'Two equally prefered languages, listed in appearance order per rfc3282. Checks c9119' ), + array( 'es, en; q=0.5', array( 'es' => 1, 'en' => '0.5' ), 'Spanish as first language and English and second' ), + array( 'en; q=0.5, es', array( 'es' => 1, 'en' => '0.5' ), 'Less prefered language first' ), + array( 'fr, en; q=0.5, es', array( 'fr' => 1, 'es' => 1, 'en' => '0.5' ), 'Three languages' ), + array( 'en; q=0.5, es', array( 'es' => 1, 'en' => '0.5' ), 'Two languages' ), + array( 'en, zh;q=0', array( 'en' => 1 ), "It's Chinese to me" ), + array( 'es; q=1, pt;q=0.7, it; q=0.6, de; q=0.1, ru;q=0', array( 'es' => '1', 'pt' => '0.7', 'it' => '0.6', 'de' => '0.1' ), 'Preference for romance languages' ), + array( 'en-gb, en-us; q=1', array( 'en-gb' => 1, 'en-us' => '1' ), 'Two equally prefered English variants' ), + ); + } + + /** + * @dataProvider provideLanguageData + * @covers WebRequest::getAcceptLang + */ + public function testAcceptLang( $acceptLanguageHeader, $expectedLanguages, $description ) { + $_SERVER = array( 'HTTP_ACCEPT_LANGUAGE' => $acceptLanguageHeader ); + $request = new WebRequest(); + $this->assertSame( $request->getAcceptLang(), $expectedLanguages, $description ); + } +} diff --git a/tests/phpunit/includes/WikiPageTest.php b/tests/phpunit/includes/WikiPageTest.php new file mode 100644 index 00000000..e0d786b9 --- /dev/null +++ b/tests/phpunit/includes/WikiPageTest.php @@ -0,0 +1,1074 @@ +tablesUsed = array_merge( + $this->tablesUsed, + array( 'page', + 'revision', + 'text', + + 'recentchanges', + 'logging', + + 'page_props', + 'pagelinks', + 'categorylinks', + 'langlinks', + 'externallinks', + 'imagelinks', + 'templatelinks', + 'iwlinks' ) ); + } + + protected function setUp() { + parent::setUp(); + $this->pages_to_delete = array(); + + LinkCache::singleton()->clear(); # avoid cached redirect status, etc + } + + protected function tearDown() { + foreach ( $this->pages_to_delete as $p ) { + /* @var $p WikiPage */ + + try { + if ( $p->exists() ) { + $p->doDeleteArticle( "testing done." ); + } + } catch ( MWException $ex ) { + // fail silently + } + } + parent::tearDown(); + } + + /** + * @param Title $title + * @param String $model + * @return WikiPage + */ + protected function newPage( $title, $model = null ) { + if ( is_string( $title ) ) { + $ns = $this->getDefaultWikitextNS(); + $title = Title::newFromText( $title, $ns ); + } + + $p = new WikiPage( $title ); + + $this->pages_to_delete[] = $p; + + return $p; + } + + /** + * @param String|Title|WikiPage $page + * @param String $text + * @param int $model + * + * @return WikiPage + */ + protected function createPage( $page, $text, $model = null ) { + if ( is_string( $page ) || $page instanceof Title ) { + $page = $this->newPage( $page, $model ); + } + + $content = ContentHandler::makeContent( $text, $page->getTitle(), $model ); + $page->doEditContent( $content, "testing", EDIT_NEW ); + + return $page; + } + + /** + * @covers WikiPage::doEditContent + */ + public function testDoEditContent() { + $page = $this->newPage( "WikiPageTest_testDoEditContent" ); + $title = $page->getTitle(); + + $content = ContentHandler::makeContent( "[[Lorem ipsum]] dolor sit amet, consetetur sadipscing elitr, sed diam " + . " nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat.", + $title, CONTENT_MODEL_WIKITEXT ); + + $page->doEditContent( $content, "[[testing]] 1" ); + + $this->assertTrue( $title->getArticleID() > 0, "Title object should have new page id" ); + $this->assertTrue( $page->getId() > 0, "WikiPage should have new page id" ); + $this->assertTrue( $title->exists(), "Title object should indicate that the page now exists" ); + $this->assertTrue( $page->exists(), "WikiPage object should indicate that the page now exists" ); + + $id = $page->getId(); + + # ------------------------ + $dbr = wfGetDB( DB_SLAVE ); + $res = $dbr->select( 'pagelinks', '*', array( 'pl_from' => $id ) ); + $n = $res->numRows(); + $res->free(); + + $this->assertEquals( 1, $n, 'pagelinks should contain one link from the page' ); + + # ------------------------ + $page = new WikiPage( $title ); + + $retrieved = $page->getContent(); + $this->assertTrue( $content->equals( $retrieved ), 'retrieved content doesn\'t equal original' ); + + # ------------------------ + $content = ContentHandler::makeContent( "At vero eos et accusam et justo duo [[dolores]] et ea rebum. " + . "Stet clita kasd [[gubergren]], no sea takimata sanctus est.", + $title, CONTENT_MODEL_WIKITEXT ); + + $page->doEditContent( $content, "testing 2" ); + + # ------------------------ + $page = new WikiPage( $title ); + + $retrieved = $page->getContent(); + $this->assertTrue( $content->equals( $retrieved ), 'retrieved content doesn\'t equal original' ); + + # ------------------------ + $dbr = wfGetDB( DB_SLAVE ); + $res = $dbr->select( 'pagelinks', '*', array( 'pl_from' => $id ) ); + $n = $res->numRows(); + $res->free(); + + $this->assertEquals( 2, $n, 'pagelinks should contain two links from the page' ); + } + + /** + * @covers WikiPage::doEdit + */ + public function testDoEdit() { + $this->hideDeprecated( "WikiPage::doEdit" ); + $this->hideDeprecated( "WikiPage::getText" ); + $this->hideDeprecated( "Revision::getText" ); + + //NOTE: assume help namespace will default to wikitext + $title = Title::newFromText( "Help:WikiPageTest_testDoEdit" ); + + $page = $this->newPage( $title ); + + $text = "[[Lorem ipsum]] dolor sit amet, consetetur sadipscing elitr, sed diam " + . " nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat."; + + $page->doEdit( $text, "[[testing]] 1" ); + + $this->assertTrue( $title->getArticleID() > 0, "Title object should have new page id" ); + $this->assertTrue( $page->getId() > 0, "WikiPage should have new page id" ); + $this->assertTrue( $title->exists(), "Title object should indicate that the page now exists" ); + $this->assertTrue( $page->exists(), "WikiPage object should indicate that the page now exists" ); + + $id = $page->getId(); + + # ------------------------ + $dbr = wfGetDB( DB_SLAVE ); + $res = $dbr->select( 'pagelinks', '*', array( 'pl_from' => $id ) ); + $n = $res->numRows(); + $res->free(); + + $this->assertEquals( 1, $n, 'pagelinks should contain one link from the page' ); + + # ------------------------ + $page = new WikiPage( $title ); + + $retrieved = $page->getText(); + $this->assertEquals( $text, $retrieved, 'retrieved text doesn\'t equal original' ); + + # ------------------------ + $text = "At vero eos et accusam et justo duo [[dolores]] et ea rebum. " + . "Stet clita kasd [[gubergren]], no sea takimata sanctus est."; + + $page->doEdit( $text, "testing 2" ); + + # ------------------------ + $page = new WikiPage( $title ); + + $retrieved = $page->getText(); + $this->assertEquals( $text, $retrieved, 'retrieved text doesn\'t equal original' ); + + # ------------------------ + $dbr = wfGetDB( DB_SLAVE ); + $res = $dbr->select( 'pagelinks', '*', array( 'pl_from' => $id ) ); + $n = $res->numRows(); + $res->free(); + + $this->assertEquals( 2, $n, 'pagelinks should contain two links from the page' ); + } + + /** + * @covers WikiPage::doQuickEdit + */ + public function testDoQuickEdit() { + global $wgUser; + + $this->hideDeprecated( "WikiPage::doQuickEdit" ); + + //NOTE: assume help namespace will default to wikitext + $page = $this->createPage( "Help:WikiPageTest_testDoQuickEdit", "original text" ); + + $text = "quick text"; + $page->doQuickEdit( $text, $wgUser, "testing q" ); + + # --------------------- + $page = new WikiPage( $page->getTitle() ); + $this->assertEquals( $text, $page->getText() ); + } + + /** + * @covers WikiPage::doQuickEditContent + */ + public function testDoQuickEditContent() { + global $wgUser; + + $page = $this->createPage( "WikiPageTest_testDoQuickEditContent", "original text", CONTENT_MODEL_WIKITEXT ); + + $content = ContentHandler::makeContent( "quick text", $page->getTitle(), CONTENT_MODEL_WIKITEXT ); + $page->doQuickEditContent( $content, $wgUser, "testing q" ); + + # --------------------- + $page = new WikiPage( $page->getTitle() ); + $this->assertTrue( $content->equals( $page->getContent() ) ); + } + + /** + * @covers WikiPage::doDeleteArticle + */ + public function testDoDeleteArticle() { + $page = $this->createPage( "WikiPageTest_testDoDeleteArticle", "[[original text]] foo", CONTENT_MODEL_WIKITEXT ); + $id = $page->getId(); + + $page->doDeleteArticle( "testing deletion" ); + + $this->assertFalse( $page->getTitle()->getArticleID() > 0, "Title object should now have page id 0" ); + $this->assertFalse( $page->getId() > 0, "WikiPage should now have page id 0" ); + $this->assertFalse( $page->exists(), "WikiPage::exists should return false after page was deleted" ); + $this->assertNull( $page->getContent(), "WikiPage::getContent should return null after page was deleted" ); + $this->assertFalse( $page->getText(), "WikiPage::getText should return false after page was deleted" ); + + $t = Title::newFromText( $page->getTitle()->getPrefixedText() ); + $this->assertFalse( $t->exists(), "Title::exists should return false after page was deleted" ); + + # ------------------------ + $dbr = wfGetDB( DB_SLAVE ); + $res = $dbr->select( 'pagelinks', '*', array( 'pl_from' => $id ) ); + $n = $res->numRows(); + $res->free(); + + $this->assertEquals( 0, $n, 'pagelinks should contain no more links from the page' ); + } + + /** + * @covers WikiPage::doDeleteUpdates + */ + public function testDoDeleteUpdates() { + $page = $this->createPage( "WikiPageTest_testDoDeleteArticle", "[[original text]] foo", CONTENT_MODEL_WIKITEXT ); + $id = $page->getId(); + + $page->doDeleteUpdates( $id ); + + # ------------------------ + $dbr = wfGetDB( DB_SLAVE ); + $res = $dbr->select( 'pagelinks', '*', array( 'pl_from' => $id ) ); + $n = $res->numRows(); + $res->free(); + + $this->assertEquals( 0, $n, 'pagelinks should contain no more links from the page' ); + } + + /** + * @covers WikiPage::getRevision + */ + public function testGetRevision() { + $page = $this->newPage( "WikiPageTest_testGetRevision" ); + + $rev = $page->getRevision(); + $this->assertNull( $rev ); + + # ----------------- + $this->createPage( $page, "some text", CONTENT_MODEL_WIKITEXT ); + + $rev = $page->getRevision(); + + $this->assertEquals( $page->getLatest(), $rev->getId() ); + $this->assertEquals( "some text", $rev->getContent()->getNativeData() ); + } + + /** + * @covers WikiPage::getContent + */ + public function testGetContent() { + $page = $this->newPage( "WikiPageTest_testGetContent" ); + + $content = $page->getContent(); + $this->assertNull( $content ); + + # ----------------- + $this->createPage( $page, "some text", CONTENT_MODEL_WIKITEXT ); + + $content = $page->getContent(); + $this->assertEquals( "some text", $content->getNativeData() ); + } + + /** + * @covers WikiPage::getText + */ + public function testGetText() { + $this->hideDeprecated( "WikiPage::getText" ); + + $page = $this->newPage( "WikiPageTest_testGetText" ); + + $text = $page->getText(); + $this->assertFalse( $text ); + + # ----------------- + $this->createPage( $page, "some text", CONTENT_MODEL_WIKITEXT ); + + $text = $page->getText(); + $this->assertEquals( "some text", $text ); + } + + /** + * @covers WikiPage::getRawText + */ + public function testGetRawText() { + $this->hideDeprecated( "WikiPage::getRawText" ); + + $page = $this->newPage( "WikiPageTest_testGetRawText" ); + + $text = $page->getRawText(); + $this->assertFalse( $text ); + + # ----------------- + $this->createPage( $page, "some text", CONTENT_MODEL_WIKITEXT ); + + $text = $page->getRawText(); + $this->assertEquals( "some text", $text ); + } + + /** + * @covers WikiPage::getContentModel + */ + public function testGetContentModel() { + global $wgContentHandlerUseDB; + + if ( !$wgContentHandlerUseDB ) { + $this->markTestSkipped( '$wgContentHandlerUseDB is disabled' ); + } + + $page = $this->createPage( "WikiPageTest_testGetContentModel", "some text", CONTENT_MODEL_JAVASCRIPT ); + + $page = new WikiPage( $page->getTitle() ); + $this->assertEquals( CONTENT_MODEL_JAVASCRIPT, $page->getContentModel() ); + } + + /** + * @covers WikiPage::getContentHandler + */ + public function testGetContentHandler() { + global $wgContentHandlerUseDB; + + if ( !$wgContentHandlerUseDB ) { + $this->markTestSkipped( '$wgContentHandlerUseDB is disabled' ); + } + + $page = $this->createPage( "WikiPageTest_testGetContentHandler", "some text", CONTENT_MODEL_JAVASCRIPT ); + + $page = new WikiPage( $page->getTitle() ); + $this->assertEquals( 'JavaScriptContentHandler', get_class( $page->getContentHandler() ) ); + } + + /** + * @covers WikiPage::exists + */ + public function testExists() { + $page = $this->newPage( "WikiPageTest_testExists" ); + $this->assertFalse( $page->exists() ); + + # ----------------- + $this->createPage( $page, "some text", CONTENT_MODEL_WIKITEXT ); + $this->assertTrue( $page->exists() ); + + $page = new WikiPage( $page->getTitle() ); + $this->assertTrue( $page->exists() ); + + # ----------------- + $page->doDeleteArticle( "done testing" ); + $this->assertFalse( $page->exists() ); + + $page = new WikiPage( $page->getTitle() ); + $this->assertFalse( $page->exists() ); + } + + public static function provideHasViewableContent() { + return array( + array( 'WikiPageTest_testHasViewableContent', false, true ), + array( 'Special:WikiPageTest_testHasViewableContent', false ), + array( 'MediaWiki:WikiPageTest_testHasViewableContent', false ), + array( 'Special:Userlogin', true ), + array( 'MediaWiki:help', true ), + ); + } + + /** + * @dataProvider provideHasViewableContent + * @covers WikiPage::hasViewableContent + */ + public function testHasViewableContent( $title, $viewable, $create = false ) { + $page = $this->newPage( $title ); + $this->assertEquals( $viewable, $page->hasViewableContent() ); + + if ( $create ) { + $this->createPage( $page, "some text", CONTENT_MODEL_WIKITEXT ); + $this->assertTrue( $page->hasViewableContent() ); + + $page = new WikiPage( $page->getTitle() ); + $this->assertTrue( $page->hasViewableContent() ); + } + } + + public static function provideGetRedirectTarget() { + return array( + array( 'WikiPageTest_testGetRedirectTarget_1', CONTENT_MODEL_WIKITEXT, "hello world", null ), + array( 'WikiPageTest_testGetRedirectTarget_2', CONTENT_MODEL_WIKITEXT, "#REDIRECT [[hello world]]", "Hello world" ), + ); + } + + /** + * @dataProvider provideGetRedirectTarget + * @covers WikiPage::getRedirectTarget + */ + public function testGetRedirectTarget( $title, $model, $text, $target ) { + $page = $this->createPage( $title, $text, $model ); + + # sanity check, because this test seems to fail for no reason for some people. + $c = $page->getContent(); + $this->assertEquals( 'WikitextContent', get_class( $c ) ); + + # now, test the actual redirect + $t = $page->getRedirectTarget(); + $this->assertEquals( $target, is_null( $t ) ? null : $t->getPrefixedText() ); + } + + /** + * @dataProvider provideGetRedirectTarget + * @covers WikiPage::isRedirect + */ + public function testIsRedirect( $title, $model, $text, $target ) { + $page = $this->createPage( $title, $text, $model ); + $this->assertEquals( !is_null( $target ), $page->isRedirect() ); + } + + public static function provideIsCountable() { + return array( + + // any + array( 'WikiPageTest_testIsCountable', + CONTENT_MODEL_WIKITEXT, + '', + 'any', + true + ), + array( 'WikiPageTest_testIsCountable', + CONTENT_MODEL_WIKITEXT, + 'Foo', + 'any', + true + ), + + // comma + array( 'WikiPageTest_testIsCountable', + CONTENT_MODEL_WIKITEXT, + 'Foo', + 'comma', + false + ), + array( 'WikiPageTest_testIsCountable', + CONTENT_MODEL_WIKITEXT, + 'Foo, bar', + 'comma', + true + ), + + // link + array( 'WikiPageTest_testIsCountable', + CONTENT_MODEL_WIKITEXT, + 'Foo', + 'link', + false + ), + array( 'WikiPageTest_testIsCountable', + CONTENT_MODEL_WIKITEXT, + 'Foo [[bar]]', + 'link', + true + ), + + // redirects + array( 'WikiPageTest_testIsCountable', + CONTENT_MODEL_WIKITEXT, + '#REDIRECT [[bar]]', + 'any', + false + ), + array( 'WikiPageTest_testIsCountable', + CONTENT_MODEL_WIKITEXT, + '#REDIRECT [[bar]]', + 'comma', + false + ), + array( 'WikiPageTest_testIsCountable', + CONTENT_MODEL_WIKITEXT, + '#REDIRECT [[bar]]', + 'link', + false + ), + + // not a content namespace + array( 'Talk:WikiPageTest_testIsCountable', + CONTENT_MODEL_WIKITEXT, + 'Foo', + 'any', + false + ), + array( 'Talk:WikiPageTest_testIsCountable', + CONTENT_MODEL_WIKITEXT, + 'Foo, bar', + 'comma', + false + ), + array( 'Talk:WikiPageTest_testIsCountable', + CONTENT_MODEL_WIKITEXT, + 'Foo [[bar]]', + 'link', + false + ), + + // not a content namespace, different model + array( 'MediaWiki:WikiPageTest_testIsCountable.js', + null, + 'Foo', + 'any', + false + ), + array( 'MediaWiki:WikiPageTest_testIsCountable.js', + null, + 'Foo, bar', + 'comma', + false + ), + array( 'MediaWiki:WikiPageTest_testIsCountable.js', + null, + 'Foo [[bar]]', + 'link', + false + ), + ); + } + + + /** + * @dataProvider provideIsCountable + * @covers WikiPage::isCountable + */ + public function testIsCountable( $title, $model, $text, $mode, $expected ) { + global $wgContentHandlerUseDB; + + $this->setMwGlobals( 'wgArticleCountMethod', $mode ); + + $title = Title::newFromText( $title ); + + if ( !$wgContentHandlerUseDB && $model && ContentHandler::getDefaultModelFor( $title ) != $model ) { + $this->markTestSkipped( "Can not use non-default content model $model for " + . $title->getPrefixedDBkey() . " with \$wgContentHandlerUseDB disabled." ); + } + + $page = $this->createPage( $title, $text, $model ); + $hasLinks = wfGetDB( DB_SLAVE )->selectField( 'pagelinks', 1, + array( 'pl_from' => $page->getId() ), __METHOD__ ); + + $editInfo = $page->prepareContentForEdit( $page->getContent() ); + + $v = $page->isCountable(); + $w = $page->isCountable( $editInfo ); + + $this->assertEquals( $expected, $v, "isCountable( null ) returned unexpected value " . var_export( $v, true ) + . " instead of " . var_export( $expected, true ) . " in mode `$mode` for text \"$text\"" ); + + $this->assertEquals( $expected, $w, "isCountable( \$editInfo ) returned unexpected value " . var_export( $v, true ) + . " instead of " . var_export( $expected, true ) . " in mode `$mode` for text \"$text\"" ); + } + + public static function provideGetParserOutput() { + return array( + array( CONTENT_MODEL_WIKITEXT, "hello ''world''\n", "

hello world

" ), + // @todo more...? + ); + } + + /** + * @dataProvider provideGetParserOutput + * @covers WikiPage::getParserOutput + */ + public function testGetParserOutput( $model, $text, $expectedHtml ) { + $page = $this->createPage( 'WikiPageTest_testGetParserOutput', $text, $model ); + + $opt = $page->makeParserOptions( 'canonical' ); + $po = $page->getParserOutput( $opt ); + $text = $po->getText(); + + $text = trim( preg_replace( '//sm', '', $text ) ); # strip injected comments + $text = preg_replace( '!\s*(

)!sm', '\1', $text ); # don't let tidy confuse us + + $this->assertEquals( $expectedHtml, $text ); + + return $po; + } + + /** + * @covers WikiPage::getParserOutput + */ + public function testGetParserOutput_nonexisting() { + static $count = 0; + $count++; + + $page = new WikiPage( new Title( "WikiPageTest_testGetParserOutput_nonexisting_$count" ) ); + + $opt = new ParserOptions(); + $po = $page->getParserOutput( $opt ); + + $this->assertFalse( $po, "getParserOutput() shall return false for non-existing pages." ); + } + + /** + * @covers WikiPage::getParserOutput + */ + public function testGetParserOutput_badrev() { + $page = $this->createPage( 'WikiPageTest_testGetParserOutput', "dummy", CONTENT_MODEL_WIKITEXT ); + + $opt = new ParserOptions(); + $po = $page->getParserOutput( $opt, $page->getLatest() + 1234 ); + + // @todo would be neat to also test deleted revision + + $this->assertFalse( $po, "getParserOutput() shall return false for non-existing revisions." ); + } + + static $sections = + + "Intro + +== stuff == +hello world + +== test == +just a test + +== foo == +more stuff +"; + + + public function dataReplaceSection() { + //NOTE: assume the Help namespace to contain wikitext + return array( + array( 'Help:WikiPageTest_testReplaceSection', + CONTENT_MODEL_WIKITEXT, + WikiPageTest::$sections, + "0", + "No more", + null, + trim( preg_replace( '/^Intro/sm', 'No more', WikiPageTest::$sections ) ) + ), + array( 'Help:WikiPageTest_testReplaceSection', + CONTENT_MODEL_WIKITEXT, + WikiPageTest::$sections, + "", + "No more", + null, + "No more" + ), + array( 'Help:WikiPageTest_testReplaceSection', + CONTENT_MODEL_WIKITEXT, + WikiPageTest::$sections, + "2", + "== TEST ==\nmore fun", + null, + trim( preg_replace( '/^== test ==.*== foo ==/sm', + "== TEST ==\nmore fun\n\n== foo ==", + WikiPageTest::$sections ) ) + ), + array( 'Help:WikiPageTest_testReplaceSection', + CONTENT_MODEL_WIKITEXT, + WikiPageTest::$sections, + "8", + "No more", + null, + trim( WikiPageTest::$sections ) + ), + array( 'Help:WikiPageTest_testReplaceSection', + CONTENT_MODEL_WIKITEXT, + WikiPageTest::$sections, + "new", + "No more", + "New", + trim( WikiPageTest::$sections ) . "\n\n== New ==\n\nNo more" + ), + ); + } + + /** + * @dataProvider dataReplaceSection + * @covers WikiPage::replaceSection + */ + public function testReplaceSection( $title, $model, $text, $section, $with, $sectionTitle, $expected ) { + $this->hideDeprecated( "WikiPage::replaceSection" ); + + $page = $this->createPage( $title, $text, $model ); + $text = $page->replaceSection( $section, $with, $sectionTitle ); + $text = trim( $text ); + + $this->assertEquals( $expected, $text ); + } + + /** + * @dataProvider dataReplaceSection + * @covers WikiPage::replaceSectionContent + */ + public function testReplaceSectionContent( $title, $model, $text, $section, $with, $sectionTitle, $expected ) { + $page = $this->createPage( $title, $text, $model ); + + $content = ContentHandler::makeContent( $with, $page->getTitle(), $page->getContentModel() ); + $c = $page->replaceSectionContent( $section, $content, $sectionTitle ); + + $this->assertEquals( $expected, is_null( $c ) ? null : trim( $c->getNativeData() ) ); + } + + /* @todo FIXME: fix this! + public function testGetUndoText() { + $this->checkHasDiff3(); + + $text = "one"; + $page = $this->createPage( "WikiPageTest_testGetUndoText", $text ); + $rev1 = $page->getRevision(); + + $text .= "\n\ntwo"; + $page->doEditContent( ContentHandler::makeContent( $text, $page->getTitle() ), "adding section two"); + $rev2 = $page->getRevision(); + + $text .= "\n\nthree"; + $page->doEditContent( ContentHandler::makeContent( $text, $page->getTitle() ), "adding section three"); + $rev3 = $page->getRevision(); + + $text .= "\n\nfour"; + $page->doEditContent( ContentHandler::makeContent( $text, $page->getTitle() ), "adding section four"); + $rev4 = $page->getRevision(); + + $text .= "\n\nfive"; + $page->doEditContent( ContentHandler::makeContent( $text, $page->getTitle() ), "adding section five"); + $rev5 = $page->getRevision(); + + $text .= "\n\nsix"; + $page->doEditContent( ContentHandler::makeContent( $text, $page->getTitle() ), "adding section six"); + $rev6 = $page->getRevision(); + + $undo6 = $page->getUndoText( $rev6 ); + if ( $undo6 === false ) $this->fail( "getUndoText failed for rev6" ); + $this->assertEquals( "one\n\ntwo\n\nthree\n\nfour\n\nfive", $undo6 ); + + $undo3 = $page->getUndoText( $rev4, $rev2 ); + if ( $undo3 === false ) $this->fail( "getUndoText failed for rev4..rev2" ); + $this->assertEquals( "one\n\ntwo\n\nfive", $undo3 ); + + $undo2 = $page->getUndoText( $rev2 ); + if ( $undo2 === false ) $this->fail( "getUndoText failed for rev2" ); + $this->assertEquals( "one\n\nfive", $undo2 ); + } + */ + + /** + * @todo FIXME: this is a better rollback test than the one below, but it keeps failing in jenkins for some reason. + */ + public function broken_testDoRollback() { + $admin = new User(); + $admin->setName( "Admin" ); + + $text = "one"; + $page = $this->newPage( "WikiPageTest_testDoRollback" ); + $page->doEditContent( ContentHandler::makeContent( $text, $page->getTitle() ), + "section one", EDIT_NEW, false, $admin ); + + $user1 = new User(); + $user1->setName( "127.0.1.11" ); + $text .= "\n\ntwo"; + $page = new WikiPage( $page->getTitle() ); + $page->doEditContent( ContentHandler::makeContent( $text, $page->getTitle() ), + "adding section two", 0, false, $user1 ); + + $user2 = new User(); + $user2->setName( "127.0.2.13" ); + $text .= "\n\nthree"; + $page = new WikiPage( $page->getTitle() ); + $page->doEditContent( ContentHandler::makeContent( $text, $page->getTitle() ), + "adding section three", 0, false, $user2 ); + + # we are having issues with doRollback spuriously failing. apparently the last revision somehow goes missing + # or not committed under some circumstances. so, make sure the last revision has the right user name. + $dbr = wfGetDB( DB_SLAVE ); + $this->assertEquals( 3, Revision::countByPageId( $dbr, $page->getId() ) ); + + $page = new WikiPage( $page->getTitle() ); + $rev3 = $page->getRevision(); + $this->assertEquals( '127.0.2.13', $rev3->getUserText() ); + + $rev2 = $rev3->getPrevious(); + $this->assertEquals( '127.0.1.11', $rev2->getUserText() ); + + $rev1 = $rev2->getPrevious(); + $this->assertEquals( 'Admin', $rev1->getUserText() ); + + # now, try the actual rollback + $admin->addGroup( "sysop" ); #XXX: make the test user a sysop... + $token = $admin->getEditToken( array( $page->getTitle()->getPrefixedText(), $user2->getName() ), null ); + $errors = $page->doRollback( $user2->getName(), "testing revert", $token, false, $details, $admin ); + + if ( $errors ) { + $this->fail( "Rollback failed:\n" . print_r( $errors, true ) . ";\n" . print_r( $details, true ) ); + } + + $page = new WikiPage( $page->getTitle() ); + $this->assertEquals( $rev2->getSha1(), $page->getRevision()->getSha1(), + "rollback did not revert to the correct revision" ); + $this->assertEquals( "one\n\ntwo", $page->getContent()->getNativeData() ); + } + + /** + * @todo FIXME: the above rollback test is better, but it keeps failing in jenkins for some reason. + * @covers WikiPage::doRollback + */ + public function testDoRollback() { + $admin = new User(); + $admin->setName( "Admin" ); + + $text = "one"; + $page = $this->newPage( "WikiPageTest_testDoRollback" ); + $page->doEditContent( ContentHandler::makeContent( $text, $page->getTitle(), CONTENT_MODEL_WIKITEXT ), + "section one", EDIT_NEW, false, $admin ); + $rev1 = $page->getRevision(); + + $user1 = new User(); + $user1->setName( "127.0.1.11" ); + $text .= "\n\ntwo"; + $page = new WikiPage( $page->getTitle() ); + $page->doEditContent( ContentHandler::makeContent( $text, $page->getTitle(), CONTENT_MODEL_WIKITEXT ), + "adding section two", 0, false, $user1 ); + + # now, try the rollback + $admin->addGroup( "sysop" ); #XXX: make the test user a sysop... + $token = $admin->getEditToken( array( $page->getTitle()->getPrefixedText(), $user1->getName() ), null ); + $errors = $page->doRollback( $user1->getName(), "testing revert", $token, false, $details, $admin ); + + if ( $errors ) { + $this->fail( "Rollback failed:\n" . print_r( $errors, true ) . ";\n" . print_r( $details, true ) ); + } + + $page = new WikiPage( $page->getTitle() ); + $this->assertEquals( $rev1->getSha1(), $page->getRevision()->getSha1(), + "rollback did not revert to the correct revision" ); + $this->assertEquals( "one", $page->getContent()->getNativeData() ); + } + + public static function provideGetAutosummary() { + return array( + array( + 'Hello there, world!', + '#REDIRECT [[Foo]]', + 0, + '/^Redirected page .*Foo/' + ), + + array( + null, + 'Hello world!', + EDIT_NEW, + '/^Created page .*Hello/' + ), + + array( + 'Hello there, world!', + '', + 0, + '/^Blanked/' + ), + + array( + 'Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut + labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et + ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.', + 'Hello world!', + 0, + '/^Replaced .*Hello/' + ), + + array( + 'foo', + 'bar', + 0, + '/^$/' + ), + ); + } + + /** + * @dataProvider provideGetAutoSummary + * @covers WikiPage::getAutosummary + */ + public function testGetAutosummary( $old, $new, $flags, $expected ) { + $this->hideDeprecated( "WikiPage::getAutosummary" ); + + $page = $this->newPage( "WikiPageTest_testGetAutosummary" ); + + $summary = $page->getAutosummary( $old, $new, $flags ); + + $this->assertTrue( (bool)preg_match( $expected, $summary ), + "Autosummary didn't match expected pattern $expected: $summary" ); + } + + public static function provideGetAutoDeleteReason() { + return array( + array( + array(), + false, + false + ), + + array( + array( + array( "first edit", null ), + ), + "/first edit.*only contributor/", + false + ), + + array( + array( + array( "first edit", null ), + array( "second edit", null ), + ), + "/second edit.*only contributor/", + true + ), + + array( + array( + array( "first edit", "127.0.2.22" ), + array( "second edit", "127.0.3.33" ), + ), + "/second edit/", + true + ), + + array( + array( + array( "first edit: " + . "Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam " + . " nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. " + . "At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea " + . "takimata sanctus est Lorem ipsum dolor sit amet.'", null ), + ), + '/first edit:.*\.\.\."/', + false + ), + + array( + array( + array( "first edit", "127.0.2.22" ), + array( "", "127.0.3.33" ), + ), + "/before blanking.*first edit/", + true + ), + + ); + } + + /** + * @dataProvider provideGetAutoDeleteReason + * @covers WikiPage::getAutoDeleteReason + */ + public function testGetAutoDeleteReason( $edits, $expectedResult, $expectedHistory ) { + global $wgUser; + + //NOTE: assume Help namespace to contain wikitext + $page = $this->newPage( "Help:WikiPageTest_testGetAutoDeleteReason" ); + + $c = 1; + + foreach ( $edits as $edit ) { + $user = new User(); + + if ( !empty( $edit[1] ) ) { + $user->setName( $edit[1] ); + } else { + $user = $wgUser; + } + + $content = ContentHandler::makeContent( $edit[0], $page->getTitle(), $page->getContentModel() ); + + $page->doEditContent( $content, "test edit $c", $c < 2 ? EDIT_NEW : 0, false, $user ); + + $c += 1; + } + + $reason = $page->getAutoDeleteReason( $hasHistory ); + + if ( is_bool( $expectedResult ) || is_null( $expectedResult ) ) { + $this->assertEquals( $expectedResult, $reason ); + } else { + $this->assertTrue( (bool)preg_match( $expectedResult, $reason ), + "Autosummary didn't match expected pattern $expectedResult: $reason" ); + } + + $this->assertEquals( $expectedHistory, $hasHistory, + "expected \$hasHistory to be " . var_export( $expectedHistory, true ) ); + + $page->doDeleteArticle( "done" ); + } + + public static function providePreSaveTransform() { + return array( + array( 'hello this is ~~~', + "hello this is [[Special:Contributions/127.0.0.1|127.0.0.1]]", + ), + array( 'hello \'\'this\'\' is ~~~', + 'hello \'\'this\'\' is ~~~', + ), + ); + } + + /** + * @dataProvider providePreSaveTransform + * @covers WikiPage::preSaveTransform + */ + public function testPreSaveTransform( $text, $expected ) { + $this->hideDeprecated( 'WikiPage::preSaveTransform' ); + $user = new User(); + $user->setName( "127.0.0.1" ); + + //NOTE: assume Help namespace to contain wikitext + $page = $this->newPage( "Help:WikiPageTest_testPreloadTransform" ); + $text = $page->preSaveTransform( $text, $user ); + + $this->assertEquals( $expected, $text ); + } +} diff --git a/tests/phpunit/includes/WikiPageTest_ContentHandlerUseDB.php b/tests/phpunit/includes/WikiPageTest_ContentHandlerUseDB.php new file mode 100644 index 00000000..2a723e85 --- /dev/null +++ b/tests/phpunit/includes/WikiPageTest_ContentHandlerUseDB.php @@ -0,0 +1,53 @@ +setMwGlobals( 'wgContentHandlerUseDB', false ); + + $dbw = wfGetDB( DB_MASTER ); + + $page_table = $dbw->tableName( 'page' ); + $revision_table = $dbw->tableName( 'revision' ); + $archive_table = $dbw->tableName( 'archive' ); + + if ( $dbw->fieldExists( $page_table, 'page_content_model' ) ) { + $dbw->query( "alter table $page_table drop column page_content_model" ); + $dbw->query( "alter table $revision_table drop column rev_content_model" ); + $dbw->query( "alter table $revision_table drop column rev_content_format" ); + $dbw->query( "alter table $archive_table drop column ar_content_model" ); + $dbw->query( "alter table $archive_table drop column ar_content_format" ); + } + } + + /** + * @covers WikiPage::getContentModel + */ + public function testGetContentModel() { + $page = $this->createPage( "WikiPageTest_testGetContentModel", "some text", CONTENT_MODEL_JAVASCRIPT ); + + $page = new WikiPage( $page->getTitle() ); + + // NOTE: since the content model is not recorded in the database, + // we expect to get the default, namely CONTENT_MODEL_WIKITEXT + $this->assertEquals( CONTENT_MODEL_WIKITEXT, $page->getContentModel() ); + } + + /** + * @covers WikiPage::getContentHandler + */ + public function testGetContentHandler() { + $page = $this->createPage( "WikiPageTest_testGetContentHandler", "some text", CONTENT_MODEL_JAVASCRIPT ); + + // NOTE: since the content model is not recorded in the database, + // we expect to get the default, namely CONTENT_MODEL_WIKITEXT + $page = new WikiPage( $page->getTitle() ); + $this->assertEquals( 'WikitextContentHandler', get_class( $page->getContentHandler() ) ); + } +} diff --git a/tests/phpunit/includes/XmlJsTest.php b/tests/phpunit/includes/XmlJsTest.php new file mode 100644 index 00000000..161468e2 --- /dev/null +++ b/tests/phpunit/includes/XmlJsTest.php @@ -0,0 +1,24 @@ +assertEquals( $value, $obj->value ); + } + + public function provideConstruction(){ + return array( + array( null ), + array( '' ), + ); + } + +} diff --git a/tests/phpunit/includes/XmlSelectTest.php b/tests/phpunit/includes/XmlSelectTest.php new file mode 100644 index 00000000..56d28b54 --- /dev/null +++ b/tests/phpunit/includes/XmlSelectTest.php @@ -0,0 +1,173 @@ +setMwGlobals( array( + 'wgWellFormedXml' => true, + ) ); + $this->select = new XmlSelect(); + } + + protected function tearDown() { + parent::tearDown(); + $this->select = null; + } + + /** + * @covers XmlSelect::__construct + */ + public function testConstructWithoutParameters() { + $this->assertEquals( '', $this->select->getHTML() ); + } + + /** + * Parameters are $name (false), $id (false), $default (false) + * @dataProvider provideConstructionParameters + * @covers XmlSelect::__construct + */ + public function testConstructParameters( $name, $id, $default, $expected ) { + $this->select = new XmlSelect( $name, $id, $default ); + $this->assertEquals( $expected, $this->select->getHTML() ); + } + + /** + * Provide parameters for testConstructParameters() which use three + * parameters: + * - $name (default: false) + * - $id (default: false) + * - $default (default: false) + * Provides a fourth parameters representing the expected HTML output + */ + public static function provideConstructionParameters() { + return array( + /** + * Values are set following a 3-bit Gray code where two successive + * values differ by only one value. + * See http://en.wikipedia.org/wiki/Gray_code + */ + # $name $id $default + array( false, false, false, '' ), + array( false, false, 'foo', '' ), + array( false, 'id', 'foo', '' ), + array( false, 'id', false, '' ), + array( 'name', 'id', false, '' ), + array( 'name', 'id', 'foo', '' ), + array( 'name', false, 'foo', '' ), + array( 'name', false, false, '' ), + ); + } + + /** + * @covers XmlSelect::addOption + */ + public function testAddOption() { + $this->select->addOption( 'foo' ); + $this->assertEquals( '', $this->select->getHTML() ); + } + + /** + * @covers XmlSelect::addOption + */ + public function testAddOptionWithDefault() { + $this->select->addOption( 'foo', true ); + $this->assertEquals( '', $this->select->getHTML() ); + } + + /** + * @covers XmlSelect::addOption + */ + public function testAddOptionWithFalse() { + $this->select->addOption( 'foo', false ); + $this->assertEquals( '', $this->select->getHTML() ); + } + + /** + * @covers XmlSelect::addOption + */ + public function testAddOptionWithValueZero() { + $this->select->addOption( 'foo', 0 ); + $this->assertEquals( '', $this->select->getHTML() ); + } + + /** + * @covers XmlSelect::setDefault + */ + public function testSetDefault() { + $this->select->setDefault( 'bar1' ); + $this->select->addOption( 'foo1' ); + $this->select->addOption( 'bar1' ); + $this->select->addOption( 'foo2' ); + $this->assertEquals( + '', $this->select->getHTML() ); + } + + /** + * Adding default later on should set the correct selection or + * raise an exception. + * To handle this, we need to render the options in getHtml() + * @covers XmlSelect::setDefault + */ + public function testSetDefaultAfterAddingOptions() { + $this->select->addOption( 'foo1' ); + $this->select->addOption( 'bar1' ); + $this->select->addOption( 'foo2' ); + $this->select->setDefault( 'bar1' ); # setting default after adding options + $this->assertEquals( + '', $this->select->getHTML() ); + } + + /** + * @covers XmlSelect::setAttribute + * @covers XmlSelect::getAttribute + */ + public function testGetAttributes() { + # create some attributes + $this->select->setAttribute( 'dummy', 0x777 ); + $this->select->setAttribute( 'string', 'euro €' ); + $this->select->setAttribute( 1911, 'razor' ); + + # verify we can retrieve them + $this->assertEquals( + $this->select->getAttribute( 'dummy' ), + 0x777 + ); + $this->assertEquals( + $this->select->getAttribute( 'string' ), + 'euro €' + ); + $this->assertEquals( + $this->select->getAttribute( 1911 ), + 'razor' + ); + + # inexistant keys should give us 'null' + $this->assertEquals( + $this->select->getAttribute( 'I DO NOT EXIT' ), + null + ); + + # verify string / integer + $this->assertEquals( + $this->select->getAttribute( '1911' ), + 'razor' + ); + $this->assertEquals( + $this->select->getAttribute( 'dummy' ), + 0x777 + ); + } +} diff --git a/tests/phpunit/includes/XmlTest.php b/tests/phpunit/includes/XmlTest.php new file mode 100644 index 00000000..8205029f --- /dev/null +++ b/tests/phpunit/includes/XmlTest.php @@ -0,0 +1,402 @@ +setNamespaces( array( + -2 => 'Media', + -1 => 'Special', + 0 => '', + 1 => 'Talk', + 2 => 'User', + 3 => 'User_talk', + 4 => 'MyWiki', + 5 => 'MyWiki_Talk', + 6 => 'File', + 7 => 'File_talk', + 8 => 'MediaWiki', + 9 => 'MediaWiki_talk', + 10 => 'Template', + 11 => 'Template_talk', + 100 => 'Custom', + 101 => 'Custom_talk', + ) ); + + $this->setMwGlobals( array( + 'wgLang' => $langObj, + 'wgWellFormedXml' => true, + ) ); + } + + /** + * @covers Xml::expandAttributes + */ + public function testExpandAttributes() { + $this->assertNull( Xml::expandAttributes( null ), + 'Converting a null list of attributes' + ); + $this->assertEquals( '', Xml::expandAttributes( array() ), + 'Converting an empty list of attributes' + ); + } + + /** + * @covers Xml::expandAttributes + */ + public function testExpandAttributesException() { + $this->setExpectedException( 'MWException' ); + Xml::expandAttributes( 'string' ); + } + + /** + * @covers Xml::element + */ + public function testElementOpen() { + $this->assertEquals( + '', + Xml::element( 'element', null, null ), + 'Opening element with no attributes' + ); + } + + /** + * @covers Xml::element + */ + public function testElementEmpty() { + $this->assertEquals( + '', + Xml::element( 'element', null, '' ), + 'Terminated empty element' + ); + } + + /** + * @covers Xml::input + */ + public function testElementInputCanHaveAValueOfZero() { + $this->assertEquals( + '', + Xml::input( 'name', false, 0 ), + 'Input with a value of 0 (bug 23797)' + ); + } + + /** + * @covers Xml::element + */ + public function testElementEscaping() { + $this->assertEquals( + 'hello <there> you & you', + Xml::element( 'element', null, 'hello you & you' ), + 'Element with no attributes and content that needs escaping' + ); + } + + /** + * @covers Xml::escapeTagsOnly + */ + public function testEscapeTagsOnly() { + $this->assertEquals( '"><', Xml::escapeTagsOnly( '"><' ), + 'replace " > and < with their HTML entitites' + ); + } + + /** + * @covers Xml::element + */ + public function testElementAttributes() { + $this->assertEquals( + '="<>">', + Xml::element( 'element', array( 'key' => 'value', '<>' => '<>' ), null ), + 'Element attributes, keys are not escaped' + ); + } + + /** + * @covers Xml::openElement + */ + public function testOpenElement() { + $this->assertEquals( + '', + Xml::openElement( 'element', array( 'k' => 'v' ) ), + 'openElement() shortcut' + ); + } + + /** + * @covers Xml::closeElement + */ + public function testCloseElement() { + $this->assertEquals( '', Xml::closeElement( 'element' ), 'closeElement() shortcut' ); + } + + /** + * @covers Xml::dateMenu + */ + public function testDateMenu() { + $curYear = intval( gmdate( 'Y' ) ); + $prevYear = $curYear - 1; + + $curMonth = intval( gmdate( 'n' ) ); + $prevMonth = $curMonth - 1; + if ( $prevMonth == 0 ) { + $prevMonth = 12; + } + $nextMonth = $curMonth + 1; + if ( $nextMonth == 13 ) { + $nextMonth = 1; + } + + $this->assertEquals( + ' ', + Xml::dateMenu( 2011, 02 ), + "Date menu for february 2011" + ); + $this->assertEquals( + ' ', + Xml::dateMenu( 2011, -1 ), + "Date menu with negative month for 'All'" + ); + $this->assertEquals( + Xml::dateMenu( $curYear, $curMonth ), + Xml::dateMenu( '', $curMonth ), + "Date menu year is the current one when not specified" + ); + + $wantedYear = $nextMonth == 1 ? $curYear : $prevYear; + $this->assertEquals( + Xml::dateMenu( $wantedYear, $nextMonth ), + Xml::dateMenu( '', $nextMonth ), + "Date menu next month is 11 months ago" + ); + + $this->assertEquals( + ' ', + Xml::dateMenu( '', '' ), + "Date menu with neither year or month" + ); + } + + /** + * @covers Xml::textarea + */ + public function testTextareaNoContent() { + $this->assertEquals( + '', + Xml::textarea( 'name', '' ), + 'textarea() with not content' + ); + } + + /** + * @covers Xml::textarea + */ + public function testTextareaAttribs() { + $this->assertEquals( + '', + Xml::textarea( 'name', '', 20, 10 ), + 'textarea() with custom attribs' + ); + } + + /** + * @covers Xml::label + */ + public function testLabelCreation() { + $this->assertEquals( + '', + Xml::label( 'name', 'id' ), + 'label() with no attribs' + ); + } + + /** + * @covers Xml::label + */ + public function testLabelAttributeCanOnlyBeClassOrTitle() { + $this->assertEquals( + '', + Xml::label( 'name', 'id', array( 'generated' => true ) ), + 'label() can not be given a generated attribute' + ); + $this->assertEquals( + '', + Xml::label( 'name', 'id', array( 'class' => 'nice' ) ), + 'label() can get a class attribute' + ); + $this->assertEquals( + '', + Xml::label( 'name', 'id', array( 'title' => 'nice tooltip' ) ), + 'label() can get a title attribute' + ); + $this->assertEquals( + '', + Xml::label( 'name', 'id', array( + 'generated' => true, + 'class' => 'nice', + 'title' => 'nice tooltip', + 'anotherattr' => 'value', + ) + ), + 'label() skip all attributes but "class" and "title"' + ); + } + + /** + * @covers Xml::languageSelector + */ + public function testLanguageSelector() { + $select = Xml::languageSelector( 'en', true, null, + array( 'id' => 'testlang' ), wfMessage( 'yourlanguage' ) ); + $this->assertEquals( + '', + $select[0] + ); + } + + /** + * @covers Xml::escapeJsString + */ + public function testEscapeJsStringSpecialChars() { + $this->assertEquals( + '\\\\\r\n', + Xml::escapeJsString( "\\\r\n" ), + 'escapeJsString() with special characters' + ); + } + + /** + * @covers Xml::encodeJsVar + */ + public function testEncodeJsVarBoolean() { + $this->assertEquals( + 'true', + Xml::encodeJsVar( true ), + 'encodeJsVar() with boolean' + ); + } + + /** + * @covers Xml::encodeJsVar + */ + public function testEncodeJsVarNull() { + $this->assertEquals( + 'null', + Xml::encodeJsVar( null ), + 'encodeJsVar() with null' + ); + } + + /** + * @covers Xml::encodeJsVar + */ + public function testEncodeJsVarArray() { + $this->assertEquals( + '["a",1]', + Xml::encodeJsVar( array( 'a', 1 ) ), + 'encodeJsVar() with array' + ); + $this->assertEquals( + '{"a":"a","b":1}', + Xml::encodeJsVar( array( 'a' => 'a', 'b' => 1 ) ), + 'encodeJsVar() with associative array' + ); + } + + /** + * @covers Xml::encodeJsVar + */ + public function testEncodeJsVarObject() { + $this->assertEquals( + '{"a":"a","b":1}', + Xml::encodeJsVar( (object)array( 'a' => 'a', 'b' => 1 ) ), + 'encodeJsVar() with object' + ); + } + + /** + * @covers Xml::encodeJsVar + */ + public function testEncodeJsVarInt() { + $this->assertEquals( + '123456', + Xml::encodeJsVar( 123456 ), + 'encodeJsVar() with int' + ); + } + + /** + * @covers Xml::encodeJsVar + */ + public function testEncodeJsVarFloat() { + $this->assertEquals( + '1.23456', + Xml::encodeJsVar( 1.23456 ), + 'encodeJsVar() with float' + ); + } + + /** + * @covers Xml::encodeJsVar + */ + public function testEncodeJsVarIntString() { + $this->assertEquals( + '"123456"', + Xml::encodeJsVar( '123456' ), + 'encodeJsVar() with int-like string' + ); + } + + /** + * @covers Xml::encodeJsVar + */ + public function testEncodeJsVarFloatString() { + $this->assertEquals( + '"1.23456"', + Xml::encodeJsVar( '1.23456' ), + 'encodeJsVar() with float-like string' + ); + } +} diff --git a/tests/phpunit/includes/XmlTypeCheckTest.php b/tests/phpunit/includes/XmlTypeCheckTest.php new file mode 100644 index 00000000..8d6f1ed7 --- /dev/null +++ b/tests/phpunit/includes/XmlTypeCheckTest.php @@ -0,0 +1,30 @@ +"; + const MAL_FORMED_XML = ""; + + /** + * @covers XMLTypeCheck::newFromString + * @covers XMLTypeCheck::getRootElement + */ + public function testWellFormedXML() { + $testXML = XmlTypeCheck::newFromString( self::WELL_FORMED_XML ); + $this->assertTrue( $testXML->wellFormed ); + $this->assertEquals( 'root', $testXML->getRootElement() ); + } + + /** + * @covers XMLTypeCheck::newFromString + */ + public function testMalFormedXML() { + $testXML = XmlTypeCheck::newFromString( self::MAL_FORMED_XML ); + $this->assertFalse( $testXML->wellFormed ); + } + +} diff --git a/tests/phpunit/includes/ZipDirectoryReaderTest.php b/tests/phpunit/includes/ZipDirectoryReaderTest.php new file mode 100644 index 00000000..2627a417 --- /dev/null +++ b/tests/phpunit/includes/ZipDirectoryReaderTest.php @@ -0,0 +1,85 @@ +zipDir = __DIR__ . '/../data/zip'; + } + + function zipCallback( $entry ) { + $this->entries[] = $entry; + } + + function readZipAssertError( $file, $error, $assertMessage ) { + $this->entries = array(); + $status = ZipDirectoryReader::read( "{$this->zipDir}/$file", array( $this, 'zipCallback' ) ); + $this->assertTrue( $status->hasMessage( $error ), $assertMessage ); + } + + function readZipAssertSuccess( $file, $assertMessage ) { + $this->entries = array(); + $status = ZipDirectoryReader::read( "{$this->zipDir}/$file", array( $this, 'zipCallback' ) ); + $this->assertTrue( $status->isOK(), $assertMessage ); + } + + public function testEmpty() { + $this->readZipAssertSuccess( 'empty.zip', 'Empty zip' ); + } + + public function testMultiDisk0() { + $this->readZipAssertError( 'split.zip', 'zip-unsupported', + 'Split zip error' ); + } + + public function testNoSignature() { + $this->readZipAssertError( 'nosig.zip', 'zip-wrong-format', + 'No signature should give "wrong format" error' ); + } + + public function testSimple() { + $this->readZipAssertSuccess( 'class.zip', 'Simple ZIP' ); + $this->assertEquals( $this->entries, array( array( + 'name' => 'Class.class', + 'mtime' => '20010115000000', + 'size' => 1, + ) ) ); + } + + public function testBadCentralEntrySignature() { + $this->readZipAssertError( 'wrong-central-entry-sig.zip', 'zip-bad', + 'Bad central entry error' ); + } + + public function testTrailingBytes() { + $this->readZipAssertError( 'trail.zip', 'zip-bad', + 'Trailing bytes error' ); + } + + public function testWrongCDStart() { + $this->readZipAssertError( 'wrong-cd-start-disk.zip', 'zip-unsupported', + 'Wrong CD start disk error' ); + } + + + public function testCentralDirectoryGap() { + $this->readZipAssertError( 'cd-gap.zip', 'zip-bad', + 'CD gap error' ); + } + + public function testCentralDirectoryTruncated() { + $this->readZipAssertError( 'cd-truncated.zip', 'zip-bad', + 'CD truncated error (should hit unpack() overrun)' ); + } + + public function testLooksLikeZip64() { + $this->readZipAssertError( 'looks-like-zip64.zip', 'zip-unsupported', + 'A file which looks like ZIP64 but isn\'t, should give error' ); + } +} diff --git a/tests/phpunit/includes/api/ApiAccountCreationTest.php b/tests/phpunit/includes/api/ApiAccountCreationTest.php new file mode 100644 index 00000000..68f80ac9 --- /dev/null +++ b/tests/phpunit/includes/api/ApiAccountCreationTest.php @@ -0,0 +1,159 @@ +setMwGlobals( array( 'wgEnableEmail' => true ) ); + } + + /** + * Test the account creation API with a valid request. Also + * make sure the new account can log in and is valid. + * + * This test does multiple API requests so it might end up being + * a bit slow. Raise the default timeout. + * @group medium + */ + public function testValid() { + global $wgServer; + + if ( !isset( $wgServer ) ) { + $this->markTestIncomplete( 'This test needs $wgServer to be set in LocalSettings.php' ); + } + + $password = User::randomPassword(); + + $ret = $this->doApiRequest( array( + 'action' => 'createaccount', + 'name' => 'Apitestnew', + 'password' => $password, + 'email' => 'test@domain.test', + 'realname' => 'Test Name' + ) ); + + $result = $ret[0]; + $this->assertNotInternalType( 'bool', $result ); + $this->assertNotInternalType( 'null', $result['createaccount'] ); + + // Should first ask for token. + $a = $result['createaccount']; + $this->assertEquals( 'needtoken', $a['result'] ); + $token = $a['token']; + + // Finally create the account + $ret = $this->doApiRequest( + array( + 'action' => 'createaccount', + 'name' => 'Apitestnew', + 'password' => $password, + 'token' => $token, + 'email' => 'test@domain.test', + 'realname' => 'Test Name' + ), + $ret[2] + ); + + $result = $ret[0]; + $this->assertNotInternalType( 'bool', $result ); + $this->assertEquals( 'success', $result['createaccount']['result'] ); + + // Try logging in with the new user. + $ret = $this->doApiRequest( array( + 'action' => 'login', + 'lgname' => 'Apitestnew', + 'lgpassword' => $password, + ) ); + + $result = $ret[0]; + $this->assertNotInternalType( 'bool', $result ); + $this->assertNotInternalType( 'null', $result['login'] ); + + $a = $result['login']['result']; + $this->assertEquals( 'NeedToken', $a ); + $token = $result['login']['token']; + + $ret = $this->doApiRequest( + array( + 'action' => 'login', + 'lgtoken' => $token, + 'lgname' => 'Apitestnew', + 'lgpassword' => $password, + ), + $ret[2] + ); + + $result = $ret[0]; + + $this->assertNotInternalType( 'bool', $result ); + $a = $result['login']['result']; + + $this->assertEquals( 'Success', $a ); + + // log out to destroy the session + $ret = $this->doApiRequest( + array( + 'action' => 'logout', + ), + $ret[2] + ); + $this->assertEquals( array(), $ret[0] ); + } + + /** + * Make sure requests with no names are invalid. + * @expectedException UsageException + */ + public function testNoName() { + $this->doApiRequest( array( + 'action' => 'createaccount', + 'token' => LoginForm::getCreateaccountToken(), + 'password' => 'password', + ) ); + } + + /** + * Make sure requests with no password are invalid. + * @expectedException UsageException + */ + public function testNoPassword() { + $this->doApiRequest( array( + 'action' => 'createaccount', + 'name' => 'testName', + 'token' => LoginForm::getCreateaccountToken(), + ) ); + } + + /** + * Make sure requests with existing users are invalid. + * @expectedException UsageException + */ + public function testExistingUser() { + $this->doApiRequest( array( + 'action' => 'createaccount', + 'name' => 'Apitestsysop', + 'token' => LoginForm::getCreateaccountToken(), + 'password' => 'password', + 'email' => 'test@domain.test', + ) ); + } + + /** + * Make sure requests with invalid emails are invalid. + * @expectedException UsageException + */ + public function testInvalidEmail() { + $this->doApiRequest( array( + 'action' => 'createaccount', + 'name' => 'Test User', + 'token' => LoginForm::getCreateaccountToken(), + 'password' => 'password', + 'email' => 'invalid', + ) ); + } +} diff --git a/tests/phpunit/includes/api/ApiBlockTest.php b/tests/phpunit/includes/api/ApiBlockTest.php new file mode 100644 index 00000000..8afb748a --- /dev/null +++ b/tests/phpunit/includes/api/ApiBlockTest.php @@ -0,0 +1,95 @@ +doLogin(); + } + + function getTokens() { + return $this->getTokenList( self::$users['sysop'] ); + } + + function addDBData() { + $user = User::newFromName( 'UTApiBlockee' ); + + if ( $user->getId() == 0 ) { + $user->addToDatabase(); + $user->setPassword( 'UTApiBlockeePassword' ); + + $user->saveSettings(); + } + } + + /** + * This test has probably always been broken and use an invalid token + * Bug tracking brokenness is https://bugzilla.wikimedia.org/35646 + * + * Root cause is https://gerrit.wikimedia.org/r/3434 + * Which made the Block/Unblock API to actually verify the token + * previously always considered valid (bug 34212). + */ + public function testMakeNormalBlock() { + $tokens = $this->getTokens(); + + $user = User::newFromName( 'UTApiBlockee' ); + + if ( !$user->getId() ) { + $this->markTestIncomplete( "The user UTApiBlockee does not exist" ); + } + + if ( !array_key_exists( 'blocktoken', $tokens ) ) { + $this->markTestIncomplete( "No block token found" ); + } + + $this->doApiRequest( array( + 'action' => 'block', + 'user' => 'UTApiBlockee', + 'reason' => 'Some reason', + 'token' => $tokens['blocktoken'] ), null, false, self::$users['sysop']->user ); + + $block = Block::newFromTarget( 'UTApiBlockee' ); + + $this->assertTrue( !is_null( $block ), 'Block is valid' ); + + $this->assertEquals( 'UTApiBlockee', (string)$block->getTarget() ); + $this->assertEquals( 'Some reason', $block->mReason ); + $this->assertEquals( 'infinity', $block->mExpiry ); + } + + /** + * Attempting to block without a token should give a UsageException with + * error message: + * "The token parameter must be set" + * + * @dataProvider provideBlockUnblockAction + * @expectedException UsageException + */ + public function testBlockingActionWithNoToken( $action ) { + $this->doApiRequest( + array( + 'action' => $action, + 'user' => 'UTApiBlockee', + 'reason' => 'Some reason', + ), + null, + false, + self::$users['sysop']->user + ); + } + + /** + * Just provide the 'block' and 'unblock' action to test both API calls + */ + public static function provideBlockUnblockAction() { + return array( + array( 'block' ), + array( 'unblock' ), + ); + } +} diff --git a/tests/phpunit/includes/api/ApiEditPageTest.php b/tests/phpunit/includes/api/ApiEditPageTest.php new file mode 100644 index 00000000..0c49b12b --- /dev/null +++ b/tests/phpunit/includes/api/ApiEditPageTest.php @@ -0,0 +1,417 @@ +resetNamespaces(); # reset namespace cache + + $this->doLogin(); + } + + public function tearDown() { + global $wgExtraNamespaces, $wgNamespaceContentModels, $wgContentHandlers, $wgContLang; + + unset( $wgExtraNamespaces[12312] ); + unset( $wgExtraNamespaces[12313] ); + + unset( $wgNamespaceContentModels[12312] ); + unset( $wgContentHandlers["testing"] ); + + MWNamespace::getCanonicalNamespaces( true ); # reset namespace cache + $wgContLang->resetNamespaces(); # reset namespace cache + + parent::tearDown(); + } + + public function testEdit() { + $name = 'Help:ApiEditPageTest_testEdit'; // assume Help namespace to default to wikitext + + // -- test new page -------------------------------------------- + $apiResult = $this->doApiRequestWithToken( array( + 'action' => 'edit', + 'title' => $name, + 'text' => 'some text', + ) ); + $apiResult = $apiResult[0]; + + // Validate API result data + $this->assertArrayHasKey( 'edit', $apiResult ); + $this->assertArrayHasKey( 'result', $apiResult['edit'] ); + $this->assertEquals( 'Success', $apiResult['edit']['result'] ); + + $this->assertArrayHasKey( 'new', $apiResult['edit'] ); + $this->assertArrayNotHasKey( 'nochange', $apiResult['edit'] ); + + $this->assertArrayHasKey( 'pageid', $apiResult['edit'] ); + + // -- test existing page, no change ---------------------------- + $data = $this->doApiRequestWithToken( array( + 'action' => 'edit', + 'title' => $name, + 'text' => 'some text', + ) ); + + $this->assertEquals( 'Success', $data[0]['edit']['result'] ); + + $this->assertArrayNotHasKey( 'new', $data[0]['edit'] ); + $this->assertArrayHasKey( 'nochange', $data[0]['edit'] ); + + // -- test existing page, with change -------------------------- + $data = $this->doApiRequestWithToken( array( + 'action' => 'edit', + 'title' => $name, + 'text' => 'different text' + ) ); + + $this->assertEquals( 'Success', $data[0]['edit']['result'] ); + + $this->assertArrayNotHasKey( 'new', $data[0]['edit'] ); + $this->assertArrayNotHasKey( 'nochange', $data[0]['edit'] ); + + $this->assertArrayHasKey( 'oldrevid', $data[0]['edit'] ); + $this->assertArrayHasKey( 'newrevid', $data[0]['edit'] ); + $this->assertNotEquals( + $data[0]['edit']['newrevid'], + $data[0]['edit']['oldrevid'], + "revision id should change after edit" + ); + } + + public function testNonTextEdit() { + $name = 'Dummy:ApiEditPageTest_testNonTextEdit'; + $data = serialize( 'some bla bla text' ); + + // -- test new page -------------------------------------------- + $apiResult = $this->doApiRequestWithToken( array( + 'action' => 'edit', + 'title' => $name, + 'text' => $data, ) ); + $apiResult = $apiResult[0]; + + // Validate API result data + $this->assertArrayHasKey( 'edit', $apiResult ); + $this->assertArrayHasKey( 'result', $apiResult['edit'] ); + $this->assertEquals( 'Success', $apiResult['edit']['result'] ); + + $this->assertArrayHasKey( 'new', $apiResult['edit'] ); + $this->assertArrayNotHasKey( 'nochange', $apiResult['edit'] ); + + $this->assertArrayHasKey( 'pageid', $apiResult['edit'] ); + + // validate resulting revision + $page = WikiPage::factory( Title::newFromText( $name ) ); + $this->assertEquals( "testing", $page->getContentModel() ); + $this->assertEquals( $data, $page->getContent()->serialize() ); + } + + public static function provideEditAppend() { + return array( + array( #0: append + 'foo', 'append', 'bar', "foobar" + ), + array( #1: prepend + 'foo', 'prepend', 'bar', "barfoo" + ), + array( #2: append to empty page + '', 'append', 'foo', "foo" + ), + array( #3: prepend to empty page + '', 'prepend', 'foo', "foo" + ), + array( #4: append to non-existing page + null, 'append', 'foo', "foo" + ), + array( #5: prepend to non-existing page + null, 'prepend', 'foo', "foo" + ), + ); + } + + /** + * @dataProvider provideEditAppend + */ + public function testEditAppend( $text, $op, $append, $expected ) { + static $count = 0; + $count++; + + // assume NS_HELP defaults to wikitext + $name = "Help:ApiEditPageTest_testEditAppend_$count"; + + // -- create page (or not) ----------------------------------------- + if ( $text !== null ) { + if ( $text === '' ) { + // can't create an empty page, so create it with some content + $this->doApiRequestWithToken( array( + 'action' => 'edit', + 'title' => $name, + 'text' => '(dummy)', ) ); + } + + list( $re ) = $this->doApiRequestWithToken( array( + 'action' => 'edit', + 'title' => $name, + 'text' => $text, ) ); + + $this->assertEquals( 'Success', $re['edit']['result'] ); // sanity + } + + // -- try append/prepend -------------------------------------------- + list( $re ) = $this->doApiRequestWithToken( array( + 'action' => 'edit', + 'title' => $name, + $op . 'text' => $append, ) ); + + $this->assertEquals( 'Success', $re['edit']['result'] ); + + // -- validate ----------------------------------------------------- + $page = new WikiPage( Title::newFromText( $name ) ); + $content = $page->getContent(); + $this->assertNotNull( $content, 'Page should have been created' ); + + $text = $content->getNativeData(); + + $this->assertEquals( $expected, $text ); + } + + /** + * Test editing of sections + */ + public function testEditSection() { + $name = 'Help:ApiEditPageTest_testEditSection'; + $page = WikiPage::factory( Title::newFromText( $name ) ); + $text = "==section 1==\ncontent 1\n==section 2==\ncontent2"; + // Preload the page with some text + $page->doEditContent( ContentHandler::makeContent( $text, $page->getTitle() ), 'summary' ); + + list( $re ) = $this->doApiRequestWithToken( array( + 'action' => 'edit', + 'title' => $name, + 'section' => '1', + 'text' => "==section 1==\nnew content 1", + ) ); + $this->assertEquals( 'Success', $re['edit']['result'] ); + $newtext = WikiPage::factory( Title::newFromText( $name) )->getContent( Revision::RAW )->getNativeData(); + $this->assertEquals( $newtext, "==section 1==\nnew content 1\n\n==section 2==\ncontent2" ); + + // Test that we raise a 'nosuchsection' error + try { + $this->doApiRequestWithToken( array( + 'action' => 'edit', + 'title' => $name, + 'section' => '9999', + 'text' => 'text', + ) ); + $this->fail( "Should have raised a UsageException" ); + } catch ( UsageException $e ) { + $this->assertEquals( $e->getCodeString(), 'nosuchsection' ); + } + } + + /** + * Test action=edit§ion=new + * Run it twice so we test adding a new section on a + * page that doesn't exist (bug 52830) and one that + * does exist + */ + public function testEditNewSection() { + $name = 'Help:ApiEditPageTest_testEditNewSection'; + + // Test on a page that does not already exist + $this->assertFalse( Title::newFromText( $name )->exists() ); + list( $re ) = $this->doApiRequestWithToken( array( + 'action' => 'edit', + 'title' => $name, + 'section' => 'new', + 'text' => 'test', + 'summary' => 'header', + )); + + $this->assertEquals( 'Success', $re['edit']['result'] ); + // Check the page text is correct + $text = WikiPage::factory( Title::newFromText( $name ) )->getContent( Revision::RAW )->getNativeData(); + $this->assertEquals( $text, "== header ==\n\ntest" ); + + // Now on one that does + $this->assertTrue( Title::newFromText( $name )->exists() ); + list( $re2 ) = $this->doApiRequestWithToken( array( + 'action' => 'edit', + 'title' => $name, + 'section' => 'new', + 'text' => 'test', + 'summary' => 'header', + )); + + $this->assertEquals( 'Success', $re2['edit']['result'] ); + $text = WikiPage::factory( Title::newFromText( $name ) )->getContent( Revision::RAW )->getNativeData(); + $this->assertEquals( $text, "== header ==\n\ntest\n\n== header ==\n\ntest" ); + } + + public function testEditConflict() { + static $count = 0; + $count++; + + // assume NS_HELP defaults to wikitext + $name = "Help:ApiEditPageTest_testEditConflict_$count"; + $title = Title::newFromText( $name ); + + $page = WikiPage::factory( $title ); + + // base edit + $page->doEditContent( new WikitextContent( "Foo" ), + "testing 1", EDIT_NEW, false, self::$users['sysop']->user ); + $this->forceRevisionDate( $page, '20120101000000' ); + $baseTime = $page->getRevision()->getTimestamp(); + + // conflicting edit + $page->doEditContent( new WikitextContent( "Foo bar" ), + "testing 2", EDIT_UPDATE, $page->getLatest(), self::$users['uploader']->user ); + $this->forceRevisionDate( $page, '20120101020202' ); + + // try to save edit, expect conflict + try { + $this->doApiRequestWithToken( array( + 'action' => 'edit', + 'title' => $name, + 'text' => 'nix bar!', + 'basetimestamp' => $baseTime, + ), null, self::$users['sysop']->user ); + + $this->fail( 'edit conflict expected' ); + } catch ( UsageException $ex ) { + $this->assertEquals( 'editconflict', $ex->getCodeString() ); + } + } + + public function testEditConflict_redirect() { + static $count = 0; + $count++; + + // assume NS_HELP defaults to wikitext + $name = "Help:ApiEditPageTest_testEditConflict_redirect_$count"; + $title = Title::newFromText( $name ); + $page = WikiPage::factory( $title ); + + $rname = "Help:ApiEditPageTest_testEditConflict_redirect_r$count"; + $rtitle = Title::newFromText( $rname ); + $rpage = WikiPage::factory( $rtitle ); + + // base edit for content + $page->doEditContent( new WikitextContent( "Foo" ), + "testing 1", EDIT_NEW, false, self::$users['sysop']->user ); + $this->forceRevisionDate( $page, '20120101000000' ); + $baseTime = $page->getRevision()->getTimestamp(); + + // base edit for redirect + $rpage->doEditContent( new WikitextContent( "#REDIRECT [[$name]]" ), + "testing 1", EDIT_NEW, false, self::$users['sysop']->user ); + $this->forceRevisionDate( $rpage, '20120101000000' ); + + // conflicting edit to redirect + $rpage->doEditContent( new WikitextContent( "#REDIRECT [[$name]]\n\n[[Category:Test]]" ), + "testing 2", EDIT_UPDATE, $page->getLatest(), self::$users['uploader']->user ); + $this->forceRevisionDate( $rpage, '20120101020202' ); + + // try to save edit; should work, because we follow the redirect + list( $re, , ) = $this->doApiRequestWithToken( array( + 'action' => 'edit', + 'title' => $rname, + 'text' => 'nix bar!', + 'basetimestamp' => $baseTime, + 'redirect' => true, + ), null, self::$users['sysop']->user ); + + $this->assertEquals( 'Success', $re['edit']['result'], + "no edit conflict expected when following redirect" ); + + // try again, without following the redirect. Should fail. + try { + $this->doApiRequestWithToken( array( + 'action' => 'edit', + 'title' => $rname, + 'text' => 'nix bar!', + 'basetimestamp' => $baseTime, + ), null, self::$users['sysop']->user ); + + $this->fail( 'edit conflict expected' ); + } catch ( UsageException $ex ) { + $this->assertEquals( 'editconflict', $ex->getCodeString() ); + } + } + + public function testEditConflict_bug41990() { + static $count = 0; + $count++; + + /* + * bug 41990: if the target page has a newer revision than the redirect, then editing the + * redirect while specifying 'redirect' and *not* specifying 'basetimestamp' erroneously + * caused an edit conflict to be detected. + */ + + // assume NS_HELP defaults to wikitext + $name = "Help:ApiEditPageTest_testEditConflict_redirect_bug41990_$count"; + $title = Title::newFromText( $name ); + $page = WikiPage::factory( $title ); + + $rname = "Help:ApiEditPageTest_testEditConflict_redirect_bug41990_r$count"; + $rtitle = Title::newFromText( $rname ); + $rpage = WikiPage::factory( $rtitle ); + + // base edit for content + $page->doEditContent( new WikitextContent( "Foo" ), + "testing 1", EDIT_NEW, false, self::$users['sysop']->user ); + $this->forceRevisionDate( $page, '20120101000000' ); + + // base edit for redirect + $rpage->doEditContent( new WikitextContent( "#REDIRECT [[$name]]" ), + "testing 1", EDIT_NEW, false, self::$users['sysop']->user ); + $this->forceRevisionDate( $rpage, '20120101000000' ); + $baseTime = $rpage->getRevision()->getTimestamp(); + + // new edit to content + $page->doEditContent( new WikitextContent( "Foo bar" ), + "testing 2", EDIT_UPDATE, $page->getLatest(), self::$users['uploader']->user ); + $this->forceRevisionDate( $rpage, '20120101020202' ); + + // try to save edit; should work, following the redirect. + list( $re, , ) = $this->doApiRequestWithToken( array( + 'action' => 'edit', + 'title' => $rname, + 'text' => 'nix bar!', + 'redirect' => true, + ), null, self::$users['sysop']->user ); + + $this->assertEquals( 'Success', $re['edit']['result'], + "no edit conflict expected here" ); + } + + protected function forceRevisionDate( WikiPage $page, $timestamp ) { + $dbw = wfGetDB( DB_MASTER ); + + $dbw->update( 'revision', + array( 'rev_timestamp' => $dbw->timestamp( $timestamp ) ), + array( 'rev_id' => $page->getLatest() ) ); + + $page->clear(); + } +} diff --git a/tests/phpunit/includes/api/ApiOptionsTest.php b/tests/phpunit/includes/api/ApiOptionsTest.php new file mode 100644 index 00000000..ad1e73ab --- /dev/null +++ b/tests/phpunit/includes/api/ApiOptionsTest.php @@ -0,0 +1,420 @@ + 'success' ); + + protected function setUp() { + parent::setUp(); + + $this->mUserMock = $this->getMockBuilder( 'User' ) + ->disableOriginalConstructor() + ->getMock(); + + // Set up groups and rights + $this->mUserMock->expects( $this->any() ) + ->method( 'getEffectiveGroups' )->will( $this->returnValue( array( '*', 'user' ) ) ); + $this->mUserMock->expects( $this->any() ) + ->method( 'isAllowed' )->will( $this->returnValue( true ) ); + + // Set up callback for User::getOptionKinds + $this->mUserMock->expects( $this->any() ) + ->method( 'getOptionKinds' )->will( $this->returnCallback( array( $this, 'getOptionKinds' ) ) ); + + // Create a new context + $this->mContext = new DerivativeContext( new RequestContext() ); + $this->mContext->getContext()->setTitle( Title::newFromText( 'Test' ) ); + $this->mContext->setUser( $this->mUserMock ); + + $main = new ApiMain( $this->mContext ); + + // Empty session + $this->mSession = array(); + + $this->mTested = new ApiOptions( $main, 'options' ); + + global $wgHooks; + if ( !isset( $wgHooks['GetPreferences'] ) ) { + $wgHooks['GetPreferences'] = array(); + } + $this->mOldGetPreferencesHooks = $wgHooks['GetPreferences']; + $wgHooks['GetPreferences'][] = array( $this, 'hookGetPreferences' ); + } + + protected function tearDown() { + global $wgHooks; + + if ( $this->mOldGetPreferencesHooks !== false ) { + $wgHooks['GetPreferences'] = $this->mOldGetPreferencesHooks; + $this->mOldGetPreferencesHooks = false; + } + + parent::tearDown(); + } + + public function hookGetPreferences( $user, &$preferences ) { + $preferences = array(); + + foreach ( array( 'name', 'willBeNull', 'willBeEmpty', 'willBeHappy' ) as $k ) { + $preferences[$k] = array( + 'type' => 'text', + 'section' => 'test', + 'label' => ' ', + ); + } + + $preferences['testmultiselect'] = array( + 'type' => 'multiselect', + 'options' => array( + 'Test' => array( + 'Some HTML here for option 1' => 'opt1', + 'Some HTML here for option 2' => 'opt2', + 'Some HTML here for option 3' => 'opt3', + 'Some HTML here for option 4' => 'opt4', + ), + ), + 'section' => 'test', + 'label' => ' ', + 'prefix' => 'testmultiselect-', + 'default' => array(), + ); + + return true; + } + + public function getOptionKinds( IContextSource $context, $options = null ) { + // Match with above. + $kinds = array( + 'name' => 'registered', + 'willBeNull' => 'registered', + 'willBeEmpty' => 'registered', + 'willBeHappy' => 'registered', + 'testmultiselect-opt1' => 'registered-multiselect', + 'testmultiselect-opt2' => 'registered-multiselect', + 'testmultiselect-opt3' => 'registered-multiselect', + 'testmultiselect-opt4' => 'registered-multiselect', + ); + + if ( $options === null ) { + return $kinds; + } + + $mapping = array(); + foreach ( $options as $key => $value ) { + if ( isset( $kinds[$key] ) ) { + $mapping[$key] = $kinds[$key]; + } elseif ( substr( $key, 0, 7 ) === 'userjs-' ) { + $mapping[$key] = 'userjs'; + } else { + $mapping[$key] = 'unused'; + } + } + + return $mapping; + } + + private function getSampleRequest( $custom = array() ) { + $request = array( + 'token' => '123ABC', + 'change' => null, + 'optionname' => null, + 'optionvalue' => null, + ); + + return array_merge( $request, $custom ); + } + + private function executeQuery( $request ) { + $this->mContext->setRequest( new FauxRequest( $request, true, $this->mSession ) ); + $this->mTested->execute(); + + return $this->mTested->getResult()->getData(); + } + + /** + * @expectedException UsageException + */ + public function testNoToken() { + $request = $this->getSampleRequest( array( 'token' => null ) ); + + $this->executeQuery( $request ); + } + + public function testAnon() { + $this->mUserMock->expects( $this->once() ) + ->method( 'isAnon' ) + ->will( $this->returnValue( true ) ); + + try { + $request = $this->getSampleRequest(); + + $this->executeQuery( $request ); + } catch ( UsageException $e ) { + $this->assertEquals( 'notloggedin', $e->getCodeString() ); + $this->assertEquals( 'Anonymous users cannot change preferences', $e->getMessage() ); + + return; + } + $this->fail( "UsageException was not thrown" ); + } + + public function testNoOptionname() { + try { + $request = $this->getSampleRequest( array( 'optionvalue' => '1' ) ); + + $this->executeQuery( $request ); + } catch ( UsageException $e ) { + $this->assertEquals( 'nooptionname', $e->getCodeString() ); + $this->assertEquals( 'The optionname parameter must be set', $e->getMessage() ); + + return; + } + $this->fail( "UsageException was not thrown" ); + } + + public function testNoChanges() { + $this->mUserMock->expects( $this->never() ) + ->method( 'resetOptions' ); + + $this->mUserMock->expects( $this->never() ) + ->method( 'setOption' ); + + $this->mUserMock->expects( $this->never() ) + ->method( 'saveSettings' ); + + try { + $request = $this->getSampleRequest(); + + $this->executeQuery( $request ); + } catch ( UsageException $e ) { + $this->assertEquals( 'nochanges', $e->getCodeString() ); + $this->assertEquals( 'No changes were requested', $e->getMessage() ); + + return; + } + $this->fail( "UsageException was not thrown" ); + } + + public function testReset() { + $this->mUserMock->expects( $this->once() ) + ->method( 'resetOptions' ) + ->with( $this->equalTo( array( 'all' ) ) ); + + $this->mUserMock->expects( $this->never() ) + ->method( 'setOption' ); + + $this->mUserMock->expects( $this->once() ) + ->method( 'saveSettings' ); + + $request = $this->getSampleRequest( array( 'reset' => '' ) ); + + $response = $this->executeQuery( $request ); + + $this->assertEquals( self::$Success, $response ); + } + + public function testResetKinds() { + $this->mUserMock->expects( $this->once() ) + ->method( 'resetOptions' ) + ->with( $this->equalTo( array( 'registered' ) ) ); + + $this->mUserMock->expects( $this->never() ) + ->method( 'setOption' ); + + $this->mUserMock->expects( $this->once() ) + ->method( 'saveSettings' ); + + $request = $this->getSampleRequest( array( 'reset' => '', 'resetkinds' => 'registered' ) ); + + $response = $this->executeQuery( $request ); + + $this->assertEquals( self::$Success, $response ); + } + + public function testOptionWithValue() { + $this->mUserMock->expects( $this->never() ) + ->method( 'resetOptions' ); + + $this->mUserMock->expects( $this->once() ) + ->method( 'setOption' ) + ->with( $this->equalTo( 'name' ), $this->equalTo( 'value' ) ); + + $this->mUserMock->expects( $this->once() ) + ->method( 'saveSettings' ); + + $request = $this->getSampleRequest( array( 'optionname' => 'name', 'optionvalue' => 'value' ) ); + + $response = $this->executeQuery( $request ); + + $this->assertEquals( self::$Success, $response ); + } + + public function testOptionResetValue() { + $this->mUserMock->expects( $this->never() ) + ->method( 'resetOptions' ); + + $this->mUserMock->expects( $this->once() ) + ->method( 'setOption' ) + ->with( $this->equalTo( 'name' ), $this->identicalTo( null ) ); + + $this->mUserMock->expects( $this->once() ) + ->method( 'saveSettings' ); + + $request = $this->getSampleRequest( array( 'optionname' => 'name' ) ); + $response = $this->executeQuery( $request ); + + $this->assertEquals( self::$Success, $response ); + } + + public function testChange() { + $this->mUserMock->expects( $this->never() ) + ->method( 'resetOptions' ); + + $this->mUserMock->expects( $this->at( 2 ) ) + ->method( 'getOptions' ); + + $this->mUserMock->expects( $this->at( 4 ) ) + ->method( 'setOption' ) + ->with( $this->equalTo( 'willBeNull' ), $this->identicalTo( null ) ); + + $this->mUserMock->expects( $this->at( 5 ) ) + ->method( 'getOptions' ); + + $this->mUserMock->expects( $this->at( 6 ) ) + ->method( 'setOption' ) + ->with( $this->equalTo( 'willBeEmpty' ), $this->equalTo( '' ) ); + + $this->mUserMock->expects( $this->at( 7 ) ) + ->method( 'getOptions' ); + + $this->mUserMock->expects( $this->at( 8 ) ) + ->method( 'setOption' ) + ->with( $this->equalTo( 'willBeHappy' ), $this->equalTo( 'Happy' ) ); + + $this->mUserMock->expects( $this->once() ) + ->method( 'saveSettings' ); + + $request = $this->getSampleRequest( array( 'change' => 'willBeNull|willBeEmpty=|willBeHappy=Happy' ) ); + + $response = $this->executeQuery( $request ); + + $this->assertEquals( self::$Success, $response ); + } + + public function testResetChangeOption() { + $this->mUserMock->expects( $this->once() ) + ->method( 'resetOptions' ); + + $this->mUserMock->expects( $this->at( 4 ) ) + ->method( 'getOptions' ); + + $this->mUserMock->expects( $this->at( 5 ) ) + ->method( 'setOption' ) + ->with( $this->equalTo( 'willBeHappy' ), $this->equalTo( 'Happy' ) ); + + $this->mUserMock->expects( $this->at( 6 ) ) + ->method( 'getOptions' ); + + $this->mUserMock->expects( $this->at( 7 ) ) + ->method( 'setOption' ) + ->with( $this->equalTo( 'name' ), $this->equalTo( 'value' ) ); + + $this->mUserMock->expects( $this->once() ) + ->method( 'saveSettings' ); + + $args = array( + 'reset' => '', + 'change' => 'willBeHappy=Happy', + 'optionname' => 'name', + 'optionvalue' => 'value' + ); + + $response = $this->executeQuery( $this->getSampleRequest( $args ) ); + + $this->assertEquals( self::$Success, $response ); + } + + public function testMultiSelect() { + $this->mUserMock->expects( $this->never() ) + ->method( 'resetOptions' ); + + $this->mUserMock->expects( $this->at( 3 ) ) + ->method( 'setOption' ) + ->with( $this->equalTo( 'testmultiselect-opt1' ), $this->identicalTo( true ) ); + + $this->mUserMock->expects( $this->at( 4 ) ) + ->method( 'setOption' ) + ->with( $this->equalTo( 'testmultiselect-opt2' ), $this->identicalTo( null ) ); + + $this->mUserMock->expects( $this->at( 5 ) ) + ->method( 'setOption' ) + ->with( $this->equalTo( 'testmultiselect-opt3' ), $this->identicalTo( false ) ); + + $this->mUserMock->expects( $this->at( 6 ) ) + ->method( 'setOption' ) + ->with( $this->equalTo( 'testmultiselect-opt4' ), $this->identicalTo( false ) ); + + $this->mUserMock->expects( $this->once() ) + ->method( 'saveSettings' ); + + $request = $this->getSampleRequest( array( + 'change' => 'testmultiselect-opt1=1|testmultiselect-opt2|testmultiselect-opt3=|testmultiselect-opt4=0' + ) ); + + $response = $this->executeQuery( $request ); + + $this->assertEquals( self::$Success, $response ); + } + + public function testUnknownOption() { + $this->mUserMock->expects( $this->never() ) + ->method( 'resetOptions' ); + + $this->mUserMock->expects( $this->never() ) + ->method( 'saveSettings' ); + + $request = $this->getSampleRequest( array( + 'change' => 'unknownOption=1' + ) ); + + $response = $this->executeQuery( $request ); + + $this->assertEquals( array( + 'options' => 'success', + 'warnings' => array( + 'options' => array( + '*' => "Validation error for 'unknownOption': not a valid preference" + ) + ) + ), $response ); + } + + public function testUserjsOption() { + $this->mUserMock->expects( $this->never() ) + ->method( 'resetOptions' ); + + $this->mUserMock->expects( $this->at( 3 ) ) + ->method( 'setOption' ) + ->with( $this->equalTo( 'userjs-option' ), $this->equalTo( '1' ) ); + + $this->mUserMock->expects( $this->once() ) + ->method( 'saveSettings' ); + + $request = $this->getSampleRequest( array( + 'change' => 'userjs-option=1' + ) ); + + $response = $this->executeQuery( $request ); + + $this->assertEquals( self::$Success, $response ); + } +} diff --git a/tests/phpunit/includes/api/ApiParseTest.php b/tests/phpunit/includes/api/ApiParseTest.php new file mode 100644 index 00000000..2d714e65 --- /dev/null +++ b/tests/phpunit/includes/api/ApiParseTest.php @@ -0,0 +1,29 @@ +doLogin(); + } + + public function testParseNonexistentPage() { + $somePage = mt_rand(); + + try { + $this->doApiRequest( array( + 'action' => 'parse', + 'page' => $somePage ) ); + + $this->fail( "API did not return an error when parsing a nonexistent page" ); + } catch ( UsageException $ex ) { + $this->assertEquals( 'missingtitle', $ex->getCodeString(), + "Parse request for nonexistent page must give 'missingtitle' error: " . var_export( $ex->getMessageArray(), true ) ); + } + } +} diff --git a/tests/phpunit/includes/api/ApiPurgeTest.php b/tests/phpunit/includes/api/ApiPurgeTest.php new file mode 100644 index 00000000..28b5ff4d --- /dev/null +++ b/tests/phpunit/includes/api/ApiPurgeTest.php @@ -0,0 +1,40 @@ +doLogin(); + } + + /** + * @group Broken + */ + public function testPurgeMainPage() { + if ( !Title::newFromText( 'UTPage' )->exists() ) { + $this->markTestIncomplete( "The article [[UTPage]] does not exist" ); + } + + $somePage = mt_rand(); + + $data = $this->doApiRequest( array( + 'action' => 'purge', + 'titles' => 'UTPage|' . $somePage . '|%5D' ) ); + + $this->assertArrayHasKey( 'purge', $data[0], + "Must receive a 'purge' result from API" ); + + $this->assertEquals( 3, count( $data[0]['purge'] ), + "Purge request for three articles should give back three results received: " . var_export( $data[0]['purge'], true ) ); + + $pages = array( 'UTPage' => 'purged', $somePage => 'missing', '%5D' => 'invalid' ); + foreach ( $data[0]['purge'] as $v ) { + $this->assertArrayHasKey( $pages[$v['title']], $v ); + } + } +} diff --git a/tests/phpunit/includes/api/ApiTest.php b/tests/phpunit/includes/api/ApiTest.php new file mode 100644 index 00000000..472f8c4a --- /dev/null +++ b/tests/phpunit/includes/api/ApiTest.php @@ -0,0 +1,259 @@ +assertEquals( + null, $mock->requireOnlyOneParameter( array( "filename" => "foo.txt", + "enablechunks" => false ), "filename", "enablechunks" ) ); + } + + /** + * @expectedException UsageException + */ + public function testRequireOnlyOneParameterZero() { + $mock = new MockApi(); + + $this->assertEquals( + null, $mock->requireOnlyOneParameter( array( "filename" => "foo.txt", + "enablechunks" => 0 ), "filename", "enablechunks" ) ); + } + + /** + * @expectedException UsageException + */ + public function testRequireOnlyOneParameterTrue() { + $mock = new MockApi(); + + $this->assertEquals( + null, $mock->requireOnlyOneParameter( array( "filename" => "foo.txt", + "enablechunks" => true ), "filename", "enablechunks" ) ); + } + + /** + * Test that the API will accept a FauxRequest and execute. The help action + * (default) throws a UsageException. Just validate we're getting proper XML + * + * @expectedException UsageException + */ + public function testApi() { + $api = new ApiMain( + new FauxRequest( array( 'action' => 'help', 'format' => 'xml' ) ) + ); + $api->execute(); + $api->getPrinter()->setBufferResult( true ); + $api->printResult( false ); + $resp = $api->getPrinter()->getBuffer(); + + libxml_use_internal_errors( true ); + $sxe = simplexml_load_string( $resp ); + $this->assertNotInternalType( "bool", $sxe ); + $this->assertThat( $sxe, $this->isInstanceOf( "SimpleXMLElement" ) ); + } + + /** + * Test result of attempted login with an empty username + */ + public function testApiLoginNoName() { + $data = $this->doApiRequest( array( 'action' => 'login', + 'lgname' => '', 'lgpassword' => self::$users['sysop']->password, + ) ); + $this->assertEquals( 'NoName', $data[0]['login']['result'] ); + } + + public function testApiLoginBadPass() { + global $wgServer; + + $user = self::$users['sysop']; + $user->user->logOut(); + + if ( !isset( $wgServer ) ) { + $this->markTestIncomplete( 'This test needs $wgServer to be set in LocalSettings.php' ); + } + $ret = $this->doApiRequest( array( + "action" => "login", + "lgname" => $user->username, + "lgpassword" => "bad", + ) ); + + $result = $ret[0]; + + $this->assertNotInternalType( "bool", $result ); + $a = $result["login"]["result"]; + $this->assertEquals( "NeedToken", $a ); + + $token = $result["login"]["token"]; + + $ret = $this->doApiRequest( + array( + "action" => "login", + "lgtoken" => $token, + "lgname" => $user->username, + "lgpassword" => "badnowayinhell", + ), + $ret[2] + ); + + $result = $ret[0]; + + $this->assertNotInternalType( "bool", $result ); + $a = $result["login"]["result"]; + + $this->assertEquals( "WrongPass", $a ); + } + + public function testApiLoginGoodPass() { + global $wgServer; + + if ( !isset( $wgServer ) ) { + $this->markTestIncomplete( 'This test needs $wgServer to be set in LocalSettings.php' ); + } + + $user = self::$users['sysop']; + $user->user->logOut(); + + $ret = $this->doApiRequest( array( + "action" => "login", + "lgname" => $user->username, + "lgpassword" => $user->password, + ) + ); + + $result = $ret[0]; + $this->assertNotInternalType( "bool", $result ); + $this->assertNotInternalType( "null", $result["login"] ); + + $a = $result["login"]["result"]; + $this->assertEquals( "NeedToken", $a ); + $token = $result["login"]["token"]; + + $ret = $this->doApiRequest( + array( + "action" => "login", + "lgtoken" => $token, + "lgname" => $user->username, + "lgpassword" => $user->password, + ), + $ret[2] + ); + + $result = $ret[0]; + + $this->assertNotInternalType( "bool", $result ); + $a = $result["login"]["result"]; + + $this->assertEquals( "Success", $a ); + } + + /** + * @group Broken + */ + public function testApiGotCookie() { + $this->markTestIncomplete( "The server can't do external HTTP requests, and the internal one won't give cookies" ); + + global $wgServer, $wgScriptPath; + + if ( !isset( $wgServer ) ) { + $this->markTestIncomplete( 'This test needs $wgServer to be set in LocalSettings.php' ); + } + $user = self::$users['sysop']; + + $req = MWHttpRequest::factory( self::$apiUrl . "?action=login&format=xml", + array( "method" => "POST", + "postData" => array( + "lgname" => $user->username, + "lgpassword" => $user->password + ) + ) + ); + $req->execute(); + + libxml_use_internal_errors( true ); + $sxe = simplexml_load_string( $req->getContent() ); + $this->assertNotInternalType( "bool", $sxe ); + $this->assertThat( $sxe, $this->isInstanceOf( "SimpleXMLElement" ) ); + $this->assertNotInternalType( "null", $sxe->login[0] ); + + $a = $sxe->login[0]->attributes()->result[0]; + $this->assertEquals( ' result="NeedToken"', $a->asXML() ); + $token = (string)$sxe->login[0]->attributes()->token; + + $req->setData( array( + "lgtoken" => $token, + "lgname" => $user->username, + "lgpassword" => $user->password ) ); + $req->execute(); + + $cj = $req->getCookieJar(); + $serverName = parse_url( $wgServer, PHP_URL_HOST ); + $this->assertNotEquals( false, $serverName ); + $serializedCookie = $cj->serializeToHttpRequest( $wgScriptPath, $serverName ); + $this->assertNotEquals( '', $serializedCookie ); + $this->assertRegexp( '/_session=[^;]*; .*UserID=[0-9]*; .*UserName=' . $user->userName . '; .*Token=/', $serializedCookie ); + + return $cj; + } + + public function testRunLogin() { + $sysopUser = self::$users['sysop']; + $data = $this->doApiRequest( array( + 'action' => 'login', + 'lgname' => $sysopUser->username, + 'lgpassword' => $sysopUser->password ) ); + + $this->assertArrayHasKey( "login", $data[0] ); + $this->assertArrayHasKey( "result", $data[0]['login'] ); + $this->assertEquals( "NeedToken", $data[0]['login']['result'] ); + $token = $data[0]['login']['token']; + + $data = $this->doApiRequest( array( + 'action' => 'login', + "lgtoken" => $token, + "lgname" => $sysopUser->username, + "lgpassword" => $sysopUser->password ), $data[2] ); + + $this->assertArrayHasKey( "login", $data[0] ); + $this->assertArrayHasKey( "result", $data[0]['login'] ); + $this->assertEquals( "Success", $data[0]['login']['result'] ); + $this->assertArrayHasKey( 'lgtoken', $data[0]['login'] ); + + return $data; + } + + public function testGettingToken() { + foreach ( self::$users as $user ) { + $this->runTokenTest( $user ); + } + } + + function runTokenTest( $user ) { + $tokens = $this->getTokenList( $user ); + + $rights = $user->user->getRights(); + + $this->assertArrayHasKey( 'edittoken', $tokens ); + $this->assertArrayHasKey( 'movetoken', $tokens ); + + if ( isset( $rights['delete'] ) ) { + $this->assertArrayHasKey( 'deletetoken', $tokens ); + } + + if ( isset( $rights['block'] ) ) { + $this->assertArrayHasKey( 'blocktoken', $tokens ); + $this->assertArrayHasKey( 'unblocktoken', $tokens ); + } + + if ( isset( $rights['protect'] ) ) { + $this->assertArrayHasKey( 'protecttoken', $tokens ); + } + + return $tokens; + } +} diff --git a/tests/phpunit/includes/api/ApiTestCase.php b/tests/phpunit/includes/api/ApiTestCase.php new file mode 100644 index 00000000..94ef9c68 --- /dev/null +++ b/tests/phpunit/includes/api/ApiTestCase.php @@ -0,0 +1,253 @@ + new TestUser( + 'Apitestsysop', + 'Api Test Sysop', + 'api_test_sysop@example.com', + array( 'sysop' ) + ), + 'uploader' => new TestUser( + 'Apitestuser', + 'Api Test User', + 'api_test_user@example.com', + array() + ) + ); + + $this->setMwGlobals( array( + 'wgMemc' => new EmptyBagOStuff(), + 'wgAuth' => new StubObject( 'wgAuth', 'AuthPlugin' ), + 'wgRequest' => new FauxRequest( array() ), + 'wgUser' => self::$users['sysop']->user, + ) ); + + $this->apiContext = new ApiTestContext(); + } + + /** + * Edits or creates a page/revision + * @param $pageName string page title + * @param $text string content of the page + * @param $summary string optional summary string for the revision + * @param $defaultNs int optional namespace id + * @return array as returned by WikiPage::doEditContent() + */ + protected function editPage( $pageName, $text, $summary = '', $defaultNs = NS_MAIN ) { + $title = Title::newFromText( $pageName, $defaultNs ); + $page = WikiPage::factory( $title ); + + return $page->doEditContent( ContentHandler::makeContent( $text, $title ), $summary ); + } + + /** + * Does the API request and returns the result. + * + * The returned value is an array containing + * - the result data (array) + * - the request (WebRequest) + * - the session data of the request (array) + * - if $appendModule is true, the Api module $module + * + * @param array $params + * @param array|null $session + * @param bool $appendModule + * @param User|null $user + * + * @return array + */ + protected function doApiRequest( array $params, array $session = null, $appendModule = false, User $user = null ) { + global $wgRequest, $wgUser; + + if ( is_null( $session ) ) { + // re-use existing global session by default + $session = $wgRequest->getSessionArray(); + } + + // set up global environment + if ( $user ) { + $wgUser = $user; + } + + $wgRequest = new FauxRequest( $params, true, $session ); + RequestContext::getMain()->setRequest( $wgRequest ); + + // set up local environment + $context = $this->apiContext->newTestContext( $wgRequest, $wgUser ); + + $module = new ApiMain( $context, true ); + + // run it! + $module->execute(); + + // construct result + $results = array( + $module->getResultData(), + $context->getRequest(), + $context->getRequest()->getSessionArray() + ); + + if ( $appendModule ) { + $results[] = $module; + } + + return $results; + } + + /** + * Add an edit token to the API request + * This is cheating a bit -- we grab a token in the correct format and then add it to the pseudo-session and to the + * request, without actually requesting a "real" edit token + * @param $params Array: key-value API params + * @param $session Array|null: session array + * @param $user User|null A User object for the context + * @return result of the API call + * @throws Exception in case wsToken is not set in the session + */ + protected function doApiRequestWithToken( array $params, array $session = null, User $user = null ) { + global $wgRequest; + + if ( $session === null ) { + $session = $wgRequest->getSessionArray(); + } + + if ( $session['wsToken'] ) { + // add edit token to fake session + $session['wsEditToken'] = $session['wsToken']; + // add token to request parameters + $params['token'] = md5( $session['wsToken'] ) . User::EDIT_TOKEN_SUFFIX; + + return $this->doApiRequest( $params, $session, false, $user ); + } else { + throw new Exception( "request data not in right format" ); + } + } + + protected function doLogin( $user = 'sysop' ) { + if ( !array_key_exists( $user, self::$users ) ) { + throw new MWException( "Can not log in to undefined user $user" ); + } + + $data = $this->doApiRequest( array( + 'action' => 'login', + 'lgname' => self::$users[ $user ]->username, + 'lgpassword' => self::$users[ $user ]->password ) ); + + $token = $data[0]['login']['token']; + + $data = $this->doApiRequest( + array( + 'action' => 'login', + 'lgtoken' => $token, + 'lgname' => self::$users[ $user ]->username, + 'lgpassword' => self::$users[ $user ]->password, + ), + $data[2] + ); + + return $data; + } + + protected function getTokenList( $user, $session = null ) { + $data = $this->doApiRequest( array( + 'action' => 'tokens', + 'type' => 'edit|delete|protect|move|block|unblock|watch' + ), $session, false, $user->user ); + + if ( !array_key_exists( 'tokens', $data[0] ) ) { + throw new MWException( 'Api failed to return a token list' ); + } + + return $data[0]['tokens']; + } + + public function testApiTestGroup() { + $groups = PHPUnit_Util_Test::getGroups( get_class( $this ) ); + $constraint = PHPUnit_Framework_Assert::logicalOr( + $this->contains( 'medium' ), + $this->contains( 'large' ) + ); + $this->assertThat( $groups, $constraint, + 'ApiTestCase::setUp can be slow, tests must be "medium" or "large"' + ); + } +} + +class UserWrapper { + public $userName; + public $password; + public $user; + + public function __construct( $userName, $password, $group = '' ) { + $this->userName = $userName; + $this->password = $password; + + $this->user = User::newFromName( $this->userName ); + if ( !$this->user->getID() ) { + $this->user = User::createNew( $this->userName, array( + "email" => "test@example.com", + "real_name" => "Test User" ) ); + } + $this->user->setPassword( $this->password ); + + if ( $group !== '' ) { + $this->user->addGroup( $group ); + } + $this->user->saveSettings(); + } +} + +class MockApi extends ApiBase { + public function execute() { + } + + public function getVersion() { + } + + public function __construct() { + } + + public function getAllowedParams() { + return array( + 'filename' => null, + 'enablechunks' => false, + 'sessionkey' => null, + ); + } +} + +class ApiTestContext extends RequestContext { + + /** + * Returns a DerivativeContext with the request variables in place + * + * @param $request WebRequest request object including parameters and session + * @param $user User or null + * @return DerivativeContext + */ + public function newTestContext( WebRequest $request, User $user = null ) { + $context = new DerivativeContext( $this ); + $context->setRequest( $request ); + if ( $user !== null ) { + $context->setUser( $user ); + } + + return $context; + } +} diff --git a/tests/phpunit/includes/api/ApiTestCaseUpload.php b/tests/phpunit/includes/api/ApiTestCaseUpload.php new file mode 100644 index 00000000..7e18b6ed --- /dev/null +++ b/tests/phpunit/includes/api/ApiTestCaseUpload.php @@ -0,0 +1,149 @@ +setMwGlobals( array( + 'wgEnableUploads' => true, + 'wgEnableAPI' => true, + ) ); + + wfSetupSession(); + + $this->clearFakeUploads(); + } + + protected function tearDown() { + $this->clearTempUpload(); + + parent::tearDown(); + } + + /** + * Helper function -- remove files and associated articles by Title + * @param $title Title: title to be removed + */ + public function deleteFileByTitle( $title ) { + if ( $title->exists() ) { + $file = wfFindFile( $title, array( 'ignoreRedirect' => true ) ); + $noOldArchive = ""; // yes this really needs to be set this way + $comment = "removing for test"; + $restrictDeletedVersions = false; + $status = FileDeleteForm::doDelete( $title, $file, $noOldArchive, $comment, $restrictDeletedVersions ); + if ( !$status->isGood() ) { + return false; + } + $page = WikiPage::factory( $title ); + $page->doDeleteArticle( "removing for test" ); + + // see if it now doesn't exist; reload + $title = Title::newFromText( $title->getText(), NS_FILE ); + } + + return !( $title && $title instanceof Title && $title->exists() ); + } + + /** + * Helper function -- remove files and associated articles with a particular filename + * @param $fileName String: filename to be removed + */ + public function deleteFileByFileName( $fileName ) { + return $this->deleteFileByTitle( Title::newFromText( $fileName, NS_FILE ) ); + } + + /** + * Helper function -- given a file on the filesystem, find matching content in the db (and associated articles) and remove them. + * @param $filePath String: path to file on the filesystem + */ + public function deleteFileByContent( $filePath ) { + $hash = FSFile::getSha1Base36FromPath( $filePath ); + $dupes = RepoGroup::singleton()->findBySha1( $hash ); + $success = true; + foreach ( $dupes as $dupe ) { + $success &= $this->deleteFileByTitle( $dupe->getTitle() ); + } + + return $success; + } + + /** + * Fake an upload by dumping the file into temp space, and adding info to $_FILES. + * (This is what PHP would normally do). + * @param $fieldName String: name this would have in the upload form + * @param $fileName String: name to title this + * @param $type String: mime type + * @param $filePath String: path where to find file contents + */ + function fakeUploadFile( $fieldName, $fileName, $type, $filePath ) { + $tmpName = tempnam( wfTempDir(), "" ); + if ( !file_exists( $filePath ) ) { + throw new Exception( "$filePath doesn't exist!" ); + } + + if ( !copy( $filePath, $tmpName ) ) { + throw new Exception( "couldn't copy $filePath to $tmpName" ); + } + + clearstatcache(); + $size = filesize( $tmpName ); + if ( $size === false ) { + throw new Exception( "couldn't stat $tmpName" ); + } + + $_FILES[$fieldName] = array( + 'name' => $fileName, + 'type' => $type, + 'tmp_name' => $tmpName, + 'size' => $size, + 'error' => null + ); + + return true; + } + + function fakeUploadChunk( $fieldName, $fileName, $type, & $chunkData ) { + $tmpName = tempnam( wfTempDir(), "" ); + // copy the chunk data to temp location: + if ( !file_put_contents( $tmpName, $chunkData ) ) { + throw new Exception( "couldn't copy chunk data to $tmpName" ); + } + + clearstatcache(); + $size = filesize( $tmpName ); + if ( $size === false ) { + throw new Exception( "couldn't stat $tmpName" ); + } + + $_FILES[$fieldName] = array( + 'name' => $fileName, + 'type' => $type, + 'tmp_name' => $tmpName, + 'size' => $size, + 'error' => null + ); + } + + function clearTempUpload() { + if ( isset( $_FILES['file']['tmp_name'] ) ) { + $tmp = $_FILES['file']['tmp_name']; + if ( file_exists( $tmp ) ) { + unlink( $tmp ); + } + } + } + + /** + * Remove traces of previous fake uploads + */ + function clearFakeUploads() { + $_FILES = array(); + } +} diff --git a/tests/phpunit/includes/api/ApiUploadTest.php b/tests/phpunit/includes/api/ApiUploadTest.php new file mode 100644 index 00000000..1540af55 --- /dev/null +++ b/tests/phpunit/includes/api/ApiUploadTest.php @@ -0,0 +1,561 @@ + 'login', + 'lgname' => $user->username, + 'lgpassword' => $user->password + ); + list( $result, , $session ) = $this->doApiRequest( $params ); + $this->assertArrayHasKey( "login", $result ); + $this->assertArrayHasKey( "result", $result['login'] ); + $this->assertEquals( "NeedToken", $result['login']['result'] ); + $token = $result['login']['token']; + + $params = array( + 'action' => 'login', + 'lgtoken' => $token, + 'lgname' => $user->username, + 'lgpassword' => $user->password + ); + list( $result, , $session ) = $this->doApiRequest( $params, $session ); + $this->assertArrayHasKey( "login", $result ); + $this->assertArrayHasKey( "result", $result['login'] ); + $this->assertEquals( "Success", $result['login']['result'] ); + $this->assertArrayHasKey( 'lgtoken', $result['login'] ); + + $this->assertNotEmpty( $session, 'API Login must return a session' ); + + return $session; + } + + /** + * @depends testLogin + */ + public function testUploadRequiresToken( $session ) { + $exception = false; + try { + $this->doApiRequest( array( + 'action' => 'upload' + ) ); + } catch ( UsageException $e ) { + $exception = true; + $this->assertEquals( "The token parameter must be set", $e->getMessage() ); + } + $this->assertTrue( $exception, "Got exception" ); + } + + /** + * @depends testLogin + */ + public function testUploadMissingParams( $session ) { + $exception = false; + try { + $this->doApiRequestWithToken( array( + 'action' => 'upload', + ), $session, self::$users['uploader']->user ); + } catch ( UsageException $e ) { + $exception = true; + $this->assertEquals( "One of the parameters filekey, file, url, statuskey is required", + $e->getMessage() ); + } + $this->assertTrue( $exception, "Got exception" ); + } + + + /** + * @depends testLogin + */ + public function testUpload( $session ) { + $extension = 'png'; + $mimeType = 'image/png'; + + try { + $randomImageGenerator = new RandomImageGenerator(); + $filePaths = $randomImageGenerator->writeImages( 1, $extension, wfTempDir() ); + } catch ( Exception $e ) { + $this->markTestIncomplete( $e->getMessage() ); + } + + $filePath = $filePaths[0]; + $fileSize = filesize( $filePath ); + $fileName = basename( $filePath ); + + $this->deleteFileByFileName( $fileName ); + $this->deleteFileByContent( $filePath ); + + if ( !$this->fakeUploadFile( 'file', $fileName, $mimeType, $filePath ) ) { + $this->markTestIncomplete( "Couldn't upload file!\n" ); + } + + $params = array( + 'action' => 'upload', + 'filename' => $fileName, + 'file' => 'dummy content', + 'comment' => 'dummy comment', + 'text' => "This is the page text for $fileName", + ); + + $exception = false; + try { + list( $result, , ) = $this->doApiRequestWithToken( $params, $session, + self::$users['uploader']->user ); + } catch ( UsageException $e ) { + $exception = true; + } + $this->assertTrue( isset( $result['upload'] ) ); + $this->assertEquals( 'Success', $result['upload']['result'] ); + $this->assertEquals( $fileSize, (int)$result['upload']['imageinfo']['size'] ); + $this->assertEquals( $mimeType, $result['upload']['imageinfo']['mime'] ); + $this->assertFalse( $exception ); + + // clean up + $this->deleteFileByFilename( $fileName ); + unlink( $filePath ); + } + + + /** + * @depends testLogin + */ + public function testUploadZeroLength( $session ) { + $mimeType = 'image/png'; + + $filePath = tempnam( wfTempDir(), "" ); + $fileName = "apiTestUploadZeroLength.png"; + + $this->deleteFileByFileName( $fileName ); + + if ( !$this->fakeUploadFile( 'file', $fileName, $mimeType, $filePath ) ) { + $this->markTestIncomplete( "Couldn't upload file!\n" ); + } + + $params = array( + 'action' => 'upload', + 'filename' => $fileName, + 'file' => 'dummy content', + 'comment' => 'dummy comment', + 'text' => "This is the page text for $fileName", + ); + + $exception = false; + try { + $this->doApiRequestWithToken( $params, $session, self::$users['uploader']->user ); + } catch ( UsageException $e ) { + $this->assertContains( 'The file you submitted was empty', $e->getMessage() ); + $exception = true; + } + $this->assertTrue( $exception ); + + // clean up + $this->deleteFileByFilename( $fileName ); + unlink( $filePath ); + } + + + /** + * @depends testLogin + */ + public function testUploadSameFileName( $session ) { + $extension = 'png'; + $mimeType = 'image/png'; + + try { + $randomImageGenerator = new RandomImageGenerator(); + $filePaths = $randomImageGenerator->writeImages( 2, $extension, wfTempDir() ); + } catch ( Exception $e ) { + $this->markTestIncomplete( $e->getMessage() ); + } + + // we'll reuse this filename + $fileName = basename( $filePaths[0] ); + + // clear any other files with the same name + $this->deleteFileByFileName( $fileName ); + + // we reuse these params + $params = array( + 'action' => 'upload', + 'filename' => $fileName, + 'file' => 'dummy content', + 'comment' => 'dummy comment', + 'text' => "This is the page text for $fileName", + ); + + // first upload .... should succeed + + if ( !$this->fakeUploadFile( 'file', $fileName, $mimeType, $filePaths[0] ) ) { + $this->markTestIncomplete( "Couldn't upload file!\n" ); + } + + $exception = false; + try { + list( $result, , $session ) = $this->doApiRequestWithToken( $params, $session, + self::$users['uploader']->user ); + } catch ( UsageException $e ) { + $exception = true; + } + $this->assertTrue( isset( $result['upload'] ) ); + $this->assertEquals( 'Success', $result['upload']['result'] ); + $this->assertFalse( $exception ); + + // second upload with the same name (but different content) + + if ( !$this->fakeUploadFile( 'file', $fileName, $mimeType, $filePaths[1] ) ) { + $this->markTestIncomplete( "Couldn't upload file!\n" ); + } + + $exception = false; + try { + list( $result, , ) = $this->doApiRequestWithToken( $params, $session, + self::$users['uploader']->user ); // FIXME: leaks a temporary file + } catch ( UsageException $e ) { + $exception = true; + } + $this->assertTrue( isset( $result['upload'] ) ); + $this->assertEquals( 'Warning', $result['upload']['result'] ); + $this->assertTrue( isset( $result['upload']['warnings'] ) ); + $this->assertTrue( isset( $result['upload']['warnings']['exists'] ) ); + $this->assertFalse( $exception ); + + // clean up + $this->deleteFileByFilename( $fileName ); + unlink( $filePaths[0] ); + unlink( $filePaths[1] ); + } + + + /** + * @depends testLogin + */ + public function testUploadSameContent( $session ) { + $extension = 'png'; + $mimeType = 'image/png'; + + try { + $randomImageGenerator = new RandomImageGenerator(); + $filePaths = $randomImageGenerator->writeImages( 1, $extension, wfTempDir() ); + } catch ( Exception $e ) { + $this->markTestIncomplete( $e->getMessage() ); + } + + $fileNames[0] = basename( $filePaths[0] ); + $fileNames[1] = "SameContentAs" . $fileNames[0]; + + // clear any other files with the same name or content + $this->deleteFileByContent( $filePaths[0] ); + $this->deleteFileByFileName( $fileNames[0] ); + $this->deleteFileByFileName( $fileNames[1] ); + + // first upload .... should succeed + + $params = array( + 'action' => 'upload', + 'filename' => $fileNames[0], + 'file' => 'dummy content', + 'comment' => 'dummy comment', + 'text' => "This is the page text for " . $fileNames[0], + ); + + if ( !$this->fakeUploadFile( 'file', $fileNames[0], $mimeType, $filePaths[0] ) ) { + $this->markTestIncomplete( "Couldn't upload file!\n" ); + } + + $exception = false; + try { + list( $result, , $session ) = $this->doApiRequestWithToken( $params, $session, + self::$users['uploader']->user ); + } catch ( UsageException $e ) { + $exception = true; + } + $this->assertTrue( isset( $result['upload'] ) ); + $this->assertEquals( 'Success', $result['upload']['result'] ); + $this->assertFalse( $exception ); + + // second upload with the same content (but different name) + + if ( !$this->fakeUploadFile( 'file', $fileNames[1], $mimeType, $filePaths[0] ) ) { + $this->markTestIncomplete( "Couldn't upload file!\n" ); + } + + $params = array( + 'action' => 'upload', + 'filename' => $fileNames[1], + 'file' => 'dummy content', + 'comment' => 'dummy comment', + 'text' => "This is the page text for " . $fileNames[1], + ); + + $exception = false; + try { + list( $result ) = $this->doApiRequestWithToken( $params, $session, + self::$users['uploader']->user ); // FIXME: leaks a temporary file + } catch ( UsageException $e ) { + $exception = true; + } + $this->assertTrue( isset( $result['upload'] ) ); + $this->assertEquals( 'Warning', $result['upload']['result'] ); + $this->assertTrue( isset( $result['upload']['warnings'] ) ); + $this->assertTrue( isset( $result['upload']['warnings']['duplicate'] ) ); + $this->assertFalse( $exception ); + + // clean up + $this->deleteFileByFilename( $fileNames[0] ); + $this->deleteFileByFilename( $fileNames[1] ); + unlink( $filePaths[0] ); + } + + /** + * @depends testLogin + */ + public function testUploadStash( $session ) { + $this->setMwGlobals( array( + 'wgUser' => self::$users['uploader']->user, // @todo FIXME: still used somewhere + ) ); + + $extension = 'png'; + $mimeType = 'image/png'; + + try { + $randomImageGenerator = new RandomImageGenerator(); + $filePaths = $randomImageGenerator->writeImages( 1, $extension, wfTempDir() ); + } catch ( Exception $e ) { + $this->markTestIncomplete( $e->getMessage() ); + } + + $filePath = $filePaths[0]; + $fileSize = filesize( $filePath ); + $fileName = basename( $filePath ); + + $this->deleteFileByFileName( $fileName ); + $this->deleteFileByContent( $filePath ); + + if ( !$this->fakeUploadFile( 'file', $fileName, $mimeType, $filePath ) ) { + $this->markTestIncomplete( "Couldn't upload file!\n" ); + } + + $params = array( + 'action' => 'upload', + 'stash' => 1, + 'filename' => $fileName, + 'file' => 'dummy content', + 'comment' => 'dummy comment', + 'text' => "This is the page text for $fileName", + ); + + $exception = false; + try { + list( $result, , $session ) = $this->doApiRequestWithToken( $params, $session, + self::$users['uploader']->user ); // FIXME: leaks a temporary file + } catch ( UsageException $e ) { + $exception = true; + } + $this->assertFalse( $exception ); + $this->assertTrue( isset( $result['upload'] ) ); + $this->assertEquals( 'Success', $result['upload']['result'] ); + $this->assertEquals( $fileSize, (int)$result['upload']['imageinfo']['size'] ); + $this->assertEquals( $mimeType, $result['upload']['imageinfo']['mime'] ); + $this->assertTrue( isset( $result['upload']['filekey'] ) ); + $this->assertEquals( $result['upload']['sessionkey'], $result['upload']['filekey'] ); + $filekey = $result['upload']['filekey']; + + // it should be visible from Special:UploadStash + // XXX ...but how to test this, with a fake WebRequest with the session? + + // now we should try to release the file from stash + $params = array( + 'action' => 'upload', + 'filekey' => $filekey, + 'filename' => $fileName, + 'comment' => 'dummy comment', + 'text' => "This is the page text for $fileName, altered", + ); + + $this->clearFakeUploads(); + $exception = false; + try { + list( $result ) = $this->doApiRequestWithToken( $params, $session, + self::$users['uploader']->user ); + } catch ( UsageException $e ) { + $exception = true; + } + $this->assertTrue( isset( $result['upload'] ) ); + $this->assertEquals( 'Success', $result['upload']['result'] ); + $this->assertFalse( $exception, "No UsageException exception." ); + + // clean up + $this->deleteFileByFilename( $fileName ); + unlink( $filePath ); + } + + /** + * @depends testLogin + */ + public function testUploadChunks( $session ) { + $this->setMwGlobals( array( + 'wgUser' => self::$users['uploader']->user, // @todo FIXME: still used somewhere + ) ); + + $chunkSize = 1048576; + // Download a large image file + // ( using RandomImageGenerator for large files is not stable ) + $mimeType = 'image/jpeg'; + $url = 'http://upload.wikimedia.org/wikipedia/commons/e/ed/Oberaargletscher_from_Oberaar%2C_2010_07.JPG'; + $filePath = wfTempDir() . '/Oberaargletscher_from_Oberaar.jpg'; + try { + // Only download if the file is not avaliable in the temp location: + if ( !is_file( $filePath ) ) { + copy( $url, $filePath ); + } + } catch ( Exception $e ) { + $this->markTestIncomplete( $e->getMessage() ); + } + + $fileSize = filesize( $filePath ); + $fileName = basename( $filePath ); + + $this->deleteFileByFileName( $fileName ); + $this->deleteFileByContent( $filePath ); + + // Base upload params: + $params = array( + 'action' => 'upload', + 'stash' => 1, + 'filename' => $fileName, + 'filesize' => $fileSize, + 'offset' => 0, + ); + + // Upload chunks + $chunkSessionKey = false; + $resultOffset = 0; + // Open the file: + $handle = @fopen( $filePath, "r" ); + if ( $handle === false ) { + $this->markTestIncomplete( "could not open file: $filePath" ); + } + while ( !feof( $handle ) ) { + // Get the current chunk + $chunkData = @fread( $handle, $chunkSize ); + + // Upload the current chunk into the $_FILE object: + $this->fakeUploadChunk( 'chunk', 'blob', $mimeType, $chunkData ); + + // Check for chunkSessionKey + if ( !$chunkSessionKey ) { + // Upload fist chunk ( and get the session key ) + try { + list( $result, , $session ) = $this->doApiRequestWithToken( $params, $session, + self::$users['uploader']->user ); + } catch ( UsageException $e ) { + $this->markTestIncomplete( $e->getMessage() ); + } + // Make sure we got a valid chunk continue: + $this->assertTrue( isset( $result['upload'] ) ); + $this->assertTrue( isset( $result['upload']['filekey'] ) ); + // If we don't get a session key mark test incomplete. + if ( !isset( $result['upload']['filekey'] ) ) { + $this->markTestIncomplete( "no filekey provided" ); + } + $chunkSessionKey = $result['upload']['filekey']; + $this->assertEquals( 'Continue', $result['upload']['result'] ); + // First chunk should have chunkSize == offset + $this->assertEquals( $chunkSize, $result['upload']['offset'] ); + $resultOffset = $result['upload']['offset']; + continue; + } + // Filekey set to chunk session + $params['filekey'] = $chunkSessionKey; + // Update the offset ( always add chunkSize for subquent chunks should be in-sync with $result['upload']['offset'] ) + $params['offset'] += $chunkSize; + // Make sure param offset is insync with resultOffset: + $this->assertEquals( $resultOffset, $params['offset'] ); + // Upload current chunk + try { + list( $result, , $session ) = $this->doApiRequestWithToken( $params, $session, + self::$users['uploader']->user ); + } catch ( UsageException $e ) { + $this->markTestIncomplete( $e->getMessage() ); + } + // Make sure we got a valid chunk continue: + $this->assertTrue( isset( $result['upload'] ) ); + $this->assertTrue( isset( $result['upload']['filekey'] ) ); + + // Check if we were on the last chunk: + if ( $params['offset'] + $chunkSize >= $fileSize ) { + $this->assertEquals( 'Success', $result['upload']['result'] ); + break; + } else { + $this->assertEquals( 'Continue', $result['upload']['result'] ); + // update $resultOffset + $resultOffset = $result['upload']['offset']; + } + } + fclose( $handle ); + + // Check that we got a valid file result: + wfDebug( __METHOD__ . " hohoh filesize {$fileSize} info {$result['upload']['imageinfo']['size']}\n\n" ); + $this->assertEquals( $fileSize, $result['upload']['imageinfo']['size'] ); + $this->assertEquals( $mimeType, $result['upload']['imageinfo']['mime'] ); + $this->assertTrue( isset( $result['upload']['filekey'] ) ); + $filekey = $result['upload']['filekey']; + + // Now we should try to release the file from stash + $params = array( + 'action' => 'upload', + 'filekey' => $filekey, + 'filename' => $fileName, + 'comment' => 'dummy comment', + 'text' => "This is the page text for $fileName, altered", + ); + $this->clearFakeUploads(); + $exception = false; + try { + list( $result ) = $this->doApiRequestWithToken( $params, $session, + self::$users['uploader']->user ); + } catch ( UsageException $e ) { + $exception = true; + } + $this->assertTrue( isset( $result['upload'] ) ); + $this->assertEquals( 'Success', $result['upload']['result'] ); + $this->assertFalse( $exception ); + + // clean up + $this->deleteFileByFilename( $fileName ); + // don't remove downloaded temporary file for fast subquent tests. + //unlink( $filePath ); + } +} diff --git a/tests/phpunit/includes/api/ApiWatchTest.php b/tests/phpunit/includes/api/ApiWatchTest.php new file mode 100644 index 00000000..028ea9ff --- /dev/null +++ b/tests/phpunit/includes/api/ApiWatchTest.php @@ -0,0 +1,148 @@ +doLogin(); + } + + function getTokens() { + return $this->getTokenList( self::$users['sysop'] ); + } + + /** + */ + public function testWatchEdit() { + $tokens = $this->getTokens(); + + $data = $this->doApiRequest( array( + 'action' => 'edit', + 'title' => 'Help:UTPage', // Help namespace is hopefully wikitext + 'text' => 'new text', + 'token' => $tokens['edittoken'], + 'watchlist' => 'watch' ) ); + $this->assertArrayHasKey( 'edit', $data[0] ); + $this->assertArrayHasKey( 'result', $data[0]['edit'] ); + $this->assertEquals( 'Success', $data[0]['edit']['result'] ); + + return $data; + } + + /** + * @depends testWatchEdit + */ + public function testWatchClear() { + $tokens = $this->getTokens(); + + $data = $this->doApiRequest( array( + 'action' => 'query', + 'list' => 'watchlist' ) ); + + if ( isset( $data[0]['query']['watchlist'] ) ) { + $wl = $data[0]['query']['watchlist']; + + foreach ( $wl as $page ) { + $data = $this->doApiRequest( array( + 'action' => 'watch', + 'title' => $page['title'], + 'unwatch' => true, + 'token' => $tokens['watchtoken'] ) ); + } + } + $data = $this->doApiRequest( array( + 'action' => 'query', + 'list' => 'watchlist' ), $data ); + $this->assertArrayHasKey( 'query', $data[0] ); + $this->assertArrayHasKey( 'watchlist', $data[0]['query'] ); + $this->assertEquals( 0, count( $data[0]['query']['watchlist'] ) ); + + return $data; + } + + /** + */ + public function testWatchProtect() { + $tokens = $this->getTokens(); + + $data = $this->doApiRequest( array( + 'action' => 'protect', + 'token' => $tokens['protecttoken'], + 'title' => 'Help:UTPage', + 'protections' => 'edit=sysop', + 'watchlist' => 'unwatch' ) ); + + $this->assertArrayHasKey( 'protect', $data[0] ); + $this->assertArrayHasKey( 'protections', $data[0]['protect'] ); + $this->assertEquals( 1, count( $data[0]['protect']['protections'] ) ); + $this->assertArrayHasKey( 'edit', $data[0]['protect']['protections'][0] ); + } + + /** + */ + public function testGetRollbackToken() { + $this->getTokens(); + + if ( !Title::newFromText( 'Help:UTPage' )->exists() ) { + $this->markTestSkipped( "The article [[Help:UTPage]] does not exist" ); //TODO: just create it? + } + + $data = $this->doApiRequest( array( + 'action' => 'query', + 'prop' => 'revisions', + 'titles' => 'Help:UTPage', + 'rvtoken' => 'rollback' ) ); + + $this->assertArrayHasKey( 'query', $data[0] ); + $this->assertArrayHasKey( 'pages', $data[0]['query'] ); + $keys = array_keys( $data[0]['query']['pages'] ); + $key = array_pop( $keys ); + + if ( isset( $data[0]['query']['pages'][$key]['missing'] ) ) { + $this->markTestSkipped( "Target page (Help:UTPage) doesn't exist" ); + } + + $this->assertArrayHasKey( 'pageid', $data[0]['query']['pages'][$key] ); + $this->assertArrayHasKey( 'revisions', $data[0]['query']['pages'][$key] ); + $this->assertArrayHasKey( 0, $data[0]['query']['pages'][$key]['revisions'] ); + $this->assertArrayHasKey( 'rollbacktoken', $data[0]['query']['pages'][$key]['revisions'][0] ); + + return $data; + } + + /** + * @group Broken + * Broken because there is currently no revision info in the $pageinfo + * + * @depends testGetRollbackToken + */ + public function testWatchRollback( $data ) { + $keys = array_keys( $data[0]['query']['pages'] ); + $key = array_pop( $keys ); + $pageinfo = $data[0]['query']['pages'][$key]; + $revinfo = $pageinfo['revisions'][0]; + + try { + $data = $this->doApiRequest( array( + 'action' => 'rollback', + 'title' => 'Help:UTPage', + 'user' => $revinfo['user'], + 'token' => $pageinfo['rollbacktoken'], + 'watchlist' => 'watch' ) ); + + $this->assertArrayHasKey( 'rollback', $data[0] ); + $this->assertArrayHasKey( 'title', $data[0]['rollback'] ); + } catch ( UsageException $ue ) { + if ( $ue->getCodeString() == 'onlyauthor' ) { + $this->markTestIncomplete( "Only one author to 'Help:UTPage', cannot test rollback" ); + } else { + $this->fail( "Received error '" . $ue->getCodeString() . "'" ); + } + } + } +} diff --git a/tests/phpunit/includes/api/PrefixUniquenessTest.php b/tests/phpunit/includes/api/PrefixUniquenessTest.php new file mode 100644 index 00000000..d9be85e3 --- /dev/null +++ b/tests/phpunit/includes/api/PrefixUniquenessTest.php @@ -0,0 +1,25 @@ +getModuleManager()->getNamesWithClasses(); + $prefixes = array(); + + foreach ( $modules as $name => $class ) { + $module = new $class( $main, $name ); + $prefix = $module->getModulePrefix(); + if ( isset( $prefixes[$prefix] ) ) { + $this->fail( "Module prefix '{$prefix}' is shared between {$class} and {$prefixes[$prefix]}" ); + } + $prefixes[$module->getModulePrefix()] = $class; + } + $this->assertTrue( true ); // dummy call to make this test non-incomplete + } +} diff --git a/tests/phpunit/includes/api/RandomImageGenerator.php b/tests/phpunit/includes/api/RandomImageGenerator.php new file mode 100644 index 00000000..59756b21 --- /dev/null +++ b/tests/phpunit/includes/api/RandomImageGenerator.php @@ -0,0 +1,468 @@ + + */ + +/** + * RandomImageGenerator: does what it says on the tin. + * Can fetch a random image, or also write a number of them to disk with random filenames. + */ +class RandomImageGenerator { + + private $dictionaryFile; + private $minWidth = 400; + private $maxWidth = 800; + private $minHeight = 400; + private $maxHeight = 800; + private $shapesToDraw = 5; + + /** + * Orientations: 0th row, 0th column, Exif orientation code, rotation 2x2 matrix that is opposite of orientation + * n.b. we do not handle the 'flipped' orientations, which is why there is no entry for 2, 4, 5, or 7. Those + * seem to be rare in real images anyway + * (we also would need a non-symmetric shape for the images to test those, like a letter F) + */ + private static $orientations = array( + array( + '0thRow' => 'top', + '0thCol' => 'left', + 'exifCode' => 1, + 'counterRotation' => array( array( 1, 0 ), array( 0, 1 ) ) + ), + array( + '0thRow' => 'bottom', + '0thCol' => 'right', + 'exifCode' => 3, + 'counterRotation' => array( array( -1, 0 ), array( 0, -1 ) ) + ), + array( + '0thRow' => 'right', + '0thCol' => 'top', + 'exifCode' => 6, + 'counterRotation' => array( array( 0, 1 ), array( 1, 0 ) ) + ), + array( + '0thRow' => 'left', + '0thCol' => 'bottom', + 'exifCode' => 8, + 'counterRotation' => array( array( 0, -1 ), array( -1, 0 ) ) + ) + ); + + + public function __construct( $options = array() ) { + foreach ( array( 'dictionaryFile', 'minWidth', 'minHeight', 'maxWidth', 'maxHeight', 'shapesToDraw' ) as $property ) { + if ( isset( $options[$property] ) ) { + $this->$property = $options[$property]; + } + } + + // find the dictionary file, to generate random names + if ( !isset( $this->dictionaryFile ) ) { + foreach ( + array( + '/usr/share/dict/words', + '/usr/dict/words', + __DIR__ . '/words.txt' + ) as $dictionaryFile + ) { + if ( is_file( $dictionaryFile ) and is_readable( $dictionaryFile ) ) { + $this->dictionaryFile = $dictionaryFile; + break; + } + } + } + if ( !isset( $this->dictionaryFile ) ) { + throw new Exception( "RandomImageGenerator: dictionary file not found or not specified properly" ); + } + } + + /** + * Writes random images with random filenames to disk in the directory you specify, or current working directory + * + * @param $number Integer: number of filenames to write + * @param $format String: optional, must be understood by ImageMagick, such as 'jpg' or 'gif' + * @param $dir String: directory, optional (will default to current working directory) + * @return Array: filenames we just wrote + */ + function writeImages( $number, $format = 'jpg', $dir = null ) { + $filenames = $this->getRandomFilenames( $number, $format, $dir ); + $imageWriteMethod = $this->getImageWriteMethod( $format ); + foreach ( $filenames as $filename ) { + $this->{$imageWriteMethod}( $this->getImageSpec(), $format, $filename ); + } + + return $filenames; + } + + + /** + * Figure out how we write images. This is a factor of both format and the local system + * @param $format (a typical extension like 'svg', 'jpg', etc.) + */ + function getImageWriteMethod( $format ) { + global $wgUseImageMagick, $wgImageMagickConvertCommand; + if ( $format === 'svg' ) { + return 'writeSvg'; + } else { + // figure out how to write images + global $wgExiv2Command; + if ( class_exists( 'Imagick' ) && $wgExiv2Command && is_executable( $wgExiv2Command ) ) { + return 'writeImageWithApi'; + } elseif ( $wgUseImageMagick && $wgImageMagickConvertCommand && is_executable( $wgImageMagickConvertCommand ) ) { + return 'writeImageWithCommandLine'; + } + } + throw new Exception( "RandomImageGenerator: could not find a suitable method to write images in '$format' format" ); + } + + /** + * Return a number of randomly-generated filenames + * Each filename uses two words randomly drawn from the dictionary, like elephantine_spatula.jpg + * + * @param $number Integer: of filenames to generate + * @param $extension String: optional, defaults to 'jpg' + * @param $dir String: optional, defaults to current working directory + * @return Array: of filenames + */ + private function getRandomFilenames( $number, $extension = 'jpg', $dir = null ) { + if ( is_null( $dir ) ) { + $dir = getcwd(); + } + $filenames = array(); + foreach ( $this->getRandomWordPairs( $number ) as $pair ) { + $basename = $pair[0] . '_' . $pair[1]; + if ( !is_null( $extension ) ) { + $basename .= '.' . $extension; + } + $basename = preg_replace( '/\s+/', '', $basename ); + $filenames[] = "$dir/$basename"; + } + + return $filenames; + } + + + /** + * Generate data representing an image of random size (within limits), + * consisting of randomly colored and sized upward pointing triangles against a random background color + * (This data is used in the writeImage* methods). + * @return {Mixed} + */ + public function getImageSpec() { + $spec = array(); + + $spec['width'] = mt_rand( $this->minWidth, $this->maxWidth ); + $spec['height'] = mt_rand( $this->minHeight, $this->maxHeight ); + $spec['fill'] = $this->getRandomColor(); + + $diagonalLength = sqrt( pow( $spec['width'], 2 ) + pow( $spec['height'], 2 ) ); + + $draws = array(); + for ( $i = 0; $i <= $this->shapesToDraw; $i++ ) { + $radius = mt_rand( 0, $diagonalLength / 4 ); + if ( $radius == 0 ) { + continue; + } + $originX = mt_rand( -1 * $radius, $spec['width'] + $radius ); + $originY = mt_rand( -1 * $radius, $spec['height'] + $radius ); + $angle = mt_rand( 0, ( 3.141592 / 2 ) * $radius ) / $radius; + $legDeltaX = round( $radius * sin( $angle ) ); + $legDeltaY = round( $radius * cos( $angle ) ); + + $draw = array(); + $draw['fill'] = $this->getRandomColor(); + $draw['shape'] = array( + array( 'x' => $originX, 'y' => $originY - $radius ), + array( 'x' => $originX + $legDeltaX, 'y' => $originY + $legDeltaY ), + array( 'x' => $originX - $legDeltaX, 'y' => $originY + $legDeltaY ), + array( 'x' => $originX, 'y' => $originY - $radius ) + ); + $draws[] = $draw; + } + + $spec['draws'] = $draws; + + return $spec; + } + + /** + * Given array( array('x' => 10, 'y' => 20), array( 'x' => 30, y=> 5 ) ) + * returns "10,20 30,5" + * Useful for SVG and imagemagick command line arguments + * @param $shape: Array of arrays, each array containing x & y keys mapped to numeric values + * @return string + */ + static function shapePointsToString( $shape ) { + $points = array(); + foreach ( $shape as $point ) { + $points[] = $point['x'] . ',' . $point['y']; + } + + return join( " ", $points ); + } + + /** + * Based on image specification, write a very simple SVG file to disk. + * Ignores the background spec because transparency is cool. :) + * @param $spec: spec describing background and shapes to draw + * @param $format: file format to write (which is obviously always svg here) + * @param $filename: filename to write to + */ + public function writeSvg( $spec, $format, $filename ) { + $svg = new SimpleXmlElement( '' ); + $svg->addAttribute( 'xmlns', 'http://www.w3.org/2000/svg' ); + $svg->addAttribute( 'version', '1.1' ); + $svg->addAttribute( 'width', $spec['width'] ); + $svg->addAttribute( 'height', $spec['height'] ); + $g = $svg->addChild( 'g' ); + foreach ( $spec['draws'] as $drawSpec ) { + $shape = $g->addChild( 'polygon' ); + $shape->addAttribute( 'fill', $drawSpec['fill'] ); + $shape->addAttribute( 'points', self::shapePointsToString( $drawSpec['shape'] ) ); + } + + if ( !$fh = fopen( $filename, 'w' ) ) { + throw new Exception( "couldn't open $filename for writing" ); + } + fwrite( $fh, $svg->asXML() ); + if ( !fclose( $fh ) ) { + throw new Exception( "couldn't close $filename" ); + } + } + + /** + * Based on an image specification, write such an image to disk, using Imagick PHP extension + * @param $spec: spec describing background and circles to draw + * @param $format: file format to write + * @param $filename: filename to write to + */ + public function writeImageWithApi( $spec, $format, $filename ) { + // this is a hack because I can't get setImageOrientation() to work. See below. + global $wgExiv2Command; + + $image = new Imagick(); + /** + * If the format is 'jpg', will also add a random orientation -- the image will be drawn rotated with triangle points + * facing in some direction (0, 90, 180 or 270 degrees) and a countering rotation should turn the triangle points upward again + */ + $orientation = self::$orientations[0]; // default is normal orientation + if ( $format == 'jpg' ) { + $orientation = self::$orientations[array_rand( self::$orientations )]; + $spec = self::rotateImageSpec( $spec, $orientation['counterRotation'] ); + } + + $image->newImage( $spec['width'], $spec['height'], new ImagickPixel( $spec['fill'] ) ); + + foreach ( $spec['draws'] as $drawSpec ) { + $draw = new ImagickDraw(); + $draw->setFillColor( $drawSpec['fill'] ); + $draw->polygon( $drawSpec['shape'] ); + $image->drawImage( $draw ); + } + + $image->setImageFormat( $format ); + + // this doesn't work, even though it's documented to do so... + // $image->setImageOrientation( $orientation['exifCode'] ); + + $image->writeImage( $filename ); + + // because the above setImageOrientation call doesn't work... nor can I get an external imagemagick binary to do this either... + // hacking this for now (only works if you have exiv2 installed, a program to read and manipulate exif) + if ( $wgExiv2Command ) { + $cmd = wfEscapeShellArg( $wgExiv2Command ) + . " -M " + . wfEscapeShellArg( "set Exif.Image.Orientation " . $orientation['exifCode'] ) + . " " + . wfEscapeShellArg( $filename ); + + $retval = 0; + $err = wfShellExec( $cmd, $retval ); + if ( $retval !== 0 ) { + print "Error with $cmd: $retval, $err\n"; + } + } + } + + /** + * Given an image specification, produce rotated version + * This is used when simulating a rotated image capture with Exif orientation + * @param $spec Object returned by getImageSpec + * @param $matrix 2x2 transformation matrix + * @return transformed Spec + */ + private static function rotateImageSpec( &$spec, $matrix ) { + $tSpec = array(); + $dims = self::matrixMultiply2x2( $matrix, $spec['width'], $spec['height'] ); + $correctionX = 0; + $correctionY = 0; + if ( $dims['x'] < 0 ) { + $correctionX = abs( $dims['x'] ); + } + if ( $dims['y'] < 0 ) { + $correctionY = abs( $dims['y'] ); + } + $tSpec['width'] = abs( $dims['x'] ); + $tSpec['height'] = abs( $dims['y'] ); + $tSpec['fill'] = $spec['fill']; + $tSpec['draws'] = array(); + foreach ( $spec['draws'] as $draw ) { + $tDraw = array( + 'fill' => $draw['fill'], + 'shape' => array() + ); + foreach ( $draw['shape'] as $point ) { + $tPoint = self::matrixMultiply2x2( $matrix, $point['x'], $point['y'] ); + $tPoint['x'] += $correctionX; + $tPoint['y'] += $correctionY; + $tDraw['shape'][] = $tPoint; + } + $tSpec['draws'][] = $tDraw; + } + + return $tSpec; + } + + /** + * Given a matrix and a pair of images, return new position + * @param $matrix: 2x2 rotation matrix + * @param $x: x-coordinate number + * @param $y: y-coordinate number + * @return Array transformed with properties x, y + */ + private static function matrixMultiply2x2( $matrix, $x, $y ) { + return array( + 'x' => $x * $matrix[0][0] + $y * $matrix[0][1], + 'y' => $x * $matrix[1][0] + $y * $matrix[1][1] + ); + } + + + /** + * Based on an image specification, write such an image to disk, using the command line ImageMagick program ('convert'). + * + * Sample command line: + * $ convert -size 100x60 xc:rgb(90,87,45) \ + * -draw 'fill rgb(12,34,56) polygon 41,39 44,57 50,57 41,39' \ + * -draw 'fill rgb(99,123,231) circle 59,39 56,57' \ + * -draw 'fill rgb(240,12,32) circle 50,21 50,3' filename.png + * + * @param $spec: spec describing background and shapes to draw + * @param $format: file format to write (unused by this method but kept so it has the same signature as writeImageWithApi) + * @param $filename: filename to write to + */ + public function writeImageWithCommandLine( $spec, $format, $filename ) { + global $wgImageMagickConvertCommand; + $args = array(); + $args[] = "-size " . wfEscapeShellArg( $spec['width'] . 'x' . $spec['height'] ); + $args[] = wfEscapeShellArg( "xc:" . $spec['fill'] ); + foreach ( $spec['draws'] as $draw ) { + $fill = $draw['fill']; + $polygon = self::shapePointsToString( $draw['shape'] ); + $drawCommand = "fill $fill polygon $polygon"; + $args[] = '-draw ' . wfEscapeShellArg( $drawCommand ); + } + $args[] = wfEscapeShellArg( $filename ); + + $command = wfEscapeShellArg( $wgImageMagickConvertCommand ) . " " . implode( " ", $args ); + $retval = null; + wfShellExec( $command, $retval ); + + return ( $retval === 0 ); + } + + /** + * Generate a string of random colors for ImageMagick or SVG, like "rgb(12, 37, 98)" + * + * @return {String} + */ + public function getRandomColor() { + $components = array(); + for ( $i = 0; $i <= 2; $i++ ) { + $components[] = mt_rand( 0, 255 ); + } + + return 'rgb(' . join( ', ', $components ) . ')'; + } + + /** + * Get an array of random pairs of random words, like array( array( 'foo', 'bar' ), array( 'quux', 'baz' ) ); + * + * @param $number Integer: number of pairs + * @return Array: of two-element arrays + */ + private function getRandomWordPairs( $number ) { + $lines = $this->getRandomLines( $number * 2 ); + // construct pairs of words + $pairs = array(); + $count = count( $lines ); + for ( $i = 0; $i < $count; $i += 2 ) { + $pairs[] = array( $lines[$i], $lines[$i + 1] ); + } + + return $pairs; + } + + /** + * Return N random lines from a file + * + * Will throw exception if the file could not be read or if it had fewer lines than requested. + * + * @param $number_desired Integer: number of lines desired + * @return Array: of exactly n elements, drawn randomly from lines the file + */ + private function getRandomLines( $number_desired ) { + $filepath = $this->dictionaryFile; + + // initialize array of lines + $lines = array(); + for ( $i = 0; $i < $number_desired; $i++ ) { + $lines[] = null; + } + + /* + * This algorithm obtains N random lines from a file in one single pass. It does this by replacing elements of + * a fixed-size array of lines, less and less frequently as it reads the file. + */ + $fh = fopen( $filepath, "r" ); + if ( !$fh ) { + throw new Exception( "couldn't open $filepath" ); + } + $line_number = 0; + $max_index = $number_desired - 1; + while ( !feof( $fh ) ) { + $line = fgets( $fh ); + if ( $line !== false ) { + $line_number++; + $line = trim( $line ); + if ( mt_rand( 0, $line_number ) <= $max_index ) { + $lines[mt_rand( 0, $max_index )] = $line; + } + } + } + fclose( $fh ); + if ( $line_number < $number_desired ) { + throw new Exception( "not enough lines in $filepath" ); + } + + return $lines; + } +} diff --git a/tests/phpunit/includes/api/format/ApiFormatPhpTest.php b/tests/phpunit/includes/api/format/ApiFormatPhpTest.php new file mode 100644 index 00000000..a0bbb2dc --- /dev/null +++ b/tests/phpunit/includes/api/format/ApiFormatPhpTest.php @@ -0,0 +1,17 @@ +apiRequest( 'php', array( 'action' => 'query', 'meta' => 'siteinfo' ) ); + + $this->assertInternalType( 'array', unserialize( $data ) ); + $this->assertGreaterThan( 0, count( (array)$data ) ); + } +} diff --git a/tests/phpunit/includes/api/format/ApiFormatTestBase.php b/tests/phpunit/includes/api/format/ApiFormatTestBase.php new file mode 100644 index 00000000..153f2cf4 --- /dev/null +++ b/tests/phpunit/includes/api/format/ApiFormatTestBase.php @@ -0,0 +1,22 @@ +createPrinterByName( $format ); + $printer->setUnescapeAmps( false ); + + $printer->initPrinter( false ); + + ob_start(); + $printer->execute(); + $out = ob_get_clean(); + + $printer->closePrinter(); + + return $out; + } +} diff --git a/tests/phpunit/includes/api/generateRandomImages.php b/tests/phpunit/includes/api/generateRandomImages.php new file mode 100644 index 00000000..87f5c4c0 --- /dev/null +++ b/tests/phpunit/includes/api/generateRandomImages.php @@ -0,0 +1,46 @@ +writeImages( $number, $format ); + } +} + +$maintClass = 'GenerateRandomImages'; +require RUN_MAINTENANCE_IF_MAIN; diff --git a/tests/phpunit/includes/api/query/ApiQueryBasicTest.php b/tests/phpunit/includes/api/query/ApiQueryBasicTest.php new file mode 100644 index 00000000..1a2aa832 --- /dev/null +++ b/tests/phpunit/includes/api/query/ApiQueryBasicTest.php @@ -0,0 +1,395 @@ +@gmail.com" + * + * 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 + */ + +require_once 'ApiQueryTestBase.php'; + +/** These tests validate basic functionality of the api query module + * + * @group API + * @group Database + * @group medium + */ +class ApiQueryBasicTest extends ApiQueryTestBase { + /** + * Create a set of pages. These must not change, otherwise the tests might give wrong results. + * @see MediaWikiTestCase::addDBData() + */ + function addDBData() { + try { + if ( Title::newFromText( 'AQBT-All' )->exists() ) { + return; + } + + // Ordering is important, as it will be returned in the same order as stored in the index + $this->editPage( 'AQBT-All', '[[Category:AQBT-Cat]] [[AQBT-Links]] {{AQBT-T}}' ); + $this->editPage( 'AQBT-Categories', '[[Category:AQBT-Cat]]' ); + $this->editPage( 'AQBT-Links', '[[AQBT-All]] [[AQBT-Categories]] [[AQBT-Templates]]' ); + $this->editPage( 'AQBT-Templates', '{{AQBT-T}}' ); + $this->editPage( 'AQBT-T', 'Content', '', NS_TEMPLATE ); + + // Refresh due to the bug with listing transclusions as links if they don't exist + $this->editPage( 'AQBT-All', '[[Category:AQBT-Cat]] [[AQBT-Links]] {{AQBT-T}}' ); + $this->editPage( 'AQBT-Templates', '{{AQBT-T}}' ); + } catch ( Exception $e ) { + $this->exceptionFromAddDBData = $e; + } + } + + private static $links = array( + array( 'prop' => 'links', 'titles' => 'AQBT-All' ), + array( 'pages' => array( + '1' => array( + 'pageid' => 1, + 'ns' => 0, + 'title' => 'AQBT-All', + 'links' => array( + array( 'ns' => 0, 'title' => 'AQBT-Links' ), + ) + ) + ) ) + ); + + private static $templates = array( + array( 'prop' => 'templates', 'titles' => 'AQBT-All' ), + array( 'pages' => array( + '1' => array( + 'pageid' => 1, + 'ns' => 0, + 'title' => 'AQBT-All', + 'templates' => array( + array( 'ns' => 10, 'title' => 'Template:AQBT-T' ), + ) + ) + ) ) + ); + + private static $categories = array( + array( 'prop' => 'categories', 'titles' => 'AQBT-All' ), + array( 'pages' => array( + '1' => array( + 'pageid' => 1, + 'ns' => 0, + 'title' => 'AQBT-All', + 'categories' => array( + array( 'ns' => 14, 'title' => 'Category:AQBT-Cat' ), + ) + ) + ) ) + ); + + private static $allpages = array( + array( 'list' => 'allpages', 'apprefix' => 'AQBT-' ), + array( 'allpages' => array( + array( 'pageid' => 1, 'ns' => 0, 'title' => 'AQBT-All' ), + array( 'pageid' => 2, 'ns' => 0, 'title' => 'AQBT-Categories' ), + array( 'pageid' => 3, 'ns' => 0, 'title' => 'AQBT-Links' ), + array( 'pageid' => 4, 'ns' => 0, 'title' => 'AQBT-Templates' ), + ) ) + ); + + private static $alllinks = array( + array( 'list' => 'alllinks', 'alprefix' => 'AQBT-' ), + array( 'alllinks' => array( + array( 'ns' => 0, 'title' => 'AQBT-All' ), + array( 'ns' => 0, 'title' => 'AQBT-Categories' ), + array( 'ns' => 0, 'title' => 'AQBT-Links' ), + array( 'ns' => 0, 'title' => 'AQBT-Templates' ), + ) ) + ); + + private static $alltransclusions = array( + array( 'list' => 'alltransclusions', 'atprefix' => 'AQBT-' ), + array( 'alltransclusions' => array( + array( 'ns' => 10, 'title' => 'Template:AQBT-T' ), + array( 'ns' => 10, 'title' => 'Template:AQBT-T' ), + ) ) + ); + + private static $allcategories = array( + array( 'list' => 'allcategories', 'acprefix' => 'AQBT-' ), + array( 'allcategories' => array( + array( '*' => 'AQBT-Cat' ), + ) ) + ); + + private static $backlinks = array( + array( 'list' => 'backlinks', 'bltitle' => 'AQBT-Links' ), + array( 'backlinks' => array( + array( 'pageid' => 1, 'ns' => 0, 'title' => 'AQBT-All' ), + ) ) + ); + + private static $embeddedin = array( + array( 'list' => 'embeddedin', 'eititle' => 'Template:AQBT-T' ), + array( 'embeddedin' => array( + array( 'pageid' => 1, 'ns' => 0, 'title' => 'AQBT-All' ), + array( 'pageid' => 4, 'ns' => 0, 'title' => 'AQBT-Templates' ), + ) ) + ); + + private static $categorymembers = array( + array( 'list' => 'categorymembers', 'cmtitle' => 'Category:AQBT-Cat' ), + array( 'categorymembers' => array( + array( 'pageid' => 1, 'ns' => 0, 'title' => 'AQBT-All' ), + array( 'pageid' => 2, 'ns' => 0, 'title' => 'AQBT-Categories' ), + ) ) + ); + + private static $generatorAllpages = array( + array( 'generator' => 'allpages', 'gapprefix' => 'AQBT-' ), + array( 'pages' => array( + '1' => array( + 'pageid' => 1, + 'ns' => 0, + 'title' => 'AQBT-All' ), + '2' => array( + 'pageid' => 2, + 'ns' => 0, + 'title' => 'AQBT-Categories' ), + '3' => array( + 'pageid' => 3, + 'ns' => 0, + 'title' => 'AQBT-Links' ), + '4' => array( + 'pageid' => 4, + 'ns' => 0, + 'title' => 'AQBT-Templates' ), + ) ) + ); + + private static $generatorLinks = array( + array( 'generator' => 'links', 'titles' => 'AQBT-Links' ), + array( 'pages' => array( + '1' => array( + 'pageid' => 1, + 'ns' => 0, + 'title' => 'AQBT-All' ), + '2' => array( + 'pageid' => 2, + 'ns' => 0, + 'title' => 'AQBT-Categories' ), + '4' => array( + 'pageid' => 4, + 'ns' => 0, + 'title' => 'AQBT-Templates' ), + ) ) + ); + + private static $generatorLinksPropLinks = array( + array( 'prop' => 'links' ), + array( 'pages' => array( + '1' => array( 'links' => array( + array( 'ns' => 0, 'title' => 'AQBT-Links' ), + ) ) + ) ) + ); + + private static $generatorLinksPropTemplates = array( + array( 'prop' => 'templates' ), + array( 'pages' => array( + '1' => array( 'templates' => array( + array( 'ns' => 10, 'title' => 'Template:AQBT-T' ) ) ), + '4' => array( 'templates' => array( + array( 'ns' => 10, 'title' => 'Template:AQBT-T' ) ) ), + ) ) + ); + + /** + * Test basic props + */ + public function testProps() { + $this->check( self::$links ); + $this->check( self::$templates ); + $this->check( self::$categories ); + } + + /** + * Test basic lists + */ + public function testLists() { + $this->check( self::$allpages ); + $this->check( self::$alllinks ); + $this->check( self::$alltransclusions ); + // This test is temporarily disabled until a sqlite bug is fixed + // $this->check( self::$allcategories ); + $this->check( self::$backlinks ); + $this->check( self::$embeddedin ); + $this->check( self::$categorymembers ); + } + + /** + * Test basic lists + */ + public function testAllTogether() { + + // All props together + $this->check( $this->merge( + self::$links, + self::$templates, + self::$categories + ) ); + + // All lists together + $this->check( $this->merge( + self::$allpages, + self::$alllinks, + self::$alltransclusions, + // This test is temporarily disabled until a sqlite bug is fixed + // self::$allcategories, + self::$backlinks, + self::$embeddedin, + self::$categorymembers + ) ); + + // All props+lists together + $this->check( $this->merge( + self::$links, + self::$templates, + self::$categories, + self::$allpages, + self::$alllinks, + self::$alltransclusions, + // This test is temporarily disabled until a sqlite bug is fixed + // self::$allcategories, + self::$backlinks, + self::$embeddedin, + self::$categorymembers + ) ); + } + + /** + * Test basic lists + */ + public function testGenerator() { + // generator=allpages + $this->check( self::$generatorAllpages ); + // generator=allpages & list=allpages + $this->check( $this->merge( + self::$generatorAllpages, + self::$allpages ) ); + // generator=links + $this->check( self::$generatorLinks ); + // generator=links & prop=links + $this->check( $this->merge( + self::$generatorLinks, + self::$generatorLinksPropLinks ) ); + // generator=links & prop=templates + $this->check( $this->merge( + self::$generatorLinks, + self::$generatorLinksPropTemplates ) ); + // generator=links & prop=links|templates + $this->check( $this->merge( + self::$generatorLinks, + self::$generatorLinksPropLinks, + self::$generatorLinksPropTemplates ) ); + // generator=links & prop=links|templates & list=allpages|... + $this->check( $this->merge( + self::$generatorLinks, + self::$generatorLinksPropLinks, + self::$generatorLinksPropTemplates, + self::$allpages, + self::$alllinks, + self::$alltransclusions, + // This test is temporarily disabled until a sqlite bug is fixed + // self::$allcategories, + self::$backlinks, + self::$embeddedin, + self::$categorymembers ) ); + } + + /** + * Test bug 51821 + */ + public function testGeneratorRedirects() { + $this->editPage( 'AQBT-Target', 'test' ); + $this->editPage( 'AQBT-Redir', '#REDIRECT [[AQBT-Target]]' ); + $this->check( array( + array( 'generator' => 'backlinks', 'gbltitle' => 'AQBT-Target', 'redirects' => '1' ), + array( + 'redirects' => array( + array( + 'from' => 'AQBT-Redir', + 'to' => 'AQBT-Target', + ) + ), + 'pages' => array( + '6' => array( + 'pageid' => 6, + 'ns' => 0, + 'title' => 'AQBT-Target', + ) + ), + ) + ) ); + } + + /** + * Recursively merges the expected values in the $item into the $all + */ + private function mergeExpected( &$all, $item ) { + foreach ( $item as $k => $v ) { + if ( array_key_exists( $k, $all ) ) { + if ( is_array( $all[$k] ) ) { + $this->mergeExpected( $all[$k], $v ); + } else { + $this->assertEquals( $all[$k], $v ); + } + } else { + $all[$k] = $v; + } + } + } + + /** + * Recursively compare arrays, ignoring mismatches in numeric key and pageids. + * @param $expected array expected values + * @param $result array returned values + */ + private function assertQueryResults( $expected, $result ) { + reset( $expected ); + reset( $result ); + while ( true ) { + $e = each( $expected ); + $r = each( $result ); + // If either of the arrays is shorter, abort. If both are done, success. + $this->assertEquals( (bool)$e, (bool)$r ); + if ( !$e ) { + break; // done + } + // continue only if keys are identical or both keys are numeric + $this->assertTrue( $e['key'] === $r['key'] || ( is_numeric( $e['key'] ) && is_numeric( $r['key'] ) ) ); + // don't compare pageids + if ( $e['key'] !== 'pageid' ) { + // If values are arrays, compare recursively, otherwise compare with === + if ( is_array( $e['value'] ) && is_array( $r['value'] ) ) { + $this->assertQueryResults( $e['value'], $r['value'] ); + } else { + $this->assertEquals( $e['value'], $r['value'] ); + } + } + } + } +} diff --git a/tests/phpunit/includes/api/query/ApiQueryContinue2Test.php b/tests/phpunit/includes/api/query/ApiQueryContinue2Test.php new file mode 100644 index 00000000..4d5ddbae --- /dev/null +++ b/tests/phpunit/includes/api/query/ApiQueryContinue2Test.php @@ -0,0 +1,68 @@ +@gmail.com" + * + * 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 3 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 + */ + +require_once 'ApiQueryContinueTestBase.php'; + +/** + * @group API + * @group Database + * @group medium + */ +class ApiQueryContinue2Test extends ApiQueryContinueTestBase { + /** + * Create a set of pages. These must not change, otherwise the tests might give wrong results. + * @see MediaWikiTestCase::addDBData() + */ + function addDBData() { + try { + $this->editPage( 'AQCT73462-A', '**AQCT73462-A** [[AQCT73462-B]] [[AQCT73462-C]]' ); + $this->editPage( 'AQCT73462-B', '[[AQCT73462-A]] **AQCT73462-B** [[AQCT73462-C]]' ); + $this->editPage( 'AQCT73462-C', '[[AQCT73462-A]] [[AQCT73462-B]] **AQCT73462-C**' ); + $this->editPage( 'AQCT73462-A', '**AQCT73462-A** [[AQCT73462-B]] [[AQCT73462-C]]' ); + $this->editPage( 'AQCT73462-B', '[[AQCT73462-A]] **AQCT73462-B** [[AQCT73462-C]]' ); + $this->editPage( 'AQCT73462-C', '[[AQCT73462-A]] [[AQCT73462-B]] **AQCT73462-C**' ); + } catch ( Exception $e ) { + $this->exceptionFromAddDBData = $e; + } + } + + /** + * @medium + */ + public function testA() { + $this->mVerbose = false; + $mk = function ( $g, $p, $gDir ) { + return array( + 'generator' => 'allpages', + 'gapprefix' => 'AQCT73462-', + 'prop' => 'links', + 'gaplimit' => "$g", + 'pllimit' => "$p", + 'gapdir' => $gDir ? "ascending" : "descending", + ); + }; + // generator + 1 prop + 1 list + $data = $this->query( $mk( 99, 99, true ), 1, 'g1p', false ); + $this->checkC( $data, $mk( 1, 1, true ), 6, 'g1p-11t' ); + $this->checkC( $data, $mk( 2, 2, true ), 3, 'g1p-22t' ); + $this->checkC( $data, $mk( 1, 1, false ), 6, 'g1p-11f' ); + $this->checkC( $data, $mk( 2, 2, false ), 3, 'g1p-22f' ); + } +} diff --git a/tests/phpunit/includes/api/query/ApiQueryContinueTest.php b/tests/phpunit/includes/api/query/ApiQueryContinueTest.php new file mode 100644 index 00000000..f494e9ca --- /dev/null +++ b/tests/phpunit/includes/api/query/ApiQueryContinueTest.php @@ -0,0 +1,313 @@ +@gmail.com" + * + * 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 + */ + +require_once 'ApiQueryContinueTestBase.php'; + +/** + * These tests validate the new continue functionality of the api query module by + * doing multiple requests with varying parameters, merging the results, and checking + * that the result matches the full data received in one no-limits call. + * + * @group API + * @group Database + * @group medium + */ +class ApiQueryContinueTest extends ApiQueryContinueTestBase { + /** + * Create a set of pages. These must not change, otherwise the tests might give wrong results. + * @see MediaWikiTestCase::addDBData() + */ + function addDBData() { + try { + $this->editPage( 'Template:AQCT-T1', '**Template:AQCT-T1**' ); + $this->editPage( 'Template:AQCT-T2', '**Template:AQCT-T2**' ); + $this->editPage( 'Template:AQCT-T3', '**Template:AQCT-T3**' ); + $this->editPage( 'Template:AQCT-T4', '**Template:AQCT-T4**' ); + $this->editPage( 'Template:AQCT-T5', '**Template:AQCT-T5**' ); + + $this->editPage( 'AQCT-1', '**AQCT-1** {{AQCT-T2}} {{AQCT-T3}} {{AQCT-T4}} {{AQCT-T5}}' ); + $this->editPage( 'AQCT-2', '[[AQCT-1]] **AQCT-2** {{AQCT-T3}} {{AQCT-T4}} {{AQCT-T5}}' ); + $this->editPage( 'AQCT-3', '[[AQCT-1]] [[AQCT-2]] **AQCT-3** {{AQCT-T4}} {{AQCT-T5}}' ); + $this->editPage( 'AQCT-4', '[[AQCT-1]] [[AQCT-2]] [[AQCT-3]] **AQCT-4** {{AQCT-T5}}' ); + $this->editPage( 'AQCT-5', '[[AQCT-1]] [[AQCT-2]] [[AQCT-3]] [[AQCT-4]] **AQCT-5**' ); + } catch ( Exception $e ) { + $this->exceptionFromAddDBData = $e; + } + } + + /** + * Test smart continue - list=allpages + * @medium + */ + public function test1List() { + $this->mVerbose = false; + $mk = function ( $l ) { + return array( + 'list' => 'allpages', + 'apprefix' => 'AQCT-', + 'aplimit' => "$l", + ); + }; + $data = $this->query( $mk( 99 ), 1, '1L', false ); + + // 1 list + $this->checkC( $data, $mk( 1 ), 5, '1L-1' ); + $this->checkC( $data, $mk( 2 ), 3, '1L-2' ); + $this->checkC( $data, $mk( 3 ), 2, '1L-3' ); + $this->checkC( $data, $mk( 4 ), 2, '1L-4' ); + $this->checkC( $data, $mk( 5 ), 1, '1L-5' ); + } + + /** + * Test smart continue - list=allpages|alltransclusions + * @medium + */ + public function test2Lists() { + $this->mVerbose = false; + $mk = function ( $l1, $l2 ) { + return array( + 'list' => 'allpages|alltransclusions', + 'apprefix' => 'AQCT-', + 'atprefix' => 'AQCT-', + 'atunique' => '', + 'aplimit' => "$l1", + 'atlimit' => "$l2", + ); + }; + // 2 lists + $data = $this->query( $mk( 99, 99 ), 1, '2L', false ); + $this->checkC( $data, $mk( 1, 1 ), 5, '2L-11' ); + $this->checkC( $data, $mk( 2, 2 ), 3, '2L-22' ); + $this->checkC( $data, $mk( 3, 3 ), 2, '2L-33' ); + $this->checkC( $data, $mk( 4, 4 ), 2, '2L-44' ); + $this->checkC( $data, $mk( 5, 5 ), 1, '2L-55' ); + } + + /** + * Test smart continue - generator=allpages, prop=links + * @medium + */ + public function testGen1Prop() { + $this->mVerbose = false; + $mk = function ( $g, $p ) { + return array( + 'generator' => 'allpages', + 'gapprefix' => 'AQCT-', + 'gaplimit' => "$g", + 'prop' => 'links', + 'pllimit' => "$p", + ); + }; + // generator + 1 prop + $data = $this->query( $mk( 99, 99 ), 1, 'G1P', false ); + $this->checkC( $data, $mk( 1, 1 ), 11, 'G1P-11' ); + $this->checkC( $data, $mk( 2, 2 ), 6, 'G1P-22' ); + $this->checkC( $data, $mk( 3, 3 ), 4, 'G1P-33' ); + $this->checkC( $data, $mk( 4, 4 ), 3, 'G1P-44' ); + $this->checkC( $data, $mk( 5, 5 ), 2, 'G1P-55' ); + } + + /** + * Test smart continue - generator=allpages, prop=links|templates + * @medium + */ + public function testGen2Prop() { + $this->mVerbose = false; + $mk = function ( $g, $p1, $p2 ) { + return array( + 'generator' => 'allpages', + 'gapprefix' => 'AQCT-', + 'gaplimit' => "$g", + 'prop' => 'links|templates', + 'pllimit' => "$p1", + 'tllimit' => "$p2", + ); + }; + // generator + 2 props + $data = $this->query( $mk( 99, 99, 99 ), 1, 'G2P', false ); + $this->checkC( $data, $mk( 1, 1, 1 ), 16, 'G2P-111' ); + $this->checkC( $data, $mk( 2, 2, 2 ), 9, 'G2P-222' ); + $this->checkC( $data, $mk( 3, 3, 3 ), 6, 'G2P-333' ); + $this->checkC( $data, $mk( 4, 4, 4 ), 4, 'G2P-444' ); + $this->checkC( $data, $mk( 5, 5, 5 ), 2, 'G2P-555' ); + $this->checkC( $data, $mk( 5, 1, 1 ), 10, 'G2P-511' ); + $this->checkC( $data, $mk( 4, 2, 2 ), 7, 'G2P-422' ); + $this->checkC( $data, $mk( 2, 3, 3 ), 7, 'G2P-233' ); + $this->checkC( $data, $mk( 2, 4, 4 ), 5, 'G2P-244' ); + $this->checkC( $data, $mk( 1, 5, 5 ), 5, 'G2P-155' ); + } + + /** + * Test smart continue - generator=allpages, prop=links, list=alltransclusions + * @medium + */ + public function testGen1Prop1List() { + $this->mVerbose = false; + $mk = function ( $g, $p, $l ) { + return array( + 'generator' => 'allpages', + 'gapprefix' => 'AQCT-', + 'gaplimit' => "$g", + 'prop' => 'links', + 'pllimit' => "$p", + 'list' => 'alltransclusions', + 'atprefix' => 'AQCT-', + 'atunique' => '', + 'atlimit' => "$l", + ); + }; + // generator + 1 prop + 1 list + $data = $this->query( $mk( 99, 99, 99 ), 1, 'G1P1L', false ); + $this->checkC( $data, $mk( 1, 1, 1 ), 11, 'G1P1L-111' ); + $this->checkC( $data, $mk( 2, 2, 2 ), 6, 'G1P1L-222' ); + $this->checkC( $data, $mk( 3, 3, 3 ), 4, 'G1P1L-333' ); + $this->checkC( $data, $mk( 4, 4, 4 ), 3, 'G1P1L-444' ); + $this->checkC( $data, $mk( 5, 5, 5 ), 2, 'G1P1L-555' ); + $this->checkC( $data, $mk( 5, 5, 1 ), 4, 'G1P1L-551' ); + $this->checkC( $data, $mk( 5, 5, 2 ), 2, 'G1P1L-552' ); + } + + /** + * Test smart continue - generator=allpages, prop=links|templates, + * list=alllinks|alltransclusions, meta=siteinfo + * @medium + */ + public function testGen2Prop2List1Meta() { + $this->mVerbose = false; + $mk = function ( $g, $p1, $p2, $l1, $l2 ) { + return array( + 'generator' => 'allpages', + 'gapprefix' => 'AQCT-', + 'gaplimit' => "$g", + 'prop' => 'links|templates', + 'pllimit' => "$p1", + 'tllimit' => "$p2", + 'list' => 'alllinks|alltransclusions', + 'alprefix' => 'AQCT-', + 'alunique' => '', + 'allimit' => "$l1", + 'atprefix' => 'AQCT-', + 'atunique' => '', + 'atlimit' => "$l2", + 'meta' => 'siteinfo', + 'siprop' => 'namespaces', + ); + }; + // generator + 1 prop + 1 list + $data = $this->query( $mk( 99, 99, 99, 99, 99 ), 1, 'G2P2L1M', false ); + $this->checkC( $data, $mk( 1, 1, 1, 1, 1 ), 16, 'G2P2L1M-11111' ); + $this->checkC( $data, $mk( 2, 2, 2, 2, 2 ), 9, 'G2P2L1M-22222' ); + $this->checkC( $data, $mk( 3, 3, 3, 3, 3 ), 6, 'G2P2L1M-33333' ); + $this->checkC( $data, $mk( 4, 4, 4, 4, 4 ), 4, 'G2P2L1M-44444' ); + $this->checkC( $data, $mk( 5, 5, 5, 5, 5 ), 2, 'G2P2L1M-55555' ); + $this->checkC( $data, $mk( 5, 5, 5, 1, 1 ), 4, 'G2P2L1M-55511' ); + $this->checkC( $data, $mk( 5, 5, 5, 2, 2 ), 2, 'G2P2L1M-55522' ); + $this->checkC( $data, $mk( 5, 1, 1, 5, 5 ), 10, 'G2P2L1M-51155' ); + $this->checkC( $data, $mk( 5, 2, 2, 5, 5 ), 5, 'G2P2L1M-52255' ); + } + + /** + * Test smart continue - generator=templates, prop=templates + * @medium + */ + public function testSameGenAndProp() { + $this->mVerbose = false; + $mk = function ( $g, $gDir, $p, $pDir ) { + return array( + 'titles' => 'AQCT-1', + 'generator' => 'templates', + 'gtllimit' => "$g", + 'gtldir' => $gDir ? 'ascending' : 'descending', + 'prop' => 'templates', + 'tllimit' => "$p", + 'tldir' => $pDir ? 'ascending' : 'descending', + ); + }; + // generator + 1 prop + $data = $this->query( $mk( 99, true, 99, true ), 1, 'G=P', false ); + + $this->checkC( $data, $mk( 1, true, 1, true ), 4, 'G=P-1t1t' ); + $this->checkC( $data, $mk( 2, true, 2, true ), 2, 'G=P-2t2t' ); + $this->checkC( $data, $mk( 3, true, 3, true ), 2, 'G=P-3t3t' ); + $this->checkC( $data, $mk( 1, true, 3, true ), 4, 'G=P-1t3t' ); + $this->checkC( $data, $mk( 3, true, 1, true ), 2, 'G=P-3t1t' ); + + $this->checkC( $data, $mk( 1, true, 1, false ), 4, 'G=P-1t1f' ); + $this->checkC( $data, $mk( 2, true, 2, false ), 2, 'G=P-2t2f' ); + $this->checkC( $data, $mk( 3, true, 3, false ), 2, 'G=P-3t3f' ); + $this->checkC( $data, $mk( 1, true, 3, false ), 4, 'G=P-1t3f' ); + $this->checkC( $data, $mk( 3, true, 1, false ), 2, 'G=P-3t1f' ); + + $this->checkC( $data, $mk( 1, false, 1, true ), 4, 'G=P-1f1t' ); + $this->checkC( $data, $mk( 2, false, 2, true ), 2, 'G=P-2f2t' ); + $this->checkC( $data, $mk( 3, false, 3, true ), 2, 'G=P-3f3t' ); + $this->checkC( $data, $mk( 1, false, 3, true ), 4, 'G=P-1f3t' ); + $this->checkC( $data, $mk( 3, false, 1, true ), 2, 'G=P-3f1t' ); + + $this->checkC( $data, $mk( 1, false, 1, false ), 4, 'G=P-1f1f' ); + $this->checkC( $data, $mk( 2, false, 2, false ), 2, 'G=P-2f2f' ); + $this->checkC( $data, $mk( 3, false, 3, false ), 2, 'G=P-3f3f' ); + $this->checkC( $data, $mk( 1, false, 3, false ), 4, 'G=P-1f3f' ); + $this->checkC( $data, $mk( 3, false, 1, false ), 2, 'G=P-3f1f' ); + } + + /** + * Test smart continue - generator=allpages, list=allpages + * @medium + */ + public function testSameGenList() { + $this->mVerbose = false; + $mk = function ( $g, $gDir, $l, $pDir ) { + return array( + 'generator' => 'allpages', + 'gapprefix' => 'AQCT-', + 'gaplimit' => "$g", + 'gapdir' => $gDir ? 'ascending' : 'descending', + 'list' => 'allpages', + 'apprefix' => 'AQCT-', + 'aplimit' => "$l", + 'apdir' => $pDir ? 'ascending' : 'descending', + ); + }; + // generator + 1 list + $data = $this->query( $mk( 99, true, 99, true ), 1, 'G=L', false ); + + $this->checkC( $data, $mk( 1, true, 1, true ), 5, 'G=L-1t1t' ); + $this->checkC( $data, $mk( 2, true, 2, true ), 3, 'G=L-2t2t' ); + $this->checkC( $data, $mk( 3, true, 3, true ), 2, 'G=L-3t3t' ); + $this->checkC( $data, $mk( 1, true, 3, true ), 5, 'G=L-1t3t' ); + $this->checkC( $data, $mk( 3, true, 1, true ), 5, 'G=L-3t1t' ); + $this->checkC( $data, $mk( 1, true, 1, false ), 5, 'G=L-1t1f' ); + $this->checkC( $data, $mk( 2, true, 2, false ), 3, 'G=L-2t2f' ); + $this->checkC( $data, $mk( 3, true, 3, false ), 2, 'G=L-3t3f' ); + $this->checkC( $data, $mk( 1, true, 3, false ), 5, 'G=L-1t3f' ); + $this->checkC( $data, $mk( 3, true, 1, false ), 5, 'G=L-3t1f' ); + $this->checkC( $data, $mk( 1, false, 1, true ), 5, 'G=L-1f1t' ); + $this->checkC( $data, $mk( 2, false, 2, true ), 3, 'G=L-2f2t' ); + $this->checkC( $data, $mk( 3, false, 3, true ), 2, 'G=L-3f3t' ); + $this->checkC( $data, $mk( 1, false, 3, true ), 5, 'G=L-1f3t' ); + $this->checkC( $data, $mk( 3, false, 1, true ), 5, 'G=L-3f1t' ); + $this->checkC( $data, $mk( 1, false, 1, false ), 5, 'G=L-1f1f' ); + $this->checkC( $data, $mk( 2, false, 2, false ), 3, 'G=L-2f2f' ); + $this->checkC( $data, $mk( 3, false, 3, false ), 2, 'G=L-3f3f' ); + $this->checkC( $data, $mk( 1, false, 3, false ), 5, 'G=L-1f3f' ); + $this->checkC( $data, $mk( 3, false, 1, false ), 5, 'G=L-3f1f' ); + } +} diff --git a/tests/phpunit/includes/api/query/ApiQueryContinueTestBase.php b/tests/phpunit/includes/api/query/ApiQueryContinueTestBase.php new file mode 100644 index 00000000..fbb1e640 --- /dev/null +++ b/tests/phpunit/includes/api/query/ApiQueryContinueTestBase.php @@ -0,0 +1,209 @@ +@gmail.com" + * + * 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 + */ + +require_once 'ApiQueryTestBase.php'; + +abstract class ApiQueryContinueTestBase extends ApiQueryTestBase { + + /** + * Enable to print in-depth debugging info during the test run + */ + protected $mVerbose = false; + + /** + * Run query() and compare against expected values + */ + protected function checkC( $expected, $params, $expectedCount, $id, $continue = true ) { + $result = $this->query( $params, $expectedCount, $id, $continue ); + $this->assertResult( $expected, $result, $id ); + } + + /** + * Run query in a loop until no more values are available + * @param array $params api parameters + * @param int $expectedCount max number of iterations + * @param string $id unit test id + * @param boolean $useContinue true to use smart continue + * @return mixed: merged results data array + * @throws Exception + */ + protected function query( $params, $expectedCount, $id, $useContinue = true ) { + if ( isset( $params['action'] ) ) { + $this->assertEquals( 'query', $params['action'], 'Invalid query action' ); + } else { + $params['action'] = 'query'; + } + if ( $useContinue && !isset( $params['continue'] ) ) { + $params['continue'] = ''; + } + $count = 0; + $result = array(); + $continue = array(); + do { + $request = array_merge( $params, $continue ); + uksort( $request, function ( $a, $b ) { + // put 'continue' params at the end - lazy method + $a = strpos( $a, 'continue' ) !== false ? 'zzz ' . $a : $a; + $b = strpos( $b, 'continue' ) !== false ? 'zzz ' . $b : $b; + + return strcmp( $a, $b ); + } ); + $reqStr = http_build_query( $request ); + //$reqStr = str_replace( '&', ' & ', $reqStr ); + $this->assertLessThan( $expectedCount, $count, "$id more data: $reqStr" ); + if ( $this->mVerbose ) { + print "$id (#$count): $reqStr\n"; + } + try { + $data = $this->doApiRequest( $request ); + } catch ( Exception $e ) { + throw new Exception( "$id on $count", 0, $e ); + } + $data = $data[0]; + if ( isset( $data['warnings'] ) ) { + $warnings = json_encode( $data['warnings'] ); + $this->fail( "$id Warnings on #$count in $reqStr\n$warnings" ); + } + $this->assertArrayHasKey( 'query', $data, "$id no 'query' on #$count in $reqStr" ); + if ( isset( $data['continue'] ) ) { + $continue = $data['continue']; + unset( $data['continue'] ); + } else { + $continue = array(); + } + if ( $this->mVerbose ) { + $this->printResult( $data ); + } + $this->mergeResult( $result, $data ); + $count++; + if ( empty( $continue ) ) { + // $this->assertEquals( $expectedCount, $count, "$id finished early" ); + if ( $expectedCount > $count ) { + print "***** $id Finished early in $count turns. $expectedCount was expected\n"; + } + + return $result; + } elseif ( !$useContinue ) { + $this->assertFalse( 'Non-smart query must be requested all at once' ); + } + } while ( true ); + } + + private function printResult( $data ) { + $q = $data['query']; + $print = array(); + if ( isset( $q['pages'] ) ) { + foreach ( $q['pages'] as $p ) { + $m = $p['title']; + if ( isset( $p['links'] ) ) { + $m .= '/[' . implode( ',', array_map( + function ( $v ) { + return $v['title']; + }, + $p['links'] ) ) . ']'; + } + if ( isset( $p['categories'] ) ) { + $m .= '/(' . implode( ',', array_map( + function ( $v ) { + return str_replace( 'Category:', '', $v['title'] ); + }, + $p['categories'] ) ) . ')'; + } + $print[] = $m; + } + } + if ( isset( $q['allcategories'] ) ) { + $print[] = '*Cats/(' . implode( ',', array_map( + function ( $v ) { + return $v['*']; + }, + $q['allcategories'] ) ) . ')'; + } + self::GetItems( $q, 'allpages', 'Pages', $print ); + self::GetItems( $q, 'alllinks', 'Links', $print ); + self::GetItems( $q, 'alltransclusions', 'Trnscl', $print ); + print ' ' . implode( ' ', $print ) . "\n"; + } + + private static function GetItems( $q, $moduleName, $name, &$print ) { + if ( isset( $q[$moduleName] ) ) { + $print[] = "*$name/[" . implode( ',', + array_map( function ( $v ) { + return $v['title']; + }, + $q[$moduleName] ) ) . ']'; + } + } + + /** + * Recursively merge the new result returned from the query to the previous results. + * @param mixed $results + * @param mixed $newResult + * @param bool $numericIds If true, treat keys as ids to be merged instead of appending + */ + protected function mergeResult( &$results, $newResult, $numericIds = false ) { + $this->assertEquals( is_array( $results ), is_array( $newResult ), 'Type of result and data do not match' ); + if ( !is_array( $results ) ) { + $this->assertEquals( $results, $newResult, 'Repeated result must be the same as before' ); + } else { + $sort = null; + foreach ( $newResult as $key => $value ) { + if ( !$numericIds && $sort === null ) { + if ( !is_array( $value ) ) { + $sort = false; + } elseif ( array_key_exists( 'title', $value ) ) { + $sort = function ( $a, $b ) { + return strcmp( $a['title'], $b['title'] ); + }; + } else { + $sort = false; + } + } + $keyExists = array_key_exists( $key, $results ); + if ( is_numeric( $key ) ) { + if ( $numericIds ) { + if ( !$keyExists ) { + $results[$key] = $value; + } else { + $this->mergeResult( $results[$key], $value ); + } + } else { + $results[] = $value; + } + } elseif ( !$keyExists ) { + $results[$key] = $value; + } else { + $this->mergeResult( $results[$key], $value, $key === 'pages' ); + } + } + if ( $numericIds ) { + ksort( $results, SORT_NUMERIC ); + } elseif ( $sort !== null && $sort !== false ) { + uasort( $results, $sort ); + } + } + } +} diff --git a/tests/phpunit/includes/api/query/ApiQueryRevisionsTest.php b/tests/phpunit/includes/api/query/ApiQueryRevisionsTest.php new file mode 100644 index 00000000..1bca2256 --- /dev/null +++ b/tests/phpunit/includes/api/query/ApiQueryRevisionsTest.php @@ -0,0 +1,39 @@ +doEdit( 'Some text', 'inserting content' ); + + $apiResult = $this->doApiRequest( array( + 'action' => 'query', + 'prop' => 'revisions', + 'titles' => $pageName, + 'rvprop' => 'content', + ) ); + $this->assertArrayHasKey( 'query', $apiResult[0] ); + $this->assertArrayHasKey( 'pages', $apiResult[0]['query'] ); + foreach ( $apiResult[0]['query']['pages'] as $page ) { + $this->assertArrayHasKey( 'revisions', $page ); + foreach ( $page['revisions'] as $revision ) { + $this->assertArrayHasKey( 'contentformat', $revision, + 'contentformat should be included when asking content so client knows how to interpret it' + ); + $this->assertArrayHasKey( 'contentmodel', $revision, + 'contentmodel should be included when asking content so client knows how to interpret it' + ); + } + } + } +} diff --git a/tests/phpunit/includes/api/query/ApiQueryTest.php b/tests/phpunit/includes/api/query/ApiQueryTest.php new file mode 100644 index 00000000..f5645555 --- /dev/null +++ b/tests/phpunit/includes/api/query/ApiQueryTest.php @@ -0,0 +1,66 @@ +doLogin(); + } + + public function testTitlesGetNormalized() { + + global $wgMetaNamespace; + + $data = $this->doApiRequest( array( + 'action' => 'query', + 'titles' => 'Project:articleA|article_B' ) ); + + $this->assertArrayHasKey( 'query', $data[0] ); + $this->assertArrayHasKey( 'normalized', $data[0]['query'] ); + + // Forge a normalized title + $to = Title::newFromText( $wgMetaNamespace . ':ArticleA' ); + + $this->assertEquals( + array( + 'from' => 'Project:articleA', + 'to' => $to->getPrefixedText(), + ), + $data[0]['query']['normalized'][0] + ); + + $this->assertEquals( + array( + 'from' => 'article_B', + 'to' => 'Article B' + ), + $data[0]['query']['normalized'][1] + ); + } + + public function testTitlesAreRejectedIfInvalid() { + $title = false; + while ( !$title || Title::newFromText( $title )->exists() ) { + $title = md5( mt_rand( 0, 10000 ) + rand( 0, 999000 ) ); + } + + $data = $this->doApiRequest( array( + 'action' => 'query', + 'titles' => $title . '|Talk:' ) ); + + $this->assertArrayHasKey( 'query', $data[0] ); + $this->assertArrayHasKey( 'pages', $data[0]['query'] ); + $this->assertEquals( 2, count( $data[0]['query']['pages'] ) ); + + $this->assertArrayHasKey( -2, $data[0]['query']['pages'] ); + $this->assertArrayHasKey( -1, $data[0]['query']['pages'] ); + + $this->assertArrayHasKey( 'missing', $data[0]['query']['pages'][-2] ); + $this->assertArrayHasKey( 'invalid', $data[0]['query']['pages'][-1] ); + } +} diff --git a/tests/phpunit/includes/api/query/ApiQueryTestBase.php b/tests/phpunit/includes/api/query/ApiQueryTestBase.php new file mode 100644 index 00000000..8ee8ea96 --- /dev/null +++ b/tests/phpunit/includes/api/query/ApiQueryTestBase.php @@ -0,0 +1,150 @@ +@gmail.com" + * + * 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 3 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 + */ + +/** This class has some common functionality for testing query module + */ +abstract class ApiQueryTestBase extends ApiTestCase { + + const PARAM_ASSERT = <<validateRequestExpectedPair( $v ); + $request = array_merge_recursive( $request, $req ); + $this->mergeExpected( $expected, $exp ); + } + + return array( $request, $expected ); + } + + /** + * Check that the parameter is a valid two element array, + * with the first element being API request and the second - expected result + */ + private function validateRequestExpectedPair( $v ) { + $this->assertType( 'array', $v, self::PARAM_ASSERT ); + $this->assertEquals( 2, count( $v ), self::PARAM_ASSERT ); + $this->assertArrayHasKey( 0, $v, self::PARAM_ASSERT ); + $this->assertArrayHasKey( 1, $v, self::PARAM_ASSERT ); + $this->assertType( 'array', $v[0], self::PARAM_ASSERT ); + $this->assertType( 'array', $v[1], self::PARAM_ASSERT ); + + return $v; + } + + /** + * Recursively merges the expected values in the $item into the $all + */ + private function mergeExpected( &$all, $item ) { + foreach ( $item as $k => $v ) { + if ( array_key_exists( $k, $all ) ) { + if ( is_array( $all[$k] ) ) { + $this->mergeExpected( $all[$k], $v ); + } else { + $this->assertEquals( $all[$k], $v ); + } + } else { + $all[$k] = $v; + } + } + } + + /** + * Checks that the request's result matches the expected results. + * @param $values array is a two element array( request, expected_results ) + * @throws Exception + */ + protected function check( $values ) { + list( $req, $exp ) = $this->validateRequestExpectedPair( $values ); + if ( !array_key_exists( 'action', $req ) ) { + $req['action'] = 'query'; + } + foreach ( $req as &$val ) { + if ( is_array( $val ) ) { + $val = implode( '|', array_unique( $val ) ); + } + } + $result = $this->doApiRequest( $req ); + $this->assertResult( array( 'query' => $exp ), $result[0], $req ); + } + + protected function assertResult( $exp, $result, $message = '' ) { + try { + $this->assertResultRecursive( $exp, $result ); + } catch ( Exception $e ) { + if ( is_array( $message ) ) { + $message = http_build_query( $message ); + } + print "\nRequest: $message\n"; + print "\nExpected:\n"; + print_r( $exp ); + print "\nResult:\n"; + print_r( $result ); + throw $e; // rethrow it + } + } + + /** + * Recursively compare arrays, ignoring mismatches in numeric key and pageids. + * @param $expected array expected values + * @param $result array returned values + */ + private function assertResultRecursive( $expected, $result ) { + reset( $expected ); + reset( $result ); + while ( true ) { + $e = each( $expected ); + $r = each( $result ); + // If either of the arrays is shorter, abort. If both are done, success. + $this->assertEquals( (bool)$e, (bool)$r ); + if ( !$e ) { + break; // done + } + // continue only if keys are identical or both keys are numeric + $this->assertTrue( $e['key'] === $r['key'] || ( is_numeric( $e['key'] ) && is_numeric( $r['key'] ) ) ); + // don't compare pageids + if ( $e['key'] !== 'pageid' ) { + // If values are arrays, compare recursively, otherwise compare with === + if ( is_array( $e['value'] ) && is_array( $r['value'] ) ) { + $this->assertResultRecursive( $e['value'], $r['value'] ); + } else { + $this->assertEquals( $e['value'], $r['value'] ); + } + } + } + } +} diff --git a/tests/phpunit/includes/api/words.txt b/tests/phpunit/includes/api/words.txt new file mode 100644 index 00000000..7ce23ee3 --- /dev/null +++ b/tests/phpunit/includes/api/words.txt @@ -0,0 +1,1000 @@ +Andaquian +Anoplanthus +Araquaju +Astrophyton +Avarish +Batonga +Bdellidae +Betoyan +Bismarck +Britishness +Carmen +Chatillon +Clement +Coryphaena +Croton +Cyrillianism +Dagomba +Decimus +Dichorisandra +Duculinae +Empusa +Escallonia +Fathometer +Fon +Fundulinae +Gadswoons +Gederathite +Gemini +Gerbera +Gregarinida +Gyracanthus +Halopsychidae +Hasidim +Hemerobius +Ichthyosauridae +Iscariot +Jeames +Jesuitry +Jovian +Judaization +Katie +Ladin +Langhian +Lapithaean +Lisette +Macrochira +Malaxis +Malvastrum +Maranhao +Marxian +Maurist +Metrosideros +Micky +Microsporon +Odacidae +Ophiuchid +Osmorhiza +Paguma +Palesman +Papayaceae +Pastinaca +Philoxenian +Pleurostigma +Rarotongan +Rhodoraceae +Rong +Saho +Sanyakoan +Sardanapalian +Sauropoda +Sedentaria +Shambu +Shukulumbwe +Solonian +Spaniardization +Spirochaetaceae +Stomatopoda +Stratiotes +Taiwanhemp +Titanically +Venetianed +Victrola +Yuman +abatis +abaton +abjoint +acanthoma +acari +acceptance +actinography +acuteness +addiment +adelite +adelomorphic +adelphogamy +adipocele +aelurophobia +affined +aflaunt +agathokakological +aischrolatreia +alarmedly +alebench +aleurone +allelotropic +allerion +alloplastic +allowable +alternacy +alternariose +altricial +ambitionist +amendment +amiableness +amicableness +ammo +amortizable +anchorate +anemometrically +angelocracy +angelological +anodal +anomalure +antedate +antiagglutinin +antirationalist +antiscorbutic +antisplasher +antithesize +antiunionist +antoecian +apolegamic +appropriation +archididascalian +archival +arteriophlebotomy +articulable +asseveration +assignation +atelo +atrienses +atrophy +atterminement +atypic +automower +aveloz +awrist +azteca +bairnteam +balsamweed +bannerman +beardy +becry +beek +beggarwise +bescab +bestness +bethel +bewildering +bibliophilism +bitterblain +blakeberyed +boccarella +bocedization +boobyalla +bourbon +bowbent +bowerbird +brachygnathous +brail +branchiferous +brelaw +brew +brideweed +bridgeable +brombenzamide +buddler +burbankian +burr +buskin +cacochymical +calefactory +caliper +canaliculus +candidature +canellaceous +canniness +canning +cantilene +carbonatation +carthamic +caseum +caudated +causationist +ceruleite +chalder +chalta +charmel +chekan +chillness +chirogymnast +chirpling +chlorinous +cholanthrene +chondroblast +chromatography +chromophilous +chronical +cicatrice +cinchonine +city +clubbing +coastal +coaxially +coercible +coeternity +coff +coinventor +collyba +combinator +complanation +comprehensibility +conchuela +congenital +context +contranatural +corallum +cordately +cornupete +corolliferous +coroneted +corticosterone +coseat +cottage +crocetin +crossleted +crottels +curvedness +cycadeous +cyclism +cylindrically +cynanche +cyrtoceratitic +cystospasm +danceress +dancette +dawny +daydreamy +debar +decarburization +decorousness +decrepitness +delirious +deozonizer +dermatosis +desma +deutencephalic +diacetate +diarthrodial +diathermy +dicolic +dimastigate +dimidiation +dipetto +disavowable +disintrench +disman +dismay +disorder +disoxygenation +dithionous +dogman +dragonfly +dramatical +drawspan +drubbly +drunk +duskly +ecderonic +ectocuniform +ectocyst +ehrwaldite +electrocute +elemicin +embracing +emotionality +enactment +enamor +enclave +endameba +endochylous +endocrinologist +endolymph +endothecal +entasia +epigeous +episcopicide +epitrichial +erminee +erraticalness +eruptivity +erythrocytoschisis +esperance +estuous +eucrystalline +eugeny +evacuant +everbloomer +evocation +exarchateship +exasperate +excorticate +excrementary +exile +expandedly +exponency +expressionist +expulsion +extemporary +extollation +extortive +extrabulbar +extraprostatic +facticide +fairer +fakery +fasibitikite +fatiscent +fearless +febrifuge +ferie +fibrousness +fingered +fisheye +flagpole +flagrantness +fleche +fluidism +folliculin +footbreadth +forceps +forecontrive +forthbring +foveated +fuchsin +fungicidal +funori +gamelang +gametically +garvanzo +gasoliner +gastrophile +germproof +gerontism +gigantical +glaciology +godmotherhood +gooseherd +gordunite +gove +gracilis +greathead +grieveship +guidable +gyromancy +gyrostat +habitus +hailweed +handhole +hangalai +haznadar +heliced +hemihypertrophy +hemimorphic +hemistrumectomy +heptavalent +heptite +herbalist +herpetology +hesperid +hexacarbon +hieromnemon +hobbyless +holodactylic +homoeoarchy +hopperings +hospitable +houseboat +huh +huntedly +hydroponics +hydrosomal +hyperdactylia +hyperperistalsis +hypogeocarpous +ideogram +idiopathical +illegitimate +imambarah +impotently +improvise +impuberal +inaccurately +incarnant +inchoation +incliner +incredulous +indiscriminateness +indulgenced +inebriation +inexpressiveness +infibulate +inflectedness +iniome +ink +inquietly +insaturable +insinuative +instiller +institutive +insultproof +interactionist +intercensal +interpenetrable +intertranspicuous +intrinsicality +inwards +iridiocyte +iridoparalysis +irreportable +isoprene +isosmotic +izard +jacuaru +jaculative +jerkined +joe +joyous +julienne +justicehood +kali +kalidium +katha +kathal +keelage +keratomycosis +khaki +khedival +kinkily +knife +kolo +kraken +kwarta +labba +labber +laboress +lacunar +latch +lauric +lawter +lectotype +leeches +legible +lepidosteoid +leucobasalt +leverer +libellate +limnimeter +lithography +lithotypic +locomotor +logarithmetically +logistician +lyncine +lysogenesis +machan +macromyelon +maharana +mandibulate +manganapatite +marchpane +mas +masochistic +mastaba +matching +meditatively +megalopolitan +melaniline +mentum +mercaptides +mestome +metasomatism +meterless +micronuclear +micropetalous +microreaction +microsporophore +mileway +milliarium +millisecond +misbind +miscollocation +misreader +modernicide +modification +modulant +monkfish +monoamino +monocarbide +monographical +morphinomaniac +mullein +munge +mutilate +mycophagist +myelosarcoma +myospasm +myriadly +nagaika +naphthionate +natant +naviculaeform +nayward +neallotype +necrophilia +nectared +neigher +neogamous +neurodynia +neurorthopteran +nidation +nieceship +nitrobacteria +nitrosification +nogheaded +nonassertive +noneuphonious +nonextant +nonincrease +nonintermittent +nonmetallic +nonprehensile +nonremunerative +nonsocial +nonvesting +noontime +noreaster +nounal +nub +nucleoplasm +nullisome +numero +numerous +oblongatal +observe +obtusilingual +obvert +occipitoatlantal +oceanside +ochlophobist +odontiasis +opalescence +opticon +oraculousness +orarium +organically +orthopedically +ostosis +overadvance +overbuilt +overdiscouragement +overdoer +overhardy +overjocular +overmagnify +overofficered +overpotent +overprizer +overrunner +overshrink +oversimply +oversplash +ovology +oxskin +oxychloride +oxygenant +ozokerite +pactional +palaeoanthropography +palaeographical +palaeopsychology +palliasse +palpebral +pandaric +pantelegraph +papicolist +papulate +parakinetic +parasitism +parochialic +parochialize +passionlike +patch +paucidentate +pawnbrokeress +pecite +pecky +pedipulation +pellitory +perfilograph +periblast +perigemmal +periost +periplus +perishable +periwig +permansive +persistingly +persymmetrical +phantom +phasmatrope +philocaly +philogyny +philosophister +philotherianism +phorology +phototrophic +phrator +phratral +phthisipneumony +physogastry +phytologic +phytoptid +pianograph +picqueter +piculet +pigeoner +pimaric +pinesap +pist +planometer +platano +playful +plea +pleuropneumonic +plowwoman +plump +pluviographical +pneumocele +podophthalmate +polyad +polythalamian +poppyhead +portamento +portmanteau +portraitlike +possible +potassamide +powderer +praepubis +preanesthetic +prebarbaric +predealer +predomination +prefactory +preirrigational +prelector +presbytership +presecure +preservable +prespecialist +preventionism +prewound +princely +priorship +proannexationist +proanthropos +probeable +probouleutic +profitless +proplasma +prosectorial +protecting +protochemistry +protosulphate +pseudoataxia +psilology +psychoneurotic +pterygial +publicist +purgation +purplishness +putatively +pyracene +pyrenomycete +pyromancy +pyrophone +quadroon +quailhead +qualifier +quaternal +rabblelike +rambunctious +rapidness +ratably +rationalism +razor +reannoy +recultivation +regulable +reimplant +reimposition +reimprison +reinjure +reinspiration +reintroduce +remantle +reprehensibility +reptant +require +resteal +restful +returnability +revisableness +rewash +rewhirl +reyield +rhizotomy +rhodamine +rigwiddie +rimester +ripper +rippet +rockish +rockwards +rollicky +roosters +rooted +rosal +rozum +saccharated +sagamore +sagy +salesmanship +salivous +sallet +salta +saprostomous +satiation +sauropsid +sawarra +sawback +scabish +scabrate +scampavia +scientificophilosophical +scirrosity +scoliometer +scolopendrelloid +secantly +seignioral +semibull +semic +seminarianism +semiped +semiprivate +semispherical +semispontaneous +seneschal +septendecimal +serotherapist +servation +sesquisulphuret +severish +sextipartite +sextubercular +shipyard +shuckpen +siderosis +silex +sillyhow +silverbelly +silverbelly +simulacrum +sisham +sixte +skeiner +skiapod +slopped +slubby +smalts +sockmaker +solute +somethingness +somnify +southwester +spathilla +spectrochemical +sphagnology +spinales +spiriting +spirling +spirochetemia +spreadboard +spurflower +squawdom +squeezing +staircase +staker +stamphead +statolith +stekan +stellulate +stinker +stomodaea +streamingly +strikingness +strouthocamelian +stuprum +subacutely +subboreal +subcontractor +subendorsement +subprofitable +subserviate +subsneer +subungual +sucuruju +sugan +sulphocarbolate +summerwood +superficialist +superinference +superregenerative +supplicate +suspendible +synchronizer +syntectic +tachyglossate +tailless +taintment +takingly +taletelling +tarpon +tasteful +taxeater +taxy +teache +teachless +teg +tegmen +teletyper +temperable +ten +tenent +teskere +testes +thallogen +thapsia +thewness +thickety +thiobacteria +thorniness +throwing +thyroprivic +tinnitus +tocalote +tolerationist +tonalamatl +torvous +totality +tottering +toug +tracheopathia +tragedical +translucent +trifoveolate +trilaurin +trophoplasmatic +trunkless +turbanless +turnpiker +twangle +twitterboned +ultraornate +umbilication +unabatingly +unabjured +unadequateness +unaffectedness +unarriving +unassorted +unattacked +unbenumbed +unboasted +unburning +uncensorious +uncongested +uncontemnedly +uncontemporary +uncrook +uncrystallizability +uncurb +uncustomariness +underbillow +undercanopy +underestimation +underhanging +underpetticoated +underpropped +undersole +understocking +underworld +undevout +undisappointing +undistinctive +unfiscal +unfluted +unfreckled +ungentilize +unglobe +unhelped +unhomogeneously +unifoliate +uninflammable +uninterrogated +unisonal +unkindled +unlikeableness +unlisty +unlocked +unmoving +unmultipliable +unnestled +unnoticed +unobservable +unobviated +unoffensively +unofficerlike +unpoetic +unpractically +unquestionableness +unrehearsed +unrevised +unrhetorical +unsadden +unsaluting +unscriptural +unseeking +unshowed +unsolicitous +unsprouted +unsubjective +unsubsidized +unsymbolic +untenant +unterrified +untranquil +untraversed +untrusty +untying +unwillful +unwinding +upspring +uptwist +urachovesical +uropygial +vagabondism +varicoid +varletess +vasal +ventrocaudal +verisimilitude +vermigerous +vibrometer +viminal +virus +vocationalism +voguey +vulnerability +waggle +wamblingly +warmus +waxer +waying +wedgeable +wellmaker +whomever +wigged +witchlike +wokas +woodrowel +woodsman +woolding +xanthelasmic +xiphosternum +yachtman +yachtsmanlike +yelp +zoophytal \ No newline at end of file diff --git a/tests/phpunit/includes/cache/GenderCacheTest.php b/tests/phpunit/includes/cache/GenderCacheTest.php new file mode 100644 index 00000000..ce2db5d7 --- /dev/null +++ b/tests/phpunit/includes/cache/GenderCacheTest.php @@ -0,0 +1,104 @@ +getID() == 0 ) { + $user->addToDatabase(); + $user->setPassword( 'UTMalePassword' ); + } + //ensure the right gender + $user->setOption( 'gender', 'male' ); + $user->saveSettings(); + + $user = User::newFromName( 'UTFemale' ); + if ( $user->getID() == 0 ) { + $user->addToDatabase(); + $user->setPassword( 'UTFemalePassword' ); + } + //ensure the right gender + $user->setOption( 'gender', 'female' ); + $user->saveSettings(); + + $user = User::newFromName( 'UTDefaultGender' ); + if ( $user->getID() == 0 ) { + $user->addToDatabase(); + $user->setPassword( 'UTDefaultGenderPassword' ); + } + //ensure the default gender + $user->setOption( 'gender', null ); + $user->saveSettings(); + } + + /** + * test usernames + * + * @dataProvider provideUserGenders + * @covers GenderCache::getGenderOf + */ + public function testUserName( $username, $expectedGender ) { + $genderCache = GenderCache::singleton(); + $gender = $genderCache->getGenderOf( $username ); + $this->assertEquals( $gender, $expectedGender, "GenderCache normal" ); + } + + /** + * genderCache should work with user objects, too + * + * @dataProvider provideUserGenders + * @covers GenderCache::getGenderOf + */ + public function testUserObjects( $username, $expectedGender ) { + $genderCache = GenderCache::singleton(); + $user = User::newFromName( $username ); + $gender = $genderCache->getGenderOf( $user ); + $this->assertEquals( $gender, $expectedGender, "GenderCache normal" ); + } + + public static function provideUserGenders() { + return array( + array( 'UTMale', 'male' ), + array( 'UTFemale', 'female' ), + array( 'UTDefaultGender', 'unknown' ), + array( 'UTNotExist', 'unknown' ), + //some not valid user + array( '127.0.0.1', 'unknown' ), + array( 'user@test', 'unknown' ), + ); + } + + /** + * test strip of subpages to avoid unnecessary queries + * against the never existing username + * + * @dataProvider provideStripSubpages + * @covers GenderCache::getGenderOf + */ + public function testStripSubpages( $pageWithSubpage, $expectedGender ) { + $genderCache = GenderCache::singleton(); + $gender = $genderCache->getGenderOf( $pageWithSubpage ); + $this->assertEquals( $gender, $expectedGender, "GenderCache must strip of subpages" ); + } + + public static function provideStripSubpages() { + return array( + array( 'UTMale/subpage', 'male' ), + array( 'UTFemale/subpage', 'female' ), + array( 'UTDefaultGender/subpage', 'unknown' ), + array( 'UTNotExist/subpage', 'unknown' ), + array( '127.0.0.1/subpage', 'unknown' ), + ); + } +} diff --git a/tests/phpunit/includes/cache/MessageCacheTest.php b/tests/phpunit/includes/cache/MessageCacheTest.php new file mode 100644 index 00000000..803acf73 --- /dev/null +++ b/tests/phpunit/includes/cache/MessageCacheTest.php @@ -0,0 +1,128 @@ +configureLanguages(); + MessageCache::singleton()->enable(); + } + + /** + * Helper function -- setup site language for testing + */ + protected function configureLanguages() { + // for the test, we need the content language to be anything but English, + // let's choose e.g. German (de) + $langCode = 'de'; + $langObj = Language::factory( $langCode ); + + $this->setMwGlobals( array( + 'wgLanguageCode' => $langCode, + 'wgLang' => $langObj, + 'wgContLang' => $langObj, + ) ); + } + + function addDBData() { + $this->configureLanguages(); + + // Set up messages and fallbacks ab -> ru -> de + $this->makePage( 'FallbackLanguageTest-Full', 'ab' ); + $this->makePage( 'FallbackLanguageTest-Full', 'ru' ); + $this->makePage( 'FallbackLanguageTest-Full', 'de' ); + + // Fallbacks where ab does not exist + $this->makePage( 'FallbackLanguageTest-Partial', 'ru' ); + $this->makePage( 'FallbackLanguageTest-Partial', 'de' ); + + // Fallback to the content language + $this->makePage( 'FallbackLanguageTest-ContLang', 'de' ); + + // Add customizations for an existing message. + $this->makePage( 'sunday', 'ru' ); + + // Full key tests -- always want russian + $this->makePage( 'MessageCacheTest-FullKeyTest', 'ab' ); + $this->makePage( 'MessageCacheTest-FullKeyTest', 'ru' ); + + // In content language -- get base if no derivative + $this->makePage( 'FallbackLanguageTest-NoDervContLang', 'de', 'de/none', false ); + } + + /** + * Helper function for addDBData -- adds a simple page to the database + * + * @param string $title Title of page to be created + * @param string $lang Language and content of the created page + * @param string|null $content Content of the created page, or null for a generic string + * @param bool $createSubPage Set to false if a root page should be created + */ + protected function makePage( $title, $lang, $content = null, $createSubPage = true ) { + global $wgContLang; + + if ( $content === null ) { + $content = $lang; + } + if ( $lang !== $wgContLang->getCode() || $createSubPage ) { + $title = "$title/$lang"; + } + + $title = Title::newFromText( $title, NS_MEDIAWIKI ); + $wikiPage = new WikiPage( $title ); + $contentHandler = ContentHandler::makeContent( $content, $title ); + $wikiPage->doEditContent( $contentHandler, "$lang translation test case" ); + } + + /** + * Test message fallbacks, bug #1495 + * + * @dataProvider provideMessagesForFallback + */ + public function testMessageFallbacks( $message, $lang, $expectedContent ) { + $result = MessageCache::singleton()->get( $message, true, $lang ); + $this->assertEquals( $expectedContent, $result, "Message fallback failed." ); + } + + function provideMessagesForFallback() { + return array( + array( 'FallbackLanguageTest-Full', 'ab', 'ab' ), + array( 'FallbackLanguageTest-Partial', 'ab', 'ru' ), + array( 'FallbackLanguageTest-ContLang', 'ab', 'de' ), + array( 'FallbackLanguageTest-None', 'ab', false ), + + // Existing message with customizations on the fallbacks + array( 'sunday', 'ab', 'амҽыш' ), + + // bug 46579 + array( 'FallbackLanguageTest-NoDervContLang', 'de', 'de/none' ), + // UI language different from content language should only use de/none as last option + array( 'FallbackLanguageTest-NoDervContLang', 'fit', 'de/none' ), + ); + } + + /** + * There's a fallback case where the message key is given as fully qualified -- this + * should ignore the passed $lang and use the language from the key + * + * @dataProvider provideMessagesForFullKeys + */ + public function testFullKeyBehaviour( $message, $lang, $expectedContent ) { + $result = MessageCache::singleton()->get( $message, true, $lang, true ); + $this->assertEquals( $expectedContent, $result, "Full key message fallback failed." ); + } + + function provideMessagesForFullKeys() { + return array( + array( 'MessageCacheTest-FullKeyTest/ru', 'ru', 'ru' ), + array( 'MessageCacheTest-FullKeyTest/ru', 'ab', 'ru' ), + array( 'MessageCacheTest-FullKeyTest/ru/foo', 'ru', false ), + ); + } + +} diff --git a/tests/phpunit/includes/cache/ProcessCacheLRUTest.php b/tests/phpunit/includes/cache/ProcessCacheLRUTest.php new file mode 100644 index 00000000..d3793d83 --- /dev/null +++ b/tests/phpunit/includes/cache/ProcessCacheLRUTest.php @@ -0,0 +1,237 @@ +assertAttributeEquals( array(), 'cache', $cache, $msg ); + } + + /** + * Helper to fill a cache object passed by reference + */ + function fillCache( &$cache, $numEntries ) { + // Fill cache with three values + for ( $i = 1; $i <= $numEntries; $i++ ) { + $cache->set( "cache-key-$i", "prop-$i", "value-$i" ); + } + } + + /** + * Generates an array of what would be expected in cache for a given cache + * size and a number of entries filled in sequentially + */ + function getExpectedCache( $cacheMaxEntries, $entryToFill ) { + $expected = array(); + + if ( $entryToFill === 0 ) { + # The cache is empty! + return array(); + } elseif ( $entryToFill <= $cacheMaxEntries ) { + # Cache is not fully filled + $firstKey = 1; + } else { + # Cache overflowed + $firstKey = 1 + $entryToFill - $cacheMaxEntries; + } + + $lastKey = $entryToFill; + + for ( $i = $firstKey; $i <= $lastKey; $i++ ) { + $expected["cache-key-$i"] = array( "prop-$i" => "value-$i" ); + } + + return $expected; + } + + /** + * Highlight diff between assertEquals and assertNotSame + */ + public function testPhpUnitArrayEquality() { + $one = array( 'A' => 1, 'B' => 2 ); + $two = array( 'B' => 2, 'A' => 1 ); + $this->assertEquals( $one, $two ); // == + $this->assertNotSame( $one, $two ); // === + } + + /** + * @dataProvider provideInvalidConstructorArg + * @expectedException MWException + */ + public function testConstructorGivenInvalidValue( $maxSize ) { + new ProcessCacheLRUTestable( $maxSize ); + } + + /** + * Value which are forbidden by the constructor + */ + public static function provideInvalidConstructorArg() { + return array( + array( null ), + array( array() ), + array( new stdClass() ), + array( 0 ), + array( '5' ), + array( -1 ), + ); + } + + public function testAddAndGetAKey() { + $oneCache = new ProcessCacheLRUTestable( 1 ); + $this->assertCacheEmpty( $oneCache ); + + // First set just one value + $oneCache->set( 'cache-key', 'prop1', 'value1' ); + $this->assertEquals( 1, $oneCache->getEntriesCount() ); + $this->assertTrue( $oneCache->has( 'cache-key', 'prop1' ) ); + $this->assertEquals( 'value1', $oneCache->get( 'cache-key', 'prop1' ) ); + } + + public function testDeleteOldKey() { + $oneCache = new ProcessCacheLRUTestable( 1 ); + $this->assertCacheEmpty( $oneCache ); + + $oneCache->set( 'cache-key', 'prop1', 'value1' ); + $oneCache->set( 'cache-key', 'prop1', 'value2' ); + $this->assertEquals( 'value2', $oneCache->get( 'cache-key', 'prop1' ) ); + } + + /** + * This test that we properly overflow when filling a cache with + * a sequence of always different cache-keys. Meant to verify we correclty + * delete the older key. + * + * @dataProvider provideCacheFilling + * @param $cacheMaxEntries Maximum entry the created cache will hold + * @param $entryToFill Number of entries to insert in the created cache. + */ + public function testFillingCache( $cacheMaxEntries, $entryToFill, $msg = '' ) { + $cache = new ProcessCacheLRUTestable( $cacheMaxEntries ); + $this->fillCache( $cache, $entryToFill ); + + $this->assertSame( + $this->getExpectedCache( $cacheMaxEntries, $entryToFill ), + $cache->getCache(), + "Filling a $cacheMaxEntries entries cache with $entryToFill entries" + ); + } + + /** + * Provider for testFillingCache + */ + public static function provideCacheFilling() { + // ($cacheMaxEntries, $entryToFill, $msg='') + return array( + array( 1, 0 ), + array( 1, 1 ), + array( 1, 2 ), # overflow + array( 5, 33 ), # overflow + ); + } + + /** + * Create a cache with only one remaining entry then update + * the first inserted entry. Should bump it to the top. + */ + public function testReplaceExistingKeyShouldBumpEntryToTop() { + $maxEntries = 3; + + $cache = new ProcessCacheLRUTestable( $maxEntries ); + // Fill cache leaving just one remaining slot + $this->fillCache( $cache, $maxEntries - 1 ); + + // Set an existing cache key + $cache->set( "cache-key-1", "prop-1", "new-value-for-1" ); + + $this->assertSame( + array( + 'cache-key-2' => array( 'prop-2' => 'value-2' ), + 'cache-key-1' => array( 'prop-1' => 'new-value-for-1' ), + ), + $cache->getCache() + ); + } + + public function testRecentlyAccessedKeyStickIn() { + $cache = new ProcessCacheLRUTestable( 2 ); + $cache->set( 'first', 'prop1', 'value1' ); + $cache->set( 'second', 'prop2', 'value2' ); + + // Get first + $cache->get( 'first', 'prop1' ); + // Cache a third value, should invalidate the least used one + $cache->set( 'third', 'prop3', 'value3' ); + + $this->assertFalse( $cache->has( 'second', 'prop2' ) ); + } + + /** + * This first create a full cache then update the value for the 2nd + * filled entry. + * Given a cache having 1,2,3 as key, updating 2 should bump 2 to + * the top of the queue with the new value: 1,3,2* (* = updated). + */ + public function testReplaceExistingKeyInAFullCacheShouldBumpToTop() { + $maxEntries = 3; + + $cache = new ProcessCacheLRUTestable( $maxEntries ); + $this->fillCache( $cache, $maxEntries ); + + // Set an existing cache key + $cache->set( "cache-key-2", "prop-2", "new-value-for-2" ); + $this->assertSame( + array( + 'cache-key-1' => array( 'prop-1' => 'value-1' ), + 'cache-key-3' => array( 'prop-3' => 'value-3' ), + 'cache-key-2' => array( 'prop-2' => 'new-value-for-2' ), + ), + $cache->getCache() + ); + $this->assertEquals( 'new-value-for-2', + $cache->get( 'cache-key-2', 'prop-2' ) + ); + } + + public function testBumpExistingKeyToTop() { + $cache = new ProcessCacheLRUTestable( 3 ); + $this->fillCache( $cache, 3 ); + + // Set the very first cache key to a new value + $cache->set( "cache-key-1", "prop-1", "new value for 1" ); + $this->assertEquals( + array( + 'cache-key-2' => array( 'prop-2' => 'value-2' ), + 'cache-key-3' => array( 'prop-3' => 'value-3' ), + 'cache-key-1' => array( 'prop-1' => 'new value for 1' ), + ), + $cache->getCache() + ); + } +} + +/** + * Overrides some ProcessCacheLRU methods and properties accessibility. + */ +class ProcessCacheLRUTestable extends ProcessCacheLRU { + public $cache = array(); + + public function getCache() { + return $this->cache; + } + + public function getEntriesCount() { + return count( $this->cache ); + } +} diff --git a/tests/phpunit/includes/content/ContentHandlerTest.php b/tests/phpunit/includes/content/ContentHandlerTest.php new file mode 100644 index 00000000..aedf594d --- /dev/null +++ b/tests/phpunit/includes/content/ContentHandlerTest.php @@ -0,0 +1,451 @@ +setMwGlobals( array( + 'wgExtraNamespaces' => array( + 12312 => 'Dummy', + 12313 => 'Dummy_talk', + ), + // The below tests assume that namespaces not mentioned here (Help, User, MediaWiki, ..) + // default to CONTENT_MODEL_WIKITEXT. + 'wgNamespaceContentModels' => array( + 12312 => 'testing', + ), + 'wgContentHandlers' => array( + CONTENT_MODEL_WIKITEXT => 'WikitextContentHandler', + CONTENT_MODEL_JAVASCRIPT => 'JavaScriptContentHandler', + CONTENT_MODEL_CSS => 'CssContentHandler', + CONTENT_MODEL_TEXT => 'TextContentHandler', + 'testing' => 'DummyContentHandlerForTesting', + ), + ) ); + + // Reset namespace cache + MWNamespace::getCanonicalNamespaces( true ); + $wgContLang->resetNamespaces(); + } + + public function tearDown() { + global $wgContLang; + + // Reset namespace cache + MWNamespace::getCanonicalNamespaces( true ); + $wgContLang->resetNamespaces(); + + parent::tearDown(); + } + + public static function dataGetDefaultModelFor() { + return array( + array( 'Help:Foo', CONTENT_MODEL_WIKITEXT ), + array( 'Help:Foo.js', CONTENT_MODEL_WIKITEXT ), + array( 'Help:Foo/bar.js', CONTENT_MODEL_WIKITEXT ), + array( 'User:Foo', CONTENT_MODEL_WIKITEXT ), + array( 'User:Foo.js', CONTENT_MODEL_WIKITEXT ), + array( 'User:Foo/bar.js', CONTENT_MODEL_JAVASCRIPT ), + array( 'User:Foo/bar.css', CONTENT_MODEL_CSS ), + array( 'User talk:Foo/bar.css', CONTENT_MODEL_WIKITEXT ), + array( 'User:Foo/bar.js.xxx', CONTENT_MODEL_WIKITEXT ), + array( 'User:Foo/bar.xxx', CONTENT_MODEL_WIKITEXT ), + array( 'MediaWiki:Foo.js', CONTENT_MODEL_JAVASCRIPT ), + array( 'MediaWiki:Foo.css', CONTENT_MODEL_CSS ), + array( 'MediaWiki:Foo.JS', CONTENT_MODEL_WIKITEXT ), + array( 'MediaWiki:Foo.CSS', CONTENT_MODEL_WIKITEXT ), + array( 'MediaWiki:Foo.css.xxx', CONTENT_MODEL_WIKITEXT ), + ); + } + + /** + * @dataProvider dataGetDefaultModelFor + * @covers ContentHandler::getDefaultModelFor + */ + public function testGetDefaultModelFor( $title, $expectedModelId ) { + $title = Title::newFromText( $title ); + $this->assertEquals( $expectedModelId, ContentHandler::getDefaultModelFor( $title ) ); + } + + /** + * @dataProvider dataGetDefaultModelFor + * @covers ContentHandler::getForTitle + */ + public function testGetForTitle( $title, $expectedContentModel ) { + $title = Title::newFromText( $title ); + $handler = ContentHandler::getForTitle( $title ); + $this->assertEquals( $expectedContentModel, $handler->getModelID() ); + } + + public static function dataGetLocalizedName() { + return array( + array( null, null ), + array( "xyzzy", null ), + + // XXX: depends on content language + array( CONTENT_MODEL_JAVASCRIPT, '/javascript/i' ), + ); + } + + /** + * @dataProvider dataGetLocalizedName + * @covers ContentHandler::getLocalizedName + */ + public function testGetLocalizedName( $id, $expected ) { + $name = ContentHandler::getLocalizedName( $id ); + + if ( $expected ) { + $this->assertNotNull( $name, "no name found for content model $id" ); + $this->assertTrue( preg_match( $expected, $name ) > 0, + "content model name for #$id did not match pattern $expected" + ); + } else { + $this->assertEquals( $id, $name, "localization of unknown model $id should have " + . "fallen back to use the model id directly." + ); + } + } + + public static function dataGetPageLanguage() { + global $wgLanguageCode; + + return array( + array( "Main", $wgLanguageCode ), + array( "Dummy:Foo", $wgLanguageCode ), + array( "MediaWiki:common.js", 'en' ), + array( "User:Foo/common.js", 'en' ), + array( "MediaWiki:common.css", 'en' ), + array( "User:Foo/common.css", 'en' ), + array( "User:Foo", $wgLanguageCode ), + + array( CONTENT_MODEL_JAVASCRIPT, 'javascript' ), + ); + } + + /** + * @dataProvider dataGetPageLanguage + * @covers ContentHandler::getPageLanguage + */ + public function testGetPageLanguage( $title, $expected ) { + if ( is_string( $title ) ) { + $title = Title::newFromText( $title ); + } + + $expected = wfGetLangObj( $expected ); + + $handler = ContentHandler::getForTitle( $title ); + $lang = $handler->getPageLanguage( $title ); + + $this->assertEquals( $expected->getCode(), $lang->getCode() ); + } + + public static function dataGetContentText_Null() { + return array( + array( 'fail' ), + array( 'serialize' ), + array( 'ignore' ), + ); + } + + /** + * @dataProvider dataGetContentText_Null + * @covers ContentHandler::getContentText + */ + public function testGetContentText_Null( $contentHandlerTextFallback ) { + $this->setMwGlobals( 'wgContentHandlerTextFallback', $contentHandlerTextFallback ); + + $content = null; + + $text = ContentHandler::getContentText( $content ); + $this->assertEquals( '', $text ); + } + + public static function dataGetContentText_TextContent() { + return array( + array( 'fail' ), + array( 'serialize' ), + array( 'ignore' ), + ); + } + + /** + * @dataProvider dataGetContentText_TextContent + * @covers ContentHandler::getContentText + */ + public function testGetContentText_TextContent( $contentHandlerTextFallback ) { + $this->setMwGlobals( 'wgContentHandlerTextFallback', $contentHandlerTextFallback ); + + $content = new WikitextContent( "hello world" ); + + $text = ContentHandler::getContentText( $content ); + $this->assertEquals( $content->getNativeData(), $text ); + } + + /** + * ContentHandler::getContentText should have thrown an exception for non-text Content object + * @expectedException MWException + * @covers ContentHandler::getContentText + */ + public function testGetContentText_NonTextContent_fail() { + $this->setMwGlobals( 'wgContentHandlerTextFallback', 'fail' ); + + $content = new DummyContentForTesting( "hello world" ); + + ContentHandler::getContentText( $content ); + } + + /** + * @covers ContentHandler::getContentText + */ + public function testGetContentText_NonTextContent_serialize() { + $this->setMwGlobals( 'wgContentHandlerTextFallback', 'serialize' ); + + $content = new DummyContentForTesting( "hello world" ); + + $text = ContentHandler::getContentText( $content ); + $this->assertEquals( $content->serialize(), $text ); + } + + /** + * @covers ContentHandler::getContentText + */ + public function testGetContentText_NonTextContent_ignore() { + $this->setMwGlobals( 'wgContentHandlerTextFallback', 'ignore' ); + + $content = new DummyContentForTesting( "hello world" ); + + $text = ContentHandler::getContentText( $content ); + $this->assertNull( $text ); + } + + /* + public static function makeContent( $text, Title $title, $modelId = null, $format = null ) {} + */ + + public static function dataMakeContent() { + return array( + array( 'hallo', 'Help:Test', null, null, CONTENT_MODEL_WIKITEXT, 'hallo', false ), + array( 'hallo', 'MediaWiki:Test.js', null, null, CONTENT_MODEL_JAVASCRIPT, 'hallo', false ), + array( serialize( 'hallo' ), 'Dummy:Test', null, null, "testing", 'hallo', false ), + + array( 'hallo', 'Help:Test', null, CONTENT_FORMAT_WIKITEXT, CONTENT_MODEL_WIKITEXT, 'hallo', false ), + array( 'hallo', 'MediaWiki:Test.js', null, CONTENT_FORMAT_JAVASCRIPT, CONTENT_MODEL_JAVASCRIPT, 'hallo', false ), + array( serialize( 'hallo' ), 'Dummy:Test', null, "testing", "testing", 'hallo', false ), + + array( 'hallo', 'Help:Test', CONTENT_MODEL_CSS, null, CONTENT_MODEL_CSS, 'hallo', false ), + array( 'hallo', 'MediaWiki:Test.js', CONTENT_MODEL_CSS, null, CONTENT_MODEL_CSS, 'hallo', false ), + array( serialize( 'hallo' ), 'Dummy:Test', CONTENT_MODEL_CSS, null, CONTENT_MODEL_CSS, serialize( 'hallo' ), false ), + + array( 'hallo', 'Help:Test', CONTENT_MODEL_WIKITEXT, "testing", null, null, true ), + array( 'hallo', 'MediaWiki:Test.js', CONTENT_MODEL_CSS, "testing", null, null, true ), + array( 'hallo', 'Dummy:Test', CONTENT_MODEL_JAVASCRIPT, "testing", null, null, true ), + ); + } + + /** + * @dataProvider dataMakeContent + * @covers ContentHandler::makeContent + */ + public function testMakeContent( $data, $title, $modelId, $format, $expectedModelId, $expectedNativeData, $shouldFail ) { + $title = Title::newFromText( $title ); + + try { + $content = ContentHandler::makeContent( $data, $title, $modelId, $format ); + + if ( $shouldFail ) { + $this->fail( "ContentHandler::makeContent should have failed!" ); + } + + $this->assertEquals( $expectedModelId, $content->getModel(), 'bad model id' ); + $this->assertEquals( $expectedNativeData, $content->getNativeData(), 'bads native data' ); + } catch ( MWException $ex ) { + if ( !$shouldFail ) { + $this->fail( "ContentHandler::makeContent failed unexpectedly: " . $ex->getMessage() ); + } else { + // dummy, so we don't get the "test did not perform any assertions" message. + $this->assertTrue( true ); + } + } + } + + /* + public function testSupportsSections() { + $this->markTestIncomplete( "not yet implemented" ); + } + */ + + /** + * @covers ContentHandler::runLegacyHooks + */ + public function testRunLegacyHooks() { + Hooks::register( 'testRunLegacyHooks', __CLASS__ . '::dummyHookHandler' ); + + $content = new WikitextContent( 'test text' ); + $ok = ContentHandler::runLegacyHooks( 'testRunLegacyHooks', array( 'foo', &$content, 'bar' ), false ); + + $this->assertTrue( $ok, "runLegacyHooks should have returned true" ); + $this->assertEquals( "TEST TEXT", $content->getNativeData() ); + } + + public static function dummyHookHandler( $foo, &$text, $bar ) { + if ( $text === null || $text === false ) { + return false; + } + + $text = strtoupper( $text ); + + return true; + } +} + +class DummyContentHandlerForTesting extends ContentHandler { + + public function __construct( $dataModel ) { + parent::__construct( $dataModel, array( "testing" ) ); + } + + /** + * Serializes Content object of the type supported by this ContentHandler. + * + * @param Content $content the Content object to serialize + * @param null $format the desired serialization format + * @return String serialized form of the content + */ + public function serializeContent( Content $content, $format = null ) { + return $content->serialize(); + } + + /** + * Unserializes a Content object of the type supported by this ContentHandler. + * + * @param $blob String serialized form of the content + * @param null $format the format used for serialization + * @return Content the Content object created by deserializing $blob + */ + public function unserializeContent( $blob, $format = null ) { + $d = unserialize( $blob ); + + return new DummyContentForTesting( $d ); + } + + /** + * Creates an empty Content object of the type supported by this ContentHandler. + * + */ + public function makeEmptyContent() { + return new DummyContentForTesting( '' ); + } +} + +class DummyContentForTesting extends AbstractContent { + + public function __construct( $data ) { + parent::__construct( "testing" ); + + $this->data = $data; + } + + public function serialize( $format = null ) { + return serialize( $this->data ); + } + + /** + * @return String a string representing the content in a way useful for building a full text search index. + * If no useful representation exists, this method returns an empty string. + */ + public function getTextForSearchIndex() { + return ''; + } + + /** + * @return String the wikitext to include when another page includes this content, or false if the content is not + * includable in a wikitext page. + */ + public function getWikitextForTransclusion() { + return false; + } + + /** + * Returns a textual representation of the content suitable for use in edit summaries and log messages. + * + * @param int $maxlength Maximum length of the summary text. + * @return string The summary text. + */ + public function getTextForSummary( $maxlength = 250 ) { + return ''; + } + + /** + * Returns native represenation of the data. Interpretation depends on the data model used, + * as given by getDataModel(). + * + * @return mixed the native representation of the content. Could be a string, a nested array + * structure, an object, a binary blob... anything, really. + */ + public function getNativeData() { + return $this->data; + } + + /** + * returns the content's nominal size in bogo-bytes. + * + * @return int + */ + public function getSize() { + return strlen( $this->data ); + } + + /** + * Return a copy of this Content object. The following must be true for the object returned + * if $copy = $original->copy() + * + * * get_class($original) === get_class($copy) + * * $original->getModel() === $copy->getModel() + * * $original->equals( $copy ) + * + * If and only if the Content object is imutable, the copy() method can and should + * return $this. That is, $copy === $original may be true, but only for imutable content + * objects. + * + * @return Content. A copy of this object. + */ + public function copy() { + return $this; + } + + /** + * Returns true if this content is countable as a "real" wiki page, provided + * that it's also in a countable location (e.g. a current revision in the main namespace). + * + * @param boolean $hasLinks if it is known whether this content contains links, provide this information here, + * to avoid redundant parsing to find out. + * @return boolean + */ + public function isCountable( $hasLinks = null ) { + return false; + } + + /** + * @param Title $title + * @param null $revId + * @param null|ParserOptions $options + * @param boolean $generateHtml whether to generate Html (default: true). If false, + * the result of calling getText() on the ParserOutput object returned by + * this method is undefined. + * + * @return ParserOutput + */ + public function getParserOutput( Title $title, $revId = null, ParserOptions $options = null, $generateHtml = true ) { + return new ParserOutput( $this->getNativeData() ); + } +} diff --git a/tests/phpunit/includes/content/CssContentTest.php b/tests/phpunit/includes/content/CssContentTest.php new file mode 100644 index 00000000..bd6d41fe --- /dev/null +++ b/tests/phpunit/includes/content/CssContentTest.php @@ -0,0 +1,87 @@ +setName( '127.0.0.1' ); + + $this->setMwGlobals( array( + 'wgUser' => $user, + 'wgTextModelsToParse' => array( + CONTENT_MODEL_CSS, + ) + ) ); + } + + public function newContent( $text ) { + return new CssContent( $text ); + } + + public static function dataGetParserOutput() { + return array( + array( + 'MediaWiki:Test.css', + null, + "hello \n", + "
\nhello <world>\n\n
" + ), + array( + 'MediaWiki:Test.css', + null, + "/* hello [[world]] */\n", + "
\n/* hello [[world]] */\n\n
", + array( + 'Links' => array( + array( 'World' => 0 ) + ) + ) + ), + + // TODO: more...? + ); + } + + /** + * @covers CssContent::getModel + */ + public function testGetModel() { + $content = $this->newContent( 'hello world.' ); + + $this->assertEquals( CONTENT_MODEL_CSS, $content->getModel() ); + } + + /** + * @covers CssContent::getContentHandler + */ + public function testGetContentHandler() { + $content = $this->newContent( 'hello world.' ); + + $this->assertEquals( CONTENT_MODEL_CSS, $content->getContentHandler()->getModelID() ); + } + + public static function dataEquals() { + return array( + array( new CssContent( 'hallo' ), null, false ), + array( new CssContent( 'hallo' ), new CssContent( 'hallo' ), true ), + array( new CssContent( 'hallo' ), new WikitextContent( 'hallo' ), false ), + array( new CssContent( 'hallo' ), new CssContent( 'HALLO' ), false ), + ); + } + + /** + * @dataProvider dataEquals + * @covers CssContent::equals + */ + public function testEquals( Content $a, Content $b = null, $equal = false ) { + $this->assertEquals( $equal, $a->equals( $b ) ); + } +} diff --git a/tests/phpunit/includes/content/JavaScriptContentTest.php b/tests/phpunit/includes/content/JavaScriptContentTest.php new file mode 100644 index 00000000..c8616ff0 --- /dev/null +++ b/tests/phpunit/includes/content/JavaScriptContentTest.php @@ -0,0 +1,287 @@ +\n", + "
\nhello <world>\n\n
" + ), + array( + 'MediaWiki:Test.js', + null, + "hello(); // [[world]]\n", + "
\nhello(); // [[world]]\n\n
", + array( + 'Links' => array( + array( 'World' => 0 ) + ) + ) + ), + + // TODO: more...? + ); + } + + // XXX: Unused function + public static function dataGetSection() { + return array( + array( WikitextContentTest::$sections, + '0', + null + ), + array( WikitextContentTest::$sections, + '2', + null + ), + array( WikitextContentTest::$sections, + '8', + null + ), + ); + } + + // XXX: Unused function + public static function dataReplaceSection() { + return array( + array( WikitextContentTest::$sections, + '0', + 'No more', + null, + null + ), + array( WikitextContentTest::$sections, + '', + 'No more', + null, + null + ), + array( WikitextContentTest::$sections, + '2', + "== TEST ==\nmore fun", + null, + null + ), + array( WikitextContentTest::$sections, + '8', + 'No more', + null, + null + ), + array( WikitextContentTest::$sections, + 'new', + 'No more', + 'New', + null + ), + ); + } + + /** + * @covers JavaScriptContent::addSectionHeader + */ + public function testAddSectionHeader() { + $content = $this->newContent( 'hello world' ); + $c = $content->addSectionHeader( 'test' ); + + $this->assertTrue( $content->equals( $c ) ); + } + + // XXX: currently, preSaveTransform is applied to scripts. this may change or become optional. + public static function dataPreSaveTransform() { + return array( + array( 'hello this is ~~~', + "hello this is [[Special:Contributions/127.0.0.1|127.0.0.1]]", + ), + array( 'hello \'\'this\'\' is ~~~', + 'hello \'\'this\'\' is ~~~', + ), + array( " Foo \n ", + " Foo", + ), + ); + } + + public static function dataPreloadTransform() { + return array( + array( 'hello this is ~~~', + 'hello this is ~~~', + ), + array( 'hello \'\'this\'\' is foobar', + 'hello \'\'this\'\' is foobar', + ), + ); + } + + public static function dataGetRedirectTarget() { + return array( + array( '#REDIRECT [[Test]]', + null, + ), + array( '#REDIRECT Test', + null, + ), + array( '* #REDIRECT [[Test]]', + null, + ), + ); + } + + /** + * @todo Test needs database! + */ + /* + public function getRedirectChain() { + $text = $this->getNativeData(); + return Title::newFromRedirectArray( $text ); + } + */ + + /** + * @todo Test needs database! + */ + /* + public function getUltimateRedirectTarget() { + $text = $this->getNativeData(); + return Title::newFromRedirectRecurse( $text ); + } + */ + + public static function dataIsCountable() { + return array( + array( '', + null, + 'any', + true + ), + array( 'Foo', + null, + 'any', + true + ), + array( 'Foo', + null, + 'comma', + false + ), + array( 'Foo, bar', + null, + 'comma', + false + ), + array( 'Foo', + null, + 'link', + false + ), + array( 'Foo [[bar]]', + null, + 'link', + false + ), + array( 'Foo', + true, + 'link', + false + ), + array( 'Foo [[bar]]', + false, + 'link', + false + ), + array( '#REDIRECT [[bar]]', + true, + 'any', + true + ), + array( '#REDIRECT [[bar]]', + true, + 'comma', + false + ), + array( '#REDIRECT [[bar]]', + true, + 'link', + false + ), + ); + } + + public static function dataGetTextForSummary() { + return array( + array( "hello\nworld.", + 16, + 'hello world.', + ), + array( 'hello world.', + 8, + 'hello...', + ), + array( '[[hello world]].', + 8, + '[[hel...', + ), + ); + } + + /** + * @covers JavaScriptContent::matchMagicWord + */ + public function testMatchMagicWord() { + $mw = MagicWord::get( "staticredirect" ); + + $content = $this->newContent( "#REDIRECT [[FOO]]\n__STATICREDIRECT__" ); + $this->assertFalse( $content->matchMagicWord( $mw ), "should not have matched magic word, since it's not wikitext" ); + } + + /** + * @covers JavaScriptContent::updateRedirect + */ + public function testUpdateRedirect() { + $target = Title::newFromText( "testUpdateRedirect_target" ); + + $content = $this->newContent( "#REDIRECT [[Someplace]]" ); + $newContent = $content->updateRedirect( $target ); + + $this->assertTrue( $content->equals( $newContent ), "content should be unchanged since it's not wikitext" ); + } + + /** + * @covers JavaScriptContent::getModel + */ + public function testGetModel() { + $content = $this->newContent( "hello world." ); + + $this->assertEquals( CONTENT_MODEL_JAVASCRIPT, $content->getModel() ); + } + + /** + * @covers JavaScriptContent::getContentHandler + */ + public function testGetContentHandler() { + $content = $this->newContent( "hello world." ); + + $this->assertEquals( CONTENT_MODEL_JAVASCRIPT, $content->getContentHandler()->getModelID() ); + } + + public static function dataEquals() { + return array( + array( new JavaScriptContent( "hallo" ), null, false ), + array( new JavaScriptContent( "hallo" ), new JavaScriptContent( "hallo" ), true ), + array( new JavaScriptContent( "hallo" ), new CssContent( "hallo" ), false ), + array( new JavaScriptContent( "hallo" ), new JavaScriptContent( "HALLO" ), false ), + ); + } +} diff --git a/tests/phpunit/includes/content/TextContentTest.php b/tests/phpunit/includes/content/TextContentTest.php new file mode 100644 index 00000000..a1f099f3 --- /dev/null +++ b/tests/phpunit/includes/content/TextContentTest.php @@ -0,0 +1,458 @@ +setName( '127.0.0.1' ); + + $this->setMwGlobals( array( + 'wgUser' => $user, + 'wgTextModelsToParse' => array( + CONTENT_MODEL_WIKITEXT, + CONTENT_MODEL_CSS, + CONTENT_MODEL_JAVASCRIPT, + ), + 'wgUseTidy' => false, + 'wgAlwaysUseTidy' => false, + ) ); + + $this->context = new RequestContext( new FauxRequest() ); + $this->context->setTitle( Title::newFromText( 'Test' ) ); + $this->context->setUser( $user ); + } + + public function newContent( $text ) { + return new TextContent( $text ); + } + + public static function dataGetParserOutput() { + return array( + array( + 'TextContentTest_testGetParserOutput', + CONTENT_MODEL_TEXT, + "hello ''world'' & [[stuff]]\n", "hello ''world'' & [[stuff]]", + array( + 'Links' => array() + ) + ), + // TODO: more...? + ); + } + + /** + * @dataProvider dataGetParserOutput + * @covers TextContent::getParserOutput + */ + public function testGetParserOutput( $title, $model, $text, $expectedHtml, $expectedFields = null ) { + $title = Title::newFromText( $title ); + $content = ContentHandler::makeContent( $text, $title, $model ); + + $po = $content->getParserOutput( $title ); + + $html = $po->getText(); + $html = preg_replace( '##sm', '', $html ); // strip comments + + $this->assertEquals( $expectedHtml, trim( $html ) ); + + if ( $expectedFields ) { + foreach ( $expectedFields as $field => $exp ) { + $f = 'get' . ucfirst( $field ); + $v = call_user_func( array( $po, $f ) ); + + if ( is_array( $exp ) ) { + $this->assertArrayEquals( $exp, $v ); + } else { + $this->assertEquals( $exp, $v ); + } + } + } + + // TODO: assert more properties + } + + public static function dataPreSaveTransform() { + return array( + array( + #0: no signature resolution + 'hello this is ~~~', + 'hello this is ~~~', + ), + array( + #1: rtrim + " Foo \n ", + ' Foo', + ), + ); + } + + /** + * @dataProvider dataPreSaveTransform + * @covers TextContent::preSaveTransform + */ + public function testPreSaveTransform( $text, $expected ) { + global $wgContLang; + + $options = ParserOptions::newFromUserAndLang( $this->context->getUser(), $wgContLang ); + + $content = $this->newContent( $text ); + $content = $content->preSaveTransform( $this->context->getTitle(), $this->context->getUser(), $options ); + + $this->assertEquals( $expected, $content->getNativeData() ); + } + + public static function dataPreloadTransform() { + return array( + array( + 'hello this is ~~~', + 'hello this is ~~~', + ), + ); + } + + /** + * @dataProvider dataPreloadTransform + * @covers TextContent::preloadTransform + */ + public function testPreloadTransform( $text, $expected ) { + global $wgContLang; + $options = ParserOptions::newFromUserAndLang( $this->context->getUser(), $wgContLang ); + + $content = $this->newContent( $text ); + $content = $content->preloadTransform( $this->context->getTitle(), $options ); + + $this->assertEquals( $expected, $content->getNativeData() ); + } + + public static function dataGetRedirectTarget() { + return array( + array( '#REDIRECT [[Test]]', + null, + ), + ); + } + + /** + * @dataProvider dataGetRedirectTarget + * @covers TextContent::getRedirectTarget + */ + public function testGetRedirectTarget( $text, $expected ) { + $content = $this->newContent( $text ); + $t = $content->getRedirectTarget(); + + if ( is_null( $expected ) ) { + $this->assertNull( $t, "text should not have generated a redirect target: $text" ); + } else { + $this->assertEquals( $expected, $t->getPrefixedText() ); + } + } + + /** + * @dataProvider dataGetRedirectTarget + * @covers TextContent::isRedirect + */ + public function testIsRedirect( $text, $expected ) { + $content = $this->newContent( $text ); + + $this->assertEquals( !is_null( $expected ), $content->isRedirect() ); + } + + /** + * @todo Test needs database! Should be done by a test class in the Database group. + */ + /* + public function getRedirectChain() { + $text = $this->getNativeData(); + return Title::newFromRedirectArray( $text ); + } + */ + + /** + * @todo Test needs database! Should be done by a test class in the Database group. + */ + /* + public function getUltimateRedirectTarget() { + $text = $this->getNativeData(); + return Title::newFromRedirectRecurse( $text ); + } + */ + + public static function dataIsCountable() { + return array( + array( '', + null, + 'any', + true + ), + array( 'Foo', + null, + 'any', + true + ), + array( 'Foo', + null, + 'comma', + false + ), + array( 'Foo, bar', + null, + 'comma', + false + ), + ); + } + + /** + * @dataProvider dataIsCountable + * @group Database + * @covers TextContent::isCountable + */ + public function testIsCountable( $text, $hasLinks, $mode, $expected ) { + $this->setMwGlobals( 'wgArticleCountMethod', $mode ); + + $content = $this->newContent( $text ); + + $v = $content->isCountable( $hasLinks, $this->context->getTitle() ); + + $this->assertEquals( $expected, $v, 'isCountable() returned unexpected value ' . var_export( $v, true ) + . ' instead of ' . var_export( $expected, true ) . " in mode `$mode` for text \"$text\"" ); + } + + public static function dataGetTextForSummary() { + return array( + array( "hello\nworld.", + 16, + 'hello world.', + ), + array( 'hello world.', + 8, + 'hello...', + ), + array( '[[hello world]].', + 8, + '[[hel...', + ), + ); + } + + /** + * @dataProvider dataGetTextForSummary + * @covers TextContent::getTextForSummary + */ + public function testGetTextForSummary( $text, $maxlength, $expected ) { + $content = $this->newContent( $text ); + + $this->assertEquals( $expected, $content->getTextForSummary( $maxlength ) ); + } + + /** + * @covers TextContent::getTextForSearchIndex + */ + public function testGetTextForSearchIndex() { + $content = $this->newContent( 'hello world.' ); + + $this->assertEquals( 'hello world.', $content->getTextForSearchIndex() ); + } + + /** + * @covers TextContent::copy + */ + public function testCopy() { + $content = $this->newContent( 'hello world.' ); + $copy = $content->copy(); + + $this->assertTrue( $content->equals( $copy ), 'copy must be equal to original' ); + $this->assertEquals( 'hello world.', $copy->getNativeData() ); + } + + /** + * @covers TextContent::getSize + */ + public function testGetSize() { + $content = $this->newContent( 'hello world.' ); + + $this->assertEquals( 12, $content->getSize() ); + } + + /** + * @covers TextContent::getNativeData + */ + public function testGetNativeData() { + $content = $this->newContent( 'hello world.' ); + + $this->assertEquals( 'hello world.', $content->getNativeData() ); + } + + /** + * @covers TextContent::getWikitextForTransclusion + */ + public function testGetWikitextForTransclusion() { + $content = $this->newContent( 'hello world.' ); + + $this->assertEquals( 'hello world.', $content->getWikitextForTransclusion() ); + } + + /** + * @covers TextContent::getModel + */ + public function testGetModel() { + $content = $this->newContent( "hello world." ); + + $this->assertEquals( CONTENT_MODEL_TEXT, $content->getModel() ); + } + + /** + * @covers TextContent::getContentHandler + */ + public function testGetContentHandler() { + $content = $this->newContent( "hello world." ); + + $this->assertEquals( CONTENT_MODEL_TEXT, $content->getContentHandler()->getModelID() ); + } + + public static function dataIsEmpty() { + return array( + array( '', true ), + array( ' ', false ), + array( '0', false ), + array( 'hallo welt.', false ), + ); + } + + /** + * @dataProvider dataIsEmpty + * @covers TextContent::isEmpty + */ + public function testIsEmpty( $text, $empty ) { + $content = $this->newContent( $text ); + + $this->assertEquals( $empty, $content->isEmpty() ); + } + + public static function dataEquals() { + return array( + array( new TextContent( "hallo" ), null, false ), + array( new TextContent( "hallo" ), new TextContent( "hallo" ), true ), + array( new TextContent( "hallo" ), new JavaScriptContent( "hallo" ), false ), + array( new TextContent( "hallo" ), new WikitextContent( "hallo" ), false ), + array( new TextContent( "hallo" ), new TextContent( "HALLO" ), false ), + ); + } + + /** + * @dataProvider dataEquals + * @covers TextContent::equals + */ + public function testEquals( Content $a, Content $b = null, $equal = false ) { + $this->assertEquals( $equal, $a->equals( $b ) ); + } + + public static function dataGetDeletionUpdates() { + return array( + array( "TextContentTest_testGetSecondaryDataUpdates_1", + CONTENT_MODEL_TEXT, "hello ''world''\n", + array() + ), + array( "TextContentTest_testGetSecondaryDataUpdates_2", + CONTENT_MODEL_TEXT, "hello [[world test 21344]]\n", + array() + ), + // TODO: more...? + ); + } + + /** + * @dataProvider dataGetDeletionUpdates + * @covers TextContent::getDeletionUpdates + */ + public function testDeletionUpdates( $title, $model, $text, $expectedStuff ) { + $ns = $this->getDefaultWikitextNS(); + $title = Title::newFromText( $title, $ns ); + + $content = ContentHandler::makeContent( $text, $title, $model ); + + $page = WikiPage::factory( $title ); + $page->doEditContent( $content, '' ); + + $updates = $content->getDeletionUpdates( $page ); + + // make updates accessible by class name + foreach ( $updates as $update ) { + $class = get_class( $update ); + $updates[$class] = $update; + } + + if ( !$expectedStuff ) { + $this->assertTrue( true ); // make phpunit happy + return; + } + + foreach ( $expectedStuff as $class => $fieldValues ) { + $this->assertArrayHasKey( $class, $updates, "missing an update of type $class" ); + + $update = $updates[$class]; + + foreach ( $fieldValues as $field => $value ) { + $v = $update->$field; #if the field doesn't exist, just crash and burn + $this->assertEquals( $value, $v, "unexpected value for field $field in instance of $class" ); + } + } + + $page->doDeleteArticle( '' ); + } + + public static function provideConvert() { + return array( + array( // #0 + 'Hallo Welt', + CONTENT_MODEL_WIKITEXT, + 'lossless', + 'Hallo Welt' + ), + array( // #1 + 'Hallo Welt', + CONTENT_MODEL_WIKITEXT, + 'lossless', + 'Hallo Welt' + ), + array( // #1 + 'Hallo Welt', + CONTENT_MODEL_CSS, + 'lossless', + 'Hallo Welt' + ), + array( // #1 + 'Hallo Welt', + CONTENT_MODEL_JAVASCRIPT, + 'lossless', + 'Hallo Welt' + ), + ); + } + + /** + * @dataProvider provideConvert + * @covers TextContent::convert + */ + public function testConvert( $text, $model, $lossy, $expectedNative ) { + $content = $this->newContent( $text ); + + $converted = $content->convert( $model, $lossy ); + + if ( $expectedNative === false ) { + $this->assertFalse( $converted, "conversion to $model was expected to fail!" ); + } else { + $this->assertInstanceOf( 'Content', $converted ); + $this->assertEquals( $expectedNative, $converted->getNativeData() ); + } + } +} diff --git a/tests/phpunit/includes/content/WikitextContentHandlerTest.php b/tests/phpunit/includes/content/WikitextContentHandlerTest.php new file mode 100644 index 00000000..75a72784 --- /dev/null +++ b/tests/phpunit/includes/content/WikitextContentHandlerTest.php @@ -0,0 +1,227 @@ +handler = ContentHandler::getForModelID( CONTENT_MODEL_WIKITEXT ); + } + + /** + * @covers WikitextContentHandler::serializeContent + */ + public function testSerializeContent() { + $content = new WikitextContent( 'hello world' ); + + $this->assertEquals( 'hello world', $this->handler->serializeContent( $content ) ); + $this->assertEquals( 'hello world', $this->handler->serializeContent( $content, CONTENT_FORMAT_WIKITEXT ) ); + + try { + $this->handler->serializeContent( $content, 'dummy/foo' ); + $this->fail( "serializeContent() should have failed on unknown format" ); + } catch ( MWException $e ) { + // ok, as expected + } + } + + /** + * @covers WikitextContentHandler::unserializeContent + */ + public function testUnserializeContent() { + $content = $this->handler->unserializeContent( 'hello world' ); + $this->assertEquals( 'hello world', $content->getNativeData() ); + + $content = $this->handler->unserializeContent( 'hello world', CONTENT_FORMAT_WIKITEXT ); + $this->assertEquals( 'hello world', $content->getNativeData() ); + + try { + $this->handler->unserializeContent( 'hello world', 'dummy/foo' ); + $this->fail( "unserializeContent() should have failed on unknown format" ); + } catch ( MWException $e ) { + // ok, as expected + } + } + + /** + * @covers WikitextContentHandler::makeEmptyContent + */ + public function testMakeEmptyContent() { + $content = $this->handler->makeEmptyContent(); + + $this->assertTrue( $content->isEmpty() ); + $this->assertEquals( '', $content->getNativeData() ); + } + + public static function dataIsSupportedFormat() { + return array( + array( null, true ), + array( CONTENT_FORMAT_WIKITEXT, true ), + array( 99887766, false ), + ); + } + + /** + * @dataProvider provideMakeRedirectContent + * @param Title|string $title Title object or string for Title::newFromText() + * @param string $expected Serialized form of the content object built + * @covers WikitextContentHandler::makeRedirectContent + */ + public function testMakeRedirectContent( $title, $expected ) { + global $wgContLang; + $wgContLang->resetNamespaces(); + + if ( is_string( $title ) ) { + $title = Title::newFromText( $title ); + } + $content = $this->handler->makeRedirectContent( $title ); + $this->assertEquals( $expected, $content->serialize() ); + } + + public static function provideMakeRedirectContent() { + return array( + array( 'Hello', '#REDIRECT [[Hello]]' ), + array( 'Template:Hello', '#REDIRECT [[Template:Hello]]' ), + array( 'Hello#section', '#REDIRECT [[Hello#section]]' ), + array( 'user:john_doe#section', '#REDIRECT [[User:John doe#section]]' ), + array( 'MEDIAWIKI:FOOBAR', '#REDIRECT [[MediaWiki:FOOBAR]]' ), + array( 'Category:Foo', '#REDIRECT [[:Category:Foo]]' ), + array( Title::makeTitle( NS_MAIN, 'en:Foo' ), '#REDIRECT [[en:Foo]]' ), + array( Title::makeTitle( NS_MAIN, 'Foo', '', 'en' ), '#REDIRECT [[:en:Foo]]' ), + array( Title::makeTitle( NS_MAIN, 'Bar', 'fragment', 'google' ), '#REDIRECT [[google:Bar#fragment]]' ), + ); + } + + /** + * @dataProvider dataIsSupportedFormat + * @covers WikitextContentHandler::isSupportedFormat + */ + public function testIsSupportedFormat( $format, $supported ) { + $this->assertEquals( $supported, $this->handler->isSupportedFormat( $format ) ); + } + + public static function dataMerge3() { + return array( + array( + "first paragraph + + second paragraph\n", + + "FIRST paragraph + + second paragraph\n", + + "first paragraph + + SECOND paragraph\n", + + "FIRST paragraph + + SECOND paragraph\n", + ), + + array( "first paragraph + second paragraph\n", + + "Bla bla\n", + + "Blubberdibla\n", + + false, + ), + ); + } + + /** + * @dataProvider dataMerge3 + * @covers WikitextContentHandler::merge3 + */ + public function testMerge3( $old, $mine, $yours, $expected ) { + $this->checkHasDiff3(); + + // test merge + $oldContent = new WikitextContent( $old ); + $myContent = new WikitextContent( $mine ); + $yourContent = new WikitextContent( $yours ); + + $merged = $this->handler->merge3( $oldContent, $myContent, $yourContent ); + + $this->assertEquals( $expected, $merged ? $merged->getNativeData() : $merged ); + } + + public static function dataGetAutosummary() { + return array( + array( + 'Hello there, world!', + '#REDIRECT [[Foo]]', + 0, + '/^Redirected page .*Foo/' + ), + + array( + null, + 'Hello world!', + EDIT_NEW, + '/^Created page .*Hello/' + ), + + array( + 'Hello there, world!', + '', + 0, + '/^Blanked/' + ), + + array( + 'Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut + labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et + ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.', + 'Hello world!', + 0, + '/^Replaced .*Hello/' + ), + + array( + 'foo', + 'bar', + 0, + '/^$/' + ), + ); + } + + /** + * @dataProvider dataGetAutosummary + * @covers WikitextContentHandler::getAutosummary + */ + public function testGetAutosummary( $old, $new, $flags, $expected ) { + $oldContent = is_null( $old ) ? null : new WikitextContent( $old ); + $newContent = is_null( $new ) ? null : new WikitextContent( $new ); + + $summary = $this->handler->getAutosummary( $oldContent, $newContent, $flags ); + + $this->assertTrue( (bool)preg_match( $expected, $summary ), "Autosummary didn't match expected pattern $expected: $summary" ); + } + + /** + * @todo Text case requires database, should be done by a test class in the Database group + */ + /* + public function testGetAutoDeleteReason( Title $title, &$hasHistory ) {} + */ + + /** + * @todo Text case requires database, should be done by a test class in the Database group + */ + /* + public function testGetUndoContent( Revision $current, Revision $undo, Revision $undoafter = null ) {} + */ +} diff --git a/tests/phpunit/includes/content/WikitextContentTest.php b/tests/phpunit/includes/content/WikitextContentTest.php new file mode 100644 index 00000000..9f20073d --- /dev/null +++ b/tests/phpunit/includes/content/WikitextContentTest.php @@ -0,0 +1,404 @@ +hello world\n

" + ), + // TODO: more...? + ); + } + + public static function dataGetSecondaryDataUpdates() { + return array( + array( "WikitextContentTest_testGetSecondaryDataUpdates_1", + CONTENT_MODEL_WIKITEXT, "hello ''world''\n", + array( + 'LinksUpdate' => array( + 'mRecursive' => true, + 'mLinks' => array() + ) + ) + ), + array( "WikitextContentTest_testGetSecondaryDataUpdates_2", + CONTENT_MODEL_WIKITEXT, "hello [[world test 21344]]\n", + array( + 'LinksUpdate' => array( + 'mRecursive' => true, + 'mLinks' => array( + array( 'World_test_21344' => 0 ) + ) + ) + ) + ), + // TODO: more...? + ); + } + + /** + * @dataProvider dataGetSecondaryDataUpdates + * @group Database + * @covers WikitextContent::getSecondaryDataUpdates + */ + public function testGetSecondaryDataUpdates( $title, $model, $text, $expectedStuff ) { + $ns = $this->getDefaultWikitextNS(); + $title = Title::newFromText( $title, $ns ); + + $content = ContentHandler::makeContent( $text, $title, $model ); + + $page = WikiPage::factory( $title ); + $page->doEditContent( $content, '' ); + + $updates = $content->getSecondaryDataUpdates( $title ); + + // make updates accessible by class name + foreach ( $updates as $update ) { + $class = get_class( $update ); + $updates[$class] = $update; + } + + foreach ( $expectedStuff as $class => $fieldValues ) { + $this->assertArrayHasKey( $class, $updates, "missing an update of type $class" ); + + $update = $updates[$class]; + + foreach ( $fieldValues as $field => $value ) { + $v = $update->$field; #if the field doesn't exist, just crash and burn + $this->assertEquals( $value, $v, "unexpected value for field $field in instance of $class" ); + } + } + + $page->doDeleteArticle( '' ); + } + + public static function dataGetSection() { + return array( + array( WikitextContentTest::$sections, + "0", + "Intro" + ), + array( WikitextContentTest::$sections, + "2", + "== test == +just a test" + ), + array( WikitextContentTest::$sections, + "8", + false + ), + ); + } + + /** + * @dataProvider dataGetSection + * @covers WikitextContent::getSection + */ + public function testGetSection( $text, $sectionId, $expectedText ) { + $content = $this->newContent( $text ); + + $sectionContent = $content->getSection( $sectionId ); + if ( is_object( $sectionContent ) ) { + $sectionText = $sectionContent->getNativeData(); + } else { + $sectionText = $sectionContent; + } + + $this->assertEquals( $expectedText, $sectionText ); + } + + public static function dataReplaceSection() { + return array( + array( WikitextContentTest::$sections, + "0", + "No more", + null, + trim( preg_replace( '/^Intro/sm', 'No more', WikitextContentTest::$sections ) ) + ), + array( WikitextContentTest::$sections, + "", + "No more", + null, + "No more" + ), + array( WikitextContentTest::$sections, + "2", + "== TEST ==\nmore fun", + null, + trim( preg_replace( '/^== test ==.*== foo ==/sm', "== TEST ==\nmore fun\n\n== foo ==", WikitextContentTest::$sections ) ) + ), + array( WikitextContentTest::$sections, + "8", + "No more", + null, + WikitextContentTest::$sections + ), + array( WikitextContentTest::$sections, + "new", + "No more", + "New", + trim( WikitextContentTest::$sections ) . "\n\n\n== New ==\n\nNo more" + ), + ); + } + + /** + * @dataProvider dataReplaceSection + * @covers WikitextContent::replaceSection + */ + public function testReplaceSection( $text, $section, $with, $sectionTitle, $expected ) { + $content = $this->newContent( $text ); + $c = $content->replaceSection( $section, $this->newContent( $with ), $sectionTitle ); + + $this->assertEquals( $expected, is_null( $c ) ? null : $c->getNativeData() ); + } + + /** + * @covers WikitextContent::addSectionHeader + */ + public function testAddSectionHeader() { + $content = $this->newContent( 'hello world' ); + $content = $content->addSectionHeader( 'test' ); + + $this->assertEquals( "== test ==\n\nhello world", $content->getNativeData() ); + } + + public static function dataPreSaveTransform() { + return array( + array( 'hello this is ~~~', + "hello this is [[Special:Contributions/127.0.0.1|127.0.0.1]]", + ), + array( 'hello \'\'this\'\' is ~~~', + 'hello \'\'this\'\' is ~~~', + ), + array( // rtrim + " Foo \n ", + " Foo", + ), + ); + } + + public static function dataPreloadTransform() { + return array( + array( 'hello this is ~~~', + "hello this is ~~~", + ), + array( 'hello \'\'this\'\' is foobar', + 'hello \'\'this\'\' is bar', + ), + ); + } + + public static function dataGetRedirectTarget() { + return array( + array( '#REDIRECT [[Test]]', + 'Test', + ), + array( '#REDIRECT Test', + null, + ), + array( '* #REDIRECT [[Test]]', + null, + ), + ); + } + + public static function dataGetTextForSummary() { + return array( + array( "hello\nworld.", + 16, + 'hello world.', + ), + array( 'hello world.', + 8, + 'hello...', + ), + array( '[[hello world]].', + 8, + 'hel...', + ), + ); + } + + /** + * @todo Test needs database! Should be done by a test class in the Database group. + */ + /* + public function getRedirectChain() { + $text = $this->getNativeData(); + return Title::newFromRedirectArray( $text ); + } + */ + + /** + * @todo Test needs database! Should be done by a test class in the Database group. + */ + /* + public function getUltimateRedirectTarget() { + $text = $this->getNativeData(); + return Title::newFromRedirectRecurse( $text ); + } + */ + + public static function dataIsCountable() { + return array( + array( '', + null, + 'any', + true + ), + array( 'Foo', + null, + 'any', + true + ), + array( 'Foo', + null, + 'comma', + false + ), + array( 'Foo, bar', + null, + 'comma', + true + ), + array( 'Foo', + null, + 'link', + false + ), + array( 'Foo [[bar]]', + null, + 'link', + true + ), + array( 'Foo', + true, + 'link', + true + ), + array( 'Foo [[bar]]', + false, + 'link', + false + ), + array( '#REDIRECT [[bar]]', + true, + 'any', + false + ), + array( '#REDIRECT [[bar]]', + true, + 'comma', + false + ), + array( '#REDIRECT [[bar]]', + true, + 'link', + false + ), + ); + } + + /** + * @covers WikitextContent::matchMagicWord + */ + public function testMatchMagicWord() { + $mw = MagicWord::get( "staticredirect" ); + + $content = $this->newContent( "#REDIRECT [[FOO]]\n__STATICREDIRECT__" ); + $this->assertTrue( $content->matchMagicWord( $mw ), "should have matched magic word" ); + + $content = $this->newContent( "#REDIRECT [[FOO]]" ); + $this->assertFalse( $content->matchMagicWord( $mw ), "should not have matched magic word" ); + } + + /** + * @covers WikitextContent::updateRedirect + */ + public function testUpdateRedirect() { + $target = Title::newFromText( "testUpdateRedirect_target" ); + + // test with non-redirect page + $content = $this->newContent( "hello world." ); + $newContent = $content->updateRedirect( $target ); + + $this->assertTrue( $content->equals( $newContent ), "content should be unchanged" ); + + // test with actual redirect + $content = $this->newContent( "#REDIRECT [[Someplace]]" ); + $newContent = $content->updateRedirect( $target ); + + $this->assertFalse( $content->equals( $newContent ), "content should have changed" ); + $this->assertTrue( $newContent->isRedirect(), "new content should be a redirect" ); + + $this->assertEquals( $target->getFullText(), $newContent->getRedirectTarget()->getFullText() ); + } + + /** + * @covers WikitextContent::getModel + */ + public function testGetModel() { + $content = $this->newContent( "hello world." ); + + $this->assertEquals( CONTENT_MODEL_WIKITEXT, $content->getModel() ); + } + + /** + * @covers WikitextContent::getContentHandler + */ + public function testGetContentHandler() { + $content = $this->newContent( "hello world." ); + + $this->assertEquals( CONTENT_MODEL_WIKITEXT, $content->getContentHandler()->getModelID() ); + } + + public static function dataEquals() { + return array( + array( new WikitextContent( "hallo" ), null, false ), + array( new WikitextContent( "hallo" ), new WikitextContent( "hallo" ), true ), + array( new WikitextContent( "hallo" ), new JavaScriptContent( "hallo" ), false ), + array( new WikitextContent( "hallo" ), new TextContent( "hallo" ), false ), + array( new WikitextContent( "hallo" ), new WikitextContent( "HALLO" ), false ), + ); + } + + public static function dataGetDeletionUpdates() { + return array( + array( "WikitextContentTest_testGetSecondaryDataUpdates_1", + CONTENT_MODEL_WIKITEXT, "hello ''world''\n", + array( 'LinksDeletionUpdate' => array() ) + ), + array( "WikitextContentTest_testGetSecondaryDataUpdates_2", + CONTENT_MODEL_WIKITEXT, "hello [[world test 21344]]\n", + array( 'LinksDeletionUpdate' => array() ) + ), + // @todo more...? + ); + } +} diff --git a/tests/phpunit/includes/db/DatabaseMysqlBaseTest.php b/tests/phpunit/includes/db/DatabaseMysqlBaseTest.php new file mode 100644 index 00000000..ba63c091 --- /dev/null +++ b/tests/phpunit/includes/db/DatabaseMysqlBaseTest.php @@ -0,0 +1,209 @@ +addIdentifierQuotes( $in ); + $this->assertEquals($expected, $quoted); + } + + + /** + * Feeds testAddIdentifierQuotes + * + * Named per bug 20281 convention. + */ + function provideDiapers() { + return array( + // Format: expected, input + array( '``', '' ), + + // Yeah I really hate loosely typed PHP idiocies nowadays + array( '``', null ), + + // Dear codereviewer, guess what addIdentifierQuotes() + // will return with thoses: + array( '``', false ), + array( '`1`', true ), + + // We never know what could happen + array( '`0`', 0 ), + array( '`1`', 1 ), + + // Whatchout! Should probably use something more meaningful + array( "`'`", "'" ), # single quote + array( '`"`', '"' ), # double quote + array( '````', '`' ), # backtick + array( '`’`', '’' ), # apostrophe (look at your encyclopedia) + + // sneaky NUL bytes are lurking everywhere + array( '``', "\0" ), + array( '`xyzzy`', "\0x\0y\0z\0z\0y\0" ), + + // unicode chars + array( + self::createUnicodeString( '`\u0001a\uFFFFb`' ), + self::createUnicodeString( '\u0001a\uFFFFb' ) + ), + array( + self::createUnicodeString( '`\u0001\uFFFF`' ), + self::createUnicodeString( '\u0001\u0000\uFFFF\u0000' ) + ), + array( '`☃`', '☃' ), + array( '`メインページ`', 'メインページ' ), + array( '`Басты_бет`', 'Басты_бет' ), + + // Real world: + array( '`Alix`', 'Alix' ), # while( ! $recovered ) { sleep(); } + array( '`Backtick: ```', 'Backtick: `' ), + array( '`This is a test`', 'This is a test' ), + ); + } + + private static function createUnicodeString($str) { + return json_decode( '"' . $str . '"' ); + } + + function getMockForViews() { + $db = $this->getMockBuilder( 'DatabaseMysql' ) + ->disableOriginalConstructor() + ->setMethods( array( 'fetchRow', 'query' ) ) + ->getMock(); + + $db->expects( $this->any() ) + ->method( 'query' ) + ->with( $this->anything() ) + ->will( + $this->returnValue( null ) + ); + + $db->expects( $this->any() ) + ->method( 'fetchRow' ) + ->with( $this->anything() ) + ->will( $this->onConsecutiveCalls( + array( 'Tables_in_' => 'view1' ), + array( 'Tables_in_' => 'view2' ), + array( 'Tables_in_' => 'myview' ), + false # no more rows + )); + return $db; + } + /** + * @covers DatabaseMysqlBase::listViews + */ + function testListviews() { + $db = $this->getMockForViews(); + + // The first call populate an internal cache of views + $this->assertEquals( array( 'view1', 'view2', 'myview'), + $db->listViews() ); + $this->assertEquals( array( 'view1', 'view2', 'myview'), + $db->listViews() ); + + // Prefix filtering + $this->assertEquals( array( 'view1', 'view2' ), + $db->listViews( 'view' ) ); + $this->assertEquals( array( 'myview' ), + $db->listViews( 'my' ) ); + $this->assertEquals( array(), + $db->listViews( 'UNUSED_PREFIX' ) ); + $this->assertEquals( array( 'view1', 'view2', 'myview'), + $db->listViews( '' ) ); + } + + /** + * @covers DatabaseMysqlBase::isView + * @dataProvider provideViewExistanceChecks + */ + function testIsView( $isView, $viewName ) { + $db = $this->getMockForViews(); + + switch( $isView ) { + case true: + $this->assertTrue( $db->isView( $viewName ), + "$viewName should be considered a view" ); + break; + + case false: + $this->assertFalse( $db->isView( $viewName ), + "$viewName has not been defined as a view" ); + break; + } + + } + + function provideViewExistanceChecks() { + return array( + // format: whether it is a view, view name + array( true, 'view1' ), + array( true, 'view2' ), + array( true, 'myview' ), + + array( false, 'user' ), + + array( false, 'view10' ), + array( false, 'my' ), + array( false, 'OH_MY_GOD' ), # they killed kenny! + ); + } + +} diff --git a/tests/phpunit/includes/db/DatabaseSQLTest.php b/tests/phpunit/includes/db/DatabaseSQLTest.php new file mode 100644 index 00000000..bdd567e7 --- /dev/null +++ b/tests/phpunit/includes/db/DatabaseSQLTest.php @@ -0,0 +1,721 @@ +database = new DatabaseTestHelper( __CLASS__ ); + } + + protected function assertLastSql( $sqlText ) { + $this->assertEquals( + $this->database->getLastSqls(), + $sqlText + ); + } + + /** + * @dataProvider provideSelect + * @covers DatabaseBase::select + */ + public function testSelect( $sql, $sqlText ) { + $this->database->select( + $sql['tables'], + $sql['fields'], + isset( $sql['conds'] ) ? $sql['conds'] : array(), + __METHOD__, + isset( $sql['options'] ) ? $sql['options'] : array(), + isset( $sql['join_conds'] ) ? $sql['join_conds'] : array() + ); + $this->assertLastSql( $sqlText ); + } + + public static function provideSelect() { + return array( + array( + array( + 'tables' => 'table', + 'fields' => array( 'field', 'alias' => 'field2' ), + 'conds' => array( 'alias' => 'text' ), + ), + "SELECT field,field2 AS alias " . + "FROM table " . + "WHERE alias = 'text'" + ), + array( + array( + 'tables' => 'table', + 'fields' => array( 'field', 'alias' => 'field2' ), + 'conds' => array( 'alias' => 'text' ), + 'options' => array( 'LIMIT' => 1, 'ORDER BY' => 'field' ), + ), + "SELECT field,field2 AS alias " . + "FROM table " . + "WHERE alias = 'text' " . + "ORDER BY field " . + "LIMIT 1" + ), + array( + array( + 'tables' => array( 'table', 't2' => 'table2' ), + 'fields' => array( 'tid', 'field', 'alias' => 'field2', 't2.id' ), + 'conds' => array( 'alias' => 'text' ), + 'options' => array( 'LIMIT' => 1, 'ORDER BY' => 'field' ), + 'join_conds' => array( 't2' => array( + 'LEFT JOIN', 'tid = t2.id' + ) ), + ), + "SELECT tid,field,field2 AS alias,t2.id " . + "FROM table LEFT JOIN table2 t2 ON ((tid = t2.id)) " . + "WHERE alias = 'text' " . + "ORDER BY field " . + "LIMIT 1" + ), + array( + array( + 'tables' => array( 'table', 't2' => 'table2' ), + 'fields' => array( 'tid', 'field', 'alias' => 'field2', 't2.id' ), + 'conds' => array( 'alias' => 'text' ), + 'options' => array( 'LIMIT' => 1, 'GROUP BY' => 'field', 'HAVING' => 'COUNT(*) > 1' ), + 'join_conds' => array( 't2' => array( + 'LEFT JOIN', 'tid = t2.id' + ) ), + ), + "SELECT tid,field,field2 AS alias,t2.id " . + "FROM table LEFT JOIN table2 t2 ON ((tid = t2.id)) " . + "WHERE alias = 'text' " . + "GROUP BY field HAVING COUNT(*) > 1 " . + "LIMIT 1" + ), + array( + array( + 'tables' => array( 'table', 't2' => 'table2' ), + 'fields' => array( 'tid', 'field', 'alias' => 'field2', 't2.id' ), + 'conds' => array( 'alias' => 'text' ), + 'options' => array( 'LIMIT' => 1, 'GROUP BY' => array( 'field', 'field2' ), 'HAVING' => array( 'COUNT(*) > 1', 'field' => 1 ) ), + 'join_conds' => array( 't2' => array( + 'LEFT JOIN', 'tid = t2.id' + ) ), + ), + "SELECT tid,field,field2 AS alias,t2.id " . + "FROM table LEFT JOIN table2 t2 ON ((tid = t2.id)) " . + "WHERE alias = 'text' " . + "GROUP BY field,field2 HAVING (COUNT(*) > 1) AND field = '1' " . + "LIMIT 1" + ), + array( + array( + 'tables' => array( 'table' ), + 'fields' => array( 'alias' => 'field' ), + 'conds' => array( 'alias' => array( 1, 2, 3, 4 ) ), + ), + "SELECT field AS alias " . + "FROM table " . + "WHERE alias IN ('1','2','3','4')" + ), + ); + } + + /** + * @dataProvider provideUpdate + * @covers DatabaseBase::update + */ + public function testUpdate( $sql, $sqlText ) { + $this->database->update( + $sql['table'], + $sql['values'], + $sql['conds'], + __METHOD__, + isset( $sql['options'] ) ? $sql['options'] : array() + ); + $this->assertLastSql( $sqlText ); + } + + public static function provideUpdate() { + return array( + array( + array( + 'table' => 'table', + 'values' => array( 'field' => 'text', 'field2' => 'text2' ), + 'conds' => array( 'alias' => 'text' ), + ), + "UPDATE table " . + "SET field = 'text'" . + ",field2 = 'text2' " . + "WHERE alias = 'text'" + ), + array( + array( + 'table' => 'table', + 'values' => array( 'field = other', 'field2' => 'text2' ), + 'conds' => array( 'id' => '1' ), + ), + "UPDATE table " . + "SET field = other" . + ",field2 = 'text2' " . + "WHERE id = '1'" + ), + array( + array( + 'table' => 'table', + 'values' => array( 'field = other', 'field2' => 'text2' ), + 'conds' => '*', + ), + "UPDATE table " . + "SET field = other" . + ",field2 = 'text2'" + ), + ); + } + + /** + * @dataProvider provideDelete + * @covers DatabaseBase::delete + */ + public function testDelete( $sql, $sqlText ) { + $this->database->delete( + $sql['table'], + $sql['conds'], + __METHOD__ + ); + $this->assertLastSql( $sqlText ); + } + + public static function provideDelete() { + return array( + array( + array( + 'table' => 'table', + 'conds' => array( 'alias' => 'text' ), + ), + "DELETE FROM table " . + "WHERE alias = 'text'" + ), + array( + array( + 'table' => 'table', + 'conds' => '*', + ), + "DELETE FROM table" + ), + ); + } + + /** + * @dataProvider provideUpsert + * @covers DatabaseBase::upsert + */ + public function testUpsert( $sql, $sqlText ) { + $this->database->upsert( + $sql['table'], + $sql['rows'], + $sql['uniqueIndexes'], + $sql['set'], + __METHOD__ + ); + $this->assertLastSql( $sqlText ); + } + + public static function provideUpsert() { + return array( + array( + array( + 'table' => 'upsert_table', + 'rows' => array( 'field' => 'text', 'field2' => 'text2' ), + 'uniqueIndexes' => array( 'field' ), + 'set' => array( 'field' => 'set' ), + ), + "BEGIN; " . + "UPDATE upsert_table " . + "SET field = 'set' " . + "WHERE ((field = 'text')); " . + "INSERT IGNORE INTO upsert_table " . + "(field,field2) " . + "VALUES ('text','text2'); " . + "COMMIT" + ), + ); + } + + /** + * @dataProvider provideDeleteJoin + * @covers DatabaseBase::deleteJoin + */ + public function testDeleteJoin( $sql, $sqlText ) { + $this->database->deleteJoin( + $sql['delTable'], + $sql['joinTable'], + $sql['delVar'], + $sql['joinVar'], + $sql['conds'], + __METHOD__ + ); + $this->assertLastSql( $sqlText ); + } + + public static function provideDeleteJoin() { + return array( + array( + array( + 'delTable' => 'table', + 'joinTable' => 'table_join', + 'delVar' => 'field', + 'joinVar' => 'field_join', + 'conds' => array( 'alias' => 'text' ), + ), + "DELETE FROM table " . + "WHERE field IN (" . + "SELECT field_join FROM table_join WHERE alias = 'text'" . + ")" + ), + array( + array( + 'delTable' => 'table', + 'joinTable' => 'table_join', + 'delVar' => 'field', + 'joinVar' => 'field_join', + 'conds' => '*', + ), + "DELETE FROM table " . + "WHERE field IN (" . + "SELECT field_join FROM table_join " . + ")" + ), + ); + } + + /** + * @dataProvider provideInsert + * @covers DatabaseBase::insert + */ + public function testInsert( $sql, $sqlText ) { + $this->database->insert( + $sql['table'], + $sql['rows'], + __METHOD__, + isset( $sql['options'] ) ? $sql['options'] : array() + ); + $this->assertLastSql( $sqlText ); + } + + public static function provideInsert() { + return array( + array( + array( + 'table' => 'table', + 'rows' => array( 'field' => 'text', 'field2' => 2 ), + ), + "INSERT INTO table " . + "(field,field2) " . + "VALUES ('text','2')" + ), + array( + array( + 'table' => 'table', + 'rows' => array( 'field' => 'text', 'field2' => 2 ), + 'options' => 'IGNORE', + ), + "INSERT IGNORE INTO table " . + "(field,field2) " . + "VALUES ('text','2')" + ), + array( + array( + 'table' => 'table', + 'rows' => array( + array( 'field' => 'text', 'field2' => 2 ), + array( 'field' => 'multi', 'field2' => 3 ), + ), + 'options' => 'IGNORE', + ), + "INSERT IGNORE INTO table " . + "(field,field2) " . + "VALUES " . + "('text','2')," . + "('multi','3')" + ), + ); + } + + /** + * @dataProvider provideInsertSelect + * @covers DatabaseBase::insertSelect + */ + public function testInsertSelect( $sql, $sqlText ) { + $this->database->insertSelect( + $sql['destTable'], + $sql['srcTable'], + $sql['varMap'], + $sql['conds'], + __METHOD__, + isset( $sql['insertOptions'] ) ? $sql['insertOptions'] : array(), + isset( $sql['selectOptions'] ) ? $sql['selectOptions'] : array() + ); + $this->assertLastSql( $sqlText ); + } + + public static function provideInsertSelect() { + return array( + array( + array( + 'destTable' => 'insert_table', + 'srcTable' => 'select_table', + 'varMap' => array( 'field_insert' => 'field_select', 'field' => 'field2' ), + 'conds' => '*', + ), + "INSERT INTO insert_table " . + "(field_insert,field) " . + "SELECT field_select,field2 " . + "FROM select_table" + ), + array( + array( + 'destTable' => 'insert_table', + 'srcTable' => 'select_table', + 'varMap' => array( 'field_insert' => 'field_select', 'field' => 'field2' ), + 'conds' => array( 'field' => 2 ), + ), + "INSERT INTO insert_table " . + "(field_insert,field) " . + "SELECT field_select,field2 " . + "FROM select_table " . + "WHERE field = '2'" + ), + array( + array( + 'destTable' => 'insert_table', + 'srcTable' => 'select_table', + 'varMap' => array( 'field_insert' => 'field_select', 'field' => 'field2' ), + 'conds' => array( 'field' => 2 ), + 'insertOptions' => 'IGNORE', + 'selectOptions' => array( 'ORDER BY' => 'field' ), + ), + "INSERT IGNORE INTO insert_table " . + "(field_insert,field) " . + "SELECT field_select,field2 " . + "FROM select_table " . + "WHERE field = '2' " . + "ORDER BY field" + ), + ); + } + + /** + * @dataProvider provideReplace + * @covers DatabaseBase::replace + */ + public function testReplace( $sql, $sqlText ) { + $this->database->replace( + $sql['table'], + $sql['uniqueIndexes'], + $sql['rows'], + __METHOD__ + ); + $this->assertLastSql( $sqlText ); + } + + public static function provideReplace() { + return array( + array( + array( + 'table' => 'replace_table', + 'uniqueIndexes' => array( 'field' ), + 'rows' => array( 'field' => 'text', 'field2' => 'text2' ), + ), + "DELETE FROM replace_table " . + "WHERE ( field='text' ); " . + "INSERT INTO replace_table " . + "(field,field2) " . + "VALUES ('text','text2')" + ), + array( + array( + 'table' => 'module_deps', + 'uniqueIndexes' => array( array( 'md_module', 'md_skin' ) ), + 'rows' => array( + 'md_module' => 'module', + 'md_skin' => 'skin', + 'md_deps' => 'deps', + ), + ), + "DELETE FROM module_deps " . + "WHERE ( md_module='module' AND md_skin='skin' ); " . + "INSERT INTO module_deps " . + "(md_module,md_skin,md_deps) " . + "VALUES ('module','skin','deps')" + ), + array( + array( + 'table' => 'module_deps', + 'uniqueIndexes' => array( array( 'md_module', 'md_skin' ) ), + 'rows' => array( + array( + 'md_module' => 'module', + 'md_skin' => 'skin', + 'md_deps' => 'deps', + ), array( + 'md_module' => 'module2', + 'md_skin' => 'skin2', + 'md_deps' => 'deps2', + ), + ), + ), + "DELETE FROM module_deps " . + "WHERE ( md_module='module' AND md_skin='skin' ); " . + "INSERT INTO module_deps " . + "(md_module,md_skin,md_deps) " . + "VALUES ('module','skin','deps'); " . + "DELETE FROM module_deps " . + "WHERE ( md_module='module2' AND md_skin='skin2' ); " . + "INSERT INTO module_deps " . + "(md_module,md_skin,md_deps) " . + "VALUES ('module2','skin2','deps2')" + ), + array( + array( + 'table' => 'module_deps', + 'uniqueIndexes' => array( 'md_module', 'md_skin' ), + 'rows' => array( + array( + 'md_module' => 'module', + 'md_skin' => 'skin', + 'md_deps' => 'deps', + ), array( + 'md_module' => 'module2', + 'md_skin' => 'skin2', + 'md_deps' => 'deps2', + ), + ), + ), + "DELETE FROM module_deps " . + "WHERE ( md_module='module' ) OR ( md_skin='skin' ); " . + "INSERT INTO module_deps " . + "(md_module,md_skin,md_deps) " . + "VALUES ('module','skin','deps'); " . + "DELETE FROM module_deps " . + "WHERE ( md_module='module2' ) OR ( md_skin='skin2' ); " . + "INSERT INTO module_deps " . + "(md_module,md_skin,md_deps) " . + "VALUES ('module2','skin2','deps2')" + ), + array( + array( + 'table' => 'module_deps', + 'uniqueIndexes' => array(), + 'rows' => array( + 'md_module' => 'module', + 'md_skin' => 'skin', + 'md_deps' => 'deps', + ), + ), + "INSERT INTO module_deps " . + "(md_module,md_skin,md_deps) " . + "VALUES ('module','skin','deps')" + ), + ); + } + + /** + * @dataProvider provideNativeReplace + * @covers DatabaseBase::nativeReplace + */ + public function testNativeReplace( $sql, $sqlText ) { + $this->database->nativeReplace( + $sql['table'], + $sql['rows'], + __METHOD__ + ); + $this->assertLastSql( $sqlText ); + } + + public static function provideNativeReplace() { + return array( + array( + array( + 'table' => 'replace_table', + 'rows' => array( 'field' => 'text', 'field2' => 'text2' ), + ), + "REPLACE INTO replace_table " . + "(field,field2) " . + "VALUES ('text','text2')" + ), + ); + } + + /** + * @dataProvider provideConditional + * @covers DatabaseBase::conditional + */ + public function testConditional( $sql, $sqlText ) { + $this->assertEquals( trim( $this->database->conditional( + $sql['conds'], + $sql['true'], + $sql['false'] + ) ), $sqlText ); + } + + public static function provideConditional() { + return array( + array( + array( + 'conds' => array( 'field' => 'text' ), + 'true' => 1, + 'false' => 'NULL', + ), + "(CASE WHEN field = 'text' THEN 1 ELSE NULL END)" + ), + array( + array( + 'conds' => array( 'field' => 'text', 'field2' => 'anothertext' ), + 'true' => 1, + 'false' => 'NULL', + ), + "(CASE WHEN field = 'text' AND field2 = 'anothertext' THEN 1 ELSE NULL END)" + ), + array( + array( + 'conds' => 'field=1', + 'true' => 1, + 'false' => 'NULL', + ), + "(CASE WHEN field=1 THEN 1 ELSE NULL END)" + ), + ); + } + + /** + * @dataProvider provideBuildConcat + * @covers DatabaseBase::buildConcat + */ + public function testBuildConcat( $stringList, $sqlText ) { + $this->assertEquals( trim( $this->database->buildConcat( + $stringList + ) ), $sqlText ); + } + + public static function provideBuildConcat() { + return array( + array( + array( 'field', 'field2' ), + "CONCAT(field,field2)" + ), + array( + array( "'test'", 'field2' ), + "CONCAT('test',field2)" + ), + ); + } + + /** + * @dataProvider provideBuildLike + * @covers DatabaseBase::buildLike + */ + public function testBuildLike( $array, $sqlText ) { + $this->assertEquals( trim( $this->database->buildLike( + $array + ) ), $sqlText ); + } + + public static function provideBuildLike() { + return array( + array( + 'text', + "LIKE 'text'" + ), + array( + array( 'text', new LikeMatch( '%' ) ), + "LIKE 'text%'" + ), + array( + array( 'text', new LikeMatch( '%' ), 'text2' ), + "LIKE 'text%text2'" + ), + array( + array( 'text', new LikeMatch( '_' ) ), + "LIKE 'text_'" + ), + ); + } + + /** + * @dataProvider provideUnionQueries + * @covers DatabaseBase::unionQueries + */ + public function testUnionQueries( $sql, $sqlText ) { + $this->assertEquals( trim( $this->database->unionQueries( + $sql['sqls'], + $sql['all'] + ) ), $sqlText ); + } + + public static function provideUnionQueries() { + return array( + array( + array( + 'sqls' => array( 'RAW SQL', 'RAW2SQL' ), + 'all' => true, + ), + "(RAW SQL) UNION ALL (RAW2SQL)" + ), + array( + array( + 'sqls' => array( 'RAW SQL', 'RAW2SQL' ), + 'all' => false, + ), + "(RAW SQL) UNION (RAW2SQL)" + ), + array( + array( + 'sqls' => array( 'RAW SQL', 'RAW2SQL', 'RAW3SQL' ), + 'all' => false, + ), + "(RAW SQL) UNION (RAW2SQL) UNION (RAW3SQL)" + ), + ); + } + + /** + * @covers DatabaseBase::commit + */ + public function testTransactionCommit() { + $this->database->begin( __METHOD__ ); + $this->database->commit( __METHOD__ ); + $this->assertLastSql( 'BEGIN; COMMIT' ); + } + + /** + * @covers DatabaseBase::rollback + */ + public function testTransactionRollback() { + $this->database->begin( __METHOD__ ); + $this->database->rollback( __METHOD__ ); + $this->assertLastSql( 'BEGIN; ROLLBACK' ); + } + + /** + * @covers DatabaseBase::dropTable + */ + public function testDropTable() { + $this->database->setExistingTables( array( 'table' ) ); + $this->database->dropTable( 'table', __METHOD__ ); + $this->assertLastSql( 'DROP TABLE table' ); + } + + /** + * @covers DatabaseBase::dropTable + */ + public function testDropNonExistingTable() { + $this->assertFalse( + $this->database->dropTable( 'non_existing', __METHOD__ ) + ); + } +} diff --git a/tests/phpunit/includes/db/DatabaseSqliteTest.php b/tests/phpunit/includes/db/DatabaseSqliteTest.php new file mode 100644 index 00000000..70ee9465 --- /dev/null +++ b/tests/phpunit/includes/db/DatabaseSqliteTest.php @@ -0,0 +1,421 @@ +lastQuery = $sql; + + return true; + } + + /** + * Override parent visibility to public + */ + public function replaceVars( $s ) { + return parent::replaceVars( $s ); + } +} + +/** + * @group sqlite + * @group Database + * @group medium + */ +class DatabaseSqliteTest extends MediaWikiTestCase { + + /** + * @var MockDatabaseSqlite + */ + var $db; + + protected function setUp() { + parent::setUp(); + + if ( !Sqlite::isPresent() ) { + $this->markTestSkipped( 'No SQLite support detected' ); + } + $this->db = new MockDatabaseSqlite(); + if ( version_compare( $this->db->getServerVersion(), '3.6.0', '<' ) ) { + $this->markTestSkipped( "SQLite at least 3.6 required, {$this->db->getServerVersion()} found" ); + } + } + + private function replaceVars( $sql ) { + // normalize spacing to hide implementation details + return preg_replace( '/\s+/', ' ', $this->db->replaceVars( $sql ) ); + } + + private function assertResultIs( $expected, $res ) { + $this->assertNotNull( $res ); + $i = 0; + foreach ( $res as $row ) { + foreach ( $expected[$i] as $key => $value ) { + $this->assertTrue( isset( $row->$key ) ); + $this->assertEquals( $value, $row->$key ); + } + $i++; + } + $this->assertEquals( count( $expected ), $i, 'Unexpected number of rows' ); + } + + public static function provideAddQuotes() { + return array( + array( // #0: empty + '', "''" + ), + array( // #1: simple + 'foo bar', "'foo bar'" + ), + array( // #2: including quote + 'foo\'bar', "'foo''bar'" + ), + array( // #3: including \0 (must be represented as hex, per https://bugs.php.net/bug.php?id=63419) + "x\0y", + "x'780079'", + ), + array( // #4: blob object (must be represented as hex) + new Blob( "hello" ), + "x'68656c6c6f'", + ), + ); + } + + /** + * @dataProvider provideAddQuotes() + * @covers DatabaseSqlite::addQuotes + */ + public function testAddQuotes( $value, $expected ) { + // check quoting + $db = new DatabaseSqliteStandalone( ':memory:' ); + $this->assertEquals( $expected, $db->addQuotes( $value ), 'string not quoted as expected' ); + + // ok, quoting works as expected, now try a round trip. + $re = $db->query( 'select ' . $db->addQuotes( $value ) ); + + $this->assertTrue( $re !== false, 'query failed' ); + + if ( $row = $re->fetchRow() ) { + if ( $value instanceof Blob ) { + $value = $value->fetch(); + } + + $this->assertEquals( $value, $row[0], 'string mangled by the database' ); + } else { + $this->fail( 'query returned no result' ); + } + } + + /** + * @covers DatabaseSqlite::replaceVars + */ + public function testReplaceVars() { + $this->assertEquals( 'foo', $this->replaceVars( 'foo' ), "Don't break anything accidentally" ); + + $this->assertEquals( "CREATE TABLE /**/foo (foo_key INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, " + . "foo_bar TEXT, foo_name TEXT NOT NULL DEFAULT '', foo_int INTEGER, foo_int2 INTEGER );", + $this->replaceVars( "CREATE TABLE /**/foo (foo_key int unsigned NOT NULL PRIMARY KEY AUTO_INCREMENT, + foo_bar char(13), foo_name varchar(255) binary NOT NULL DEFAULT '', foo_int tinyint ( 8 ), foo_int2 int(16) ) ENGINE=MyISAM;" ) + ); + + $this->assertEquals( "CREATE TABLE foo ( foo1 REAL, foo2 REAL, foo3 REAL );", + $this->replaceVars( "CREATE TABLE foo ( foo1 FLOAT, foo2 DOUBLE( 1,10), foo3 DOUBLE PRECISION );" ) + ); + + $this->assertEquals( "CREATE TABLE foo ( foo_binary1 BLOB, foo_binary2 BLOB );", + $this->replaceVars( "CREATE TABLE foo ( foo_binary1 binary(16), foo_binary2 varbinary(32) );" ) + ); + + $this->assertEquals( "CREATE TABLE text ( text_foo TEXT );", + $this->replaceVars( "CREATE TABLE text ( text_foo tinytext );" ), + 'Table name changed' + ); + + $this->assertEquals( "CREATE TABLE foo ( foobar INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL );", + $this->replaceVars( "CREATE TABLE foo ( foobar INT PRIMARY KEY NOT NULL AUTO_INCREMENT );" ) + ); + $this->assertEquals( "CREATE TABLE foo ( foobar INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL );", + $this->replaceVars( "CREATE TABLE foo ( foobar INT PRIMARY KEY AUTO_INCREMENT NOT NULL );" ) + ); + + $this->assertEquals( "CREATE TABLE enums( enum1 TEXT, myenum TEXT)", + $this->replaceVars( "CREATE TABLE enums( enum1 ENUM('A', 'B'), myenum ENUM ('X', 'Y'))" ) + ); + + $this->assertEquals( "ALTER TABLE foo ADD COLUMN foo_bar INTEGER DEFAULT 42", + $this->replaceVars( "ALTER TABLE foo\nADD COLUMN foo_bar int(10) unsigned DEFAULT 42" ) + ); + } + + /** + * @covers DatabaseSqlite::tableName + */ + public function testTableName() { + // @todo Moar! + $db = new DatabaseSqliteStandalone( ':memory:' ); + $this->assertEquals( 'foo', $db->tableName( 'foo' ) ); + $this->assertEquals( 'sqlite_master', $db->tableName( 'sqlite_master' ) ); + $db->tablePrefix( 'foo' ); + $this->assertEquals( 'sqlite_master', $db->tableName( 'sqlite_master' ) ); + $this->assertEquals( 'foobar', $db->tableName( 'bar' ) ); + } + + /** + * @covers DatabaseSqlite::duplicateTableStructure + */ + public function testDuplicateTableStructure() { + $db = new DatabaseSqliteStandalone( ':memory:' ); + $db->query( 'CREATE TABLE foo(foo, barfoo)' ); + + $db->duplicateTableStructure( 'foo', 'bar' ); + $this->assertEquals( 'CREATE TABLE "bar"(foo, barfoo)', + $db->selectField( 'sqlite_master', 'sql', array( 'name' => 'bar' ) ), + 'Normal table duplication' + ); + + $db->duplicateTableStructure( 'foo', 'baz', true ); + $this->assertEquals( 'CREATE TABLE "baz"(foo, barfoo)', + $db->selectField( 'sqlite_temp_master', 'sql', array( 'name' => 'baz' ) ), + 'Creation of temporary duplicate' + ); + $this->assertEquals( 0, + $db->selectField( 'sqlite_master', 'COUNT(*)', array( 'name' => 'baz' ) ), + 'Create a temporary duplicate only' + ); + } + + /** + * @covers DatabaseSqlite::duplicateTableStructure + */ + public function testDuplicateTableStructureVirtual() { + $db = new DatabaseSqliteStandalone( ':memory:' ); + if ( $db->getFulltextSearchModule() != 'FTS3' ) { + $this->markTestSkipped( 'FTS3 not supported, cannot create virtual tables' ); + } + $db->query( 'CREATE VIRTUAL TABLE "foo" USING FTS3(foobar)' ); + + $db->duplicateTableStructure( 'foo', 'bar' ); + $this->assertEquals( 'CREATE VIRTUAL TABLE "bar" USING FTS3(foobar)', + $db->selectField( 'sqlite_master', 'sql', array( 'name' => 'bar' ) ), + 'Duplication of virtual tables' + ); + + $db->duplicateTableStructure( 'foo', 'baz', true ); + $this->assertEquals( 'CREATE VIRTUAL TABLE "baz" USING FTS3(foobar)', + $db->selectField( 'sqlite_master', 'sql', array( 'name' => 'baz' ) ), + "Can't create temporary virtual tables, should fall back to non-temporary duplication" + ); + } + + /** + * @covers DatabaseSqlite::deleteJoin + */ + public function testDeleteJoin() { + $db = new DatabaseSqliteStandalone( ':memory:' ); + $db->query( 'CREATE TABLE a (a_1)', __METHOD__ ); + $db->query( 'CREATE TABLE b (b_1, b_2)', __METHOD__ ); + $db->insert( 'a', array( + array( 'a_1' => 1 ), + array( 'a_1' => 2 ), + array( 'a_1' => 3 ), + ), + __METHOD__ + ); + $db->insert( 'b', array( + array( 'b_1' => 2, 'b_2' => 'a' ), + array( 'b_1' => 3, 'b_2' => 'b' ), + ), + __METHOD__ + ); + $db->deleteJoin( 'a', 'b', 'a_1', 'b_1', array( 'b_2' => 'a' ), __METHOD__ ); + $res = $db->query( "SELECT * FROM a", __METHOD__ ); + $this->assertResultIs( array( + array( 'a_1' => 1 ), + array( 'a_1' => 3 ), + ), + $res + ); + } + + public function testEntireSchema() { + global $IP; + + $result = Sqlite::checkSqlSyntax( "$IP/maintenance/tables.sql" ); + if ( $result !== true ) { + $this->fail( $result ); + } + $this->assertTrue( true ); // avoid test being marked as incomplete due to lack of assertions + } + + /** + * Runs upgrades of older databases and compares results with current schema + * @todo Currently only checks list of tables + */ + public function testUpgrades() { + global $IP, $wgVersion, $wgProfileToDatabase; + + // Versions tested + $versions = array( + //'1.13', disabled for now, was totally screwed up + // SQLite wasn't included in 1.14 + '1.15', + '1.16', + '1.17', + '1.18', + ); + + // Mismatches for these columns we can safely ignore + $ignoredColumns = array( + 'user_newtalk.user_last_timestamp', // r84185 + ); + + $currentDB = new DatabaseSqliteStandalone( ':memory:' ); + $currentDB->sourceFile( "$IP/maintenance/tables.sql" ); + if ( $wgProfileToDatabase ) { + $currentDB->sourceFile( "$IP/maintenance/sqlite/archives/patch-profiling.sql" ); + } + $currentTables = $this->getTables( $currentDB ); + sort( $currentTables ); + + foreach ( $versions as $version ) { + $versions = "upgrading from $version to $wgVersion"; + $db = $this->prepareDB( $version ); + $tables = $this->getTables( $db ); + $this->assertEquals( $currentTables, $tables, "Different tables $versions" ); + foreach ( $tables as $table ) { + $currentCols = $this->getColumns( $currentDB, $table ); + $cols = $this->getColumns( $db, $table ); + $this->assertEquals( + array_keys( $currentCols ), + array_keys( $cols ), + "Mismatching columns for table \"$table\" $versions" + ); + foreach ( $currentCols as $name => $column ) { + $fullName = "$table.$name"; + $this->assertEquals( + (bool)$column->pk, + (bool)$cols[$name]->pk, + "PRIMARY KEY status does not match for column $fullName $versions" + ); + if ( !in_array( $fullName, $ignoredColumns ) ) { + $this->assertEquals( + (bool)$column->notnull, + (bool)$cols[$name]->notnull, + "NOT NULL status does not match for column $fullName $versions" + ); + $this->assertEquals( + $column->dflt_value, + $cols[$name]->dflt_value, + "Default values does not match for column $fullName $versions" + ); + } + } + $currentIndexes = $this->getIndexes( $currentDB, $table ); + $indexes = $this->getIndexes( $db, $table ); + $this->assertEquals( + array_keys( $currentIndexes ), + array_keys( $indexes ), + "mismatching indexes for table \"$table\" $versions" + ); + } + $db->close(); + } + } + + /** + * @covers DatabaseSqlite::insertId + */ + public function testInsertIdType() { + $db = new DatabaseSqliteStandalone( ':memory:' ); + + $databaseCreation = $db->query( 'CREATE TABLE a ( a_1 )', __METHOD__ ); + $this->assertInstanceOf( 'ResultWrapper', $databaseCreation, "Database creation" ); + + $insertion = $db->insert( 'a', array( 'a_1' => 10 ), __METHOD__ ); + $this->assertTrue( $insertion, "Insertion worked" ); + + $this->assertInternalType( 'integer', $db->insertId(), "Actual typecheck" ); + $this->assertTrue( $db->close(), "closing database" ); + } + + private function prepareDB( $version ) { + static $maint = null; + if ( $maint === null ) { + $maint = new FakeMaintenance(); + $maint->loadParamsAndArgs( null, array( 'quiet' => 1 ) ); + } + + global $IP; + $db = new DatabaseSqliteStandalone( ':memory:' ); + $db->sourceFile( "$IP/tests/phpunit/data/db/sqlite/tables-$version.sql" ); + $updater = DatabaseUpdater::newForDB( $db, false, $maint ); + $updater->doUpdates( array( 'core' ) ); + + return $db; + } + + private function getTables( $db ) { + $list = array_flip( $db->listTables() ); + $excluded = array( + 'external_user', // removed from core in 1.22 + 'math', // moved out of core in 1.18 + 'trackbacks', // removed from core in 1.19 + 'searchindex', + 'searchindex_content', + 'searchindex_segments', + 'searchindex_segdir', + // FTS4 ready!!1 + 'searchindex_docsize', + 'searchindex_stat', + ); + foreach ( $excluded as $t ) { + unset( $list[$t] ); + } + $list = array_flip( $list ); + sort( $list ); + + return $list; + } + + private function getColumns( $db, $table ) { + $cols = array(); + $res = $db->query( "PRAGMA table_info($table)" ); + $this->assertNotNull( $res ); + foreach ( $res as $col ) { + $cols[$col->name] = $col; + } + ksort( $cols ); + + return $cols; + } + + private function getIndexes( $db, $table ) { + $indexes = array(); + $res = $db->query( "PRAGMA index_list($table)" ); + $this->assertNotNull( $res ); + foreach ( $res as $index ) { + $res2 = $db->query( "PRAGMA index_info({$index->name})" ); + $this->assertNotNull( $res2 ); + $index->columns = array(); + foreach ( $res2 as $col ) { + $index->columns[] = $col; + } + $indexes[$index->name] = $index; + } + ksort( $indexes ); + + return $indexes; + } + + public function testCaseInsensitiveLike() { + // TODO: Test this for all databases + $db = new DatabaseSqliteStandalone( ':memory:' ); + $res = $db->query( 'SELECT "a" LIKE "A" AS a' ); + $row = $res->fetchRow(); + $this->assertFalse( (bool)$row['a'] ); + } +} diff --git a/tests/phpunit/includes/db/DatabaseTest.php b/tests/phpunit/includes/db/DatabaseTest.php new file mode 100644 index 00000000..301fc990 --- /dev/null +++ b/tests/phpunit/includes/db/DatabaseTest.php @@ -0,0 +1,234 @@ +db = wfGetDB( DB_MASTER ); + } + + protected function tearDown() { + parent::tearDown(); + if ( $this->functionTest ) { + $this->dropFunctions(); + $this->functionTest = false; + } + } + /** + * @covers DatabaseBase::dropTable + */ + public function testAddQuotesNull() { + $check = "NULL"; + if ( $this->db->getType() === 'sqlite' || $this->db->getType() === 'oracle' ) { + $check = "''"; + } + $this->assertEquals( $check, $this->db->addQuotes( null ) ); + } + + public function testAddQuotesInt() { + # returning just "1234" should be ok too, though... + # maybe + $this->assertEquals( + "'1234'", + $this->db->addQuotes( 1234 ) ); + } + + public function testAddQuotesFloat() { + # returning just "1234.5678" would be ok too, though + $this->assertEquals( + "'1234.5678'", + $this->db->addQuotes( 1234.5678 ) ); + } + + public function testAddQuotesString() { + $this->assertEquals( + "'string'", + $this->db->addQuotes( 'string' ) ); + } + + public function testAddQuotesStringQuote() { + $check = "'string''s cause trouble'"; + if ( $this->db->getType() === 'mysql' ) { + $check = "'string\'s cause trouble'"; + } + $this->assertEquals( + $check, + $this->db->addQuotes( "string's cause trouble" ) ); + } + + private function getSharedTableName( $table, $database, $prefix, $format = 'quoted' ) { + global $wgSharedDB, $wgSharedTables, $wgSharedPrefix; + + $oldName = $wgSharedDB; + $oldTables = $wgSharedTables; + $oldPrefix = $wgSharedPrefix; + + $wgSharedDB = $database; + $wgSharedTables = array( $table ); + $wgSharedPrefix = $prefix; + + $ret = $this->db->tableName( $table, $format ); + + $wgSharedDB = $oldName; + $wgSharedTables = $oldTables; + $wgSharedPrefix = $oldPrefix; + + return $ret; + } + + private function prefixAndQuote( $table, $database = null, $prefix = null, $format = 'quoted' ) { + if ( $this->db->getType() === 'sqlite' || $format !== 'quoted' ) { + $quote = ''; + } elseif ( $this->db->getType() === 'mysql' ) { + $quote = '`'; + } elseif ( $this->db->getType() === 'oracle' ) { + $quote = '/*Q*/'; + } else { + $quote = '"'; + } + + if ( $database !== null ) { + if ( $this->db->getType() === 'oracle' ) { + $database = $quote . $database . '.'; + } else { + $database = $quote . $database . $quote . '.'; + } + } + + if ( $prefix === null ) { + $prefix = $this->dbPrefix(); + } + + if ( $this->db->getType() === 'oracle' ) { + return strtoupper($database . $quote . $prefix . $table); + } else { + return $database . $quote . $prefix . $table . $quote; + } + } + + public function testTableNameLocal() { + $this->assertEquals( + $this->prefixAndQuote( 'tablename' ), + $this->db->tableName( 'tablename' ) + ); + } + + public function testTableNameRawLocal() { + $this->assertEquals( + $this->prefixAndQuote( 'tablename', null, null, 'raw' ), + $this->db->tableName( 'tablename', 'raw' ) + ); + } + + public function testTableNameShared() { + $this->assertEquals( + $this->prefixAndQuote( 'tablename', 'sharedatabase', 'sh_' ), + $this->getSharedTableName( 'tablename', 'sharedatabase', 'sh_' ) + ); + + $this->assertEquals( + $this->prefixAndQuote( 'tablename', 'sharedatabase', null ), + $this->getSharedTableName( 'tablename', 'sharedatabase', null ) + ); + } + + public function testTableNameRawShared() { + $this->assertEquals( + $this->prefixAndQuote( 'tablename', 'sharedatabase', 'sh_', 'raw' ), + $this->getSharedTableName( 'tablename', 'sharedatabase', 'sh_', 'raw' ) + ); + + $this->assertEquals( + $this->prefixAndQuote( 'tablename', 'sharedatabase', null, 'raw' ), + $this->getSharedTableName( 'tablename', 'sharedatabase', null, 'raw' ) + ); + } + + public function testTableNameForeign() { + $this->assertEquals( + $this->prefixAndQuote( 'tablename', 'databasename', '' ), + $this->db->tableName( 'databasename.tablename' ) + ); + } + + public function testTableNameRawForeign() { + $this->assertEquals( + $this->prefixAndQuote( 'tablename', 'databasename', '', 'raw' ), + $this->db->tableName( 'databasename.tablename', 'raw' ) + ); + } + + public function testFillPreparedEmpty() { + $sql = $this->db->fillPrepared( + 'SELECT * FROM interwiki', array() ); + $this->assertEquals( + "SELECT * FROM interwiki", + $sql ); + } + + public function testFillPreparedQuestion() { + $sql = $this->db->fillPrepared( + 'SELECT * FROM cur WHERE cur_namespace=? AND cur_title=?', + array( 4, "Snicker's_paradox" ) ); + + $check = "SELECT * FROM cur WHERE cur_namespace='4' AND cur_title='Snicker''s_paradox'"; + if ( $this->db->getType() === 'mysql' ) { + $check = "SELECT * FROM cur WHERE cur_namespace='4' AND cur_title='Snicker\'s_paradox'"; + } + $this->assertEquals( $check, $sql ); + } + + public function testFillPreparedBang() { + $sql = $this->db->fillPrepared( + 'SELECT user_id FROM ! WHERE user_name=?', + array( '"user"', "Slash's Dot" ) ); + + $check = "SELECT user_id FROM \"user\" WHERE user_name='Slash''s Dot'"; + if ( $this->db->getType() === 'mysql' ) { + $check = "SELECT user_id FROM \"user\" WHERE user_name='Slash\'s Dot'"; + } + $this->assertEquals( $check, $sql ); + } + + public function testFillPreparedRaw() { + $sql = $this->db->fillPrepared( + "SELECT * FROM cur WHERE cur_title='This_\\&_that,_WTF\\?\\!'", + array( '"user"', "Slash's Dot" ) ); + $this->assertEquals( + "SELECT * FROM cur WHERE cur_title='This_&_that,_WTF?!'", + $sql ); + } + + public function testStoredFunctions() { + if ( !in_array( wfGetDB( DB_MASTER )->getType(), array( 'mysql', 'postgres' ) ) ) { + $this->markTestSkipped( 'MySQL or Postgres required' ); + } + global $IP; + $this->dropFunctions(); + $this->functionTest = true; + $this->assertTrue( $this->db->sourceFile( "$IP/tests/phpunit/data/db/{$this->db->getType()}/functions.sql" ) ); + $res = $this->db->query( 'SELECT mw_test_function() AS test', __METHOD__ ); + $this->assertEquals( 42, $res->fetchObject()->test ); + } + + private function dropFunctions() { + $this->db->query( 'DROP FUNCTION IF EXISTS mw_test_function' + . ( $this->db->getType() == 'postgres' ? '()' : '' ) + ); + } + + public function testUnknownTableCorruptsResults() { + $res = $this->db->select( 'page', '*', array( 'page_id' => 1 ) ); + $this->assertFalse( $this->db->tableExists( 'foobarbaz' ) ); + $this->assertInternalType( 'int', $res->numRows() ); + } +} diff --git a/tests/phpunit/includes/db/DatabaseTestHelper.php b/tests/phpunit/includes/db/DatabaseTestHelper.php new file mode 100644 index 00000000..790f273c --- /dev/null +++ b/tests/phpunit/includes/db/DatabaseTestHelper.php @@ -0,0 +1,166 @@ +testName = $testName; + } + + /** + * Returns SQL queries grouped by '; ' + * Clear the list of queries that have been done so far. + */ + public function getLastSqls() { + $lastSqls = implode( '; ', $this->lastSqls ); + $this->lastSqls = array(); + + return $lastSqls; + } + + public function setExistingTables( $tablesExists ) { + $this->tablesExists = (array)$tablesExists; + } + + protected function addSql( $sql ) { + // clean up spaces before and after some words and the whole string + $this->lastSqls[] = trim( preg_replace( + '/\s{2,}(?=FROM|WHERE|GROUP BY|ORDER BY|LIMIT)|(?<=SELECT|INSERT|UPDATE)\s{2,}/', + ' ', $sql + ) ); + } + + protected function checkFunctionName( $fname ) { + if ( substr( $fname, 0, strlen( $this->testName ) ) !== $this->testName ) { + throw new MWException( 'function name does not start with test class. ' . + $fname . ' vs. ' . $this->testName . '. ' . + 'Please provide __METHOD__ to database methods.' ); + } + } + + function strencode( $s ) { + // Choose apos to avoid handling of escaping double quotes in quoted text + return str_replace( "'", "\'", $s ); + } + + public function addIdentifierQuotes( $s ) { + // no escaping to avoid handling of double quotes in quoted text + return $s; + } + + public function query( $sql, $fname = '', $tempIgnore = false ) { + $this->checkFunctionName( $fname ); + $this->addSql( $sql ); + + return parent::query( $sql, $fname, $tempIgnore ); + } + + public function tableExists( $table, $fname = __METHOD__ ) { + $this->checkFunctionName( $fname ); + + return in_array( $table, (array)$this->tablesExists ); + } + + // Redeclare parent method to make it public + public function nativeReplace( $table, $rows, $fname ) { + return parent::nativeReplace( $table, $rows, $fname ); + } + + function getType() { + return 'test'; + } + + function open( $server, $user, $password, $dbName ) { + return false; + } + + function fetchObject( $res ) { + return false; + } + + function fetchRow( $res ) { + return false; + } + + function numRows( $res ) { + return -1; + } + + function numFields( $res ) { + return -1; + } + + function fieldName( $res, $n ) { + return 'test'; + } + + function insertId() { + return -1; + } + + function dataSeek( $res, $row ) { + /* nop */ + } + + function lastErrno() { + return -1; + } + + function lastError() { + return 'test'; + } + + function fieldInfo( $table, $field ) { + return false; + } + + function indexInfo( $table, $index, $fname = 'Database::indexInfo' ) { + return false; + } + + function affectedRows() { + return -1; + } + + function getSoftwareLink() { + return 'test'; + } + + function getServerVersion() { + return 'test'; + } + + function getServerInfo() { + return 'test'; + } + + protected function closeConnection() { + return false; + } + + protected function doQuery( $sql ) { + return array(); + } +} diff --git a/tests/phpunit/includes/db/ORMRowTest.php b/tests/phpunit/includes/db/ORMRowTest.php new file mode 100644 index 00000000..27d4d0e8 --- /dev/null +++ b/tests/phpunit/includes/db/ORMRowTest.php @@ -0,0 +1,226 @@ + + */ +abstract class ORMRowTest extends \MediaWikiTestCase { + + /** + * @since 1.20 + * @return string + */ + abstract protected function getRowClass(); + + /** + * @since 1.20 + * @return IORMTable + */ + abstract protected function getTableInstance(); + + /** + * @since 1.20 + * @return array + */ + abstract public function constructorTestProvider(); + + /** + * @since 1.20 + * @param IORMRow $row + * @param array $data + */ + protected function verifyFields( IORMRow $row, array $data ) { + foreach ( array_keys( $data ) as $fieldName ) { + $this->assertEquals( $data[$fieldName], $row->getField( $fieldName ) ); + } + } + + /** + * @since 1.20 + * @param array $data + * @param boolean $loadDefaults + * @return IORMRow + */ + protected function getRowInstance( array $data, $loadDefaults ) { + $class = $this->getRowClass(); + + return new $class( $this->getTableInstance(), $data, $loadDefaults ); + } + + /** + * @since 1.20 + * @return array + */ + protected function getMockValues() { + return array( + 'id' => 1, + 'str' => 'foobar4645645', + 'int' => 42, + 'float' => 4.2, + 'bool' => true, + 'array' => array( 42, 'foobar' ), + 'blob' => new stdClass() + ); + } + + /** + * @since 1.20 + * @return array + */ + protected function getMockFields() { + $mockValues = $this->getMockValues(); + $mockFields = array(); + + foreach ( $this->getTableInstance()->getFields() as $name => $type ) { + if ( $name !== 'id' ) { + $mockFields[$name] = $mockValues[$type]; + } + } + + return $mockFields; + } + + /** + * @since 1.20 + * @return array of IORMRow + */ + public function instanceProvider() { + $instances = array(); + + foreach ( $this->constructorTestProvider() as $arguments ) { + $instances[] = array( call_user_func_array( array( $this, 'getRowInstance' ), $arguments ) ); + } + + return $instances; + } + + /** + * @dataProvider constructorTestProvider + */ + public function testConstructor( array $data, $loadDefaults ) { + $this->verifyFields( $this->getRowInstance( $data, $loadDefaults ), $data ); + } + + /** + * @dataProvider constructorTestProvider + */ + public function testSaveAndRemove( array $data, $loadDefaults ) { + $item = $this->getRowInstance( $data, $loadDefaults ); + + $this->assertTrue( $item->save() ); + + $this->assertTrue( $item->hasIdField() ); + $this->assertTrue( is_integer( $item->getId() ) ); + + $id = $item->getId(); + + $this->assertTrue( $item->save() ); + + $this->assertEquals( $id, $item->getId() ); + + $this->verifyFields( $item, $data ); + + $this->assertTrue( $item->remove() ); + + $this->assertFalse( $item->hasIdField() ); + + $this->assertTrue( $item->save() ); + + $this->verifyFields( $item, $data ); + + $this->assertTrue( $item->remove() ); + + $this->assertFalse( $item->hasIdField() ); + + $this->verifyFields( $item, $data ); + } + + /** + * @dataProvider instanceProvider + */ + public function testSetField( IORMRow $item ) { + foreach ( $this->getMockFields() as $name => $value ) { + $item->setField( $name, $value ); + $this->assertEquals( $value, $item->getField( $name ) ); + } + } + + /** + * @since 1.20 + * @param array $expected + * @param IORMRow $item + */ + protected function assertFieldValues( array $expected, IORMRow $item ) { + foreach ( $expected as $name => $type ) { + if ( $name !== 'id' ) { + $this->assertEquals( $expected[$name], $item->getField( $name ) ); + } + } + } + + /** + * @dataProvider instanceProvider + */ + public function testSetFields( IORMRow $item ) { + $originalValues = $item->getFields(); + + $item->setFields( array(), false ); + + foreach ( $item->getTable()->getFields() as $name => $type ) { + $originalHas = array_key_exists( $name, $originalValues ); + $newHas = $item->hasField( $name ); + + $this->assertEquals( $originalHas, $newHas ); + + if ( $originalHas && $newHas ) { + $this->assertEquals( $originalValues[$name], $item->getField( $name ) ); + } + } + + $mockFields = $this->getMockFields(); + + $item->setFields( $mockFields, false ); + + $this->assertFieldValues( $originalValues, $item ); + + $item->setFields( $mockFields, true ); + + $this->assertFieldValues( $mockFields, $item ); + } + + // TODO: test all of the methods! + +} diff --git a/tests/phpunit/includes/db/ORMTableTest.php b/tests/phpunit/includes/db/ORMTableTest.php new file mode 100644 index 00000000..e583d1bc --- /dev/null +++ b/tests/phpunit/includes/db/ORMTableTest.php @@ -0,0 +1,146 @@ + + * @author Daniel Kinzler + */ +class ORMTableTest extends MediaWikiTestCase { + + /** + * @since 1.21 + * @return string + */ + protected function getTableClass() { + return 'PageORMTableForTesting'; + } + + /** + * @since 1.21 + * @return IORMTable + */ + public function getTable() { + $class = $this->getTableClass(); + + return $class::singleton(); + } + + /** + * @since 1.21 + * @return string + */ + public function getRowClass() { + return $this->getTable()->getRowClass(); + } + + /** + * @since 1.21 + */ + public function testSingleton() { + $class = $this->getTableClass(); + + $this->assertInstanceOf( $class, $class::singleton() ); + $this->assertTrue( $class::singleton() === $class::singleton() ); + } + + /** + * @since 1.21 + */ + public function testIgnoreErrorsOverride() { + $table = $this->getTable(); + + $db = $table->getReadDbConnection(); + $db->ignoreErrors( true ); + + try { + $table->rawSelect( "this is invalid" ); + $this->fail( "An invalid query should trigger a DBQueryError even if ignoreErrors is enabled." ); + } catch ( DBQueryError $ex ) { + $this->assertTrue( true, "just making phpunit happy" ); + } + + $db->ignoreErrors( false ); + } +} + +/** + * Dummy ORM table for testing, reading Title objects from the page table. + * + * @since 1.21 + */ + +class PageORMTableForTesting extends ORMTable { + + /** + * @see ORMTable::getName + * + * @return string + */ + public function getName() { + return 'page'; + } + + /** + * @see ORMTable::getRowClass + * + * @return string + */ + public function getRowClass() { + return 'Title'; + } + + /** + * @see ORMTable::newRow + * + * @return IORMRow + */ + public function newRow( array $data, $loadDefaults = false ) { + return Title::makeTitle( $data['namespace'], $data['title'] ); + } + + /** + * @see ORMTable::getFields + * + * @return array + */ + public function getFields() { + return array( + 'id' => 'int', + 'namespace' => 'int', + 'title' => 'str', + ); + } + + /** + * @see ORMTable::getFieldPrefix + * + * @return string + */ + protected function getFieldPrefix() { + return 'page_'; + } +} diff --git a/tests/phpunit/includes/db/TestORMRowTest.php b/tests/phpunit/includes/db/TestORMRowTest.php new file mode 100644 index 00000000..f65642b8 --- /dev/null +++ b/tests/phpunit/includes/db/TestORMRowTest.php @@ -0,0 +1,215 @@ + + */ +require_once __DIR__ . "/ORMRowTest.php"; + +class TestORMRowTest extends ORMRowTest { + + /** + * @since 1.20 + * @return string + */ + protected function getRowClass() { + return 'TestORMRow'; + } + + /** + * @since 1.20 + * @return IORMTable + */ + protected function getTableInstance() { + return TestORMTable::singleton(); + } + + protected function setUp() { + parent::setUp(); + + $dbw = wfGetDB( DB_MASTER ); + + $isSqlite = $GLOBALS['wgDBtype'] === 'sqlite'; + $isPostgres = $GLOBALS['wgDBtype'] === 'postgres'; + + $idField = $isSqlite ? 'INTEGER' : 'INT unsigned'; + $primaryKey = $isSqlite ? 'PRIMARY KEY AUTOINCREMENT' : 'auto_increment PRIMARY KEY'; + + if ( $isPostgres ) { + $dbw->query( + 'CREATE TABLE IF NOT EXISTS ' . $dbw->tableName( 'orm_test' ) . "( + test_id serial PRIMARY KEY, + test_name TEXT NOT NULL DEFAULT '', + test_age INTEGER NOT NULL DEFAULT 0, + test_height REAL NOT NULL DEFAULT 0, + test_awesome INTEGER NOT NULL DEFAULT 0, + test_stuff BYTEA, + test_moarstuff BYTEA, + test_time TIMESTAMPTZ + );", + __METHOD__ + ); + } else { + $dbw->query( + 'CREATE TABLE IF NOT EXISTS ' . $dbw->tableName( 'orm_test' ) . '( + test_id ' . $idField . ' NOT NULL ' . $primaryKey . ', + test_name VARCHAR(255) NOT NULL, + test_age TINYINT unsigned NOT NULL, + test_height FLOAT NOT NULL, + test_awesome TINYINT unsigned NOT NULL, + test_stuff BLOB NOT NULL, + test_moarstuff BLOB NOT NULL, + test_time varbinary(14) NOT NULL + );', + __METHOD__ + ); + } + } + + protected function tearDown() { + $dbw = wfGetDB( DB_MASTER ); + $dbw->dropTable( 'orm_test', __METHOD__ ); + + parent::tearDown(); + } + + public function constructorTestProvider() { + $dbw = wfGetDB( DB_MASTER ); + return array( + array( + array( + 'name' => 'Foobar', + 'time' => $dbw->timestamp( '20120101020202' ), + 'age' => 42, + 'height' => 9000.1, + 'awesome' => true, + 'stuff' => array( 13, 11, 7, 5, 3, 2 ), + 'moarstuff' => (object)array( 'foo' => 'bar', 'bar' => array( 4, 2 ), 'baz' => true ) + ), + true + ), + ); + } + + /** + * @since 1.21 + * @return array + */ + protected function getMockValues() { + return array( + 'id' => 1, + 'str' => 'foobar4645645', + 'int' => 42, + 'float' => 4.2, + 'bool' => '', + 'array' => array( 42, 'foobar' ), + 'blob' => new stdClass() + ); + } +} + +class TestORMRow extends ORMRow { +} + +class TestORMTable extends ORMTable { + + /** + * Returns the name of the database table objects of this type are stored in. + * + * @since 1.20 + * + * @return string + */ + public function getName() { + return 'orm_test'; + } + + /** + * Returns the name of a IORMRow implementing class that + * represents single rows in this table. + * + * @since 1.20 + * + * @return string + */ + public function getRowClass() { + return 'TestORMRow'; + } + + /** + * Returns an array with the fields and their types this object contains. + * This corresponds directly to the fields in the database, without prefix. + * + * field name => type + * + * Allowed types: + * * id + * * str + * * int + * * float + * * bool + * * array + * * blob + * + * @since 1.20 + * + * @return array + */ + public function getFields() { + return array( + 'id' => 'id', + 'name' => 'str', + 'age' => 'int', + 'height' => 'float', + 'awesome' => 'bool', + 'stuff' => 'array', + 'moarstuff' => 'blob', + 'time' => 'str', // TS_MW + ); + } + + /** + * Gets the db field prefix. + * + * @since 1.20 + * + * @return string + */ + protected function getFieldPrefix() { + return 'test_'; + } +} diff --git a/tests/phpunit/includes/debug/MWDebugTest.php b/tests/phpunit/includes/debug/MWDebugTest.php new file mode 100644 index 00000000..6926b1c8 --- /dev/null +++ b/tests/phpunit/includes/debug/MWDebugTest.php @@ -0,0 +1,72 @@ +assertEquals( + array( array( + 'msg' => 'logging a string', + 'type' => 'log', + 'caller' => __METHOD__, + ) ), + MWDebug::getLog() + ); + } + + public function testAddWarning() { + MWDebug::warning( 'Warning message' ); + $this->assertEquals( + array( array( + 'msg' => 'Warning message', + 'type' => 'warn', + 'caller' => 'MWDebugTest::testAddWarning', + ) ), + MWDebug::getLog() + ); + } + + public function testAvoidDuplicateDeprecations() { + MWDebug::deprecated( 'wfOldFunction', '1.0', 'component' ); + MWDebug::deprecated( 'wfOldFunction', '1.0', 'component' ); + + // assertCount() not available on WMF integration server + $this->assertEquals( 1, + count( MWDebug::getLog() ), + "Only one deprecated warning per function should be kept" + ); + } + + public function testAvoidNonConsecutivesDuplicateDeprecations() { + MWDebug::deprecated( 'wfOldFunction', '1.0', 'component' ); + MWDebug::warning( 'some warning' ); + MWDebug::log( 'we could have logged something too' ); + // Another deprecation + MWDebug::deprecated( 'wfOldFunction', '1.0', 'component' ); + + // assertCount() not available on WMF integration server + $this->assertEquals( 3, + count( MWDebug::getLog() ), + "Only one deprecated warning per function should be kept" + ); + } +} diff --git a/tests/phpunit/includes/filebackend/FileBackendTest.php b/tests/phpunit/includes/filebackend/FileBackendTest.php new file mode 100644 index 00000000..fcfa724f --- /dev/null +++ b/tests/phpunit/includes/filebackend/FileBackendTest.php @@ -0,0 +1,2306 @@ +getCliArg( 'use-filebackend=' ) ) { + if ( self::$backendToUse ) { + $this->singleBackend = self::$backendToUse; + } else { + $name = $this->getCliArg( 'use-filebackend=' ); + $useConfig = array(); + foreach ( $wgFileBackends as $conf ) { + if ( $conf['name'] == $name ) { + $useConfig = $conf; + break; + } + } + $useConfig['name'] = 'localtesting'; // swap name + $useConfig['shardViaHashLevels'] = array( // test sharding + 'unittest-cont1' => array( 'levels' => 1, 'base' => 16, 'repeat' => 1 ) + ); + $class = $useConfig['class']; + self::$backendToUse = new $class( $useConfig ); + $this->singleBackend = self::$backendToUse; + } + } else { + $this->singleBackend = new FSFileBackend( array( + 'name' => 'localtesting', + 'lockManager' => 'fsLockManager', + #'parallelize' => 'implicit', + 'wikiId' => wfWikiID() . $uniqueId, + 'containerPaths' => array( + 'unittest-cont1' => "{$tmpPrefix}-localtesting-cont1", + 'unittest-cont2' => "{$tmpPrefix}-localtesting-cont2" ) + ) ); + } + $this->multiBackend = new FileBackendMultiWrite( array( + 'name' => 'localtesting', + 'lockManager' => 'fsLockManager', + 'parallelize' => 'implicit', + 'wikiId' => wfWikiId() . $uniqueId, + 'backends' => array( + array( + 'name' => 'localmultitesting1', + 'class' => 'FSFileBackend', + 'lockManager' => 'nullLockManager', + 'containerPaths' => array( + 'unittest-cont1' => "{$tmpPrefix}-localtestingmulti1-cont1", + 'unittest-cont2' => "{$tmpPrefix}-localtestingmulti1-cont2" ), + 'isMultiMaster' => false + ), + array( + 'name' => 'localmultitesting2', + 'class' => 'FSFileBackend', + 'lockManager' => 'nullLockManager', + 'containerPaths' => array( + 'unittest-cont1' => "{$tmpPrefix}-localtestingmulti2-cont1", + 'unittest-cont2' => "{$tmpPrefix}-localtestingmulti2-cont2" ), + 'isMultiMaster' => true + ) + ) + ) ); + $this->filesToPrune = array(); + } + + private static function baseStorePath() { + return 'mwstore://localtesting'; + } + + private function backendClass() { + return get_class( $this->backend ); + } + + /** + * @dataProvider provider_testIsStoragePath + * @covers FileBackend::isStoragePath + */ + public function testIsStoragePath( $path, $isStorePath ) { + $this->assertEquals( $isStorePath, FileBackend::isStoragePath( $path ), + "FileBackend::isStoragePath on path '$path'" ); + } + + public static function provider_testIsStoragePath() { + return array( + array( 'mwstore://', true ), + array( 'mwstore://backend', true ), + array( 'mwstore://backend/container', true ), + array( 'mwstore://backend/container/', true ), + array( 'mwstore://backend/container/path', true ), + array( 'mwstore://backend//container/', true ), + array( 'mwstore://backend//container//', true ), + array( 'mwstore://backend//container//path', true ), + array( 'mwstore:///', true ), + array( 'mwstore:/', false ), + array( 'mwstore:', false ), + ); + } + + /** + * @dataProvider provider_testSplitStoragePath + * @covers FileBackend::splitStoragePath + */ + public function testSplitStoragePath( $path, $res ) { + $this->assertEquals( $res, FileBackend::splitStoragePath( $path ), + "FileBackend::splitStoragePath on path '$path'" ); + } + + public static function provider_testSplitStoragePath() { + return array( + array( 'mwstore://backend/container', array( 'backend', 'container', '' ) ), + array( 'mwstore://backend/container/', array( 'backend', 'container', '' ) ), + array( 'mwstore://backend/container/path', array( 'backend', 'container', 'path' ) ), + array( 'mwstore://backend/container//path', array( 'backend', 'container', '/path' ) ), + array( 'mwstore://backend//container/path', array( null, null, null ) ), + array( 'mwstore://backend//container//path', array( null, null, null ) ), + array( 'mwstore://', array( null, null, null ) ), + array( 'mwstore://backend', array( null, null, null ) ), + array( 'mwstore:///', array( null, null, null ) ), + array( 'mwstore:/', array( null, null, null ) ), + array( 'mwstore:', array( null, null, null ) ) + ); + } + + /** + * @dataProvider provider_normalizeStoragePath + * @covers FileBackend::normalizeStoragePath + */ + public function testNormalizeStoragePath( $path, $res ) { + $this->assertEquals( $res, FileBackend::normalizeStoragePath( $path ), + "FileBackend::normalizeStoragePath on path '$path'" ); + } + + public static function provider_normalizeStoragePath() { + return array( + array( 'mwstore://backend/container', 'mwstore://backend/container' ), + array( 'mwstore://backend/container/', 'mwstore://backend/container' ), + array( 'mwstore://backend/container/path', 'mwstore://backend/container/path' ), + array( 'mwstore://backend/container//path', 'mwstore://backend/container/path' ), + array( 'mwstore://backend/container///path', 'mwstore://backend/container/path' ), + array( 'mwstore://backend/container///path//to///obj', 'mwstore://backend/container/path/to/obj' ), + array( 'mwstore://', null ), + array( 'mwstore://backend', null ), + array( 'mwstore://backend//container/path', null ), + array( 'mwstore://backend//container//path', null ), + array( 'mwstore:///', null ), + array( 'mwstore:/', null ), + array( 'mwstore:', null ), + ); + } + + /** + * @dataProvider provider_testParentStoragePath + * @covers FileBackend::parentStoragePath + */ + public function testParentStoragePath( $path, $res ) { + $this->assertEquals( $res, FileBackend::parentStoragePath( $path ), + "FileBackend::parentStoragePath on path '$path'" ); + } + + public static function provider_testParentStoragePath() { + return array( + array( 'mwstore://backend/container/path/to/obj', 'mwstore://backend/container/path/to' ), + array( 'mwstore://backend/container/path/to', 'mwstore://backend/container/path' ), + array( 'mwstore://backend/container/path', 'mwstore://backend/container' ), + array( 'mwstore://backend/container', null ), + array( 'mwstore://backend/container/path/to/obj/', 'mwstore://backend/container/path/to' ), + array( 'mwstore://backend/container/path/to/', 'mwstore://backend/container/path' ), + array( 'mwstore://backend/container/path/', 'mwstore://backend/container' ), + array( 'mwstore://backend/container/', null ), + ); + } + + /** + * @dataProvider provider_testExtensionFromPath + * @covers FileBackend::extensionFromPath + */ + public function testExtensionFromPath( $path, $res ) { + $this->assertEquals( $res, FileBackend::extensionFromPath( $path ), + "FileBackend::extensionFromPath on path '$path'" ); + } + + public static function provider_testExtensionFromPath() { + return array( + array( 'mwstore://backend/container/path.txt', 'txt' ), + array( 'mwstore://backend/container/path.svg.png', 'png' ), + array( 'mwstore://backend/container/path', '' ), + array( 'mwstore://backend/container/path.', '' ), + ); + } + + /** + * @dataProvider provider_testStore + */ + public function testStore( $op ) { + $this->filesToPrune[] = $op['src']; + + $this->backend = $this->singleBackend; + $this->tearDownFiles(); + $this->doTestStore( $op ); + $this->tearDownFiles(); + + $this->backend = $this->multiBackend; + $this->tearDownFiles(); + $this->doTestStore( $op ); + $this->filesToPrune[] = $op['src']; # avoid file leaking + $this->tearDownFiles(); + } + + /** + * @covers FileBackend::doOperation + */ + private function doTestStore( $op ) { + $backendName = $this->backendClass(); + + $source = $op['src']; + $dest = $op['dst']; + $this->prepare( array( 'dir' => dirname( $dest ) ) ); + + file_put_contents( $source, "Unit test file" ); + + if ( isset( $op['overwrite'] ) || isset( $op['overwriteSame'] ) ) { + $this->backend->store( $op ); + } + + $status = $this->backend->doOperation( $op ); + + $this->assertGoodStatus( $status, + "Store from $source to $dest succeeded without warnings ($backendName)." ); + $this->assertEquals( true, $status->isOK(), + "Store from $source to $dest succeeded ($backendName)." ); + $this->assertEquals( array( 0 => true ), $status->success, + "Store from $source to $dest has proper 'success' field in Status ($backendName)." ); + $this->assertEquals( true, file_exists( $source ), + "Source file $source still exists ($backendName)." ); + $this->assertEquals( true, $this->backend->fileExists( array( 'src' => $dest ) ), + "Destination file $dest exists ($backendName)." ); + + $this->assertEquals( filesize( $source ), + $this->backend->getFileSize( array( 'src' => $dest ) ), + "Destination file $dest has correct size ($backendName)." ); + + $props1 = FSFile::getPropsFromPath( $source ); + $props2 = $this->backend->getFileProps( array( 'src' => $dest ) ); + $this->assertEquals( $props1, $props2, + "Source and destination have the same props ($backendName)." ); + + $this->assertBackendPathsConsistent( array( $dest ) ); + } + + public static function provider_testStore() { + $cases = array(); + + $tmpName = TempFSFile::factory( "unittests_", 'txt' )->getPath(); + $toPath = self::baseStorePath() . '/unittest-cont1/e/fun/obj1.txt'; + $op = array( 'op' => 'store', 'src' => $tmpName, 'dst' => $toPath ); + $cases[] = array( + $op, // operation + $tmpName, // source + $toPath, // dest + ); + + $op2 = $op; + $op2['overwrite'] = true; + $cases[] = array( + $op2, // operation + $tmpName, // source + $toPath, // dest + ); + + $op2 = $op; + $op2['overwriteSame'] = true; + $cases[] = array( + $op2, // operation + $tmpName, // source + $toPath, // dest + ); + + return $cases; + } + + /** + * @dataProvider provider_testCopy + * @covers FileBackend::doOperation + */ + public function testCopy( $op ) { + $this->backend = $this->singleBackend; + $this->tearDownFiles(); + $this->doTestCopy( $op ); + $this->tearDownFiles(); + + $this->backend = $this->multiBackend; + $this->tearDownFiles(); + $this->doTestCopy( $op ); + $this->tearDownFiles(); + } + + private function doTestCopy( $op ) { + $backendName = $this->backendClass(); + + $source = $op['src']; + $dest = $op['dst']; + $this->prepare( array( 'dir' => dirname( $source ) ) ); + $this->prepare( array( 'dir' => dirname( $dest ) ) ); + + if ( isset( $op['ignoreMissingSource'] ) ) { + $status = $this->backend->doOperation( $op ); + $this->assertGoodStatus( $status, + "Move from $source to $dest succeeded without warnings ($backendName)." ); + $this->assertEquals( array( 0 => true ), $status->success, + "Move from $source to $dest has proper 'success' field in Status ($backendName)." ); + $this->assertEquals( false, $this->backend->fileExists( array( 'src' => $source ) ), + "Source file $source does not exist ($backendName)." ); + $this->assertEquals( false, $this->backend->fileExists( array( 'src' => $dest ) ), + "Destination file $dest does not exist ($backendName)." ); + + return; // done + } + + $status = $this->backend->doOperation( + array( 'op' => 'create', 'content' => 'blahblah', 'dst' => $source ) ); + $this->assertGoodStatus( $status, + "Creation of file at $source succeeded ($backendName)." ); + + if ( isset( $op['overwrite'] ) || isset( $op['overwriteSame'] ) ) { + $this->backend->copy( $op ); + } + + $status = $this->backend->doOperation( $op ); + + $this->assertGoodStatus( $status, + "Copy from $source to $dest succeeded without warnings ($backendName)." ); + $this->assertEquals( true, $status->isOK(), + "Copy from $source to $dest succeeded ($backendName)." ); + $this->assertEquals( array( 0 => true ), $status->success, + "Copy from $source to $dest has proper 'success' field in Status ($backendName)." ); + $this->assertEquals( true, $this->backend->fileExists( array( 'src' => $source ) ), + "Source file $source still exists ($backendName)." ); + $this->assertEquals( true, $this->backend->fileExists( array( 'src' => $dest ) ), + "Destination file $dest exists after copy ($backendName)." ); + + $this->assertEquals( + $this->backend->getFileSize( array( 'src' => $source ) ), + $this->backend->getFileSize( array( 'src' => $dest ) ), + "Destination file $dest has correct size ($backendName)." ); + + $props1 = $this->backend->getFileProps( array( 'src' => $source ) ); + $props2 = $this->backend->getFileProps( array( 'src' => $dest ) ); + $this->assertEquals( $props1, $props2, + "Source and destination have the same props ($backendName)." ); + + $this->assertBackendPathsConsistent( array( $source, $dest ) ); + } + + public static function provider_testCopy() { + $cases = array(); + + $source = self::baseStorePath() . '/unittest-cont1/e/file.txt'; + $dest = self::baseStorePath() . '/unittest-cont2/a/fileMoved.txt'; + + $op = array( 'op' => 'copy', 'src' => $source, 'dst' => $dest ); + $cases[] = array( + $op, // operation + $source, // source + $dest, // dest + ); + + $op2 = $op; + $op2['overwrite'] = true; + $cases[] = array( + $op2, // operation + $source, // source + $dest, // dest + ); + + $op2 = $op; + $op2['overwriteSame'] = true; + $cases[] = array( + $op2, // operation + $source, // source + $dest, // dest + ); + + $op2 = $op; + $op2['ignoreMissingSource'] = true; + $cases[] = array( + $op2, // operation + $source, // source + $dest, // dest + ); + + $op2 = $op; + $op2['ignoreMissingSource'] = true; + $cases[] = array( + $op2, // operation + self::baseStorePath() . '/unittest-cont-bad/e/file.txt', // source + $dest, // dest + ); + + return $cases; + } + + /** + * @dataProvider provider_testMove + * @covers FileBackend::doOperation + */ + public function testMove( $op ) { + $this->backend = $this->singleBackend; + $this->tearDownFiles(); + $this->doTestMove( $op ); + $this->tearDownFiles(); + + $this->backend = $this->multiBackend; + $this->tearDownFiles(); + $this->doTestMove( $op ); + $this->tearDownFiles(); + } + + private function doTestMove( $op ) { + $backendName = $this->backendClass(); + + $source = $op['src']; + $dest = $op['dst']; + $this->prepare( array( 'dir' => dirname( $source ) ) ); + $this->prepare( array( 'dir' => dirname( $dest ) ) ); + + if ( isset( $op['ignoreMissingSource'] ) ) { + $status = $this->backend->doOperation( $op ); + $this->assertGoodStatus( $status, + "Move from $source to $dest succeeded without warnings ($backendName)." ); + $this->assertEquals( array( 0 => true ), $status->success, + "Move from $source to $dest has proper 'success' field in Status ($backendName)." ); + $this->assertEquals( false, $this->backend->fileExists( array( 'src' => $source ) ), + "Source file $source does not exist ($backendName)." ); + $this->assertEquals( false, $this->backend->fileExists( array( 'src' => $dest ) ), + "Destination file $dest does not exist ($backendName)." ); + + return; // done + } + + $status = $this->backend->doOperation( + array( 'op' => 'create', 'content' => 'blahblah', 'dst' => $source ) ); + $this->assertGoodStatus( $status, + "Creation of file at $source succeeded ($backendName)." ); + + if ( isset( $op['overwrite'] ) || isset( $op['overwriteSame'] ) ) { + $this->backend->copy( $op ); + } + + $status = $this->backend->doOperation( $op ); + $this->assertGoodStatus( $status, + "Move from $source to $dest succeeded without warnings ($backendName)." ); + $this->assertEquals( true, $status->isOK(), + "Move from $source to $dest succeeded ($backendName)." ); + $this->assertEquals( array( 0 => true ), $status->success, + "Move from $source to $dest has proper 'success' field in Status ($backendName)." ); + $this->assertEquals( false, $this->backend->fileExists( array( 'src' => $source ) ), + "Source file $source does not still exists ($backendName)." ); + $this->assertEquals( true, $this->backend->fileExists( array( 'src' => $dest ) ), + "Destination file $dest exists after move ($backendName)." ); + + $this->assertNotEquals( + $this->backend->getFileSize( array( 'src' => $source ) ), + $this->backend->getFileSize( array( 'src' => $dest ) ), + "Destination file $dest has correct size ($backendName)." ); + + $props1 = $this->backend->getFileProps( array( 'src' => $source ) ); + $props2 = $this->backend->getFileProps( array( 'src' => $dest ) ); + $this->assertEquals( false, $props1['fileExists'], + "Source file does not exist accourding to props ($backendName)." ); + $this->assertEquals( true, $props2['fileExists'], + "Destination file exists accourding to props ($backendName)." ); + + $this->assertBackendPathsConsistent( array( $source, $dest ) ); + } + + public static function provider_testMove() { + $cases = array(); + + $source = self::baseStorePath() . '/unittest-cont1/e/file.txt'; + $dest = self::baseStorePath() . '/unittest-cont2/a/fileMoved.txt'; + + $op = array( 'op' => 'move', 'src' => $source, 'dst' => $dest ); + $cases[] = array( + $op, // operation + $source, // source + $dest, // dest + ); + + $op2 = $op; + $op2['overwrite'] = true; + $cases[] = array( + $op2, // operation + $source, // source + $dest, // dest + ); + + $op2 = $op; + $op2['overwriteSame'] = true; + $cases[] = array( + $op2, // operation + $source, // source + $dest, // dest + ); + + $op2 = $op; + $op2['ignoreMissingSource'] = true; + $cases[] = array( + $op2, // operation + $source, // source + $dest, // dest + ); + + $op2 = $op; + $op2['ignoreMissingSource'] = true; + $cases[] = array( + $op2, // operation + self::baseStorePath() . '/unittest-cont-bad/e/file.txt', // source + $dest, // dest + ); + + return $cases; + } + + /** + * @dataProvider provider_testDelete + * @covers FileBackend::doOperation + */ + public function testDelete( $op, $withSource, $okStatus ) { + $this->backend = $this->singleBackend; + $this->tearDownFiles(); + $this->doTestDelete( $op, $withSource, $okStatus ); + $this->tearDownFiles(); + + $this->backend = $this->multiBackend; + $this->tearDownFiles(); + $this->doTestDelete( $op, $withSource, $okStatus ); + $this->tearDownFiles(); + } + + private function doTestDelete( $op, $withSource, $okStatus ) { + $backendName = $this->backendClass(); + + $source = $op['src']; + $this->prepare( array( 'dir' => dirname( $source ) ) ); + + if ( $withSource ) { + $status = $this->backend->doOperation( + array( 'op' => 'create', 'content' => 'blahblah', 'dst' => $source ) ); + $this->assertGoodStatus( $status, + "Creation of file at $source succeeded ($backendName)." ); + } + + $status = $this->backend->doOperation( $op ); + if ( $okStatus ) { + $this->assertGoodStatus( $status, + "Deletion of file at $source succeeded without warnings ($backendName)." ); + $this->assertEquals( true, $status->isOK(), + "Deletion of file at $source succeeded ($backendName)." ); + $this->assertEquals( array( 0 => true ), $status->success, + "Deletion of file at $source has proper 'success' field in Status ($backendName)." ); + } else { + $this->assertEquals( false, $status->isOK(), + "Deletion of file at $source failed ($backendName)." ); + } + + $this->assertEquals( false, $this->backend->fileExists( array( 'src' => $source ) ), + "Source file $source does not exist after move ($backendName)." ); + + $this->assertFalse( + $this->backend->getFileSize( array( 'src' => $source ) ), + "Source file $source has correct size (false) ($backendName)." ); + + $props1 = $this->backend->getFileProps( array( 'src' => $source ) ); + $this->assertFalse( $props1['fileExists'], + "Source file $source does not exist according to props ($backendName)." ); + + $this->assertBackendPathsConsistent( array( $source ) ); + } + + public static function provider_testDelete() { + $cases = array(); + + $source = self::baseStorePath() . '/unittest-cont1/e/myfacefile.txt'; + + $op = array( 'op' => 'delete', 'src' => $source ); + $cases[] = array( + $op, // operation + true, // with source + true // succeeds + ); + + $cases[] = array( + $op, // operation + false, // without source + false // fails + ); + + $op['ignoreMissingSource'] = true; + $cases[] = array( + $op, // operation + false, // without source + true // succeeds + ); + + $op['ignoreMissingSource'] = true; + $op['src'] = self::baseStorePath() . '/unittest-cont-bad/e/file.txt'; + $cases[] = array( + $op, // operation + false, // without source + true // succeeds + ); + + return $cases; + } + + /** + * @dataProvider provider_testDescribe + * @covers FileBackend::doOperation + */ + public function testDescribe( $op, $withSource, $okStatus ) { + $this->backend = $this->singleBackend; + $this->tearDownFiles(); + $this->doTestDescribe( $op, $withSource, $okStatus ); + $this->tearDownFiles(); + + $this->backend = $this->multiBackend; + $this->tearDownFiles(); + $this->doTestDescribe( $op, $withSource, $okStatus ); + $this->tearDownFiles(); + } + + private function doTestDescribe( $op, $withSource, $okStatus ) { + $backendName = $this->backendClass(); + + $source = $op['src']; + $this->prepare( array( 'dir' => dirname( $source ) ) ); + + if ( $withSource ) { + $status = $this->backend->doOperation( + array( 'op' => 'create', 'content' => 'blahblah', 'dst' => $source ) ); + $this->assertGoodStatus( $status, + "Creation of file at $source succeeded ($backendName)." ); + } + + $status = $this->backend->doOperation( $op ); + if ( $okStatus ) { + $this->assertGoodStatus( $status, + "Describe of file at $source succeeded without warnings ($backendName)." ); + $this->assertEquals( true, $status->isOK(), + "Describe of file at $source succeeded ($backendName)." ); + $this->assertEquals( array( 0 => true ), $status->success, + "Describe of file at $source has proper 'success' field in Status ($backendName)." ); + } else { + $this->assertEquals( false, $status->isOK(), + "Describe of file at $source failed ($backendName)." ); + } + + $this->assertBackendPathsConsistent( array( $source ) ); + } + + public static function provider_testDescribe() { + $cases = array(); + + $source = self::baseStorePath() . '/unittest-cont1/e/myfacefile.txt'; + + $op = array( 'op' => 'describe', 'src' => $source, + 'headers' => array( 'X-Content-Length' => '91.3', 'Content-Old-Header' => '' ), + 'disposition' => 'inline' ); + $cases[] = array( + $op, // operation + true, // with source + true // succeeds + ); + + $cases[] = array( + $op, // operation + false, // without source + false // fails + ); + + return $cases; + } + + /** + * @dataProvider provider_testCreate + * @covers FileBackend::doOperation + */ + public function testCreate( $op, $alreadyExists, $okStatus, $newSize ) { + $this->backend = $this->singleBackend; + $this->tearDownFiles(); + $this->doTestCreate( $op, $alreadyExists, $okStatus, $newSize ); + $this->tearDownFiles(); + + $this->backend = $this->multiBackend; + $this->tearDownFiles(); + $this->doTestCreate( $op, $alreadyExists, $okStatus, $newSize ); + $this->tearDownFiles(); + } + + private function doTestCreate( $op, $alreadyExists, $okStatus, $newSize ) { + $backendName = $this->backendClass(); + + $dest = $op['dst']; + $this->prepare( array( 'dir' => dirname( $dest ) ) ); + + $oldText = 'blah...blah...waahwaah'; + if ( $alreadyExists ) { + $status = $this->backend->doOperation( + array( 'op' => 'create', 'content' => $oldText, 'dst' => $dest ) ); + $this->assertGoodStatus( $status, + "Creation of file at $dest succeeded ($backendName)." ); + } + + $status = $this->backend->doOperation( $op ); + if ( $okStatus ) { + $this->assertGoodStatus( $status, + "Creation of file at $dest succeeded without warnings ($backendName)." ); + $this->assertEquals( true, $status->isOK(), + "Creation of file at $dest succeeded ($backendName)." ); + $this->assertEquals( array( 0 => true ), $status->success, + "Creation of file at $dest has proper 'success' field in Status ($backendName)." ); + } else { + $this->assertEquals( false, $status->isOK(), + "Creation of file at $dest failed ($backendName)." ); + } + + $this->assertEquals( true, $this->backend->fileExists( array( 'src' => $dest ) ), + "Destination file $dest exists after creation ($backendName)." ); + + $props1 = $this->backend->getFileProps( array( 'src' => $dest ) ); + $this->assertEquals( true, $props1['fileExists'], + "Destination file $dest exists according to props ($backendName)." ); + if ( $okStatus ) { // file content is what we saved + $this->assertEquals( $newSize, $props1['size'], + "Destination file $dest has expected size according to props ($backendName)." ); + $this->assertEquals( $newSize, + $this->backend->getFileSize( array( 'src' => $dest ) ), + "Destination file $dest has correct size ($backendName)." ); + } else { // file content is some other previous text + $this->assertEquals( strlen( $oldText ), $props1['size'], + "Destination file $dest has original size according to props ($backendName)." ); + $this->assertEquals( strlen( $oldText ), + $this->backend->getFileSize( array( 'src' => $dest ) ), + "Destination file $dest has original size according to props ($backendName)." ); + } + + $this->assertBackendPathsConsistent( array( $dest ) ); + } + + /** + * @dataProvider provider_testCreate + */ + public static function provider_testCreate() { + $cases = array(); + + $dest = self::baseStorePath() . '/unittest-cont2/a/myspacefile.txt'; + + $op = array( 'op' => 'create', 'content' => 'test test testing', 'dst' => $dest ); + $cases[] = array( + $op, // operation + false, // no dest already exists + true, // succeeds + strlen( $op['content'] ) + ); + + $op2 = $op; + $op2['content'] = "\n"; + $cases[] = array( + $op2, // operation + false, // no dest already exists + true, // succeeds + strlen( $op2['content'] ) + ); + + $op2 = $op; + $op2['content'] = "fsf\n waf 3kt"; + $cases[] = array( + $op2, // operation + true, // dest already exists + false, // fails + strlen( $op2['content'] ) + ); + + $op2 = $op; + $op2['content'] = "egm'g gkpe gpqg eqwgwqg"; + $op2['overwrite'] = true; + $cases[] = array( + $op2, // operation + true, // dest already exists + true, // succeeds + strlen( $op2['content'] ) + ); + + $op2 = $op; + $op2['content'] = "39qjmg3-qg"; + $op2['overwriteSame'] = true; + $cases[] = array( + $op2, // operation + true, // dest already exists + false, // succeeds + strlen( $op2['content'] ) + ); + + return $cases; + } + + /** + * @covers FileBackend::doQuickOperations + */ + public function testDoQuickOperations() { + $this->backend = $this->singleBackend; + $this->doTestDoQuickOperations(); + $this->tearDownFiles(); + + $this->backend = $this->multiBackend; + $this->doTestDoQuickOperations(); + $this->tearDownFiles(); + } + + private function doTestDoQuickOperations() { + $backendName = $this->backendClass(); + + $base = self::baseStorePath(); + $files = array( + "$base/unittest-cont1/e/fileA.a", + "$base/unittest-cont1/e/fileB.a", + "$base/unittest-cont1/e/fileC.a" + ); + $createOps = array(); + $purgeOps = array(); + foreach ( $files as $path ) { + $status = $this->prepare( array( 'dir' => dirname( $path ) ) ); + $this->assertGoodStatus( $status, + "Preparing $path succeeded without warnings ($backendName)." ); + $createOps[] = array( 'op' => 'create', 'dst' => $path, 'content' => mt_rand( 0, 50000 ) ); + $copyOps[] = array( 'op' => 'copy', 'src' => $path, 'dst' => "$path-2" ); + $moveOps[] = array( 'op' => 'move', 'src' => "$path-2", 'dst' => "$path-3" ); + $purgeOps[] = array( 'op' => 'delete', 'src' => $path ); + $purgeOps[] = array( 'op' => 'delete', 'src' => "$path-3" ); + } + $purgeOps[] = array( 'op' => 'null' ); + + $this->assertGoodStatus( + $this->backend->doQuickOperations( $createOps ), + "Creation of source files succeeded ($backendName)." ); + foreach ( $files as $file ) { + $this->assertTrue( $this->backend->fileExists( array( 'src' => $file ) ), + "File $file exists." ); + } + + $this->assertGoodStatus( + $this->backend->doQuickOperations( $copyOps ), + "Quick copy of source files succeeded ($backendName)." ); + foreach ( $files as $file ) { + $this->assertTrue( $this->backend->fileExists( array( 'src' => "$file-2" ) ), + "File $file-2 exists." ); + } + + $this->assertGoodStatus( + $this->backend->doQuickOperations( $moveOps ), + "Quick move of source files succeeded ($backendName)." ); + foreach ( $files as $file ) { + $this->assertTrue( $this->backend->fileExists( array( 'src' => "$file-3" ) ), + "File $file-3 move in." ); + $this->assertFalse( $this->backend->fileExists( array( 'src' => "$file-2" ) ), + "File $file-2 moved away." ); + } + + $this->assertGoodStatus( + $this->backend->quickCopy( array( 'src' => $files[0], 'dst' => $files[0] ) ), + "Copy of file {$files[0]} over itself succeeded ($backendName)." ); + $this->assertTrue( $this->backend->fileExists( array( 'src' => $files[0] ) ), + "File {$files[0]} still exists." ); + + $this->assertGoodStatus( + $this->backend->quickMove( array( 'src' => $files[0], 'dst' => $files[0] ) ), + "Move of file {$files[0]} over itself succeeded ($backendName)." ); + $this->assertTrue( $this->backend->fileExists( array( 'src' => $files[0] ) ), + "File {$files[0]} still exists." ); + + $this->assertGoodStatus( + $this->backend->doQuickOperations( $purgeOps ), + "Quick deletion of source files succeeded ($backendName)." ); + foreach ( $files as $file ) { + $this->assertFalse( $this->backend->fileExists( array( 'src' => $file ) ), + "File $file purged." ); + $this->assertFalse( $this->backend->fileExists( array( 'src' => "$file-3" ) ), + "File $file-3 purged." ); + } + } + + /** + * @dataProvider provider_testConcatenate + */ + public function testConcatenate( $op, $srcs, $srcsContent, $alreadyExists, $okStatus ) { + $this->filesToPrune[] = $op['dst']; + + $this->backend = $this->singleBackend; + $this->tearDownFiles(); + $this->doTestConcatenate( $op, $srcs, $srcsContent, $alreadyExists, $okStatus ); + $this->filesToPrune[] = $op['dst']; # avoid file leaking + $this->tearDownFiles(); + + $this->backend = $this->multiBackend; + $this->tearDownFiles(); + $this->doTestConcatenate( $op, $srcs, $srcsContent, $alreadyExists, $okStatus ); + $this->filesToPrune[] = $op['dst']; # avoid file leaking + $this->tearDownFiles(); + } + + private function doTestConcatenate( $params, $srcs, $srcsContent, $alreadyExists, $okStatus ) { + $backendName = $this->backendClass(); + + $expContent = ''; + // Create sources + $ops = array(); + foreach ( $srcs as $i => $source ) { + $this->prepare( array( 'dir' => dirname( $source ) ) ); + $ops[] = array( + 'op' => 'create', // operation + 'dst' => $source, // source + 'content' => $srcsContent[$i] + ); + $expContent .= $srcsContent[$i]; + } + $status = $this->backend->doOperations( $ops ); + + $this->assertGoodStatus( $status, + "Creation of source files succeeded ($backendName)." ); + + $dest = $params['dst']; + if ( $alreadyExists ) { + $ok = file_put_contents( $dest, 'blah...blah...waahwaah' ) !== false; + $this->assertEquals( true, $ok, + "Creation of file at $dest succeeded ($backendName)." ); + } else { + $ok = file_put_contents( $dest, '' ) !== false; + $this->assertEquals( true, $ok, + "Creation of 0-byte file at $dest succeeded ($backendName)." ); + } + + // Combine the files into one + $status = $this->backend->concatenate( $params ); + if ( $okStatus ) { + $this->assertGoodStatus( $status, + "Creation of concat file at $dest succeeded without warnings ($backendName)." ); + $this->assertEquals( true, $status->isOK(), + "Creation of concat file at $dest succeeded ($backendName)." ); + } else { + $this->assertEquals( false, $status->isOK(), + "Creation of concat file at $dest failed ($backendName)." ); + } + + if ( $okStatus ) { + $this->assertEquals( true, is_file( $dest ), + "Dest concat file $dest exists after creation ($backendName)." ); + } else { + $this->assertEquals( true, is_file( $dest ), + "Dest concat file $dest exists after failed creation ($backendName)." ); + } + + $contents = file_get_contents( $dest ); + $this->assertNotEquals( false, $contents, "File at $dest exists ($backendName)." ); + + if ( $okStatus ) { + $this->assertEquals( $expContent, $contents, + "Concat file at $dest has correct contents ($backendName)." ); + } else { + $this->assertNotEquals( $expContent, $contents, + "Concat file at $dest has correct contents ($backendName)." ); + } + } + + public static function provider_testConcatenate() { + $cases = array(); + + $rand = mt_rand( 0, 2000000000 ) . time(); + $dest = wfTempDir() . "/randomfile!$rand.txt"; + $srcs = array( + self::baseStorePath() . '/unittest-cont1/e/file1.txt', + self::baseStorePath() . '/unittest-cont1/e/file2.txt', + self::baseStorePath() . '/unittest-cont1/e/file3.txt', + self::baseStorePath() . '/unittest-cont1/e/file4.txt', + self::baseStorePath() . '/unittest-cont1/e/file5.txt', + self::baseStorePath() . '/unittest-cont1/e/file6.txt', + self::baseStorePath() . '/unittest-cont1/e/file7.txt', + self::baseStorePath() . '/unittest-cont1/e/file8.txt', + self::baseStorePath() . '/unittest-cont1/e/file9.txt', + self::baseStorePath() . '/unittest-cont1/e/file10.txt' + ); + $content = array( + 'egfage', + 'ageageag', + 'rhokohlr', + 'shgmslkg', + 'kenga', + 'owagmal', + 'kgmae', + 'g eak;g', + 'lkaem;a', + 'legma' + ); + $params = array( 'srcs' => $srcs, 'dst' => $dest ); + + $cases[] = array( + $params, // operation + $srcs, // sources + $content, // content for each source + false, // no dest already exists + true, // succeeds + ); + + $cases[] = array( + $params, // operation + $srcs, // sources + $content, // content for each source + true, // dest already exists + false, // succeeds + ); + + return $cases; + } + + /** + * @dataProvider provider_testGetFileStat + * @covers FileBackend::getFileStat + */ + public function testGetFileStat( $path, $content, $alreadyExists ) { + $this->backend = $this->singleBackend; + $this->tearDownFiles(); + $this->doTestGetFileStat( $path, $content, $alreadyExists ); + $this->tearDownFiles(); + + $this->backend = $this->multiBackend; + $this->tearDownFiles(); + $this->doTestGetFileStat( $path, $content, $alreadyExists ); + $this->tearDownFiles(); + } + + private function doTestGetFileStat( $path, $content, $alreadyExists ) { + $backendName = $this->backendClass(); + + if ( $alreadyExists ) { + $this->prepare( array( 'dir' => dirname( $path ) ) ); + $status = $this->create( array( 'dst' => $path, 'content' => $content ) ); + $this->assertGoodStatus( $status, + "Creation of file at $path succeeded ($backendName)." ); + + $size = $this->backend->getFileSize( array( 'src' => $path ) ); + $time = $this->backend->getFileTimestamp( array( 'src' => $path ) ); + $stat = $this->backend->getFileStat( array( 'src' => $path ) ); + + $this->assertEquals( strlen( $content ), $size, + "Correct file size of '$path'" ); + $this->assertTrue( abs( time() - wfTimestamp( TS_UNIX, $time ) ) < 10, + "Correct file timestamp of '$path'" ); + + $size = $stat['size']; + $time = $stat['mtime']; + $this->assertEquals( strlen( $content ), $size, + "Correct file size of '$path'" ); + $this->assertTrue( abs( time() - wfTimestamp( TS_UNIX, $time ) ) < 10, + "Correct file timestamp of '$path'" ); + + $this->backend->clearCache( array( $path ) ); + + $size = $this->backend->getFileSize( array( 'src' => $path ) ); + + $this->assertEquals( strlen( $content ), $size, + "Correct file size of '$path'" ); + + $this->backend->preloadCache( array( $path ) ); + + $size = $this->backend->getFileSize( array( 'src' => $path ) ); + + $this->assertEquals( strlen( $content ), $size, + "Correct file size of '$path'" ); + } else { + $size = $this->backend->getFileSize( array( 'src' => $path ) ); + $time = $this->backend->getFileTimestamp( array( 'src' => $path ) ); + $stat = $this->backend->getFileStat( array( 'src' => $path ) ); + + $this->assertFalse( $size, "Correct file size of '$path'" ); + $this->assertFalse( $time, "Correct file timestamp of '$path'" ); + $this->assertFalse( $stat, "Correct file stat of '$path'" ); + } + } + + public static function provider_testGetFileStat() { + $cases = array(); + + $base = self::baseStorePath(); + $cases[] = array( "$base/unittest-cont1/e/b/z/some_file.txt", "some file contents", true ); + $cases[] = array( "$base/unittest-cont1/e/b/some-other_file.txt", "", true ); + $cases[] = array( "$base/unittest-cont1/e/b/some-diff_file.txt", null, false ); + + return $cases; + } + + /** + * @dataProvider provider_testGetFileContents + * @covers FileBackend::getFileContents + * @covers FileBackend::getFileContentsMulti + */ + public function testGetFileContents( $source, $content ) { + $this->backend = $this->singleBackend; + $this->tearDownFiles(); + $this->doTestGetFileContents( $source, $content ); + $this->tearDownFiles(); + + $this->backend = $this->multiBackend; + $this->tearDownFiles(); + $this->doTestGetFileContents( $source, $content ); + $this->tearDownFiles(); + } + + private function doTestGetFileContents( $source, $content ) { + $backendName = $this->backendClass(); + + $srcs = (array)$source; + $content = (array)$content; + foreach ( $srcs as $i => $src ) { + $this->prepare( array( 'dir' => dirname( $src ) ) ); + $status = $this->backend->doOperation( + array( 'op' => 'create', 'content' => $content[$i], 'dst' => $src ) ); + $this->assertGoodStatus( $status, + "Creation of file at $src succeeded ($backendName)." ); + } + + if ( is_array( $source ) ) { + $contents = $this->backend->getFileContentsMulti( array( 'srcs' => $source ) ); + foreach ( $contents as $path => $data ) { + $this->assertNotEquals( false, $data, "Contents of $path exists ($backendName)." ); + $this->assertEquals( current( $content ), $data, "Contents of $path is correct ($backendName)." ); + next( $content ); + } + $this->assertEquals( $source, array_keys( $contents ), "Contents in right order ($backendName)." ); + $this->assertEquals( count( $source ), count( $contents ), "Contents array size correct ($backendName)." ); + } else { + $data = $this->backend->getFileContents( array( 'src' => $source ) ); + $this->assertNotEquals( false, $data, "Contents of $source exists ($backendName)." ); + $this->assertEquals( $content[0], $data, "Contents of $source is correct ($backendName)." ); + } + } + + public static function provider_testGetFileContents() { + $cases = array(); + + $base = self::baseStorePath(); + $cases[] = array( "$base/unittest-cont1/e/b/z/some_file.txt", "some file contents" ); + $cases[] = array( "$base/unittest-cont1/e/b/some-other_file.txt", "more file contents" ); + $cases[] = array( + array( "$base/unittest-cont1/e/a/x.txt", "$base/unittest-cont1/e/a/y.txt", + "$base/unittest-cont1/e/a/z.txt" ), + array( "contents xx", "contents xy", "contents xz" ) + ); + + return $cases; + } + + /** + * @dataProvider provider_testGetLocalCopy + * @covers FileBackend::getLocalCopy + */ + public function testGetLocalCopy( $source, $content ) { + $this->backend = $this->singleBackend; + $this->tearDownFiles(); + $this->doTestGetLocalCopy( $source, $content ); + $this->tearDownFiles(); + + $this->backend = $this->multiBackend; + $this->tearDownFiles(); + $this->doTestGetLocalCopy( $source, $content ); + $this->tearDownFiles(); + } + + private function doTestGetLocalCopy( $source, $content ) { + $backendName = $this->backendClass(); + + $srcs = (array)$source; + $content = (array)$content; + foreach ( $srcs as $i => $src ) { + $this->prepare( array( 'dir' => dirname( $src ) ) ); + $status = $this->backend->doOperation( + array( 'op' => 'create', 'content' => $content[$i], 'dst' => $src ) ); + $this->assertGoodStatus( $status, + "Creation of file at $src succeeded ($backendName)." ); + } + + if ( is_array( $source ) ) { + $tmpFiles = $this->backend->getLocalCopyMulti( array( 'srcs' => $source ) ); + foreach ( $tmpFiles as $path => $tmpFile ) { + $this->assertNotNull( $tmpFile, + "Creation of local copy of $path succeeded ($backendName)." ); + $contents = file_get_contents( $tmpFile->getPath() ); + $this->assertNotEquals( false, $contents, "Local copy of $path exists ($backendName)." ); + $this->assertEquals( current( $content ), $contents, "Local copy of $path is correct ($backendName)." ); + next( $content ); + } + $this->assertEquals( $source, array_keys( $tmpFiles ), "Local copies in right order ($backendName)." ); + $this->assertEquals( count( $source ), count( $tmpFiles ), "Local copies array size correct ($backendName)." ); + } else { + $tmpFile = $this->backend->getLocalCopy( array( 'src' => $source ) ); + $this->assertNotNull( $tmpFile, + "Creation of local copy of $source succeeded ($backendName)." ); + $contents = file_get_contents( $tmpFile->getPath() ); + $this->assertNotEquals( false, $contents, "Local copy of $source exists ($backendName)." ); + $this->assertEquals( $content[0], $contents, "Local copy of $source is correct ($backendName)." ); + } + + $obj = new stdClass(); + $tmpFile->bind( $obj ); + } + + public static function provider_testGetLocalCopy() { + $cases = array(); + + $base = self::baseStorePath(); + $cases[] = array( "$base/unittest-cont1/e/a/z/some_file.txt", "some file contents" ); + $cases[] = array( "$base/unittest-cont1/e/a/some-other_file.txt", "more file contents" ); + $cases[] = array( "$base/unittest-cont1/e/a/\$odd&.txt", "test file contents" ); + $cases[] = array( + array( "$base/unittest-cont1/e/a/x.txt", "$base/unittest-cont1/e/a/y.txt", + "$base/unittest-cont1/e/a/z.txt" ), + array( "contents xx", "contents xy", "contents xz" ) + ); + + return $cases; + } + + /** + * @dataProvider provider_testGetLocalReference + * @covers FileBackend::getLocalReference + */ + public function testGetLocalReference( $source, $content ) { + $this->backend = $this->singleBackend; + $this->tearDownFiles(); + $this->doTestGetLocalReference( $source, $content ); + $this->tearDownFiles(); + + $this->backend = $this->multiBackend; + $this->tearDownFiles(); + $this->doTestGetLocalReference( $source, $content ); + $this->tearDownFiles(); + } + + private function doTestGetLocalReference( $source, $content ) { + $backendName = $this->backendClass(); + + $srcs = (array)$source; + $content = (array)$content; + foreach ( $srcs as $i => $src ) { + $this->prepare( array( 'dir' => dirname( $src ) ) ); + $status = $this->backend->doOperation( + array( 'op' => 'create', 'content' => $content[$i], 'dst' => $src ) ); + $this->assertGoodStatus( $status, + "Creation of file at $src succeeded ($backendName)." ); + } + + if ( is_array( $source ) ) { + $tmpFiles = $this->backend->getLocalReferenceMulti( array( 'srcs' => $source ) ); + foreach ( $tmpFiles as $path => $tmpFile ) { + $this->assertNotNull( $tmpFile, + "Creation of local copy of $path succeeded ($backendName)." ); + $contents = file_get_contents( $tmpFile->getPath() ); + $this->assertNotEquals( false, $contents, "Local ref of $path exists ($backendName)." ); + $this->assertEquals( current( $content ), $contents, "Local ref of $path is correct ($backendName)." ); + next( $content ); + } + $this->assertEquals( $source, array_keys( $tmpFiles ), "Local refs in right order ($backendName)." ); + $this->assertEquals( count( $source ), count( $tmpFiles ), "Local refs array size correct ($backendName)." ); + } else { + $tmpFile = $this->backend->getLocalReference( array( 'src' => $source ) ); + $this->assertNotNull( $tmpFile, + "Creation of local copy of $source succeeded ($backendName)." ); + $contents = file_get_contents( $tmpFile->getPath() ); + $this->assertNotEquals( false, $contents, "Local ref of $source exists ($backendName)." ); + $this->assertEquals( $content[0], $contents, "Local ref of $source is correct ($backendName)." ); + } + } + + public static function provider_testGetLocalReference() { + $cases = array(); + + $base = self::baseStorePath(); + $cases[] = array( "$base/unittest-cont1/e/a/z/some_file.txt", "some file contents" ); + $cases[] = array( "$base/unittest-cont1/e/a/some-other_file.txt", "more file contents" ); + $cases[] = array( "$base/unittest-cont1/e/a/\$odd&.txt", "test file contents" ); + $cases[] = array( + array( "$base/unittest-cont1/e/a/x.txt", "$base/unittest-cont1/e/a/y.txt", + "$base/unittest-cont1/e/a/z.txt" ), + array( "contents xx", "contents xy", "contents xz" ) + ); + + return $cases; + } + + /** + * @covers FileBackend::getLocalCopy + * @covers FileBackend::getLocalReference + */ + public function testGetLocalCopyAndReference404() { + $this->backend = $this->singleBackend; + $this->tearDownFiles(); + $this->doTestGetLocalCopyAndReference404(); + $this->tearDownFiles(); + + $this->backend = $this->multiBackend; + $this->tearDownFiles(); + $this->doTestGetLocalCopyAndReference404(); + $this->tearDownFiles(); + } + + public function doTestGetLocalCopyAndReference404() { + $backendName = $this->backendClass(); + + $base = self::baseStorePath(); + + $tmpFile = $this->backend->getLocalCopy( array( + 'src' => "$base/unittest-cont1/not-there" ) ); + $this->assertEquals( null, $tmpFile, "Local copy of not existing file is null ($backendName)." ); + + $tmpFile = $this->backend->getLocalReference( array( + 'src' => "$base/unittest-cont1/not-there" ) ); + $this->assertEquals( null, $tmpFile, "Local ref of not existing file is null ($backendName)." ); + } + + /** + * @dataProvider provider_testGetFileHttpUrl + * @covers FileBackend::getFileHttpUrl + */ + public function testGetFileHttpUrl( $source, $content ) { + $this->backend = $this->singleBackend; + $this->tearDownFiles(); + $this->doTestGetFileHttpUrl( $source, $content ); + $this->tearDownFiles(); + + $this->backend = $this->multiBackend; + $this->tearDownFiles(); + $this->doTestGetFileHttpUrl( $source, $content ); + $this->tearDownFiles(); + } + + private function doTestGetFileHttpUrl( $source, $content ) { + $backendName = $this->backendClass(); + + $this->prepare( array( 'dir' => dirname( $source ) ) ); + $status = $this->backend->doOperation( + array( 'op' => 'create', 'content' => $content, 'dst' => $source ) ); + $this->assertGoodStatus( $status, + "Creation of file at $source succeeded ($backendName)." ); + + $url = $this->backend->getFileHttpUrl( array( 'src' => $source ) ); + + if ( $url !== null ) { // supported + $data = Http::request( "GET", $url ); + $this->assertEquals( $content, $data, + "HTTP GET of URL has right contents ($backendName)." ); + } + } + + public static function provider_testGetFileHttpUrl() { + $cases = array(); + + $base = self::baseStorePath(); + $cases[] = array( "$base/unittest-cont1/e/a/z/some_file.txt", "some file contents" ); + $cases[] = array( "$base/unittest-cont1/e/a/some-other_file.txt", "more file contents" ); + $cases[] = array( "$base/unittest-cont1/e/a/\$odd&.txt", "test file contents" ); + + return $cases; + } + + /** + * @dataProvider provider_testPrepareAndClean + * @covers FileBackend::prepare + * @covers FileBackend::clean + */ + public function testPrepareAndClean( $path, $isOK ) { + $this->backend = $this->singleBackend; + $this->doTestPrepareAndClean( $path, $isOK ); + $this->tearDownFiles(); + + $this->backend = $this->multiBackend; + $this->doTestPrepareAndClean( $path, $isOK ); + $this->tearDownFiles(); + } + + public static function provider_testPrepareAndClean() { + $base = self::baseStorePath(); + + return array( + array( "$base/unittest-cont1/e/a/z/some_file1.txt", true ), + array( "$base/unittest-cont2/a/z/some_file2.txt", true ), + # Specific to FS backend with no basePath field set + #array( "$base/unittest-cont3/a/z/some_file3.txt", false ), + ); + } + + private function doTestPrepareAndClean( $path, $isOK ) { + $backendName = $this->backendClass(); + + $status = $this->prepare( array( 'dir' => dirname( $path ) ) ); + if ( $isOK ) { + $this->assertGoodStatus( $status, + "Preparing dir $path succeeded without warnings ($backendName)." ); + $this->assertEquals( true, $status->isOK(), + "Preparing dir $path succeeded ($backendName)." ); + } else { + $this->assertEquals( false, $status->isOK(), + "Preparing dir $path failed ($backendName)." ); + } + + $status = $this->backend->clean( array( 'dir' => dirname( $path ) ) ); + if ( $isOK ) { + $this->assertGoodStatus( $status, + "Cleaning dir $path succeeded without warnings ($backendName)." ); + $this->assertEquals( true, $status->isOK(), + "Cleaning dir $path succeeded ($backendName)." ); + } else { + $this->assertEquals( false, $status->isOK(), + "Cleaning dir $path failed ($backendName)." ); + } + } + + public function testRecursiveClean() { + $this->backend = $this->singleBackend; + $this->doTestRecursiveClean(); + $this->tearDownFiles(); + + $this->backend = $this->multiBackend; + $this->doTestRecursiveClean(); + $this->tearDownFiles(); + } + + /** + * @covers FileBackend::clean + */ + private function doTestRecursiveClean() { + $backendName = $this->backendClass(); + + $base = self::baseStorePath(); + $dirs = array( + "$base/unittest-cont1", + "$base/unittest-cont1/e", + "$base/unittest-cont1/e/a", + "$base/unittest-cont1/e/a/b", + "$base/unittest-cont1/e/a/b/c", + "$base/unittest-cont1/e/a/b/c/d0", + "$base/unittest-cont1/e/a/b/c/d1", + "$base/unittest-cont1/e/a/b/c/d2", + "$base/unittest-cont1/e/a/b/c/d0/1", + "$base/unittest-cont1/e/a/b/c/d0/2", + "$base/unittest-cont1/e/a/b/c/d1/3", + "$base/unittest-cont1/e/a/b/c/d1/4", + "$base/unittest-cont1/e/a/b/c/d2/5", + "$base/unittest-cont1/e/a/b/c/d2/6" + ); + foreach ( $dirs as $dir ) { + $status = $this->prepare( array( 'dir' => $dir ) ); + $this->assertGoodStatus( $status, + "Preparing dir $dir succeeded without warnings ($backendName)." ); + } + + if ( $this->backend instanceof FSFileBackend ) { + foreach ( $dirs as $dir ) { + $this->assertEquals( true, $this->backend->directoryExists( array( 'dir' => $dir ) ), + "Dir $dir exists ($backendName)." ); + } + } + + $status = $this->backend->clean( + array( 'dir' => "$base/unittest-cont1", 'recursive' => 1 ) ); + $this->assertGoodStatus( $status, + "Recursive cleaning of dir $dir succeeded without warnings ($backendName)." ); + + foreach ( $dirs as $dir ) { + $this->assertEquals( false, $this->backend->directoryExists( array( 'dir' => $dir ) ), + "Dir $dir no longer exists ($backendName)." ); + } + } + + // @todo testSecure + + /** + * @covers FileBackend::doOperations + */ + public function testDoOperations() { + $this->backend = $this->singleBackend; + $this->tearDownFiles(); + $this->doTestDoOperations(); + $this->tearDownFiles(); + + $this->backend = $this->multiBackend; + $this->tearDownFiles(); + $this->doTestDoOperations(); + $this->tearDownFiles(); + } + + private function doTestDoOperations() { + $base = self::baseStorePath(); + + $fileA = "$base/unittest-cont1/e/a/b/fileA.txt"; + $fileAContents = '3tqtmoeatmn4wg4qe-mg3qt3 tq'; + $fileB = "$base/unittest-cont1/e/a/b/fileB.txt"; + $fileBContents = 'g-jmq3gpqgt3qtg q3GT '; + $fileC = "$base/unittest-cont1/e/a/b/fileC.txt"; + $fileCContents = 'eigna[ogmewt 3qt g3qg flew[ag'; + $fileD = "$base/unittest-cont1/e/a/b/fileD.txt"; + + $this->prepare( array( 'dir' => dirname( $fileA ) ) ); + $this->create( array( 'dst' => $fileA, 'content' => $fileAContents ) ); + $this->prepare( array( 'dir' => dirname( $fileB ) ) ); + $this->create( array( 'dst' => $fileB, 'content' => $fileBContents ) ); + $this->prepare( array( 'dir' => dirname( $fileC ) ) ); + $this->create( array( 'dst' => $fileC, 'content' => $fileCContents ) ); + $this->prepare( array( 'dir' => dirname( $fileD ) ) ); + + $status = $this->backend->doOperations( array( + array( 'op' => 'describe', 'src' => $fileA, + 'headers' => array( 'X-Content-Length' => '91.3' ), 'disposition' => 'inline' ), + array( 'op' => 'copy', 'src' => $fileA, 'dst' => $fileC, 'overwrite' => 1 ), + // Now: A:, B:, C:, D: (file:) + array( 'op' => 'copy', 'src' => $fileC, 'dst' => $fileA, 'overwriteSame' => 1 ), + // Now: A:, B:, C:, D: + array( 'op' => 'move', 'src' => $fileC, 'dst' => $fileD, 'overwrite' => 1 ), + // Now: A:, B:, C:, D: + array( 'op' => 'move', 'src' => $fileB, 'dst' => $fileC ), + // Now: A:, B:, C:, D: + array( 'op' => 'move', 'src' => $fileD, 'dst' => $fileA, 'overwriteSame' => 1 ), + // Now: A:, B:, C:, D: + array( 'op' => 'move', 'src' => $fileC, 'dst' => $fileA, 'overwrite' => 1 ), + // Now: A:, B:, C:, D: + array( 'op' => 'copy', 'src' => $fileA, 'dst' => $fileC ), + // Now: A:, B:, C:, D: + array( 'op' => 'move', 'src' => $fileA, 'dst' => $fileC, 'overwriteSame' => 1 ), + // Now: A:, B:, C:, D: + array( 'op' => 'copy', 'src' => $fileC, 'dst' => $fileC, 'overwrite' => 1 ), + // Does nothing + array( 'op' => 'copy', 'src' => $fileC, 'dst' => $fileC, 'overwriteSame' => 1 ), + // Does nothing + array( 'op' => 'move', 'src' => $fileC, 'dst' => $fileC, 'overwrite' => 1 ), + // Does nothing + array( 'op' => 'move', 'src' => $fileC, 'dst' => $fileC, 'overwriteSame' => 1 ), + // Does nothing + array( 'op' => 'null' ), + // Does nothing + ) ); + + $this->assertGoodStatus( $status, "Operation batch succeeded" ); + $this->assertEquals( true, $status->isOK(), "Operation batch succeeded" ); + $this->assertEquals( 14, count( $status->success ), + "Operation batch has correct success array" ); + + $this->assertEquals( false, $this->backend->fileExists( array( 'src' => $fileA ) ), + "File does not exist at $fileA" ); + $this->assertEquals( false, $this->backend->fileExists( array( 'src' => $fileB ) ), + "File does not exist at $fileB" ); + $this->assertEquals( false, $this->backend->fileExists( array( 'src' => $fileD ) ), + "File does not exist at $fileD" ); + + $this->assertEquals( true, $this->backend->fileExists( array( 'src' => $fileC ) ), + "File exists at $fileC" ); + $this->assertEquals( $fileBContents, + $this->backend->getFileContents( array( 'src' => $fileC ) ), + "Correct file contents of $fileC" ); + $this->assertEquals( strlen( $fileBContents ), + $this->backend->getFileSize( array( 'src' => $fileC ) ), + "Correct file size of $fileC" ); + $this->assertEquals( wfBaseConvert( sha1( $fileBContents ), 16, 36, 31 ), + $this->backend->getFileSha1Base36( array( 'src' => $fileC ) ), + "Correct file SHA-1 of $fileC" ); + } + + /** + * @covers FileBackend::doOperations + */ + public function testDoOperationsPipeline() { + $this->backend = $this->singleBackend; + $this->tearDownFiles(); + $this->doTestDoOperationsPipeline(); + $this->tearDownFiles(); + + $this->backend = $this->multiBackend; + $this->tearDownFiles(); + $this->doTestDoOperationsPipeline(); + $this->tearDownFiles(); + } + + // concurrency orientated + private function doTestDoOperationsPipeline() { + $base = self::baseStorePath(); + + $fileAContents = '3tqtmoeatmn4wg4qe-mg3qt3 tq'; + $fileBContents = 'g-jmq3gpqgt3qtg q3GT '; + $fileCContents = 'eigna[ogmewt 3qt g3qg flew[ag'; + + $tmpNameA = TempFSFile::factory( "unittests_", 'txt' )->getPath(); + file_put_contents( $tmpNameA, $fileAContents ); + $tmpNameB = TempFSFile::factory( "unittests_", 'txt' )->getPath(); + file_put_contents( $tmpNameB, $fileBContents ); + $tmpNameC = TempFSFile::factory( "unittests_", 'txt' )->getPath(); + file_put_contents( $tmpNameC, $fileCContents ); + + $this->filesToPrune[] = $tmpNameA; # avoid file leaking + $this->filesToPrune[] = $tmpNameB; # avoid file leaking + $this->filesToPrune[] = $tmpNameC; # avoid file leaking + + $fileA = "$base/unittest-cont1/e/a/b/fileA.txt"; + $fileB = "$base/unittest-cont1/e/a/b/fileB.txt"; + $fileC = "$base/unittest-cont1/e/a/b/fileC.txt"; + $fileD = "$base/unittest-cont1/e/a/b/fileD.txt"; + + $this->prepare( array( 'dir' => dirname( $fileA ) ) ); + $this->create( array( 'dst' => $fileA, 'content' => $fileAContents ) ); + $this->prepare( array( 'dir' => dirname( $fileB ) ) ); + $this->prepare( array( 'dir' => dirname( $fileC ) ) ); + $this->prepare( array( 'dir' => dirname( $fileD ) ) ); + + $status = $this->backend->doOperations( array( + array( 'op' => 'store', 'src' => $tmpNameA, 'dst' => $fileA, 'overwriteSame' => 1 ), + array( 'op' => 'store', 'src' => $tmpNameB, 'dst' => $fileB, 'overwrite' => 1 ), + array( 'op' => 'store', 'src' => $tmpNameC, 'dst' => $fileC, 'overwrite' => 1 ), + array( 'op' => 'copy', 'src' => $fileA, 'dst' => $fileC, 'overwrite' => 1 ), + // Now: A:, B:, C:, D: (file:) + array( 'op' => 'copy', 'src' => $fileC, 'dst' => $fileA, 'overwriteSame' => 1 ), + // Now: A:, B:, C:, D: + array( 'op' => 'move', 'src' => $fileC, 'dst' => $fileD, 'overwrite' => 1 ), + // Now: A:, B:, C:, D: + array( 'op' => 'move', 'src' => $fileB, 'dst' => $fileC ), + // Now: A:, B:, C:, D: + array( 'op' => 'move', 'src' => $fileD, 'dst' => $fileA, 'overwriteSame' => 1 ), + // Now: A:, B:, C:, D: + array( 'op' => 'move', 'src' => $fileC, 'dst' => $fileA, 'overwrite' => 1 ), + // Now: A:, B:, C:, D: + array( 'op' => 'copy', 'src' => $fileA, 'dst' => $fileC ), + // Now: A:, B:, C:, D: + array( 'op' => 'move', 'src' => $fileA, 'dst' => $fileC, 'overwriteSame' => 1 ), + // Now: A:, B:, C:, D: + array( 'op' => 'copy', 'src' => $fileC, 'dst' => $fileC, 'overwrite' => 1 ), + // Does nothing + array( 'op' => 'copy', 'src' => $fileC, 'dst' => $fileC, 'overwriteSame' => 1 ), + // Does nothing + array( 'op' => 'move', 'src' => $fileC, 'dst' => $fileC, 'overwrite' => 1 ), + // Does nothing + array( 'op' => 'move', 'src' => $fileC, 'dst' => $fileC, 'overwriteSame' => 1 ), + // Does nothing + array( 'op' => 'null' ), + // Does nothing + ) ); + + $this->assertGoodStatus( $status, "Operation batch succeeded" ); + $this->assertEquals( true, $status->isOK(), "Operation batch succeeded" ); + $this->assertEquals( 16, count( $status->success ), + "Operation batch has correct success array" ); + + $this->assertEquals( false, $this->backend->fileExists( array( 'src' => $fileA ) ), + "File does not exist at $fileA" ); + $this->assertEquals( false, $this->backend->fileExists( array( 'src' => $fileB ) ), + "File does not exist at $fileB" ); + $this->assertEquals( false, $this->backend->fileExists( array( 'src' => $fileD ) ), + "File does not exist at $fileD" ); + + $this->assertEquals( true, $this->backend->fileExists( array( 'src' => $fileC ) ), + "File exists at $fileC" ); + $this->assertEquals( $fileBContents, + $this->backend->getFileContents( array( 'src' => $fileC ) ), + "Correct file contents of $fileC" ); + $this->assertEquals( strlen( $fileBContents ), + $this->backend->getFileSize( array( 'src' => $fileC ) ), + "Correct file size of $fileC" ); + $this->assertEquals( wfBaseConvert( sha1( $fileBContents ), 16, 36, 31 ), + $this->backend->getFileSha1Base36( array( 'src' => $fileC ) ), + "Correct file SHA-1 of $fileC" ); + } + + /** + * @covers FileBackend::doOperations + */ + public function testDoOperationsFailing() { + $this->backend = $this->singleBackend; + $this->tearDownFiles(); + $this->doTestDoOperationsFailing(); + $this->tearDownFiles(); + + $this->backend = $this->multiBackend; + $this->tearDownFiles(); + $this->doTestDoOperationsFailing(); + $this->tearDownFiles(); + } + + private function doTestDoOperationsFailing() { + $base = self::baseStorePath(); + + $fileA = "$base/unittest-cont2/a/b/fileA.txt"; + $fileAContents = '3tqtmoeatmn4wg4qe-mg3qt3 tq'; + $fileB = "$base/unittest-cont2/a/b/fileB.txt"; + $fileBContents = 'g-jmq3gpqgt3qtg q3GT '; + $fileC = "$base/unittest-cont2/a/b/fileC.txt"; + $fileCContents = 'eigna[ogmewt 3qt g3qg flew[ag'; + $fileD = "$base/unittest-cont2/a/b/fileD.txt"; + + $this->prepare( array( 'dir' => dirname( $fileA ) ) ); + $this->create( array( 'dst' => $fileA, 'content' => $fileAContents ) ); + $this->prepare( array( 'dir' => dirname( $fileB ) ) ); + $this->create( array( 'dst' => $fileB, 'content' => $fileBContents ) ); + $this->prepare( array( 'dir' => dirname( $fileC ) ) ); + $this->create( array( 'dst' => $fileC, 'content' => $fileCContents ) ); + + $status = $this->backend->doOperations( array( + array( 'op' => 'copy', 'src' => $fileA, 'dst' => $fileC, 'overwrite' => 1 ), + // Now: A:, B:, C:, D: (file:) + array( 'op' => 'copy', 'src' => $fileC, 'dst' => $fileA, 'overwriteSame' => 1 ), + // Now: A:, B:, C:, D: + array( 'op' => 'copy', 'src' => $fileB, 'dst' => $fileD, 'overwrite' => 1 ), + // Now: A:, B:, C:, D: + array( 'op' => 'move', 'src' => $fileC, 'dst' => $fileD ), + // Now: A:, B:, C:, D: (failed) + array( 'op' => 'move', 'src' => $fileB, 'dst' => $fileC, 'overwriteSame' => 1 ), + // Now: A:, B:, C:, D: (failed) + array( 'op' => 'move', 'src' => $fileB, 'dst' => $fileA, 'overwrite' => 1 ), + // Now: A:, B:, C:, D: + array( 'op' => 'delete', 'src' => $fileD ), + // Now: A:, B:, C:, D: + array( 'op' => 'null' ), + // Does nothing + ), array( 'force' => 1 ) ); + + $this->assertNotEquals( array(), $status->errors, "Operation had warnings" ); + $this->assertEquals( true, $status->isOK(), "Operation batch succeeded" ); + $this->assertEquals( 8, count( $status->success ), + "Operation batch has correct success array" ); + + $this->assertEquals( false, $this->backend->fileExists( array( 'src' => $fileB ) ), + "File does not exist at $fileB" ); + $this->assertEquals( false, $this->backend->fileExists( array( 'src' => $fileD ) ), + "File does not exist at $fileD" ); + + $this->assertEquals( true, $this->backend->fileExists( array( 'src' => $fileA ) ), + "File does not exist at $fileA" ); + $this->assertEquals( true, $this->backend->fileExists( array( 'src' => $fileC ) ), + "File exists at $fileC" ); + $this->assertEquals( $fileBContents, + $this->backend->getFileContents( array( 'src' => $fileA ) ), + "Correct file contents of $fileA" ); + $this->assertEquals( strlen( $fileBContents ), + $this->backend->getFileSize( array( 'src' => $fileA ) ), + "Correct file size of $fileA" ); + $this->assertEquals( wfBaseConvert( sha1( $fileBContents ), 16, 36, 31 ), + $this->backend->getFileSha1Base36( array( 'src' => $fileA ) ), + "Correct file SHA-1 of $fileA" ); + } + + /** + * @covers FileBackend::getFileList + */ + public function testGetFileList() { + $this->backend = $this->singleBackend; + $this->tearDownFiles(); + $this->doTestGetFileList(); + $this->tearDownFiles(); + + $this->backend = $this->multiBackend; + $this->tearDownFiles(); + $this->doTestGetFileList(); + $this->tearDownFiles(); + } + + private function doTestGetFileList() { + $backendName = $this->backendClass(); + $base = self::baseStorePath(); + + // Should have no errors + $iter = $this->backend->getFileList( array( 'dir' => "$base/unittest-cont-notexists" ) ); + + $files = array( + "$base/unittest-cont1/e/test1.txt", + "$base/unittest-cont1/e/test2.txt", + "$base/unittest-cont1/e/test3.txt", + "$base/unittest-cont1/e/subdir1/test1.txt", + "$base/unittest-cont1/e/subdir1/test2.txt", + "$base/unittest-cont1/e/subdir2/test3.txt", + "$base/unittest-cont1/e/subdir2/test4.txt", + "$base/unittest-cont1/e/subdir2/subdir/test1.txt", + "$base/unittest-cont1/e/subdir2/subdir/test2.txt", + "$base/unittest-cont1/e/subdir2/subdir/test3.txt", + "$base/unittest-cont1/e/subdir2/subdir/test4.txt", + "$base/unittest-cont1/e/subdir2/subdir/test5.txt", + "$base/unittest-cont1/e/subdir2/subdir/sub/test0.txt", + "$base/unittest-cont1/e/subdir2/subdir/sub/120-px-file.txt", + ); + + // Add the files + $ops = array(); + foreach ( $files as $file ) { + $this->prepare( array( 'dir' => dirname( $file ) ) ); + $ops[] = array( 'op' => 'create', 'content' => 'xxy', 'dst' => $file ); + } + $status = $this->backend->doQuickOperations( $ops ); + $this->assertGoodStatus( $status, + "Creation of files succeeded ($backendName)." ); + $this->assertEquals( true, $status->isOK(), + "Creation of files succeeded with OK status ($backendName)." ); + + // Expected listing at root + $expected = array( + "e/test1.txt", + "e/test2.txt", + "e/test3.txt", + "e/subdir1/test1.txt", + "e/subdir1/test2.txt", + "e/subdir2/test3.txt", + "e/subdir2/test4.txt", + "e/subdir2/subdir/test1.txt", + "e/subdir2/subdir/test2.txt", + "e/subdir2/subdir/test3.txt", + "e/subdir2/subdir/test4.txt", + "e/subdir2/subdir/test5.txt", + "e/subdir2/subdir/sub/test0.txt", + "e/subdir2/subdir/sub/120-px-file.txt", + ); + sort( $expected ); + + // Actual listing (no trailing slash) at root + $iter = $this->backend->getFileList( array( 'dir' => "$base/unittest-cont1" ) ); + $list = $this->listToArray( $iter ); + sort( $list ); + $this->assertEquals( $expected, $list, "Correct file listing ($backendName)." ); + + // Actual listing (no trailing slash) at root with advise + $iter = $this->backend->getFileList( array( 'dir' => "$base/unittest-cont1", 'adviseStat' => 1 ) ); + $list = $this->listToArray( $iter ); + sort( $list ); + $this->assertEquals( $expected, $list, "Correct file listing ($backendName)." ); + + // Actual listing (with trailing slash) at root + $list = array(); + $iter = $this->backend->getFileList( array( 'dir' => "$base/unittest-cont1/" ) ); + foreach ( $iter as $file ) { + $list[] = $file; + } + sort( $list ); + $this->assertEquals( $expected, $list, "Correct file listing ($backendName)." ); + + // Expected listing at subdir + $expected = array( + "test1.txt", + "test2.txt", + "test3.txt", + "test4.txt", + "test5.txt", + "sub/test0.txt", + "sub/120-px-file.txt", + ); + sort( $expected ); + + // Actual listing (no trailing slash) at subdir + $iter = $this->backend->getFileList( array( 'dir' => "$base/unittest-cont1/e/subdir2/subdir" ) ); + $list = $this->listToArray( $iter ); + sort( $list ); + $this->assertEquals( $expected, $list, "Correct file listing ($backendName)." ); + + // Actual listing (no trailing slash) at subdir with advise + $iter = $this->backend->getFileList( array( 'dir' => "$base/unittest-cont1/e/subdir2/subdir", 'adviseStat' => 1 ) ); + $list = $this->listToArray( $iter ); + sort( $list ); + $this->assertEquals( $expected, $list, "Correct file listing ($backendName)." ); + + // Actual listing (with trailing slash) at subdir + $list = array(); + $iter = $this->backend->getFileList( array( 'dir' => "$base/unittest-cont1/e/subdir2/subdir/" ) ); + foreach ( $iter as $file ) { + $list[] = $file; + } + sort( $list ); + $this->assertEquals( $expected, $list, "Correct file listing ($backendName)." ); + + // Actual listing (using iterator second time) + $list = $this->listToArray( $iter ); + sort( $list ); + $this->assertEquals( $expected, $list, "Correct file listing ($backendName), second iteration." ); + + // Actual listing (top files only) at root + $iter = $this->backend->getTopFileList( array( 'dir' => "$base/unittest-cont1" ) ); + $list = $this->listToArray( $iter ); + sort( $list ); + $this->assertEquals( array(), $list, "Correct top file listing ($backendName)." ); + + // Expected listing (top files only) at subdir + $expected = array( + "test1.txt", + "test2.txt", + "test3.txt", + "test4.txt", + "test5.txt" + ); + sort( $expected ); + + // Actual listing (top files only) at subdir + $iter = $this->backend->getTopFileList( array( 'dir' => "$base/unittest-cont1/e/subdir2/subdir" ) ); + $list = $this->listToArray( $iter ); + sort( $list ); + $this->assertEquals( $expected, $list, "Correct top file listing ($backendName)." ); + + // Actual listing (top files only) at subdir with advise + $iter = $this->backend->getTopFileList( array( 'dir' => "$base/unittest-cont1/e/subdir2/subdir", 'adviseStat' => 1 ) ); + $list = $this->listToArray( $iter ); + sort( $list ); + $this->assertEquals( $expected, $list, "Correct top file listing ($backendName)." ); + + foreach ( $files as $file ) { // clean up + $this->backend->doOperation( array( 'op' => 'delete', 'src' => $file ) ); + } + + $iter = $this->backend->getFileList( array( 'dir' => "$base/unittest-cont1/not/exists" ) ); + foreach ( $iter as $iter ) { + // no errors + } + } + + /** + * @covers FileBackend::getTopDirectoryList + * @covers FileBackend::getDirectoryList + */ + public function testGetDirectoryList() { + $this->backend = $this->singleBackend; + $this->tearDownFiles(); + $this->doTestGetDirectoryList(); + $this->tearDownFiles(); + + $this->backend = $this->multiBackend; + $this->tearDownFiles(); + $this->doTestGetDirectoryList(); + $this->tearDownFiles(); + } + + private function doTestGetDirectoryList() { + $backendName = $this->backendClass(); + + $base = self::baseStorePath(); + $files = array( + "$base/unittest-cont1/e/test1.txt", + "$base/unittest-cont1/e/test2.txt", + "$base/unittest-cont1/e/test3.txt", + "$base/unittest-cont1/e/subdir1/test1.txt", + "$base/unittest-cont1/e/subdir1/test2.txt", + "$base/unittest-cont1/e/subdir2/test3.txt", + "$base/unittest-cont1/e/subdir2/test4.txt", + "$base/unittest-cont1/e/subdir2/subdir/test1.txt", + "$base/unittest-cont1/e/subdir3/subdir/test2.txt", + "$base/unittest-cont1/e/subdir4/subdir/test3.txt", + "$base/unittest-cont1/e/subdir4/subdir/test4.txt", + "$base/unittest-cont1/e/subdir4/subdir/test5.txt", + "$base/unittest-cont1/e/subdir4/subdir/sub/test0.txt", + "$base/unittest-cont1/e/subdir4/subdir/sub/120-px-file.txt", + ); + + // Add the files + $ops = array(); + foreach ( $files as $file ) { + $this->prepare( array( 'dir' => dirname( $file ) ) ); + $ops[] = array( 'op' => 'create', 'content' => 'xxy', 'dst' => $file ); + } + $status = $this->backend->doQuickOperations( $ops ); + $this->assertGoodStatus( $status, + "Creation of files succeeded ($backendName)." ); + $this->assertEquals( true, $status->isOK(), + "Creation of files succeeded with OK status ($backendName)." ); + + $this->assertEquals( true, + $this->backend->directoryExists( array( 'dir' => "$base/unittest-cont1/e/subdir1" ) ), + "Directory exists in ($backendName)." ); + $this->assertEquals( true, + $this->backend->directoryExists( array( 'dir' => "$base/unittest-cont1/e/subdir2/subdir" ) ), + "Directory exists in ($backendName)." ); + $this->assertEquals( false, + $this->backend->directoryExists( array( 'dir' => "$base/unittest-cont1/e/subdir2/test1.txt" ) ), + "Directory does not exists in ($backendName)." ); + + // Expected listing + $expected = array( + "e", + ); + sort( $expected ); + + // Actual listing (no trailing slash) + $list = array(); + $iter = $this->backend->getTopDirectoryList( array( 'dir' => "$base/unittest-cont1" ) ); + foreach ( $iter as $file ) { + $list[] = $file; + } + sort( $list ); + + $this->assertEquals( $expected, $list, "Correct top dir listing ($backendName)." ); + + // Expected listing + $expected = array( + "subdir1", + "subdir2", + "subdir3", + "subdir4", + ); + sort( $expected ); + + // Actual listing (no trailing slash) + $list = array(); + $iter = $this->backend->getTopDirectoryList( array( 'dir' => "$base/unittest-cont1/e" ) ); + foreach ( $iter as $file ) { + $list[] = $file; + } + sort( $list ); + + $this->assertEquals( $expected, $list, "Correct top dir listing ($backendName)." ); + + // Actual listing (with trailing slash) + $list = array(); + $iter = $this->backend->getTopDirectoryList( array( 'dir' => "$base/unittest-cont1/e/" ) ); + foreach ( $iter as $file ) { + $list[] = $file; + } + sort( $list ); + + $this->assertEquals( $expected, $list, "Correct top dir listing ($backendName)." ); + + // Expected listing + $expected = array( + "subdir", + ); + sort( $expected ); + + // Actual listing (no trailing slash) + $list = array(); + $iter = $this->backend->getTopDirectoryList( array( 'dir' => "$base/unittest-cont1/e/subdir2" ) ); + foreach ( $iter as $file ) { + $list[] = $file; + } + sort( $list ); + + $this->assertEquals( $expected, $list, "Correct top dir listing ($backendName)." ); + + // Actual listing (with trailing slash) + $list = array(); + $iter = $this->backend->getTopDirectoryList( array( 'dir' => "$base/unittest-cont1/e/subdir2/" ) ); + foreach ( $iter as $file ) { + $list[] = $file; + } + sort( $list ); + + $this->assertEquals( $expected, $list, "Correct top dir listing ($backendName)." ); + + // Actual listing (using iterator second time) + $list = array(); + foreach ( $iter as $file ) { + $list[] = $file; + } + sort( $list ); + + $this->assertEquals( $expected, $list, "Correct top dir listing ($backendName), second iteration." ); + + // Expected listing (recursive) + $expected = array( + "e", + "e/subdir1", + "e/subdir2", + "e/subdir3", + "e/subdir4", + "e/subdir2/subdir", + "e/subdir3/subdir", + "e/subdir4/subdir", + "e/subdir4/subdir/sub", + ); + sort( $expected ); + + // Actual listing (recursive) + $list = array(); + $iter = $this->backend->getDirectoryList( array( 'dir' => "$base/unittest-cont1/" ) ); + foreach ( $iter as $file ) { + $list[] = $file; + } + sort( $list ); + + $this->assertEquals( $expected, $list, "Correct dir listing ($backendName)." ); + + // Expected listing (recursive) + $expected = array( + "subdir", + "subdir/sub", + ); + sort( $expected ); + + // Actual listing (recursive) + $list = array(); + $iter = $this->backend->getDirectoryList( array( 'dir' => "$base/unittest-cont1/e/subdir4" ) ); + foreach ( $iter as $file ) { + $list[] = $file; + } + sort( $list ); + + $this->assertEquals( $expected, $list, "Correct dir listing ($backendName)." ); + + // Actual listing (recursive, second time) + $list = array(); + foreach ( $iter as $file ) { + $list[] = $file; + } + sort( $list ); + + $this->assertEquals( $expected, $list, "Correct dir listing ($backendName)." ); + + $iter = $this->backend->getDirectoryList( array( 'dir' => "$base/unittest-cont1/e/subdir1" ) ); + $items = $this->listToArray( $iter ); + $this->assertEquals( array(), $items, "Directory listing is empty." ); + + foreach ( $files as $file ) { // clean up + $this->backend->doOperation( array( 'op' => 'delete', 'src' => $file ) ); + } + + $iter = $this->backend->getDirectoryList( array( 'dir' => "$base/unittest-cont1/not/exists" ) ); + foreach ( $iter as $file ) { + // no errors + } + + $items = $this->listToArray( $iter ); + $this->assertEquals( array(), $items, "Directory listing is empty." ); + + $iter = $this->backend->getDirectoryList( array( 'dir' => "$base/unittest-cont1/e/not/exists" ) ); + $items = $this->listToArray( $iter ); + $this->assertEquals( array(), $items, "Directory listing is empty." ); + } + + /** + * @covers FileBackend::lockFiles + * @covers FileBackend::unlockFiles + */ + public function testLockCalls() { + $this->backend = $this->singleBackend; + $this->doTestLockCalls(); + } + + private function doTestLockCalls() { + $backendName = $this->backendClass(); + + $paths = array( + "test1.txt", + "test2.txt", + "test3.txt", + "subdir1", + "subdir1", // duplicate + "subdir1/test1.txt", + "subdir1/test2.txt", + "subdir2", + "subdir2", // duplicate + "subdir2/test3.txt", + "subdir2/test4.txt", + "subdir2/subdir", + "subdir2/subdir/test1.txt", + "subdir2/subdir/test2.txt", + "subdir2/subdir/test3.txt", + "subdir2/subdir/test4.txt", + "subdir2/subdir/test5.txt", + "subdir2/subdir/sub", + "subdir2/subdir/sub/test0.txt", + "subdir2/subdir/sub/120-px-file.txt", + ); + + for ( $i = 0; $i < 25; $i++ ) { + $status = $this->backend->lockFiles( $paths, LockManager::LOCK_EX ); + $this->assertEquals( print_r( array(), true ), print_r( $status->errors, true ), + "Locking of files succeeded ($backendName) ($i)." ); + $this->assertEquals( true, $status->isOK(), + "Locking of files succeeded with OK status ($backendName) ($i)." ); + + $status = $this->backend->lockFiles( $paths, LockManager::LOCK_SH ); + $this->assertEquals( print_r( array(), true ), print_r( $status->errors, true ), + "Locking of files succeeded ($backendName) ($i)." ); + $this->assertEquals( true, $status->isOK(), + "Locking of files succeeded with OK status ($backendName) ($i)." ); + + $status = $this->backend->unlockFiles( $paths, LockManager::LOCK_SH ); + $this->assertEquals( print_r( array(), true ), print_r( $status->errors, true ), + "Locking of files succeeded ($backendName) ($i)." ); + $this->assertEquals( true, $status->isOK(), + "Locking of files succeeded with OK status ($backendName) ($i)." ); + + $status = $this->backend->unlockFiles( $paths, LockManager::LOCK_EX ); + $this->assertEquals( print_r( array(), true ), print_r( $status->errors, true ), + "Locking of files succeeded ($backendName). ($i)" ); + $this->assertEquals( true, $status->isOK(), + "Locking of files succeeded with OK status ($backendName) ($i)." ); + + ## Flip the acquire/release ordering around ## + + $status = $this->backend->lockFiles( $paths, LockManager::LOCK_SH ); + $this->assertEquals( print_r( array(), true ), print_r( $status->errors, true ), + "Locking of files succeeded ($backendName) ($i)." ); + $this->assertEquals( true, $status->isOK(), + "Locking of files succeeded with OK status ($backendName) ($i)." ); + + $status = $this->backend->lockFiles( $paths, LockManager::LOCK_EX ); + $this->assertEquals( print_r( array(), true ), print_r( $status->errors, true ), + "Locking of files succeeded ($backendName) ($i)." ); + $this->assertEquals( true, $status->isOK(), + "Locking of files succeeded with OK status ($backendName) ($i)." ); + + $status = $this->backend->unlockFiles( $paths, LockManager::LOCK_EX ); + $this->assertEquals( print_r( array(), true ), print_r( $status->errors, true ), + "Locking of files succeeded ($backendName). ($i)" ); + $this->assertEquals( true, $status->isOK(), + "Locking of files succeeded with OK status ($backendName) ($i)." ); + + $status = $this->backend->unlockFiles( $paths, LockManager::LOCK_SH ); + $this->assertEquals( print_r( array(), true ), print_r( $status->errors, true ), + "Locking of files succeeded ($backendName) ($i)." ); + $this->assertEquals( true, $status->isOK(), + "Locking of files succeeded with OK status ($backendName) ($i)." ); + } + + $status = Status::newGood(); + $sl = $this->backend->getScopedFileLocks( $paths, LockManager::LOCK_EX, $status ); + $this->assertType( 'ScopedLock', $sl, + "Scoped locking of files succeeded ($backendName)." ); + $this->assertEquals( array(), $status->errors, + "Scoped locking of files succeeded ($backendName)." ); + $this->assertEquals( true, $status->isOK(), + "Scoped locking of files succeeded with OK status ($backendName)." ); + + ScopedLock::release( $sl ); + $this->assertEquals( null, $sl, + "Scoped unlocking of files succeeded ($backendName)." ); + $this->assertEquals( array(), $status->errors, + "Scoped unlocking of files succeeded ($backendName)." ); + $this->assertEquals( true, $status->isOK(), + "Scoped unlocking of files succeeded with OK status ($backendName)." ); + } + + // helper function + private function listToArray( $iter ) { + return is_array( $iter ) ? $iter : iterator_to_array( $iter ); + } + + // test helper wrapper for backend prepare() function + private function prepare( array $params ) { + return $this->backend->prepare( $params ); + } + + // test helper wrapper for backend prepare() function + private function create( array $params ) { + $params['op'] = 'create'; + + return $this->backend->doQuickOperations( array( $params ) ); + } + + function tearDownFiles() { + foreach ( $this->filesToPrune as $file ) { + if ( is_file( $file ) ) { + unlink( $file ); + } + } + $containers = array( 'unittest-cont1', 'unittest-cont2' ); + foreach ( $containers as $container ) { + $this->deleteFiles( $container ); + } + $this->filesToPrune = array(); + } + + private function deleteFiles( $container ) { + $base = self::baseStorePath(); + $iter = $this->backend->getFileList( array( 'dir' => "$base/$container" ) ); + if ( $iter ) { + foreach ( $iter as $file ) { + $this->backend->quickDelete( array( 'src' => "$base/$container/$file" ) ); + } + // free the directory, to avoid Permission denied under windows on rmdir + unset( $iter ); + } + $this->backend->clean( array( 'dir' => "$base/$container", 'recursive' => 1 ) ); + } + + function assertBackendPathsConsistent( array $paths ) { + if ( $this->backend instanceof FileBackendMultiWrite ) { + $status = $this->backend->consistencyCheck( $paths ); + $this->assertGoodStatus( $status, "Files synced: " . implode( ',', $paths ) ); + } + } + + function assertGoodStatus( $status, $msg ) { + $this->assertEquals( print_r( array(), 1 ), print_r( $status->errors, 1 ), $msg ); + } +} diff --git a/tests/phpunit/includes/filerepo/FileRepoTest.php b/tests/phpunit/includes/filerepo/FileRepoTest.php new file mode 100644 index 00000000..e3a75567 --- /dev/null +++ b/tests/phpunit/includes/filerepo/FileRepoTest.php @@ -0,0 +1,55 @@ + 'foobar' + ) ); + } + + /** + * @expectedException MWException + * @covers FileRepo::__construct + */ + public function testFileRepoConstructionOptionNeedBackendKey() { + new FileRepo( array( + 'name' => 'foobar' + ) ); + } + + /** + * @covers FileRepo::__construct + */ + public function testFileRepoConstructionWithRequiredOptions() { + $f = new FileRepo( array( + 'name' => 'FileRepoTestRepository', + 'backend' => new FSFileBackend( array( + 'name' => 'local-testing', + 'lockManager' => 'nullLockManager', + 'containerPaths' => array() + ) ) + ) ); + $this->assertInstanceOf( 'FileRepo', $f ); + } +} diff --git a/tests/phpunit/includes/filerepo/StoreBatchTest.php b/tests/phpunit/includes/filerepo/StoreBatchTest.php new file mode 100644 index 00000000..b33c1bbb --- /dev/null +++ b/tests/phpunit/includes/filerepo/StoreBatchTest.php @@ -0,0 +1,134 @@ +getCliArg( 'use-filebackend=' ) ) { + $name = $this->getCliArg( 'use-filebackend=' ); + $useConfig = array(); + foreach ( $wgFileBackends as $conf ) { + if ( $conf['name'] == $name ) { + $useConfig = $conf; + } + } + $useConfig['name'] = 'local-testing'; // swap name + $class = $useConfig['class']; + $backend = new $class( $useConfig ); + } else { + $backend = new FSFileBackend( array( + 'name' => 'local-testing', + 'lockManager' => 'nullLockManager', + 'containerPaths' => array( + 'unittests-public' => "{$tmpPrefix}-public", + 'unittests-thumb' => "{$tmpPrefix}-thumb", + 'unittests-temp' => "{$tmpPrefix}-temp", + 'unittests-deleted' => "{$tmpPrefix}-deleted", + ) + ) ); + } + $this->repo = new FileRepo( array( + 'name' => 'unittests', + 'backend' => $backend + ) ); + + $this->date = gmdate( "YmdHis" ); + $this->createdFiles = array(); + } + + protected function tearDown() { + $this->repo->cleanupBatch( $this->createdFiles ); // delete files + foreach ( $this->createdFiles as $tmp ) { // delete dirs + $tmp = $this->repo->resolveVirtualUrl( $tmp ); + while ( $tmp = FileBackend::parentStoragePath( $tmp ) ) { + $this->repo->getBackend()->clean( array( 'dir' => $tmp ) ); + } + } + parent::tearDown(); + } + + /** + * Store a file or virtual URL source into a media file name. + * + * @param $originalName string The title of the image + * @param $srcPath string The filepath or virtual URL + * @param $flags integer Flags to pass into repo::store(). + * @return FileRepoStatus + */ + private function storeit( $originalName, $srcPath, $flags ) { + $hashPath = $this->repo->getHashPath( $originalName ); + $dstRel = "$hashPath{$this->date}!$originalName"; + $dstUrlRel = $hashPath . $this->date . '!' . rawurlencode( $originalName ); + + $result = $this->repo->store( $srcPath, 'temp', $dstRel, $flags ); + $result->value = $this->repo->getVirtualUrl( 'temp' ) . '/' . $dstUrlRel; + $this->createdFiles[] = $result->value; + + return $result; + } + + /** + * Test storing a file using different flags. + * + * @param $fn string The title of the image + * @param $infn string The name of the file (in the filesystem) + * @param $otherfn string The name of the different file (in the filesystem) + * @param $fromrepo bool 'true' if we want to copy from a virtual URL out of the Repo. + */ + private function storecohort( $fn, $infn, $otherfn, $fromrepo ) { + $f = $this->storeit( $fn, $infn, 0 ); + $this->assertTrue( $f->isOK(), 'failed to store a new file' ); + $this->assertEquals( $f->failCount, 0, "counts wrong {$f->successCount} {$f->failCount}" ); + $this->assertEquals( $f->successCount, 1, "counts wrong {$f->successCount} {$f->failCount}" ); + if ( $fromrepo ) { + $f = $this->storeit( "Other-$fn", $infn, FileRepo::OVERWRITE ); + $infn = $f->value; + } + // This should work because we're allowed to overwrite + $f = $this->storeit( $fn, $infn, FileRepo::OVERWRITE ); + $this->assertTrue( $f->isOK(), 'We should be allowed to overwrite' ); + $this->assertEquals( $f->failCount, 0, "counts wrong {$f->successCount} {$f->failCount}" ); + $this->assertEquals( $f->successCount, 1, "counts wrong {$f->successCount} {$f->failCount}" ); + // This should fail because we're overwriting. + $f = $this->storeit( $fn, $infn, 0 ); + $this->assertFalse( $f->isOK(), 'We should not be allowed to overwrite' ); + $this->assertEquals( $f->failCount, 1, "counts wrong {$f->successCount} {$f->failCount}" ); + $this->assertEquals( $f->successCount, 0, "counts wrong {$f->successCount} {$f->failCount}" ); + // This should succeed because we're overwriting the same content. + $f = $this->storeit( $fn, $infn, FileRepo::OVERWRITE_SAME ); + $this->assertTrue( $f->isOK(), 'We should be able to overwrite the same content' ); + $this->assertEquals( $f->failCount, 0, "counts wrong {$f->successCount} {$f->failCount}" ); + $this->assertEquals( $f->successCount, 1, "counts wrong {$f->successCount} {$f->failCount}" ); + // This should fail because we're overwriting different content. + if ( $fromrepo ) { + $f = $this->storeit( "Other-$fn", $otherfn, FileRepo::OVERWRITE ); + $otherfn = $f->value; + } + $f = $this->storeit( $fn, $otherfn, FileRepo::OVERWRITE_SAME ); + $this->assertFalse( $f->isOK(), 'We should not be allowed to overwrite different content' ); + $this->assertEquals( $f->failCount, 1, "counts wrong {$f->successCount} {$f->failCount}" ); + $this->assertEquals( $f->successCount, 0, "counts wrong {$f->successCount} {$f->failCount}" ); + } + + /** + * @covers FileRepo::store + */ + public function teststore() { + global $IP; + $this->storecohort( "Test1.png", "$IP/skins/monobook/wiki.png", "$IP/skins/monobook/video.png", false ); + $this->storecohort( "Test2.png", "$IP/skins/monobook/wiki.png", "$IP/skins/monobook/video.png", true ); + } +} diff --git a/tests/phpunit/includes/installer/InstallDocFormatterTest.php b/tests/phpunit/includes/installer/InstallDocFormatterTest.php new file mode 100644 index 00000000..0e5f2671 --- /dev/null +++ b/tests/phpunit/includes/installer/InstallDocFormatterTest.php @@ -0,0 +1,65 @@ +assertEquals( + $expected, + InstallDocFormatter::format( $unformattedText ), + $message + ); + } + + /** + * Provider for testFormat() + */ + public static function provideDocFormattingTests() { + # Format: (expected string, unformattedText string, optional message) + return array( + # Escape some wikitext + array( 'Install <tag>', 'Install ', 'Escaping <' ), + array( 'Install {{template}}', 'Install {{template}}', 'Escaping [[' ), + array( 'Install [[page]]', 'Install [[page]]', 'Escaping {{' ), + array( 'Install __TOC__', 'Install __TOC__', 'Escaping __' ), + array( 'Install ', "Install \r", 'Removing \r' ), + + # Transform \t{1,2} into :{1,2} + array( ':One indentation', "\tOne indentation", 'Replacing a single \t' ), + array( '::Two indentations', "\t\tTwo indentations", 'Replacing 2 x \t' ), + + # Transform 'bug 123' links + array( + '[https://bugzilla.wikimedia.org/123 bug 123]', + 'bug 123', 'Testing bug 123 links' ), + array( + '([https://bugzilla.wikimedia.org/987654 bug 987654])', + '(bug 987654)', 'Testing (bug 987654) links' ), + + # "bug abc" shouldn't work + array( 'bug foobar', 'bug foobar', "Don't match bug followed by non-digits" ), + array( 'bug !!fakefake!!', 'bug !!fakefake!!', "Don't match bug followed by non-digits" ), + + # Transform '$wgFooBar' links + array( + '[http://www.mediawiki.org/wiki/Manual:$wgFooBar $wgFooBar]', + '$wgFooBar', 'Testing basic $wgFooBar' ), + array( + '[http://www.mediawiki.org/wiki/Manual:$wgFooBar45 $wgFooBar45]', + '$wgFooBar45', 'Testing $wgFooBar45 (with numbers)' ), + array( + '[http://www.mediawiki.org/wiki/Manual:$wgFoo_Bar $wgFoo_Bar]', + '$wgFoo_Bar', 'Testing $wgFoo_Bar (with underscore)' ), + + # Icky variables that shouldn't link + array( '$myAwesomeVariable', '$myAwesomeVariable', 'Testing $myAwesomeVariable (not starting with $wg)' ), + array( '$()not!a&Var', '$()not!a&Var', 'Testing $()not!a&Var (obviously not a variable)' ), + ); + } +} diff --git a/tests/phpunit/includes/installer/OracleInstallerTest.php b/tests/phpunit/includes/installer/OracleInstallerTest.php new file mode 100644 index 00000000..66e65592 --- /dev/null +++ b/tests/phpunit/includes/installer/OracleInstallerTest.php @@ -0,0 +1,48 @@ +assertEquals( $expected, + OracleInstaller::checkConnectStringFormat( $connectString ), + $msg + ); + } + + /** + * Provider to test OracleInstaller::checkConnectStringFormat() + */ + function provideOracleConnectStrings() { + // expected result, connectString[, message] + return array( + array( true, 'simple_01', 'Simple TNS name' ), + array( true, 'simple_01.world', 'TNS name with domain' ), + array( true, 'simple_01.domain.net', 'TNS name with domain' ), + array( true, 'host123', 'Host only' ), + array( true, 'host123.domain.net', 'FQDN only' ), + array( true, '//host123.domain.net', 'FQDN URL only' ), + array( true, '123.223.213.132', 'Host IP only' ), + array( true, 'host:1521', 'Host and port' ), + array( true, 'host:1521/service', 'Host, port and service' ), + array( true, 'host:1521/service:shared', 'Host, port, service and shared server type' ), + array( true, 'host:1521/service:dedicated', 'Host, port, service and dedicated server type' ), + array( true, 'host:1521/service:pooled', 'Host, port, service and pooled server type' ), + array( true, 'host:1521/service:shared/instance1', 'Host, port, service, server type and instance' ), + array( true, 'host:1521//instance1', 'Host, port and instance' ), + ); + } + +} diff --git a/tests/phpunit/includes/jobqueue/JobQueueTest.php b/tests/phpunit/includes/jobqueue/JobQueueTest.php new file mode 100644 index 00000000..4e51c4fc --- /dev/null +++ b/tests/phpunit/includes/jobqueue/JobQueueTest.php @@ -0,0 +1,329 @@ +tablesUsed[] = 'job'; + } + + protected function setUp() { + global $wgJobTypeConf; + parent::setUp(); + + $this->setMwGlobals( 'wgMemc', new HashBagOStuff() ); + + if ( $this->getCliArg( 'use-jobqueue=' ) ) { + $name = $this->getCliArg( 'use-jobqueue=' ); + if ( !isset( $wgJobTypeConf[$name] ) ) { + throw new MWException( "No \$wgJobTypeConf entry for '$name'." ); + } + $baseConfig = $wgJobTypeConf[$name]; + } else { + $baseConfig = array( 'class' => 'JobQueueDB' ); + } + $baseConfig['type'] = 'null'; + $baseConfig['wiki'] = wfWikiID(); + $variants = array( + 'queueRand' => array( 'order' => 'random', 'claimTTL' => 0 ), + 'queueRandTTL' => array( 'order' => 'random', 'claimTTL' => 10 ), + 'queueTimestamp' => array( 'order' => 'timestamp', 'claimTTL' => 0 ), + 'queueTimestampTTL' => array( 'order' => 'timestamp', 'claimTTL' => 10 ), + 'queueFifo' => array( 'order' => 'fifo', 'claimTTL' => 0 ), + 'queueFifoTTL' => array( 'order' => 'fifo', 'claimTTL' => 10 ), + ); + foreach ( $variants as $q => $settings ) { + try { + $this->$q = JobQueue::factory( $settings + $baseConfig ); + if ( !( $this->$q instanceof JobQueueDB ) ) { + $this->$q->setTestingPrefix( 'unittests-' . wfRandomString( 32 ) ); + } + } catch ( MWException $e ) { + // unsupported? + // @todo What if it was another error? + }; + } + } + + protected function tearDown() { + parent::tearDown(); + foreach ( + array( + 'queueRand', 'queueRandTTL', 'queueTimestamp', 'queueTimestampTTL', + 'queueFifo', 'queueFifoTTL' + ) as $q + ) { + if ( $this->$q ) { + $this->$q->delete(); + } + $this->$q = null; + } + } + + /** + * @dataProvider provider_queueLists + */ + public function testProperties( $queue, $recycles, $desc ) { + $queue = $this->$queue; + if ( !$queue ) { + $this->markTestSkipped( $desc ); + } + + $this->assertEquals( wfWikiID(), $queue->getWiki(), "Proper wiki ID ($desc)" ); + $this->assertEquals( 'null', $queue->getType(), "Proper job type ($desc)" ); + } + + /** + * @dataProvider provider_queueLists + */ + public function testBasicOperations( $queue, $recycles, $desc ) { + $queue = $this->$queue; + if ( !$queue ) { + $this->markTestSkipped( $desc ); + } + + $this->assertTrue( $queue->isEmpty(), "Queue is empty ($desc)" ); + + $queue->flushCaches(); + $this->assertEquals( 0, $queue->getSize(), "Queue is empty ($desc)" ); + $this->assertEquals( 0, $queue->getAcquiredCount(), "Queue is empty ($desc)" ); + + $this->assertTrue( $queue->push( $this->newJob() ), "Push worked ($desc)" ); + $this->assertTrue( $queue->batchPush( array( $this->newJob() ) ), "Push worked ($desc)" ); + + $this->assertFalse( $queue->isEmpty(), "Queue is not empty ($desc)" ); + + $queue->flushCaches(); + $this->assertEquals( 2, $queue->getSize(), "Queue size is correct ($desc)" ); + $this->assertEquals( 0, $queue->getAcquiredCount(), "No jobs active ($desc)" ); + $jobs = iterator_to_array( $queue->getAllQueuedJobs() ); + $this->assertEquals( 2, count( $jobs ), "Queue iterator size is correct ($desc)" ); + + $job1 = $queue->pop(); + $this->assertFalse( $queue->isEmpty(), "Queue is not empty ($desc)" ); + + $queue->flushCaches(); + $this->assertEquals( 1, $queue->getSize(), "Queue size is correct ($desc)" ); + + $queue->flushCaches(); + if ( $recycles ) { + $this->assertEquals( 1, $queue->getAcquiredCount(), "Active job count ($desc)" ); + } else { + $this->assertEquals( 0, $queue->getAcquiredCount(), "Active job count ($desc)" ); + } + + $job2 = $queue->pop(); + $this->assertTrue( $queue->isEmpty(), "Queue is empty ($desc)" ); + $this->assertEquals( 0, $queue->getSize(), "Queue is empty ($desc)" ); + + $queue->flushCaches(); + if ( $recycles ) { + $this->assertEquals( 2, $queue->getAcquiredCount(), "Active job count ($desc)" ); + } else { + $this->assertEquals( 0, $queue->getAcquiredCount(), "Active job count ($desc)" ); + } + + $queue->ack( $job1 ); + + $queue->flushCaches(); + if ( $recycles ) { + $this->assertEquals( 1, $queue->getAcquiredCount(), "Active job count ($desc)" ); + } else { + $this->assertEquals( 0, $queue->getAcquiredCount(), "Active job count ($desc)" ); + } + + $queue->ack( $job2 ); + + $queue->flushCaches(); + $this->assertEquals( 0, $queue->getAcquiredCount(), "Active job count ($desc)" ); + + $this->assertTrue( $queue->batchPush( array( $this->newJob(), $this->newJob() ) ), + "Push worked ($desc)" ); + $this->assertFalse( $queue->isEmpty(), "Queue is not empty ($desc)" ); + + $queue->delete(); + $queue->flushCaches(); + $this->assertTrue( $queue->isEmpty(), "Queue is empty ($desc)" ); + $this->assertEquals( 0, $queue->getSize(), "Queue is empty ($desc)" ); + } + + /** + * @dataProvider provider_queueLists + */ + public function testBasicDeduplication( $queue, $recycles, $desc ) { + $queue = $this->$queue; + if ( !$queue ) { + $this->markTestSkipped( $desc ); + } + + $this->assertTrue( $queue->isEmpty(), "Queue is empty ($desc)" ); + + $queue->flushCaches(); + $this->assertEquals( 0, $queue->getSize(), "Queue is empty ($desc)" ); + $this->assertEquals( 0, $queue->getAcquiredCount(), "Queue is empty ($desc)" ); + + $this->assertTrue( + $queue->batchPush( + array( $this->newDedupedJob(), $this->newDedupedJob(), $this->newDedupedJob() ) + ), + "Push worked ($desc)" ); + + $this->assertFalse( $queue->isEmpty(), "Queue is not empty ($desc)" ); + + $queue->flushCaches(); + $this->assertEquals( 1, $queue->getSize(), "Queue size is correct ($desc)" ); + $this->assertEquals( 0, $queue->getAcquiredCount(), "No jobs active ($desc)" ); + + $this->assertTrue( + $queue->batchPush( + array( $this->newDedupedJob(), $this->newDedupedJob(), $this->newDedupedJob() ) + ), + "Push worked ($desc)" + ); + + $this->assertFalse( $queue->isEmpty(), "Queue is not empty ($desc)" ); + + $queue->flushCaches(); + $this->assertEquals( 1, $queue->getSize(), "Queue size is correct ($desc)" ); + $this->assertEquals( 0, $queue->getAcquiredCount(), "No jobs active ($desc)" ); + + $job1 = $queue->pop(); + $this->assertTrue( $queue->isEmpty(), "Queue is empty ($desc)" ); + + $queue->flushCaches(); + $this->assertEquals( 0, $queue->getSize(), "Queue is empty ($desc)" ); + if ( $recycles ) { + $this->assertEquals( 1, $queue->getAcquiredCount(), "Active job count ($desc)" ); + } else { + $this->assertEquals( 0, $queue->getAcquiredCount(), "Active job count ($desc)" ); + } + + $queue->ack( $job1 ); + + $queue->flushCaches(); + $this->assertEquals( 0, $queue->getAcquiredCount(), "Active job count ($desc)" ); + } + + /** + * @dataProvider provider_queueLists + */ + public function testRootDeduplication( $queue, $recycles, $desc ) { + $queue = $this->$queue; + if ( !$queue ) { + $this->markTestSkipped( $desc ); + } + + $this->assertTrue( $queue->isEmpty(), "Queue is empty ($desc)" ); + + $queue->flushCaches(); + $this->assertEquals( 0, $queue->getSize(), "Queue is empty ($desc)" ); + $this->assertEquals( 0, $queue->getAcquiredCount(), "Queue is empty ($desc)" ); + + $id = wfRandomString( 32 ); + $root1 = Job::newRootJobParams( "nulljobspam:$id" ); // task ID/timestamp + for ( $i = 0; $i < 5; ++$i ) { + $this->assertTrue( $queue->push( $this->newJob( 0, $root1 ) ), "Push worked ($desc)" ); + } + $queue->deduplicateRootJob( $this->newJob( 0, $root1 ) ); + sleep( 1 ); // roo job timestamp will increase + $root2 = Job::newRootJobParams( "nulljobspam:$id" ); // task ID/timestamp + $this->assertNotEquals( $root1['rootJobTimestamp'], $root2['rootJobTimestamp'], + "Root job signatures have different timestamps." ); + for ( $i = 0; $i < 5; ++$i ) { + $this->assertTrue( $queue->push( $this->newJob( 0, $root2 ) ), "Push worked ($desc)" ); + } + $queue->deduplicateRootJob( $this->newJob( 0, $root2 ) ); + + $this->assertFalse( $queue->isEmpty(), "Queue is not empty ($desc)" ); + + $queue->flushCaches(); + $this->assertEquals( 10, $queue->getSize(), "Queue size is correct ($desc)" ); + $this->assertEquals( 0, $queue->getAcquiredCount(), "No jobs active ($desc)" ); + + $dupcount = 0; + $jobs = array(); + do { + $job = $queue->pop(); + if ( $job ) { + $jobs[] = $job; + $queue->ack( $job ); + } + if ( $job instanceof DuplicateJob ) { + ++$dupcount; + } + } while ( $job ); + + $this->assertEquals( 10, count( $jobs ), "Correct number of jobs popped ($desc)" ); + $this->assertEquals( 5, $dupcount, "Correct number of duplicate jobs popped ($desc)" ); + } + + /** + * @dataProvider provider_fifoQueueLists + */ + public function testJobOrder( $queue, $recycles, $desc ) { + $queue = $this->$queue; + if ( !$queue ) { + $this->markTestSkipped( $desc ); + } + + $this->assertTrue( $queue->isEmpty(), "Queue is empty ($desc)" ); + + $queue->flushCaches(); + $this->assertEquals( 0, $queue->getSize(), "Queue is empty ($desc)" ); + $this->assertEquals( 0, $queue->getAcquiredCount(), "Queue is empty ($desc)" ); + + for ( $i = 0; $i < 10; ++$i ) { + $this->assertTrue( $queue->push( $this->newJob( $i ) ), "Push worked ($desc)" ); + } + + for ( $i = 0; $i < 10; ++$i ) { + $job = $queue->pop(); + $this->assertTrue( $job instanceof Job, "Jobs popped from queue ($desc)" ); + $params = $job->getParams(); + $this->assertEquals( $i, $params['i'], "Job popped from queue is FIFO ($desc)" ); + $queue->ack( $job ); + } + + $this->assertFalse( $queue->pop(), "Queue is not empty ($desc)" ); + + $queue->flushCaches(); + $this->assertEquals( 0, $queue->getSize(), "Queue is empty ($desc)" ); + $this->assertEquals( 0, $queue->getAcquiredCount(), "No jobs active ($desc)" ); + } + + public static function provider_queueLists() { + return array( + array( 'queueRand', false, 'Random queue without ack()' ), + array( 'queueRandTTL', true, 'Random queue with ack()' ), + array( 'queueTimestamp', false, 'Time ordered queue without ack()' ), + array( 'queueTimestampTTL', true, 'Time ordered queue with ack()' ), + array( 'queueFifo', false, 'FIFO ordered queue without ack()' ), + array( 'queueFifoTTL', true, 'FIFO ordered queue with ack()' ) + ); + } + + public static function provider_fifoQueueLists() { + return array( + array( 'queueFifo', false, 'Ordered queue without ack()' ), + array( 'queueFifoTTL', true, 'Ordered queue with ack()' ) + ); + } + + function newJob( $i = 0, $rootJob = array() ) { + return new NullJob( Title::newMainPage(), + array( 'lives' => 0, 'usleep' => 0, 'removeDuplicates' => 0, 'i' => $i ) + $rootJob ); + } + + function newDedupedJob( $i = 0, $rootJob = array() ) { + return new NullJob( Title::newMainPage(), + array( 'lives' => 0, 'usleep' => 0, 'removeDuplicates' => 1, 'i' => $i ) + $rootJob ); + } +} diff --git a/tests/phpunit/includes/json/FormatJsonTest.php b/tests/phpunit/includes/json/FormatJsonTest.php new file mode 100644 index 00000000..149be05b --- /dev/null +++ b/tests/phpunit/includes/json/FormatJsonTest.php @@ -0,0 +1,161 @@ + new stdClass, + 'emptyArray' => array(), + 'string' => 'foobar\\', + 'filledArray' => array( + array( + 123, + 456, + ), + // Nested json works without problems + '"7":["8",{"9":"10"}]', + // Whitespace clean up doesn't touch strings that look alike + "{\n\t\"emptyObject\": {\n\t},\n\t\"emptyArray\": [ ]\n}", + ), + ); + + // 4 space indent, no trailing whitespace, no trailing linefeed + $json = '{ + "emptyObject": {}, + "emptyArray": [], + "string": "foobar\\\\", + "filledArray": [ + [ + 123, + 456 + ], + "\"7\":[\"8\",{\"9\":\"10\"}]", + "{\n\t\"emptyObject\": {\n\t},\n\t\"emptyArray\": [ ]\n}" + ] +}'; + + $json = str_replace( "\r", '', $json ); // Windows compat + $this->assertSame( $json, FormatJson::encode( $obj, true ) ); + } + + public static function provideEncodeDefault() { + return self::getEncodeTestCases( array() ); + } + + /** + * @dataProvider provideEncodeDefault + */ + public function testEncodeDefault( $from, $to ) { + $this->assertSame( $to, FormatJson::encode( $from ) ); + } + + public static function provideEncodeUtf8() { + return self::getEncodeTestCases( array( 'unicode' ) ); + } + + /** + * @dataProvider provideEncodeUtf8 + */ + public function testEncodeUtf8( $from, $to ) { + $this->assertSame( $to, FormatJson::encode( $from, false, FormatJson::UTF8_OK ) ); + } + + public static function provideEncodeXmlMeta() { + return self::getEncodeTestCases( array( 'xmlmeta' ) ); + } + + /** + * @dataProvider provideEncodeXmlMeta + */ + public function testEncodeXmlMeta( $from, $to ) { + $this->assertSame( $to, FormatJson::encode( $from, false, FormatJson::XMLMETA_OK ) ); + } + + public static function provideEncodeAllOk() { + return self::getEncodeTestCases( array( 'unicode', 'xmlmeta' ) ); + } + + /** + * @dataProvider provideEncodeAllOk + */ + public function testEncodeAllOk( $from, $to ) { + $this->assertSame( $to, FormatJson::encode( $from, false, FormatJson::ALL_OK ) ); + } + + public function testEncodePhpBug46944() { + $this->assertNotEquals( + '\ud840\udc00', + strtolower( FormatJson::encode( "\xf0\xa0\x80\x80" ) ), + 'Test encoding an broken json_encode character (U+20000)' + ); + } + + public function testDecodeReturnType() { + $this->assertInternalType( + 'object', + FormatJson::decode( '{"Name": "Cheeso", "Rank": 7}' ), + 'Default to object' + ); + + $this->assertInternalType( + 'array', + FormatJson::decode( '{"Name": "Cheeso", "Rank": 7}', true ), + 'Optional array' + ); + } + + /** + * Generate a set of test cases for a particular combination of encoder options. + * + * @param array $unescapedGroups List of character groups to leave unescaped + * @return array: Arrays of unencoded strings and corresponding encoded strings + */ + private static function getEncodeTestCases( array $unescapedGroups ) { + $groups = array( + 'always' => array( + // Forward slash (always unescaped) + '/' => '/', + + // Control characters + "\0" => '\u0000', + "\x08" => '\b', + "\t" => '\t', + "\n" => '\n', + "\r" => '\r', + "\f" => '\f', + "\x1f" => '\u001f', // representative example + + // Double quotes + '"' => '\"', + + // Backslashes + '\\' => '\\\\', + '\\\\' => '\\\\\\\\', + '\\u00e9' => '\\\u00e9', // security check for Unicode unescaping + + // Line terminators + "\xe2\x80\xa8" => '\u2028', + "\xe2\x80\xa9" => '\u2029', + ), + 'unicode' => array( + "\xc3\xa9" => '\u00e9', + "\xf0\x9d\x92\x9e" => '\ud835\udc9e', // U+1D49E, outside the BMP + ), + 'xmlmeta' => array( + '<' => '\u003C', // JSON_HEX_TAG uses uppercase hex digits + '>' => '\u003E', + '&' => '\u0026', + ), + ); + + $cases = array(); + foreach ( $groups as $name => $rules ) { + $leaveUnescaped = in_array( $name, $unescapedGroups ); + foreach ( $rules as $from => $to ) { + $cases[] = array( $from, '"' . ( $leaveUnescaped ? $from : $to ) . '"' ); + } + } + + return $cases; + } +} diff --git a/tests/phpunit/includes/libs/CSSJanusTest.php b/tests/phpunit/includes/libs/CSSJanusTest.php new file mode 100644 index 00000000..5a3c1619 --- /dev/null +++ b/tests/phpunit/includes/libs/CSSJanusTest.php @@ -0,0 +1,606 @@ +assertEquals( $transformedA, $cssB, 'Test A-B transformation' ); + + $transformedB = CSSJanus::transform( $cssB ); + $this->assertEquals( $transformedB, $cssA, 'Test B-A transformation' ); + } else { + // If no B version is provided, it means + // the output should equal the input. + $transformedA = CSSJanus::transform( $cssA ); + $this->assertEquals( $transformedA, $cssA, 'Nothing was flipped' ); + } + } + + /** + * @dataProvider provideTransformAdvancedCases + */ + public function testTransformAdvanced( $code, $expectedOutput, $options = array() ) { + $swapLtrRtlInURL = isset( $options['swapLtrRtlInURL'] ) ? $options['swapLtrRtlInURL'] : false; + $swapLeftRightInURL = isset( $options['swapLeftRightInURL'] ) ? $options['swapLeftRightInURL'] : false; + + $flipped = CSSJanus::transform( $code, $swapLtrRtlInURL, $swapLeftRightInURL ); + + $this->assertEquals( $expectedOutput, $flipped, + 'Test flipping, options: url-ltr-rtl=' . ( $swapLtrRtlInURL ? 'true' : 'false' ) + . ' url-left-right=' . ( $swapLeftRightInURL ? 'true' : 'false' ) + ); + } + + /** + * @dataProvider provideTransformBrokenCases + * @group Broken + */ + public function testTransformBroken( $code, $expectedOutput ) { + $flipped = CSSJanus::transform( $code ); + + $this->assertEquals( $expectedOutput, $flipped, 'Test flipping' ); + } + + /** + * These transform cases are tested *in both directions* + * No need to declare a principle twice in both directions here. + */ + public static function provideTransformCases() { + return array( + // Property keys + array( + '.foo { left: 0; }', + '.foo { right: 0; }' + ), + // Guard against partial keys + // (CSS currently doesn't have flippable properties + // that contain the direction as part of the key without + // dash separation) + array( + '.foo { alright: 0; }' + ), + array( + '.foo { balleft: 0; }' + ), + + // Dashed property keys + array( + '.foo { padding-left: 0; }', + '.foo { padding-right: 0; }' + ), + array( + '.foo { margin-left: 0; }', + '.foo { margin-right: 0; }' + ), + array( + '.foo { border-left: 0; }', + '.foo { border-right: 0; }' + ), + + // Double-dashed property keys + array( + '.foo { border-left-color: red; }', + '.foo { border-right-color: red; }' + ), + array( + // Includes unknown properties? + '.foo { x-left-y: 0; }', + '.foo { x-right-y: 0; }' + ), + + // Multi-value properties + array( + '.foo { padding: 0; }' + ), + array( + '.foo { padding: 0 1px; }' + ), + array( + '.foo { padding: 0 1px 2px; }' + ), + array( + '.foo { padding: 0 1px 2px 3px; }', + '.foo { padding: 0 3px 2px 1px; }' + ), + + // Shorthand / Four notation + array( + '.foo { padding: .25em 15px 0pt 0ex; }', + '.foo { padding: .25em 0ex 0pt 15px; }' + ), + array( + '.foo { margin: 1px -4px 3px 2px; }', + '.foo { margin: 1px 2px 3px -4px; }' + ), + array( + '.foo { padding: 0 15px .25em 0; }', + '.foo { padding: 0 0 .25em 15px; }' + ), + array( + '.foo { padding: 1px 4.1grad 3px 2%; }', + '.foo { padding: 1px 2% 3px 4.1grad; }' + ), + array( + '.foo { padding: 1px 2px 3px auto; }', + '.foo { padding: 1px auto 3px 2px; }' + ), + array( + '.foo { padding: 1px inherit 3px auto; }', + '.foo { padding: 1px auto 3px inherit; }' + ), + // border-radius assigns different meanings to the values + array( + '.foo { border-radius: .25em 15px 0pt 0ex; }', + '.foo { border-radius: 15px .25em 0ex 0pt; }' + ), + array( + '.foo { border-radius: 0px 0px 5px 5px; }', + ), + // Ensure the rule doesn't break other stuff + array( + '.foo { x-unknown: a b c d; }' + ), + array( + '.foo barpx 0 2% { opacity: 0; }' + ), + array( + '#settings td p strong' + ), + array( + // Color names + '.foo { border-color: red green blue white }', + '.foo { border-color: red white blue green }', + ), + array( + // Color name, hexdecimal, RGB & RGBA + '.foo { border-color: red #f00 rgb(255, 0, 0) rgba(255, 0, 0, 0.5) }', + '.foo { border-color: red rgba(255, 0, 0, 0.5) rgb(255, 0, 0) #f00 }', + ), + array( + // Color name, hexdecimal, HSL & HSLA + '.foo { border-color: red #f00 hsl(0, 100%, 50%) hsla(0, 100%, 50%, 0.5) }', + '.foo { border-color: red hsla(0, 100%, 50%, 0.5) hsl(0, 100%, 50%) #f00 }', + ), + array( + // Do not mangle 5 or more values + '.foo { -x-unknown: 1 2 3 4 5; }' + ), + array( + '.foo { -x-unknown: 1 2 3 4 5 6; }' + ), + + // Shorthand / Three notation + array( + '.foo { margin: 1em 0 .25em; }' + ), + array( + '.foo { margin:-1.5em 0 -.75em; }' + ), + + // Shorthand / Two notation + array( + '.foo { padding: 1px 2px; }' + ), + + // Shorthand / One notation + array( + '.foo { padding: 1px; }' + ), + + // text-shadow and box-shadow + array( + '.foo { box-shadow: -6px 3px 8px 5px rgba(0, 0, 0, 0.25); }', + '.foo { box-shadow: 6px 3px 8px 5px rgba(0, 0, 0, 0.25); }', + ), + array( + '.foo { box-shadow: inset -6px 3px 8px 5px rgba(0, 0, 0, 0.25); }', + '.foo { box-shadow: inset 6px 3px 8px 5px rgba(0, 0, 0, 0.25); }', + ), + array( + '.foo { text-shadow: orange 2px 0; }', + '.foo { text-shadow: orange -2px 0; }', + ), + array( + '.foo { text-shadow: 2px 0 orange; }', + '.foo { text-shadow: -2px 0 orange; }', + ), + array( + // Don't mangle zeroes + '.foo { text-shadow: orange 0 2px; }' + ), + + // Direction + // Note: This differs from the Python implementation, + // see also CSSJanus::fixDirection for more info. + array( + '.foo { direction: ltr; }', + '.foo { direction: rtl; }' + ), + array( + '.foo { direction: rtl; }', + '.foo { direction: ltr; }' + ), + array( + 'input { direction: ltr; }', + 'input { direction: rtl; }' + ), + array( + 'input { direction: rtl; }', + 'input { direction: ltr; }' + ), + array( + 'body { direction: ltr; }', + 'body { direction: rtl; }' + ), + array( + '.foo, body, input { direction: ltr; }', + '.foo, body, input { direction: rtl; }' + ), + array( + 'body { padding: 10px; direction: ltr; }', + 'body { padding: 10px; direction: rtl; }' + ), + array( + 'body { direction: ltr } .myClass { direction: ltr }', + 'body { direction: rtl } .myClass { direction: rtl }' + ), + + // Left/right values + array( + '.foo { float: left; }', + '.foo { float: right; }' + ), + array( + '.foo { text-align: left; }', + '.foo { text-align: right; }' + ), + array( + '.foo { -x-unknown: left; }', + '.foo { -x-unknown: right; }' + ), + // Guard against selectors that look flippable + array( + '.column-left { width: 0; }' + ), + array( + 'a.left { width: 0; }' + ), + array( + 'a.leftification { width: 0; }' + ), + array( + 'a.ltr { width: 0; }' + ), + array( + #
+ '.a-ltr.png { width: 0; }' + ), + array( + # + 'foo-ltr[attr="x"] { width: 0; }' + ), + array( + 'div.left > span.right+span.left { width: 0; }' + ), + array( + '.thisclass .left .myclass { width: 0; }' + ), + array( + '.thisclass .left .myclass #myid { width: 0; }' + ), + + // Cursor values (east/west) + array( + '.foo { cursor: e-resize; }', + '.foo { cursor: w-resize; }' + ), + array( + '.foo { cursor: se-resize; }', + '.foo { cursor: sw-resize; }' + ), + array( + '.foo { cursor: ne-resize; }', + '.foo { cursor: nw-resize; }' + ), + + // Background + array( + '.foo { background-position: top left; }', + '.foo { background-position: top right; }' + ), + array( + '.foo { background: url(/foo/bar.png) top left; }', + '.foo { background: url(/foo/bar.png) top right; }' + ), + array( + '.foo { background: url(/foo/bar.png) top left no-repeat; }', + '.foo { background: url(/foo/bar.png) top right no-repeat; }' + ), + array( + '.foo { background: url(/foo/bar.png) no-repeat top left; }', + '.foo { background: url(/foo/bar.png) no-repeat top right; }' + ), + array( + '.foo { background: #fff url(/foo/bar.png) no-repeat top left; }', + '.foo { background: #fff url(/foo/bar.png) no-repeat top right; }' + ), + array( + '.foo { background-position: 100% 40%; }', + '.foo { background-position: 0% 40%; }' + ), + array( + '.foo { background-position: 23% 0; }', + '.foo { background-position: 77% 0; }' + ), + array( + '.foo { background-position: 23% auto; }', + '.foo { background-position: 77% auto; }' + ), + array( + '.foo { background-position-x: 23%; }', + '.foo { background-position-x: 77%; }' + ), + array( + '.foo { background-position-y: 23%; }', + '.foo { background-position-y: 23%; }' + ), + array( + '.foo { background:url(../foo.png) no-repeat 75% 50%; }', + '.foo { background:url(../foo.png) no-repeat 25% 50%; }' + ), + array( + '.foo { background: 10% 20% } .bar { background: 40% 30% }', + '.foo { background: 90% 20% } .bar { background: 60% 30% }' + ), + + // Multiple rules + array( + 'body { direction: rtl; float: right; } .foo { direction: ltr; float: right; }', + 'body { direction: ltr; float: left; } .foo { direction: rtl; float: left; }', + ), + + // Duplicate properties + array( + '.foo { float: left; float: right; float: left; }', + '.foo { float: right; float: left; float: right; }', + ), + + // Preserve comments + array( + '/* left /* right */left: 10px', + '/* left /* right */right: 10px' + ), + array( + '/*left*//*left*/left: 10px', + '/*left*//*left*/right: 10px' + ), + array( + '/* Going right is cool */ .foo { width: 0 }', + ), + array( + "/* padding-right 1 2 3 4 */\n#test { width: 0}\n/*right*/" + ), + array( + "/** Two line comment\n * left\n \*/\n#test {width: 0}" + ), + + // @noflip annotation + array( + // before selector (single) + '/* @noflip */ div { float: left; }' + ), + array( + // before selector (multiple) + '/* @noflip */ div, .notme { float: left; }' + ), + array( + // inside selector + 'div, /* @noflip */ .foo { float: left; }' + ), + array( + // after selector + 'div, .notme /* @noflip */ { float: left; }' + ), + array( + // before multiple rules + '/* @noflip */ div { float: left; } .foo { float: left; }', + '/* @noflip */ div { float: left; } .foo { float: right; }' + ), + array( + // support parentheses in selector + '/* @noflip */ .test:not(:first) { margin-right: -0.25em; margin-left: 0.25em; }', + '/* @noflip */ .test:not(:first) { margin-right: -0.25em; margin-left: 0.25em; }' + ), + array( + // after multiple rules + '.foo { float: left; } /* @noflip */ div { float: left; }', + '.foo { float: right; } /* @noflip */ div { float: left; }' + ), + array( + // before multiple properties + 'div { /* @noflip */ float: left; text-align: left; }', + 'div { /* @noflip */ float: left; text-align: right; }' + ), + array( + // after multiple properties + 'div { float: left; /* @noflip */ text-align: left; }', + 'div { float: right; /* @noflip */ text-align: left; }' + ), + + // Guard against css3 stuff + array( + 'background-image: -moz-linear-gradient(#326cc1, #234e8c);' + ), + array( + 'background-image: -webkit-gradient(linear, 100% 0%, 0% 0%, from(#666666), to(#ffffff));' + ), + + // CSS syntax / white-space variations + // spaces, no spaces, tabs, new lines, omitting semi-colons + array( + ".foo { left: 0; }", + ".foo { right: 0; }" + ), + array( + ".foo{ left: 0; }", + ".foo{ right: 0; }" + ), + array( + ".foo{ left: 0 }", + ".foo{ right: 0 }" + ), + array( + ".foo{left:0 }", + ".foo{right:0 }" + ), + array( + ".foo{left:0}", + ".foo{right:0}" + ), + array( + ".foo { left : 0 ; }", + ".foo { right : 0 ; }" + ), + array( + ".foo\n { left : 0 ; }", + ".foo\n { right : 0 ; }" + ), + array( + ".foo\n { \nleft : 0 ; }", + ".foo\n { \nright : 0 ; }" + ), + array( + ".foo\n { \n left : 0 ; }", + ".foo\n { \n right : 0 ; }" + ), + array( + ".foo\n { \n left\n : 0; }", + ".foo\n { \n right\n : 0; }" + ), + array( + ".foo \n { \n left\n : 0; }", + ".foo \n { \n right\n : 0; }" + ), + array( + ".foo\n{\nleft\n:\n0;}", + ".foo\n{\nright\n:\n0;}" + ), + array( + ".foo\n.bar {\n\tleft: 0;\n}", + ".foo\n.bar {\n\tright: 0;\n}" + ), + array( + ".foo\t{\tleft\t:\t0;}", + ".foo\t{\tright\t:\t0;}" + ), + + // Guard against partial keys + array( + '.foo { leftxx: 0; }', + '.foo { leftxx: 0; }' + ), + array( + '.foo { rightxx: 0; }', + '.foo { rightxx: 0; }' + ), + ); + } + + /** + * These cases are tested in one way only (format: actual, expected, msg). + * If both ways can be tested, either put both versions in here or move + * it to provideTransformCases(). + */ + public static function provideTransformAdvancedCases() { + $bgPairs = array( + # [ - _ . ] <-> [ left right ltr rtl ] + 'foo.jpg' => 'foo.jpg', + 'left.jpg' => 'right.jpg', + 'ltr.jpg' => 'rtl.jpg', + + 'foo-left.png' => 'foo-right.png', + 'foo_left.png' => 'foo_right.png', + 'foo.left.png' => 'foo.right.png', + + 'foo-ltr.png' => 'foo-rtl.png', + 'foo_ltr.png' => 'foo_rtl.png', + 'foo.ltr.png' => 'foo.rtl.png', + + 'left-foo.png' => 'right-foo.png', + 'left_foo.png' => 'right_foo.png', + 'left.foo.png' => 'right.foo.png', + + 'ltr-foo.png' => 'rtl-foo.png', + 'ltr_foo.png' => 'rtl_foo.png', + 'ltr.foo.png' => 'rtl.foo.png', + + 'foo-ltr-left.gif' => 'foo-rtl-right.gif', + 'foo_ltr_left.gif' => 'foo_rtl_right.gif', + 'foo.ltr.left.gif' => 'foo.rtl.right.gif', + 'foo-ltr_left.gif' => 'foo-rtl_right.gif', + 'foo_ltr.left.gif' => 'foo_rtl.right.gif', + ); + $provider = array(); + foreach ( $bgPairs as $left => $right ) { + # By default '-rtl' and '-left' etc. are not touched, + # Only when the appropiate parameter is set. + $provider[] = array( + ".foo { background: url(images/$left); }", + ".foo { background: url(images/$left); }" + ); + $provider[] = array( + ".foo { background: url(images/$right); }", + ".foo { background: url(images/$right); }" + ); + $provider[] = array( + ".foo { background: url(images/$left); }", + ".foo { background: url(images/$right); }", + array( + 'swapLtrRtlInURL' => true, + 'swapLeftRightInURL' => true, + ) + ); + $provider[] = array( + ".foo { background: url(images/$right); }", + ".foo { background: url(images/$left); }", + array( + 'swapLtrRtlInURL' => true, + 'swapLeftRightInURL' => true, + ) + ); + } + + return $provider; + } + + /** + * Cases that are currently failing, but + * should be looked at in the future as enhancements and/or bug fix + */ + public static function provideTransformBrokenCases() { + return array( + // Guard against selectors that look flippable + array( + # + 'foo-left-x[attr="x"] { width: 0; }', + 'foo-left-x[attr="x"] { width: 0; }' + ), + array( + #
+ '.foo[data-left="x"] { width: 0; }', + '.foo[data-left="x"] { width: 0; }' + ), + ); + } +} diff --git a/tests/phpunit/includes/libs/CSSMinTest.php b/tests/phpunit/includes/libs/CSSMinTest.php new file mode 100644 index 00000000..951dd7b9 --- /dev/null +++ b/tests/phpunit/includes/libs/CSSMinTest.php @@ -0,0 +1,133 @@ +setMwGlobals( array( + 'wgServer' => $server, + 'wgCanonicalServer' => $server, + ) ); + } + + /** + * @dataProvider provideMinifyCases + */ + public function testMinify( $code, $expectedOutput ) { + $minified = CSSMin::minify( $code ); + + $this->assertEquals( $expectedOutput, $minified, 'Minified output should be in the form expected.' ); + } + + public static function provideMinifyCases() { + return array( + // Whitespace + array( "\r\t\f \v\n\r", "" ), + array( "foo, bar {\n\tprop: value;\n}", "foo,bar{prop:value}" ), + + // Loose comments + array( "/* foo */", "" ), + array( "/*******\n foo\n *******/", "" ), + array( "/*!\n foo\n */", "" ), + + // Inline comments in various different places + array( "/* comment */foo, bar {\n\tprop: value;\n}", "foo,bar{prop:value}" ), + array( "foo/* comment */, bar {\n\tprop: value;\n}", "foo,bar{prop:value}" ), + array( "foo,/* comment */ bar {\n\tprop: value;\n}", "foo,bar{prop:value}" ), + array( "foo, bar/* comment */ {\n\tprop: value;\n}", "foo,bar{prop:value}" ), + array( "foo, bar {\n\t/* comment */prop: value;\n}", "foo,bar{prop:value}" ), + array( "foo, bar {\n\tprop: /* comment */value;\n}", "foo,bar{prop:value}" ), + array( "foo, bar {\n\tprop: value /* comment */;\n}", "foo,bar{prop:value }" ), + array( "foo, bar {\n\tprop: value; /* comment */\n}", "foo,bar{prop:value; }" ), + + // Keep track of things that aren't as minified as much as they + // could be (bug 35493) + array( 'foo { prop: value ;}', 'foo{prop:value }' ), + array( 'foo { prop : value; }', 'foo{prop :value}' ), + array( 'foo { prop: value ; }', 'foo{prop:value }' ), + array( 'foo { font-family: "foo" , "bar"; }', 'foo{font-family:"foo" ,"bar"}' ), + array( "foo { src:\n\turl('foo') ,\n\turl('bar') ; }", "foo{src:url('foo') ,url('bar') }" ), + + // Interesting cases with string values + // - Double quotes, single quotes + array( 'foo { content: ""; }', 'foo{content:""}' ), + array( "foo { content: ''; }", "foo{content:''}" ), + array( 'foo { content: "\'"; }', 'foo{content:"\'"}' ), + array( "foo { content: '\"'; }", "foo{content:'\"'}" ), + // - Whitespace in string values + array( 'foo { content: " "; }', 'foo{content:" "}' ), + ); + } + + /** + * @dataProvider provideRemapCases + */ + public function testRemap( $message, $params, $expectedOutput ) { + $remapped = call_user_func_array( 'CSSMin::remap', $params ); + + $messageAdd = " Case: $message"; + $this->assertEquals( $expectedOutput, $remapped, 'CSSMin::remap should return the expected url form.' . $messageAdd ); + } + + public static function provideRemapCases() { + // Parameter signature: + // CSSMin::remap( $code, $local, $remote, $embedData = true ) + return array( + array( + 'Simple case', + array( 'foo { prop: url(bar.png); }', false, 'http://example.org', false ), + 'foo { prop: url(http://example.org/bar.png); }', + ), + array( + 'Without trailing slash', + array( 'foo { prop: url(../bar.png); }', false, 'http://example.org/quux', false ), + 'foo { prop: url(http://example.org/quux/../bar.png); }', + ), + array( + 'With trailing slash on remote (bug 27052)', + array( 'foo { prop: url(../bar.png); }', false, 'http://example.org/quux/', false ), + 'foo { prop: url(http://example.org/quux/../bar.png); }', + ), + array( + 'Guard against stripping double slashes from query', + array( 'foo { prop: url(bar.png?corge=//grault); }', false, 'http://example.org/quux/', false ), + 'foo { prop: url(http://example.org/quux/bar.png?corge=//grault); }', + ), + array( + 'Expand absolute paths', + array( 'foo { prop: url(/w/skin/images/bar.png); }', false, 'http://example.org/quux', false ), + 'foo { prop: url(http://doc.example.org/w/skin/images/bar.png); }', + ), + ); + } + + /** + * Seperated because they are currently broken (bug 35492) + * + * @group Broken + * @dataProvider provideStringCases + */ + public function testMinifyWithCSSStringValues( $code, $expectedOutput ) { + $this->testMinifyOutput( $code, $expectedOutput ); + } + + public static function provideStringCases() { + return array( + // String values should be respected + // - More than one space in a string value + array( 'foo { content: " "; }', 'foo{content:" "}' ), + // - Using a tab in a string value (turns into a space) + array( "foo { content: '\t'; }", "foo{content:'\t'}" ), + // - Using css-like syntax in string values + array( 'foo::after { content: "{;}"; position: absolute; }', 'foo::after{content:"{;}";position:absolute}' ), + ); + } +} diff --git a/tests/phpunit/includes/libs/GenericArrayObjectTest.php b/tests/phpunit/includes/libs/GenericArrayObjectTest.php new file mode 100644 index 00000000..7436c43c --- /dev/null +++ b/tests/phpunit/includes/libs/GenericArrayObjectTest.php @@ -0,0 +1,263 @@ + + */ +abstract class GenericArrayObjectTest extends MediaWikiTestCase { + + /** + * Returns objects that can serve as elements in the concrete GenericArrayObject deriving class being tested. + * + * @since 1.20 + * + * @return array + */ + abstract public function elementInstancesProvider(); + + /** + * Returns the name of the concrete class being tested. + * + * @since 1.20 + * + * @return string + */ + abstract public function getInstanceClass(); + + /** + * Provides instances of the concrete class being tested. + * + * @since 1.20 + * + * @return array + */ + public function instanceProvider() { + $instances = array(); + + foreach ( $this->elementInstancesProvider() as $elementInstances ) { + $instances[] = $this->getNew( $elementInstances[0] ); + } + + return $this->arrayWrap( $instances ); + } + + /** + * @since 1.20 + * + * @param array $elements + * + * @return GenericArrayObject + */ + protected function getNew( array $elements = array() ) { + $class = $this->getInstanceClass(); + + return new $class( $elements ); + } + + /** + * @dataProvider elementInstancesProvider + * + * @since 1.20 + * + * @param array $elements + */ + public function testConstructor( array $elements ) { + $arrayObject = $this->getNew( $elements ); + + $this->assertEquals( count( $elements ), $arrayObject->count() ); + } + + /** + * @dataProvider elementInstancesProvider + * + * @since 1.20 + * + * @param array $elements + */ + public function testIsEmpty( array $elements ) { + $arrayObject = $this->getNew( $elements ); + + $this->assertEquals( $elements === array(), $arrayObject->isEmpty() ); + } + + /** + * @dataProvider instanceProvider + * + * @since 1.20 + * + * @param GenericArrayObject $list + */ + public function testUnset( GenericArrayObject $list ) { + if ( $list->isEmpty() ) { + $this->assertTrue( true ); // We cannot test unset if there are no elements + } else { + $offset = $list->getIterator()->key(); + $count = $list->count(); + $list->offsetUnset( $offset ); + $this->assertEquals( $count - 1, $list->count() ); + } + + if ( !$list->isEmpty() ) { + $offset = $list->getIterator()->key(); + $count = $list->count(); + unset( $list[$offset] ); + $this->assertEquals( $count - 1, $list->count() ); + } + } + + /** + * @dataProvider elementInstancesProvider + * + * @since 1.20 + * + * @param array $elements + */ + public function testAppend( array $elements ) { + $list = $this->getNew(); + + $listSize = count( $elements ); + + foreach ( $elements as $element ) { + $list->append( $element ); + } + + $this->assertEquals( $listSize, $list->count() ); + + $list = $this->getNew(); + + foreach ( $elements as $element ) { + $list[] = $element; + } + + $this->assertEquals( $listSize, $list->count() ); + + $this->checkTypeChecks( function ( GenericArrayObject $list, $element ) { + $list->append( $element ); + } ); + } + + /** + * @since 1.20 + * + * @param callback $function + */ + protected function checkTypeChecks( $function ) { + $excption = null; + $list = $this->getNew(); + + $elementClass = $list->getObjectType(); + + foreach ( array( 42, 'foo', array(), new stdClass(), 4.2 ) as $element ) { + $validValid = $element instanceof $elementClass; + + try { + call_user_func( $function, $list, $element ); + $valid = true; + } catch ( InvalidArgumentException $exception ) { + $valid = false; + } + + $this->assertEquals( + $validValid, + $valid, + 'Object of invalid type got successfully added to a GenericArrayObject' + ); + } + } + + /** + * @dataProvider elementInstancesProvider + * + * @since 1.20 + * + * @param array $elements + */ + public function testOffsetSet( array $elements ) { + if ( $elements === array() ) { + $this->assertTrue( true ); + + return; + } + + $list = $this->getNew(); + + $element = reset( $elements ); + $list->offsetSet( 42, $element ); + $this->assertEquals( $element, $list->offsetGet( 42 ) ); + + $list = $this->getNew(); + + $element = reset( $elements ); + $list['oHai'] = $element; + $this->assertEquals( $element, $list['oHai'] ); + + $list = $this->getNew(); + + $element = reset( $elements ); + $list->offsetSet( 9001, $element ); + $this->assertEquals( $element, $list[9001] ); + + $list = $this->getNew(); + + $element = reset( $elements ); + $list->offsetSet( null, $element ); + $this->assertEquals( $element, $list[0] ); + + $list = $this->getNew(); + $offset = 0; + + foreach ( $elements as $element ) { + $list->offsetSet( null, $element ); + $this->assertEquals( $element, $list[$offset++] ); + } + + $this->assertEquals( count( $elements ), $list->count() ); + + $this->checkTypeChecks( function ( GenericArrayObject $list, $element ) { + $list->offsetSet( mt_rand(), $element ); + } ); + } + + /** + * @dataProvider instanceProvider + * + * @since 1.21 + * + * @param GenericArrayObject $list + */ + public function testSerialization( GenericArrayObject $list ) { + $serialization = serialize( $list ); + $copy = unserialize( $serialization ); + + $this->assertEquals( $serialization, serialize( $copy ) ); + $this->assertEquals( count( $list ), count( $copy ) ); + + $list = $list->getArrayCopy(); + $copy = $copy->getArrayCopy(); + + $this->assertArrayEquals( $list, $copy, true, true ); + } +} diff --git a/tests/phpunit/includes/libs/IEUrlExtensionTest.php b/tests/phpunit/includes/libs/IEUrlExtensionTest.php new file mode 100644 index 00000000..66fe915a --- /dev/null +++ b/tests/phpunit/includes/libs/IEUrlExtensionTest.php @@ -0,0 +1,126 @@ +assertEquals( + 'y', + IEUrlExtension::findIE6Extension( 'x.y' ), + 'Simple extension' + ); + } + + public function testSimpleNoExt() { + $this->assertEquals( + '', + IEUrlExtension::findIE6Extension( 'x' ), + 'No extension' + ); + } + + public function testEmpty() { + $this->assertEquals( + '', + IEUrlExtension::findIE6Extension( '' ), + 'Empty string' + ); + } + + public function testQuestionMark() { + $this->assertEquals( + '', + IEUrlExtension::findIE6Extension( '?' ), + 'Question mark only' + ); + } + + public function testExtQuestionMark() { + $this->assertEquals( + 'x', + IEUrlExtension::findIE6Extension( '.x?' ), + 'Extension then question mark' + ); + } + + public function testQuestionMarkExt() { + $this->assertEquals( + 'x', + IEUrlExtension::findIE6Extension( '?.x' ), + 'Question mark then extension' + ); + } + + public function testInvalidChar() { + $this->assertEquals( + '', + IEUrlExtension::findIE6Extension( '.x*' ), + 'Extension with invalid character' + ); + } + + public function testInvalidCharThenExtension() { + $this->assertEquals( + 'x', + IEUrlExtension::findIE6Extension( '*.x' ), + 'Invalid character followed by an extension' + ); + } + + public function testMultipleQuestionMarks() { + $this->assertEquals( + 'c', + IEUrlExtension::findIE6Extension( 'a?b?.c?.d?e?f' ), + 'Multiple question marks' + ); + } + + public function testExeException() { + $this->assertEquals( + 'd', + IEUrlExtension::findIE6Extension( 'a?b?.exe?.d?.e' ), + '.exe exception' + ); + } + + public function testExeException2() { + $this->assertEquals( + 'exe', + IEUrlExtension::findIE6Extension( 'a?b?.exe' ), + '.exe exception 2' + ); + } + + public function testHash() { + $this->assertEquals( + '', + IEUrlExtension::findIE6Extension( 'a#b.c' ), + 'Hash character preceding extension' + ); + } + + public function testHash2() { + $this->assertEquals( + '', + IEUrlExtension::findIE6Extension( 'a?#b.c' ), + 'Hash character preceding extension 2' + ); + } + + public function testDotAtEnd() { + $this->assertEquals( + '', + IEUrlExtension::findIE6Extension( '.' ), + 'Dot at end of string' + ); + } + + public function testTwoDots() { + $this->assertEquals( + 'z', + IEUrlExtension::findIE6Extension( 'x.y.z' ), + 'Two dots' + ); + } +} diff --git a/tests/phpunit/includes/libs/JavaScriptMinifierTest.php b/tests/phpunit/includes/libs/JavaScriptMinifierTest.php new file mode 100644 index 00000000..ab72e361 --- /dev/null +++ b/tests/phpunit/includes/libs/JavaScriptMinifierTest.php @@ -0,0 +1,170 @@ + bar", "" ), + array( "--> Foo", "" ), + array( "x --> y", "x-->y" ), + + // Semicolon insertion + array( "(function(){return\nx;})", "(function(){return\nx;})" ), + array( "throw\nx;", "throw\nx;" ), + array( "while(p){continue\nx;}", "while(p){continue\nx;}" ), + array( "while(p){break\nx;}", "while(p){break\nx;}" ), + array( "var\nx;", "var x;" ), + array( "x\ny;", "x\ny;" ), + array( "x\n++y;", "x\n++y;" ), + array( "x\n!y;", "x\n!y;" ), + array( "x\n{y}", "x\n{y}" ), + array( "x\n+y;", "x+y;" ), + array( "x\n(y);", "x(y);" ), + array( "5.\nx;", "5.\nx;" ), + array( "0xFF.\nx;", "0xFF.x;" ), + array( "5.3.\nx;", "5.3.x;" ), + + // Semicolon insertion between an expression having an inline + // comment after it, and a statement on the next line (bug 27046). + array( "var a = this //foo bar \n for ( b = 0; c < d; b++ ) {}", "var a=this\nfor(b=0;cparse( $minified, 'minify-test.js', 1 ); + + $this->assertEquals( $expectedOutput, $minified, "Minified output should be in the form expected." ); + } + + public static function provideBug32548() { + return array( + array( + // This one gets interpreted all together by the prior code; + // no break at the 'E' happens. + '1.23456789E55', + ), + array( + // This one breaks under the bad code; splits between 'E' and '+' + '1.23456789E+5', + ), + array( + // This one breaks under the bad code; splits between 'E' and '-' + '1.23456789E-5', + ), + ); + } + + /** + * @dataProvider provideBug32548 + */ + public function testBug32548Exponent( $num ) { + // Long line breaking was being incorrectly done between the base and + // exponent part of a number, causing a syntax error. The line should + // instead break at the start of the number. + $prefix = 'var longVarName' . str_repeat( '_', 973 ) . '='; + $suffix = ',shortVarName=0;'; + + $input = $prefix . $num . $suffix; + $expected = $prefix . "\n" . $num . $suffix; + + $minified = JavaScriptMinifier::minify( $input ); + + $this->assertEquals( $expected, $minified, "Line breaks must not occur in middle of exponent" ); + } +} diff --git a/tests/phpunit/includes/logging/LogFormatterTest.php b/tests/phpunit/includes/logging/LogFormatterTest.php new file mode 100644 index 00000000..e8ccf433 --- /dev/null +++ b/tests/phpunit/includes/logging/LogFormatterTest.php @@ -0,0 +1,207 @@ +setMwGlobals( array( + 'wgLogTypes' => array( 'phpunit' ), + 'wgLogActionsHandlers' => array( 'phpunit/test' => 'LogFormatter', + 'phpunit/param' => 'LogFormatter' ), + 'wgUser' => User::newFromName( 'Testuser' ), + 'wgExtensionMessagesFiles' => array( 'LogTests' => __DIR__ . '/LogTests.i18n.php' ), + ) ); + + $wgLang->getLocalisationCache()->recache( $wgLang->getCode() ); + + $this->user = User::newFromName( 'Testuser' ); + $this->title = Title::newMainPage(); + + $this->context = new RequestContext(); + $this->context->setUser( $this->user ); + $this->context->setTitle( $this->title ); + $this->context->setLanguage( $wgLang ); + } + + protected function tearDown() { + parent::tearDown(); + + global $wgLang; + $wgLang->getLocalisationCache()->recache( $wgLang->getCode() ); + } + + public function newLogEntry( $action, $params ) { + $logEntry = new ManualLogEntry( 'phpunit', $action ); + $logEntry->setPerformer( $this->user ); + $logEntry->setTarget( $this->title ); + $logEntry->setComment( 'A very good reason' ); + + $logEntry->setParameters( $params ); + + return $logEntry; + } + + public function testNormalLogParams() { + $entry = $this->newLogEntry( 'test', array() ); + $formatter = LogFormatter::newFromEntry( $entry ); + $formatter->setContext( $this->context ); + + $formatter->setShowUserToolLinks( false ); + $paramsWithoutTools = $formatter->getMessageParametersForTesting(); + unset( $formatter->parsedParameters ); + + $formatter->setShowUserToolLinks( true ); + $paramsWithTools = $formatter->getMessageParametersForTesting(); + + $userLink = Linker::userLink( + $this->user->getId(), + $this->user->getName() + ); + + $userTools = Linker::userToolLinksRedContribs( + $this->user->getId(), + $this->user->getName(), + $this->user->getEditCount() + ); + + $titleLink = Linker::link( $this->title, null, array(), array() ); + + // $paramsWithoutTools and $paramsWithTools should be only different + // in index 0 + $this->assertEquals( $paramsWithoutTools[1], $paramsWithTools[1] ); + $this->assertEquals( $paramsWithoutTools[2], $paramsWithTools[2] ); + + $this->assertEquals( $userLink, $paramsWithoutTools[0]['raw'] ); + $this->assertEquals( $userLink . $userTools, $paramsWithTools[0]['raw'] ); + + $this->assertEquals( $this->user->getName(), $paramsWithoutTools[1] ); + + $this->assertEquals( $titleLink, $paramsWithoutTools[2]['raw'] ); + } + + public function testLogParamsTypeRaw() { + $params = array( '4:raw:raw' => Linker::link( $this->title, null, array(), array() ) ); + $expected = Linker::link( $this->title, null, array(), array() ); + + $entry = $this->newLogEntry( 'param', $params ); + $formatter = LogFormatter::newFromEntry( $entry ); + $formatter->setContext( $this->context ); + + $logParam = $formatter->getActionText(); + + $this->assertEquals( $expected, $logParam ); + } + + public function testLogParamsTypeMsg() { + $params = array( '4:msg:msg' => 'log-description-phpunit' ); + $expected = wfMessage( 'log-description-phpunit' )->text(); + + $entry = $this->newLogEntry( 'param', $params ); + $formatter = LogFormatter::newFromEntry( $entry ); + $formatter->setContext( $this->context ); + + $logParam = $formatter->getActionText(); + + $this->assertEquals( $expected, $logParam ); + } + + public function testLogParamsTypeMsgContent() { + $params = array( '4:msg-content:msgContent' => 'log-description-phpunit' ); + $expected = wfMessage( 'log-description-phpunit' )->inContentLanguage()->text(); + + $entry = $this->newLogEntry( 'param', $params ); + $formatter = LogFormatter::newFromEntry( $entry ); + $formatter->setContext( $this->context ); + + $logParam = $formatter->getActionText(); + + $this->assertEquals( $expected, $logParam ); + } + + public function testLogParamsTypeNumber() { + global $wgLang; + + $params = array( '4:number:number' => 123456789 ); + $expected = $wgLang->formatNum( 123456789 ); + + $entry = $this->newLogEntry( 'param', $params ); + $formatter = LogFormatter::newFromEntry( $entry ); + $formatter->setContext( $this->context ); + + $logParam = $formatter->getActionText(); + + $this->assertEquals( $expected, $logParam ); + } + + public function testLogParamsTypeUserLink() { + $params = array( '4:user-link:userLink' => $this->user->getName() ); + $expected = Linker::userLink( + $this->user->getId(), + $this->user->getName() + ); + + $entry = $this->newLogEntry( 'param', $params ); + $formatter = LogFormatter::newFromEntry( $entry ); + $formatter->setContext( $this->context ); + + $logParam = $formatter->getActionText(); + + $this->assertEquals( $expected, $logParam ); + } + + public function testLogParamsTypeTitleLink() { + $params = array( '4:title-link:titleLink' => $this->title->getText() ); + $expected = Linker::link( $this->title, null, array(), array() ); + + $entry = $this->newLogEntry( 'param', $params ); + $formatter = LogFormatter::newFromEntry( $entry ); + $formatter->setContext( $this->context ); + + $logParam = $formatter->getActionText(); + + $this->assertEquals( $expected, $logParam ); + } + + public function testLogParamsTypePlain() { + $params = array( '4:plain:plain' => 'Some plain text' ); + $expected = 'Some plain text'; + + $entry = $this->newLogEntry( 'param', $params ); + $formatter = LogFormatter::newFromEntry( $entry ); + $formatter->setContext( $this->context ); + + $logParam = $formatter->getActionText(); + + $this->assertEquals( $expected, $logParam ); + } + + public function testLogComment() { + $entry = $this->newLogEntry( 'test', array() ); + $formatter = LogFormatter::newFromEntry( $entry ); + $formatter->setContext( $this->context ); + + $comment = ltrim( Linker::commentBlock( $entry->getComment() ) ); + + $this->assertEquals( $comment, $formatter->getComment() ); + } +} diff --git a/tests/phpunit/includes/logging/LogTests.i18n.php b/tests/phpunit/includes/logging/LogTests.i18n.php new file mode 100644 index 00000000..78787ba1 --- /dev/null +++ b/tests/phpunit/includes/logging/LogTests.i18n.php @@ -0,0 +1,15 @@ + 'PHPUnit-log', + 'log-description-phpunit' => 'Log for PHPUnit-tests', + 'logentry-phpunit-test' => '$1 {{GENDER:$2|tests}} with page $3', + 'logentry-phpunit-param' => '$4', +); diff --git a/tests/phpunit/includes/media/BitmapMetadataHandlerTest.php b/tests/phpunit/includes/media/BitmapMetadataHandlerTest.php new file mode 100644 index 00000000..a0e63a8a --- /dev/null +++ b/tests/phpunit/includes/media/BitmapMetadataHandlerTest.php @@ -0,0 +1,167 @@ +setMwGlobals( 'wgShowEXIF', false ); + + $this->filePath = __DIR__ . '/../../data/media/'; + } + + /** + * Test if having conflicting metadata values from different + * types of metadata, that the right one takes precedence. + * + * Basically the file has IPTC and XMP metadata, the + * IPTC should override the XMP, except for the multilingual + * translation (to en) where XMP should win. + * @covers BitmapMetadataHandler::Jpeg + */ + public function testMultilingualCascade() { + if ( !extension_loaded( 'exif' ) ) { + $this->markTestSkipped( "This test needs the exif extension." ); + } + if ( !extension_loaded( 'xml' ) ) { + $this->markTestSkipped( "This test needs the xml extension." ); + } + + $this->setMwGlobals( 'wgShowEXIF', true ); + + $meta = BitmapMetadataHandler::Jpeg( $this->filePath . + '/Xmp-exif-multilingual_test.jpg' ); + + $expected = array( + 'x-default' => 'right(iptc)', + 'en' => 'right translation', + '_type' => 'lang' + ); + + $this->assertArrayHasKey( 'ImageDescription', $meta, + 'Did not extract any ImageDescription info?!' ); + + $this->assertEquals( $expected, $meta['ImageDescription'] ); + } + + /** + * Test for jpeg comments are being handled by + * BitmapMetadataHandler correctly. + * + * There's more extensive tests of comment extraction in + * JpegMetadataExtractorTests.php + * @covers BitmapMetadataHandler::Jpeg + */ + public function testJpegComment() { + $meta = BitmapMetadataHandler::Jpeg( $this->filePath . + 'jpeg-comment-utf.jpg' ); + + $this->assertEquals( 'UTF-8 JPEG Comment — ¼', + $meta['JPEGFileComment'][0] ); + } + + /** + * Make sure a bad iptc block doesn't stop the other metadata + * from being extracted. + * @covers BitmapMetadataHandler::Jpeg + */ + public function testBadIPTC() { + $meta = BitmapMetadataHandler::Jpeg( $this->filePath . + 'iptc-invalid-psir.jpg' ); + $this->assertEquals( 'Created with GIMP', $meta['JPEGFileComment'][0] ); + } + + /** + * @covers BitmapMetadataHandler::Jpeg + */ + public function testIPTCDates() { + $meta = BitmapMetadataHandler::Jpeg( $this->filePath . + 'iptc-timetest.jpg' ); + + $this->assertEquals( '2020:07:14 01:36:05', $meta['DateTimeDigitized'] ); + $this->assertEquals( '1997:03:02 00:01:02', $meta['DateTimeOriginal'] ); + } + + /** + * File has an invalid time (+ one valid but really weird time) + * that shouldn't be included + * @covers BitmapMetadataHandler::Jpeg + */ + public function testIPTCDatesInvalid() { + $meta = BitmapMetadataHandler::Jpeg( $this->filePath . + 'iptc-timetest-invalid.jpg' ); + + $this->assertEquals( '1845:03:02 00:01:02', $meta['DateTimeOriginal'] ); + $this->assertFalse( isset( $meta['DateTimeDigitized'] ) ); + } + + /** + * XMP data should take priority over iptc data + * when hash has been updated, but not when + * the hash is wrong. + * @covers BitmapMetadataHandler::addMetadata + * @covers BitmapMetadataHandler::getMetadataArray + */ + public function testMerging() { + $merger = new BitmapMetadataHandler(); + $merger->addMetadata( array( 'foo' => 'xmp' ), 'xmp-general' ); + $merger->addMetadata( array( 'bar' => 'xmp' ), 'xmp-general' ); + $merger->addMetadata( array( 'baz' => 'xmp' ), 'xmp-general' ); + $merger->addMetadata( array( 'fred' => 'xmp' ), 'xmp-general' ); + $merger->addMetadata( array( 'foo' => 'iptc (hash)' ), 'iptc-good-hash' ); + $merger->addMetadata( array( 'bar' => 'iptc (bad hash)' ), 'iptc-bad-hash' ); + $merger->addMetadata( array( 'baz' => 'iptc (bad hash)' ), 'iptc-bad-hash' ); + $merger->addMetadata( array( 'fred' => 'iptc (no hash)' ), 'iptc-no-hash' ); + $merger->addMetadata( array( 'baz' => 'exif' ), 'exif' ); + + $actual = $merger->getMetadataArray(); + $expected = array( + 'foo' => 'xmp', + 'bar' => 'iptc (bad hash)', + 'baz' => 'exif', + 'fred' => 'xmp', + ); + $this->assertEquals( $expected, $actual ); + } + + /** + * @covers BitmapMetadataHandler::png + */ + public function testPNGXMP() { + if ( !extension_loaded( 'xml' ) ) { + $this->markTestSkipped( "This test needs the xml extension." ); + } + $handler = new BitmapMetadataHandler(); + $result = $handler->png( $this->filePath . 'xmp.png' ); + $expected = array( + 'frameCount' => 0, + 'loopCount' => 1, + 'duration' => 0, + 'bitDepth' => 1, + 'colorType' => 'index-coloured', + 'metadata' => array( + 'SerialNumber' => '123456789', + '_MW_PNG_VERSION' => 1, + ), + ); + $this->assertEquals( $expected, $result ); + } + + /** + * @covers BitmapMetadataHandler::png + */ + public function testPNGNative() { + $handler = new BitmapMetadataHandler(); + $result = $handler->png( $this->filePath . 'Png-native-test.png' ); + $expected = 'http://example.com/url'; + $this->assertEquals( $expected, $result['metadata']['Identifier']['x-default'] ); + } + + /** + * @covers BitmapMetadataHandler::getTiffByteOrder + */ + public function testTiffByteOrder() { + $handler = new BitmapMetadataHandler(); + $res = $handler->getTiffByteOrder( $this->filePath . 'test.tiff' ); + $this->assertEquals( 'LE', $res ); + } +} diff --git a/tests/phpunit/includes/media/BitmapScalingTest.php b/tests/phpunit/includes/media/BitmapScalingTest.php new file mode 100644 index 00000000..9395b660 --- /dev/null +++ b/tests/phpunit/includes/media/BitmapScalingTest.php @@ -0,0 +1,137 @@ +setMwGlobals( array( + 'wgMaxImageArea' => 1.25e7, // 3500x3500 + 'wgCustomConvertCommand' => 'dummy', // Set so that we don't get client side rendering + ) ); + } + + /** + * @dataProvider provideNormaliseParams + * @covers BitmapHandler::normaliseParams + */ + public function testNormaliseParams( $fileDimensions, $expectedParams, $params, $msg ) { + $file = new FakeDimensionFile( $fileDimensions ); + $handler = new BitmapHandler; + $valid = $handler->normaliseParams( $file, $params ); + $this->assertTrue( $valid ); + $this->assertEquals( $expectedParams, $params, $msg ); + } + + public static function provideNormaliseParams() { + return array( + /* Regular resize operations */ + array( + array( 1024, 768 ), + array( + 'width' => 512, 'height' => 384, + 'physicalWidth' => 512, 'physicalHeight' => 384, + 'page' => 1, + ), + array( 'width' => 512 ), + 'Resizing with width set', + ), + array( + array( 1024, 768 ), + array( + 'width' => 512, 'height' => 384, + 'physicalWidth' => 512, 'physicalHeight' => 384, + 'page' => 1, + ), + array( 'width' => 512, 'height' => 768 ), + 'Resizing with height set too high', + ), + array( + array( 1024, 768 ), + array( + 'width' => 512, 'height' => 384, + 'physicalWidth' => 512, 'physicalHeight' => 384, + 'page' => 1, + ), + array( 'width' => 1024, 'height' => 384 ), + 'Resizing with height set', + ), + + /* Very tall images */ + array( + array( 1000, 100 ), + array( + 'width' => 5, 'height' => 1, + 'physicalWidth' => 5, 'physicalHeight' => 1, + 'page' => 1, + ), + array( 'width' => 5 ), + 'Very wide image', + ), + + array( + array( 100, 1000 ), + array( + 'width' => 1, 'height' => 10, + 'physicalWidth' => 1, 'physicalHeight' => 10, + 'page' => 1, + ), + array( 'width' => 1 ), + 'Very high image', + ), + array( + array( 100, 1000 ), + array( + 'width' => 1, 'height' => 5, + 'physicalWidth' => 1, 'physicalHeight' => 10, + 'page' => 1, + ), + array( 'width' => 10, 'height' => 5 ), + 'Very high image with height set', + ), + /* Max image area */ + array( + array( 4000, 4000 ), + array( + 'width' => 5000, 'height' => 5000, + 'physicalWidth' => 4000, 'physicalHeight' => 4000, + 'page' => 1, + ), + array( 'width' => 5000 ), + 'Bigger than max image size but doesn\'t need scaling', + ), + ); + } + + /** + * @covers BitmapHandler::doTransform + */ + public function testTooBigImage() { + $file = new FakeDimensionFile( array( 4000, 4000 ) ); + $handler = new BitmapHandler; + $params = array( 'width' => '3700' ); // Still bigger than max size. + $this->assertEquals( 'TransformParameterError', + get_class( $handler->doTransform( $file, 'dummy path', '', $params ) ) ); + } + + /** + * @covers BitmapHandler::doTransform + */ + public function testTooBigMustRenderImage() { + $file = new FakeDimensionFile( array( 4000, 4000 ) ); + $file->mustRender = true; + $handler = new BitmapHandler; + $params = array( 'width' => '5000' ); // Still bigger than max size. + $this->assertEquals( 'TransformParameterError', + get_class( $handler->doTransform( $file, 'dummy path', '', $params ) ) ); + } + + /** + * @covers BitmapHandler::getImageArea + */ + public function testImageArea() { + $file = new FakeDimensionFile( array( 7, 9 ) ); + $handler = new BitmapHandler; + $this->assertEquals( 63, $handler->getImageArea( $file ) ); + } +} diff --git a/tests/phpunit/includes/media/ExifBitmapTest.php b/tests/phpunit/includes/media/ExifBitmapTest.php new file mode 100644 index 00000000..a2e0eb62 --- /dev/null +++ b/tests/phpunit/includes/media/ExifBitmapTest.php @@ -0,0 +1,139 @@ +markTestSkipped( "This test needs the exif extension." ); + } + + $this->setMwGlobals( 'wgShowEXIF', true ); + + $this->handler = new ExifBitmapHandler; + + } + + /** + * @covers ExifBitmapHandler::isMetadataValid + */ + public function testIsOldBroken() { + $res = $this->handler->isMetadataValid( null, ExifBitmapHandler::OLD_BROKEN_FILE ); + $this->assertEquals( ExifBitmapHandler::METADATA_COMPATIBLE, $res ); + } + + /** + * @covers ExifBitmapHandler::isMetadataValid + */ + public function testIsBrokenFile() { + $res = $this->handler->isMetadataValid( null, ExifBitmapHandler::BROKEN_FILE ); + $this->assertEquals( ExifBitmapHandler::METADATA_GOOD, $res ); + } + + /** + * @covers ExifBitmapHandler::isMetadataValid + */ + public function testIsInvalid() { + $res = $this->handler->isMetadataValid( null, 'Something Invalid Here.' ); + $this->assertEquals( ExifBitmapHandler::METADATA_BAD, $res ); + } + + /** + * @covers ExifBitmapHandler::isMetadataValid + */ + public function testGoodMetadata() { + $meta = 'a:16:{s:10:"ImageWidth";i:20;s:11:"ImageLength";i:20;s:13:"BitsPerSample";a:3:{i:0;i:8;i:1;i:8;i:2;i:8;}s:11:"Compression";i:5;s:25:"PhotometricInterpretation";i:2;s:16:"ImageDescription";s:17:"Created with GIMP";s:12:"StripOffsets";i:8;s:11:"Orientation";i:1;s:15:"SamplesPerPixel";i:3;s:12:"RowsPerStrip";i:64;s:15:"StripByteCounts";i:238;s:11:"XResolution";s:19:"1207959552/16777216";s:11:"YResolution";s:19:"1207959552/16777216";s:19:"PlanarConfiguration";i:1;s:14:"ResolutionUnit";i:2;s:22:"MEDIAWIKI_EXIF_VERSION";i:2;}'; + $res = $this->handler->isMetadataValid( null, $meta ); + $this->assertEquals( ExifBitmapHandler::METADATA_GOOD, $res ); + } + + /** + * @covers ExifBitmapHandler::isMetadataValid + */ + public function testIsOldGood() { + $meta = 'a:16:{s:10:"ImageWidth";i:20;s:11:"ImageLength";i:20;s:13:"BitsPerSample";a:3:{i:0;i:8;i:1;i:8;i:2;i:8;}s:11:"Compression";i:5;s:25:"PhotometricInterpretation";i:2;s:16:"ImageDescription";s:17:"Created with GIMP";s:12:"StripOffsets";i:8;s:11:"Orientation";i:1;s:15:"SamplesPerPixel";i:3;s:12:"RowsPerStrip";i:64;s:15:"StripByteCounts";i:238;s:11:"XResolution";s:19:"1207959552/16777216";s:11:"YResolution";s:19:"1207959552/16777216";s:19:"PlanarConfiguration";i:1;s:14:"ResolutionUnit";i:2;s:22:"MEDIAWIKI_EXIF_VERSION";i:1;}'; + $res = $this->handler->isMetadataValid( null, $meta ); + $this->assertEquals( ExifBitmapHandler::METADATA_COMPATIBLE, $res ); + } + + /** + * Handle metadata from paged tiff handler (gotten via instant commons) gracefully. + * @covers ExifBitmapHandler::isMetadataValid + */ + public function testPagedTiffHandledGracefully() { + $meta = 'a:6:{s:9:"page_data";a:1:{i:1;a:5:{s:5:"width";i:643;s:6:"height";i:448;s:5:"alpha";s:4:"true";s:4:"page";i:1;s:6:"pixels";i:288064;}}s:10:"page_count";i:1;s:10:"first_page";i:1;s:9:"last_page";i:1;s:4:"exif";a:9:{s:10:"ImageWidth";i:643;s:11:"ImageLength";i:448;s:11:"Compression";i:5;s:25:"PhotometricInterpretation";i:2;s:11:"Orientation";i:1;s:15:"SamplesPerPixel";i:4;s:12:"RowsPerStrip";i:50;s:19:"PlanarConfiguration";i:1;s:22:"MEDIAWIKI_EXIF_VERSION";i:1;}s:21:"TIFF_METADATA_VERSION";s:3:"1.4";}'; + $res = $this->handler->isMetadataValid( null, $meta ); + $this->assertEquals( ExifBitmapHandler::METADATA_BAD, $res ); + } + + /** + * @covers ExifBitmapHandler::convertMetadataVersion + */ + public function testConvertMetadataLatest() { + $metadata = array( + 'foo' => array( 'First', 'Second', '_type' => 'ol' ), + 'MEDIAWIKI_EXIF_VERSION' => 2 + ); + $res = $this->handler->convertMetadataVersion( $metadata, 2 ); + $this->assertEquals( $metadata, $res ); + } + + /** + * @covers ExifBitmapHandler::convertMetadataVersion + */ + public function testConvertMetadataToOld() { + $metadata = array( + 'foo' => array( 'First', 'Second', '_type' => 'ol' ), + 'bar' => array( 'First', 'Second', '_type' => 'ul' ), + 'baz' => array( 'First', 'Second' ), + 'fred' => 'Single', + 'MEDIAWIKI_EXIF_VERSION' => 2, + ); + $expected = array( + 'foo' => "\n#First\n#Second", + 'bar' => "\n*First\n*Second", + 'baz' => "\n*First\n*Second", + 'fred' => 'Single', + 'MEDIAWIKI_EXIF_VERSION' => 1, + ); + $res = $this->handler->convertMetadataVersion( $metadata, 1 ); + $this->assertEquals( $expected, $res ); + } + + /** + * @covers ExifBitmapHandler::convertMetadataVersion + */ + public function testConvertMetadataSoftware() { + $metadata = array( + 'Software' => array( array( 'GIMP', '1.1' ) ), + 'MEDIAWIKI_EXIF_VERSION' => 2, + ); + $expected = array( + 'Software' => 'GIMP (Version 1.1)', + 'MEDIAWIKI_EXIF_VERSION' => 1, + ); + $res = $this->handler->convertMetadataVersion( $metadata, 1 ); + $this->assertEquals( $expected, $res ); + } + + /** + * @covers ExifBitmapHandler::convertMetadataVersion + */ + public function testConvertMetadataSoftwareNormal() { + $metadata = array( + 'Software' => array( "GIMP 1.2", "vim" ), + 'MEDIAWIKI_EXIF_VERSION' => 2, + ); + $expected = array( + 'Software' => "\n*GIMP 1.2\n*vim", + 'MEDIAWIKI_EXIF_VERSION' => 1, + ); + $res = $this->handler->convertMetadataVersion( $metadata, 1 ); + $this->assertEquals( $expected, $res ); + } +} diff --git a/tests/phpunit/includes/media/ExifRotationTest.php b/tests/phpunit/includes/media/ExifRotationTest.php new file mode 100644 index 00000000..64276d92 --- /dev/null +++ b/tests/phpunit/includes/media/ExifRotationTest.php @@ -0,0 +1,248 @@ +markTestSkipped( "This test needs the exif extension." ); + } + + $this->handler = new BitmapHandler(); + $filePath = __DIR__ . '/../../data/media'; + + $tmpDir = $this->getNewTempDirectory(); + + $this->repo = new FSRepo( array( + 'name' => 'temp', + 'url' => 'http://localhost/thumbtest', + 'backend' => new FSFileBackend( array( + 'name' => 'localtesting', + 'lockManager' => 'nullLockManager', + 'containerPaths' => array( 'temp-thumb' => $tmpDir, 'data' => $filePath ) + ) ) + ) ); + + $this->setMwGlobals( array( + 'wgShowEXIF' => true, + 'wgEnableAutoRotation' => true, + ) ); + } + + /** + * @dataProvider provideFiles + */ + public function testMetadata( $name, $type, $info ) { + if ( !BitmapHandler::canRotate() ) { + $this->markTestSkipped( "This test needs a rasterizer that can auto-rotate." ); + } + $file = $this->dataFile( $name, $type ); + $this->assertEquals( $info['width'], $file->getWidth(), "$name: width check" ); + $this->assertEquals( $info['height'], $file->getHeight(), "$name: height check" ); + } + + /** + * + * @dataProvider provideFiles + */ + public function testRotationRendering( $name, $type, $info, $thumbs ) { + if ( !BitmapHandler::canRotate() ) { + $this->markTestSkipped( "This test needs a rasterizer that can auto-rotate." ); + } + foreach ( $thumbs as $size => $out ) { + if ( preg_match( '/^(\d+)px$/', $size, $matches ) ) { + $params = array( + 'width' => $matches[1], + ); + } elseif ( preg_match( '/^(\d+)x(\d+)px$/', $size, $matches ) ) { + $params = array( + 'width' => $matches[1], + 'height' => $matches[2] + ); + } else { + throw new MWException( 'bogus test data format ' . $size ); + } + + $file = $this->dataFile( $name, $type ); + $thumb = $file->transform( $params, File::RENDER_NOW | File::RENDER_FORCE ); + + $this->assertEquals( $out[0], $thumb->getWidth(), "$name: thumb reported width check for $size" ); + $this->assertEquals( $out[1], $thumb->getHeight(), "$name: thumb reported height check for $size" ); + + $gis = getimagesize( $thumb->getLocalCopyPath() ); + if ( $out[0] > $info['width'] ) { + // Physical image won't be scaled bigger than the original. + $this->assertEquals( $info['width'], $gis[0], "$name: thumb actual width check for $size" ); + $this->assertEquals( $info['height'], $gis[1], "$name: thumb actual height check for $size" ); + } else { + $this->assertEquals( $out[0], $gis[0], "$name: thumb actual width check for $size" ); + $this->assertEquals( $out[1], $gis[1], "$name: thumb actual height check for $size" ); + } + } + } + + /* Utility function */ + private function dataFile( $name, $type ) { + return new UnregisteredLocalFile( false, $this->repo, + "mwstore://localtesting/data/$name", $type ); + } + + public static function provideFiles() { + return array( + array( + 'landscape-plain.jpg', + 'image/jpeg', + array( + 'width' => 1024, + 'height' => 768, + ), + array( + '800x600px' => array( 800, 600 ), + '9999x800px' => array( 1067, 800 ), + '800px' => array( 800, 600 ), + '600px' => array( 600, 450 ), + ) + ), + array( + 'portrait-rotated.jpg', + 'image/jpeg', + array( + 'width' => 768, // as rotated + 'height' => 1024, // as rotated + ), + array( + '800x600px' => array( 450, 600 ), + '9999x800px' => array( 600, 800 ), + '800px' => array( 800, 1067 ), + '600px' => array( 600, 800 ), + ) + ) + ); + } + + /** + * Same as before, but with auto-rotation disabled. + * @dataProvider provideFilesNoAutoRotate + */ + public function testMetadataNoAutoRotate( $name, $type, $info ) { + $this->setMwGlobals( 'wgEnableAutoRotation', false ); + + $file = $this->dataFile( $name, $type ); + $this->assertEquals( $info['width'], $file->getWidth(), "$name: width check" ); + $this->assertEquals( $info['height'], $file->getHeight(), "$name: height check" ); + } + + /** + * + * @dataProvider provideFilesNoAutoRotate + */ + public function testRotationRenderingNoAutoRotate( $name, $type, $info, $thumbs ) { + $this->setMwGlobals( 'wgEnableAutoRotation', false ); + + foreach ( $thumbs as $size => $out ) { + if ( preg_match( '/^(\d+)px$/', $size, $matches ) ) { + $params = array( + 'width' => $matches[1], + ); + } elseif ( preg_match( '/^(\d+)x(\d+)px$/', $size, $matches ) ) { + $params = array( + 'width' => $matches[1], + 'height' => $matches[2] + ); + } else { + throw new MWException( 'bogus test data format ' . $size ); + } + + $file = $this->dataFile( $name, $type ); + $thumb = $file->transform( $params, File::RENDER_NOW | File::RENDER_FORCE ); + + $this->assertEquals( $out[0], $thumb->getWidth(), "$name: thumb reported width check for $size" ); + $this->assertEquals( $out[1], $thumb->getHeight(), "$name: thumb reported height check for $size" ); + + $gis = getimagesize( $thumb->getLocalCopyPath() ); + if ( $out[0] > $info['width'] ) { + // Physical image won't be scaled bigger than the original. + $this->assertEquals( $info['width'], $gis[0], "$name: thumb actual width check for $size" ); + $this->assertEquals( $info['height'], $gis[1], "$name: thumb actual height check for $size" ); + } else { + $this->assertEquals( $out[0], $gis[0], "$name: thumb actual width check for $size" ); + $this->assertEquals( $out[1], $gis[1], "$name: thumb actual height check for $size" ); + } + } + } + + public static function provideFilesNoAutoRotate() { + return array( + array( + 'landscape-plain.jpg', + 'image/jpeg', + array( + 'width' => 1024, + 'height' => 768, + ), + array( + '800x600px' => array( 800, 600 ), + '9999x800px' => array( 1067, 800 ), + '800px' => array( 800, 600 ), + '600px' => array( 600, 450 ), + ) + ), + array( + 'portrait-rotated.jpg', + 'image/jpeg', + array( + 'width' => 1024, // since not rotated + 'height' => 768, // since not rotated + ), + array( + '800x600px' => array( 800, 600 ), + '9999x800px' => array( 1067, 800 ), + '800px' => array( 800, 600 ), + '600px' => array( 600, 450 ), + ) + ) + ); + } + + + const TEST_WIDTH = 100; + const TEST_HEIGHT = 200; + + /** + * @dataProvider provideBitmapExtractPreRotationDimensions + */ + public function testBitmapExtractPreRotationDimensions( $rotation, $expected ) { + $result = $this->handler->extractPreRotationDimensions( array( + 'physicalWidth' => self::TEST_WIDTH, + 'physicalHeight' => self::TEST_HEIGHT, + ), $rotation ); + $this->assertEquals( $expected, $result ); + } + + public static function provideBitmapExtractPreRotationDimensions() { + return array( + array( + 0, + array( self::TEST_WIDTH, self::TEST_HEIGHT ) + ), + array( + 90, + array( self::TEST_HEIGHT, self::TEST_WIDTH ) + ), + array( + 180, + array( self::TEST_WIDTH, self::TEST_HEIGHT ) + ), + array( + 270, + array( self::TEST_HEIGHT, self::TEST_WIDTH ) + ), + ); + } +} diff --git a/tests/phpunit/includes/media/ExifTest.php b/tests/phpunit/includes/media/ExifTest.php new file mode 100644 index 00000000..dea36b03 --- /dev/null +++ b/tests/phpunit/includes/media/ExifTest.php @@ -0,0 +1,46 @@ +markTestSkipped( "This test needs the exif extension." ); + } + + $this->mediaPath = __DIR__ . '/../../data/media/'; + + + + $this->setMwGlobals( 'wgShowEXIF', true ); + } + + public function testGPSExtraction() { + $filename = $this->mediaPath . 'exif-gps.jpg'; + $seg = JpegMetadataExtractor::segmentSplitter( $filename ); + $exif = new Exif( $filename, $seg['byteOrder'] ); + $data = $exif->getFilteredData(); + $expected = array( + 'GPSLatitude' => 88.5180555556, + 'GPSLongitude' => -21.12357, + 'GPSAltitude' => -3.141592653, + 'GPSDOP' => '5/1', + 'GPSVersionID' => '2.2.0.0', + ); + $this->assertEquals( $expected, $data, '', 0.0000000001 ); + } + + public function testUnicodeUserComment() { + $filename = $this->mediaPath . 'exif-user-comment.jpg'; + $seg = JpegMetadataExtractor::segmentSplitter( $filename ); + $exif = new Exif( $filename, $seg['byteOrder'] ); + $data = $exif->getFilteredData(); + + $expected = array( + 'UserComment' => 'test⁔comment' + ); + $this->assertEquals( $expected, $data ); + } +} diff --git a/tests/phpunit/includes/media/FakeDimensionFile.php b/tests/phpunit/includes/media/FakeDimensionFile.php new file mode 100644 index 00000000..7926000b --- /dev/null +++ b/tests/phpunit/includes/media/FakeDimensionFile.php @@ -0,0 +1,28 @@ +dimensions = $dimensions; + } + + public function getWidth( $page = 1 ) { + return $this->dimensions[0]; + } + + public function getHeight( $page = 1 ) { + return $this->dimensions[1]; + } + + public function mustRender() { + return $this->mustRender; + } + + public function getPath() { + return ''; + } +} \ No newline at end of file diff --git a/tests/phpunit/includes/media/FormatMetadataTest.php b/tests/phpunit/includes/media/FormatMetadataTest.php new file mode 100644 index 00000000..a073e4ca --- /dev/null +++ b/tests/phpunit/includes/media/FormatMetadataTest.php @@ -0,0 +1,59 @@ +markTestSkipped( "This test needs the exif extension." ); + } + $filePath = __DIR__ . '/../../data/media'; + $this->backend = new FSFileBackend( array( + 'name' => 'localtesting', + 'lockManager' => 'nullLockManager', + 'containerPaths' => array( 'data' => $filePath ) + ) ); + $this->repo = new FSRepo( array( + 'name' => 'temp', + 'url' => 'http://localhost/thumbtest', + 'backend' => $this->backend + ) ); + + $this->setMwGlobals( 'wgShowEXIF', true ); + } + + public function testInvalidDate() { + $file = $this->dataFile( 'broken_exif_date.jpg', 'image/jpeg' ); + + // Throws an error if bug hit + $meta = $file->formatMetadata(); + $this->assertNotEquals( false, $meta, 'Valid metadata extracted' ); + + // Find date exif entry + $this->assertArrayHasKey( 'visible', $meta ); + $dateIndex = null; + foreach ( $meta['visible'] as $i => $data ) { + if ( $data['id'] == 'exif-datetimeoriginal' ) { + $dateIndex = $i; + } + } + $this->assertNotNull( $dateIndex, 'Date entry exists in metadata' ); + $this->assertEquals( '0000:01:00 00:02:27', + $meta['visible'][$dateIndex]['value'], + 'File with invalid date metadata (bug 29471)' ); + } + + private function dataFile( $name, $type ) { + return new UnregisteredLocalFile( false, $this->repo, + "mwstore://localtesting/data/$name", $type ); + } +} diff --git a/tests/phpunit/includes/media/GIFMetadataExtractorTest.php b/tests/phpunit/includes/media/GIFMetadataExtractorTest.php new file mode 100644 index 00000000..9e3f9244 --- /dev/null +++ b/tests/phpunit/includes/media/GIFMetadataExtractorTest.php @@ -0,0 +1,107 @@ +mediaPath = __DIR__ . '/../../data/media/'; + } + + /** + * Put in a file, and see if the metadata coming out is as expected. + * @param $filename String + * @param $expected Array The extracted metadata. + * @dataProvider provideGetMetadata + * @covers GIFMetadataExtractor::getMetadata + */ + public function testGetMetadata( $filename, $expected ) { + $actual = GIFMetadataExtractor::getMetadata( $this->mediaPath . $filename ); + $this->assertEquals( $expected, $actual ); + } + + public static function provideGetMetadata() { + + $xmpNugget = << + + + + + The interwebs + + + + Bawolff + + + A file to test GIF + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +EOF; + $xmpNugget = str_replace( "\r", '', $xmpNugget ); // Windows compat + + return array( + array( + 'nonanimated.gif', + array( + 'comment' => array( 'GIF test file ⁕ Created with GIMP' ), + 'duration' => 0.1, + 'frameCount' => 1, + 'looped' => false, + 'xmp' => '', + ) + ), + array( + 'animated.gif', + array( + 'comment' => array( 'GIF test file . Created with GIMP' ), + 'duration' => 2.4, + 'frameCount' => 4, + 'looped' => true, + 'xmp' => '', + ) + ), + + array( + 'animated-xmp.gif', + array( + 'xmp' => $xmpNugget, + 'duration' => 2.4, + 'frameCount' => 4, + 'looped' => true, + 'comment' => array( 'GIƒ·test·file' ), + ) + ), + ); + } +} diff --git a/tests/phpunit/includes/media/GIFTest.php b/tests/phpunit/includes/media/GIFTest.php new file mode 100644 index 00000000..c8e729c8 --- /dev/null +++ b/tests/phpunit/includes/media/GIFTest.php @@ -0,0 +1,120 @@ +filePath = __DIR__ . '/../../data/media'; + $this->backend = new FSFileBackend( array( + 'name' => 'localtesting', + 'lockManager' => 'nullLockManager', + 'containerPaths' => array( 'data' => $this->filePath ) + ) ); + $this->repo = new FSRepo( array( + 'name' => 'temp', + 'url' => 'http://localhost/thumbtest', + 'backend' => $this->backend + ) ); + $this->handler = new GIFHandler(); + } + + /** + * @covers GIFHandler::getMetadata + */ + public function testInvalidFile() { + $res = $this->handler->getMetadata( null, $this->filePath . '/README' ); + $this->assertEquals( GIFHandler::BROKEN_FILE, $res ); + } + + /** + * @param $filename String basename of the file to check + * @param $expected boolean Expected result. + * @dataProvider provideIsAnimated + * @covers GIFHandler::isAnimatedImage + */ + public function testIsAnimanted( $filename, $expected ) { + $file = $this->dataFile( $filename, 'image/gif' ); + $actual = $this->handler->isAnimatedImage( $file ); + $this->assertEquals( $expected, $actual ); + } + + public static function provideIsAnimated() { + return array( + array( 'animated.gif', true ), + array( 'nonanimated.gif', false ), + ); + } + + /** + * @param $filename String + * @param $expected Integer Total image area + * @dataProvider provideGetImageArea + * @covers GIFHandler::getImageArea + */ + public function testGetImageArea( $filename, $expected ) { + $file = $this->dataFile( $filename, 'image/gif' ); + $actual = $this->handler->getImageArea( $file, $file->getWidth(), $file->getHeight() ); + $this->assertEquals( $expected, $actual ); + } + + public static function provideGetImageArea() { + return array( + array( 'animated.gif', 5400 ), + array( 'nonanimated.gif', 1350 ), + ); + } + + /** + * @param $metadata String Serialized metadata + * @param $expected Integer One of the class constants of GIFHandler + * @dataProvider provideIsMetadataValid + * @covers GIFHandler::isMetadataValid + */ + public function testIsMetadataValid( $metadata, $expected ) { + $actual = $this->handler->isMetadataValid( null, $metadata ); + $this->assertEquals( $expected, $actual ); + } + + public static function provideIsMetadataValid() { + return array( + array( GIFHandler::BROKEN_FILE, GIFHandler::METADATA_GOOD ), + array( '', GIFHandler::METADATA_BAD ), + array( null, GIFHandler::METADATA_BAD ), + array( 'Something invalid!', GIFHandler::METADATA_BAD ), + array( 'a:4:{s:10:"frameCount";i:1;s:6:"looped";b:0;s:8:"duration";d:0.1000000000000000055511151231257827021181583404541015625;s:8:"metadata";a:2:{s:14:"GIFFileComment";a:1:{i:0;s:35:"GIF test file ⁕ Created with GIMP";}s:15:"_MW_GIF_VERSION";i:1;}}', GIFHandler::METADATA_GOOD ), + ); + } + + /** + * @param $filename String + * @param $expected String Serialized array + * @dataProvider provideGetMetadata + * @covers GIFHandler::getMetadata + */ + public function testGetMetadata( $filename, $expected ) { + $file = $this->dataFile( $filename, 'image/gif' ); + $actual = $this->handler->getMetadata( $file, "$this->filePath/$filename" ); + $this->assertEquals( unserialize( $expected ), unserialize( $actual ) ); + } + + public static function provideGetMetadata() { + return array( + array( 'nonanimated.gif', 'a:4:{s:10:"frameCount";i:1;s:6:"looped";b:0;s:8:"duration";d:0.1000000000000000055511151231257827021181583404541015625;s:8:"metadata";a:2:{s:14:"GIFFileComment";a:1:{i:0;s:35:"GIF test file ⁕ Created with GIMP";}s:15:"_MW_GIF_VERSION";i:1;}}' ), + array( 'animated-xmp.gif', 'a:4:{s:10:"frameCount";i:4;s:6:"looped";b:1;s:8:"duration";d:2.399999999999999911182158029987476766109466552734375;s:8:"metadata";a:5:{s:6:"Artist";s:7:"Bawolff";s:16:"ImageDescription";a:2:{s:9:"x-default";s:18:"A file to test GIF";s:5:"_type";s:4:"lang";}s:15:"SublocationDest";s:13:"The interwebs";s:14:"GIFFileComment";a:1:{i:0;s:16:"GIƒ·test·file";}s:15:"_MW_GIF_VERSION";i:1;}}' ), + ); + } + + private function dataFile( $name, $type ) { + return new UnregisteredLocalFile( false, $this->repo, + "mwstore://localtesting/data/$name", $type ); + } +} diff --git a/tests/phpunit/includes/media/IPTCTest.php b/tests/phpunit/includes/media/IPTCTest.php new file mode 100644 index 00000000..81c1d287 --- /dev/null +++ b/tests/phpunit/includes/media/IPTCTest.php @@ -0,0 +1,81 @@ +assertEquals( 'UTF-8', $res ); + } + + /** + * @covers IPTC::Parse + */ + public function testIPTCParseNoCharset88591() { + // basically IPTC for keyword with value of 0xBC which is 1/4 in iso-8859-1 + // This data doesn't specify a charset. We're supposed to guess + // (which basically means utf-8 if valid, windows 1252 (iso 8859-1) if not) + $iptcData = "Photoshop 3.0\08BIM\4\4\0\0\0\0\0\x06\x1c\x02\x19\x00\x01\xBC"; + $res = IPTC::Parse( $iptcData ); + $this->assertEquals( array( '¼' ), $res['Keywords'] ); + } + + /** + * @covers IPTC::Parse + */ + public function testIPTCParseNoCharset88591b() { + /* This one contains a sequence that's valid iso 8859-1 but not valid utf8 */ + /* \xC3 = Ã, \xB8 = ¸ */ + $iptcData = "Photoshop 3.0\08BIM\4\4\0\0\0\0\0\x09\x1c\x02\x19\x00\x04\xC3\xC3\xC3\xB8"; + $res = IPTC::Parse( $iptcData ); + $this->assertEquals( array( 'ÃÃø' ), $res['Keywords'] ); + } + + /** + * Same as testIPTCParseNoCharset88591b, but forcing the charset to utf-8. + * What should happen is the first "\xC3\xC3" should be dropped as invalid, + * leaving \xC3\xB8, which is ø + * @covers IPTC::Parse + */ + public function testIPTCParseForcedUTFButInvalid() { + $iptcData = "Photoshop 3.0\08BIM\4\4\0\0\0\0\0\x11\x1c\x02\x19\x00\x04\xC3\xC3\xC3\xB8" + . "\x1c\x01\x5A\x00\x03\x1B\x25\x47"; + $res = IPTC::Parse( $iptcData ); + $this->assertEquals( array( 'ø' ), $res['Keywords'] ); + } + + /** + * @covers IPTC::Parse + */ + public function testIPTCParseNoCharsetUTF8() { + $iptcData = "Photoshop 3.0\08BIM\4\4\0\0\0\0\0\x07\x1c\x02\x19\x00\x02¼"; + $res = IPTC::Parse( $iptcData ); + $this->assertEquals( array( '¼' ), $res['Keywords'] ); + } + + /** + * Testing something that has 2 values for keyword + * @covers IPTC::Parse + */ + public function testIPTCParseMulti() { + $iptcData = /* identifier */ "Photoshop 3.0\08BIM\4\4" + /* length */ . "\0\0\0\0\0\x0D" + . "\x1c\x02\x19" . "\x00\x01" . "\xBC" + . "\x1c\x02\x19" . "\x00\x02" . "\xBC\xBD"; + $res = IPTC::Parse( $iptcData ); + $this->assertEquals( array( '¼', '¼½' ), $res['Keywords'] ); + } + + /** + * @covers IPTC::Parse + */ + public function testIPTCParseUTF8() { + // This has the magic "\x1c\x01\x5A\x00\x03\x1B\x25\x47" which marks content as UTF8. + $iptcData = "Photoshop 3.0\08BIM\4\4\0\0\0\0\0\x0F\x1c\x02\x19\x00\x02¼\x1c\x01\x5A\x00\x03\x1B\x25\x47"; + $res = IPTC::Parse( $iptcData ); + $this->assertEquals( array( '¼' ), $res['Keywords'] ); + } +} diff --git a/tests/phpunit/includes/media/JpegMetadataExtractorTest.php b/tests/phpunit/includes/media/JpegMetadataExtractorTest.php new file mode 100644 index 00000000..eafc8a2e --- /dev/null +++ b/tests/phpunit/includes/media/JpegMetadataExtractorTest.php @@ -0,0 +1,109 @@ +filePath = __DIR__ . '/../../data/media/'; + } + + /** + * We also use this test to test padding bytes don't + * screw stuff up + * + * @param string $file filename + * + * @dataProvider provideUtf8Comment + */ + public function testUtf8Comment( $file ) { + $res = JpegMetadataExtractor::segmentSplitter( $this->filePath . $file ); + $this->assertEquals( array( 'UTF-8 JPEG Comment — ¼' ), $res['COM'] ); + } + + public static function provideUtf8Comment() { + return array( + array( 'jpeg-comment-utf.jpg' ), + array( 'jpeg-padding-even.jpg' ), + array( 'jpeg-padding-odd.jpg' ), + ); + } + + /** The file is iso-8859-1, but it should get auto converted */ + public function testIso88591Comment() { + $res = JpegMetadataExtractor::segmentSplitter( $this->filePath . 'jpeg-comment-iso8859-1.jpg' ); + $this->assertEquals( array( 'ISO-8859-1 JPEG Comment - ¼' ), $res['COM'] ); + } + + /** Comment values that are non-textual (random binary junk) should not be shown. + * The example test file has a comment with a 0x5 byte in it which is a control character + * and considered binary junk for our purposes. + */ + public function testBinaryCommentStripped() { + $res = JpegMetadataExtractor::segmentSplitter( $this->filePath . 'jpeg-comment-binary.jpg' ); + $this->assertEmpty( $res['COM'] ); + } + + /* Very rarely a file can have multiple comments. + * Order of comments is based on order inside the file. + */ + public function testMultipleComment() { + $res = JpegMetadataExtractor::segmentSplitter( $this->filePath . 'jpeg-comment-multiple.jpg' ); + $this->assertEquals( array( 'foo', 'bar' ), $res['COM'] ); + } + + public function testXMPExtraction() { + $res = JpegMetadataExtractor::segmentSplitter( $this->filePath . 'jpeg-xmp-psir.jpg' ); + $expected = file_get_contents( $this->filePath . 'jpeg-xmp-psir.xmp' ); + $this->assertEquals( $expected, $res['XMP'] ); + } + + public function testPSIRExtraction() { + $res = JpegMetadataExtractor::segmentSplitter( $this->filePath . 'jpeg-xmp-psir.jpg' ); + $expected = '50686f746f73686f7020332e30003842494d04040000000000181c02190004746573741c02190003666f6f1c020000020004'; + $this->assertEquals( $expected, bin2hex( $res['PSIR'][0] ) ); + } + + public function testXMPExtractionAltAppId() { + $res = JpegMetadataExtractor::segmentSplitter( $this->filePath . 'jpeg-xmp-alt.jpg' ); + $expected = file_get_contents( $this->filePath . 'jpeg-xmp-psir.xmp' ); + $this->assertEquals( $expected, $res['XMP'] ); + } + + + public function testIPTCHashComparisionNoHash() { + $segments = JpegMetadataExtractor::segmentSplitter( $this->filePath . 'jpeg-xmp-psir.jpg' ); + $res = JpegMetadataExtractor::doPSIR( $segments['PSIR'][0] ); + + $this->assertEquals( 'iptc-no-hash', $res ); + } + + public function testIPTCHashComparisionBadHash() { + $segments = JpegMetadataExtractor::segmentSplitter( $this->filePath . 'jpeg-iptc-bad-hash.jpg' ); + $res = JpegMetadataExtractor::doPSIR( $segments['PSIR'][0] ); + + $this->assertEquals( 'iptc-bad-hash', $res ); + } + + public function testIPTCHashComparisionGoodHash() { + $segments = JpegMetadataExtractor::segmentSplitter( $this->filePath . 'jpeg-iptc-good-hash.jpg' ); + $res = JpegMetadataExtractor::doPSIR( $segments['PSIR'][0] ); + + $this->assertEquals( 'iptc-good-hash', $res ); + } + + public function testExifByteOrder() { + $res = JpegMetadataExtractor::segmentSplitter( $this->filePath . 'exif-user-comment.jpg' ); + $expected = 'BE'; + $this->assertEquals( $expected, $res['byteOrder'] ); + } +} diff --git a/tests/phpunit/includes/media/JpegTest.php b/tests/phpunit/includes/media/JpegTest.php new file mode 100644 index 00000000..9af4f1e1 --- /dev/null +++ b/tests/phpunit/includes/media/JpegTest.php @@ -0,0 +1,33 @@ +markTestSkipped( "This test needs the exif extension." ); + } + + $this->filePath = __DIR__ . '/../../data/media/'; + + + $this->setMwGlobals( 'wgShowEXIF', true ); + } + + public function testInvalidFile() { + $jpeg = new JpegHandler; + $res = $jpeg->getMetadata( null, $this->filePath . 'README' ); + $this->assertEquals( ExifBitmapHandler::BROKEN_FILE, $res ); + } + + public function testJpegMetadataExtraction() { + $h = new JpegHandler; + $res = $h->getMetadata( null, $this->filePath . 'test.jpg' ); + $expected = 'a:7:{s:16:"ImageDescription";s:9:"Test file";s:11:"XResolution";s:4:"72/1";s:11:"YResolution";s:4:"72/1";s:14:"ResolutionUnit";i:2;s:16:"YCbCrPositioning";i:1;s:15:"JPEGFileComment";a:1:{i:0;s:17:"Created with GIMP";}s:22:"MEDIAWIKI_EXIF_VERSION";i:2;}'; + + // Unserialize in case serialization format ever changes. + $this->assertEquals( unserialize( $expected ), unserialize( $res ) ); + } +} diff --git a/tests/phpunit/includes/media/MediaHandlerTest.php b/tests/phpunit/includes/media/MediaHandlerTest.php new file mode 100644 index 00000000..c28898bb --- /dev/null +++ b/tests/phpunit/includes/media/MediaHandlerTest.php @@ -0,0 +1,53 @@ + 50, + 'height' => 50, + 'tests' => array( + 50 => 50, + 17 => 17, + 18 => 18 ) ), + array( + 'width' => 366, + 'height' => 300, + 'tests' => array( + 50 => 61, + 17 => 21, + 18 => 22 ) ), + array( + 'width' => 300, + 'height' => 366, + 'tests' => array( + 50 => 41, + 17 => 14, + 18 => 15 ) ), + array( + 'width' => 100, + 'height' => 400, + 'tests' => array( + 50 => 12, + 17 => 4, + 18 => 4 ) ) ); + foreach ( $vals as $row ) { + $tests = $row['tests']; + $height = $row['height']; + $width = $row['width']; + foreach ( $tests as $max => $expected ) { + $y = round( $expected * $height / $width ); + $result = MediaHandler::fitBoxWidth( $width, $height, $max ); + $y2 = round( $result * $height / $width ); + $this->assertEquals( $expected, + $result, + "($width, $height, $max) wanted: {$expected}x$y, got: {$result}x$y2" ); + } + } + } +} diff --git a/tests/phpunit/includes/media/PNGMetadataExtractorTest.php b/tests/phpunit/includes/media/PNGMetadataExtractorTest.php new file mode 100644 index 00000000..939f2cfc --- /dev/null +++ b/tests/phpunit/includes/media/PNGMetadataExtractorTest.php @@ -0,0 +1,155 @@ +filePath = __DIR__ . '/../../data/media/'; + } + + /** + * Tests zTXt tag (compressed textual metadata) + */ + public function testPngNativetZtxt() { + $this->checkPHPExtension( 'zlib' ); + + $meta = PNGMetadataExtractor::getMetadata( $this->filePath . + 'Png-native-test.png' ); + $expected = "foo bar baz foo foo foo foof foo foo foo foo"; + $this->assertArrayHasKey( 'text', $meta ); + $meta = $meta['text']; + $this->assertArrayHasKey( 'Make', $meta ); + $this->assertArrayHasKey( 'x-default', $meta['Make'] ); + + $this->assertEquals( $expected, $meta['Make']['x-default'] ); + } + + /** + * Test tEXt tag (Uncompressed textual metadata) + */ + public function testPngNativeText() { + $meta = PNGMetadataExtractor::getMetadata( $this->filePath . + 'Png-native-test.png' ); + $expected = "Some long image desc"; + $this->assertArrayHasKey( 'text', $meta ); + $meta = $meta['text']; + $this->assertArrayHasKey( 'ImageDescription', $meta ); + $this->assertArrayHasKey( 'x-default', $meta['ImageDescription'] ); + $this->assertArrayHasKey( '_type', $meta['ImageDescription'] ); + + $this->assertEquals( $expected, $meta['ImageDescription']['x-default'] ); + } + + /** + * tEXt tags must be encoded iso-8859-1 (vs iTXt which are utf-8) + * Make sure non-ascii characters get converted properly + */ + public function testPngNativeTextNonAscii() { + $meta = PNGMetadataExtractor::getMetadata( $this->filePath . + 'Png-native-test.png' ); + + // Note the Copyright symbol here is a utf-8 one + // (aka \xC2\xA9) where in the file its iso-8859-1 + // encoded as just \xA9. + $expected = "© 2010 Bawolff"; + + $this->assertArrayHasKey( 'text', $meta ); + $meta = $meta['text']; + $this->assertArrayHasKey( 'Copyright', $meta ); + $this->assertArrayHasKey( 'x-default', $meta['Copyright'] ); + + $this->assertEquals( $expected, $meta['Copyright']['x-default'] ); + } + + /** + * Test extraction of pHYs tags, which can tell what the + * actual resolution of the image is (aka in dots per meter). + */ + /* + public function testPngPhysTag() { + $meta = PNGMetadataExtractor::getMetadata( $this->filePath . + 'Png-native-test.png' ); + + $this->assertArrayHasKey( 'text', $meta ); + $meta = $meta['text']; + + $this->assertEquals( '2835/100', $meta['XResolution'] ); + $this->assertEquals( '2835/100', $meta['YResolution'] ); + $this->assertEquals( 3, $meta['ResolutionUnit'] ); // 3 = cm + } + */ + + /** + * Given a normal static PNG, check the animation metadata returned. + */ + public function testStaticPngAnimationMetadata() { + $meta = PNGMetadataExtractor::getMetadata( $this->filePath . + 'Png-native-test.png' ); + + $this->assertEquals( 0, $meta['frameCount'] ); + $this->assertEquals( 1, $meta['loopCount'] ); + $this->assertEquals( 0, $meta['duration'] ); + } + + /** + * Given an animated APNG image file + * check it gets animated metadata right. + */ + public function testApngAnimationMetadata() { + $meta = PNGMetadataExtractor::getMetadata( $this->filePath . + 'Animated_PNG_example_bouncing_beach_ball.png' ); + + $this->assertEquals( 20, $meta['frameCount'] ); + // Note loop count of 0 = infinity + $this->assertEquals( 0, $meta['loopCount'] ); + $this->assertEquals( 1.5, $meta['duration'], '', 0.00001 ); + } + + public function testPngBitDepth8() { + $meta = PNGMetadataExtractor::getMetadata( $this->filePath . + 'Png-native-test.png' ); + + $this->assertEquals( 8, $meta['bitDepth'] ); + } + + public function testPngBitDepth1() { + $meta = PNGMetadataExtractor::getMetadata( $this->filePath . + '1bit-png.png' ); + $this->assertEquals( 1, $meta['bitDepth'] ); + } + + + public function testPngIndexColour() { + $meta = PNGMetadataExtractor::getMetadata( $this->filePath . + 'Png-native-test.png' ); + + $this->assertEquals( 'index-coloured', $meta['colorType'] ); + } + + public function testPngRgbColour() { + $meta = PNGMetadataExtractor::getMetadata( $this->filePath . + 'rgb-png.png' ); + $this->assertEquals( 'truecolour-alpha', $meta['colorType'] ); + } + + public function testPngRgbNoAlphaColour() { + $meta = PNGMetadataExtractor::getMetadata( $this->filePath . + 'rgb-na-png.png' ); + $this->assertEquals( 'truecolour', $meta['colorType'] ); + } + + public function testPngGreyscaleColour() { + $meta = PNGMetadataExtractor::getMetadata( $this->filePath . + 'greyscale-png.png' ); + $this->assertEquals( 'greyscale-alpha', $meta['colorType'] ); + } + + public function testPngGreyscaleNoAlphaColour() { + $meta = PNGMetadataExtractor::getMetadata( $this->filePath . + 'greyscale-na-png.png' ); + $this->assertEquals( 'greyscale', $meta['colorType'] ); + } +} diff --git a/tests/phpunit/includes/media/PNGTest.php b/tests/phpunit/includes/media/PNGTest.php new file mode 100644 index 00000000..ad4c2493 --- /dev/null +++ b/tests/phpunit/includes/media/PNGTest.php @@ -0,0 +1,123 @@ +filePath = __DIR__ . '/../../data/media'; + $this->backend = new FSFileBackend( array( + 'name' => 'localtesting', + 'lockManager' => 'nullLockManager', + 'containerPaths' => array( 'data' => $this->filePath ) + ) ); + $this->repo = new FSRepo( array( + 'name' => 'temp', + 'url' => 'http://localhost/thumbtest', + 'backend' => $this->backend + ) ); + $this->handler = new PNGHandler(); + } + + /** + * @covers PNGHandler::getMetadata + */ + public function testInvalidFile() { + $res = $this->handler->getMetadata( null, $this->filePath . '/README' ); + $this->assertEquals( PNGHandler::BROKEN_FILE, $res ); + } + + /** + * @param $filename String basename of the file to check + * @param $expected boolean Expected result. + * @dataProvider provideIsAnimated + * @covers PNGHandler::isAnimatedImage + */ + public function testIsAnimanted( $filename, $expected ) { + $file = $this->dataFile( $filename, 'image/png' ); + $actual = $this->handler->isAnimatedImage( $file ); + $this->assertEquals( $expected, $actual ); + } + + public static function provideIsAnimated() { + return array( + array( 'Animated_PNG_example_bouncing_beach_ball.png', true ), + array( '1bit-png.png', false ), + ); + } + + /** + * @param $filename String + * @param $expected Integer Total image area + * @dataProvider provideGetImageArea + * @covers PNGHandler::getImageArea + */ + public function testGetImageArea( $filename, $expected ) { + $file = $this->dataFile( $filename, 'image/png' ); + $actual = $this->handler->getImageArea( $file, $file->getWidth(), $file->getHeight() ); + $this->assertEquals( $expected, $actual ); + } + + public static function provideGetImageArea() { + return array( + array( '1bit-png.png', 2500 ), + array( 'greyscale-png.png', 2500 ), + array( 'Png-native-test.png', 126000 ), + array( 'Animated_PNG_example_bouncing_beach_ball.png', 10000 ), + ); + } + + /** + * @param $metadata String Serialized metadata + * @param $expected Integer One of the class constants of PNGHandler + * @dataProvider provideIsMetadataValid + * @covers PNGHandler::isMetadataValid + */ + public function testIsMetadataValid( $metadata, $expected ) { + $actual = $this->handler->isMetadataValid( null, $metadata ); + $this->assertEquals( $expected, $actual ); + } + + public static function provideIsMetadataValid() { + return array( + array( PNGHandler::BROKEN_FILE, PNGHandler::METADATA_GOOD ), + array( '', PNGHandler::METADATA_BAD ), + array( null, PNGHandler::METADATA_BAD ), + array( 'Something invalid!', PNGHandler::METADATA_BAD ), + array( 'a:6:{s:10:"frameCount";i:0;s:9:"loopCount";i:1;s:8:"duration";d:0;s:8:"bitDepth";i:8;s:9:"colorType";s:10:"truecolour";s:8:"metadata";a:1:{s:15:"_MW_PNG_VERSION";i:1;}}', PNGHandler::METADATA_GOOD ), + ); + } + + /** + * @param $filename String + * @param $expected String Serialized array + * @dataProvider provideGetMetadata + * @covers PNGHandler::getMetadata + */ + public function testGetMetadata( $filename, $expected ) { + $file = $this->dataFile( $filename, 'image/png' ); + $actual = $this->handler->getMetadata( $file, "$this->filePath/$filename" ); +// $this->assertEquals( unserialize( $expected ), unserialize( $actual ) ); + $this->assertEquals( ( $expected ), ( $actual ) ); + } + + public static function provideGetMetadata() { + return array( + array( 'rgb-na-png.png', 'a:6:{s:10:"frameCount";i:0;s:9:"loopCount";i:1;s:8:"duration";d:0;s:8:"bitDepth";i:8;s:9:"colorType";s:10:"truecolour";s:8:"metadata";a:1:{s:15:"_MW_PNG_VERSION";i:1;}}' ), + array( 'xmp.png', 'a:6:{s:10:"frameCount";i:0;s:9:"loopCount";i:1;s:8:"duration";d:0;s:8:"bitDepth";i:1;s:9:"colorType";s:14:"index-coloured";s:8:"metadata";a:2:{s:12:"SerialNumber";s:9:"123456789";s:15:"_MW_PNG_VERSION";i:1;}}' ), + ); + } + + private function dataFile( $name, $type ) { + return new UnregisteredLocalFile( false, $this->repo, + "mwstore://localtesting/data/$name", $type ); + } +} diff --git a/tests/phpunit/includes/media/SVGMetadataExtractorTest.php b/tests/phpunit/includes/media/SVGMetadataExtractorTest.php new file mode 100644 index 00000000..257009b0 --- /dev/null +++ b/tests/phpunit/includes/media/SVGMetadataExtractorTest.php @@ -0,0 +1,112 @@ +assertMetadata( $infile, $expected ); + } + + /** + * @dataProvider provideSvgFilesWithXMLMetadata + */ + public function testGetXMLMetadata( $infile, $expected ) { + $r = new XMLReader(); + if ( !method_exists( $r, 'readInnerXML' ) ) { + $this->markTestSkipped( 'XMLReader::readInnerXML() does not exist (libxml >2.6.20 needed).' ); + + return; + } + $this->assertMetadata( $infile, $expected ); + } + + function assertMetadata( $infile, $expected ) { + try { + $data = SVGMetadataExtractor::getMetadata( $infile ); + $this->assertEquals( $expected, $data, 'SVG metadata extraction test' ); + } catch ( MWException $e ) { + if ( $expected === false ) { + $this->assertTrue( true, 'SVG metadata extracted test (expected failure)' ); + } else { + throw $e; + } + } + } + + public static function provideSvgFiles() { + $base = __DIR__ . '/../../data/media'; + + return array( + array( + "$base/Wikimedia-logo.svg", + array( + 'width' => 1024, + 'height' => 1024, + 'originalWidth' => '1024', + 'originalHeight' => '1024', + ) + ), + array( + "$base/QA_icon.svg", + array( + 'width' => 60, + 'height' => 60, + 'originalWidth' => '60', + 'originalHeight' => '60', + ) + ), + array( + "$base/Gtk-media-play-ltr.svg", + array( + 'width' => 60, + 'height' => 60, + 'originalWidth' => '60.0000000', + 'originalHeight' => '60.0000000', + ) + ), + array( + "$base/Toll_Texas_1.svg", + // This file triggered bug 31719, needs entity expansion in the xmlns checks + array( + 'width' => 385, + 'height' => 385, + 'originalWidth' => '385', + 'originalHeight' => '385.0004883', + ) + ) + ); + } + + public static function provideSvgFilesWithXMLMetadata() { + $base = __DIR__ . '/../../data/media'; + $metadata = ' + + image/svg+xml + + + '; + $metadata = str_replace( "\r", '', $metadata ); // Windows compat + return array( + array( + "$base/US_states_by_total_state_tax_revenue.svg", + array( + 'height' => 593, + 'metadata' => $metadata, + 'width' => 959, + 'originalWidth' => '958.69', + 'originalHeight' => '592.78998', + ) + ), + ); + } +} diff --git a/tests/phpunit/includes/media/TiffTest.php b/tests/phpunit/includes/media/TiffTest.php new file mode 100644 index 00000000..8d74b98d --- /dev/null +++ b/tests/phpunit/includes/media/TiffTest.php @@ -0,0 +1,39 @@ +markTestSkipped( "This test needs the exif extension." ); + } + + $this->setMwGlobals( 'wgShowEXIF', true ); + + $this->filePath = __DIR__ . '/../../data/media/'; + $this->handler = new TiffHandler; + } + + /** + * @covers TiffHandler::getMetadata + */ + public function testInvalidFile() { + $res = $this->handler->getMetadata( null, $this->filePath . 'README' ); + $this->assertEquals( ExifBitmapHandler::BROKEN_FILE, $res ); + } + + /** + * @covers TiffHandler::getMetadata + */ + public function testTiffMetadataExtraction() { + $res = $this->handler->getMetadata( null, $this->filePath . 'test.tiff' ); + $expected = 'a:16:{s:10:"ImageWidth";i:20;s:11:"ImageLength";i:20;s:13:"BitsPerSample";a:3:{i:0;i:8;i:1;i:8;i:2;i:8;}s:11:"Compression";i:5;s:25:"PhotometricInterpretation";i:2;s:16:"ImageDescription";s:17:"Created with GIMP";s:12:"StripOffsets";i:8;s:11:"Orientation";i:1;s:15:"SamplesPerPixel";i:3;s:12:"RowsPerStrip";i:64;s:15:"StripByteCounts";i:238;s:11:"XResolution";s:19:"1207959552/16777216";s:11:"YResolution";s:19:"1207959552/16777216";s:19:"PlanarConfiguration";i:1;s:14:"ResolutionUnit";i:2;s:22:"MEDIAWIKI_EXIF_VERSION";i:2;}'; + // Re-unserialize in case there are subtle differences between how versions + // of php serialize stuff. + $this->assertEquals( unserialize( $expected ), unserialize( $res ) ); + } +} diff --git a/tests/phpunit/includes/media/XMPTest.php b/tests/phpunit/includes/media/XMPTest.php new file mode 100644 index 00000000..d12e9b00 --- /dev/null +++ b/tests/phpunit/includes/media/XMPTest.php @@ -0,0 +1,166 @@ +markTestSkipped( 'Requires libxml to do XMP parsing' ); + } + } + + /** + * Put XMP in, compare what comes out... + * + * @param $xmp String the actual xml data. + * @param $expected Array expected result of parsing the xmp. + * @param $info String Short sentence on what's being tested. + * + * @throws Exception + * @dataProvider provideXMPParse + */ + public function testXMPParse( $xmp, $expected, $info ) { + if ( !is_string( $xmp ) || !is_array( $expected ) ) { + throw new Exception( "Invalid data provided to " . __METHOD__ ); + } + $reader = new XMPReader; + $reader->parse( $xmp ); + $this->assertEquals( $expected, $reader->getResults(), $info, 0.0000000001 ); + } + + public static function provideXMPParse() { + $xmpPath = __DIR__ . '/../../data/xmp/'; + $data = array(); + + // $xmpFiles format: array of arrays with first arg file base name, + // with the actual file having .xmp on the end for the xmp + // and .result.php on the end for a php file containing the result + // array. Second argument is some info on what's being tested. + $xmpFiles = array( + array( '1', 'parseType=Resource test' ), + array( '2', 'Structure with mixed attribute and element props' ), + array( '3', 'Extra qualifiers (that should be ignored)' ), + array( '3-invalid', 'Test ignoring qualifiers that look like normal props' ), + array( '4', 'Flash as qualifier' ), + array( '5', 'Flash as qualifier 2' ), + array( '6', 'Multiple rdf:Description' ), + array( '7', 'Generic test of several property types' ), + array( 'flash', 'Test of Flash property' ), + array( 'invalid-child-not-struct', 'Test child props not in struct or ignored' ), + array( 'no-recognized-props', 'Test namespace and no recognized props' ), + array( 'no-namespace', 'Test non-namespaced attributes are ignored' ), + array( 'bag-for-seq', "Allow bag's instead of seq's. (bug 27105)" ), + array( 'utf16BE', 'UTF-16BE encoding' ), + array( 'utf16LE', 'UTF-16LE encoding' ), + array( 'utf32BE', 'UTF-32BE encoding' ), + array( 'utf32LE', 'UTF-32LE encoding' ), + array( 'xmpExt', 'Extended XMP missing second part' ), + array( 'gps', 'Handling of exif GPS parameters in XMP' ), + ); + + foreach ( $xmpFiles as $file ) { + $xmp = file_get_contents( $xmpPath . $file[0] . '.xmp' ); + // I'm not sure if this is the best way to handle getting the + // result array, but it seems kind of big to put directly in the test + // file. + $result = null; + include $xmpPath . $file[0] . '.result.php'; + $data[] = array( $xmp, $result, '[' . $file[0] . '.xmp] ' . $file[1] ); + } + + return $data; + } + + /** Test ExtendedXMP block support. (Used when the XMP has to be split + * over multiple jpeg segments, due to 64k size limit on jpeg segments. + * + * @todo This is based on what the standard says. Need to find a real + * world example file to double check the support for this is right. + */ + public function testExtendedXMP() { + $xmpPath = __DIR__ . '/../../data/xmp/'; + $standardXMP = file_get_contents( $xmpPath . 'xmpExt.xmp' ); + $extendedXMP = file_get_contents( $xmpPath . 'xmpExt2.xmp' ); + + $md5sum = '28C74E0AC2D796886759006FBE2E57B7'; // of xmpExt2.xmp + $length = pack( 'N', strlen( $extendedXMP ) ); + $offset = pack( 'N', 0 ); + $extendedPacket = $md5sum . $length . $offset . $extendedXMP; + + $reader = new XMPReader(); + $reader->parse( $standardXMP ); + $reader->parseExtended( $extendedPacket ); + $actual = $reader->getResults(); + + $expected = array( + 'xmp-exif' => array( + 'DigitalZoomRatio' => '0/10', + 'Flash' => 9, + 'FNumber' => '2/10', + ) + ); + + $this->assertEquals( $expected, $actual ); + } + + /** + * This test has an extended XMP block with a wrong guid (md5sum) + * and thus should only return the StandardXMP, not the ExtendedXMP. + */ + public function testExtendedXMPWithWrongGUID() { + $xmpPath = __DIR__ . '/../../data/xmp/'; + $standardXMP = file_get_contents( $xmpPath . 'xmpExt.xmp' ); + $extendedXMP = file_get_contents( $xmpPath . 'xmpExt2.xmp' ); + + $md5sum = '28C74E0AC2D796886759006FBE2E57B9'; // Note last digit. + $length = pack( 'N', strlen( $extendedXMP ) ); + $offset = pack( 'N', 0 ); + $extendedPacket = $md5sum . $length . $offset . $extendedXMP; + + $reader = new XMPReader(); + $reader->parse( $standardXMP ); + $reader->parseExtended( $extendedPacket ); + $actual = $reader->getResults(); + + $expected = array( + 'xmp-exif' => array( + 'DigitalZoomRatio' => '0/10', + 'Flash' => 9, + ) + ); + + $this->assertEquals( $expected, $actual ); + } + + /** + * Have a high offset to simulate a missing packet, + * which should cause it to ignore the ExtendedXMP packet. + */ + public function testExtendedXMPMissingPacket() { + $xmpPath = __DIR__ . '/../../data/xmp/'; + $standardXMP = file_get_contents( $xmpPath . 'xmpExt.xmp' ); + $extendedXMP = file_get_contents( $xmpPath . 'xmpExt2.xmp' ); + + $md5sum = '28C74E0AC2D796886759006FBE2E57B7'; // of xmpExt2.xmp + $length = pack( 'N', strlen( $extendedXMP ) ); + $offset = pack( 'N', 2048 ); + $extendedPacket = $md5sum . $length . $offset . $extendedXMP; + + $reader = new XMPReader(); + $reader->parse( $standardXMP ); + $reader->parseExtended( $extendedPacket ); + $actual = $reader->getResults(); + + $expected = array( + 'xmp-exif' => array( + 'DigitalZoomRatio' => '0/10', + 'Flash' => 9, + ) + ); + + $this->assertEquals( $expected, $actual ); + } +} diff --git a/tests/phpunit/includes/media/XMPValidateTest.php b/tests/phpunit/includes/media/XMPValidateTest.php new file mode 100644 index 00000000..96bf5e47 --- /dev/null +++ b/tests/phpunit/includes/media/XMPValidateTest.php @@ -0,0 +1,46 @@ +assertEquals( $expected, $value ); + } + + public static function provideDates() { + /* For reference valid date formats are: + * YYYY + * YYYY-MM + * YYYY-MM-DD + * YYYY-MM-DDThh:mmTZD + * YYYY-MM-DDThh:mm:ssTZD + * YYYY-MM-DDThh:mm:ss.sTZD + * (Time zone is optional) + */ + return array( + array( '1992', '1992' ), + array( '1992-04', '1992:04' ), + array( '1992-02-01', '1992:02:01' ), + array( '2011-09-29', '2011:09:29' ), + array( '1982-12-15T20:12', '1982:12:15 20:12' ), + array( '1982-12-15T20:12Z', '1982:12:15 20:12' ), + array( '1982-12-15T20:12+02:30', '1982:12:15 22:42' ), + array( '1982-12-15T01:12-02:30', '1982:12:14 22:42' ), + array( '1982-12-15T20:12:11', '1982:12:15 20:12:11' ), + array( '1982-12-15T20:12:11Z', '1982:12:15 20:12:11' ), + array( '1982-12-15T20:12:11+01:10', '1982:12:15 21:22:11' ), + array( '2045-12-15T20:12:11', '2045:12:15 20:12:11' ), + array( '1867-06-01T15:00:00', '1867:06:01 15:00:00' ), + /* some invalid ones */ + array( '2001--12', null ), + array( '2001-5-12', null ), + array( '2001-5-12TZ', null ), + array( '2001-05-12T15', null ), + array( '2001-12T15:13', null ), + ); + } +} diff --git a/tests/phpunit/includes/normal/CleanUpTest.php b/tests/phpunit/includes/normal/CleanUpTest.php new file mode 100644 index 00000000..52dd2ef5 --- /dev/null +++ b/tests/phpunit/includes/normal/CleanUpTest.php @@ -0,0 +1,409 @@ + + * http://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 + */ + +/** + * Additional tests for UtfNormal::cleanUp() function, inclusion + * regression checks for known problems. + * Requires PHPUnit. + * + * @ingroup UtfNormal + * @group Large + * + * We ignore code coverage for this test suite until they are rewritten + * to use data providers (bug 46561). + * @codeCoverageIgnore + */ +class CleanUpTest extends MediaWikiTestCase { + /** @todo document */ + public function testAscii() { + $text = 'This is plain ASCII text.'; + $this->assertEquals( $text, UtfNormal::cleanUp( $text ) ); + } + + /** @todo document */ + public function testNull() { + $text = "a \x00 null"; + $expect = "a \xef\xbf\xbd null"; + $this->assertEquals( + bin2hex( $expect ), + bin2hex( UtfNormal::cleanUp( $text ) ) ); + } + + /** @todo document */ + public function testLatin() { + $text = "L'\xc3\xa9cole"; + $this->assertEquals( $text, UtfNormal::cleanUp( $text ) ); + } + + /** @todo document */ + public function testLatinNormal() { + $text = "L'e\xcc\x81cole"; + $expect = "L'\xc3\xa9cole"; + $this->assertEquals( $expect, UtfNormal::cleanUp( $text ) ); + } + + /** + * This test is *very* expensive! + * @todo document + */ + function XtestAllChars() { + $rep = UTF8_REPLACEMENT; + for ( $i = 0x0; $i < UNICODE_MAX; $i++ ) { + $char = codepointToUtf8( $i ); + $clean = UtfNormal::cleanUp( $char ); + $x = sprintf( "%04X", $i ); + + if ( $i % 0x1000 == 0 ) { + echo "U+$x\n"; + } + + if ( $i == 0x0009 || + $i == 0x000a || + $i == 0x000d || + ( $i > 0x001f && $i < UNICODE_SURROGATE_FIRST ) || + ( $i > UNICODE_SURROGATE_LAST && $i < 0xfffe ) || + ( $i > 0xffff && $i <= UNICODE_MAX ) + ) { + if ( isset( UtfNormal::$utfCanonicalComp[$char] ) || isset( UtfNormal::$utfCanonicalDecomp[$char] ) ) { + $comp = UtfNormal::NFC( $char ); + $this->assertEquals( + bin2hex( $comp ), + bin2hex( $clean ), + "U+$x should be decomposed" ); + } else { + $this->assertEquals( + bin2hex( $char ), + bin2hex( $clean ), + "U+$x should be intact" ); + } + } else { + $this->assertEquals( bin2hex( $rep ), bin2hex( $clean ), $x ); + } + } + } + + /** @todo document */ + public function testAllBytes() { + $this->doTestBytes( '', '' ); + $this->doTestBytes( 'x', '' ); + $this->doTestBytes( '', 'x' ); + $this->doTestBytes( 'x', 'x' ); + } + + /** @todo document */ + function doTestBytes( $head, $tail ) { + for ( $i = 0x0; $i < 256; $i++ ) { + $char = $head . chr( $i ) . $tail; + $clean = UtfNormal::cleanUp( $char ); + $x = sprintf( "%02X", $i ); + + if ( $i == 0x0009 || + $i == 0x000a || + $i == 0x000d || + ( $i > 0x001f && $i < 0x80 ) + ) { + $this->assertEquals( + bin2hex( $char ), + bin2hex( $clean ), + "ASCII byte $x should be intact" ); + if ( $char != $clean ) { + return; + } + } else { + $norm = $head . UTF8_REPLACEMENT . $tail; + $this->assertEquals( + bin2hex( $norm ), + bin2hex( $clean ), + "Forbidden byte $x should be rejected" ); + if ( $norm != $clean ) { + return; + } + } + } + } + + /** @todo document */ + public function testDoubleBytes() { + $this->doTestDoubleBytes( '', '' ); + $this->doTestDoubleBytes( 'x', '' ); + $this->doTestDoubleBytes( '', 'x' ); + $this->doTestDoubleBytes( 'x', 'x' ); + } + + /** + * @todo document + */ + function doTestDoubleBytes( $head, $tail ) { + for ( $first = 0xc0; $first < 0x100; $first += 2 ) { + for ( $second = 0x80; $second < 0x100; $second += 2 ) { + $char = $head . chr( $first ) . chr( $second ) . $tail; + $clean = UtfNormal::cleanUp( $char ); + $x = sprintf( "%02X,%02X", $first, $second ); + if ( $first > 0xc1 && + $first < 0xe0 && + $second < 0xc0 + ) { + $norm = UtfNormal::NFC( $char ); + $this->assertEquals( + bin2hex( $norm ), + bin2hex( $clean ), + "Pair $x should be intact" ); + if ( $norm != $clean ) { + return; + } + } elseif ( $first > 0xfd || $second > 0xbf ) { + # fe and ff are not legal head bytes -- expect two replacement chars + $norm = $head . UTF8_REPLACEMENT . UTF8_REPLACEMENT . $tail; + $this->assertEquals( + bin2hex( $norm ), + bin2hex( $clean ), + "Forbidden pair $x should be rejected" ); + if ( $norm != $clean ) { + return; + } + } else { + $norm = $head . UTF8_REPLACEMENT . $tail; + $this->assertEquals( + bin2hex( $norm ), + bin2hex( $clean ), + "Forbidden pair $x should be rejected" ); + if ( $norm != $clean ) { + return; + } + } + } + } + } + + /** @todo document */ + public function testTripleBytes() { + $this->doTestTripleBytes( '', '' ); + $this->doTestTripleBytes( 'x', '' ); + $this->doTestTripleBytes( '', 'x' ); + $this->doTestTripleBytes( 'x', 'x' ); + } + + /** @todo document */ + function doTestTripleBytes( $head, $tail ) { + for ( $first = 0xc0; $first < 0x100; $first += 2 ) { + for ( $second = 0x80; $second < 0x100; $second += 2 ) { + #for( $third = 0x80; $third < 0x100; $third++ ) { + for ( $third = 0x80; $third < 0x81; $third++ ) { + $char = $head . chr( $first ) . chr( $second ) . chr( $third ) . $tail; + $clean = UtfNormal::cleanUp( $char ); + $x = sprintf( "%02X,%02X,%02X", $first, $second, $third ); + + if ( $first >= 0xe0 && + $first < 0xf0 && + $second < 0xc0 && + $third < 0xc0 + ) { + if ( $first == 0xe0 && $second < 0xa0 ) { + $this->assertEquals( + bin2hex( $head . UTF8_REPLACEMENT . $tail ), + bin2hex( $clean ), + "Overlong triplet $x should be rejected" ); + } elseif ( $first == 0xed && + ( chr( $first ) . chr( $second ) . chr( $third ) ) >= UTF8_SURROGATE_FIRST + ) { + $this->assertEquals( + bin2hex( $head . UTF8_REPLACEMENT . $tail ), + bin2hex( $clean ), + "Surrogate triplet $x should be rejected" ); + } else { + $this->assertEquals( + bin2hex( UtfNormal::NFC( $char ) ), + bin2hex( $clean ), + "Triplet $x should be intact" ); + } + } elseif ( $first > 0xc1 && $first < 0xe0 && $second < 0xc0 ) { + $this->assertEquals( + bin2hex( UtfNormal::NFC( $head . chr( $first ) . chr( $second ) ) . UTF8_REPLACEMENT . $tail ), + bin2hex( $clean ), + "Valid 2-byte $x + broken tail" ); + } elseif ( $second > 0xc1 && $second < 0xe0 && $third < 0xc0 ) { + $this->assertEquals( + bin2hex( $head . UTF8_REPLACEMENT . UtfNormal::NFC( chr( $second ) . chr( $third ) . $tail ) ), + bin2hex( $clean ), + "Broken head + valid 2-byte $x" ); + } elseif ( ( $first > 0xfd || $second > 0xfd ) && + ( ( $second > 0xbf && $third > 0xbf ) || + ( $second < 0xc0 && $third < 0xc0 ) || + ( $second > 0xfd ) || + ( $third > 0xfd ) ) + ) { + # fe and ff are not legal head bytes -- expect three replacement chars + $this->assertEquals( + bin2hex( $head . UTF8_REPLACEMENT . UTF8_REPLACEMENT . UTF8_REPLACEMENT . $tail ), + bin2hex( $clean ), + "Forbidden triplet $x should be rejected" ); + } elseif ( $first > 0xc2 && $second < 0xc0 && $third < 0xc0 ) { + $this->assertEquals( + bin2hex( $head . UTF8_REPLACEMENT . $tail ), + bin2hex( $clean ), + "Forbidden triplet $x should be rejected" ); + } else { + $this->assertEquals( + bin2hex( $head . UTF8_REPLACEMENT . UTF8_REPLACEMENT . $tail ), + bin2hex( $clean ), + "Forbidden triplet $x should be rejected" ); + } + } + } + } + } + + /** @todo document */ + public function testChunkRegression() { + # Check for regression against a chunking bug + $text = "\x46\x55\xb8" . + "\xdc\x96" . + "\xee" . + "\xe7" . + "\x44" . + "\xaa" . + "\x2f\x25"; + $expect = "\x46\x55\xef\xbf\xbd" . + "\xdc\x96" . + "\xef\xbf\xbd" . + "\xef\xbf\xbd" . + "\x44" . + "\xef\xbf\xbd" . + "\x2f\x25"; + + $this->assertEquals( + bin2hex( $expect ), + bin2hex( UtfNormal::cleanUp( $text ) ) ); + } + + /** @todo document */ + public function testInterposeRegression() { + $text = "\x4e\x30" . + "\xb1" . # bad tail + "\x3a" . + "\x92" . # bad tail + "\x62\x3a" . + "\x84" . # bad tail + "\x43" . + "\xc6" . # bad head + "\x3f" . + "\x92" . # bad tail + "\xad" . # bad tail + "\x7d" . + "\xd9\x95"; + + $expect = "\x4e\x30" . + "\xef\xbf\xbd" . + "\x3a" . + "\xef\xbf\xbd" . + "\x62\x3a" . + "\xef\xbf\xbd" . + "\x43" . + "\xef\xbf\xbd" . + "\x3f" . + "\xef\xbf\xbd" . + "\xef\xbf\xbd" . + "\x7d" . + "\xd9\x95"; + + $this->assertEquals( + bin2hex( $expect ), + bin2hex( UtfNormal::cleanUp( $text ) ) ); + } + + /** @todo document */ + public function testOverlongRegression() { + $text = "\x67" . + "\x1a" . # forbidden ascii + "\xea" . # bad head + "\xc1\xa6" . # overlong sequence + "\xad" . # bad tail + "\x1c" . # forbidden ascii + "\xb0" . # bad tail + "\x3c" . + "\x9e"; # bad tail + $expect = "\x67" . + "\xef\xbf\xbd" . + "\xef\xbf\xbd" . + "\xef\xbf\xbd" . + "\xef\xbf\xbd" . + "\xef\xbf\xbd" . + "\xef\xbf\xbd" . + "\x3c" . + "\xef\xbf\xbd"; + $this->assertEquals( + bin2hex( $expect ), + bin2hex( UtfNormal::cleanUp( $text ) ) ); + } + + /** @todo document */ + public function testSurrogateRegression() { + $text = "\xed\xb4\x96" . # surrogate 0xDD16 + "\x83" . # bad tail + "\xb4" . # bad tail + "\xac"; # bad head + $expect = "\xef\xbf\xbd" . + "\xef\xbf\xbd" . + "\xef\xbf\xbd" . + "\xef\xbf\xbd"; + $this->assertEquals( + bin2hex( $expect ), + bin2hex( UtfNormal::cleanUp( $text ) ) ); + } + + /** @todo document */ + public function testBomRegression() { + $text = "\xef\xbf\xbe" . # U+FFFE, illegal char + "\xb2" . # bad tail + "\xef" . # bad head + "\x59"; + $expect = "\xef\xbf\xbd" . + "\xef\xbf\xbd" . + "\xef\xbf\xbd" . + "\x59"; + $this->assertEquals( + bin2hex( $expect ), + bin2hex( UtfNormal::cleanUp( $text ) ) ); + } + + /** @todo document */ + public function testForbiddenRegression() { + $text = "\xef\xbf\xbf"; # U+FFFF, illegal char + $expect = "\xef\xbf\xbd"; + $this->assertEquals( + bin2hex( $expect ), + bin2hex( UtfNormal::cleanUp( $text ) ) ); + } + + /** @todo document */ + public function testHangulRegression() { + $text = "\xed\x9c\xaf" . # Hangul char + "\xe1\x87\x81"; # followed by another final jamo + $expect = $text; # Should *not* change. + $this->assertEquals( + bin2hex( $expect ), + bin2hex( UtfNormal::cleanUp( $text ) ) ); + } +} diff --git a/tests/phpunit/includes/objectcache/BagOStuffTest.php b/tests/phpunit/includes/objectcache/BagOStuffTest.php new file mode 100644 index 00000000..aa783943 --- /dev/null +++ b/tests/phpunit/includes/objectcache/BagOStuffTest.php @@ -0,0 +1,149 @@ + + */ +class BagOStuffTest extends MediaWikiTestCase { + private $cache; + + protected function setUp() { + parent::setUp(); + + // type defined through parameter + if ( $this->getCliArg( 'use-bagostuff=' ) ) { + $name = $this->getCliArg( 'use-bagostuff=' ); + + $this->cache = ObjectCache::newFromId( $name ); + } else { + // no type defined - use simple hash + $this->cache = new HashBagOStuff; + } + + $this->cache->delete( wfMemcKey( 'test' ) ); + } + + protected function tearDown() { + } + + public function testMerge() { + $key = wfMemcKey( 'test' ); + + $usleep = 0; + + /** + * Callback method: append "merged" to whatever is in cache. + * + * @param BagOStuff $cache + * @param string $key + * @param int $existingValue + * @use int $usleep + * @return int + */ + $callback = function ( BagOStuff $cache, $key, $existingValue ) use ( &$usleep ) { + // let's pretend this is an expensive callback to test concurrent merge attempts + usleep( $usleep ); + + if ( $existingValue === false ) { + return 'merged'; + } + + return $existingValue . 'merged'; + }; + + // merge on non-existing value + $merged = $this->cache->merge( $key, $callback, 0 ); + $this->assertTrue( $merged ); + $this->assertEquals( $this->cache->get( $key ), 'merged' ); + + // merge on existing value + $merged = $this->cache->merge( $key, $callback, 0 ); + $this->assertTrue( $merged ); + $this->assertEquals( $this->cache->get( $key ), 'mergedmerged' ); + + /* + * Test concurrent merges by forking this process, if: + * - not manually called with --use-bagostuff + * - pcntl_fork is supported by the system + * - cache type will correctly support calls over forks + */ + $fork = (bool)$this->getCliArg( 'use-bagostuff=' ); + $fork &= function_exists( 'pcntl_fork' ); + $fork &= !$this->cache instanceof HashBagOStuff; + $fork &= !$this->cache instanceof EmptyBagOStuff; + $fork &= !$this->cache instanceof MultiWriteBagOStuff; + if ( $fork ) { + // callback should take awhile now so that we can test concurrent merge attempts + $usleep = 5000; + + $pid = pcntl_fork(); + if ( $pid == -1 ) { + // can't fork, ignore this test... + } elseif ( $pid ) { + // wait a little, making sure that the child process is calling merge + usleep( 3000 ); + + // attempt a merge - this should fail + $merged = $this->cache->merge( $key, $callback, 0, 1 ); + + // merge has failed because child process was merging (and we only attempted once) + $this->assertFalse( $merged ); + + // make sure the child's merge is completed and verify + usleep( 3000 ); + $this->assertEquals( $this->cache->get( $key ), 'mergedmergedmerged' ); + } else { + $this->cache->merge( $key, $callback, 0, 1 ); + + // Note: I'm not even going to check if the merge worked, I'll + // compare values in the parent process to test if this merge worked. + // I'm just going to exit this child process, since I don't want the + // child to output any test results (would be rather confusing to + // have test output twice) + exit; + } + } + } + + public function testAdd() { + $key = wfMemcKey( 'test' ); + $this->assertTrue( $this->cache->add( $key, 'test' ) ); + } + + public function testGet() { + $value = array( 'this' => 'is', 'a' => 'test' ); + + $key = wfMemcKey( 'test' ); + $this->cache->add( $key, $value ); + $this->assertEquals( $this->cache->get( $key ), $value ); + } + + /** + * @covers BagOStuff::incr + */ + public function testIncr() { + $key = wfMemcKey( 'test' ); + $this->cache->add( $key, 0 ); + $this->cache->incr( $key ); + $expectedValue = 1; + $actualValue = $this->cache->get( $key ); + $this->assertEquals( $expectedValue, $actualValue, 'Value should be 1 after incrementing' ); + } + + public function testGetMulti() { + $value1 = array( 'this' => 'is', 'a' => 'test' ); + $value2 = array( 'this' => 'is', 'another' => 'test' ); + + $key1 = wfMemcKey( 'test1' ); + $key2 = wfMemcKey( 'test2' ); + + $this->cache->add( $key1, $value1 ); + $this->cache->add( $key2, $value2 ); + + $this->assertEquals( $this->cache->getMulti( array( $key1, $key2 ) ), array( $key1 => $value1, $key2 => $value2 ) ); + + // cleanup + $this->cache->delete( $key1 ); + $this->cache->delete( $key2 ); + } +} diff --git a/tests/phpunit/includes/parser/MagicVariableTest.php b/tests/phpunit/includes/parser/MagicVariableTest.php new file mode 100644 index 00000000..c2c97c01 --- /dev/null +++ b/tests/phpunit/includes/parser/MagicVariableTest.php @@ -0,0 +1,242 @@ +setMwGlobals( array( + 'wgLanguageCode' => 'en', + 'wgContLang' => $contLang, + ) ); + + $this->testParser = new Parser(); + $this->testParser->Options( ParserOptions::newFromUserAndLang( new User, $contLang ) ); + + # initialize parser output + $this->testParser->clearState(); + + # Needs a title to do magic word stuff + $title = Title::newFromText( 'Tests' ); + $title->mRedirect = false; # Else it needs a db connection just to check if it's a redirect (when deciding the page language) + + $this->testParser->setTitle( $title ); + } + + /** + * @param int $num upper limit for numbers + * @return array of numbers from 1 up to $num + */ + private static function createProviderUpTo( $num ) { + $ret = array(); + for ( $i = 1; $i <= $num; $i++ ) { + $ret[] = array( $i ); + } + + return $ret; + } + + /** + * @return array of months numbers (as an integer) + */ + public static function provideMonths() { + return self::createProviderUpTo( 12 ); + } + + /** + * @return array of days numbers (as an integer) + */ + public static function provideDays() { + return self::createProviderUpTo( 31 ); + } + + ############### TESTS ############################################# + # @todo FIXME: + # - those got copy pasted, we can probably make them cleaner + # - tests are lacking useful messages + + # day + + /** @dataProvider provideDays */ + public function testCurrentdayIsUnPadded( $day ) { + $this->assertUnPadded( 'currentday', $day ); + } + + /** @dataProvider provideDays */ + public function testCurrentdaytwoIsZeroPadded( $day ) { + $this->assertZeroPadded( 'currentday2', $day ); + } + + /** @dataProvider provideDays */ + public function testLocaldayIsUnPadded( $day ) { + $this->assertUnPadded( 'localday', $day ); + } + + /** @dataProvider provideDays */ + public function testLocaldaytwoIsZeroPadded( $day ) { + $this->assertZeroPadded( 'localday2', $day ); + } + + # month + + /** @dataProvider provideMonths */ + public function testCurrentmonthIsZeroPadded( $month ) { + $this->assertZeroPadded( 'currentmonth', $month ); + } + + /** @dataProvider provideMonths */ + public function testCurrentmonthoneIsUnPadded( $month ) { + $this->assertUnPadded( 'currentmonth1', $month ); + } + + /** @dataProvider provideMonths */ + public function testLocalmonthIsZeroPadded( $month ) { + $this->assertZeroPadded( 'localmonth', $month ); + } + + /** @dataProvider provideMonths */ + public function testLocalmonthoneIsUnPadded( $month ) { + $this->assertUnPadded( 'localmonth1', $month ); + } + + # revision day + + /** @dataProvider provideDays */ + public function testRevisiondayIsUnPadded( $day ) { + $this->assertUnPadded( 'revisionday', $day ); + } + + /** @dataProvider provideDays */ + public function testRevisiondaytwoIsZeroPadded( $day ) { + $this->assertZeroPadded( 'revisionday2', $day ); + } + + # revision month + + /** @dataProvider provideMonths */ + public function testRevisionmonthIsZeroPadded( $month ) { + $this->assertZeroPadded( 'revisionmonth', $month ); + } + + /** @dataProvider provideMonths */ + public function testRevisionmonthoneIsUnPadded( $month ) { + $this->assertUnPadded( 'revisionmonth1', $month ); + } + + /** + * Rough tests for {{SERVERNAME}} magic word + * Bug 31176 + * @group Database + * @dataProvider provideDataServernameFromDifferentProtocols + */ + public function testServernameFromDifferentProtocols( $server ) { + $this->setMwGlobals( 'wgServer', $server ); + + $this->assertMagic( 'localhost', 'servername' ); + } + + public static function provideDataServernameFromDifferentProtocols() { + return array( + array( 'http://localhost/' ), + array( 'https://localhost/' ), + array( '//localhost/' ), # bug 31176 + ); + } + + ############### HELPERS ############################################ + + /** assertion helper expecting a magic output which is zero padded */ + public function assertZeroPadded( $magic, $value ) { + $this->assertMagicPadding( $magic, $value, '%02d' ); + } + + /** assertion helper expecting a magic output which is unpadded */ + public function assertUnPadded( $magic, $value ) { + $this->assertMagicPadding( $magic, $value, '%d' ); + } + + /** + * Main assertion helper for magic variables padding + * @param $magic string Magic variable name + * @param $value mixed Month or day + * @param $format string sprintf format for $value + */ + private function assertMagicPadding( $magic, $value, $format ) { + # Initialize parser timestamp as year 2010 at 12h34 56s. + # month and day are given by the caller ($value). Month < 12! + if ( $value > 12 ) { + $month = $value % 12; + } else { + $month = $value; + } + + $this->setParserTS( + sprintf( '2010%02d%02d123456', $month, $value ) + ); + + # please keep the following commented line of code. It helps debugging. + //print "\nDEBUG (value $value):" . sprintf( '2010%02d%02d123456', $value, $value ) . "\n"; + + # format expectation and test it + $expected = sprintf( $format, $value ); + $this->assertMagic( $expected, $magic ); + } + + /** helper to set the parser timestamp and revision timestamp */ + private function setParserTS( $ts ) { + $this->testParser->Options()->setTimestamp( $ts ); + $this->testParser->mRevisionTimestamp = $ts; + } + + /** + * Assertion helper to test a magic variable output + */ + private function assertMagic( $expected, $magic ) { + if ( in_array( $magic, $this->expectedAsInteger ) ) { + $expected = (int)$expected; + } + + # Generate a message for the assertion + $msg = sprintf( "Magic %s should be <%s:%s>", + $magic, + $expected, + gettype( $expected ) + ); + + $this->assertSame( + $expected, + $this->testParser->getVariableValue( $magic ), + $msg + ); + } +} diff --git a/tests/phpunit/includes/parser/MediaWikiParserTest.php b/tests/phpunit/includes/parser/MediaWikiParserTest.php new file mode 100644 index 00000000..c120ca34 --- /dev/null +++ b/tests/phpunit/includes/parser/MediaWikiParserTest.php @@ -0,0 +1,120 @@ + "\\'", '\\' => '\\\\' ) ); + /* This used to be ucfirst( basename( dirname( $filename ) ) ) + * and then was ucfirst( basename( $filename, '.txt' ) + * but that didn't work with names like foo.tests.txt + */ + $parserTestClassName = str_replace( '.', '_', ucfirst( $testsName ) ); + $parserTestClassDefinition = <<addTestSuite( $parserTestClassName ); + } + return $suite; + } + + /** + * Write $msg under log group 'tests-parser' + * @param string $msg Message to log + */ + protected static function debug( $msg ) { + return wfDebugLog( 'tests-parser', wfGetCaller() . ' ' . $msg ); + } +} diff --git a/tests/phpunit/includes/parser/NewParserTest.php b/tests/phpunit/includes/parser/NewParserTest.php new file mode 100644 index 00000000..eac4de5c --- /dev/null +++ b/tests/phpunit/includes/parser/NewParserTest.php @@ -0,0 +1,969 @@ +getCliArg( 'regex=' ) ) { + $this->regex = $this->getCliArg( 'regex=' ); + } else { + # Matches anything + $this->regex = ''; + } + + $this->keepUploads = $this->getCliArg( 'keep-uploads' ); + + $tmpGlobals = array(); + + $tmpGlobals['wgLanguageCode'] = 'en'; + $tmpGlobals['wgContLang'] = Language::factory( 'en' ); + $tmpGlobals['wgSitename'] = 'MediaWiki'; + $tmpGlobals['wgServer'] = 'http://example.org'; + $tmpGlobals['wgScript'] = '/index.php'; + $tmpGlobals['wgScriptPath'] = '/'; + $tmpGlobals['wgArticlePath'] = '/wiki/$1'; + $tmpGlobals['wgActionPaths'] = array(); + $tmpGlobals['wgVariantArticlePath'] = false; + $tmpGlobals['wgExtensionAssetsPath'] = '/extensions'; + $tmpGlobals['wgStylePath'] = '/skins'; + $tmpGlobals['wgEnableUploads'] = true; + $tmpGlobals['wgThumbnailScriptPath'] = false; + $tmpGlobals['wgLocalFileRepo'] = array( + 'class' => 'LocalRepo', + 'name' => 'local', + 'url' => 'http://example.com/images', + 'hashLevels' => 2, + 'transformVia404' => false, + 'backend' => 'local-backend' + ); + $tmpGlobals['wgForeignFileRepos'] = array(); + $tmpGlobals['wgDefaultExternalStore'] = array(); + $tmpGlobals['wgEnableParserCache'] = false; + $tmpGlobals['wgCapitalLinks'] = true; + $tmpGlobals['wgNoFollowLinks'] = true; + $tmpGlobals['wgNoFollowDomainExceptions'] = array(); + $tmpGlobals['wgExternalLinkTarget'] = false; + $tmpGlobals['wgThumbnailScriptPath'] = false; + $tmpGlobals['wgUseImageResize'] = true; + $tmpGlobals['wgAllowExternalImages'] = true; + $tmpGlobals['wgRawHtml'] = false; + $tmpGlobals['wgUseTidy'] = false; + $tmpGlobals['wgAlwaysUseTidy'] = false; + $tmpGlobals['wgWellFormedXml'] = true; + $tmpGlobals['wgAllowMicrodataAttributes'] = true; + $tmpGlobals['wgExperimentalHtmlIds'] = false; + $tmpGlobals['wgAdaptiveMessageCache'] = true; + $tmpGlobals['wgUseDatabaseMessages'] = true; + $tmpGlobals['wgLocaltimezone'] = 'UTC'; + $tmpGlobals['wgDeferredUpdateList'] = array(); + $tmpGlobals['wgGroupPermissions'] = array( + '*' => array( + 'createaccount' => true, + 'read' => true, + 'edit' => true, + 'createpage' => true, + 'createtalk' => true, + ) ); + $tmpGlobals['wgNamespaceProtection'] = array( NS_MEDIAWIKI => 'editinterface' ); + + $tmpGlobals['wgParser'] = new StubObject( + 'wgParser', $GLOBALS['wgParserConf']['class'], + array( $GLOBALS['wgParserConf'] ) ); + + $tmpGlobals['wgFileExtensions'][] = 'svg'; + $tmpGlobals['wgSVGConverter'] = 'rsvg'; + $tmpGlobals['wgSVGConverters']['rsvg'] = + '$path/rsvg-convert -w $width -h $height $input -o $output'; + + if ( $GLOBALS['wgStyleDirectory'] === false ) { + $tmpGlobals['wgStyleDirectory'] = "$IP/skins"; + } + + # Replace all media handlers with a mock. We do not need to generate + # actual thumbnails to do parser testing, we only care about receiving + # a ThumbnailImage properly initialized. + global $wgMediaHandlers; + foreach ( $wgMediaHandlers as $type => $handler ) { + $tmpGlobals['wgMediaHandlers'][$type] = 'MockBitmapHandler'; + } + // Vector images have to be handled slightly differently + $tmpGlobals['wgMediaHandlers']['image/svg+xml'] = 'MockSvgHandler'; + + $tmpHooks = $wgHooks; + $tmpHooks['ParserTestParser'][] = 'ParserTestParserHook::setup'; + $tmpHooks['ParserGetVariableValueTs'][] = 'ParserTest::getFakeTimestamp'; + $tmpGlobals['wgHooks'] = $tmpHooks; + # add a namespace shadowing a interwiki link, to test + # proper precedence when resolving links. (bug 51680) + $tmpGlobals['wgExtraNamespaces'] = array( 100 => 'MemoryAlpha' ); + + $this->setMwGlobals( $tmpGlobals ); + + $this->savedWeirdGlobals['image_alias'] = $wgNamespaceAliases['Image']; + $this->savedWeirdGlobals['image_talk_alias'] = $wgNamespaceAliases['Image_talk']; + + $wgNamespaceAliases['Image'] = NS_FILE; + $wgNamespaceAliases['Image_talk'] = NS_FILE_TALK; + + MWNamespace::getCanonicalNamespaces( true ); # reset namespace cache + $wgContLang->resetNamespaces(); # reset namespace cache + } + + protected function tearDown() { + global $wgNamespaceAliases, $wgContLang; + + $wgNamespaceAliases['Image'] = $this->savedWeirdGlobals['image_alias']; + $wgNamespaceAliases['Image_talk'] = $this->savedWeirdGlobals['image_talk_alias']; + + // Restore backends + RepoGroup::destroySingleton(); + FileBackendGroup::destroySingleton(); + + // Remove temporary pages from the link cache + LinkCache::singleton()->clear(); + + // Restore message cache (temporary pages and $wgUseDatabaseMessages) + MessageCache::destroyInstance(); + + parent::tearDown(); + + MWNamespace::getCanonicalNamespaces( true ); # reset namespace cache + $wgContLang->resetNamespaces(); # reset namespace cache + } + + public static function tearDownAfterClass() { + ParserTest::tearDownInterwikis(); + parent::tearDownAfterClass(); + } + + function addDBData() { + $this->tablesUsed[] = 'site_stats'; + # disabled for performance + #$this->tablesUsed[] = 'image'; + + # Update certain things in site_stats + $this->db->insert( 'site_stats', + array( 'ss_row_id' => 1, 'ss_images' => 2, 'ss_good_articles' => 1 ), + __METHOD__ + ); + + $user = User::newFromId( 0 ); + LinkCache::singleton()->clear(); # Avoids the odd failure at creating the nullRevision + + # Upload DB table entries for files. + # We will upload the actual files later. Note that if anything causes LocalFile::load() + # to be triggered before then, it will break via maybeUpgrade() setting the fileExists + # member to false and storing it in cache. + # 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 = wfLocalFile( Title::makeTitle( NS_FILE, 'Foobar.jpg' ) ); + if ( !$this->db->selectField( 'image', '1', array( 'img_name' => $image->getName() ) ) ) { + $image->recordUpload2( + '', // archive name + 'Upload of some lame file', + 'Some lame file', + array( + 'size' => 7881, + 'width' => 1941, + 'height' => 220, + 'bits' => 8, + 'media_type' => MEDIATYPE_BITMAP, + 'mime' => 'image/jpeg', + 'metadata' => serialize( array() ), + 'sha1' => wfBaseConvert( '1', 16, 36, 31 ), + 'fileExists' => true ), + $this->db->timestamp( '20010115123500' ), $user + ); + } + + $image = wfLocalFile( Title::makeTitle( NS_FILE, 'Thumb.png' ) ); + if ( !$this->db->selectField( 'image', '1', array( 'img_name' => $image->getName() ) ) ) { + $image->recordUpload2( + '', // archive name + 'Upload of some lame thumbnail', + 'Some lame thumbnail', + array( + 'size' => 22589, + 'width' => 135, + 'height' => 135, + 'bits' => 8, + 'media_type' => MEDIATYPE_BITMAP, + 'mime' => 'image/png', + 'metadata' => serialize( array() ), + 'sha1' => wfBaseConvert( '2', 16, 36, 31 ), + 'fileExists' => true ), + $this->db->timestamp( '20130225203040' ), $user + ); + } + + # This image will be blacklisted in [[MediaWiki:Bad image list]] + $image = wfLocalFile( Title::makeTitle( NS_FILE, 'Bad.jpg' ) ); + if ( !$this->db->selectField( 'image', '1', array( 'img_name' => $image->getName() ) ) ) { + $image->recordUpload2( + '', // archive name + 'zomgnotcensored', + 'Borderline image', + array( + 'size' => 12345, + 'width' => 320, + 'height' => 240, + 'bits' => 24, + 'media_type' => MEDIATYPE_BITMAP, + 'mime' => 'image/jpeg', + 'metadata' => serialize( array() ), + 'sha1' => wfBaseConvert( '3', 16, 36, 31 ), + 'fileExists' => true ), + $this->db->timestamp( '20010115123500' ), $user + ); + } + $image = wfLocalFile( Title::makeTitle( NS_FILE, 'Foobar.svg' ) ); + if ( !$this->db->selectField( 'image', '1', array( 'img_name' => $image->getName() ) ) ) { + $image->recordUpload2( '', 'Upload of some lame SVG', 'Some lame SVG', array( + 'size' => 12345, + 'width' => 200, + 'height' => 200, + 'bits' => 24, + 'media_type' => MEDIATYPE_DRAWING, + 'mime' => 'image/svg+xml', + 'metadata' => serialize( array() ), + 'sha1' => wfBaseConvert( '', 16, 36, 31 ), + 'fileExists' => true + ), $this->db->timestamp( '20010115123500' ), $user ); + } + } + + //ParserTest setup/teardown functions + + /** + * Set up the global variables for a consistent environment for each test. + * Ideally this should replace the global configuration entirely. + */ + protected function setupGlobals( $opts = array(), $config = '' ) { + global $wgFileBackends; + # Find out values for some special options. + $lang = + self::getOptionValue( 'language', $opts, 'en' ); + $variant = + self::getOptionValue( 'variant', $opts, false ); + $maxtoclevel = + self::getOptionValue( 'wgMaxTocLevel', $opts, 999 ); + $linkHolderBatchSize = + self::getOptionValue( 'wgLinkHolderBatchSize', $opts, 1000 ); + + $uploadDir = $this->getUploadDir(); + if ( $this->getCliArg( 'use-filebackend=' ) ) { + if ( self::$backendToUse ) { + $backend = self::$backendToUse; + } else { + $name = $this->getCliArg( 'use-filebackend=' ); + $useConfig = array(); + foreach ( $wgFileBackends as $conf ) { + if ( $conf['name'] == $name ) { + $useConfig = $conf; + } + } + $useConfig['name'] = 'local-backend'; // swap name + $class = $conf['class']; + self::$backendToUse = new $class( $useConfig ); + $backend = self::$backendToUse; + } + } 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( array( + 'name' => 'local-backend', + 'lockManager' => 'nullLockManager', + 'containerPaths' => array( + 'local-public' => "$uploadDir", + 'local-thumb' => "$uploadDir/thumb", + ) + ) ); + } + + $settings = array( + 'wgLocalFileRepo' => array( + 'class' => 'LocalRepo', + 'name' => 'local', + 'url' => 'http://example.com/images', + 'hashLevels' => 2, + 'transformVia404' => false, + 'backend' => $backend + ), + 'wgEnableUploads' => self::getOptionValue( 'wgEnableUploads', $opts, true ), + 'wgLanguageCode' => $lang, + 'wgDBprefix' => $this->db->getType() != 'oracle' ? 'unittest_' : 'ut_', + 'wgRawHtml' => self::getOptionValue( 'wgRawHtml', $opts, false ), + 'wgNamespacesWithSubpages' => array( NS_MAIN => isset( $opts['subpage'] ) ), + 'wgAllowExternalImages' => self::getOptionValue( 'wgAllowExternalImages', $opts, true ), + 'wgMaxTocLevel' => $maxtoclevel, + 'wgUseTeX' => isset( $opts['math'] ) || isset( $opts['texvc'] ), + 'wgMathDirectory' => $uploadDir . '/math', + 'wgDefaultLanguageVariant' => $variant, + 'wgLinkHolderBatchSize' => $linkHolderBatchSize, + ); + + if ( $config ) { + $configLines = explode( "\n", $config ); + + foreach ( $configLines as $line ) { + list( $var, $value ) = explode( '=', $line, 2 ); + + $settings[$var] = eval( "return $value;" ); //??? + } + } + + $this->savedGlobals = array(); + + /** @since 1.20 */ + wfRunHooks( 'ParserTestGlobals', array( &$settings ) ); + + $langObj = Language::factory( $lang ); + $settings['wgContLang'] = $langObj; + $settings['wgLang'] = $langObj; + + $context = new RequestContext(); + $settings['wgOut'] = $context->getOutput(); + $settings['wgUser'] = $context->getUser(); + $settings['wgRequest'] = $context->getRequest(); + + foreach ( $settings as $var => $val ) { + if ( array_key_exists( $var, $GLOBALS ) ) { + $this->savedGlobals[$var] = $GLOBALS[$var]; + } + + $GLOBALS[$var] = $val; + } + + MagicWord::clearCache(); + + # The entries saved into RepoGroup cache with previous globals will be wrong. + RepoGroup::destroySingleton(); + FileBackendGroup::destroySingleton(); + + # Create dummy files in storage + $this->setupUploads(); + + # Publish the articles after we have the final language set + $this->publishTestArticles(); + + MessageCache::destroyInstance(); + + return $context; + } + + /** + * Get an FS upload directory (only applies to FSFileBackend) + * + * @return String: the directory + */ + protected function getUploadDir() { + if ( $this->keepUploads ) { + $dir = wfTempDir() . '/mwParser-images'; + + if ( is_dir( $dir ) ) { + return $dir; + } + } else { + $dir = wfTempDir() . "/mwParser-" . mt_rand() . "-images"; + } + + // wfDebug( "Creating upload directory $dir\n" ); + if ( file_exists( $dir ) ) { + wfDebug( "Already exists!\n" ); + + return $dir; + } + + return $dir; + } + + /** + * Create a dummy uploads directory which will contain a couple + * of files in order to pass existence tests. + * + * @return String: the directory + */ + protected function setupUploads() { + global $IP; + + $base = $this->getBaseDir(); + $backend = RepoGroup::singleton()->getLocalRepo()->getBackend(); + $backend->prepare( array( 'dir' => "$base/local-public/3/3a" ) ); + $backend->store( array( + 'src' => "$IP/skins/monobook/headbg.jpg", 'dst' => "$base/local-public/3/3a/Foobar.jpg" + ) ); + $backend->prepare( array( 'dir' => "$base/local-public/e/ea" ) ); + $backend->store( array( + 'src' => "$IP/skins/monobook/wiki.png", 'dst' => "$base/local-public/e/ea/Thumb.png" + ) ); + $backend->prepare( array( 'dir' => "$base/local-public/0/09" ) ); + $backend->store( array( + 'src' => "$IP/skins/monobook/headbg.jpg", 'dst' => "$base/local-public/0/09/Bad.jpg" + ) ); + + // No helpful SVG file to copy, so make one ourselves + $tmpDir = wfTempDir(); + $tempFsFile = new TempFSFile( "$tmpDir/Foobar.svg" ); + $tempFsFile->autocollect(); // destroy file when $tempFsFile leaves scope + file_put_contents( "$tmpDir/Foobar.svg", + '' . + 'Foo' ); + + $backend->prepare( array( 'dir' => "$base/local-public/f/ff" ) ); + $backend->quickStore( array( + 'src' => "$tmpDir/Foobar.svg", 'dst' => "$base/local-public/f/ff/Foobar.svg" + ) ); + } + + /** + * Restore default values and perform any necessary clean-up + * after each test runs. + */ + protected function teardownGlobals() { + $this->teardownUploads(); + + foreach ( $this->savedGlobals as $var => $val ) { + $GLOBALS[$var] = $val; + } + } + + /** + * Remove the dummy uploads directory + */ + private function teardownUploads() { + if ( $this->keepUploads ) { + return; + } + + $backend = RepoGroup::singleton()->getLocalRepo()->getBackend(); + if ( $backend instanceof MockFileBackend ) { + # In memory backend, so dont bother cleaning them up. + return; + } + + $base = $this->getBaseDir(); + // delete the files first, then the dirs. + self::deleteFiles( + array( + "$base/local-public/3/3a/Foobar.jpg", + "$base/local-thumb/3/3a/Foobar.jpg/180px-Foobar.jpg", + "$base/local-thumb/3/3a/Foobar.jpg/200px-Foobar.jpg", + "$base/local-thumb/3/3a/Foobar.jpg/640px-Foobar.jpg", + "$base/local-thumb/3/3a/Foobar.jpg/120px-Foobar.jpg", + "$base/local-thumb/3/3a/Foobar.jpg/1280px-Foobar.jpg", + "$base/local-thumb/3/3a/Foobar.jpg/20px-Foobar.jpg", + "$base/local-thumb/3/3a/Foobar.jpg/270px-Foobar.jpg", + "$base/local-thumb/3/3a/Foobar.jpg/300px-Foobar.jpg", + "$base/local-thumb/3/3a/Foobar.jpg/30px-Foobar.jpg", + "$base/local-thumb/3/3a/Foobar.jpg/360px-Foobar.jpg", + "$base/local-thumb/3/3a/Foobar.jpg/400px-Foobar.jpg", + "$base/local-thumb/3/3a/Foobar.jpg/40px-Foobar.jpg", + "$base/local-thumb/3/3a/Foobar.jpg/70px-Foobar.jpg", + "$base/local-thumb/3/3a/Foobar.jpg/960px-Foobar.jpg", + + "$base/local-public/e/ea/Thumb.png", + + "$base/local-public/0/09/Bad.jpg", + + "$base/local-public/f/ff/Foobar.svg", + "$base/local-thumb/f/ff/Foobar.svg/180px-Foobar.svg.jpg", + "$base/local-thumb/f/ff/Foobar.svg/270px-Foobar.svg.jpg", + "$base/local-thumb/f/ff/Foobar.svg/360px-Foobar.svg.jpg", + "$base/local-thumb/f/ff/Foobar.svg/langde-180px-Foobar.svg.jpg", + "$base/local-thumb/f/ff/Foobar.svg/langde-270px-Foobar.svg.jpg", + "$base/local-thumb/f/ff/Foobar.svg/langde-360px-Foobar.svg.jpg", + + "$base/local-public/math/f/a/5/fa50b8b616463173474302ca3e63586b.png", + ) + ); + } + + /** + * Delete the specified files, if they exist. + * @param $files Array: full paths to files to delete. + */ + private static function deleteFiles( $files ) { + $backend = RepoGroup::singleton()->getLocalRepo()->getBackend(); + foreach ( $files as $file ) { + $backend->delete( array( 'src' => $file ), array( 'force' => 1 ) ); + } + foreach ( $files as $file ) { + $tmp = $file; + while ( $tmp = FileBackend::parentStoragePath( $tmp ) ) { + if ( !$backend->clean( array( 'dir' => $tmp ) )->isOK() ) { + break; + } + } + } + } + + protected function getBaseDir() { + return 'mwstore://local-backend'; + } + + public function parserTestProvider() { + if ( $this->file === false ) { + global $wgParserTestFiles; + $this->file = $wgParserTestFiles[0]; + } + + return new TestFileIterator( $this->file, $this ); + } + + /** + * Set the file from whose tests will be run by this instance + */ + public function setParserTestFile( $filename ) { + $this->file = $filename; + } + + /** + * @group medium + * @dataProvider parserTestProvider + */ + public function testParserTest( $desc, $input, $result, $opts, $config ) { + if ( $this->regex != '' && !preg_match( '/' . $this->regex . '/', $desc ) ) { + $this->assertTrue( true ); // XXX: don't flood output with "test made no assertions" + //$this->markTestSkipped( 'Filtered out by the user' ); + return; + } + + if ( !$this->isWikitextNS( NS_MAIN ) ) { + // parser tests frequently assume that the main namespace contains wikitext. + // @todo When setting up pages, force the content model. Only skip if + // $wgtContentModelUseDB is false. + $this->markTestSkipped( "Main namespace does not support wikitext," + . "skipping parser test: $desc" ); + } + + wfDebug( "Running parser test: $desc\n" ); + + $opts = $this->parseOptions( $opts ); + $context = $this->setupGlobals( $opts, $config ); + + $user = $context->getUser(); + $options = ParserOptions::newFromContext( $context ); + + 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 ); + + # Parser test requiring math. Make sure texvc is executable + # or just skip such tests. + if ( isset( $opts['math'] ) || isset( $opts['texvc'] ) ) { + global $wgTexvc; + + if ( !isset( $wgTexvc ) ) { + $this->markTestSkipped( "SKIPPED: \$wgTexvc is not set" ); + } elseif ( !is_executable( $wgTexvc ) ) { + $this->markTestSkipped( "SKIPPED: texvc binary does not exist" + . " or is not executable.\n" + . "Current configuration is:\n\$wgTexvc = '$wgTexvc'" ); + } + } + + if ( isset( $opts['pst'] ) ) { + $out = $parser->preSaveTransform( $input, $title, $user, $options ); + } elseif ( isset( $opts['msg'] ) ) { + $out = $parser->transformMsg( $input, $options, $title ); + } elseif ( isset( $opts['section'] ) ) { + $section = $opts['section']; + $out = $parser->getSection( $input, $section ); + } elseif ( isset( $opts['replace'] ) ) { + $section = $opts['replace'][0]; + $replace = $opts['replace'][1]; + $out = $parser->replaceSection( $input, $section, $replace ); + } elseif ( isset( $opts['comment'] ) ) { + $out = Linker::formatComment( $input, $title, $local ); + } elseif ( isset( $opts['preload'] ) ) { + $out = $parser->getPreloadText( $input, $title, $options ); + } else { + $output = $parser->parse( $input, $title, $options, true, true, 1337 ); + $output->setTOCEnabled( !isset( $opts['notoc'] ) ); + $out = $output->getText(); + + if ( isset( $opts['showtitle'] ) ) { + if ( $output->getTitleText() ) { + $title = $output->getTitleText(); + } + + $out = "$title\n$out"; + } + + if ( isset( $opts['ill'] ) ) { + $out = $this->tidy( implode( ' ', $output->getLanguageLinks() ) ); + } elseif ( isset( $opts['cat'] ) ) { + $outputPage = $context->getOutput(); + $outputPage->addCategoryLinks( $output->getCategories() ); + $cats = $outputPage->getCategoryLinks(); + + if ( isset( $cats['normal'] ) ) { + $out = $this->tidy( implode( ' ', $cats['normal'] ) ); + } else { + $out = ''; + } + } + $parser->mPreprocessor = null; + + $result = $this->tidy( $result ); + } + + $this->teardownGlobals(); + + $this->assertEquals( $result, $out, $desc ); + } + + /** + * Run a fuzz test series + * Draw input from a set of test files + * + * @todo fixme Needs some work to not eat memory until the world explodes + * + * @group ParserFuzz + */ + public function testFuzzTests() { + global $wgParserTestFiles; + + $files = $wgParserTestFiles; + + if ( $this->getCliArg( 'file=' ) ) { + $files = array( $this->getCliArg( 'file=' ) ); + } + + $dict = $this->getFuzzInput( $files ); + $dictSize = strlen( $dict ); + $logMaxLength = log( $this->maxFuzzTestLength ); + + ini_set( 'memory_limit', $this->memoryLimit * 1048576 ); + + $user = new User; + $opts = ParserOptions::newFromUser( $user ); + $title = Title::makeTitle( NS_MAIN, 'Parser_test' ); + + $id = 1; + + while ( true ) { + + // Generate test input + mt_srand( ++$this->fuzzSeed ); + $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 ); + } + + $this->setupGlobals(); + $parser = $this->getParser(); + + // Run the test + try { + $parser->parse( $input, $title, $opts ); + $this->assertTrue( true, "Test $id, fuzz seed {$this->fuzzSeed}" ); + } catch ( Exception $exception ) { + $input_dump = sprintf( "string(%d) \"%s\"\n", strlen( $input ), $input ); + + $this->assertTrue( false, "Test $id, fuzz seed {$this->fuzzSeed}. \n\n" . + "Input: $input_dump\n\nError: {$exception->getMessage()}\n\n" . + "Backtrace: {$exception->getTraceAsString()}" ); + } + + $this->teardownGlobals(); + $parser->__destruct(); + + if ( $id % 100 == 0 ) { + $usage = intval( memory_get_usage( true ) / $this->memoryLimit / 1048576 * 100 ); + //echo "{$this->fuzzSeed}: $numSuccess/$numTotal (mem: $usage%)\n"; + if ( $usage > 90 ) { + $ret = "Out of memory:\n"; + $memStats = $this->getMemoryBreakdown(); + + foreach ( $memStats as $name => $usage ) { + $ret .= "$name: $usage\n"; + } + + throw new MWException( $ret ); + } + } + + $id++; + } + } + + //Various getter functions + + /** + * Get an input dictionary from a set of parser test files + */ + function getFuzzInput( $filenames ) { + $dict = ''; + + foreach ( $filenames as $filename ) { + $contents = file_get_contents( $filename ); + preg_match_all( '/!!\s*input\n(.*?)\n!!\s*result/s', $contents, $matches ); + + foreach ( $matches[1] as $match ) { + $dict .= $match . "\n"; + } + } + + return $dict; + } + + /** + * Get a memory usage breakdown + */ + function getMemoryBreakdown() { + $memStats = array(); + + foreach ( $GLOBALS as $name => $value ) { + $memStats['$' . $name] = strlen( serialize( $value ) ); + } + + $classes = get_declared_classes(); + + foreach ( $classes as $class ) { + $rc = new ReflectionClass( $class ); + $props = $rc->getStaticProperties(); + $memStats[$class] = strlen( serialize( $props ) ); + $methods = $rc->getMethods(); + + foreach ( $methods as $method ) { + $memStats[$class] += strlen( serialize( $method->getStaticVariables() ) ); + } + } + + $functions = get_defined_functions(); + + foreach ( $functions['user'] as $function ) { + $rf = new ReflectionFunction( $function ); + $memStats["$function()"] = strlen( serialize( $rf->getStaticVariables() ) ); + } + + asort( $memStats ); + + return $memStats; + } + + /** + * Get a Parser object + */ + function getParser( $preprocessor = null ) { + global $wgParserConf; + + $class = $wgParserConf['class']; + $parser = new $class( array( 'preprocessorClass' => $preprocessor ) + $wgParserConf ); + + wfRunHooks( 'ParserTestParser', array( &$parser ) ); + + return $parser; + } + + //Various action functions + + public function addArticle( $name, $text, $line ) { + self::$articles[$name] = array( $text, $line ); + } + + public function publishTestArticles() { + if ( empty( self::$articles ) ) { + return; + } + + foreach ( self::$articles as $name => $info ) { + list( $text, $line ) = $info; + ParserTest::addArticle( $name, $text, $line, 'ignoreduplicate' ); + } + } + + /** + * Steal a callback function from the primary parser, save it for + * application to our scary parser. If the hook is not installed, + * abort processing of this file. + * + * @param $name String + * @return Bool true if tag hook is present + */ + public function requireHook( $name ) { + global $wgParser; + $wgParser->firstCallInit(); // make sure hooks are loaded. + return isset( $wgParser->mTagHooks[$name] ); + } + + public function requireFunctionHook( $name ) { + global $wgParser; + $wgParser->firstCallInit(); // make sure hooks are loaded. + return isset( $wgParser->mFunctionHooks[$name] ); + } + + //Various "cleanup" functions + + /** + * Run the "tidy" command on text if the $wgUseTidy + * global is true + * + * @param $text String: the text to tidy + * @return String + */ + protected function tidy( $text ) { + global $wgUseTidy; + + if ( $wgUseTidy ) { + $text = MWTidy::tidy( $text ); + } + + return $text; + } + + /** + * Remove last character if it is a newline + */ + public function removeEndingNewline( $s ) { + if ( substr( $s, -1 ) === "\n" ) { + return substr( $s, 0, -1 ); + } else { + return $s; + } + } + + //Test options parser functions + + protected function parseOptions( $instring ) { + $opts = array(); + // foo + // foo=bar + // foo="bar baz" + // foo=[[bar baz]] + // foo=bar,"baz quux" + $regex = '/\b + ([\w-]+) # Key + \b + (?:\s* + = # First sub-value + \s* + ( + " + [^"]* # Quoted val + " + | + \[\[ + [^]]* # Link target + \]\] + | + [\w-]+ # Plain word + ) + (?:\s* + , # Sub-vals 1..N + \s* + ( + "[^"]*" # Quoted val + | + \[\[[^]]*\]\] # Link target + | + [\w-]+ # Plain word + ) + )* + )? + /x'; + + if ( preg_match_all( $regex, $instring, $matches, PREG_SET_ORDER ) ) { + foreach ( $matches as $bits ) { + array_shift( $bits ); + $key = strtolower( array_shift( $bits ) ); + if ( count( $bits ) == 0 ) { + $opts[$key] = true; + } elseif ( count( $bits ) == 1 ) { + $opts[$key] = $this->cleanupOption( array_shift( $bits ) ); + } else { + // Array! + $opts[$key] = array_map( array( $this, 'cleanupOption' ), $bits ); + } + } + } + + return $opts; + } + + protected function cleanupOption( $opt ) { + if ( substr( $opt, 0, 1 ) == '"' ) { + return substr( $opt, 1, -1 ); + } + + if ( substr( $opt, 0, 2 ) == '[[' ) { + return substr( $opt, 2, -2 ); + } + + return $opt; + } + + /** + * Use a regex to find out the value of an option + * @param $key String: name of option val to retrieve + * @param $opts Options array to look in + * @param $default Mixed: default value returned if not found + */ + protected static function getOptionValue( $key, $opts, $default ) { + $key = strtolower( $key ); + + if ( isset( $opts[$key] ) ) { + return $opts[$key]; + } else { + return $default; + } + } +} diff --git a/tests/phpunit/includes/parser/ParserMethodsTest.php b/tests/phpunit/includes/parser/ParserMethodsTest.php new file mode 100644 index 00000000..e5c5cb21 --- /dev/null +++ b/tests/phpunit/includes/parser/ParserMethodsTest.php @@ -0,0 +1,95 @@ +~~~', + 'hello \'\'this\'\' is ~~~', + ), + ); + } + + /** + * @dataProvider providePreSaveTransform + * @covers Parser::preSaveTransform + */ + public function testPreSaveTransform( $text, $expected ) { + global $wgParser; + + $title = Title::newFromText( str_replace( '::', '__', __METHOD__ ) ); + $user = new User(); + $user->setName( "127.0.0.1" ); + $popts = ParserOptions::newFromUser( $user ); + $text = $wgParser->preSaveTransform( $text, $title, $user, $popts ); + + $this->assertEquals( $expected, $text ); + } + + /** + * @covers Parser::callParserFunction + */ + public function testCallParserFunction() { + global $wgParser; + + // Normal parses test passing PPNodes. Test passing an array. + $title = Title::newFromText( str_replace( '::', '__', __METHOD__ ) ); + $wgParser->startExternalParse( $title, new ParserOptions(), Parser::OT_HTML ); + $frame = $wgParser->getPreprocessor()->newFrame(); + $ret = $wgParser->callParserFunction( $frame, '#tag', + array( 'pre', 'foo', 'style' => 'margin-left: 1.6em' ) + ); + $ret['text'] = $wgParser->mStripState->unstripBoth( $ret['text'] ); + $this->assertSame( array( + 'found' => true, + 'text' => '
foo
', + ), $ret, 'callParserFunction works for {{#tag:pre|foo|style=margin-left: 1.6em}}' ); + } + + /** + * @covers Parser::parse + * @covers ParserOutput::getSections + */ + public function testGetSections() { + global $wgParser; + + $title = Title::newFromText( str_replace( '::', '__', __METHOD__ ) ); + $out = $wgParser->parse( "==foo==\n

bar

\n==baz==\n", $title, new ParserOptions() ); + $this->assertSame( array( + array( + 'toclevel' => 1, + 'level' => '2', + 'line' => 'foo', + 'number' => '1', + 'index' => '1', + 'fromtitle' => $title->getPrefixedDBkey(), + 'byteoffset' => 0, + 'anchor' => 'foo', + ), + array( + 'toclevel' => 1, + 'level' => '2', + 'line' => 'bar', + 'number' => '2', + 'index' => '', + 'fromtitle' => false, + 'byteoffset' => null, + 'anchor' => 'bar', + ), + array( + 'toclevel' => 1, + 'level' => '2', + 'line' => 'baz', + 'number' => '3', + 'index' => '2', + 'fromtitle' => $title->getPrefixedDBkey(), + 'byteoffset' => 21, + 'anchor' => 'baz', + ), + ), $out->getSections(), 'getSections() with proper value when

is used' ); + } + //@Todo Add tests for cleanSig() / cleanSigInSig(), getSection(), replaceSection(), getPreloadText() +} diff --git a/tests/phpunit/includes/parser/ParserOutputTest.php b/tests/phpunit/includes/parser/ParserOutputTest.php new file mode 100644 index 00000000..c73666da --- /dev/null +++ b/tests/phpunit/includes/parser/ParserOutputTest.php @@ -0,0 +1,59 @@ +assertEquals( $shouldMatch, ParserOutput::isLinkInternal( $server, $url ) ); + } + + /** + * @covers ParserOutput::setExtensionData + * @covers ParserOutput::getExtensionData + */ + public function testExtensionData() { + $po = new ParserOutput(); + + $po->setExtensionData( "one", "Foo" ); + + $this->assertEquals( "Foo", $po->getExtensionData( "one" ) ); + $this->assertNull( $po->getExtensionData( "spam" ) ); + + $po->setExtensionData( "two", "Bar" ); + $this->assertEquals( "Foo", $po->getExtensionData( "one" ) ); + $this->assertEquals( "Bar", $po->getExtensionData( "two" ) ); + + $po->setExtensionData( "one", null ); + $this->assertNull( $po->getExtensionData( "one" ) ); + $this->assertEquals( "Bar", $po->getExtensionData( "two" ) ); + } +} diff --git a/tests/phpunit/includes/parser/ParserPreloadTest.php b/tests/phpunit/includes/parser/ParserPreloadTest.php new file mode 100644 index 00000000..d12fee36 --- /dev/null +++ b/tests/phpunit/includes/parser/ParserPreloadTest.php @@ -0,0 +1,80 @@ +testParserOptions = ParserOptions::newFromUserAndLang( new User, $wgContLang ); + + $this->testParser = new Parser(); + $this->testParser->Options( $this->testParserOptions ); + $this->testParser->clearState(); + + $this->title = Title::newFromText( 'Preload Test' ); + } + + protected function tearDown() { + parent::tearDown(); + + unset( $this->testParser ); + unset( $this->title ); + } + + /** + * @covers Parser::getPreloadText + */ + public function testPreloadSimpleText() { + $this->assertPreloaded( 'simple', 'simple' ); + } + + /** + * @covers Parser::getPreloadText + */ + public function testPreloadedPreIsUnstripped() { + $this->assertPreloaded( + '
monospaced
', + '
monospaced
', + '
 in preloaded text must be unstripped (bug 27467)'
+		);
+	}
+
+	/**
+	 * @covers Parser::getPreloadText
+	 */
+	public function testPreloadedNowikiIsUnstripped() {
+		$this->assertPreloaded(
+			'[[Dummy title]]',
+			'[[Dummy title]]',
+			' in preloaded text must be unstripped (bug 27467)'
+		);
+	}
+
+	protected function assertPreloaded( $expected, $text, $msg = '' ) {
+		$this->assertEquals(
+			$expected,
+			$this->testParser->getPreloadText(
+				$text,
+				$this->title,
+				$this->testParserOptions
+			),
+			$msg
+		);
+	}
+}
diff --git a/tests/phpunit/includes/parser/PreprocessorTest.php b/tests/phpunit/includes/parser/PreprocessorTest.php
new file mode 100644
index 00000000..8aee937c
--- /dev/null
+++ b/tests/phpunit/includes/parser/PreprocessorTest.php
@@ -0,0 +1,239 @@
+mOptions = ParserOptions::newFromUserAndLang( new User, $wgContLang );
+		$name = isset( $wgParserConf['preprocessorClass'] ) ? $wgParserConf['preprocessorClass'] : 'Preprocessor_DOM';
+
+		$this->mPreprocessor = new $name( $this );
+	}
+
+	function getStripList() {
+		return array( 'gallery', 'display map' /* Used by Maps, see r80025 CR */, '/foo' );
+	}
+
+	public static function provideCases() {
+		return array(
+			array( "Foo", "Foo" ),
+			array( "", "<!-- Foo -->" ),
+			array( "", "<!-- Foo --><!-- Bar -->" ),
+			array( "  ", "<!-- Foo -->  <!-- Bar -->" ),
+			array( " \n ", "<!-- Foo --> \n <!-- Bar -->" ),
+			array( " \n \n", "<!-- Foo --> \n <!-- Bar -->\n" ),
+			array( "  \n", "<!-- Foo -->  <!-- Bar -->\n" ),
+			array( "Bar", "<!-->Bar" ),
+			array( "\n== Baz ==\n", "== Foo ==\n  <!-- Bar -->\n== Baz ==\n" ),
+			array( "", "gallery" ),
+			array( "Foo  Bar", "Foo gallery Bar" ),
+			array( "", "gallery</gallery>" ),
+			array( " ", "<foo> gallery</gallery>" ),
+			array( " ", "<foo> gallery<gallery></gallery>" ),
+			array( " Foo bar ", "<noinclude> Foo bar </noinclude>" ),
+			array( "\n{{Foo}}\n", "<noinclude>\n\n</noinclude>" ),
+			array( "\n{{Foo}}\n\n", "<noinclude>\n\n</noinclude>\n" ),
+			array( "foo bar", "galleryfoo bar" ),
+			array( "<{{foo}}>", "<>" ),
+			array( "<{{{foo}}}>", "<foo>" ),
+			array( "", "gallery</gallery</gallery>" ),
+			array( "=== Foo === ", "=== Foo === " ),
+			array( "=== Foo === ", "==<!-- -->= Foo === " ),
+			array( "=== Foo === ", "=== Foo ==<!-- -->= " ),
+			array( "=== Foo ===\n", "=== Foo ===<!-- -->\n" ),
+			array( "=== Foo === \n", "=== Foo ===<!-- --> <!-- -->\n" ),
+			array( "== Foo ==\n== Bar == \n", "== Foo ==\n== Bar == \n" ),
+			array( "===========", "===========" ),
+			array( "Foo\n=\n==\n=\n", "Foo\n=\n==\n=\n" ),
+			array( "{{Foo}}", "" ),
+			array( "\n{{Foo}}", "\n" ),
+			array( "{{Foo|bar}}", "" ),
+			array( "{{Foo|bar}}a", "a" ),
+			array( "{{Foo|bar|baz}}", "" ),
+			array( "{{Foo|1=bar}}", "" ),
+			array( "{{Foo|=bar}}", "" ),
+			array( "{{Foo|bar=baz}}", "" ),
+			array( "{{Foo|{{bar}}=baz}}", "" ),
+			array( "{{Foo|1=bar|baz}}", "" ),
+			array( "{{Foo|1=bar|2=baz}}", "" ),
+			array( "{{Foo|bar|foo=baz}}", "" ),
+			array( "{{{1}}}", "1" ),
+			array( "{{{1|}}}", "1" ),
+			array( "{{{Foo}}}", "Foo" ),
+			array( "{{{Foo|}}}", "Foo" ),
+			array( "{{{Foo|bar|baz}}}", "Foobarbaz" ),
+			array( "{{Foo}}", "{<!-- -->{Foo}}" ),
+			array( "{{{{Foobar}}}}", "{Foobar}" ),
+			array( "{{{ {{Foo}} }}}", " <template><title>Foo " ),
+			array( "{{ {{{Foo}}} }}", "" ),
+			array( "{{{{{Foo}}}}}", "" ),
+			array( "{{{{{Foo}} }}}", "<template><title>Foo " ),
+			array( "{{{{{{Foo}}}}}}", "<tplarg><title>Foo" ),
+			array( "{{{{{{Foo}}}}}", "{" ),
+			array( "[[[Foo]]", "[[[Foo]]" ),
+			array( "{{Foo|[[[[bar]]|baz]]}}", "" ), // This test is important, since it means the difference between having the [[ rule stacked or not
+			array( "{{Foo|[[[[bar]|baz]]}}", "{{Foo|[[[[bar]|baz]]}}" ),
+			array( "{{Foo|Foo [[[[bar]|baz]]}}", "{{Foo|Foo [[[[bar]|baz]]}}" ),
+			array( "Foo BarBaz", "Foo display mapBar</display map             >Baz" ),
+			array( "Foo BarBaz", "Foo display map fooBar</display map             >Baz" ),
+			array( "Foo ", "Foo gallery bar="baz" " ),
+			array( "Foo ", "Foo gallery bar="1" baz=2 " ),
+			array( "Foo", "/fooFoo<//foo>" ), # Worth blacklisting IMHO
+			array( "{{#ifexpr: ({{{1|1}}} = 2) | Foo | Bar }}", "" ),
+			array( "{{#if: {{{1|}}} | Foo | {{Bar}} }}", "" ),
+			array( "{{#if: {{{1|}}} | Foo | [[Bar]] }}", "" ),
+			array( "{{#if: {{{1|}}} | [[Foo]] | Bar }}", "" ),
+			array( "{{#if: {{{1|}}} | 1 | {{#if: {{{1|}}} | 2 | 3 }} }}", "" ),
+			array( "{{ {{Foo}}", "{{ " ),
+			array( "{{Foobar {{Foo}} {{Bar}} {{Baz}} ", "{{Foobar    " ),
+			array( "[[Foo]] |", "[[Foo]] |" ),
+			array( "{{Foo|Bar|", "{{Foo|Bar|" ),
+			array( "[[Foo]", "[[Foo]" ),
+			array( "[[Foo|Bar]", "[[Foo|Bar]" ),
+			array( "{{Foo| [[Bar] }}", "{{Foo| [[Bar] }}" ),
+			array( "{{Foo| [[Bar|Baz] }}", "{{Foo| [[Bar|Baz] }}" ),
+			array( "{{Foo|bar=[[baz]}}", "{{Foo|bar=[[baz]}}" ),
+			array( "{{foo|", "{{foo|" ),
+			array( "{{foo|}", "{{foo|}" ),
+			array( "{{foo|} }}", "" ),
+			array( "{{foo|bar=|}", "{{foo|bar=|}" ),
+			array( "{{Foo|} Bar=", "{{Foo|} Bar=" ),
+			array( "{{Foo|} Bar=}}", "" ),
+			/* array( file_get_contents( __DIR__ . '/QuoteQuran.txt' ), file_get_contents( __DIR__ . '/QuoteQuranExpanded.txt' ) ), */
+		);
+	}
+
+	/**
+	 * Get XML preprocessor tree from the preprocessor (which may not be the
+	 * native XML-based one).
+	 *
+	 * @param string $wikiText
+	 * @return string
+	 */
+	protected function preprocessToXml( $wikiText ) {
+		if ( method_exists( $this->mPreprocessor, 'preprocessToXml' ) ) {
+			return $this->normalizeXml( $this->mPreprocessor->preprocessToXml( $wikiText ) );
+		}
+
+		$dom = $this->mPreprocessor->preprocessToObj( $wikiText );
+		if ( is_callable( array( $dom, 'saveXML' ) ) ) {
+			return $dom->saveXML();
+		} else {
+			return $this->normalizeXml( $dom->__toString() );
+		}
+	}
+
+	/**
+	 * Normalize XML string to the form that a DOMDocument saves out.
+	 *
+	 * @param string $xml
+	 * @return string
+	 */
+	protected function normalizeXml( $xml ) {
+		return preg_replace( '!<([a-z]+)/>!', '<$1>', str_replace( ' />', '/>', $xml ) );
+	}
+
+	/**
+	 * @dataProvider provideCases
+	 * @covers Preprocessor_DOM::preprocessToXml
+	 */
+	public function testPreprocessorOutput( $wikiText, $expectedXml ) {
+		$this->assertEquals( $this->normalizeXml( $expectedXml ), $this->preprocessToXml( $wikiText ) );
+	}
+
+	/**
+	 * These are more complex test cases taken out of wiki articles.
+	 */
+	public static function provideFiles() {
+		return array(
+			array( "QuoteQuran" ), # http://en.wikipedia.org/w/index.php?title=Template:QuoteQuran/sandbox&oldid=237348988 GFDL + CC-BY-SA by Striver
+			array( "Factorial" ), # http://en.wikipedia.org/w/index.php?title=Template:Factorial&oldid=98548758 GFDL + CC-BY-SA by Polonium
+			array( "All_system_messages" ), # http://tl.wiktionary.org/w/index.php?title=Suleras:All_system_messages&oldid=2765 GPL text generated by MediaWiki
+			array( "Fundraising" ), # http://tl.wiktionary.org/w/index.php?title=MediaWiki:Sitenotice&oldid=5716 GFDL + CC-BY-SA, copied there by Sky Harbor.
+			array( "NestedTemplates" ), # bug 27936
+		);
+	}
+
+	/**
+	 * @dataProvider provideFiles
+	 * @covers Preprocessor_DOM::preprocessToXml
+	 */
+	public function testPreprocessorOutputFiles( $filename ) {
+		$folder = __DIR__ . "/../../../parser/preprocess";
+		$wikiText = file_get_contents( "$folder/$filename.txt" );
+		$output = $this->preprocessToXml( $wikiText );
+
+		$expectedFilename = "$folder/$filename.expected";
+		if ( file_exists( $expectedFilename ) ) {
+			$expectedXml = $this->normalizeXml( file_get_contents( $expectedFilename ) );
+			$this->assertEquals( $expectedXml, $output );
+		} else {
+			$tempFilename = tempnam( $folder, "$filename." );
+			file_put_contents( $tempFilename, $output );
+			$this->markTestIncomplete( "File $expectedFilename missing. Output stored as $tempFilename" );
+		}
+	}
+
+	/**
+	 * Tests from Bug 28642 · https://bugzilla.wikimedia.org/28642
+	 */
+	public static function provideHeadings() {
+		return array( /* These should become headings: */
+			array( "== h ==", "== h ==<!--c1-->" ),
+			array( "== h == 	", "== h == 	<!--c1-->" ),
+			array( "== h == 	", "== h ==<!--c1--> 	" ),
+			array( "== h == 	 	", "== h == 	<!--c1--> 	" ),
+			array( "== h ==", "== h ==<!--c1--><!--c2-->" ),
+			array( "== h == 	", "== h == 	<!--c1--><!--c2-->" ),
+			array( "== h == 	", "== h ==<!--c1--><!--c2--> 	" ),
+			array( "== h == 	 	", "== h == 	<!--c1--><!--c2--> 	" ),
+			array( "== h == 	  ", "== h == 	<!--c1-->  <!--c2-->" ),
+			array( "== h ==   	", "== h ==<!--c1-->  <!--c2--> 	" ),
+			array( "== h == 	   	", "== h == 	<!--c1-->  <!--c2--> 	" ),
+			array( "== h ==", "== h ==<!--c1--><!--c2--><!--c3-->" ),
+			array( "== h ==  ", "== h ==<!--c1-->  <!--c2--><!--c3-->" ),
+			array( "== h ==  ", "== h ==<!--c1--><!--c2-->  <!--c3-->" ),
+			array( "== h ==    ", "== h ==<!--c1-->  <!--c2-->  <!--c3-->" ),
+			array( "== h ==  ", "== h ==  <!--c1--><!--c2--><!--c3-->" ),
+			array( "== h ==    ", "== h ==  <!--c1-->  <!--c2--><!--c3-->" ),
+			array( "== h ==    ", "== h ==  <!--c1--><!--c2-->  <!--c3-->" ),
+			array( "== h ==      ", "== h ==  <!--c1-->  <!--c2-->  <!--c3-->" ),
+			array( "== h ==  ", "== h ==<!--c1--><!--c2--><!--c3-->  " ),
+			array( "== h ==    ", "== h ==<!--c1-->  <!--c2--><!--c3-->  " ),
+			array( "== h ==    ", "== h ==<!--c1--><!--c2-->  <!--c3-->  " ),
+			array( "== h ==      ", "== h ==<!--c1-->  <!--c2-->  <!--c3-->  " ),
+			array( "== h ==    ", "== h ==  <!--c1--><!--c2--><!--c3-->  " ),
+			array( "== h ==      ", "== h ==  <!--c1-->  <!--c2--><!--c3-->  " ),
+			array( "== h ==      ", "== h ==  <!--c1--><!--c2-->  <!--c3-->  " ),
+			array( "== h ==        ", "== h ==  <!--c1-->  <!--c2-->  <!--c3-->  " ),
+			array( "== h == 	", "== h ==<!--c1--> 	<!--c2-->" ),
+			array( "== h == 	 	", "== h == 	<!--c1--> 	<!--c2-->" ),
+			array( "== h == 	 	", "== h ==<!--c1--> 	<!--c2--> 	" ),
+
+			/* These are not working: */
+			array( "== h == x   ", "== h == x <!--c1--><!--c2--><!--c3-->  " ),
+			array( "== h == x   ", "== h ==<!--c1--> x <!--c2--><!--c3-->  " ),
+			array( "== h == x ", "== h ==<!--c1--><!--c2--><!--c3--> x " ),
+		);
+	}
+
+	/**
+	 * @dataProvider provideHeadings
+	 * @covers Preprocessor_DOM::preprocessToXml
+	 */
+	public function testHeadings( $wikiText, $expectedXml ) {
+		$this->assertEquals( $this->normalizeXml( $expectedXml ), $this->preprocessToXml( $wikiText ) );
+	}
+}
diff --git a/tests/phpunit/includes/parser/TagHooksTest.php b/tests/phpunit/includes/parser/TagHooksTest.php
new file mode 100644
index 00000000..61cbe45d
--- /dev/null
+++ b/tests/phpunit/includes/parser/TagHooksTest.php
@@ -0,0 +1,82 @@
+bar" ), array( "foo\nbar" ), array( "foo\rbar" ) );
+	}
+
+	protected function setUp() {
+		parent::setUp();
+
+		$this->setMwGlobals( 'wgAlwaysUseTidy', false );
+	}
+
+	/**
+	 * @dataProvider provideValidNames
+	 */
+	public function testTagHooks( $tag ) {
+		global $wgParserConf, $wgContLang;
+		$parser = new Parser( $wgParserConf );
+
+		$parser->setHook( $tag, array( $this, 'tagCallback' ) );
+		$parserOutput = $parser->parse( "Foo<$tag>BarBaz", Title::newFromText( 'Test' ), ParserOptions::newFromUserAndLang( new User, $wgContLang ) );
+		$this->assertEquals( "

FooOneBaz\n

", $parserOutput->getText() ); + + $parser->mPreprocessor = null; # Break the Parser <-> Preprocessor cycle + } + + /** + * @dataProvider provideBadNames + * @expectedException MWException + */ + public function testBadTagHooks( $tag ) { + global $wgParserConf, $wgContLang; + $parser = new Parser( $wgParserConf ); + + $parser->setHook( $tag, array( $this, 'tagCallback' ) ); + $parser->parse( "Foo<$tag>BarBaz", Title::newFromText( 'Test' ), ParserOptions::newFromUserAndLang( new User, $wgContLang ) ); + $this->fail( 'Exception not thrown.' ); + } + + /** + * @dataProvider provideValidNames + */ + public function testFunctionTagHooks( $tag ) { + global $wgParserConf, $wgContLang; + $parser = new Parser( $wgParserConf ); + + $parser->setFunctionTagHook( $tag, array( $this, 'functionTagCallback' ), 0 ); + $parserOutput = $parser->parse( "Foo<$tag>BarBaz", Title::newFromText( 'Test' ), ParserOptions::newFromUserAndLang( new User, $wgContLang ) ); + $this->assertEquals( "

FooOneBaz\n

", $parserOutput->getText() ); + + $parser->mPreprocessor = null; # Break the Parser <-> Preprocessor cycle + } + + /** + * @dataProvider provideBadNames + * @expectedException MWException + */ + public function testBadFunctionTagHooks( $tag ) { + global $wgParserConf, $wgContLang; + $parser = new Parser( $wgParserConf ); + + $parser->setFunctionTagHook( $tag, array( $this, 'functionTagCallback' ), SFH_OBJECT_ARGS ); + $parser->parse( "Foo<$tag>BarBaz", Title::newFromText( 'Test' ), ParserOptions::newFromUserAndLang( new User, $wgContLang ) ); + $this->fail( 'Exception not thrown.' ); + } + + function tagCallback( $text, $params, $parser ) { + return str_rot13( $text ); + } + + function functionTagCallback( &$parser, $frame, $code, $attribs ) { + return str_rot13( $code ); + } +} diff --git a/tests/phpunit/includes/parser/TidyTest.php b/tests/phpunit/includes/parser/TidyTest.php new file mode 100644 index 00000000..57a88b9d --- /dev/null +++ b/tests/phpunit/includes/parser/TidyTest.php @@ -0,0 +1,44 @@ +markTestSkipped( 'Tidy not found' ); + } + } + + /** + * @dataProvider provideTestWrapping + */ + public function testTidyWrapping( $expected, $text, $msg = '' ) { + $text = MWTidy::tidy( $text ); + // We don't care about where Tidy wants to stick is

s + $text = trim( preg_replace( '##', '', $text ) ); + // Windows, we love you! + $text = str_replace( "\r", '', $text ); + $this->assertEquals( $expected, $text, $msg ); + } + + public function provideTestWrapping() { + return array( + array( + 'foo', + 'foo', + ' should survive tidy' + ), + array( + 'foo', + 'foo', + ' should survive tidy' + ), + array( 'foo', 'foo', ' should survive tidy' ), + array( "\nfoo", 'foo', ' should survive tidy' ), + array( "\nfoo", 'foo', ' should survive tidy' ), + ); + } +} \ No newline at end of file diff --git a/tests/phpunit/includes/search/SearchEngineTest.php b/tests/phpunit/includes/search/SearchEngineTest.php new file mode 100644 index 00000000..87067038 --- /dev/null +++ b/tests/phpunit/includes/search/SearchEngineTest.php @@ -0,0 +1,176 @@ +db->getType(); + $dbSupported = + ( $dbType === 'mysql' ) + || ( $dbType === 'sqlite' && $this->db->getFulltextSearchModule() == 'FTS3' ); + + if ( !$dbSupported ) { + $this->markTestSkipped( "MySQL or SQLite with FTS3 only" ); + } + + $searchType = $this->db->getSearchEngine(); + $this->search = new $searchType( $this->db ); + } + + protected function tearDown() { + unset( $this->search ); + + parent::tearDown(); + } + + function pageExists( $title ) { + return false; + } + + function addDBData() { + if ( $this->pageExists( 'Not_Main_Page' ) ) { + return; + } + + if ( !$this->isWikitextNS( NS_MAIN ) ) { + // @todo cover the case of non-wikitext content in the main namespace + return; + } + + $this->insertPage( "Not_Main_Page", "This is not a main page", 0 ); + $this->insertPage( 'Talk:Not_Main_Page', 'This is not a talk page to the main page, see [[smithee]]', 1 ); + $this->insertPage( 'Smithee', 'A smithee is one who smiths. See also [[Alan Smithee]]', 0 ); + $this->insertPage( 'Talk:Smithee', 'This article sucks.', 1 ); + $this->insertPage( 'Unrelated_page', 'Nothing in this page is about the S word.', 0 ); + $this->insertPage( 'Another_page', 'This page also is unrelated.', 0 ); + $this->insertPage( 'Help:Help', 'Help me!', 4 ); + $this->insertPage( 'Thppt', 'Blah blah', 0 ); + $this->insertPage( 'Alan_Smithee', 'yum', 0 ); + $this->insertPage( 'Pages', 'are\'food', 0 ); + $this->insertPage( 'HalfOneUp', 'AZ', 0 ); + $this->insertPage( 'FullOneUp', 'AZ', 0 ); + $this->insertPage( 'HalfTwoLow', 'az', 0 ); + $this->insertPage( 'FullTwoLow', 'az', 0 ); + $this->insertPage( 'HalfNumbers', '1234567890', 0 ); + $this->insertPage( 'FullNumbers', '1234567890', 0 ); + $this->insertPage( 'DomainName', 'example.com', 0 ); + } + + function fetchIds( $results ) { + if ( !$this->isWikitextNS( NS_MAIN ) ) { + $this->markTestIncomplete( __CLASS__ . " does no yet support non-wikitext content " + . "in the main namespace" ); + } + + $this->assertTrue( is_object( $results ) ); + + $matches = array(); + $row = $results->next(); + while ( $row ) { + $matches[] = $row->getTitle()->getPrefixedText(); + $row = $results->next(); + } + $results->free(); + # Search is not guaranteed to return results in a certain order; + # sort them numerically so we will compare simply that we received + # the expected matches. + sort( $matches ); + + return $matches; + } + + /** + * Insert a new page + * + * @param $pageName String: page name + * @param $text String: page's content + * @param $n Integer: unused + */ + function insertPage( $pageName, $text, $ns ) { + $title = Title::newFromText( $pageName, $ns ); + + $user = User::newFromName( 'WikiSysop' ); + $comment = 'Search Test'; + + // avoid memory leak...? + LinkCache::singleton()->clear(); + + $page = WikiPage::factory( $title ); + $page->doEditContent( ContentHandler::makeContent( $text, $title ), $comment, 0, false, $user ); + + $this->pageList[] = array( $title, $page->getId() ); + + return true; + } + + public function testFullWidth() { + $this->assertEquals( + array( 'FullOneUp', 'FullTwoLow', 'HalfOneUp', 'HalfTwoLow' ), + $this->fetchIds( $this->search->searchText( 'AZ' ) ), + "Search for normalized from Half-width Upper" ); + $this->assertEquals( + array( 'FullOneUp', 'FullTwoLow', 'HalfOneUp', 'HalfTwoLow' ), + $this->fetchIds( $this->search->searchText( 'az' ) ), + "Search for normalized from Half-width Lower" ); + $this->assertEquals( + array( 'FullOneUp', 'FullTwoLow', 'HalfOneUp', 'HalfTwoLow' ), + $this->fetchIds( $this->search->searchText( 'AZ' ) ), + "Search for normalized from Full-width Upper" ); + $this->assertEquals( + array( 'FullOneUp', 'FullTwoLow', 'HalfOneUp', 'HalfTwoLow' ), + $this->fetchIds( $this->search->searchText( 'az' ) ), + "Search for normalized from Full-width Lower" ); + } + + public function testTextSearch() { + $this->assertEquals( + array( 'Smithee' ), + $this->fetchIds( $this->search->searchText( 'smithee' ) ), + "Plain search failed" ); + } + + public function testTextPowerSearch() { + $this->search->setNamespaces( array( 0, 1, 4 ) ); + $this->assertEquals( + array( + 'Smithee', + 'Talk:Not Main Page', + ), + $this->fetchIds( $this->search->searchText( 'smithee' ) ), + "Power search failed" ); + } + + public function testTitleSearch() { + $this->assertEquals( + array( + 'Alan Smithee', + 'Smithee', + ), + $this->fetchIds( $this->search->searchTitle( 'smithee' ) ), + "Title search failed" ); + } + + public function testTextTitlePowerSearch() { + $this->search->setNamespaces( array( 0, 1, 4 ) ); + $this->assertEquals( + array( + 'Alan Smithee', + 'Smithee', + 'Talk:Smithee', + ), + $this->fetchIds( $this->search->searchTitle( 'smithee' ) ), + "Title power search failed" ); + } +} diff --git a/tests/phpunit/includes/search/SearchUpdateTest.php b/tests/phpunit/includes/search/SearchUpdateTest.php new file mode 100644 index 00000000..2f4fd501 --- /dev/null +++ b/tests/phpunit/includes/search/SearchUpdateTest.php @@ -0,0 +1,74 @@ +setMwGlobals( 'wgSearchType', 'MockSearch' ); + } + + function updateText( $text ) { + return trim( SearchUpdate::updateText( $text ) ); + } + + public function testUpdateText() { + $this->assertEquals( + 'test', + $this->updateText( '

TeSt
' ), + 'HTML stripped, text lowercased' + ); + + $this->assertEquals( + 'foo bar boz quux', + $this->updateText( << +
foo
bar + bozquux + +EOT + ), 'Stripping HTML tables' ); + + $this->assertEquals( + 'a b', + $this->updateText( 'a > b' ), + 'Handle unclosed tags' + ); + + $text = str_pad( "foo assertNotEquals( + '', + $this->updateText( $text ), + 'Bug 18609' + ); + } + + public function testBug32712() { + $text = "text „http://example.com“ text"; + $result = $this->updateText( $text ); + $processed = preg_replace( '/Q/u', 'Q', $result ); + $this->assertTrue( + $processed != '', + 'Link surrounded by unicode quotes should not fail UTF-8 validation' + ); + } +} diff --git a/tests/phpunit/includes/site/MediaWikiSiteTest.php b/tests/phpunit/includes/site/MediaWikiSiteTest.php new file mode 100644 index 00000000..c5d52d33 --- /dev/null +++ b/tests/phpunit/includes/site/MediaWikiSiteTest.php @@ -0,0 +1,90 @@ + + */ +class MediaWikiSiteTest extends SiteTest { + + public function testNormalizePageTitle() { + $site = new MediaWikiSite(); + $site->setGlobalId( 'enwiki' ); + + //NOTE: this does not actually call out to the enwiki site to perform the normalization, + // but uses a local Title object to do so. This is hardcoded on SiteLink::normalizePageTitle + // for the case that MW_PHPUNIT_TEST is set. + $this->assertEquals( 'Foo', $site->normalizePageName( ' foo ' ) ); + } + + public function fileUrlProvider() { + return array( + // url, filepath, path arg, expected + array( 'https://en.wikipedia.org', '/w/$1', 'api.php', 'https://en.wikipedia.org/w/api.php' ), + array( 'https://en.wikipedia.org', '/w/', 'api.php', 'https://en.wikipedia.org/w/' ), + array( 'https://en.wikipedia.org', '/foo/page.php?name=$1', 'api.php', 'https://en.wikipedia.org/foo/page.php?name=api.php' ), + array( 'https://en.wikipedia.org', '/w/$1', '', 'https://en.wikipedia.org/w/' ), + array( 'https://en.wikipedia.org', '/w/$1', 'foo/bar/api.php', 'https://en.wikipedia.org/w/foo/bar/api.php' ), + ); + } + + /** + * @dataProvider fileUrlProvider + * @covers MediaWikiSite::getFileUrl + */ + public function testGetFileUrl( $url, $filePath, $pathArgument, $expected ) { + $site = new MediaWikiSite(); + $site->setFilePath( $url . $filePath ); + + $this->assertEquals( $expected, $site->getFileUrl( $pathArgument ) ); + } + + public static function provideGetPageUrl() { + return array( + // path, page, expected substring + array( 'http://acme.test/wiki/$1', 'Berlin', '/wiki/Berlin' ), + array( 'http://acme.test/wiki/', 'Berlin', '/wiki/' ), + array( 'http://acme.test/w/index.php?title=$1', 'Berlin', '/w/index.php?title=Berlin' ), + array( 'http://acme.test/wiki/$1', '', '/wiki/' ), + array( 'http://acme.test/wiki/$1', 'Berlin/sub page', '/wiki/Berlin/sub_page' ), + array( 'http://acme.test/wiki/$1', 'Cork (city) ', '/Cork_(city)' ), + array( 'http://acme.test/wiki/$1', 'M&M', '/wiki/M%26M' ), + ); + } + + /** + * @dataProvider provideGetPageUrl + * @covers MediaWikiSite::getPageUrl + */ + public function testGetPageUrl( $path, $page, $expected ) { + $site = new MediaWikiSite(); + $site->setLinkPath( $path ); + + $this->assertContains( $path, $site->getPageUrl() ); + $this->assertContains( $expected, $site->getPageUrl( $page ) ); + } +} diff --git a/tests/phpunit/includes/site/SiteListTest.php b/tests/phpunit/includes/site/SiteListTest.php new file mode 100644 index 00000000..8af2fc1b --- /dev/null +++ b/tests/phpunit/includes/site/SiteListTest.php @@ -0,0 +1,197 @@ + + */ +class SiteListTest extends MediaWikiTestCase { + + /** + * Returns instances of SiteList implementing objects. + * @return array + */ + public function siteListProvider() { + $sitesArrays = $this->siteArrayProvider(); + + $listInstances = array(); + + foreach ( $sitesArrays as $sitesArray ) { + $listInstances[] = new SiteList( $sitesArray[0] ); + } + + return $this->arrayWrap( $listInstances ); + } + + /** + * Returns arrays with instances of Site implementing objects. + * @return array + */ + public function siteArrayProvider() { + $sites = TestSites::getSites(); + + $siteArrays = array(); + + $siteArrays[] = $sites; + + $siteArrays[] = array( array_shift( $sites ) ); + + $siteArrays[] = array( array_shift( $sites ), array_shift( $sites ) ); + + return $this->arrayWrap( $siteArrays ); + } + + /** + * @dataProvider siteListProvider + * @param SiteList $sites + * @covers SiteList::isEmpty + */ + public function testIsEmpty( SiteList $sites ) { + $this->assertEquals( count( $sites ) === 0, $sites->isEmpty() ); + } + + /** + * @dataProvider siteListProvider + * @param SiteList $sites + * @covers SiteList::getSite + */ + public function testGetSiteByGlobalId( SiteList $sites ) { + if ( $sites->isEmpty() ) { + $this->assertTrue( true ); + } else { + /** + * @var Site $site + */ + foreach ( $sites as $site ) { + $this->assertEquals( $site, $sites->getSite( $site->getGlobalId() ) ); + } + } + } + + /** + * @dataProvider siteListProvider + * @param SiteList $sites + * @covers SiteList::getSiteByInternalId + */ + public function testGetSiteByInternalId( $sites ) { + /** + * @var Site $site + */ + foreach ( $sites as $site ) { + if ( is_integer( $site->getInternalId() ) ) { + $this->assertEquals( $site, $sites->getSiteByInternalId( $site->getInternalId() ) ); + } + } + + $this->assertTrue( true ); + } + + /** + * @dataProvider siteListProvider + * @param SiteList $sites + * @covers SiteList::hasSite + */ + public function testHasGlobalId( $sites ) { + $this->assertFalse( $sites->hasSite( 'non-existing-global-id' ) ); + $this->assertFalse( $sites->hasInternalId( 720101010 ) ); + + if ( !$sites->isEmpty() ) { + /** + * @var Site $site + */ + foreach ( $sites as $site ) { + $this->assertTrue( $sites->hasSite( $site->getGlobalId() ) ); + } + } + } + + /** + * @dataProvider siteListProvider + * @param SiteList $sites + * @covers SiteList::hasInternalId + */ + public function testHasInternallId( $sites ) { + /** + * @var Site $site + */ + foreach ( $sites as $site ) { + if ( is_integer( $site->getInternalId() ) ) { + $this->assertTrue( $site, $sites->hasInternalId( $site->getInternalId() ) ); + } + } + + $this->assertFalse( $sites->hasInternalId( -1 ) ); + } + + /** + * @dataProvider siteListProvider + * @param SiteList $sites + * @covers SiteList::getGlobalIdentifiers + */ + public function testGetGlobalIdentifiers( SiteList $sites ) { + $identifiers = $sites->getGlobalIdentifiers(); + + $this->assertTrue( is_array( $identifiers ) ); + + $expected = array(); + + /** + * @var Site $site + */ + foreach ( $sites as $site ) { + $expected[] = $site->getGlobalId(); + } + + $this->assertArrayEquals( $expected, $identifiers ); + } + + /** + * @dataProvider siteListProvider + * + * @since 1.21 + * + * @param SiteList $list + * @covers SiteList::getSerializationData + * @covers SiteList::unserialize + */ + public function testSerialization( SiteList $list ) { + $serialization = serialize( $list ); + /** + * @var SiteArray $copy + */ + $copy = unserialize( $serialization ); + + $this->assertArrayEquals( $list->getGlobalIdentifiers(), $copy->getGlobalIdentifiers() ); + + /** + * @var Site $site + */ + foreach ( $list as $site ) { + $this->assertTrue( $copy->hasInternalId( $site->getInternalId() ) ); + } + } +} diff --git a/tests/phpunit/includes/site/SiteSQLStoreTest.php b/tests/phpunit/includes/site/SiteSQLStoreTest.php new file mode 100644 index 00000000..6002c1a1 --- /dev/null +++ b/tests/phpunit/includes/site/SiteSQLStoreTest.php @@ -0,0 +1,134 @@ + + */ +class SiteSQLStoreTest extends MediaWikiTestCase { + + /** + * @covers SiteSQLStore::getSites + */ + public function testGetSites() { + $expectedSites = TestSites::getSites(); + TestSites::insertIntoDb(); + + $store = SiteSQLStore::newInstance(); + + $sites = $store->getSites(); + + $this->assertInstanceOf( 'SiteList', $sites ); + + /** + * @var Site $site + */ + foreach ( $sites as $site ) { + $this->assertInstanceOf( 'Site', $site ); + } + + foreach ( $expectedSites as $site ) { + if ( $site->getGlobalId() !== null ) { + $this->assertTrue( $sites->hasSite( $site->getGlobalId() ) ); + } + } + } + + /** + * @covers SiteSQLStore::saveSites + */ + public function testSaveSites() { + $store = SiteSQLStore::newInstance(); + + $sites = array(); + + $site = new Site(); + $site->setGlobalId( 'ertrywuutr' ); + $site->setLanguageCode( 'en' ); + $sites[] = $site; + + $site = new MediaWikiSite(); + $site->setGlobalId( 'sdfhxujgkfpth' ); + $site->setLanguageCode( 'nl' ); + $sites[] = $site; + + $this->assertTrue( $store->saveSites( $sites ) ); + + $site = $store->getSite( 'ertrywuutr' ); + $this->assertInstanceOf( 'Site', $site ); + $this->assertEquals( 'en', $site->getLanguageCode() ); + $this->assertTrue( is_integer( $site->getInternalId() ) ); + $this->assertTrue( $site->getInternalId() >= 0 ); + + $site = $store->getSite( 'sdfhxujgkfpth' ); + $this->assertInstanceOf( 'Site', $site ); + $this->assertEquals( 'nl', $site->getLanguageCode() ); + $this->assertTrue( is_integer( $site->getInternalId() ) ); + $this->assertTrue( $site->getInternalId() >= 0 ); + } + + /** + * @covers SiteSQLStore::reset + */ + public function testReset() { + $store1 = SiteSQLStore::newInstance(); + $store2 = SiteSQLStore::newInstance(); + + // initialize internal cache + $this->assertGreaterThan( 0, $store1->getSites()->count() ); + $this->assertGreaterThan( 0, $store2->getSites()->count() ); + + // Clear actual data. Will purge the external cache and reset the internal + // cache in $store1, but not the internal cache in store2. + $this->assertTrue( $store1->clear() ); + + // sanity check: $store2 should have a stale cache now + $this->assertNotNull( $store2->getSite( 'enwiki' ) ); + + // purge cache + $store2->reset(); + + // ...now the internal cache of $store2 should be updated and thus empty. + $site = $store2->getSite( 'enwiki' ); + $this->assertNull( $site ); + } + + /** + * @covers SiteSQLStore::clear + */ + public function testClear() { + $store = SiteSQLStore::newInstance(); + $this->assertTrue( $store->clear() ); + + $site = $store->getSite( 'enwiki' ); + $this->assertNull( $site ); + + $sites = $store->getSites(); + $this->assertEquals( 0, $sites->count() ); + } +} diff --git a/tests/phpunit/includes/site/SiteTest.php b/tests/phpunit/includes/site/SiteTest.php new file mode 100644 index 00000000..29c1ff33 --- /dev/null +++ b/tests/phpunit/includes/site/SiteTest.php @@ -0,0 +1,296 @@ + + */ +class SiteTest extends MediaWikiTestCase { + + public function instanceProvider() { + return $this->arrayWrap( TestSites::getSites() ); + } + + /** + * @dataProvider instanceProvider + * @param Site $site + * @covers Site::getInterwikiIds + */ + public function testGetInterwikiIds( Site $site ) { + $this->assertInternalType( 'array', $site->getInterwikiIds() ); + } + + /** + * @dataProvider instanceProvider + * @param Site $site + * @covers Site::getNavigationIds + */ + public function testGetNavigationIds( Site $site ) { + $this->assertInternalType( 'array', $site->getNavigationIds() ); + } + + /** + * @dataProvider instanceProvider + * @param Site $site + * @covers Site::addNavigationId + */ + public function testAddNavigationId( Site $site ) { + $site->addNavigationId( 'foobar' ); + $this->assertTrue( in_array( 'foobar', $site->getNavigationIds(), true ) ); + } + + /** + * @dataProvider instanceProvider + * @param Site $site + * @covers Site::addInterwikiId + */ + public function testAddInterwikiId( Site $site ) { + $site->addInterwikiId( 'foobar' ); + $this->assertTrue( in_array( 'foobar', $site->getInterwikiIds(), true ) ); + } + + /** + * @dataProvider instanceProvider + * @param Site $site + * @covers Site::getLanguageCode + */ + public function testGetLanguageCode( Site $site ) { + $this->assertTypeOrValue( 'string', $site->getLanguageCode(), null ); + } + + /** + * @dataProvider instanceProvider + * @param Site $site + * @covers Site::setLanguageCode + */ + public function testSetLanguageCode( Site $site ) { + $site->setLanguageCode( 'en' ); + $this->assertEquals( 'en', $site->getLanguageCode() ); + } + + /** + * @dataProvider instanceProvider + * @param Site $site + * @covers Site::normalizePageName + */ + public function testNormalizePageName( Site $site ) { + $this->assertInternalType( 'string', $site->normalizePageName( 'Foobar' ) ); + } + + /** + * @dataProvider instanceProvider + * @param Site $site + * @covers Site::getGlobalId + */ + public function testGetGlobalId( Site $site ) { + $this->assertTypeOrValue( 'string', $site->getGlobalId(), null ); + } + + /** + * @dataProvider instanceProvider + * @param Site $site + * @covers Site::setGlobalId + */ + public function testSetGlobalId( Site $site ) { + $site->setGlobalId( 'foobar' ); + $this->assertEquals( 'foobar', $site->getGlobalId() ); + } + + /** + * @dataProvider instanceProvider + * @param Site $site + * @covers Site::getType + */ + public function testGetType( Site $site ) { + $this->assertInternalType( 'string', $site->getType() ); + } + + /** + * @dataProvider instanceProvider + * @param Site $site + * @covers Site::getPath + */ + public function testGetPath( Site $site ) { + $this->assertTypeOrValue( 'string', $site->getPath( 'page_path' ), null ); + $this->assertTypeOrValue( 'string', $site->getPath( 'file_path' ), null ); + $this->assertTypeOrValue( 'string', $site->getPath( 'foobar' ), null ); + } + + /** + * @dataProvider instanceProvider + * @param Site $site + * @covers Site::getAllPaths + */ + public function testGetAllPaths( Site $site ) { + $this->assertInternalType( 'array', $site->getAllPaths() ); + } + + /** + * @dataProvider instanceProvider + * @param Site $site + * @covers Site::setPath + * @covers Site::removePath + */ + public function testSetAndRemovePath( Site $site ) { + $count = count( $site->getAllPaths() ); + + $site->setPath( 'spam', 'http://www.wikidata.org/$1' ); + $site->setPath( 'spam', 'http://www.wikidata.org/foo/$1' ); + $site->setPath( 'foobar', 'http://www.wikidata.org/bar/$1' ); + + $this->assertEquals( $count + 2, count( $site->getAllPaths() ) ); + + $this->assertInternalType( 'string', $site->getPath( 'foobar' ) ); + $this->assertEquals( 'http://www.wikidata.org/foo/$1', $site->getPath( 'spam' ) ); + + $site->removePath( 'spam' ); + $site->removePath( 'foobar' ); + + $this->assertEquals( $count, count( $site->getAllPaths() ) ); + + $this->assertNull( $site->getPath( 'foobar' ) ); + $this->assertNull( $site->getPath( 'spam' ) ); + } + + /** + * @covers Site::setLinkPath + */ + public function testSetLinkPath() { + $site = new Site(); + $path = "TestPath/$1"; + + $site->setLinkPath( $path ); + $this->assertEquals( $path, $site->getLinkPath() ); + } + + /** + * @covers Site::getLinkPathType + */ + public function testGetLinkPathType() { + $site = new Site(); + + $path = 'TestPath/$1'; + $site->setLinkPath( $path ); + $this->assertEquals( $path, $site->getPath( $site->getLinkPathType() ) ); + + $path = 'AnotherPath/$1'; + $site->setPath( $site->getLinkPathType(), $path ); + $this->assertEquals( $path, $site->getLinkPath() ); + } + + /** + * @covers Site::setPath + */ + public function testSetPath() { + $site = new Site(); + + $path = 'TestPath/$1'; + $site->setPath( 'foo', $path ); + + $this->assertEquals( $path, $site->getPath( 'foo' ) ); + } + + /** + * @covers Site::setPath + * @covers Site::getProtocol + */ + public function testProtocolRelativePath() { + $site = new Site(); + + $type = $site->getLinkPathType(); + $path = '//acme.com/'; // protocol-relative URL + $site->setPath( $type, $path ); + + $this->assertEquals( '', $site->getProtocol() ); + } + + public static function provideGetPageUrl() { + //NOTE: the assumption that the URL is built by replacing $1 + // with the urlencoded version of $page + // is true for Site but not guaranteed for subclasses. + // Subclasses need to override this provider appropriately. + + return array( + array( #0 + 'http://acme.test/TestPath/$1', + 'Foo', + '/TestPath/Foo', + ), + array( #1 + 'http://acme.test/TestScript?x=$1&y=bla', + 'Foo', + 'TestScript?x=Foo&y=bla', + ), + array( #2 + 'http://acme.test/TestPath/$1', + 'foo & bar/xyzzy (quux-shmoox?)', + '/TestPath/foo%20%26%20bar%2Fxyzzy%20%28quux-shmoox%3F%29', + ), + ); + } + + /** + * @dataProvider provideGetPageUrl + * @covers Site::getPageUrl + */ + public function testGetPageUrl( $path, $page, $expected ) { + $site = new Site(); + + //NOTE: the assumption that getPageUrl is based on getLinkPath + // is true for Site but not guaranteed for subclasses. + // Subclasses need to override this test case appropriately. + $site->setLinkPath( $path ); + $this->assertContains( $path, $site->getPageUrl() ); + + $this->assertContains( $expected, $site->getPageUrl( $page ) ); + } + + protected function assertTypeOrFalse( $type, $value ) { + if ( $value === false ) { + $this->assertTrue( true ); + } else { + $this->assertInternalType( $type, $value ); + } + } + + /** + * @dataProvider instanceProvider + * @param Site $site + * @covers Site::serialize + * @covers Site::unserialize + */ + public function testSerialization( Site $site ) { + $this->assertInstanceOf( 'Serializable', $site ); + + $serialization = serialize( $site ); + $newInstance = unserialize( $serialization ); + + $this->assertInstanceOf( 'Site', $newInstance ); + + $this->assertEquals( $serialization, serialize( $newInstance ) ); + } +} diff --git a/tests/phpunit/includes/site/TestSites.php b/tests/phpunit/includes/site/TestSites.php new file mode 100644 index 00000000..f224b7d7 --- /dev/null +++ b/tests/phpunit/includes/site/TestSites.php @@ -0,0 +1,100 @@ + + */ +class TestSites { + + /** + * @since 1.21 + * + * @return array + */ + public static function getSites() { + $sites = array(); + + $site = new Site(); + $site->setGlobalId( 'foobar' ); + $sites[] = $site; + + $site = new MediaWikiSite(); + $site->setGlobalId( 'enwiktionary' ); + $site->setGroup( 'wiktionary' ); + $site->setLanguageCode( 'en' ); + $site->addNavigationId( 'enwiktionary' ); + $site->setPath( MediaWikiSite::PATH_PAGE, "https://en.wiktionary.org/wiki/$1" ); + $site->setPath( MediaWikiSite::PATH_FILE, "https://en.wiktionary.org/w/$1" ); + $sites[] = $site; + + $site = new MediaWikiSite(); + $site->setGlobalId( 'dewiktionary' ); + $site->setGroup( 'wiktionary' ); + $site->setLanguageCode( 'de' ); + $site->addInterwikiId( 'dewiktionary' ); + $site->addInterwikiId( 'wiktionaryde' ); + $site->setPath( MediaWikiSite::PATH_PAGE, "https://de.wiktionary.org/wiki/$1" ); + $site->setPath( MediaWikiSite::PATH_FILE, "https://de.wiktionary.org/w/$1" ); + $sites[] = $site; + + $site = new Site(); + $site->setGlobalId( 'spam' ); + $site->setGroup( 'spam' ); + $site->setLanguageCode( 'en' ); + $site->addNavigationId( 'spam' ); + $site->addNavigationId( 'spamz' ); + $site->addInterwikiId( 'spamzz' ); + $site->setLinkPath( "http://spamzz.test/testing/" ); + $sites[] = $site; + + foreach ( array( 'en', 'de', 'nl', 'sv', 'sr', 'no', 'nn' ) as $langCode ) { + $site = new MediaWikiSite(); + $site->setGlobalId( $langCode . 'wiki' ); + $site->setGroup( 'wikipedia' ); + $site->setLanguageCode( $langCode ); + $site->addInterwikiId( $langCode ); + $site->addNavigationId( $langCode ); + $site->setPath( MediaWikiSite::PATH_PAGE, "https://$langCode.wikipedia.org/wiki/$1" ); + $site->setPath( MediaWikiSite::PATH_FILE, "https://$langCode.wikipedia.org/w/$1" ); + $sites[] = $site; + } + + return $sites; + } + + /** + * Inserts sites into the database for the unit tests that need them. + * + * @since 0.1 + */ + public static function insertIntoDb() { + $sitesTable = SiteSQLStore::newInstance(); + $sitesTable->clear(); + $sitesTable->saveSites( TestSites::getSites() ); + } +} diff --git a/tests/phpunit/includes/specials/QueryAllSpecialPagesTest.php b/tests/phpunit/includes/specials/QueryAllSpecialPagesTest.php new file mode 100644 index 00000000..a806b4ac --- /dev/null +++ b/tests/phpunit/includes/specials/QueryAllSpecialPagesTest.php @@ -0,0 +1,79 @@ +manualTest ) ) { + $this->queryPages[$class] = new $class; + } + } + } + + /** + * Test SQL for each of our QueryPages objects + * @group Database + */ + public function testQuerypageSqlQuery() { + global $wgDBtype; + + foreach ( $this->queryPages as $page ) { + + // With MySQL, skips special pages reopening a temporary table + // See http://bugs.mysql.com/bug.php?id=10327 + if ( + $wgDBtype === 'mysql' + && in_array( $page->getName(), $this->reopensTempTable ) + ) { + $this->markTestSkipped( "SQL query for page {$page->getName()} can not be tested on MySQL backend (it reopens a temporary table)" ); + continue; + } + + $msg = "SQL query for page {$page->getName()} should give a result wrapper object"; + + $result = $page->reallyDoQuery( 50 ); + if ( $result instanceof ResultWrapper ) { + $this->assertTrue( true, $msg ); + } else { + $this->assertFalse( false, $msg ); + } + } + } +} diff --git a/tests/phpunit/includes/specials/SpecialPreferencesTest.php b/tests/phpunit/includes/specials/SpecialPreferencesTest.php new file mode 100644 index 00000000..6c637c65 --- /dev/null +++ b/tests/phpunit/includes/specials/SpecialPreferencesTest.php @@ -0,0 +1,60 @@ +setMwGlobals( 'wgMaxSigChars', 2 ); + + $user = $this->getMock( 'User' ); + $user->expects( $this->any() ) + ->method( 'isAnon' ) + ->will( $this->returnValue( false ) ); + + # Yeah foreach requires an array, not NULL =( + $user->expects( $this->any() ) + ->method( 'getEffectiveGroups' ) + ->will( $this->returnValue( array() ) ); + + # The mocked user has a long nickname + $user->expects( $this->any() ) + ->method( 'getOption' ) + ->will( $this->returnValueMap( array( + array( 'nickname', null, false, 'superlongnickname' ), + ) + ) ); + + # Validate the mock (FIXME should probably be removed) + $this->assertFalse( $user->isAnon() ); + $this->assertEquals( array(), + $user->getEffectiveGroups() ); + $this->assertEquals( 'superlongnickname', + $user->getOption( 'nickname' ) ); + + # Forge a request to call the special page + $context = new RequestContext(); + $context->setRequest( new FauxRequest() ); + $context->setUser( $user ); + $context->setTitle( Title::newFromText( 'Test' ) ); + + # Do the call, should not spurt a fatal error. + $special = new SpecialPreferences(); + $special->setContext( $context ); + $special->execute( array() ); + } + +} diff --git a/tests/phpunit/includes/specials/SpecialRecentchangesTest.php b/tests/phpunit/includes/specials/SpecialRecentchangesTest.php new file mode 100644 index 00000000..436eb2e2 --- /dev/null +++ b/tests/phpunit/includes/specials/SpecialRecentchangesTest.php @@ -0,0 +1,125 @@ +setRequest( new FauxRequest( $requestOptions ) ); + + # setup the rc object + $this->rc = new SpecialRecentChanges(); + $this->rc->setContext( $context ); + $formOptions = $this->rc->setup( null ); + + # Filter out rc_timestamp conditions which depends on the test runtime + # This condition is not needed as of march 2, 2011 -- hashar + # @todo FIXME: Find a way to generate the correct rc_timestamp + $queryConditions = array_filter( + $this->rc->buildMainQueryConds( $formOptions ), + 'SpecialRecentchangesTest::filterOutRcTimestampCondition' + ); + + $this->assertEquals( + $expected, + $queryConditions, + $message + ); + } + + /** return false if condition begin with 'rc_timestamp ' */ + private static function filterOutRcTimestampCondition( $var ) { + return ( false === strpos( $var, 'rc_timestamp ' ) ); + } + + public function testRcNsFilter() { + $this->assertConditions( + array( # expected + 'rc_bot' => 0, + #0 => "rc_timestamp >= '20110223000000'", + 1 => "rc_namespace = '0'", + ), + array( + 'namespace' => NS_MAIN, + ), + "rc conditions with no options (aka default setting)" + ); + } + + public function testRcNsFilterInversion() { + $this->assertConditions( + array( # expected + #0 => "rc_timestamp >= '20110223000000'", + 'rc_bot' => 0, + 1 => sprintf( "rc_namespace != '%s'", NS_MAIN ), + ), + array( + 'namespace' => NS_MAIN, + 'invert' => 1, + ), + "rc conditions with namespace inverted" + ); + } + + /** + * @bug 2429 + * @dataProvider provideNamespacesAssociations + */ + public function testRcNsFilterAssociation( $ns1, $ns2 ) { + $this->assertConditions( + array( # expected + #0 => "rc_timestamp >= '20110223000000'", + 'rc_bot' => 0, + 1 => sprintf( "(rc_namespace = '%s' OR rc_namespace = '%s')", $ns1, $ns2 ), + ), + array( + 'namespace' => $ns1, + 'associated' => 1, + ), + "rc conditions with namespace inverted" + ); + } + + /** + * @bug 2429 + * @dataProvider provideNamespacesAssociations + */ + public function testRcNsFilterAssociationWithInversion( $ns1, $ns2 ) { + $this->assertConditions( + array( # expected + #0 => "rc_timestamp >= '20110223000000'", + 'rc_bot' => 0, + 1 => sprintf( "(rc_namespace != '%s' AND rc_namespace != '%s')", $ns1, $ns2 ), + ), + array( + 'namespace' => $ns1, + 'associated' => 1, + 'invert' => 1, + ), + "rc conditions with namespace inverted" + ); + } + + /** + * Provides associated namespaces to test recent changes + * namespaces association filtering. + */ + public static function provideNamespacesAssociations() { + return array( # (NS => Associated_NS) + array( NS_MAIN, NS_TALK ), + array( NS_TALK, NS_MAIN ), + ); + } +} diff --git a/tests/phpunit/includes/specials/SpecialSearchTest.php b/tests/phpunit/includes/specials/SpecialSearchTest.php new file mode 100644 index 00000000..17e883fd --- /dev/null +++ b/tests/phpunit/includes/specials/SpecialSearchTest.php @@ -0,0 +1,139 @@ + true, 'ns6' => true). NULL to use default options. + * @param $userOptions Array User options to test with. For example array('searchNs5' => 1 );. NULL to use default options. + * @param $expectedProfile An expected search profile name + * @param $expectedNs Array Expected namespaces + */ + public function testProfileAndNamespaceLoading( + $requested, $userOptions, $expectedProfile, $expectedNS, + $message = 'Profile name and namespaces mismatches!' + ) { + $context = new RequestContext; + $context->setUser( + $this->newUserWithSearchNS( $userOptions ) + ); + /* + $context->setRequest( new FauxRequest( array( + 'ns5'=>true, + 'ns6'=>true, + ) )); + */ + $context->setRequest( new FauxRequest( $requested ) ); + $search = new SpecialSearch(); + $search->setContext( $context ); + $search->load(); + + /** + * Verify profile name and namespace in the same assertion to make + * sure we will be able to fully compare the above code. PHPUnit stop + * after an assertion fail. + */ + $this->assertEquals( + array( /** Expected: */ + 'ProfileName' => $expectedProfile, + 'Namespaces' => $expectedNS, + ) + , array( /** Actual: */ + 'ProfileName' => $search->getProfile(), + 'Namespaces' => $search->getNamespaces(), + ) + , $message + ); + } + + public static function provideSearchOptionsTests() { + $defaultNS = SearchEngine::defaultNamespaces(); + $EMPTY_REQUEST = array(); + $NO_USER_PREF = null; + + return array( + /** + * Parameters: + * , + * Followed by expected values: + * , + * Then an optional message. + */ + array( + $EMPTY_REQUEST, $NO_USER_PREF, + 'default', $defaultNS, + 'Bug 33270: No request nor user preferences should give default profile' + ), + array( + array( 'ns5' => 1 ), $NO_USER_PREF, + 'advanced', array( 5 ), + 'Web request with specific NS should override user preference' + ), + array( + $EMPTY_REQUEST, array( + 'searchNs2' => 1, + 'searchNs14' => 1, + ) + array_fill_keys( array_map( function ( $ns ) { + return "searchNs$ns"; + }, $defaultNS ), 0 ), + 'advanced', array( 2, 14 ), + 'Bug 33583: search with no option should honor User search preferences' + . ' and have all other namespace disabled' + ), + ); + } + + /** + * Helper to create a new User object with given options + * User remains anonymous though + */ + function newUserWithSearchNS( $opt = null ) { + $u = User::newFromId( 0 ); + if ( $opt === null ) { + return $u; + } + foreach ( $opt as $name => $value ) { + $u->setOption( $name, $value ); + } + + return $u; + } + + /** + * Verify we do not expand search term in on search result page + * https://gerrit.wikimedia.org/r/4841 + */ + public function testSearchTermIsNotExpanded() { + + # Initialize [[Special::Search]] + $search = new SpecialSearch(); + $search->getContext()->setTitle( Title::newFromText( 'Special:Search' ) ); + $search->load(); + + # Simulate a user searching for a given term + $term = '{{SITENAME}}'; + $search->showResults( $term ); + + # Lookup the HTML page title set for that page + $pageTitle = $search + ->getContext() + ->getOutput() + ->getHTMLTitle(); + + # Compare :-] + $this->assertRegExp( + '/' . preg_quote( $term ) . '/', + $pageTitle, + "Search term '{$term}' should not be expanded in Special:Search <title>" + ); + } +} diff --git a/tests/phpunit/includes/upload/UploadBaseTest.php b/tests/phpunit/includes/upload/UploadBaseTest.php new file mode 100644 index 00000000..982b46b2 --- /dev/null +++ b/tests/phpunit/includes/upload/UploadBaseTest.php @@ -0,0 +1,147 @@ +<?php + +/** + * @group Upload + */ +class UploadBaseTest extends MediaWikiTestCase { + + /** @var UploadTestHandler */ + protected $upload; + + protected function setUp() { + global $wgHooks; + parent::setUp(); + + $this->upload = new UploadTestHandler; + $this->hooks = $wgHooks; + $wgHooks['InterwikiLoadPrefix'][] = function ( $prefix, &$data ) { + return false; + }; + } + + protected function tearDown() { + global $wgHooks; + $wgHooks = $this->hooks; + + parent::tearDown(); + } + + + /** + * First checks the return code + * of UploadBase::getTitle() and then the actual returned title + * + * @dataProvider provideTestTitleValidation + * @covers UploadBase::getTitle + */ + public function testTitleValidation( $srcFilename, $dstFilename, $code, $msg ) { + /* Check the result code */ + $this->assertEquals( $code, + $this->upload->testTitleValidation( $srcFilename ), + "$msg code" ); + + /* If we expect a valid title, check the title itself. */ + if ( $code == UploadBase::OK ) { + $this->assertEquals( $dstFilename, + $this->upload->getTitle()->getText(), + "$msg text" ); + } + } + + /** + * Test various forms of valid and invalid titles that can be supplied. + */ + public static function provideTestTitleValidation() { + return array( + /* Test a valid title */ + array( 'ValidTitle.jpg', 'ValidTitle.jpg', UploadBase::OK, + 'upload valid title' ), + /* A title with a slash */ + array( 'A/B.jpg', 'B.jpg', UploadBase::OK, + 'upload title with slash' ), + /* A title with illegal char */ + array( 'A:B.jpg', 'A-B.jpg', UploadBase::OK, + 'upload title with colon' ), + /* Stripping leading File: prefix */ + array( 'File:C.jpg', 'C.jpg', UploadBase::OK, + 'upload title with File prefix' ), + /* Test illegal suggested title (r94601) */ + array( '%281%29.JPG', null, UploadBase::ILLEGAL_FILENAME, + 'illegal title for upload' ), + /* A title without extension */ + array( 'A', null, UploadBase::FILETYPE_MISSING, + 'upload title without extension' ), + /* A title with no basename */ + array( '.jpg', null, UploadBase::MIN_LENGTH_PARTNAME, + 'upload title without basename' ), + /* A title that is longer than 255 bytes */ + array( str_repeat( 'a', 255 ) . '.jpg', null, UploadBase::FILENAME_TOO_LONG, + 'upload title longer than 255 bytes' ), + /* A title that is longer than 240 bytes */ + array( str_repeat( 'a', 240 ) . '.jpg', null, UploadBase::FILENAME_TOO_LONG, + 'upload title longer than 240 bytes' ), + ); + } + + /** + * Test the upload verification functions + * @covers UploadBase::verifyUpload + */ + public function testVerifyUpload() { + /* Setup with zero file size */ + $this->upload->initializePathInfo( '', '', 0 ); + $result = $this->upload->verifyUpload(); + $this->assertEquals( UploadBase::EMPTY_FILE, + $result['status'], + 'upload empty file' ); + } + + // Helper used to create an empty file of size $size. + private function createFileOfSize( $size ) { + $filename = tempnam( wfTempDir(), "mwuploadtest" ); + + $fh = fopen( $filename, 'w' ); + ftruncate( $fh, $size ); + fclose( $fh ); + + return $filename; + } + + /** + * test uploading a 100 bytes file with $wgMaxUploadSize = 100 + * + * This method should be abstracted so we can test different settings. + */ + public function testMaxUploadSize() { + global $wgMaxUploadSize; + $savedGlobal = $wgMaxUploadSize; // save global + global $wgFileExtensions; + $wgFileExtensions[] = 'txt'; + + $wgMaxUploadSize = 100; + + $filename = $this->createFileOfSize( $wgMaxUploadSize ); + $this->upload->initializePathInfo( basename( $filename ) . '.txt', $filename, 100 ); + $result = $this->upload->verifyUpload(); + unlink( $filename ); + + $this->assertEquals( + array( 'status' => UploadBase::OK ), $result ); + + $wgMaxUploadSize = $savedGlobal; // restore global + } +} + +class UploadTestHandler extends UploadBase { + public function initializeFromRequest( &$request ) { + } + + public function testTitleValidation( $name ) { + $this->mTitle = false; + $this->mDesiredDestName = $name; + $this->mTitleError = UploadBase::OK; + $this->getTitle(); + + return $this->mTitleError; + } +} diff --git a/tests/phpunit/includes/upload/UploadFromUrlTest.php b/tests/phpunit/includes/upload/UploadFromUrlTest.php new file mode 100644 index 00000000..a75fba69 --- /dev/null +++ b/tests/phpunit/includes/upload/UploadFromUrlTest.php @@ -0,0 +1,350 @@ +<?php + +/** + * @group Broken + * @group Upload + * @group Database + */ +class UploadFromUrlTest extends ApiTestCase { + protected function setUp() { + parent::setUp(); + + $this->setMwGlobals( array( + 'wgEnableUploads' => true, + 'wgAllowCopyUploads' => true, + 'wgAllowAsyncCopyUploads' => true, + ) ); + wfSetupSession(); + + if ( wfLocalFile( 'UploadFromUrlTest.png' )->exists() ) { + $this->deleteFile( 'UploadFromUrlTest.png' ); + } + } + + protected function doApiRequest( array $params, array $unused = null, $appendModule = false, User $user = null ) { + $sessionId = session_id(); + session_write_close(); + + $req = new FauxRequest( $params, true, $_SESSION ); + $module = new ApiMain( $req, true ); + $module->execute(); + + wfSetupSession( $sessionId ); + + return array( $module->getResultData(), $req ); + } + + /** + * Ensure that the job queue is empty before continuing + */ + public function testClearQueue() { + $job = JobQueueGroup::singleton()->pop(); + while ( $job ) { + $job = JobQueueGroup::singleton()->pop(); + } + $this->assertFalse( $job ); + } + + /** + * @todo Document why we test login, since the $wgUser hack used doesn't + * require login + */ + public function testLogin() { + $data = $this->doApiRequest( array( + 'action' => 'login', + 'lgname' => $this->user->userName, + 'lgpassword' => $this->user->passWord ) ); + $this->assertArrayHasKey( "login", $data[0] ); + $this->assertArrayHasKey( "result", $data[0]['login'] ); + $this->assertEquals( "NeedToken", $data[0]['login']['result'] ); + $token = $data[0]['login']['token']; + + $data = $this->doApiRequest( array( + 'action' => 'login', + "lgtoken" => $token, + 'lgname' => $this->user->userName, + 'lgpassword' => $this->user->passWord ) ); + + $this->assertArrayHasKey( "login", $data[0] ); + $this->assertArrayHasKey( "result", $data[0]['login'] ); + $this->assertEquals( "Success", $data[0]['login']['result'] ); + $this->assertArrayHasKey( 'lgtoken', $data[0]['login'] ); + + return $data; + } + + /** + * @depends testLogin + * @depends testClearQueue + */ + public function testSetupUrlDownload( $data ) { + $token = $this->user->getEditToken(); + $exception = false; + + try { + $this->doApiRequest( array( + 'action' => 'upload', + ) ); + } catch ( UsageException $e ) { + $exception = true; + $this->assertEquals( "The token parameter must be set", $e->getMessage() ); + } + $this->assertTrue( $exception, "Got exception" ); + + $exception = false; + try { + $this->doApiRequest( array( + 'action' => 'upload', + 'token' => $token, + ), $data ); + } catch ( UsageException $e ) { + $exception = true; + $this->assertEquals( "One of the parameters sessionkey, file, url, statuskey is required", + $e->getMessage() ); + } + $this->assertTrue( $exception, "Got exception" ); + + $exception = false; + try { + $this->doApiRequest( array( + 'action' => 'upload', + 'url' => 'http://www.example.com/test.png', + 'token' => $token, + ), $data ); + } catch ( UsageException $e ) { + $exception = true; + $this->assertEquals( "The filename parameter must be set", $e->getMessage() ); + } + $this->assertTrue( $exception, "Got exception" ); + + $this->user->removeGroup( 'sysop' ); + $exception = false; + try { + $this->doApiRequest( array( + 'action' => 'upload', + 'url' => 'http://www.example.com/test.png', + 'filename' => 'UploadFromUrlTest.png', + 'token' => $token, + ), $data ); + } catch ( UsageException $e ) { + $exception = true; + $this->assertEquals( "Permission denied", $e->getMessage() ); + } + $this->assertTrue( $exception, "Got exception" ); + + $this->user->addGroup( 'sysop' ); + $data = $this->doApiRequest( array( + 'action' => 'upload', + 'url' => 'http://bits.wikimedia.org/skins-1.5/common/images/poweredby_mediawiki_88x31.png', + 'asyncdownload' => 1, + 'filename' => 'UploadFromUrlTest.png', + 'token' => $token, + ), $data ); + + $this->assertEquals( $data[0]['upload']['result'], 'Queued', 'Queued upload' ); + + $job = JobQueueGroup::singleton()->pop(); + $this->assertThat( $job, $this->isInstanceOf( 'UploadFromUrlJob' ), 'Queued upload inserted' ); + } + + /** + * @depends testLogin + * @depends testClearQueue + */ + public function testAsyncUpload( $data ) { + $token = $this->user->getEditToken(); + + $this->user->addGroup( 'users' ); + + $data = $this->doAsyncUpload( $token, true ); + $this->assertEquals( $data[0]['upload']['result'], 'Success' ); + $this->assertEquals( $data[0]['upload']['filename'], 'UploadFromUrlTest.png' ); + $this->assertTrue( wfLocalFile( $data[0]['upload']['filename'] )->exists() ); + + $this->deleteFile( 'UploadFromUrlTest.png' ); + + return $data; + } + + /** + * @depends testLogin + * @depends testClearQueue + */ + public function testAsyncUploadWarning( $data ) { + $token = $this->user->getEditToken(); + + $this->user->addGroup( 'users' ); + + $data = $this->doAsyncUpload( $token ); + + $this->assertEquals( $data[0]['upload']['result'], 'Warning' ); + $this->assertTrue( isset( $data[0]['upload']['sessionkey'] ) ); + + $data = $this->doApiRequest( array( + 'action' => 'upload', + 'sessionkey' => $data[0]['upload']['sessionkey'], + 'filename' => 'UploadFromUrlTest.png', + 'ignorewarnings' => 1, + 'token' => $token, + ) ); + $this->assertEquals( $data[0]['upload']['result'], 'Success' ); + $this->assertEquals( $data[0]['upload']['filename'], 'UploadFromUrlTest.png' ); + $this->assertTrue( wfLocalFile( $data[0]['upload']['filename'] )->exists() ); + + $this->deleteFile( 'UploadFromUrlTest.png' ); + + return $data; + } + + /** + * @depends testLogin + * @depends testClearQueue + */ + public function testSyncDownload( $data ) { + $token = $this->user->getEditToken(); + + $job = JobQueueGroup::singleton()->pop(); + $this->assertFalse( $job, 'Starting with an empty jobqueue' ); + + $this->user->addGroup( 'users' ); + $data = $this->doApiRequest( array( + 'action' => 'upload', + 'filename' => 'UploadFromUrlTest.png', + 'url' => 'http://bits.wikimedia.org/skins-1.5/common/images/poweredby_mediawiki_88x31.png', + 'ignorewarnings' => true, + 'token' => $token, + ), $data ); + + $job = JobQueueGroup::singleton()->pop(); + $this->assertFalse( $job ); + + $this->assertEquals( 'Success', $data[0]['upload']['result'] ); + $this->deleteFile( 'UploadFromUrlTest.png' ); + + return $data; + } + + public function testLeaveMessage() { + $token = $this->user->user->getEditToken(); + + $talk = $this->user->user->getTalkPage(); + if ( $talk->exists() ) { + $page = WikiPage::factory( $talk ); + $page->doDeleteArticle( '' ); + } + + $this->assertFalse( (bool)$talk->getArticleID( Title::GAID_FOR_UPDATE ), 'User talk does not exist' ); + + $this->doApiRequest( array( + 'action' => 'upload', + 'filename' => 'UploadFromUrlTest.png', + 'url' => 'http://bits.wikimedia.org/skins-1.5/common/images/poweredby_mediawiki_88x31.png', + 'asyncdownload' => 1, + 'token' => $token, + 'leavemessage' => 1, + 'ignorewarnings' => 1, + ) ); + + $job = JobQueueGroup::singleton()->pop(); + $this->assertEquals( 'UploadFromUrlJob', get_class( $job ) ); + $job->run(); + + $this->assertTrue( wfLocalFile( 'UploadFromUrlTest.png' )->exists() ); + $this->assertTrue( (bool)$talk->getArticleID( Title::GAID_FOR_UPDATE ), 'User talk exists' ); + + $this->deleteFile( 'UploadFromUrlTest.png' ); + + $talkRev = Revision::newFromTitle( $talk ); + $talkSize = $talkRev->getSize(); + + $exception = false; + try { + $this->doApiRequest( array( + 'action' => 'upload', + 'filename' => 'UploadFromUrlTest.png', + 'url' => 'http://bits.wikimedia.org/skins-1.5/common/images/poweredby_mediawiki_88x31.png', + 'asyncdownload' => 1, + 'token' => $token, + 'leavemessage' => 1, + ) ); + } catch ( UsageException $e ) { + $exception = true; + $this->assertEquals( 'Using leavemessage without ignorewarnings is not supported', $e->getMessage() ); + } + $this->assertTrue( $exception ); + + $job = JobQueueGroup::singleton()->pop(); + $this->assertFalse( $job ); + + return; + /* + // Broken until using leavemessage with ignorewarnings is supported + $job->run(); + + $this->assertFalse( wfLocalFile( 'UploadFromUrlTest.png' )->exists() ); + + $talkRev = Revision::newFromTitle( $talk ); + $this->assertTrue( $talkRev->getSize() > $talkSize, 'New message left' ); + */ + } + + /** + * Helper function to perform an async upload, execute the job and fetch + * the status + * + * @return array The result of action=upload&statuskey=key + */ + private function doAsyncUpload( $token, $ignoreWarnings = false, $leaveMessage = false ) { + $params = array( + 'action' => 'upload', + 'filename' => 'UploadFromUrlTest.png', + 'url' => 'http://bits.wikimedia.org/skins-1.5/common/images/poweredby_mediawiki_88x31.png', + 'asyncdownload' => 1, + 'token' => $token, + ); + if ( $ignoreWarnings ) { + $params['ignorewarnings'] = 1; + } + if ( $leaveMessage ) { + $params['leavemessage'] = 1; + } + + $data = $this->doApiRequest( $params ); + $this->assertEquals( $data[0]['upload']['result'], 'Queued' ); + $this->assertTrue( isset( $data[0]['upload']['statuskey'] ) ); + $statusKey = $data[0]['upload']['statuskey']; + + $job = JobQueueGroup::singleton()->pop(); + $this->assertEquals( 'UploadFromUrlJob', get_class( $job ) ); + + $status = $job->run(); + $this->assertTrue( $status ); + + $data = $this->doApiRequest( array( + 'action' => 'upload', + 'statuskey' => $statusKey, + 'token' => $token, + ) ); + + return $data; + } + + /** + * + */ + protected function deleteFile( $name ) { + $t = Title::newFromText( $name, NS_FILE ); + $this->assertTrue( $t->exists(), "File '$name' exists" ); + + if ( $t->exists() ) { + $file = wfFindFile( $name, array( 'ignoreRedirect' => true ) ); + $empty = ""; + FileDeleteForm::doDelete( $t, $file, $empty, "none", true ); + $page = WikiPage::factory( $t ); + $page->doDeleteArticle( "testing" ); + } + $t = Title::newFromText( $name, NS_FILE ); + + $this->assertFalse( $t->exists(), "File '$name' was deleted" ); + } +} diff --git a/tests/phpunit/includes/upload/UploadStashTest.php b/tests/phpunit/includes/upload/UploadStashTest.php new file mode 100644 index 00000000..7a0fea48 --- /dev/null +++ b/tests/phpunit/includes/upload/UploadStashTest.php @@ -0,0 +1,76 @@ +<?php +/** + * @group Database + */ +class UploadStashTest extends MediaWikiTestCase { + /** + * @var Array of UploadStashTestUser + */ + public static $users; + + protected function setUp() { + parent::setUp(); + + // Setup a file for bug 29408 + $this->bug29408File = __DIR__ . '/bug29408'; + file_put_contents( $this->bug29408File, "\x00" ); + + self::$users = array( + 'sysop' => new TestUser( + 'Uploadstashtestsysop', + 'Upload Stash Test Sysop', + 'upload_stash_test_sysop@example.com', + array( 'sysop' ) + ), + 'uploader' => new TestUser( + 'Uploadstashtestuser', + 'Upload Stash Test User', + 'upload_stash_test_user@example.com', + array() + ) + ); + } + + protected function tearDown() { + if ( file_exists( $this->bug29408File . "." ) ) { + unlink( $this->bug29408File . "." ); + } + + if ( file_exists( $this->bug29408File ) ) { + unlink( $this->bug29408File ); + } + + parent::tearDown(); + } + + public function testBug29408() { + $this->setMwGlobals( 'wgUser', self::$users['uploader']->user ); + + $repo = RepoGroup::singleton()->getLocalRepo(); + $stash = new UploadStash( $repo ); + + // Throws exception caught by PHPUnit on failure + $file = $stash->stashFile( $this->bug29408File ); + // We'll never reach this point if we hit bug 29408 + $this->assertTrue( true, 'Unrecognized file without extension' ); + + $stash->removeFile( $file->getFileKey() ); + } + + public function testValidRequest() { + $request = new FauxRequest( array( 'wpFileKey' => 'foo' ) ); + $this->assertFalse( UploadFromStash::isValidRequest( $request ), 'Check failure on bad wpFileKey' ); + + $request = new FauxRequest( array( 'wpSessionKey' => 'foo' ) ); + $this->assertFalse( UploadFromStash::isValidRequest( $request ), 'Check failure on bad wpSessionKey' ); + + $request = new FauxRequest( array( 'wpFileKey' => 'testkey-test.test' ) ); + $this->assertTrue( UploadFromStash::isValidRequest( $request ), 'Check good wpFileKey' ); + + $request = new FauxRequest( array( 'wpFileKey' => 'testkey-test.test' ) ); + $this->assertTrue( UploadFromStash::isValidRequest( $request ), 'Check good wpSessionKey' ); + + $request = new FauxRequest( array( 'wpFileKey' => 'testkey-test.test', 'wpSessionKey' => 'foo' ) ); + $this->assertTrue( UploadFromStash::isValidRequest( $request ), 'Check key precedence' ); + } +} diff --git a/tests/phpunit/install-phpunit.sh b/tests/phpunit/install-phpunit.sh new file mode 100644 index 00000000..1f602935 --- /dev/null +++ b/tests/phpunit/install-phpunit.sh @@ -0,0 +1,37 @@ +#!/bin/sh + +has_binary () { + if [ -z `which $1` ]; then + return 1 + fi + return 0 +} + +if [ `id -u` -ne 0 ]; then + echo '*** ERROR: Must be root to run' + exit 1 +fi + +if ( has_binary phpunit ); then + echo PHPUnit already installed +else if ( has_binary pear ); then + echo Installing phpunit with pear + pear channel-discover pear.phpunit.de + pear channel-discover components.ez.no + pear channel-discover pear.symfony.com + pear update-channels + pear install --alldeps phpunit/PHPUnit +else if ( has_binary apt-get ); then + echo Installing phpunit with apt-get + apt-get install phpunit +else if ( has_binary yum ); then + echo Installing phpunit with yum + yum install phpunit +else if ( has_binary port ); then + echo Installing phpunit with macports + port install php5-unit +fi +fi +fi +fi +fi diff --git a/tests/phpunit/languages/LanguageAmTest.php b/tests/phpunit/languages/LanguageAmTest.php new file mode 100644 index 00000000..a644f5e0 --- /dev/null +++ b/tests/phpunit/languages/LanguageAmTest.php @@ -0,0 +1,35 @@ +<?php +/** + * @author Santhosh Thottingal + * @copyright Copyright © 2012, Santhosh Thottingal + * @file + */ + +/** Tests for MediaWiki languages/LanguageAm.php */ +class LanguageAmTest extends LanguageClassesTestCase { + /** + * @dataProvider providePlural + * @covers Language::convertPlural + */ + public function testPlural( $result, $value ) { + $forms = array( 'one', 'other' ); + $this->assertEquals( $result, $this->getLang()->convertPlural( $value, $forms ) ); + } + + /** + * @dataProvider providePlural + * @covers Language::getPluralRuleType + */ + public function testGetPluralRuleType( $result, $value ) { + $this->assertEquals( $result, $this->getLang()->getPluralRuleType( $value ) ); + } + + public static function providePlural() { + return array( + array( 'one', 0 ), + array( 'one', 1 ), + array( 'other', 2 ), + array( 'other', 200 ), + ); + } +} diff --git a/tests/phpunit/languages/LanguageArTest.php b/tests/phpunit/languages/LanguageArTest.php new file mode 100644 index 00000000..7b48f236 --- /dev/null +++ b/tests/phpunit/languages/LanguageArTest.php @@ -0,0 +1,87 @@ +<?php +/** + * Based on LanguagMlTest + * @file + */ + +/** Tests for MediaWiki languages/LanguageAr.php */ +class LanguageArTest extends LanguageClassesTestCase { + /** + * @covers Language::formatNum + * @todo split into a test and a dataprovider + */ + public function testFormatNum() { + $this->assertEquals( '١٬٢٣٤٬٥٦٧', $this->getLang()->formatNum( '1234567' ) ); + $this->assertEquals( '-١٢٫٨٩', $this->getLang()->formatNum( -12.89 ) ); + } + + /** + * Mostly to test the raw ascii feature. + * @dataProvider providerSprintfDate + * @covers Language::sprintfDate + */ + public function testSprintfDate( $format, $date, $expected ) { + $this->assertEquals( $expected, $this->getLang()->sprintfDate( $format, $date ) ); + } + + public static function providerSprintfDate() { + return array( + array( + 'xg "vs" g', + '20120102030410', + 'يناير vs ٣' + ), + array( + 'xmY', + '20120102030410', + '١٤٣٣' + ), + array( + 'xnxmY', + '20120102030410', + '1433' + ), + array( + 'xN xmj xmn xN xmY', + '20120102030410', + ' 7 2 ١٤٣٣' + ), + ); + } + + /** + * @dataProvider providePlural + * @covers Language::convertPlural + */ + public function testPlural( $result, $value ) { + $forms = array( 'zero', 'one', 'two', 'few', 'many', 'other' ); + $this->assertEquals( $result, $this->getLang()->convertPlural( $value, $forms ) ); + } + + /** + * @dataProvider providePlural + * @covers Language::getPluralRuleType + */ + public function testGetPluralRuleType( $result, $value ) { + $this->assertEquals( $result, $this->getLang()->getPluralRuleType( $value ) ); + } + + public static function providePlural() { + return array( + array( 'zero', 0 ), + array( 'one', 1 ), + array( 'two', 2 ), + array( 'few', 3 ), + array( 'few', 9 ), + array( 'few', 110 ), + array( 'many', 11 ), + array( 'many', 15 ), + array( 'many', 99 ), + array( 'many', 9999 ), + array( 'other', 100 ), + array( 'other', 102 ), + array( 'other', 1000 ), + array( 'other', 1.7 ), + ); + } +} diff --git a/tests/phpunit/languages/LanguageBeTest.php b/tests/phpunit/languages/LanguageBeTest.php new file mode 100644 index 00000000..7bd586af --- /dev/null +++ b/tests/phpunit/languages/LanguageBeTest.php @@ -0,0 +1,42 @@ +<?php +/** + * @author Santhosh Thottingal + * @copyright Copyright © 2012, Santhosh Thottingal + * @file + */ + +/** Tests for MediaWiki languages/LanguageBe.php */ +class LanguageBeTest extends LanguageClassesTestCase { + /** + * @dataProvider providePlural + * @covers Language::convertPlural + */ + public function testPlural( $result, $value ) { + $forms = array( 'one', 'few', 'many', 'other' ); + $this->assertEquals( $result, $this->getLang()->convertPlural( $value, $forms ) ); + } + + /** + * @dataProvider providePlural + * @covers Language::getPluralRuleType + */ + public function testGetPluralRuleType( $result, $value ) { + $this->assertEquals( $result, $this->getLang()->getPluralRuleType( $value ) ); + } + + public static function providePlural() { + return array( + array( 'one', 1 ), + array( 'many', 11 ), + array( 'one', 91 ), + array( 'one', 121 ), + array( 'few', 2 ), + array( 'few', 3 ), + array( 'few', 4 ), + array( 'few', 334 ), + array( 'many', 5 ), + array( 'many', 15 ), + array( 'many', 120 ), + ); + } +} diff --git a/tests/phpunit/languages/LanguageBe_taraskTest.php b/tests/phpunit/languages/LanguageBe_taraskTest.php new file mode 100644 index 00000000..dbdb5889 --- /dev/null +++ b/tests/phpunit/languages/LanguageBe_taraskTest.php @@ -0,0 +1,95 @@ +<?php + +class LanguageBe_taraskTest extends LanguageClassesTestCase { + /** + * Make sure the language code we are given is indeed + * be-tarask. This is to ensure LanguageClassesTestCase + * does not give us the wrong language. + */ + public function testBeTaraskTestsUsesBeTaraskCode() { + $this->assertEquals( 'be-tarask', + $this->getLang()->getCode() + ); + } + + /** + * @see bug 23156 & r64981 + * @covers Language::commafy + */ + public function testSearchRightSingleQuotationMarkAsApostroph() { + $this->assertEquals( + "'", + $this->getLang()->normalizeForSearch( '’' ), + 'bug 23156: U+2019 conversion to U+0027' + ); + } + + /** + * @see bug 23156 & r64981 + * @covers Language::commafy + */ + public function testCommafy() { + $this->assertEquals( '1,234,567', $this->getLang()->commafy( '1234567' ) ); + $this->assertEquals( '12,345', $this->getLang()->commafy( '12345' ) ); + } + + /** + * @see bug 23156 & r64981 + * @covers Language::commafy + */ + public function testDoesNotCommafyFourDigitsNumber() { + $this->assertEquals( '1234', $this->getLang()->commafy( '1234' ) ); + } + + /** + * @dataProvider providePlural + * @covers Language::convertPlural + */ + public function testPlural( $result, $value ) { + $forms = array( 'one', 'few', 'many', 'other' ); + $this->assertEquals( $result, $this->getLang()->convertPlural( $value, $forms ) ); + } + + /** + * @dataProvider providePlural + * @covers Language::getPluralRuleType + */ + public function testGetPluralRuleType( $result, $value ) { + $this->assertEquals( $result, $this->getLang()->getPluralRuleType( $value ) ); + } + + public static function providePlural() { + return array( + array( 'one', 1 ), + array( 'many', 11 ), + array( 'one', 91 ), + array( 'one', 121 ), + array( 'few', 2 ), + array( 'few', 3 ), + array( 'few', 4 ), + array( 'few', 334 ), + array( 'many', 5 ), + array( 'many', 15 ), + array( 'many', 120 ), + ); + } + + /** + * @dataProvider providePluralTwoForms + * @covers Language::convertPlural + */ + public function testPluralTwoForms( $result, $value ) { + $forms = array( '1=one', 'other' ); + $this->assertEquals( $result, $this->getLang()->convertPlural( $value, $forms ) ); + } + + public static function providePluralTwoForms() { + return array( + array( 'other', 0 ), + array( 'one', 1 ), + array( 'other', 11 ), + array( 'other', 91 ), + array( 'other', 121 ), + ); + } +} diff --git a/tests/phpunit/languages/LanguageBhoTest.php b/tests/phpunit/languages/LanguageBhoTest.php new file mode 100644 index 00000000..187bfbbc --- /dev/null +++ b/tests/phpunit/languages/LanguageBhoTest.php @@ -0,0 +1,35 @@ +<?php +/** + * @author Santhosh Thottingal + * @copyright Copyright © 2012, Santhosh Thottingal + * @file + */ + +/** Tests for MediaWiki languages/LanguageBho.php */ +class LanguageBhoTest extends LanguageClassesTestCase { + /** + * @dataProvider providePlural + * @covers Language::convertPlural + */ + public function testPlural( $result, $value ) { + $forms = array( 'one', 'other' ); + $this->assertEquals( $result, $this->getLang()->convertPlural( $value, $forms ) ); + } + + /** + * @dataProvider providePlural + * @covers Language::getPluralRuleType + */ + public function testGetPluralRuleType( $result, $value ) { + $this->assertEquals( $result, $this->getLang()->getPluralRuleType( $value ) ); + } + + public static function providePlural() { + return array( + array( 'one', 0 ), + array( 'one', 1 ), + array( 'other', 2 ), + array( 'other', 200 ), + ); + } +} diff --git a/tests/phpunit/languages/LanguageBsTest.php b/tests/phpunit/languages/LanguageBsTest.php new file mode 100644 index 00000000..7aca2ab1 --- /dev/null +++ b/tests/phpunit/languages/LanguageBsTest.php @@ -0,0 +1,42 @@ +<?php +/** + * @author Santhosh Thottingal + * @copyright Copyright © 2012, Santhosh Thottingal + * @file + */ + +/** Tests for Croatian (hrvatski) */ +class LanguageBsTest extends LanguageClassesTestCase { + /** + * @dataProvider providePlural + * @covers Language::convertPlural + */ + public function testPlural( $result, $value ) { + $forms = array( 'one', 'few', 'other' ); + $this->assertEquals( $result, $this->getLang()->convertPlural( $value, $forms ) ); + } + + /** + * @dataProvider providePlural + * @covers Language::getPluralRuleType + */ + public function testGetPluralRuleType( $result, $value ) { + $this->assertEquals( $result, $this->getLang()->getPluralRuleType( $value ) ); + } + + public static function providePlural() { + return array( + array( 'other', 0 ), + array( 'one', 1 ), + array( 'few', 2 ), + array( 'few', 4 ), + array( 'other', 5 ), + array( 'other', 11 ), + array( 'other', 20 ), + array( 'one', 21 ), + array( 'few', 24 ), + array( 'other', 25 ), + array( 'other', 200 ), + ); + } +} diff --git a/tests/phpunit/languages/LanguageClassesTestCase.php b/tests/phpunit/languages/LanguageClassesTestCase.php new file mode 100644 index 00000000..632e037f --- /dev/null +++ b/tests/phpunit/languages/LanguageClassesTestCase.php @@ -0,0 +1,74 @@ +<?php +/** + * Helping class to run tests using a clean language instance. + * + * This is intended for the MediaWiki language class tests under + * tests/phpunit/languages. + * + * Before each tests, a new language object is build which you + * can retrieve in your test using the $this->getLang() method: + * + * @par Using the crafted language object: + * @code + * function testHasLanguageObject() { + * $langObject = $this->getLang(); + * $this->assertInstanceOf( 'LanguageFoo', + * $langObject + * ); + * } + * @endcode + */ +abstract class LanguageClassesTestCase extends MediaWikiTestCase { + /** + * Internal language object + * + * A new object is created before each tests thanks to PHPUnit + * setUp() method, it is deleted after each test too. To get + * this object you simply use the getLang method. + * + * You must have setup a language code first. See $LanguageClassCode + * @code + * function testWeAreTheChampions() { + * $this->getLang(); # language object + * } + * @endcode + */ + private $languageObject; + + /** + * @return Language + */ + protected function getLang() { + return $this->languageObject; + } + + /** + * Create a new language object before each test. + */ + protected function setUp() { + parent::setUp(); + $found = preg_match( '/Language(.+)Test/', get_called_class(), $m ); + if ( $found ) { + # Normalize language code since classes uses underscores + $m[1] = str_replace( '_', '-', $m[1] ); + } else { + # Fallback to english language + $m[1] = 'en'; + wfDebug( + __METHOD__ . " could not extract a language name " + . "out of " . get_called_class() . " failling back to 'en'\n" + ); + } + // @todo validate $m[1] which should be a valid language code + $this->languageObject = Language::factory( $m[1] ); + } + + /** + * Delete the internal language object so each test start + * out with a fresh language instance. + */ + protected function tearDown() { + unset( $this->languageObject ); + parent::tearDown(); + } +} diff --git a/tests/phpunit/languages/LanguageCsTest.php b/tests/phpunit/languages/LanguageCsTest.php new file mode 100644 index 00000000..da9e6b88 --- /dev/null +++ b/tests/phpunit/languages/LanguageCsTest.php @@ -0,0 +1,41 @@ +<?php +/** + * @author Santhosh Thottingal + * @copyright Copyright © 2012, Santhosh Thottingal + * @file + */ + +/** Tests for MediaWiki languages/classes/Languagecs.php */ +class LanguageCsTest extends LanguageClassesTestCase { + /** + * @dataProvider providePlural + * @covers Language::convertPlural + */ + public function testPlural( $result, $value ) { + $forms = array( 'one', 'few', 'other' ); + $this->assertEquals( $result, $this->getLang()->convertPlural( $value, $forms ) ); + } + + /** + * @dataProvider providePlural + * @covers Language::getPluralRuleType + */ + public function testGetPluralRuleType( $result, $value ) { + $this->assertEquals( $result, $this->getLang()->getPluralRuleType( $value ) ); + } + + public static function providePlural() { + return array( + array( 'other', 0 ), + array( 'one', 1 ), + array( 'few', 2 ), + array( 'few', 3 ), + array( 'few', 4 ), + array( 'other', 5 ), + array( 'other', 11 ), + array( 'other', 20 ), + array( 'other', 25 ), + array( 'other', 200 ), + ); + } +} diff --git a/tests/phpunit/languages/LanguageCuTest.php b/tests/phpunit/languages/LanguageCuTest.php new file mode 100644 index 00000000..07193172 --- /dev/null +++ b/tests/phpunit/languages/LanguageCuTest.php @@ -0,0 +1,42 @@ +<?php +/** + * @author Santhosh Thottingal + * @copyright Copyright © 2012, Santhosh Thottingal + * @file + */ + +/** Tests for MediaWiki languages/LanguageCu.php */ +class LanguageCuTest extends LanguageClassesTestCase { + /** + * @dataProvider providePlural + * @covers Language::convertPlural + */ + public function testPlural( $result, $value ) { + $forms = array( 'one', 'two', 'few', 'other' ); + $this->assertEquals( $result, $this->getLang()->convertPlural( $value, $forms ) ); + } + + /** + * @dataProvider providePlural + * @covers Language::getPluralRuleType + */ + public function testGetPluralRuleType( $result, $value ) { + $this->assertEquals( $result, $this->getLang()->getPluralRuleType( $value ) ); + } + + public static function providePlural() { + return array( + array( 'other', 0 ), + array( 'one', 1 ), + array( 'two', 2 ), + array( 'few', 3 ), + array( 'few', 4 ), + array( 'other', 5 ), + array( 'one', 11 ), + array( 'other', 20 ), + array( 'two', 22 ), + array( 'few', 223 ), + array( 'other', 200 ), + ); + } +} diff --git a/tests/phpunit/languages/LanguageCyTest.php b/tests/phpunit/languages/LanguageCyTest.php new file mode 100644 index 00000000..eaf663a8 --- /dev/null +++ b/tests/phpunit/languages/LanguageCyTest.php @@ -0,0 +1,43 @@ +<?php +/** + * @author Santhosh Thottingal + * @copyright Copyright © 2012, Santhosh Thottingal + * @file + */ + +/** Tests for MediaWiki languages/classes/LanguageCy.php */ +class LanguageCyTest extends LanguageClassesTestCase { + /** + * @dataProvider providePlural + * @covers Language::convertPlural + */ + public function testPlural( $result, $value ) { + $forms = array( 'zero', 'one', 'two', 'few', 'many', 'other' ); + $this->assertEquals( $result, $this->getLang()->convertPlural( $value, $forms ) ); + } + + /** + * @dataProvider providePlural + * @covers Language::getPluralRuleType + */ + public function testGetPluralRuleType( $result, $value ) { + $this->assertEquals( $result, $this->getLang()->getPluralRuleType( $value ) ); + } + + public static function providePlural() { + return array( + array( 'zero', 0 ), + array( 'one', 1 ), + array( 'two', 2 ), + array( 'few', 3 ), + array( 'many', 6 ), + array( 'other', 4 ), + array( 'other', 5 ), + array( 'other', 11 ), + array( 'other', 20 ), + array( 'other', 22 ), + array( 'other', 223 ), + array( 'other', 200.00 ), + ); + } +} diff --git a/tests/phpunit/languages/LanguageDsbTest.php b/tests/phpunit/languages/LanguageDsbTest.php new file mode 100644 index 00000000..94c11bcc --- /dev/null +++ b/tests/phpunit/languages/LanguageDsbTest.php @@ -0,0 +1,41 @@ +<?php +/** + * @author Santhosh Thottingal + * @copyright Copyright © 2012, Santhosh Thottingal + * @file + */ + +/** Tests for MediaWiki languages/classes/LanguageDsb.php */ +class LanguageDsbTest extends LanguageClassesTestCase { + /** + * @dataProvider providePlural + * @covers Language::convertPlural + */ + public function testPlural( $result, $value ) { + $forms = array( 'one', 'two', 'few', 'other' ); + $this->assertEquals( $result, $this->getLang()->convertPlural( $value, $forms ) ); + } + + /** + * @dataProvider providePlural + * @covers Language::getPluralRuleType + */ + public function testGetPluralRuleType( $result, $value ) { + $this->assertEquals( $result, $this->getLang()->getPluralRuleType( $value ) ); + } + + public static function providePlural() { + return array( + array( 'other', 0 ), + array( 'one', 1 ), + array( 'one', 101 ), + array( 'one', 90001 ), + array( 'two', 2 ), + array( 'few', 3 ), + array( 'few', 203 ), + array( 'few', 4 ), + array( 'other', 99 ), + array( 'other', 555 ), + ); + } +} diff --git a/tests/phpunit/languages/LanguageFrTest.php b/tests/phpunit/languages/LanguageFrTest.php new file mode 100644 index 00000000..46b65011 --- /dev/null +++ b/tests/phpunit/languages/LanguageFrTest.php @@ -0,0 +1,35 @@ +<?php +/** + * @author Santhosh Thottingal + * @copyright Copyright © 2012, Santhosh Thottingal + * @file + */ + +/** Tests for MediaWiki languages/classes/LanguageFr.php */ +class LanguageFrTest extends LanguageClassesTestCase { + /** + * @dataProvider providePlural + * @covers Language::convertPlural + */ + public function testPlural( $result, $value ) { + $forms = array( 'one', 'other' ); + $this->assertEquals( $result, $this->getLang()->convertPlural( $value, $forms ) ); + } + + /** + * @dataProvider providePlural + * @covers Language::getPluralRuleType + */ + public function testGetPluralRuleType( $result, $value ) { + $this->assertEquals( $result, $this->getLang()->getPluralRuleType( $value ) ); + } + + public static function providePlural() { + return array( + array( 'one', 0 ), + array( 'one', 1 ), + array( 'other', 2 ), + array( 'other', 200 ), + ); + } +} diff --git a/tests/phpunit/languages/LanguageGaTest.php b/tests/phpunit/languages/LanguageGaTest.php new file mode 100644 index 00000000..c009f56b --- /dev/null +++ b/tests/phpunit/languages/LanguageGaTest.php @@ -0,0 +1,35 @@ +<?php +/** + * @author Santhosh Thottingal + * @copyright Copyright © 2012, Santhosh Thottingal + * @file + */ + +/** Tests for MediaWiki languages/classes/LanguageGa.php */ +class LanguageGaTest extends LanguageClassesTestCase { + /** + * @dataProvider providePlural + * @covers Language::convertPlural + */ + public function testPlural( $result, $value ) { + $forms = array( 'one', 'two', 'other' ); + $this->assertEquals( $result, $this->getLang()->convertPlural( $value, $forms ) ); + } + + /** + * @dataProvider providePlural + * @covers Language::getPluralRuleType + */ + public function testGetPluralRuleType( $result, $value ) { + $this->assertEquals( $result, $this->getLang()->getPluralRuleType( $value ) ); + } + + public static function providePlural() { + return array( + array( 'other', 0 ), + array( 'one', 1 ), + array( 'two', 2 ), + array( 'other', 200 ), + ); + } +} diff --git a/tests/phpunit/languages/LanguageGdTest.php b/tests/phpunit/languages/LanguageGdTest.php new file mode 100644 index 00000000..0b2612b2 --- /dev/null +++ b/tests/phpunit/languages/LanguageGdTest.php @@ -0,0 +1,53 @@ +<?php +/** + * @author Santhosh Thottingal + * @copyright Copyright © 2012-2013, Santhosh Thottingal + * @file + */ + +/** Tests for MediaWiki languages/classes/LanguageGd.php */ +class LanguageGdTest extends LanguageClassesTestCase { + /** + * @dataProvider providerPlural + * @covers Language::convertPlural + */ + public function testPlural( $result, $value ) { + $forms = array( 'one', 'two', 'few', 'other' ); + $this->assertEquals( $result, $this->getLang()->convertPlural( $value, $forms ) ); + } + + public static function providerPlural() { + return array( + array( 'other', 0 ), + array( 'one', 1 ), + array( 'two', 2 ), + array( 'one', 11 ), + array( 'two', 12 ), + array( 'few', 3 ), + array( 'few', 19 ), + array( 'other', 200 ), + ); + } + + /** + * @dataProvider providerPluralExplicit + * @covers Language::convertPlural + */ + public function testExplicitPlural( $result, $value ) { + $forms = array( 'one', 'two', 'few', 'other', '11=Form11', '12=Form12' ); + $this->assertEquals( $result, $this->getLang()->convertPlural( $value, $forms ) ); + } + + public static function providerPluralExplicit() { + return array( + array( 'other', 0 ), + array( 'one', 1 ), + array( 'two', 2 ), + array( 'Form11', 11 ), + array( 'Form12', 12 ), + array( 'few', 3 ), + array( 'few', 19 ), + array( 'other', 200 ), + ); + } +} diff --git a/tests/phpunit/languages/LanguageGvTest.php b/tests/phpunit/languages/LanguageGvTest.php new file mode 100644 index 00000000..fc58022a --- /dev/null +++ b/tests/phpunit/languages/LanguageGvTest.php @@ -0,0 +1,44 @@ +<?php +/** + * Test for Manx (Gaelg) language + * + * @author Santhosh Thottingal + * @copyright Copyright © 2013, Santhosh Thottingal + * @file + */ + +class LanguageGvTest extends LanguageClassesTestCase { + /** + * @dataProvider providePlural + * @covers Language::convertPlural + */ + public function testPlural( $result, $value ) { + $forms = array( 'one', 'two', 'few', 'other' ); + $this->assertEquals( $result, $this->getLang()->convertPlural( $value, $forms ) ); + } + + /** + * @dataProvider providePlural + * @covers Language::getPluralRuleType + */ + public function testGetPluralRuleType( $result, $value ) { + $this->assertEquals( $result, $this->getLang()->getPluralRuleType( $value ) ); + } + + public static function providePlural() { + return array( + array( 'few', 0 ), + array( 'one', 1 ), + array( 'two', 2 ), + array( 'other', 3 ), + array( 'few', 20 ), + array( 'one', 21 ), + array( 'two', 22 ), + array( 'other', 23 ), + array( 'other', 50 ), + array( 'few', 60 ), + array( 'other', 80 ), + array( 'few', 100 ) + ); + } +} diff --git a/tests/phpunit/languages/LanguageHeTest.php b/tests/phpunit/languages/LanguageHeTest.php new file mode 100644 index 00000000..8edc6ddf --- /dev/null +++ b/tests/phpunit/languages/LanguageHeTest.php @@ -0,0 +1,132 @@ +<?php +/** + * @author Amir E. Aharoni + * @copyright Copyright © 2012, Amir E. Aharoni + * @file + */ + +/** Tests for MediaWiki languages/classes/LanguageHe.php */ +class LanguageHeTest extends LanguageClassesTestCase { + /** + * The most common usage for the plural forms is two forms, + * for singular and plural. In this case, the second form + * is technically dual, but in practice it's used as plural. + * In some cases, usually with expressions of time, three forms + * are needed - singular, dual and plural. + * CLDR also specifies a fourth form for multiples of 10, + * which is very rare. It also has a mistake, because + * the number 10 itself is supposed to be just plural, + * so currently it's overridden in MediaWiki. + */ + + // @todo the below test*PluralForms test methods can be refactored + // to use a single test method and data provider.. + + /** + * @dataProvider provideTwoPluralForms + * @covers Language::convertPlural + */ + public function testTwoPluralForms( $result, $value ) { + $forms = array( 'one', 'other' ); + $this->assertEquals( $result, $this->getLang()->convertPlural( $value, $forms ) ); + } + + /** + * @dataProvider provideThreePluralForms + * @covers Language::convertPlural + */ + public function testThreePluralForms( $result, $value ) { + $forms = array( 'one', 'two', 'other' ); + $this->assertEquals( $result, $this->getLang()->convertPlural( $value, $forms ) ); + } + + /** + * @dataProvider provideFourPluralForms + * @covers Language::convertPlural + */ + public function testFourPluralForms( $result, $value ) { + $forms = array( 'one', 'two', 'many', 'other' ); + $this->assertEquals( $result, $this->getLang()->convertPlural( $value, $forms ) ); + } + + /** + * @dataProvider provideFourPluralForms + * @covers Language::convertPlural + */ + public function testGetPluralRuleType( $result, $value ) { + $this->assertEquals( $result, $this->getLang()->getPluralRuleType( $value ) ); + } + + public static function provideTwoPluralForms() { + return array( + array( 'other', 0 ), // Zero - plural + array( 'one', 1 ), // Singular + array( 'other', 2 ), // No third form provided, use it as plural + array( 'other', 3 ), // Plural - other + array( 'other', 10 ), // No fourth form provided, use it as plural + array( 'other', 20 ), // No fourth form provided, use it as plural + ); + } + + public static function provideThreePluralForms() { + return array( + array( 'other', 0 ), // Zero - plural + array( 'one', 1 ), // Singular + array( 'two', 2 ), // Dual + array( 'other', 3 ), // Plural - other + array( 'other', 10 ), // No fourth form provided, use it as plural + array( 'other', 20 ), // No fourth form provided, use it as plural + ); + } + + public static function provideFourPluralForms() { + return array( + array( 'other', 0 ), // Zero - plural + array( 'one', 1 ), // Singular + array( 'two', 2 ), // Dual + array( 'other', 3 ), // Plural - other + array( 'other', 10 ), // 10 is supposed to be plural (other), not "many" + array( 'many', 20 ), // Fourth form provided - rare, but supported by CLDR + ); + } + + /** + * @dataProvider provideGrammar + * @covers Language::convertGrammar + */ + public function testGrammar( $result, $word, $case ) { + $this->assertEquals( $result, $this->getLang()->convertGrammar( $word, $case ) ); + } + + // The comments in the beginning of the line help avoid RTL problems + // with text editors. + public static function provideGrammar() { + return array( + array( + /* result */'וויקיפדיה', + /* word */'ויקיפדיה', + /* case */'תחילית', + ), + array( + /* result */'וולפגנג', + /* word */'וולפגנג', + /* case */'prefixed', + ), + array( + /* result */'קובץ', + /* word */'הקובץ', + /* case */'תחילית', + ), + array( + /* result */'־Wikipedia', + /* word */'Wikipedia', + /* case */'תחילית', + ), + array( + /* result */'־1995', + /* word */'1995', + /* case */'תחילית', + ), + ); + } +} diff --git a/tests/phpunit/languages/LanguageHiTest.php b/tests/phpunit/languages/LanguageHiTest.php new file mode 100644 index 00000000..f6d2c9e9 --- /dev/null +++ b/tests/phpunit/languages/LanguageHiTest.php @@ -0,0 +1,35 @@ +<?php +/** + * @author Santhosh Thottingal + * @copyright Copyright © 2012, Santhosh Thottingal + * @file + */ + +/** Tests for MediaWiki languages/LanguageHi.php */ +class LanguageHiTest extends LanguageClassesTestCase { + /** + * @dataProvider providePlural + * @covers Language::convertPlural + */ + public function testPlural( $result, $value ) { + $forms = array( 'one', 'other' ); + $this->assertEquals( $result, $this->getLang()->convertPlural( $value, $forms ) ); + } + + /** + * @dataProvider providePlural + * @covers Language::getPluralRuleType + */ + public function testGetPluralRuleType( $result, $value ) { + $this->assertEquals( $result, $this->getLang()->getPluralRuleType( $value ) ); + } + + public static function providePlural() { + return array( + array( 'one', 0 ), + array( 'one', 1 ), + array( 'other', 2 ), + array( 'other', 200 ), + ); + } +} diff --git a/tests/phpunit/languages/LanguageHrTest.php b/tests/phpunit/languages/LanguageHrTest.php new file mode 100644 index 00000000..644c5255 --- /dev/null +++ b/tests/phpunit/languages/LanguageHrTest.php @@ -0,0 +1,42 @@ +<?php +/** + * @author Santhosh Thottingal + * @copyright Copyright © 2012, Santhosh Thottingal + * @file + */ + +/** Tests for MediaWiki languages/classes/LanguageHr.php */ +class LanguageHrTest extends LanguageClassesTestCase { + /** + * @dataProvider providePlural + * @covers Language::convertPlural + */ + public function testPlural( $result, $value ) { + $forms = array( 'one', 'few', 'other' ); + $this->assertEquals( $result, $this->getLang()->convertPlural( $value, $forms ) ); + } + + /** + * @dataProvider providePlural + * @covers Language::getPluralRuleType + */ + public function testGetPluralRuleType( $result, $value ) { + $this->assertEquals( $result, $this->getLang()->getPluralRuleType( $value ) ); + } + + public static function providePlural() { + return array( + array( 'other', 0 ), + array( 'one', 1 ), + array( 'few', 2 ), + array( 'few', 4 ), + array( 'other', 5 ), + array( 'other', 11 ), + array( 'other', 20 ), + array( 'one', 21 ), + array( 'few', 24 ), + array( 'other', 25 ), + array( 'other', 200 ), + ); + } +} diff --git a/tests/phpunit/languages/LanguageHsbTest.php b/tests/phpunit/languages/LanguageHsbTest.php new file mode 100644 index 00000000..f95a43bf --- /dev/null +++ b/tests/phpunit/languages/LanguageHsbTest.php @@ -0,0 +1,41 @@ +<?php +/** + * @author Santhosh Thottingal + * @copyright Copyright © 2012, Santhosh Thottingal + * @file + */ + +/** Tests for MediaWiki languages/classes/LanguageHsb.php */ +class LanguageHsbTest extends LanguageClassesTestCase { + /** + * @dataProvider providePlural + * @covers Language::convertPlural + */ + public function testPlural( $result, $value ) { + $forms = array( 'one', 'two', 'few', 'other' ); + $this->assertEquals( $result, $this->getLang()->convertPlural( $value, $forms ) ); + } + + /** + * @dataProvider providePlural + * @covers Language::getPluralRuleType + */ + public function testGetPluralRuleType( $result, $value ) { + $this->assertEquals( $result, $this->getLang()->getPluralRuleType( $value ) ); + } + + public static function providePlural() { + return array( + array( 'other', 0 ), + array( 'one', 1 ), + array( 'one', 101 ), + array( 'one', 90001 ), + array( 'two', 2 ), + array( 'few', 3 ), + array( 'few', 203 ), + array( 'few', 4 ), + array( 'other', 99 ), + array( 'other', 555 ), + ); + } +} diff --git a/tests/phpunit/languages/LanguageHuTest.php b/tests/phpunit/languages/LanguageHuTest.php new file mode 100644 index 00000000..ee9197d7 --- /dev/null +++ b/tests/phpunit/languages/LanguageHuTest.php @@ -0,0 +1,35 @@ +<?php +/** + * @author Santhosh Thottingal + * @copyright Copyright © 2012, Santhosh Thottingal + * @file + */ + +/** Tests for MediaWiki languages/LanguageHu.php */ +class LanguageHuTest extends LanguageClassesTestCase { + /** + * @dataProvider providePlural + * @covers Language::convertPlural + */ + public function testPlural( $result, $value ) { + $forms = array( 'one', 'other' ); + $this->assertEquals( $result, $this->getLang()->convertPlural( $value, $forms ) ); + } + + /** + * @dataProvider providePlural + * @covers Language::getPluralRuleType + */ + public function testGetPluralRuleType( $result, $value ) { + $this->assertEquals( $result, $this->getLang()->getPluralRuleType( $value ) ); + } + + public static function providePlural() { + return array( + array( 'other', 0 ), + array( 'one', 1 ), + array( 'other', 2 ), + array( 'other', 200 ), + ); + } +} diff --git a/tests/phpunit/languages/LanguageHyTest.php b/tests/phpunit/languages/LanguageHyTest.php new file mode 100644 index 00000000..92e0ef94 --- /dev/null +++ b/tests/phpunit/languages/LanguageHyTest.php @@ -0,0 +1,35 @@ +<?php +/** + * @author Santhosh Thottingal + * @copyright Copyright © 2012, Santhosh Thottingal + * @file + */ + +/** Tests for Armenian (Հայերեն) */ +class LanguageHyTest extends LanguageClassesTestCase { + /** + * @dataProvider providePlural + * @covers Language::convertPlural + */ + public function testPlural( $result, $value ) { + $forms = array( 'one', 'other' ); + $this->assertEquals( $result, $this->getLang()->convertPlural( $value, $forms ) ); + } + + /** + * @dataProvider providePlural + * @covers Language::getPluralRuleType + */ + public function testGetPluralRuleType( $result, $value ) { + $this->assertEquals( $result, $this->getLang()->getPluralRuleType( $value ) ); + } + + public static function providePlural() { + return array( + array( 'one', 0 ), + array( 'one', 1 ), + array( 'other', 2 ), + array( 'other', 200 ), + ); + } +} diff --git a/tests/phpunit/languages/LanguageKshTest.php b/tests/phpunit/languages/LanguageKshTest.php new file mode 100644 index 00000000..568a3780 --- /dev/null +++ b/tests/phpunit/languages/LanguageKshTest.php @@ -0,0 +1,35 @@ +<?php +/** + * @author Santhosh Thottingal + * @copyright Copyright © 2012, Santhosh Thottingal + * @file + */ + +/** Tests for MediaWiki languages/classes/LanguageKsh.php */ +class LanguageKshTest extends LanguageClassesTestCase { + /** + * @dataProvider providePlural + * @covers Language::convertPlural + */ + public function testPlural( $result, $value ) { + $forms = array( 'one', 'other', 'zero' ); + $this->assertEquals( $result, $this->getLang()->convertPlural( $value, $forms ) ); + } + + /** + * @dataProvider providePlural + * @covers Language::getPluralRuleType + */ + public function testGetPluralRuleType( $result, $value ) { + $this->assertEquals( $result, $this->getLang()->getPluralRuleType( $value ) ); + } + + public static function providePlural() { + return array( + array( 'zero', 0 ), + array( 'one', 1 ), + array( 'other', 2 ), + array( 'other', 200 ), + ); + } +} diff --git a/tests/phpunit/languages/LanguageLnTest.php b/tests/phpunit/languages/LanguageLnTest.php new file mode 100644 index 00000000..10b3234f --- /dev/null +++ b/tests/phpunit/languages/LanguageLnTest.php @@ -0,0 +1,35 @@ +<?php +/** + * @author Santhosh Thottingal + * @copyright Copyright © 2012, Santhosh Thottingal + * @file + */ + +/** Tests for MediaWiki languages/classes/LanguageLn.php */ +class LanguageLnTest extends LanguageClassesTestCase { + /** + * @dataProvider providePlural + * @covers Language::convertPlural + */ + public function testPlural( $result, $value ) { + $forms = array( 'one', 'other' ); + $this->assertEquals( $result, $this->getLang()->convertPlural( $value, $forms ) ); + } + + /** + * @dataProvider providePlural + * @covers Language::getPluralRuleType + */ + public function testGetPluralRuleType( $result, $value ) { + $this->assertEquals( $result, $this->getLang()->getPluralRuleType( $value ) ); + } + + public static function providePlural() { + return array( + array( 'one', 0 ), + array( 'one', 1 ), + array( 'other', 2 ), + array( 'other', 200 ), + ); + } +} diff --git a/tests/phpunit/languages/LanguageLtTest.php b/tests/phpunit/languages/LanguageLtTest.php new file mode 100644 index 00000000..30642f62 --- /dev/null +++ b/tests/phpunit/languages/LanguageLtTest.php @@ -0,0 +1,63 @@ +<?php +/** + * @author Santhosh Thottingal + * @copyright Copyright © 2012, Santhosh Thottingal + * @file + */ + +/** Tests for MediaWiki languages/LanguageLt.php */ +class LanguageLtTest extends LanguageClassesTestCase { + /** + * @dataProvider providePlural + * @covers Language::convertPlural + */ + public function testPlural( $result, $value ) { + $forms = array( 'one', 'few', 'other' ); + $this->assertEquals( $result, $this->getLang()->convertPlural( $value, $forms ) ); + } + + /** + * @dataProvider providePlural + * @covers Language::getPluralRuleType + */ + public function testGetPluralRuleType( $result, $value ) { + $this->assertEquals( $result, $this->getLang()->getPluralRuleType( $value ) ); + } + + public static function providePlural() { + return array( + array( 'other', 0 ), + array( 'one', 1 ), + array( 'few', 2 ), + array( 'few', 9 ), + array( 'other', 10 ), + array( 'other', 11 ), + array( 'other', 20 ), + array( 'one', 21 ), + array( 'few', 32 ), + array( 'one', 41 ), + array( 'one', 40001 ), + ); + } + + /** + * @dataProvider providePluralTwoForms + * @covers Language::convertPlural + */ + public function testOneFewPlural( $result, $value ) { + $forms = array( 'one', 'other' ); + // This fails for 21, but not sure why. + $this->assertEquals( $result, $this->getLang()->convertPlural( $value, $forms ) ); + } + + public static function providePluralTwoForms() { + return array( + array( 'one', 1 ), + array( 'other', 2 ), + array( 'other', 15 ), + array( 'other', 20 ), + array( 'one', 21 ), + array( 'other', 22 ), + ); + } +} diff --git a/tests/phpunit/languages/LanguageLvTest.php b/tests/phpunit/languages/LanguageLvTest.php new file mode 100644 index 00000000..7120cfe3 --- /dev/null +++ b/tests/phpunit/languages/LanguageLvTest.php @@ -0,0 +1,44 @@ +<?php +/** + * @author Santhosh Thottingal + * @copyright Copyright © 2012, Santhosh Thottingal + * @file + */ + +/** Tests for Latvian */ +class LanguageLvTest extends LanguageClassesTestCase { + /** + * @dataProvider providePlural + * @covers Language::convertPlural + */ + public function testPlural( $result, $value ) { + $forms = array( 'zero', 'one', 'other' ); + $this->assertEquals( $result, $this->getLang()->convertPlural( $value, $forms ) ); + } + + /** + * @dataProvider providePlural + * @covers Language::getPluralRuleType + */ + public function testGetPluralRuleType( $result, $value ) { + $this->assertEquals( $result, $this->getLang()->getPluralRuleType( $value ) ); + } + + public static function providePlural() { + return array( + array( 'zero', 0 ), + array( 'one', 1 ), + array( 'zero', 11 ), + array( 'one', 21 ), + array( 'zero', 411 ), + array( 'other', 2 ), + array( 'other', 9 ), + array( 'zero', 12 ), + array( 'other', 12.345 ), + array( 'zero', 20 ), + array( 'other', 22 ), + array( 'one', 31 ), + array( 'zero', 200 ), + ); + } +} diff --git a/tests/phpunit/languages/LanguageMgTest.php b/tests/phpunit/languages/LanguageMgTest.php new file mode 100644 index 00000000..65e8fd7b --- /dev/null +++ b/tests/phpunit/languages/LanguageMgTest.php @@ -0,0 +1,36 @@ +<?php +/** + * @author Santhosh Thottingal + * @copyright Copyright © 2012, Santhosh Thottingal + * @file + */ + +/** Tests for MediaWiki languages/classes/LanguageMg.php */ +class LanguageMgTest extends LanguageClassesTestCase { + /** + * @dataProvider providePlural + * @covers Language::convertPlural + */ + public function testPlural( $result, $value ) { + $forms = array( 'one', 'other' ); + $this->assertEquals( $result, $this->getLang()->convertPlural( $value, $forms ) ); + } + + /** + * @dataProvider providePlural + * @covers Language::getPluralRuleType + */ + public function testGetPluralRuleType( $result, $value ) { + $this->assertEquals( $result, $this->getLang()->getPluralRuleType( $value ) ); + } + + public static function providePlural() { + return array( + array( 'one', 0 ), + array( 'one', 1 ), + array( 'other', 2 ), + array( 'other', 200 ), + array( 'other', 123.3434 ), + ); + } +} diff --git a/tests/phpunit/languages/LanguageMkTest.php b/tests/phpunit/languages/LanguageMkTest.php new file mode 100644 index 00000000..ed155263 --- /dev/null +++ b/tests/phpunit/languages/LanguageMkTest.php @@ -0,0 +1,40 @@ +<?php +/** + * @author Santhosh Thottingal + * @copyright Copyright © 2012, Santhosh Thottingal + * @file + */ + +/** Tests for македонски/Macedonian */ +class LanguageMkTest extends LanguageClassesTestCase { + /** + * @dataProvider providePlural + * @covers Language::convertPlural + */ + public function testPlural( $result, $value ) { + $forms = array( 'one', 'other' ); + $this->assertEquals( $result, $this->getLang()->convertPlural( $value, $forms ) ); + } + + /** + * @dataProvider providePlural + * @covers Language::getPluralRuleType + */ + public function testGetPluralRuleType( $result, $value ) { + $this->assertEquals( $result, $this->getLang()->getPluralRuleType( $value ) ); + } + + public static function providePlural() { + return array( + array( 'other', 0 ), + array( 'one', 1 ), + array( 'one', 11 ), + array( 'one', 21 ), + array( 'one', 411 ), + array( 'other', 12.345 ), + array( 'other', 20 ), + array( 'one', 31 ), + array( 'other', 200 ), + ); + } +} diff --git a/tests/phpunit/languages/LanguageMlTest.php b/tests/phpunit/languages/LanguageMlTest.php new file mode 100644 index 00000000..4fa45ce3 --- /dev/null +++ b/tests/phpunit/languages/LanguageMlTest.php @@ -0,0 +1,38 @@ +<?php +/** + * @author Santhosh Thottingal + * @copyright Copyright © 2011, Santhosh Thottingal + * @file + */ + +/** Tests for MediaWiki languages/LanguageMl.php */ +class LanguageMlTest extends LanguageClassesTestCase { + + /** + * @dataProvider providerFormatNum + * @see bug 29495 + * @covers Language::formatNum + */ + public function testFormatNum( $result, $value ) { + $this->assertEquals( $result, $this->getLang()->formatNum( $value ) ); + } + + public static function providerFormatNum() { + return array( + array( '12,34,567', '1234567' ), + array( '12,345', '12345' ), + array( '1', '1' ), + array( '123', '123' ), + array( '1,234', '1234' ), + array( '12,345.56', '12345.56' ), + array( '12,34,56,79,81,23,45,678', '12345679812345678' ), + array( '.12345', '.12345' ), + array( '-12,00,000', '-1200000' ), + array( '-98', '-98' ), + array( '-98', -98 ), + array( '-1,23,45,678', -12345678 ), + array( '', '' ), + array( '', null ), + ); + } +} diff --git a/tests/phpunit/languages/LanguageMoTest.php b/tests/phpunit/languages/LanguageMoTest.php new file mode 100644 index 00000000..e0e54ca8 --- /dev/null +++ b/tests/phpunit/languages/LanguageMoTest.php @@ -0,0 +1,45 @@ +<?php +/** + * @author Santhosh Thottingal + * @copyright Copyright © 2012, Santhosh Thottingal + * @file + */ + +/** Tests for MediaWiki languages/classes/LanguageMo.php */ +class LanguageMoTest extends LanguageClassesTestCase { + /** + * @dataProvider providePlural + * @covers Language::convertPlural + */ + public function testPlural( $result, $value ) { + $forms = array( 'one', 'few', 'other' ); + $this->assertEquals( $result, $this->getLang()->convertPlural( $value, $forms ) ); + } + + /** + * @dataProvider providePlural + * @covers Language::getPluralRuleType + */ + public function testGetPluralRuleType( $result, $value ) { + $this->assertEquals( $result, $this->getLang()->getPluralRuleType( $value ) ); + } + + public static function providePlural() { + return array( + array( 'few', 0 ), + array( 'one', 1 ), + array( 'few', 2 ), + array( 'few', 19 ), + array( 'other', 20 ), + array( 'other', 99 ), + array( 'other', 100 ), + array( 'few', 101 ), + array( 'few', 119 ), + array( 'other', 120 ), + array( 'other', 200 ), + array( 'few', 201 ), + array( 'few', 219 ), + array( 'other', 220 ), + ); + } +} diff --git a/tests/phpunit/languages/LanguageMtTest.php b/tests/phpunit/languages/LanguageMtTest.php new file mode 100644 index 00000000..96d2bc92 --- /dev/null +++ b/tests/phpunit/languages/LanguageMtTest.php @@ -0,0 +1,77 @@ +<?php +/** + * @author Amir E. Aharoni + * @copyright Copyright © 2012, Amir E. Aharoni + * @file + */ + +/** Tests for MediaWiki languages/classes/LanguageMt.php */ +class LanguageMtTest extends LanguageClassesTestCase { + /** + * @dataProvider providePlural + * @covers Language::convertPlural + */ + public function testPlural( $result, $value ) { + $forms = array( 'one', 'few', 'many', 'other' ); + $this->assertEquals( $result, $this->getLang()->convertPlural( $value, $forms ) ); + } + + /** + * @dataProvider providePlural + * @covers Language::getPluralRuleType + */ + public function testGetPluralRuleType( $result, $value ) { + $this->assertEquals( $result, $this->getLang()->getPluralRuleType( $value ) ); + } + + public static function providePlural() { + return array( + array( 'few', 0 ), + array( 'one', 1 ), + array( 'few', 2 ), + array( 'few', 10 ), + array( 'many', 11 ), + array( 'many', 19 ), + array( 'other', 20 ), + array( 'other', 99 ), + array( 'other', 100 ), + array( 'other', 101 ), + array( 'few', 102 ), + array( 'few', 110 ), + array( 'many', 111 ), + array( 'many', 119 ), + array( 'other', 120 ), + array( 'other', 201 ), + ); + } + + /** + * @dataProvider providePluralTwoForms + * @covers Language::convertPlural + */ + public function testPluralTwoForms( $result, $value ) { + $forms = array( 'one', 'other' ); + $this->assertEquals( $result, $this->getLang()->convertPlural( $value, $forms ) ); + } + + public static function providePluralTwoForms() { + return array( + array( 'other', 0 ), + array( 'one', 1 ), + array( 'other', 2 ), + array( 'other', 10 ), + array( 'other', 11 ), + array( 'other', 19 ), + array( 'other', 20 ), + array( 'other', 99 ), + array( 'other', 100 ), + array( 'other', 101 ), + array( 'other', 102 ), + array( 'other', 110 ), + array( 'other', 111 ), + array( 'other', 119 ), + array( 'other', 120 ), + array( 'other', 201 ), + ); + } +} diff --git a/tests/phpunit/languages/LanguageNlTest.php b/tests/phpunit/languages/LanguageNlTest.php new file mode 100644 index 00000000..26bd691a --- /dev/null +++ b/tests/phpunit/languages/LanguageNlTest.php @@ -0,0 +1,24 @@ +<?php +/** + * @author Santhosh Thottingal + * @copyright Copyright © 2011, Santhosh Thottingal + * @file + */ + +/** Tests for MediaWiki languages/LanguageNl.php */ +class LanguageNlTest extends LanguageClassesTestCase { + + /** + * @covers Language::formatNum + * @todo split into a test and a dataprovider + */ + public function testFormatNum() { + $this->assertEquals( '1.234.567', $this->getLang()->formatNum( '1234567' ) ); + $this->assertEquals( '12.345', $this->getLang()->formatNum( '12345' ) ); + $this->assertEquals( '1', $this->getLang()->formatNum( '1' ) ); + $this->assertEquals( '123', $this->getLang()->formatNum( '123' ) ); + $this->assertEquals( '1.234', $this->getLang()->formatNum( '1234' ) ); + $this->assertEquals( '12.345,56', $this->getLang()->formatNum( '12345.56' ) ); + $this->assertEquals( ',1234556', $this->getLang()->formatNum( '.1234556' ) ); + } +} diff --git a/tests/phpunit/languages/LanguageNsoTest.php b/tests/phpunit/languages/LanguageNsoTest.php new file mode 100644 index 00000000..18efd736 --- /dev/null +++ b/tests/phpunit/languages/LanguageNsoTest.php @@ -0,0 +1,34 @@ +<?php +/** + * @author Amir E. Aharoni + * @copyright Copyright © 2012, Amir E. Aharoni + * @file + */ + +/** Tests for MediaWiki languages/classes/LanguageNso.php */ +class LanguageNsoTest extends LanguageClassesTestCase { + /** + * @dataProvider providePlural + * @covers Language::convertPlural + */ + public function testPlural( $result, $value ) { + $forms = array( 'one', 'other' ); + $this->assertEquals( $result, $this->getLang()->convertPlural( $value, $forms ) ); + } + + /** + * @dataProvider providePlural + * @covers Language::getPluralRuleType + */ + public function testGetPluralRuleType( $result, $value ) { + $this->assertEquals( $result, $this->getLang()->getPluralRuleType( $value ) ); + } + + public static function providePlural() { + return array( + array( 'one', 0 ), + array( 'one', 1 ), + array( 'other', 2 ), + ); + } +} diff --git a/tests/phpunit/languages/LanguagePlTest.php b/tests/phpunit/languages/LanguagePlTest.php new file mode 100644 index 00000000..d180037b --- /dev/null +++ b/tests/phpunit/languages/LanguagePlTest.php @@ -0,0 +1,77 @@ +<?php +/** + * @author Amir E. Aharoni + * @copyright Copyright © 2012, Amir E. Aharoni + * @file + */ + +/** Tests for MediaWiki languages/classes/LanguagePl.php */ +class LanguagePlTest extends LanguageClassesTestCase { + /** + * @dataProvider providePlural + * @covers Language::convertPlural + */ + public function testPlural( $result, $value ) { + $forms = array( 'one', 'few', 'many' ); + $this->assertEquals( $result, $this->getLang()->convertPlural( $value, $forms ) ); + } + + /** + * @dataProvider providePlural + * @covers Language::getPluralRuleType + */ + public function testGetPluralRuleType( $result, $value ) { + $this->assertEquals( $result, $this->getLang()->getPluralRuleType( $value ) ); + } + + public static function providePlural() { + return array( + array( 'many', 0 ), + array( 'one', 1 ), + array( 'few', 2 ), + array( 'few', 3 ), + array( 'few', 4 ), + array( 'many', 5 ), + array( 'many', 9 ), + array( 'many', 10 ), + array( 'many', 11 ), + array( 'many', 21 ), + array( 'few', 22 ), + array( 'few', 23 ), + array( 'few', 24 ), + array( 'many', 25 ), + array( 'many', 200 ), + array( 'many', 201 ), + ); + } + + /** + * @dataProvider providePluralTwoForms + * @covers Language::convertPlural + */ + public function testPluralTwoForms( $result, $value ) { + $forms = array( 'one', 'other' ); + $this->assertEquals( $result, $this->getLang()->convertPlural( $value, $forms ) ); + } + + public static function providePluralTwoForms() { + return array( + array( 'other', 0 ), + array( 'one', 1 ), + array( 'other', 2 ), + array( 'other', 3 ), + array( 'other', 4 ), + array( 'other', 5 ), + array( 'other', 9 ), + array( 'other', 10 ), + array( 'other', 11 ), + array( 'other', 21 ), + array( 'other', 22 ), + array( 'other', 23 ), + array( 'other', 24 ), + array( 'other', 25 ), + array( 'other', 200 ), + array( 'other', 201 ), + ); + } +} diff --git a/tests/phpunit/languages/LanguageRoTest.php b/tests/phpunit/languages/LanguageRoTest.php new file mode 100644 index 00000000..ae7816bc --- /dev/null +++ b/tests/phpunit/languages/LanguageRoTest.php @@ -0,0 +1,45 @@ +<?php +/** + * @author Amir E. Aharoni + * @copyright Copyright © 2012, Amir E. Aharoni + * @file + */ + +/** Tests for MediaWiki languages/classes/LanguageRo.php */ +class LanguageRoTest extends LanguageClassesTestCase { + /** + * @dataProvider providePlural + * @covers Language::convertPlural + */ + public function testPlural( $result, $value ) { + $forms = array( 'one', 'few', 'other' ); + $this->assertEquals( $result, $this->getLang()->convertPlural( $value, $forms ) ); + } + + /** + * @dataProvider providePlural + * @covers Language::getPluralRuleType + */ + public function testGetPluralRuleType( $result, $value ) { + $this->assertEquals( $result, $this->getLang()->getPluralRuleType( $value ) ); + } + + public static function providePlural() { + return array( + array( 'few', 0 ), + array( 'one', 1 ), + array( 'few', 2 ), + array( 'few', 19 ), + array( 'other', 20 ), + array( 'other', 99 ), + array( 'other', 100 ), + array( 'few', 101 ), + array( 'few', 119 ), + array( 'other', 120 ), + array( 'other', 200 ), + array( 'few', 201 ), + array( 'few', 219 ), + array( 'other', 220 ), + ); + } +} diff --git a/tests/phpunit/languages/LanguageRuTest.php b/tests/phpunit/languages/LanguageRuTest.php new file mode 100644 index 00000000..e17c7085 --- /dev/null +++ b/tests/phpunit/languages/LanguageRuTest.php @@ -0,0 +1,105 @@ +<?php +/** + * @author Amir E. Aharoni + * based on LanguageBe_tarask.php + * @copyright Copyright © 2012, Amir E. Aharoni + * @file + */ + +/** Tests for MediaWiki languages/classes/LanguageRu.php */ +class LanguageRuTest extends LanguageClassesTestCase { + /** + * @dataProvider providePlural + * @covers Language::convertPlural + */ + public function testPlural( $result, $value ) { + $forms = array( 'one', 'many', 'other' ); + $this->assertEquals( $result, $this->getLang()->convertPlural( $value, $forms ) ); + } + + /** + * Test explicit plural forms - n=FormN forms + * @covers Language::convertPlural + */ + public function testExplicitPlural() { + $forms = array( 'one','many', 'other', '12=dozen' ); + $this->assertEquals( 'dozen', $this->getLang()->convertPlural( 12, $forms ) ); + $forms = array( 'one', 'many', '100=hundred', 'other', '12=dozen' ); + $this->assertEquals( 'hundred', $this->getLang()->convertPlural( 100, $forms ) ); + } + + /** + * @dataProvider providePlural + * @covers Language::getPluralRuleType + */ + public function testGetPluralRuleType( $result, $value ) { + $this->assertEquals( $result, $this->getLang()->getPluralRuleType( $value ) ); + } + + public static function providePlural() { + return array( + array( 'one', 1 ), + array( 'many', 11 ), + array( 'one', 91 ), + array( 'one', 121 ), + array( 'other', 2 ), + array( 'other', 3 ), + array( 'other', 4 ), + array( 'other', 334 ), + array( 'many', 5 ), + array( 'many', 15 ), + array( 'many', 120 ), + ); + } + + /** + * @dataProvider providePluralTwoForms + * @covers Language::convertPlural + */ + public function testPluralTwoForms( $result, $value ) { + $forms = array( '1=one', 'other' ); + $this->assertEquals( $result, $this->getLang()->convertPlural( $value, $forms ) ); + } + + public static function providePluralTwoForms() { + return array( + array( 'one', 1 ), + array( 'other', 11 ), + array( 'other', 91 ), + array( 'other', 121 ), + ); + } + + /** + * @dataProvider providerGrammar + * @covers Language::convertGrammar + */ + public function testGrammar( $result, $word, $case ) { + $this->assertEquals( $result, $this->getLang()->convertGrammar( $word, $case ) ); + } + + public static function providerGrammar() { + return array( + array( + 'Википедии', + 'Википедия', + 'genitive', + ), + array( + 'Викитеки', + 'Викитека', + 'genitive', + ), + array( + 'Викитеке', + 'Викитека', + 'prepositional', + ), + array( + 'Викиданных', + 'Викиданные', + 'prepositional', + ), + ); + } +} diff --git a/tests/phpunit/languages/LanguageSeTest.php b/tests/phpunit/languages/LanguageSeTest.php new file mode 100644 index 00000000..533aa2bc --- /dev/null +++ b/tests/phpunit/languages/LanguageSeTest.php @@ -0,0 +1,53 @@ +<?php +/** + * @author Amir E. Aharoni + * @copyright Copyright © 2012, Amir E. Aharoni + * @file + */ + +/** Tests for MediaWiki languages/classes/LanguageSe.php */ +class LanguageSeTest extends LanguageClassesTestCase { + /** + * @dataProvider providePlural + * @covers Language::convertPlural + */ + public function testPlural( $result, $value ) { + $forms = array( 'one', 'two', 'other' ); + $this->assertEquals( $result, $this->getLang()->convertPlural( $value, $forms ) ); + } + + /** + * @dataProvider providePlural + * @covers Language::getPluralRuleType + */ + public function testGetPluralRuleType( $result, $value ) { + $this->assertEquals( $result, $this->getLang()->getPluralRuleType( $value ) ); + } + + public static function providePlural() { + return array( + array( 'other', 0 ), + array( 'one', 1 ), + array( 'two', 2 ), + array( 'other', 3 ), + ); + } + + /** + * @dataProvider providePluralTwoForms + * @covers Language::convertPlural + */ + public function testPluralTwoForms( $result, $value ) { + $forms = array( 'one', 'other' ); + $this->assertEquals( $result, $this->getLang()->convertPlural( $value, $forms ) ); + } + + public static function providePluralTwoForms() { + return array( + array( 'other', 0 ), + array( 'one', 1 ), + array( 'other', 2 ), + array( 'other', 3 ), + ); + } +} diff --git a/tests/phpunit/languages/LanguageSgsTest.php b/tests/phpunit/languages/LanguageSgsTest.php new file mode 100644 index 00000000..fa49a4dd --- /dev/null +++ b/tests/phpunit/languages/LanguageSgsTest.php @@ -0,0 +1,71 @@ +<?php +/** + * @author Amir E. Aharoni + * @copyright Copyright © 2012, Amir E. Aharoni + * @file + */ + +/** Tests for Samogitian */ +class LanguageSgsTest extends LanguageClassesTestCase { + /** + * @dataProvider providePluralAllForms + * @covers Language::convertPlural + */ + public function testPluralAllForms( $result, $value ) { + $forms = array( 'one', 'two', 'few', 'other' ); + $this->assertEquals( $result, $this->getLang()->convertPlural( $value, $forms ) ); + } + + /** + * @dataProvider providePluralAllForms + * @covers Language::getPluralRuleType + */ + public function testGetPluralRuleType( $result, $value ) { + $this->assertEquals( $result, $this->getLang()->getPluralRuleType( $value ) ); + } + + public static function providePluralAllForms() { + return array( + array( 'few', 0 ), + array( 'one', 1 ), + array( 'two', 2 ), + array( 'other', 3 ), + array( 'few', 10 ), + array( 'few', 11 ), + array( 'few', 12 ), + array( 'few', 19 ), + array( 'other', 20 ), + array( 'few', 100 ), + array( 'one', 101 ), + array( 'few', 111 ), + array( 'few', 112 ), + ); + } + + /** + * @dataProvider providePluralTwoForms + * @covers Language::convertPlural + */ + public function testPluralTwoForms( $result, $value ) { + $forms = array( 'one', 'other' ); + $this->assertEquals( $result, $this->getLang()->convertPlural( $value, $forms ) ); + } + + public static function providePluralTwoForms() { + return array( + array( 'other', 0 ), + array( 'one', 1 ), + array( 'other', 2 ), + array( 'other', 3 ), + array( 'other', 10 ), + array( 'other', 11 ), + array( 'other', 12 ), + array( 'other', 19 ), + array( 'other', 20 ), + array( 'other', 100 ), + array( 'one', 101 ), + array( 'other', 111 ), + array( 'other', 112 ), + ); + } +} diff --git a/tests/phpunit/languages/LanguageShTest.php b/tests/phpunit/languages/LanguageShTest.php new file mode 100644 index 00000000..1b390872 --- /dev/null +++ b/tests/phpunit/languages/LanguageShTest.php @@ -0,0 +1,42 @@ +<?php +/** + * @author Amir E. Aharoni + * @copyright Copyright © 2012, Amir E. Aharoni + * @file + */ + +/** Tests for srpskohrvatski / српскохрватски / Serbocroatian */ +class LanguageShTest extends LanguageClassesTestCase { + /** + * @dataProvider providePlural + * @covers Language::convertPlural + */ + public function testPlural( $result, $value ) { + $forms = array( 'one', 'few', 'other' ); + $this->assertEquals( $result, $this->getLang()->convertPlural( $value, $forms ) ); + } + + /** + * @dataProvider providePlural + * @covers Language::getPluralRuleType + */ + public function testGetPluralRuleType( $result, $value ) { + $this->assertEquals( $result, $this->getLang()->getPluralRuleType( $value ) ); + } + + public static function providePlural() { + return array( + array( 'other', 0 ), + array( 'one', 1 ), + array( 'few', 2 ), + array( 'few', 4 ), + array( 'other', 5 ), + array( 'other', 10 ), + array( 'other', 11 ), + array( 'other', 12 ), + array( 'one', 101 ), + array( 'few', 102 ), + array( 'other', 111 ), + ); + } +} diff --git a/tests/phpunit/languages/LanguageSkTest.php b/tests/phpunit/languages/LanguageSkTest.php new file mode 100644 index 00000000..cb8a13b8 --- /dev/null +++ b/tests/phpunit/languages/LanguageSkTest.php @@ -0,0 +1,42 @@ +<?php +/** + * @author Santhosh Thottingal + * @copyright Copyright © 2012, Amir E. Aharoni + * based on LanguageSkTest.php + * @file + */ + +/** Tests for MediaWiki languages/classes/LanguageSk.php */ +class LanguageSkTest extends LanguageClassesTestCase { + /** + * @dataProvider providePlural + * @covers Language::convertPlural + */ + public function testPlural( $result, $value ) { + $forms = array( 'one', 'few', 'other' ); + $this->assertEquals( $result, $this->getLang()->convertPlural( $value, $forms ) ); + } + + /** + * @dataProvider providePlural + * @covers Language::getPluralRuleType + */ + public function testGetPluralRuleType( $result, $value ) { + $this->assertEquals( $result, $this->getLang()->getPluralRuleType( $value ) ); + } + + public static function providePlural() { + return array( + array( 'other', 0 ), + array( 'one', 1 ), + array( 'few', 2 ), + array( 'few', 3 ), + array( 'few', 4 ), + array( 'other', 5 ), + array( 'other', 11 ), + array( 'other', 20 ), + array( 'other', 25 ), + array( 'other', 200 ), + ); + } +} diff --git a/tests/phpunit/languages/LanguageSlTest.php b/tests/phpunit/languages/LanguageSlTest.php new file mode 100644 index 00000000..9783dd80 --- /dev/null +++ b/tests/phpunit/languages/LanguageSlTest.php @@ -0,0 +1,44 @@ +<?php +/** + * @author Santhosh Thottingal + * @copyright Copyright © 2012, Amir E. Aharoni + * based on LanguageSkTest.php + * @file + */ + +/** Tests for MediaWiki languages/classes/LanguageSl.php */ +class LanguageSlTest extends LanguageClassesTestCase { + /** + * @dataProvider providerPlural + * @covers Language::convertPlural + */ + public function testPlural( $result, $value ) { + $forms = array( 'one', 'two', 'few', 'other' ); + $this->assertEquals( $result, $this->getLang()->convertPlural( $value, $forms ) ); + } + + /** + * @dataProvider providerPlural + * @covers Language::getPluralRuleType + */ + public function testGetPluralRuleType( $result, $value ) { + $this->assertEquals( $result, $this->getLang()->getPluralRuleType( $value ) ); + } + + public static function providerPlural() { + return array( + array( 'other', 0 ), + array( 'one', 1 ), + array( 'two', 2 ), + array( 'few', 3 ), + array( 'few', 4 ), + array( 'other', 5 ), + array( 'other', 99 ), + array( 'other', 100 ), + array( 'one', 101 ), + array( 'two', 102 ), + array( 'few', 103 ), + array( 'one', 201 ), + ); + } +} diff --git a/tests/phpunit/languages/LanguageSmaTest.php b/tests/phpunit/languages/LanguageSmaTest.php new file mode 100644 index 00000000..95cb333c --- /dev/null +++ b/tests/phpunit/languages/LanguageSmaTest.php @@ -0,0 +1,53 @@ +<?php +/** + * @author Amir E. Aharoni + * @copyright Copyright © 2012, Amir E. Aharoni + * @file + */ + +/** Tests for MediaWiki languages/classes/LanguageSma.php */ +class LanguageSmaTest extends LanguageClassesTestCase { + /** + * @dataProvider providePlural + * @covers Language::convertPlural + */ + public function testPlural( $result, $value ) { + $forms = array( 'one', 'two', 'other' ); + $this->assertEquals( $result, $this->getLang()->convertPlural( $value, $forms ) ); + } + + /** + * @dataProvider providePlural + * @covers Language::getPluralRuleType + */ + public function testGetPluralRuleType( $result, $value ) { + $this->assertEquals( $result, $this->getLang()->getPluralRuleType( $value ) ); + } + + public static function providePlural() { + return array( + array( 'other', 0 ), + array( 'one', 1 ), + array( 'two', 2 ), + array( 'other', 3 ), + ); + } + + /** + * @dataProvider providePluralTwoForms + * @covers Language::convertPlural + */ + public function testPluralTwoForms( $result, $value ) { + $forms = array( 'one', 'other' ); + $this->assertEquals( $result, $this->getLang()->convertPlural( $value, $forms ) ); + } + + public static function providePluralTwoForms() { + return array( + array( 'other', 0 ), + array( 'one', 1 ), + array( 'other', 2 ), + array( 'other', 3 ), + ); + } +} diff --git a/tests/phpunit/languages/LanguageSrTest.php b/tests/phpunit/languages/LanguageSrTest.php new file mode 100644 index 00000000..d6fedb57 --- /dev/null +++ b/tests/phpunit/languages/LanguageSrTest.php @@ -0,0 +1,246 @@ +<?php +/** + * PHPUnit tests for the Serbian language. + * The language can be represented using two scripts: + * - Latin (SR_el) + * - Cyrillic (SR_ec) + * Both representations seems to be bijective, hence MediaWiki can convert + * from one script to the other. + * + * @author Antoine Musso <hashar at free dot fr> + * @copyright Copyright © 2011, Antoine Musso <hashar at free dot fr> + * @file + * + * @todo methods in test class should be tidied: + * - Should be split into separate test methods and data providers + * - Tests for LanguageConverter and Language should probably be separate.. + */ + +/** Tests for MediaWiki languages/LanguageSr.php */ +class LanguageSrTest extends LanguageClassesTestCase { + /** + * @covers LanguageConverter::convertTo + */ + public function testEasyConversions() { + $this->assertCyrillic( + 'шђчћжШЂЧЋЖ', + 'Cyrillic guessing characters' + ); + $this->assertLatin( + 'šđč枊ĐČĆŽ', + 'Latin guessing characters' + ); + } + + /** + * @covers LanguageConverter::convertTo + */ + public function testMixedConversions() { + $this->assertCyrillic( + 'шђчћжШЂЧЋЖ - šđčćž', + 'Mostly cyrillic characters' + ); + $this->assertLatin( + 'šđč枊ĐČĆŽ - шђчћж', + 'Mostly latin characters' + ); + } + + /** + * @covers LanguageConverter::convertTo + */ + public function testSameAmountOfLatinAndCyrillicGetConverted() { + $this->assertConverted( + '4 latin: šđčć | 4 cyrillic: шђчћ', + 'sr-ec' + ); + $this->assertConverted( + '4 latin: šđčć | 4 cyrillic: шђчћ', + 'sr-el' + ); + } + + /** + * @author Nikola Smolenski + * @covers LanguageConverter::convertTo + */ + public function testConversionToCyrillic() { + //A simple convertion of Latin to Cyrillic + $this->assertEquals( 'абвг', + $this->convertToCyrillic( 'abvg' ) + ); + //Same as above, but assert that -{}-s must be removed and not converted + $this->assertEquals( 'ljабnjвгdž', + $this->convertToCyrillic( '-{lj}-ab-{nj}-vg-{dž}-' ) + ); + //A simple convertion of Cyrillic to Cyrillic + $this->assertEquals( 'абвг', + $this->convertToCyrillic( 'абвг' ) + ); + //Same as above, but assert that -{}-s must be removed and not converted + $this->assertEquals( 'ljабnjвгdž', + $this->convertToCyrillic( '-{lj}-аб-{nj}-вг-{dž}-' ) + ); + //This text has some Latin, but is recognized as Cyrillic, so it should not be converted + $this->assertEquals( 'abvgшђжчћ', + $this->convertToCyrillic( 'abvgшђжчћ' ) + ); + //Same as above, but assert that -{}-s must be removed + $this->assertEquals( 'љabvgњшђжчћџ', + $this->convertToCyrillic( '-{љ}-abvg-{њ}-шђжчћ-{џ}-' ) + ); + //This text has some Cyrillic, but is recognized as Latin, so it should be converted + $this->assertEquals( 'абвгшђжчћ', + $this->convertToCyrillic( 'абвгšđžčć' ) + ); + //Same as above, but assert that -{}-s must be removed and not converted + $this->assertEquals( 'ljабвгnjшђжчћdž', + $this->convertToCyrillic( '-{lj}-абвг-{nj}-šđžčć-{dž}-' ) + ); + // Roman numerals are not converted + $this->assertEquals( 'а I б II в III г IV шђжчћ', + $this->convertToCyrillic( 'a I b II v III g IV šđžčć' ) + ); + } + + /** + * @covers LanguageConverter::convertTo + */ + public function testConversionToLatin() { + //A simple convertion of Latin to Latin + $this->assertEquals( 'abcd', + $this->convertToLatin( 'abcd' ) + ); + //A simple convertion of Cyrillic to Latin + $this->assertEquals( 'abcd', + $this->convertToLatin( 'абцд' ) + ); + //This text has some Latin, but is recognized as Cyrillic, so it should be converted + $this->assertEquals( 'abcdšđžčć', + $this->convertToLatin( 'abcdшђжчћ' ) + ); + //This text has some Cyrillic, but is recognized as Latin, so it should not be converted + $this->assertEquals( 'абцдšđžčć', + $this->convertToLatin( 'абцдšđžčć' ) + ); + } + + /** + * @dataProvider providePlural + * @covers Language::convertPlural + */ + public function testPlural( $result, $value ) { + $forms = array( 'one', 'few', 'other' ); + $this->assertEquals( $result, $this->getLang()->convertPlural( $value, $forms ) ); + } + + /** + * @dataProvider providePlural + * @covers Language::getPluralRuleType + */ + public function testGetPluralRuleType( $result, $value ) { + $this->assertEquals( $result, $this->getLang()->getPluralRuleType( $value ) ); + } + + public static function providePlural() { + return array( + array( 'one', 1 ), + array( 'other', 11 ), + array( 'one', 91 ), + array( 'one', 121 ), + array( 'few', 2 ), + array( 'few', 3 ), + array( 'few', 4 ), + array( 'few', 334 ), + array( 'other', 5 ), + array( 'other', 15 ), + array( 'other', 120 ), + ); + } + + /** + * @dataProvider providePluralTwoForms + * @covers Language::convertPlural + */ + public function testPluralTwoForms( $result, $value ) { + $forms = array( 'one', 'other' ); + $this->assertEquals( $result, $this->getLang()->convertPlural( $value, $forms ) ); + } + + public static function providePluralTwoForms() { + return array( + array( 'one', 1 ), + array( 'other', 11 ), + array( 'other', 4 ), + array( 'one', 91 ), + array( 'one', 121 ), + ); + } + + ##### HELPERS ##################################################### + /** + *Wrapper to verify text stay the same after applying conversion + * @param $text string Text to convert + * @param $variant string Language variant 'sr-ec' or 'sr-el' + * @param $msg string Optional message + */ + protected function assertUnConverted( $text, $variant, $msg = '' ) { + $this->assertEquals( + $text, + $this->convertTo( $text, $variant ), + $msg + ); + } + + /** + * Wrapper to verify a text is different once converted to a variant. + * @param $text string Text to convert + * @param $variant string Language variant 'sr-ec' or 'sr-el' + * @param $msg string Optional message + */ + protected function assertConverted( $text, $variant, $msg = '' ) { + $this->assertNotEquals( + $text, + $this->convertTo( $text, $variant ), + $msg + ); + } + + /** + * Verifiy the given Cyrillic text is not converted when using + * using the cyrillic variant and converted to Latin when using + * the Latin variant. + */ + protected function assertCyrillic( $text, $msg = '' ) { + $this->assertUnConverted( $text, 'sr-ec', $msg ); + $this->assertConverted( $text, 'sr-el', $msg ); + } + + /** + * Verifiy the given Latin text is not converted when using + * using the Latin variant and converted to Cyrillic when using + * the Cyrillic variant. + */ + protected function assertLatin( $text, $msg = '' ) { + $this->assertUnConverted( $text, 'sr-el', $msg ); + $this->assertConverted( $text, 'sr-ec', $msg ); + } + + + /** Wrapper for converter::convertTo() method*/ + protected function convertTo( $text, $variant ) { + return $this->getLang() + ->mConverter + ->convertTo( + $text, $variant + ); + } + + protected function convertToCyrillic( $text ) { + return $this->convertTo( $text, 'sr-ec' ); + } + + protected function convertToLatin( $text ) { + return $this->convertTo( $text, 'sr-el' ); + } +} diff --git a/tests/phpunit/languages/LanguageTest.php b/tests/phpunit/languages/LanguageTest.php new file mode 100644 index 00000000..78929e23 --- /dev/null +++ b/tests/phpunit/languages/LanguageTest.php @@ -0,0 +1,1567 @@ +<?php + +class LanguageTest extends LanguageClassesTestCase { + /** + * @covers Language::convertDoubleWidth + * @covers Language::normalizeForSearch + */ + public function testLanguageConvertDoubleWidthToSingleWidth() { + $this->assertEquals( + "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz", + $this->getLang()->normalizeForSearch( + "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" + ), + 'convertDoubleWidth() with the full alphabet and digits' + ); + } + + /** + * @dataProvider provideFormattableTimes# + * @covers Language::formatTimePeriod + */ + public function testFormatTimePeriod( $seconds, $format, $expected, $desc ) { + $this->assertEquals( $expected, $this->getLang()->formatTimePeriod( $seconds, $format ), $desc ); + } + + public static function provideFormattableTimes() { + return array( + array( + 9.45, + array(), + '9.5 s', + 'formatTimePeriod() rounding (<10s)' + ), + array( + 9.45, + array( 'noabbrevs' => true ), + '9.5 seconds', + 'formatTimePeriod() rounding (<10s)' + ), + array( + 9.95, + array(), + '10 s', + 'formatTimePeriod() rounding (<10s)' + ), + array( + 9.95, + array( 'noabbrevs' => true ), + '10 seconds', + 'formatTimePeriod() rounding (<10s)' + ), + array( + 59.55, + array(), + '1 min 0 s', + 'formatTimePeriod() rounding (<60s)' + ), + array( + 59.55, + array( 'noabbrevs' => true ), + '1 minute 0 seconds', + 'formatTimePeriod() rounding (<60s)' + ), + array( + 119.55, + array(), + '2 min 0 s', + 'formatTimePeriod() rounding (<1h)' + ), + array( + 119.55, + array( 'noabbrevs' => true ), + '2 minutes 0 seconds', + 'formatTimePeriod() rounding (<1h)' + ), + array( + 3599.55, + array(), + '1 h 0 min 0 s', + 'formatTimePeriod() rounding (<1h)' + ), + array( + 3599.55, + array( 'noabbrevs' => true ), + '1 hour 0 minutes 0 seconds', + 'formatTimePeriod() rounding (<1h)' + ), + array( + 7199.55, + array(), + '2 h 0 min 0 s', + 'formatTimePeriod() rounding (>=1h)' + ), + array( + 7199.55, + array( 'noabbrevs' => true ), + '2 hours 0 minutes 0 seconds', + 'formatTimePeriod() rounding (>=1h)' + ), + array( + 7199.55, + 'avoidseconds', + '2 h 0 min', + 'formatTimePeriod() rounding (>=1h), avoidseconds' + ), + array( + 7199.55, + array( 'avoid' => 'avoidseconds', 'noabbrevs' => true ), + '2 hours 0 minutes', + 'formatTimePeriod() rounding (>=1h), avoidseconds' + ), + array( + 7199.55, + 'avoidminutes', + '2 h 0 min', + 'formatTimePeriod() rounding (>=1h), avoidminutes' + ), + array( + 7199.55, + array( 'avoid' => 'avoidminutes', 'noabbrevs' => true ), + '2 hours 0 minutes', + 'formatTimePeriod() rounding (>=1h), avoidminutes' + ), + array( + 172799.55, + 'avoidseconds', + '48 h 0 min', + 'formatTimePeriod() rounding (=48h), avoidseconds' + ), + array( + 172799.55, + array( 'avoid' => 'avoidseconds', 'noabbrevs' => true ), + '48 hours 0 minutes', + 'formatTimePeriod() rounding (=48h), avoidseconds' + ), + array( + 259199.55, + 'avoidminutes', + '3 d 0 h', + 'formatTimePeriod() rounding (>48h), avoidminutes' + ), + array( + 259199.55, + array( 'avoid' => 'avoidminutes', 'noabbrevs' => true ), + '3 days 0 hours', + 'formatTimePeriod() rounding (>48h), avoidminutes' + ), + array( + 176399.55, + 'avoidseconds', + '2 d 1 h 0 min', + 'formatTimePeriod() rounding (>48h), avoidseconds' + ), + array( + 176399.55, + array( 'avoid' => 'avoidseconds', 'noabbrevs' => true ), + '2 days 1 hour 0 minutes', + 'formatTimePeriod() rounding (>48h), avoidseconds' + ), + array( + 176399.55, + 'avoidminutes', + '2 d 1 h', + 'formatTimePeriod() rounding (>48h), avoidminutes' + ), + array( + 176399.55, + array( 'avoid' => 'avoidminutes', 'noabbrevs' => true ), + '2 days 1 hour', + 'formatTimePeriod() rounding (>48h), avoidminutes' + ), + array( + 259199.55, + 'avoidseconds', + '3 d 0 h 0 min', + 'formatTimePeriod() rounding (>48h), avoidseconds' + ), + array( + 259199.55, + array( 'avoid' => 'avoidseconds', 'noabbrevs' => true ), + '3 days 0 hours 0 minutes', + 'formatTimePeriod() rounding (>48h), avoidseconds' + ), + array( + 172801.55, + 'avoidseconds', + '2 d 0 h 0 min', + 'formatTimePeriod() rounding, (>48h), avoidseconds' + ), + array( + 172801.55, + array( 'avoid' => 'avoidseconds', 'noabbrevs' => true ), + '2 days 0 hours 0 minutes', + 'formatTimePeriod() rounding, (>48h), avoidseconds' + ), + array( + 176460.55, + array(), + '2 d 1 h 1 min 1 s', + 'formatTimePeriod() rounding, recursion, (>48h)' + ), + array( + 176460.55, + array( 'noabbrevs' => true ), + '2 days 1 hour 1 minute 1 second', + 'formatTimePeriod() rounding, recursion, (>48h)' + ), + ); + } + + /** + * @covers Language::truncate + */ + public function testTruncate() { + $this->assertEquals( + "XXX", + $this->getLang()->truncate( "1234567890", 0, 'XXX' ), + 'truncate prefix, len 0, small ellipsis' + ); + + $this->assertEquals( + "12345XXX", + $this->getLang()->truncate( "1234567890", 8, 'XXX' ), + 'truncate prefix, small ellipsis' + ); + + $this->assertEquals( + "123456789", + $this->getLang()->truncate( "123456789", 5, 'XXXXXXXXXXXXXXX' ), + 'truncate prefix, large ellipsis' + ); + + $this->assertEquals( + "XXX67890", + $this->getLang()->truncate( "1234567890", -8, 'XXX' ), + 'truncate suffix, small ellipsis' + ); + + $this->assertEquals( + "123456789", + $this->getLang()->truncate( "123456789", -5, 'XXXXXXXXXXXXXXX' ), + 'truncate suffix, large ellipsis' + ); + } + + /** + * @dataProvider provideHTMLTruncateData + * @covers Language::truncateHTML + */ + public function testTruncateHtml( $len, $ellipsis, $input, $expected ) { + // Actual HTML... + $this->assertEquals( + $expected, + $this->getLang()->truncateHTML( $input, $len, $ellipsis ) + ); + } + + /** + * @return array format is ($len, $ellipsis, $input, $expected) + */ + public static function provideHTMLTruncateData() { + return array( + array( 0, 'XXX', "1234567890", "XXX" ), + array( 8, 'XXX', "1234567890", "12345XXX" ), + array( 5, 'XXXXXXXXXXXXXXX', '1234567890', "1234567890" ), + array( 2, '***', + '<p><span style="font-weight:bold;"></span></p>', + '<p><span style="font-weight:bold;"></span></p>', + ), + array( 2, '***', + '<p><span style="font-weight:bold;">123456789</span></p>', + '<p><span style="font-weight:bold;">***</span></p>', + ), + array( 2, '***', + '<p><span style="font-weight:bold;"> 23456789</span></p>', + '<p><span style="font-weight:bold;">***</span></p>', + ), + array( 3, '***', + '<p><span style="font-weight:bold;">123456789</span></p>', + '<p><span style="font-weight:bold;">***</span></p>', + ), + array( 4, '***', + '<p><span style="font-weight:bold;">123456789</span></p>', + '<p><span style="font-weight:bold;">1***</span></p>', + ), + array( 5, '***', + '<tt><span style="font-weight:bold;">123456789</span></tt>', + '<tt><span style="font-weight:bold;">12***</span></tt>', + ), + array( 6, '***', + '<p><a href="www.mediawiki.org">123456789</a></p>', + '<p><a href="www.mediawiki.org">123***</a></p>', + ), + array( 6, '***', + '<p><a href="www.mediawiki.org">12 456789</a></p>', + '<p><a href="www.mediawiki.org">12 ***</a></p>', + ), + array( 7, '***', + '<small><span style="font-weight:bold;">123<p id="#moo">456</p>789</span></small>', + '<small><span style="font-weight:bold;">123<p id="#moo">4***</p></span></small>', + ), + array( 8, '***', + '<div><span style="font-weight:bold;">123<span>4</span>56789</span></div>', + '<div><span style="font-weight:bold;">123<span>4</span>5***</span></div>', + ), + array( 9, '***', + '<p><table style="font-weight:bold;"><tr><td>123456789</td></tr></table></p>', + '<p><table style="font-weight:bold;"><tr><td>123456789</td></tr></table></p>', + ), + array( 10, '***', + '<p><font style="font-weight:bold;">123456789</font></p>', + '<p><font style="font-weight:bold;">123456789</font></p>', + ), + ); + } + + /** + * Test Language::isWellFormedLanguageTag() + * @dataProvider provideWellFormedLanguageTags + * @covers Language::isWellFormedLanguageTag + */ + public function testWellFormedLanguageTag( $code, $message = '' ) { + $this->assertTrue( + Language::isWellFormedLanguageTag( $code ), + "validating code $code $message" + ); + } + + /** + * The test cases are based on the tests in the GaBuZoMeu parser + * written by Stéphane Bortzmeyer <bortzmeyer@nic.fr> + * and distributed as free software, under the GNU General Public Licence. + * http://www.bortzmeyer.org/gabuzomeu-parsing-language-tags.html + */ + public static function provideWellFormedLanguageTags() { + return array( + array( 'fr', 'two-letter code' ), + array( 'fr-latn', 'two-letter code with lower case script code' ), + array( 'fr-Latn-FR', 'two-letter code with title case script code and uppercase country code' ), + array( 'fr-Latn-419', 'two-letter code with title case script code and region number' ), + array( 'fr-FR', 'two-letter code with uppercase' ), + array( 'ax-TZ', 'Not in the registry, but well-formed' ), + array( 'fr-shadok', 'two-letter code with variant' ), + array( 'fr-y-myext-myext2', 'non-x singleton' ), + array( 'fra-Latn', 'ISO 639 can be 3-letters' ), + array( 'fra', 'three-letter language code' ), + array( 'fra-FX', 'three-letter language code with country code' ), + array( 'i-klingon', 'grandfathered with singleton' ), + array( 'I-kLINgon', 'tags are case-insensitive...' ), + array( 'no-bok', 'grandfathered without singleton' ), + array( 'i-enochian', 'Grandfathered' ), + array( 'x-fr-CH', 'private use' ), + array( 'es-419', 'two-letter code with region number' ), + array( 'en-Latn-GB-boont-r-extended-sequence-x-private', 'weird, but well-formed' ), + array( 'ab-x-abc-x-abc', 'anything goes after x' ), + array( 'ab-x-abc-a-a', 'anything goes after x, including several non-x singletons' ), + array( 'i-default', 'grandfathered' ), + array( 'abcd-Latn', 'Language of 4 chars reserved for future use' ), + array( 'AaBbCcDd-x-y-any-x', 'Language of 5-8 chars, registered' ), + array( 'de-CH-1901', 'with country and year' ), + array( 'en-US-x-twain', 'with country and singleton' ), + array( 'zh-cmn', 'three-letter variant' ), + array( 'zh-cmn-Hant', 'three-letter variant and script' ), + array( 'zh-cmn-Hant-HK', 'three-letter variant, script and country' ), + array( 'xr-p-lze', 'Extension' ), + ); + } + + /** + * Negative test for Language::isWellFormedLanguageTag() + * @dataProvider provideMalformedLanguageTags + * @covers Language::isWellFormedLanguageTag + */ + public function testMalformedLanguageTag( $code, $message = '' ) { + $this->assertFalse( + Language::isWellFormedLanguageTag( $code ), + "validating that code $code is a malformed language tag - $message" + ); + } + + /** + * The test cases are based on the tests in the GaBuZoMeu parser + * written by Stéphane Bortzmeyer <bortzmeyer@nic.fr> + * and distributed as free software, under the GNU General Public Licence. + * http://www.bortzmeyer.org/gabuzomeu-parsing-language-tags.html + */ + public static function provideMalformedLanguageTags() { + return array( + array( 'f', 'language too short' ), + array( 'f-Latn', 'language too short with script' ), + array( 'xr-lxs-qut', 'variants too short' ), # extlangS + array( 'fr-Latn-F', 'region too short' ), + array( 'a-value', 'language too short with region' ), + array( 'tlh-a-b-foo', 'valid three-letter with wrong variant' ), + array( 'i-notexist', 'grandfathered but not registered: invalid, even if we only test well-formedness' ), + array( 'abcdefghi-012345678', 'numbers too long' ), + array( 'ab-abc-abc-abc-abc', 'invalid extensions' ), + array( 'ab-abcd-abc', 'invalid extensions' ), + array( 'ab-ab-abc', 'invalid extensions' ), + array( 'ab-123-abc', 'invalid extensions' ), + array( 'a-Hant-ZH', 'short language with valid extensions' ), + array( 'a1-Hant-ZH', 'invalid character in language' ), + array( 'ab-abcde-abc', 'invalid extensions' ), + array( 'ab-1abc-abc', 'invalid characters in extensions' ), + array( 'ab-ab-abcd', 'invalid order of extensions' ), + array( 'ab-123-abcd', 'invalid order of extensions' ), + array( 'ab-abcde-abcd', 'invalid extensions' ), + array( 'ab-1abc-abcd', 'invalid characters in extensions' ), + array( 'ab-a-b', 'extensions too short' ), + array( 'ab-a-x', 'extensions too short, even with singleton' ), + array( 'ab--ab', 'two separators' ), + array( 'ab-abc-', 'separator in the end' ), + array( '-ab-abc', 'separator in the beginning' ), + array( 'abcd-efg', 'language too long' ), + array( 'aabbccddE', 'tag too long' ), + array( 'pa_guru', 'A tag with underscore is invalid in strict mode' ), + array( 'de-f', 'subtag too short' ), + ); + } + + /** + * Negative test for Language::isWellFormedLanguageTag() + * @covers Language::isWellFormedLanguageTag + */ + public function testLenientLanguageTag() { + $this->assertTrue( + Language::isWellFormedLanguageTag( 'pa_guru', true ), + 'pa_guru is a well-formed language tag in lenient mode' + ); + } + + /** + * Test Language::isValidBuiltInCode() + * @dataProvider provideLanguageCodes + * @covers Language::isValidBuiltInCode + */ + public function testBuiltInCodeValidation( $code, $message = '' ) { + $this->assertTrue( + (bool)Language::isValidBuiltInCode( $code ), + "validating code $code $message" + ); + } + + /** + * @covers Language::isValidBuiltInCode + */ + public function testBuiltInCodeValidationRejectUnderscore() { + $this->assertFalse( + (bool)Language::isValidBuiltInCode( 'be_tarask' ), + "reject underscore in language code" + ); + } + + public static function provideLanguageCodes() { + return array( + array( 'fr', 'Two letters, minor case' ), + array( 'EN', 'Two letters, upper case' ), + array( 'tyv', 'Three letters' ), + array( 'tokipona', 'long language code' ), + array( 'be-tarask', 'With dash' ), + array( 'Zh-classical', 'Begin with upper case, dash' ), + array( 'Be-x-old', 'With extension (two dashes)' ), + ); + } + + /** + * Test Language::isKnownLanguageTag() + * @dataProvider provideKnownLanguageTags + * @covers Language::isKnownLanguageTag + */ + public function testKnownLanguageTag( $code, $message = '' ) { + $this->assertTrue( + (bool)Language::isKnownLanguageTag( $code ), + "validating code $code - $message" + ); + } + + public static function provideKnownLanguageTags() { + return array( + array( 'fr', 'simple code' ), + array( 'bat-smg', 'an MW legacy tag' ), + array( 'sgs', 'an internal standard MW name, for which a legacy tag is used externally' ), + ); + } + + /** + * @covers Language::isKnownLanguageTag + */ + public function testKnownCldrLanguageTag() { + if ( !class_exists( 'LanguageNames' ) ) { + $this->markTestSkipped( 'The LanguageNames class is not available. The cldr extension is probably not installed.' ); + } + + $this->assertTrue( + (bool)Language::isKnownLanguageTag( 'pal' ), + 'validating code "pal" an ancient language, which probably will not appear in Names.php, but appears in CLDR in English' + ); + } + + /** + * Negative tests for Language::isKnownLanguageTag() + * @dataProvider provideUnKnownLanguageTags + * @covers Language::isKnownLanguageTag + */ + public function testUnknownLanguageTag( $code, $message = '' ) { + $this->assertFalse( + (bool)Language::isKnownLanguageTag( $code ), + "checking that code $code is invalid - $message" + ); + } + + public static function provideUnknownLanguageTags() { + return array( + array( 'mw', 'non-existent two-letter code' ), + array( 'foo"<bar', 'very invalid language code' ), + ); + } + + /** + * Test too short timestamp + * @expectedException MWException + * @covers Language::sprintfDate + */ + public function testSprintfDateTooShortTimestamp() { + $this->getLang()->sprintfDate( 'xiY', '1234567890123' ); + } + + /** + * Test too long timestamp + * @expectedException MWException + * @covers Language::sprintfDate + */ + public function testSprintfDateTooLongTimestamp() { + $this->getLang()->sprintfDate( 'xiY', '123456789012345' ); + } + + /** + * Test too short timestamp + * @expectedException MWException + * @covers Language::sprintfDate + */ + public function testSprintfDateNotAllDigitTimestamp() { + $this->getLang()->sprintfDate( 'xiY', '-1234567890123' ); + } + + /** + * @dataProvider provideSprintfDateSamples + * @covers Language::sprintfDate + */ + public function testSprintfDate( $format, $ts, $expected, $msg ) { + $this->assertEquals( + $expected, + $this->getLang()->sprintfDate( $format, $ts ), + "sprintfDate('$format', '$ts'): $msg" + ); + } + + /** + * sprintfDate should always use UTC when no zone is given. + * @dataProvider provideSprintfDateSamples + * @covers Language::sprintfDate + */ + public function testSprintfDateNoZone( $format, $ts, $expected, $ignore, $msg ) { + $oldTZ = date_default_timezone_get(); + $res = date_default_timezone_set( 'Asia/Seoul' ); + if ( !$res ) { + $this->markTestSkipped( "Error setting Timezone" ); + } + + $this->assertEquals( + $expected, + $this->getLang()->sprintfDate( $format, $ts ), + "sprintfDate('$format', '$ts'): $msg" + ); + + date_default_timezone_set( $oldTZ ); + } + + /** + * sprintfDate should use passed timezone + * @dataProvider provideSprintfDateSamples + * @covers Language::sprintfDate + */ + public function testSprintfDateTZ( $format, $ts, $ignore, $expected, $msg ) { + $tz = new DateTimeZone( 'Asia/Seoul' ); + if ( !$tz ) { + $this->markTestSkipped( "Error getting Timezone" ); + } + + $this->assertEquals( + $expected, + $this->getLang()->sprintfDate( $format, $ts, $tz ), + "sprintfDate('$format', '$ts', 'Asia/Seoul'): $msg" + ); + } + + public static function provideSprintfDateSamples() { + return array( + array( + 'xiY', + '20111212000000', + '1390', // note because we're testing English locale we get Latin-standard digits + '1390', + 'Iranian calendar full year' + ), + array( + 'xiy', + '20111212000000', + '90', + '90', + 'Iranian calendar short year' + ), + array( + 'o', + '20120101235000', + '2011', + '2011', + 'ISO 8601 (week) year' + ), + array( + 'W', + '20120101235000', + '52', + '52', + 'Week number' + ), + array( + 'W', + '20120102235000', + '1', + '1', + 'Week number' + ), + array( + 'o-\\WW-N', + '20091231235000', + '2009-W53-4', + '2009-W53-4', + 'leap week' + ), + // What follows is mostly copied from http://www.mediawiki.org/wiki/Help:Extension:ParserFunctions#.23time + array( + 'Y', + '20120102090705', + '2012', + '2012', + 'Full year' + ), + array( + 'y', + '20120102090705', + '12', + '12', + '2 digit year' + ), + array( + 'L', + '20120102090705', + '1', + '1', + 'Leap year' + ), + array( + 'n', + '20120102090705', + '1', + '1', + 'Month index, not zero pad' + ), + array( + 'N', + '20120102090705', + '01', + '01', + 'Month index. Zero pad' + ), + array( + 'M', + '20120102090705', + 'Jan', + 'Jan', + 'Month abbrev' + ), + array( + 'F', + '20120102090705', + 'January', + 'January', + 'Full month' + ), + array( + 'xg', + '20120102090705', + 'January', + 'January', + 'Genitive month name (same in EN)' + ), + array( + 'j', + '20120102090705', + '2', + '2', + 'Day of month (not zero pad)' + ), + array( + 'd', + '20120102090705', + '02', + '02', + 'Day of month (zero-pad)' + ), + array( + 'z', + '20120102090705', + '1', + '1', + 'Day of year (zero-indexed)' + ), + array( + 'D', + '20120102090705', + 'Mon', + 'Mon', + 'Day of week (abbrev)' + ), + array( + 'l', + '20120102090705', + 'Monday', + 'Monday', + 'Full day of week' + ), + array( + 'N', + '20120101090705', + '7', + '7', + 'Day of week (Mon=1, Sun=7)' + ), + array( + 'w', + '20120101090705', + '0', + '0', + 'Day of week (Sun=0, Sat=6)' + ), + array( + 'N', + '20120102090705', + '1', + '1', + 'Day of week' + ), + array( + 'a', + '20120102090705', + 'am', + 'am', + 'am vs pm' + ), + array( + 'A', + '20120102120000', + 'PM', + 'PM', + 'AM vs PM' + ), + array( + 'a', + '20120102000000', + 'am', + 'am', + 'AM vs PM' + ), + array( + 'g', + '20120102090705', + '9', + '9', + '12 hour, not Zero' + ), + array( + 'h', + '20120102090705', + '09', + '09', + '12 hour, zero padded' + ), + array( + 'G', + '20120102090705', + '9', + '9', + '24 hour, not zero' + ), + array( + 'H', + '20120102090705', + '09', + '09', + '24 hour, zero' + ), + array( + 'H', + '20120102110705', + '11', + '11', + '24 hour, zero' + ), + array( + 'i', + '20120102090705', + '07', + '07', + 'Minutes' + ), + array( + 's', + '20120102090705', + '05', + '05', + 'seconds' + ), + array( + 'U', + '20120102090705', + '1325495225', + '1325462825', + 'unix time' + ), + array( + 't', + '20120102090705', + '31', + '31', + 'Days in current month' + ), + array( + 'c', + '20120102090705', + '2012-01-02T09:07:05+00:00', + '2012-01-02T09:07:05+09:00', + 'ISO 8601 timestamp' + ), + array( + 'r', + '20120102090705', + 'Mon, 02 Jan 2012 09:07:05 +0000', + 'Mon, 02 Jan 2012 09:07:05 +0900', + 'RFC 5322' + ), + array( + 'e', + '20120102090705', + 'UTC', + 'Asia/Seoul', + 'Timezone identifier' + ), + array( + 'I', + '19880602090705', + '0', + '1', + 'DST indicator' + ), + array( + 'O', + '20120102090705', + '+0000', + '+0900', + 'Timezone offset' + ), + array( + 'P', + '20120102090705', + '+00:00', + '+09:00', + 'Timezone offset with colon' + ), + array( + 'T', + '20120102090705', + 'UTC', + 'KST', + 'Timezone abbreviation' + ), + array( + 'Z', + '20120102090705', + '0', + '32400', + 'Timezone offset in seconds' + ), + array( + 'xmj xmF xmn xmY', + '20120102090705', + '7 Safar 2 1433', + '7 Safar 2 1433', + 'Islamic' + ), + array( + 'xij xiF xin xiY', + '20120102090705', + '12 Dey 10 1390', + '12 Dey 10 1390', + 'Iranian' + ), + array( + 'xjj xjF xjn xjY', + '20120102090705', + '7 Tevet 4 5772', + '7 Tevet 4 5772', + 'Hebrew' + ), + array( + 'xjt', + '20120102090705', + '29', + '29', + 'Hebrew number of days in month' + ), + array( + 'xjx', + '20120102090705', + 'Tevet', + 'Tevet', + 'Hebrew genitive month name (No difference in EN)' + ), + array( + 'xkY', + '20120102090705', + '2555', + '2555', + 'Thai year' + ), + array( + 'xoY', + '20120102090705', + '101', + '101', + 'Minguo' + ), + array( + 'xtY', + '20120102090705', + '平成24', + '平成24', + 'nengo' + ), + array( + 'xrxkYY', + '20120102090705', + 'MMDLV2012', + 'MMDLV2012', + 'Roman numerals' + ), + array( + 'xhxjYY', + '20120102090705', + 'ה\'תשע"ב2012', + 'ה\'תשע"ב2012', + 'Hebrew numberals' + ), + array( + 'xnY', + '20120102090705', + '2012', + '2012', + 'Raw numerals (doesn\'t mean much in EN)' + ), + array( + '[[Y "(yea"\\r)]] \\"xx\\"', + '20120102090705', + '[[2012 (year)]] "x"', + '[[2012 (year)]] "x"', + 'Various escaping' + ), + + ); + } + + /** + * @dataProvider provideFormatSizes + * @covers Language::formatSize + */ + public function testFormatSize( $size, $expected, $msg ) { + $this->assertEquals( + $expected, + $this->getLang()->formatSize( $size ), + "formatSize('$size'): $msg" + ); + } + + public static function provideFormatSizes() { + return array( + array( + 0, + "0 B", + "Zero bytes" + ), + array( + 1024, + "1 KB", + "1 kilobyte" + ), + array( + 1024 * 1024, + "1 MB", + "1,024 megabytes" + ), + array( + 1024 * 1024 * 1024, + "1 GB", + "1 gigabytes" + ), + array( + pow( 1024, 4 ), + "1 TB", + "1 terabyte" + ), + array( + pow( 1024, 5 ), + "1 PB", + "1 petabyte" + ), + array( + pow( 1024, 6 ), + "1 EB", + "1,024 exabyte" + ), + array( + pow( 1024, 7 ), + "1 ZB", + "1 zetabyte" + ), + array( + pow( 1024, 8 ), + "1 YB", + "1 yottabyte" + ), + // How big!? THIS BIG! + ); + } + + /** + * @dataProvider provideFormatBitrate + * @covers Language::formatBitrate + */ + public function testFormatBitrate( $bps, $expected, $msg ) { + $this->assertEquals( + $expected, + $this->getLang()->formatBitrate( $bps ), + "formatBitrate('$bps'): $msg" + ); + } + + public static function provideFormatBitrate() { + return array( + array( + 0, + "0 bps", + "0 bits per second" + ), + array( + 999, + "999 bps", + "999 bits per second" + ), + array( + 1000, + "1 kbps", + "1 kilobit per second" + ), + array( + 1000 * 1000, + "1 Mbps", + "1 megabit per second" + ), + array( + pow( 10, 9 ), + "1 Gbps", + "1 gigabit per second" + ), + array( + pow( 10, 12 ), + "1 Tbps", + "1 terabit per second" + ), + array( + pow( 10, 15 ), + "1 Pbps", + "1 petabit per second" + ), + array( + pow( 10, 18 ), + "1 Ebps", + "1 exabit per second" + ), + array( + pow( 10, 21 ), + "1 Zbps", + "1 zetabit per second" + ), + array( + pow( 10, 24 ), + "1 Ybps", + "1 yottabit per second" + ), + array( + pow( 10, 27 ), + "1,000 Ybps", + "1,000 yottabits per second" + ), + ); + } + + + /** + * @dataProvider provideFormatDuration + * @covers Language::formatDuration + */ + public function testFormatDuration( $duration, $expected, $intervals = array() ) { + $this->assertEquals( + $expected, + $this->getLang()->formatDuration( $duration, $intervals ), + "formatDuration('$duration'): $expected" + ); + } + + public static function provideFormatDuration() { + return array( + array( + 0, + '0 seconds', + ), + array( + 1, + '1 second', + ), + array( + 2, + '2 seconds', + ), + array( + 60, + '1 minute', + ), + array( + 2 * 60, + '2 minutes', + ), + array( + 3600, + '1 hour', + ), + array( + 2 * 3600, + '2 hours', + ), + array( + 24 * 3600, + '1 day', + ), + array( + 2 * 86400, + '2 days', + ), + array( + // ( 365 + ( 24 * 3 + 25 ) / 400 ) * 86400 = 31556952 + ( 365 + ( 24 * 3 + 25 ) / 400.0 ) * 86400, + '1 year', + ), + array( + 2 * 31556952, + '2 years', + ), + array( + 10 * 31556952, + '1 decade', + ), + array( + 20 * 31556952, + '2 decades', + ), + array( + 100 * 31556952, + '1 century', + ), + array( + 200 * 31556952, + '2 centuries', + ), + array( + 1000 * 31556952, + '1 millennium', + ), + array( + 2000 * 31556952, + '2 millennia', + ), + array( + 9001, + '2 hours, 30 minutes and 1 second' + ), + array( + 3601, + '1 hour and 1 second' + ), + array( + 31556952 + 2 * 86400 + 9000, + '1 year, 2 days, 2 hours and 30 minutes' + ), + array( + 42 * 1000 * 31556952 + 42, + '42 millennia and 42 seconds' + ), + array( + 60, + '60 seconds', + array( 'seconds' ), + ), + array( + 61, + '61 seconds', + array( 'seconds' ), + ), + array( + 1, + '1 second', + array( 'seconds' ), + ), + array( + 31556952 + 2 * 86400 + 9000, + '1 year, 2 days and 150 minutes', + array( 'years', 'days', 'minutes' ), + ), + array( + 42, + '0 days', + array( 'years', 'days' ), + ), + array( + 31556952 + 2 * 86400 + 9000, + '1 year, 2 days and 150 minutes', + array( 'minutes', 'days', 'years' ), + ), + array( + 42, + '0 days', + array( 'days', 'years' ), + ), + ); + } + + /** + * @dataProvider provideCheckTitleEncodingData + * @covers Language::checkTitleEncoding + */ + public function testCheckTitleEncoding( $s ) { + $this->assertEquals( + $s, + $this->getLang()->checkTitleEncoding( $s ), + "checkTitleEncoding('$s')" + ); + } + + public static function provideCheckTitleEncodingData() { + return array( + array( "" ), + array( "United States of America" ), // 7bit ASCII + array( rawurldecode( "S%C3%A9rie%20t%C3%A9l%C3%A9vis%C3%A9e" ) ), + array( + rawurldecode( + "Acteur%7CAlbert%20Robbins%7CAnglais%7CAnn%20Donahue%7CAnthony%20E.%20Zuiker%7CCarol%20Mendelsohn" + ) + ), + // The following two data sets come from bug 36839. They fail if checkTitleEncoding uses a regexp to test for + // valid UTF-8 encoding and the pcre.recursion_limit is low (like, say, 1024). They succeed if checkTitleEncoding + // uses mb_check_encoding for its test. + array( + rawurldecode( + "Acteur%7CAlbert%20Robbins%7CAnglais%7CAnn%20Donahue%7CAnthony%20E.%20Zuiker%7CCarol%20Mendelsohn%7C" + . "Catherine%20Willows%7CDavid%20Hodges%7CDavid%20Phillips%7CGil%20Grissom%7CGreg%20Sanders%7CHodges%7C" + . "Internet%20Movie%20Database%7CJim%20Brass%7CLady%20Heather%7C" + . "Les%20Experts%20(s%C3%A9rie%20t%C3%A9l%C3%A9vis%C3%A9e)%7CLes%20Experts%20:%20Manhattan%7C" + . "Les%20Experts%20:%20Miami%7CListe%20des%20personnages%20des%20Experts%7C" + . "Liste%20des%20%C3%A9pisodes%20des%20Experts%7CMod%C3%A8le%20discussion:Palette%20Les%20Experts%7C" + . "Nick%20Stokes%7CPersonnage%20de%20fiction%7CPersonnage%20fictif%7CPersonnage%20de%20fiction%7C" + . "Personnages%20r%C3%A9currents%20dans%20Les%20Experts%7CRaymond%20Langston%7CRiley%20Adams%7C" + . "Saison%201%20des%20Experts%7CSaison%2010%20des%20Experts%7CSaison%2011%20des%20Experts%7C" + . "Saison%2012%20des%20Experts%7CSaison%202%20des%20Experts%7CSaison%203%20des%20Experts%7C" + . "Saison%204%20des%20Experts%7CSaison%205%20des%20Experts%7CSaison%206%20des%20Experts%7C" + . "Saison%207%20des%20Experts%7CSaison%208%20des%20Experts%7CSaison%209%20des%20Experts%7C" + . "Sara%20Sidle%7CSofia%20Curtis%7CS%C3%A9rie%20t%C3%A9l%C3%A9vis%C3%A9e%7CWallace%20Langham%7C" + . "Warrick%20Brown%7CWendy%20Simms%7C%C3%89tats-Unis" + ), + ), + array( + rawurldecode( + "Mod%C3%A8le%3AArrondissements%20homonymes%7CMod%C3%A8le%3ABandeau%20standard%20pour%20page%20d'homonymie%7C" + . "Mod%C3%A8le%3ABatailles%20homonymes%7CMod%C3%A8le%3ACantons%20homonymes%7C" + . "Mod%C3%A8le%3ACommunes%20fran%C3%A7aises%20homonymes%7CMod%C3%A8le%3AFilms%20homonymes%7C" + . "Mod%C3%A8le%3AGouvernements%20homonymes%7CMod%C3%A8le%3AGuerres%20homonymes%7CMod%C3%A8le%3AHomonymie%7C" + . "Mod%C3%A8le%3AHomonymie%20bateau%7CMod%C3%A8le%3AHomonymie%20d'%C3%A9tablissements%20scolaires%20ou" + . "%20universitaires%7CMod%C3%A8le%3AHomonymie%20d'%C3%AEles%7CMod%C3%A8le%3AHomonymie%20de%20clubs%20sportifs%7C" + . "Mod%C3%A8le%3AHomonymie%20de%20comt%C3%A9s%7CMod%C3%A8le%3AHomonymie%20de%20monument%7C" + . "Mod%C3%A8le%3AHomonymie%20de%20nom%20romain%7CMod%C3%A8le%3AHomonymie%20de%20parti%20politique%7C" + . "Mod%C3%A8le%3AHomonymie%20de%20route%7CMod%C3%A8le%3AHomonymie%20dynastique%7C" + . "Mod%C3%A8le%3AHomonymie%20vid%C3%A9oludique%7CMod%C3%A8le%3AHomonymie%20%C3%A9difice%20religieux%7C" + . "Mod%C3%A8le%3AInternationalisation%7CMod%C3%A8le%3AIsom%C3%A9rie%7CMod%C3%A8le%3AParonymie%7C" + . "Mod%C3%A8le%3APatronyme%7CMod%C3%A8le%3APatronyme%20basque%7CMod%C3%A8le%3APatronyme%20italien%7C" + . "Mod%C3%A8le%3APatronymie%7CMod%C3%A8le%3APersonnes%20homonymes%7CMod%C3%A8le%3ASaints%20homonymes%7C" + . "Mod%C3%A8le%3ATitres%20homonymes%7CMod%C3%A8le%3AToponymie%7CMod%C3%A8le%3AUnit%C3%A9s%20homonymes%7C" + . "Mod%C3%A8le%3AVilles%20homonymes%7CMod%C3%A8le%3A%C3%89difices%20religieux%20homonymes" + ) + ) + ); + } + + /** + * @dataProvider provideRomanNumeralsData + * @covers Language::romanNumeral + */ + public function testRomanNumerals( $num, $numerals ) { + $this->assertEquals( + $numerals, + Language::romanNumeral( $num ), + "romanNumeral('$num')" + ); + } + + public static function provideRomanNumeralsData() { + return array( + array( 1, 'I' ), + array( 2, 'II' ), + array( 3, 'III' ), + array( 4, 'IV' ), + array( 5, 'V' ), + array( 6, 'VI' ), + array( 7, 'VII' ), + array( 8, 'VIII' ), + array( 9, 'IX' ), + array( 10, 'X' ), + array( 20, 'XX' ), + array( 30, 'XXX' ), + array( 40, 'XL' ), + array( 49, 'XLIX' ), + array( 50, 'L' ), + array( 60, 'LX' ), + array( 70, 'LXX' ), + array( 80, 'LXXX' ), + array( 90, 'XC' ), + array( 99, 'XCIX' ), + array( 100, 'C' ), + array( 200, 'CC' ), + array( 300, 'CCC' ), + array( 400, 'CD' ), + array( 500, 'D' ), + array( 600, 'DC' ), + array( 700, 'DCC' ), + array( 800, 'DCCC' ), + array( 900, 'CM' ), + array( 999, 'CMXCIX' ), + array( 1000, 'M' ), + array( 1989, 'MCMLXXXIX' ), + array( 2000, 'MM' ), + array( 3000, 'MMM' ), + array( 4000, 'MMMM' ), + array( 5000, 'MMMMM' ), + array( 6000, 'MMMMMM' ), + array( 7000, 'MMMMMMM' ), + array( 8000, 'MMMMMMMM' ), + array( 9000, 'MMMMMMMMM' ), + array( 9999, 'MMMMMMMMMCMXCIX' ), + array( 10000, 'MMMMMMMMMM' ), + ); + } + + /** + * @dataProvider providePluralData + * @covers Language::convertPlural + */ + public function testConvertPlural( $expected, $number, $forms ) { + $chosen = $this->getLang()->convertPlural( $number, $forms ); + $this->assertEquals( $expected, $chosen ); + } + + public static function providePluralData() { + // Params are: [expected text, number given, [the plural forms]] + return array( + array( 'plural', 0, array( + 'singular', 'plural' + ) ), + array( 'explicit zero', 0, array( + '0=explicit zero', 'singular', 'plural' + ) ), + array( 'explicit one', 1, array( + 'singular', 'plural', '1=explicit one', + ) ), + array( 'singular', 1, array( + 'singular', 'plural', '0=explicit zero', + ) ), + array( 'plural', 3, array( + '0=explicit zero', '1=explicit one', 'singular', 'plural' + ) ), + array( 'explicit eleven', 11, array( + 'singular', 'plural', '11=explicit eleven', + ) ), + array( 'plural', 12, array( + 'singular', 'plural', '11=explicit twelve', + ) ), + array( 'plural', 12, array( + 'singular', 'plural', '=explicit form', + ) ), + array( 'other', 2, array( + 'kissa=kala', '1=2=3', 'other', + ) ), + array( '', 2, array( + '0=explicit zero', '1=explicit one', + ) ), + ); + } + + /** + * @covers Language::translateBlockExpiry() + * @dataProvider provideTranslateBlockExpiry + */ + public function testTranslateBlockExpiry( $expectedData, $str, $desc ) { + $lang = $this->getLang(); + if ( is_array( $expectedData ) ) { + list( $func, $arg ) = $expectedData; + $expected = $lang->$func( $arg ); + } else { + $expected = $expectedData; + } + $this->assertEquals( $expected, $lang->translateBlockExpiry( $str ), $desc ); + } + + public static function provideTranslateBlockExpiry() { + return array( + array( '2 hours', '2 hours', 'simple data from ipboptions' ), + array( 'indefinite', 'infinite', 'infinite from ipboptions' ), + array( 'indefinite', 'infinity', 'alternative infinite from ipboptions' ), + array( 'indefinite', 'indefinite', 'another alternative infinite from ipboptions' ), + array( array( 'formatDuration', 1023 * 60 * 60 ), '1023 hours', 'relative' ), + array( array( 'formatDuration', -1023 ), '-1023 seconds', 'negative relative' ), + array( array( 'formatDuration', 0 ), 'now', 'now' ), + array( array( 'timeanddate', '20120102070000' ), '2012-1-1 7:00 +1 day', 'mixed, handled as absolute' ), + array( array( 'timeanddate', '19910203040506' ), '1991-2-3 4:05:06', 'absolute' ), + array( array( 'timeanddate', '19700101000000' ), '1970-1-1 0:00:00', 'absolute at epoch' ), + array( array( 'timeanddate', '19691231235959' ), '1969-12-31 23:59:59', 'time before epoch' ), + array( 'dummy', 'dummy', 'return garbage as is' ), + ); + } + + /** + * @covers Language::commafy() + * @dataProvider provideCommafyData + */ + public function testCommafy( $number, $numbersWithCommas ) { + $this->assertEquals( + $numbersWithCommas, + $this->getLang()->commafy( $number ), + "commafy('$number')" + ); + } + + public static function provideCommafyData() { + return array( + array( 1, '1' ), + array( 10, '10' ), + array( 100, '100' ), + array( 1000, '1,000' ), + array( 10000, '10,000' ), + array( 100000, '100,000' ), + array( 1000000, '1,000,000' ), + array( 1.0001, '1.0001' ), + array( 10.0001, '10.0001' ), + array( 100.0001, '100.0001' ), + array( 1000.0001, '1,000.0001' ), + array( 10000.0001, '10,000.0001' ), + array( 100000.0001, '100,000.0001' ), + array( 1000000.0001, '1,000,000.0001' ), + ); + } + + /** + * @covers Language::listToText + */ + public function testListToText() { + $lang = $this->getLang(); + $and = $lang->getMessageFromDB( 'and' ); + $s = $lang->getMessageFromDB( 'word-separator' ); + $c = $lang->getMessageFromDB( 'comma-separator' ); + + $this->assertEquals( '', $lang->listToText( array() ) ); + $this->assertEquals( 'a', $lang->listToText( array( 'a' ) ) ); + $this->assertEquals( "a{$and}{$s}b", $lang->listToText( array( 'a', 'b' ) ) ); + $this->assertEquals( "a{$c}b{$and}{$s}c", $lang->listToText( array( 'a', 'b', 'c' ) ) ); + $this->assertEquals( "a{$c}b{$c}c{$and}{$s}d", $lang->listToText( array( 'a', 'b', 'c', 'd' ) ) ); + } + + /** + * @dataProvider provideIsSupportedLanguage + * @covers Language::isSupportedLanguage + */ + public function testIsSupportedLanguage( $code, $expected, $comment ) { + $this->assertEquals( $expected, Language::isSupportedLanguage( $code ), $comment ); + } + + public static function provideIsSupportedLanguage() { + return array( + array( 'en', true, 'is supported language' ), + array( 'fi', true, 'is supported language' ), + array( 'bunny', false, 'is not supported language' ), + array( 'FI', false, 'is not supported language, input should be in lower case' ), + ); + } + + /** + * @dataProvider provideGetParentLanguage + * @covers Language::getParentLanguage + */ + public function testGetParentLanguage( $code, $expected, $comment ) { + $lang = Language::factory( $code ); + if ( is_null( $expected ) ) { + $this->assertNull( $lang->getParentLanguage(), $comment ); + } else { + $this->assertEquals( $expected, $lang->getParentLanguage()->getCode(), $comment ); + } + } + + public static function provideGetParentLanguage() { + return array( + array( 'zh-cn', 'zh', 'zh is the parent language of zh-cn' ), + array( 'zh', 'zh', 'zh is defined as the parent language of zh, because zh converter can convert zh-cn to zh' ), + array( 'zh-invalid', null, 'do not be fooled by arbitrarily composed language codes' ), + array( 'en-gb', null, 'en does not have converter' ), + array( 'en', null, 'en does not have converter. Although FakeConverter handles en -> en conversion but it is useless' ), + ); + } + + /** + * @dataProvider provideGetNamespaceAliases + * @covers Language::getNamespaceAliases + */ + public function testGetNamespaceAliases( $languageCode, $subset ) { + $language = Language::factory( $languageCode ); + $aliases = $language->getNamespaceAliases(); + foreach ( $subset as $alias => $nsId ) { + $this->assertEquals( $nsId, $aliases[$alias] ); + } + } + + public static function provideGetNamespaceAliases() { + // TODO: Add tests for NS_PROJECT_TALK and GenderNamespaces + return array( + array( + 'zh', + array( + '文件' => NS_FILE, + '檔案' => NS_FILE, + ), + ), + ); + } +} diff --git a/tests/phpunit/languages/LanguageTiTest.php b/tests/phpunit/languages/LanguageTiTest.php new file mode 100644 index 00000000..e225af97 --- /dev/null +++ b/tests/phpunit/languages/LanguageTiTest.php @@ -0,0 +1,34 @@ +<?php +/** + * @author Amir E. Aharoni + * @copyright Copyright © 2012, Amir E. Aharoni + * @file + */ + +/** Tests for MediaWiki languages/classes/LanguageTi.php */ +class LanguageTiTest extends LanguageClassesTestCase { + /** + * @dataProvider providePlural + * @covers Language::convertPlural + */ + public function testPlural( $result, $value ) { + $forms = array( 'one', 'other' ); + $this->assertEquals( $result, $this->getLang()->convertPlural( $value, $forms ) ); + } + + /** + * @dataProvider providePlural + * @covers Language::getPluralRuleType + */ + public function testGetPluralRuleType( $result, $value ) { + $this->assertEquals( $result, $this->getLang()->getPluralRuleType( $value ) ); + } + + public static function providePlural() { + return array( + array( 'one', 0 ), + array( 'one', 1 ), + array( 'other', 2 ), + ); + } +} diff --git a/tests/phpunit/languages/LanguageTlTest.php b/tests/phpunit/languages/LanguageTlTest.php new file mode 100644 index 00000000..7ac51c69 --- /dev/null +++ b/tests/phpunit/languages/LanguageTlTest.php @@ -0,0 +1,34 @@ +<?php +/** + * @author Amir E. Aharoni + * @copyright Copyright © 2012, Amir E. Aharoni + * @file + */ + +/** Tests for MediaWiki languages/classes/LanguageTl.php */ +class LanguageTlTest extends LanguageClassesTestCase { + /** + * @dataProvider providePlural + * @covers Language::convertPlural + */ + public function testPlural( $result, $value ) { + $forms = array( 'one', 'other' ); + $this->assertEquals( $result, $this->getLang()->convertPlural( $value, $forms ) ); + } + + /** + * @dataProvider providePlural + * @covers Language::getPluralRuleType + */ + public function testGetPluralRuleType( $result, $value ) { + $this->assertEquals( $result, $this->getLang()->getPluralRuleType( $value ) ); + } + + public static function providePlural() { + return array( + array( 'one', 0 ), + array( 'one', 1 ), + array( 'other', 2 ), + ); + } +} diff --git a/tests/phpunit/languages/LanguageTrTest.php b/tests/phpunit/languages/LanguageTrTest.php new file mode 100644 index 00000000..8fc2795c --- /dev/null +++ b/tests/phpunit/languages/LanguageTrTest.php @@ -0,0 +1,59 @@ +<?php +/** + * @author Antoine Musso + * @copyright Copyright © 2011, Antoine Musso + * @file + */ + +/** Tests for MediaWiki languages/LanguageTr.php */ +class LanguageTrTest extends LanguageClassesTestCase { + + /** + * See @bug 28040 + * Credits to irc://irc.freenode.net/wikipedia-tr users: + * - berm + * - []LuCkY[] + * - Emperyan + * @see http://en.wikipedia.org/wiki/Dotted_and_dotless_I + * @dataProvider provideDottedAndDotlessI + */ + public function testDottedAndDotlessI( $func, $input, $inputCase, $expected ) { + if ( $func == 'ucfirst' ) { + $res = $this->getLang()->ucfirst( $input ); + } elseif ( $func == 'lcfirst' ) { + $res = $this->getLang()->lcfirst( $input ); + } else { + throw new MWException( __METHOD__ . " given an invalid function name '$func'" ); + } + + $msg = "Converting $inputCase case '$input' with $func should give '$expected'"; + + $this->assertEquals( $expected, $res, $msg ); + } + + public static function provideDottedAndDotlessI() { + return array( + # function, input, input case, expected + # Case changed: + array( 'ucfirst', 'ı', 'lower', 'I' ), + array( 'ucfirst', 'i', 'lower', 'İ' ), + array( 'lcfirst', 'I', 'upper', 'ı' ), + array( 'lcfirst', 'İ', 'upper', 'i' ), + + # Already using the correct case + array( 'ucfirst', 'I', 'upper', 'I' ), + array( 'ucfirst', 'İ', 'upper', 'İ' ), + array( 'lcfirst', 'ı', 'lower', 'ı' ), + array( 'lcfirst', 'i', 'lower', 'i' ), + + # A real example taken from bug 28040 using + # http://tr.wikipedia.org/wiki/%C4%B0Phone + array( 'lcfirst', 'iPhone', 'lower', 'iPhone' ), + + # next case is valid in Turkish but are different words if we + # consider IPhone is English! + array( 'lcfirst', 'IPhone', 'upper', 'ıPhone' ), + + ); + } +} diff --git a/tests/phpunit/languages/LanguageUkTest.php b/tests/phpunit/languages/LanguageUkTest.php new file mode 100644 index 00000000..9051bcff --- /dev/null +++ b/tests/phpunit/languages/LanguageUkTest.php @@ -0,0 +1,72 @@ +<?php +/** + * @author Amir E. Aharoni + * based on LanguageBe_tarask.php + * @copyright Copyright © 2012, Amir E. Aharoni + * @file + */ + +/** Tests for Ukrainian */ +class LanguageUkTest extends LanguageClassesTestCase { + /** + * @dataProvider providePlural + * @covers Language::convertPlural + */ + public function testPlural( $result, $value ) { + $forms = array( 'one', 'few', 'many', 'other' ); + $this->assertEquals( $result, $this->getLang()->convertPlural( $value, $forms ) ); + } + + /** + * Test explicit plural forms - n=FormN forms + * @covers Language::convertPlural + */ + public function testExplicitPlural() { + $forms = array( 'one', 'few', 'many', 'other', '12=dozen' ); + $this->assertEquals( 'dozen', $this->getLang()->convertPlural( 12, $forms ) ); + $forms = array( 'one', 'few', 'many', '100=hundred', 'other', '12=dozen' ); + $this->assertEquals( 'hundred', $this->getLang()->convertPlural( 100, $forms ) ); + } + + /** + * @dataProvider providePlural + * @covers Language::getPluralRuleType + */ + public function testGetPluralRuleType( $result, $value ) { + $this->assertEquals( $result, $this->getLang()->getPluralRuleType( $value ) ); + } + + public static function providePlural() { + return array( + array( 'one', 1 ), + array( 'many', 11 ), + array( 'one', 91 ), + array( 'one', 121 ), + array( 'few', 2 ), + array( 'few', 3 ), + array( 'few', 4 ), + array( 'few', 334 ), + array( 'many', 5 ), + array( 'many', 15 ), + array( 'many', 120 ), + ); + } + + /** + * @dataProvider providePluralTwoForms + * @covers Language::convertPlural + */ + public function testPluralTwoForms( $result, $value ) { + $forms = array( '1=one', 'other' ); + $this->assertEquals( $result, $this->getLang()->convertPlural( $value, $forms ) ); + } + + public static function providePluralTwoForms() { + return array( + array( 'one', 1 ), + array( 'other', 11 ), + array( 'other', 91 ), + array( 'other', 121 ), + ); + } +} diff --git a/tests/phpunit/languages/LanguageUzTest.php b/tests/phpunit/languages/LanguageUzTest.php new file mode 100644 index 00000000..13f57c16 --- /dev/null +++ b/tests/phpunit/languages/LanguageUzTest.php @@ -0,0 +1,121 @@ +<?php +/** + * PHPUnit tests for the Uzbek language. + * The language can be represented using two scripts: + * - Latin (uz-latn) + * - Cyrillic (uz-cyrl) + * + * @author Robin Pepermans + * @author Antoine Musso <hashar at free dot fr> + * @copyright Copyright © 2012, Robin Pepermans + * @copyright Copyright © 2011, Antoine Musso <hashar at free dot fr> + * @file + * + * @todo methods in test class should be tidied: + * - Should be split into separate test methods and data providers + * - Tests for LanguageConverter and Language should probably be separate.. + */ + +/** Tests for MediaWiki languages/LanguageUz.php */ +class LanguageUzTest extends LanguageClassesTestCase { + + /** + * @author Nikola Smolenski + * @covers LanguageConverter::convertTo + */ + public function testConversionToCyrillic() { + // A convertion of Latin to Cyrillic + $this->assertEquals( 'абвгғ', + $this->convertToCyrillic( 'abvggʻ' ) + ); + // Same as above, but assert that -{}-s must be removed and not converted + $this->assertEquals( 'ljабnjвгўоdb', + $this->convertToCyrillic( '-{lj}-ab-{nj}-vgoʻo-{db}-' ) + ); + // A simple convertion of Cyrillic to Cyrillic + $this->assertEquals( 'абвг', + $this->convertToCyrillic( 'абвг' ) + ); + // Same as above, but assert that -{}-s must be removed and not converted + $this->assertEquals( 'ljабnjвгdaž', + $this->convertToCyrillic( '-{lj}-аб-{nj}-вг-{da}-ž' ) + ); + } + + /** + * @covers LanguageConverter::convertTo + */ + public function testConversionToLatin() { + // A simple convertion of Latin to Latin + $this->assertEquals( 'abdef', + $this->convertToLatin( 'abdef' ) + ); + // A convertion of Cyrillic to Latin + $this->assertEquals( 'gʻabtsdOʻQyo', + $this->convertToLatin( 'ғабцдЎҚё' ) + ); + } + + ##### HELPERS ##################################################### + /** + * Wrapper to verify text stay the same after applying conversion + * @param $text string Text to convert + * @param $variant string Language variant 'uz-cyrl' or 'uz-latn' + * @param $msg string Optional message + */ + protected function assertUnConverted( $text, $variant, $msg = '' ) { + $this->assertEquals( + $text, + $this->convertTo( $text, $variant ), + $msg + ); + } + + /** + * Wrapper to verify a text is different once converted to a variant. + * @param $text string Text to convert + * @param $variant string Language variant 'uz-cyrl' or 'uz-latn' + * @param $msg string Optional message + */ + protected function assertConverted( $text, $variant, $msg = '' ) { + $this->assertNotEquals( + $text, + $this->convertTo( $text, $variant ), + $msg + ); + } + + /** + * Verifiy the given Cyrillic text is not converted when using + * using the cyrillic variant and converted to Latin when using + * the Latin variant. + */ + protected function assertCyrillic( $text, $msg = '' ) { + $this->assertUnConverted( $text, 'uz-cyrl', $msg ); + $this->assertConverted( $text, 'uz-latn', $msg ); + } + + /** + * Verifiy the given Latin text is not converted when using + * using the Latin variant and converted to Cyrillic when using + * the Cyrillic variant. + */ + protected function assertLatin( $text, $msg = '' ) { + $this->assertUnConverted( $text, 'uz-latn', $msg ); + $this->assertConverted( $text, 'uz-cyrl', $msg ); + } + + + /** Wrapper for converter::convertTo() method*/ + protected function convertTo( $text, $variant ) { + return $this->getLang()->mConverter->convertTo( $text, $variant ); + } + + protected function convertToCyrillic( $text ) { + return $this->convertTo( $text, 'uz-cyrl' ); + } + + protected function convertToLatin( $text ) { + return $this->convertTo( $text, 'uz-latn' ); + } +} diff --git a/tests/phpunit/languages/LanguageWaTest.php b/tests/phpunit/languages/LanguageWaTest.php new file mode 100644 index 00000000..d05196c0 --- /dev/null +++ b/tests/phpunit/languages/LanguageWaTest.php @@ -0,0 +1,34 @@ +<?php +/** + * @author Amir E. Aharoni + * @copyright Copyright © 2012, Amir E. Aharoni + * @file + */ + +/** Tests for MediaWiki languages/classes/LanguageWa.php */ +class LanguageWaTest extends LanguageClassesTestCase { + /** + * @dataProvider providePlural + * @covers Language::convertPlural + */ + public function testPlural( $result, $value ) { + $forms = array( 'one', 'other' ); + $this->assertEquals( $result, $this->getLang()->convertPlural( $value, $forms ) ); + } + + /** + * @dataProvider providePlural + * @covers Language::getPluralRuleType + */ + public function testGetPluralRuleType( $result, $value ) { + $this->assertEquals( $result, $this->getLang()->getPluralRuleType( $value ) ); + } + + public static function providePlural() { + return array( + array( 'one', 0 ), + array( 'one', 1 ), + array( 'other', 2 ), + ); + } +} diff --git a/tests/phpunit/languages/utils/CLDRPluralRuleEvaluatorTest.php b/tests/phpunit/languages/utils/CLDRPluralRuleEvaluatorTest.php new file mode 100644 index 00000000..bd3809d7 --- /dev/null +++ b/tests/phpunit/languages/utils/CLDRPluralRuleEvaluatorTest.php @@ -0,0 +1,145 @@ +<?php +/** + * @author Niklas Laxström + * @file + */ + +class CLDRPluralRuleEvaluatorTest extends MediaWikiTestCase { + /** + * @dataProvider validTestCases + */ + function testValidRules( $expected, $rules, $number, $comment ) { + $result = CLDRPluralRuleEvaluator::evaluate( $number, (array)$rules ); + $this->assertEquals( $expected, $result, $comment ); + } + + /** + * @dataProvider invalidTestCases + * @expectedException CLDRPluralRuleError + */ + function testInvalidRules( $rules, $comment ) { + CLDRPluralRuleEvaluator::evaluate( 1, (array)$rules ); + } + + function validTestCases() { + $tests = array( + # expected, number, rule, comment + array( 0, 'n is 1', 1, 'integer number and is' ), + array( 0, 'n is 1', "1", 'string integer number and is' ), + array( 0, 'n is 1', 1.0, 'float number and is' ), + array( 0, 'n is 1', "1.0", 'string float number and is' ), + array( 1, 'n is 1', 1.1, 'float number and is' ), + array( 1, 'n is 1', 2, 'float number and is' ), + + array( 0, 'n in 1,3,5', 3, '' ), + array( 1, 'n not in 1,3,5', 5, '' ), + + array( 1, 'n in 1,3,5', 2, '' ), + array( 0, 'n not in 1,3,5', 4, '' ), + + array( 0, 'n in 1..3', 2, '' ), + array( 0, 'n in 1..3', 3, 'in is inclusive' ), + array( 1, 'n in 1..3', 0, '' ), + + array( 1, 'n not in 1..3', 2, '' ), + array( 1, 'n not in 1..3', 3, 'in is inclusive' ), + array( 0, 'n not in 1..3', 0, '' ), + + array( 1, 'n is not 1 and n is not 2 and n is not 3', 1, 'and relation' ), + array( 0, 'n is not 1 and n is not 2 and n is not 4', 3, 'and relation' ), + + array( 0, 'n is not 1 or n is 1', 1, 'or relation' ), + array( 1, 'n is 1 or n is 2', 3, 'or relation' ), + + array( 0, 'n is 1', 1, 'extra whitespace' ), + + array( 0, 'n mod 3 is 1', 7, 'mod' ), + array( 0, 'n mod 3 is not 1', 4.3, 'mod with floats' ), + + array( 0, 'n within 1..3', 2, 'within with integer' ), + array( 0, 'n within 1..3', 2.5, 'within with float' ), + array( 0, 'n in 1..3', 2, 'in with integer' ), + array( 1, 'n in 1..3', 2.5, 'in with float' ), + + array( 0, 'n in 3 or n is 4 and n is 5', 3, 'and binds more tightly than or' ), + array( 1, 'n is 3 or n is 4 and n is 5', 4, 'and binds more tightly than or' ), + + array( 0, 'n mod 10 in 3..4,9 and n mod 100 not in 10..19,70..79,90..99', 24, 'breton rule' ), + array( 1, 'n mod 10 in 3..4,9 and n mod 100 not in 10..19,70..79,90..99', 25, 'breton rule' ), + + array( 0, 'n within 0..2 and n is not 2', 0, 'french rule' ), + array( 0, 'n within 0..2 and n is not 2', 1, 'french rule' ), + array( 0, 'n within 0..2 and n is not 2', 1.2, 'french rule' ), + array( 1, 'n within 0..2 and n is not 2', 2, 'french rule' ), + + array( 1, 'n in 3..10,13..19', 2, 'scottish rule - ranges with comma' ), + array( 0, 'n in 3..10,13..19', 4, 'scottish rule - ranges with comma' ), + array( 1, 'n in 3..10,13..19', 12.999, 'scottish rule - ranges with comma' ), + array( 0, 'n in 3..10,13..19', 13, 'scottish rule - ranges with comma' ), + + array( 0, '5 mod 3 is n', 2, 'n as result of mod - no need to pass' ), + + # Revision 33 new operand examples + # expected, rule, number, comment + array( 0, 'i is 1', '1.00', 'new operand i' ), + array( 0, 'v is 2', '1.00', 'new operand v' ), + array( 0, 'w is 0', '1.00', 'new operand w' ), + array( 0, 'f is 0', '1.00', 'new operand f' ), + array( 0, 't is 0', '1.00', 'new operand t' ), + + array( 0, 'i is 1', '1.30', 'new operand i' ), + array( 0, 'v is 2', '1.30', 'new operand v' ), + array( 0, 'w is 1', '1.30', 'new operand w' ), + array( 0, 'f is 30', '1.30', 'new operand f' ), + array( 0, 't is 3', '1.30', 'new operand t' ), + + array( 0, 'i is 1', '1.03', 'new operand i' ), + array( 0, 'v is 2', '1.03', 'new operand v' ), + array( 0, 'w is 2', '1.03', 'new operand w' ), + array( 0, 'f is 3', '1.03', 'new operand f' ), + array( 0, 't is 3', '1.03', 'new operand t' ), + + # Revision 33 new operator aliases + # expected, rule, number, comment + array( 0, 'n % 3 is 1', 7, 'new % operator' ), + array( 0, 'n = 1,3,5', 3, 'new = operator' ), + array( 1, 'n != 1,3,5', 5, 'new != operator' ), + + # Revision 33 samples + # expected, rule, number, comment + array( 0, 'n in 1,3,5@integer 3~10, 103~110, 1003, … @decimal 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 103.0, 1003.0, …', 3, 'samples' ), + + # Revision 33 some test cases from CLDR + array( 0, 'i = 1 and v = 0 or i = 0 and t = 1', '0.1', 'pt one' ), + array( 0, 'i = 1 and v = 0 or i = 0 and t = 1', '0.01', 'pt one' ), + array( 0, 'i = 1 and v = 0 or i = 0 and t = 1', '0.10', 'pt one' ), + array( 0, 'i = 1 and v = 0 or i = 0 and t = 1', '0.010', 'pt one' ), + array( 0, 'i = 1 and v = 0 or i = 0 and t = 1', '0.100', 'pt one' ), + array( 1, 'i = 1 and v = 0 or i = 0 and t = 1', '0.0', 'pt other' ), + array( 1, 'i = 1 and v = 0 or i = 0 and t = 1', '0.2', 'pt other' ), + array( 1, 'i = 1 and v = 0 or i = 0 and t = 1', '10.0', 'pt other' ), + array( 1, 'i = 1 and v = 0 or i = 0 and t = 1', '100.0', 'pt other' ), + array( 0, 'v = 0 and i % 10 = 2..4 and i % 100 != 12..14 or f % 10 = 2..4 and f % 100 != 12..14', '2', 'bs few' ), + array( 0, 'v = 0 and i % 10 = 2..4 and i % 100 != 12..14 or f % 10 = 2..4 and f % 100 != 12..14', '4', 'bs few' ), + array( 0, 'v = 0 and i % 10 = 2..4 and i % 100 != 12..14 or f % 10 = 2..4 and f % 100 != 12..14', '22', 'bs few' ), + array( 0, 'v = 0 and i % 10 = 2..4 and i % 100 != 12..14 or f % 10 = 2..4 and f % 100 != 12..14', '102', 'bs few' ), + array( 0, 'v = 0 and i % 10 = 2..4 and i % 100 != 12..14 or f % 10 = 2..4 and f % 100 != 12..14', '0.2', 'bs few' ), + array( 0, 'v = 0 and i % 10 = 2..4 and i % 100 != 12..14 or f % 10 = 2..4 and f % 100 != 12..14', '0.4', 'bs few' ), + array( 0, 'v = 0 and i % 10 = 2..4 and i % 100 != 12..14 or f % 10 = 2..4 and f % 100 != 12..14', '10.2', 'bs few' ), + array( 1, 'v = 0 and i % 10 = 2..4 and i % 100 != 12..14 or f % 10 = 2..4 and f % 100 != 12..14', '10.0', 'bs other' ), + + ); + + return $tests; + } + + function invalidTestCases() { + $tests = array( + array( 'n mod mod 5 is 1', 'mod mod' ), + array( 'n', 'just n' ), + array( 'n is in 5', 'is in' ), + ); + + return $tests; + } +} diff --git a/tests/phpunit/maintenance/DumpTestCase.php b/tests/phpunit/maintenance/DumpTestCase.php new file mode 100644 index 00000000..83d8c71d --- /dev/null +++ b/tests/phpunit/maintenance/DumpTestCase.php @@ -0,0 +1,379 @@ +<?php + +/** + * Base TestCase for dumps + */ +abstract class DumpTestCase extends MediaWikiLangTestCase { + + /** + * exception to be rethrown once in sound PHPUnit surrounding + * + * As the current MediaWikiTestCase::run is not robust enough to recover + * from thrown exceptions directly, we cannot throw frow within + * self::addDBData, although it would be appropriate. Hence, we catch the + * exception and store it until we are in setUp and may finally rethrow + * the exception without crashing the test suite. + * + * @var Exception|null + */ + protected $exceptionFromAddDBData = null; + + /** + * Holds the xmlreader used for analyzing an xml dump + * + * @var XMLReader|null + */ + protected $xml = null; + + /** + * Adds a revision to a page, while returning the resuting revision's id + * + * @param $page WikiPage: page to add the revision to + * @param $text string: revisions text + * @param $text string: revisions summare + * + * @throws MWExcepion + */ + protected function addRevision( Page $page, $text, $summary ) { + $status = $page->doEditContent( ContentHandler::makeContent( $text, $page->getTitle() ), $summary ); + if ( $status->isGood() ) { + $value = $status->getValue(); + $revision = $value['revision']; + $revision_id = $revision->getId(); + $text_id = $revision->getTextId(); + if ( ( $revision_id > 0 ) && ( $text_id > 0 ) ) { + return array( $revision_id, $text_id ); + } + } + throw new MWException( "Could not determine revision id (" . $status->getWikiText() . ")" ); + } + + + /** + * gunzips the given file and stores the result in the original file name + * + * @param $fname string: filename to read the gzipped data from and stored + * the gunzipped data into + */ + protected function gunzip( $fname ) { + $gzipped_contents = file_get_contents( $fname ); + if ( $gzipped_contents === false ) { + $this->fail( "Could not get contents of $fname" ); + } + + $contents = gzdecode( $gzipped_contents ); + + $this->assertEquals( + strlen( $contents ), + file_put_contents( $fname, $contents ), + '# bytes written' + ); + } + + /** + * Default set up function. + * + * Clears $wgUser, and reports errors from addDBData to PHPUnit + */ + protected function setUp() { + parent::setUp(); + + // Check if any Exception is stored for rethrowing from addDBData + // @see self::exceptionFromAddDBData + if ( $this->exceptionFromAddDBData !== null ) { + throw $this->exceptionFromAddDBData; + } + + $this->setMwGlobals( 'wgUser', new User() ); + } + + /** + * Checks for test output consisting only of lines containing ETA announcements + */ + function expectETAOutput() { + // Newer PHPUnits require assertion about the output using PHPUnit's own + // expectOutput[...] functions. However, the PHPUnit shipped prediactes + // do not allow to check /each/ line of the output using /readable/ REs. + // So we ... + // + // 1. ... add a dummy output checking to make PHPUnit not complain + // about unchecked test output + $this->expectOutputRegex( '//' ); + + // 2. Do the real output checking on our own. + $lines = explode( "\n", $this->getActualOutput() ); + $this->assertGreaterThan( 1, count( $lines ), "Minimal lines of produced output" ); + $this->assertEquals( '', array_pop( $lines ), "Output ends in LF" ); + $timestamp_re = "[0-9]{4}-[01][0-9]-[0-3][0-9] [0-2][0-9]:[0-5][0-9]:[0-6][0-9]"; + foreach ( $lines as $line ) { + $this->assertRegExp( "/$timestamp_re: .* \(ID [0-9]+\) [0-9]* pages .*, [0-9]* revs .*, ETA/", $line ); + } + } + + + /** + * Step the current XML reader until node end of given name is found. + * + * @param $name string: name of the closing element to look for + * (e.g.: "mediawiki" when looking for </mediawiki>) + * + * @return bool: true if the end node could be found. false otherwise. + */ + protected function skipToNodeEnd( $name ) { + while ( $this->xml->read() ) { + if ( $this->xml->nodeType == XMLReader::END_ELEMENT && + $this->xml->name == $name + ) { + return true; + } + } + + return false; + } + + /** + * Step the current XML reader to the first element start after the node + * end of a given name. + * + * @param $name string: name of the closing element to look for + * (e.g.: "mediawiki" when looking for </mediawiki>) + * + * @return bool: true iff new element after the closing of $name could be + * found. false otherwise. + */ + protected function skipPastNodeEnd( $name ) { + $this->assertTrue( $this->skipToNodeEnd( $name ), + "Skipping to end of $name" ); + while ( $this->xml->read() ) { + if ( $this->xml->nodeType == XMLReader::ELEMENT ) { + return true; + } + } + + return false; + } + + /** + * Opens an XML file to analyze and optionally skips past siteinfo. + * + * @param $fname string: name of file to analyze + * @param $skip_siteinfo bool: (optional) If true, step the xml reader + * to the first element after </siteinfo> + */ + protected function assertDumpStart( $fname, $skip_siteinfo = true ) { + $this->xml = new XMLReader(); + $this->assertTrue( $this->xml->open( $fname ), + "Opening temporary file $fname via XMLReader failed" ); + if ( $skip_siteinfo ) { + $this->assertTrue( $this->skipPastNodeEnd( "siteinfo" ), + "Skipping past end of siteinfo" ); + } + } + + /** + * Asserts that the xml reader is at the final closing tag of an xml file and + * closes the reader. + * + * @param $tag string: (optional) the name of the final tag + * (e.g.: "mediawiki" for </mediawiki>) + */ + protected function assertDumpEnd( $name = "mediawiki" ) { + $this->assertNodeEnd( $name, false ); + if ( $this->xml->read() ) { + $this->skipWhitespace(); + } + $this->assertEquals( $this->xml->nodeType, XMLReader::NONE, + "No proper entity left to parse" ); + $this->xml->close(); + } + + /** + * Steps the xml reader over white space + */ + protected function skipWhitespace() { + $cont = true; + while ( $cont && ( ( $this->xml->nodeType == XMLReader::WHITESPACE ) + || ( $this->xml->nodeType == XMLReader::SIGNIFICANT_WHITESPACE ) ) ) { + $cont = $this->xml->read(); + } + } + + /** + * Asserts that the xml reader is at an element of given name, and optionally + * skips past it. + * + * @param $name string: the name of the element to check for + * (e.g.: "mediawiki" for <mediawiki>) + * @param $skip bool: (optional) if true, skip past the found element + */ + protected function assertNodeStart( $name, $skip = true ) { + $this->assertEquals( $name, $this->xml->name, "Node name" ); + $this->assertEquals( XMLReader::ELEMENT, $this->xml->nodeType, "Node type" ); + if ( $skip ) { + $this->assertTrue( $this->xml->read(), "Skipping past start tag" ); + } + } + + /** + * Asserts that the xml reader is at an closing element of given name, and optionally + * skips past it. + * + * @param $name string: the name of the closing element to check for + * (e.g.: "mediawiki" for </mediawiki>) + * @param $skip bool: (optional) if true, skip past the found element + */ + protected function assertNodeEnd( $name, $skip = true ) { + $this->assertEquals( $name, $this->xml->name, "Node name" ); + $this->assertEquals( XMLReader::END_ELEMENT, $this->xml->nodeType, "Node type" ); + if ( $skip ) { + $this->assertTrue( $this->xml->read(), "Skipping past end tag" ); + } + } + + + /** + * Asserts that the xml reader is at an element of given tag that contains a given text, + * and skips over the element. + * + * @param $name string: the name of the element to check for + * (e.g.: "mediawiki" for <mediawiki>...</mediawiki>) + * @param $text string|false: If string, check if it equals the elements text. + * If false, ignore the element's text + * @param $skip_ws bool: (optional) if true, skip past white spaces that trail the + * closing element. + */ + protected function assertTextNode( $name, $text, $skip_ws = true ) { + $this->assertNodeStart( $name ); + + if ( $text !== false ) { + $this->assertEquals( $text, $this->xml->value, "Text of node " . $name ); + } + $this->assertTrue( $this->xml->read(), "Skipping past processed text of " . $name ); + $this->assertNodeEnd( $name ); + + if ( $skip_ws ) { + $this->skipWhitespace(); + } + } + + /** + * Asserts that the xml reader is at the start of a page element and skips over the first + * tags, after checking them. + * + * Besides the opening page element, this function also checks for and skips over the + * title, ns, and id tags. Hence after this function, the xml reader is at the first + * revision of the current page. + * + * @param $id int: id of the page to assert + * @param $ns int: number of namespage to assert + * @param $name string: title of the current page + */ + protected function assertPageStart( $id, $ns, $name ) { + + $this->assertNodeStart( "page" ); + $this->skipWhitespace(); + + $this->assertTextNode( "title", $name ); + $this->assertTextNode( "ns", $ns ); + $this->assertTextNode( "id", $id ); + } + + /** + * Asserts that the xml reader is at the page's closing element and skips to the next + * element. + */ + protected function assertPageEnd() { + $this->assertNodeEnd( "page" ); + $this->skipWhitespace(); + } + + /** + * Asserts that the xml reader is at a revision and checks its representation before + * skipping over it. + * + * @param $id int: id of the revision + * @param $summary string: summary of the revision + * @param $text_id int: id of the revision's text + * @param $text_bytes int: # of bytes in the revision's text + * @param $text_sha1 string: the base36 SHA-1 of the revision's text + * @param $text string|false: (optional) The revision's string, or false to check for a + * revision stub + * @param $model String: the expected content model id (default: CONTENT_MODEL_WIKITEXT) + * @param $format String: the expected format model id (default: CONTENT_FORMAT_WIKITEXT) + * @param $parentid int|false: (optional) id of the parent revision + */ + protected function assertRevision( $id, $summary, $text_id, $text_bytes, $text_sha1, $text = false, $parentid = false, + $model = CONTENT_MODEL_WIKITEXT, $format = CONTENT_FORMAT_WIKITEXT + ) { + $this->assertNodeStart( "revision" ); + $this->skipWhitespace(); + + $this->assertTextNode( "id", $id ); + if ( $parentid !== false ) { + $this->assertTextNode( "parentid", $parentid ); + } + $this->assertTextNode( "timestamp", false ); + + $this->assertNodeStart( "contributor" ); + $this->skipWhitespace(); + $this->assertTextNode( "ip", false ); + $this->assertNodeEnd( "contributor" ); + $this->skipWhitespace(); + + $this->assertTextNode( "comment", $summary ); + $this->skipWhitespace(); + + if ( $this->xml->name == "text" ) { + // note: <text> tag may occur here or at the very end. + $text_found = true; + $this->assertText( $id, $text_id, $text_bytes, $text ); + } else { + $text_found = false; + } + + $this->assertTextNode( "sha1", $text_sha1 ); + + $this->assertTextNode( "model", $model ); + $this->skipWhitespace(); + + $this->assertTextNode( "format", $format ); + $this->skipWhitespace(); + + if ( !$text_found ) { + $this->assertText( $id, $text_id, $text_bytes, $text ); + } + + $this->assertNodeEnd( "revision" ); + $this->skipWhitespace(); + } + + protected function assertText( $id, $text_id, $text_bytes, $text ) { + $this->assertNodeStart( "text", false ); + if ( $text_bytes !== false ) { + $this->assertEquals( $this->xml->getAttribute( "bytes" ), $text_bytes, + "Attribute 'bytes' of revision " . $id ); + } + + if ( $text === false ) { + // Testing for a stub + $this->assertEquals( $this->xml->getAttribute( "id" ), $text_id, + "Text id of revision " . $id ); + $this->assertFalse( $this->xml->hasValue, "Revision has text" ); + $this->assertTrue( $this->xml->read(), "Skipping text start tag" ); + if ( ( $this->xml->nodeType == XMLReader::END_ELEMENT ) + && ( $this->xml->name == "text" ) + ) { + + $this->xml->read(); + } + $this->skipWhitespace(); + } else { + // Testing for a real dump + $this->assertTrue( $this->xml->read(), "Skipping text start tag" ); + $this->assertEquals( $text, $this->xml->value, "Text of revision " . $id ); + $this->assertTrue( $this->xml->read(), "Skipping past text" ); + $this->assertNodeEnd( "text" ); + $this->skipWhitespace(); + } + } +} diff --git a/tests/phpunit/maintenance/MaintenanceTest.php b/tests/phpunit/maintenance/MaintenanceTest.php new file mode 100644 index 00000000..318ce0da --- /dev/null +++ b/tests/phpunit/maintenance/MaintenanceTest.php @@ -0,0 +1,816 @@ +<?php + +// It would be great if we were able to use PHPUnit's getMockForAbstractClass +// instead of the MaintenanceFixup hack below. However, we cannot do +// without changing the visibility and without working around hacks in +// Maintenance.php +// +// For the same reason, we cannot just use FakeMaintenance. + +/** + * makes parts of the API of Maintenance that is hidden by protected visibily + * visible for testing, and makes up for a stream closing hack in Maintenance.php. + * + * This class is solely used for being able to test Maintenance right now + * without having to apply major refactorings to fix some design issues in + * Maintenance.php. Before adding more functions here, please consider whether + * this approach is correct, or a refactoring Maintenance to separate concers + * is more appropriate. + * + * Upon refactoring, keep in mind that besides the maintenance scrits themselves + * and tests right here, also at least Extension:Maintenance make use of + * Maintenance. + * + * Due to a hack in Maintenance.php using register_shutdown_function, be sure to + * finally call simulateShutdown on MaintenanceFixup instance before a test + * ends. + * + */ +class MaintenanceFixup extends Maintenance { + + // --- Making up for the register_shutdown_function hack in Maintenance.php + + /** + * The test case that generated this instance. + * + * This member is motivated by allowing the destructor to check whether or not + * the test failed, in order to avoid unnecessary nags about omitted shutdown + * simulation. + * But as it is already available, we also usi it to flagging tests as failed + * + * @var MediaWikiTestCase + */ + private $testCase; + + /** + * shutdownSimulated === true if simulateShutdown has done it's work + * + * @var bool + */ + private $shutdownSimulated = false; + + /** + * Simulates what Maintenance wants to happen at script's end. + */ + public function simulateShutdown() { + + if ( $this->shutdownSimulated ) { + $this->testCase->fail( __METHOD__ . " called more than once" ); + } + + // The cleanup action. + $this->outputChanneled( false ); + + // Bookkeeping that we simulated the clean up. + $this->shutdownSimulated = true; + } + + // Note that the "public" here does not change visibility + public function outputChanneled( $msg, $channel = null ) { + if ( $this->shutdownSimulated ) { + if ( $msg !== false ) { + $this->testCase->fail( "Already past simulated shutdown, but msg is " + . "not false. Did the hack in Maintenance.php change? Please " + . "adapt the test case or Maintenance.php" ); + } + + // The current call is the one registered via register_shutdown_function. + // We can safely ignore it, as we simulated this one via simulateShutdown + // before (if we did not, the destructor of this instance will warn about + // it) + return; + } + + return call_user_func_array( array( "parent", __FUNCTION__ ), func_get_args() ); + } + + /** + * Safety net around register_shutdown_function of Maintenance.php + */ + public function __destruct() { + if ( !$this->shutdownSimulated ) { + // Someone generated a MaintenanceFixup instance without calling + // simulateShutdown. We'd have to raise a PHPUnit exception to correctly + // flag this illegal usage. However, we are already in a destruktor, which + // would trigger undefined behavior. Hence, we can only report to the + // error output :( Hopefully people read the PHPUnit output. + $name = $this->testCase->getName(); + fwrite( STDERR, "ERROR! Instance of " . __CLASS__ . " for test $name " + . "destructed without calling simulateShutdown method. Call " + . "simulateShutdown on the instance before it gets destructed." ); + } + + // The following guard is required, as PHP does not offer default destructors :( + if ( is_callable( "parent::__destruct" ) ) { + parent::__destruct(); + } + } + + public function __construct( MediaWikiTestCase $testCase ) { + parent::__construct(); + $this->testCase = $testCase; + } + + + // --- Making protected functions visible for test + + public function output( $out, $channel = null ) { + // Just to make PHP not nag about signature mismatches, we copied + // Maintenance::output signature. However, we do not use (or rely on) + // those variables. Instead we pass to Maintenance::output whatever we + // receive at runtime. + return call_user_func_array( array( "parent", __FUNCTION__ ), func_get_args() ); + } + + + // --- Requirements for getting instance of abstract class + + public function execute() { + $this->testCase->fail( __METHOD__ . " called unexpectedly" ); + } +} + +class MaintenanceTest extends MediaWikiTestCase { + + + /** + * The main Maintenance instance that is used for testing. + * + * @var MaintenanceFixup + */ + private $m; + + + protected function setUp() { + parent::setUp(); + $this->m = new MaintenanceFixup( $this ); + } + + protected function tearDown() { + if ( $this->m ) { + $this->m->simulateShutdown(); + $this->m = null; + } + parent::tearDown(); + } + + + /** + * asserts the output before and after simulating shutdown + * + * This function simulates shutdown of self::m. + * + * @param $preShutdownOutput string: expected output before simulating shutdown + * @param $expectNLAppending bool: Whether or not shutdown simulation is expected + * to add a newline to the output. If false, $preShutdownOutput is the + * expected output after shutdown simulation. Otherwise, + * $preShutdownOutput with an appended newline is the expected output + * after shutdown simulation. + */ + private function assertOutputPrePostShutdown( $preShutdownOutput, $expectNLAppending ) { + + $this->assertEquals( $preShutdownOutput, $this->getActualOutput(), + "Output before shutdown simulation" ); + + $this->m->simulateShutdown(); + $this->m = null; + + $postShutdownOutput = $preShutdownOutput . ( $expectNLAppending ? "\n" : "" ); + $this->expectOutputString( $postShutdownOutput ); + } + + + // Although the following tests do not seem to be too consistent (compare for + // example the newlines within the test.*StringString tests, or the + // test.*Intermittent.* tests), the objective of these tests is not to describe + // consistent behavior, but rather currently existing behavior. + + function testOutputEmpty() { + $this->m->output( "" ); + $this->assertOutputPrePostShutdown( "", false ); + } + + function testOutputString() { + $this->m->output( "foo" ); + $this->assertOutputPrePostShutdown( "foo", false ); + } + + function testOutputStringString() { + $this->m->output( "foo" ); + $this->m->output( "bar" ); + $this->assertOutputPrePostShutdown( "foobar", false ); + } + + function testOutputStringNL() { + $this->m->output( "foo\n" ); + $this->assertOutputPrePostShutdown( "foo\n", false ); + } + + function testOutputStringNLNL() { + $this->m->output( "foo\n\n" ); + $this->assertOutputPrePostShutdown( "foo\n\n", false ); + } + + function testOutputStringNLString() { + $this->m->output( "foo\nbar" ); + $this->assertOutputPrePostShutdown( "foo\nbar", false ); + } + + function testOutputStringNLStringNL() { + $this->m->output( "foo\nbar\n" ); + $this->assertOutputPrePostShutdown( "foo\nbar\n", false ); + } + + function testOutputStringNLStringNLLinewise() { + $this->m->output( "foo\n" ); + $this->m->output( "bar\n" ); + $this->assertOutputPrePostShutdown( "foo\nbar\n", false ); + } + + function testOutputStringNLStringNLArbitrary() { + $this->m->output( "" ); + $this->m->output( "foo" ); + $this->m->output( "" ); + $this->m->output( "\n" ); + $this->m->output( "ba" ); + $this->m->output( "" ); + $this->m->output( "r\n" ); + $this->assertOutputPrePostShutdown( "foo\nbar\n", false ); + } + + function testOutputStringNLStringNLArbitraryAgain() { + $this->m->output( "" ); + $this->m->output( "foo" ); + $this->m->output( "" ); + $this->m->output( "\nb" ); + $this->m->output( "a" ); + $this->m->output( "" ); + $this->m->output( "r\n" ); + $this->assertOutputPrePostShutdown( "foo\nbar\n", false ); + } + + function testOutputWNullChannelEmpty() { + $this->m->output( "", null ); + $this->assertOutputPrePostShutdown( "", false ); + } + + function testOutputWNullChannelString() { + $this->m->output( "foo", null ); + $this->assertOutputPrePostShutdown( "foo", false ); + } + + function testOutputWNullChannelStringString() { + $this->m->output( "foo", null ); + $this->m->output( "bar", null ); + $this->assertOutputPrePostShutdown( "foobar", false ); + } + + function testOutputWNullChannelStringNL() { + $this->m->output( "foo\n", null ); + $this->assertOutputPrePostShutdown( "foo\n", false ); + } + + function testOutputWNullChannelStringNLNL() { + $this->m->output( "foo\n\n", null ); + $this->assertOutputPrePostShutdown( "foo\n\n", false ); + } + + function testOutputWNullChannelStringNLString() { + $this->m->output( "foo\nbar", null ); + $this->assertOutputPrePostShutdown( "foo\nbar", false ); + } + + function testOutputWNullChannelStringNLStringNL() { + $this->m->output( "foo\nbar\n", null ); + $this->assertOutputPrePostShutdown( "foo\nbar\n", false ); + } + + function testOutputWNullChannelStringNLStringNLLinewise() { + $this->m->output( "foo\n", null ); + $this->m->output( "bar\n", null ); + $this->assertOutputPrePostShutdown( "foo\nbar\n", false ); + } + + function testOutputWNullChannelStringNLStringNLArbitrary() { + $this->m->output( "", null ); + $this->m->output( "foo", null ); + $this->m->output( "", null ); + $this->m->output( "\n", null ); + $this->m->output( "ba", null ); + $this->m->output( "", null ); + $this->m->output( "r\n", null ); + $this->assertOutputPrePostShutdown( "foo\nbar\n", false ); + } + + function testOutputWNullChannelStringNLStringNLArbitraryAgain() { + $this->m->output( "", null ); + $this->m->output( "foo", null ); + $this->m->output( "", null ); + $this->m->output( "\nb", null ); + $this->m->output( "a", null ); + $this->m->output( "", null ); + $this->m->output( "r\n", null ); + $this->assertOutputPrePostShutdown( "foo\nbar\n", false ); + } + + function testOutputWChannelString() { + $this->m->output( "foo", "bazChannel" ); + $this->assertOutputPrePostShutdown( "foo", true ); + } + + function testOutputWChannelStringNL() { + $this->m->output( "foo\n", "bazChannel" ); + $this->assertOutputPrePostShutdown( "foo", true ); + } + + function testOutputWChannelStringNLNL() { + // If this test fails, note that output takes strings with double line + // endings (although output's implementation in this situation calls + // outputChanneled with a string ending in a nl ... which is not allowed + // according to the documentation of outputChanneled) + $this->m->output( "foo\n\n", "bazChannel" ); + $this->assertOutputPrePostShutdown( "foo\n", true ); + } + + function testOutputWChannelStringNLString() { + $this->m->output( "foo\nbar", "bazChannel" ); + $this->assertOutputPrePostShutdown( "foo\nbar", true ); + } + + function testOutputWChannelStringNLStringNL() { + $this->m->output( "foo\nbar\n", "bazChannel" ); + $this->assertOutputPrePostShutdown( "foo\nbar", true ); + } + + function testOutputWChannelStringNLStringNLLinewise() { + $this->m->output( "foo\n", "bazChannel" ); + $this->m->output( "bar\n", "bazChannel" ); + $this->assertOutputPrePostShutdown( "foobar", true ); + } + + function testOutputWChannelStringNLStringNLArbitrary() { + $this->m->output( "", "bazChannel" ); + $this->m->output( "foo", "bazChannel" ); + $this->m->output( "", "bazChannel" ); + $this->m->output( "\n", "bazChannel" ); + $this->m->output( "ba", "bazChannel" ); + $this->m->output( "", "bazChannel" ); + $this->m->output( "r\n", "bazChannel" ); + $this->assertOutputPrePostShutdown( "foobar", true ); + } + + function testOutputWChannelStringNLStringNLArbitraryAgain() { + $this->m->output( "", "bazChannel" ); + $this->m->output( "foo", "bazChannel" ); + $this->m->output( "", "bazChannel" ); + $this->m->output( "\nb", "bazChannel" ); + $this->m->output( "a", "bazChannel" ); + $this->m->output( "", "bazChannel" ); + $this->m->output( "r\n", "bazChannel" ); + $this->assertOutputPrePostShutdown( "foo\nbar", true ); + } + + function testOutputWMultipleChannelsChannelChange() { + $this->m->output( "foo", "bazChannel" ); + $this->m->output( "bar", "bazChannel" ); + $this->m->output( "qux", "quuxChannel" ); + $this->m->output( "corge", "bazChannel" ); + $this->assertOutputPrePostShutdown( "foobar\nqux\ncorge", true ); + } + + function testOutputWMultipleChannelsChannelChangeNL() { + $this->m->output( "foo", "bazChannel" ); + $this->m->output( "bar\n", "bazChannel" ); + $this->m->output( "qux\n", "quuxChannel" ); + $this->m->output( "corge", "bazChannel" ); + $this->assertOutputPrePostShutdown( "foobar\nqux\ncorge", true ); + } + + function testOutputWAndWOChannelStringStartWO() { + $this->m->output( "foo" ); + $this->m->output( "bar", "bazChannel" ); + $this->m->output( "qux" ); + $this->m->output( "quux", "bazChannel" ); + $this->assertOutputPrePostShutdown( "foobar\nquxquux", true ); + } + + function testOutputWAndWOChannelStringStartW() { + $this->m->output( "foo", "bazChannel" ); + $this->m->output( "bar" ); + $this->m->output( "qux", "bazChannel" ); + $this->m->output( "quux" ); + $this->assertOutputPrePostShutdown( "foo\nbarqux\nquux", false ); + } + + function testOutputWChannelTypeSwitch() { + $this->m->output( "foo", 1 ); + $this->m->output( "bar", 1.0 ); + $this->assertOutputPrePostShutdown( "foo\nbar", true ); + } + + function testOutputIntermittentEmpty() { + $this->m->output( "foo" ); + $this->m->output( "" ); + $this->m->output( "bar" ); + $this->assertOutputPrePostShutdown( "foobar", false ); + } + + function testOutputIntermittentFalse() { + $this->m->output( "foo" ); + $this->m->output( false ); + $this->m->output( "bar" ); + $this->assertOutputPrePostShutdown( "foobar", false ); + } + + function testOutputIntermittentFalseAfterOtherChannel() { + $this->m->output( "qux", "quuxChannel" ); + $this->m->output( "foo" ); + $this->m->output( false ); + $this->m->output( "bar" ); + $this->assertOutputPrePostShutdown( "qux\nfoobar", false ); + } + + function testOutputWNullChannelIntermittentEmpty() { + $this->m->output( "foo", null ); + $this->m->output( "", null ); + $this->m->output( "bar", null ); + $this->assertOutputPrePostShutdown( "foobar", false ); + } + + function testOutputWNullChannelIntermittentFalse() { + $this->m->output( "foo", null ); + $this->m->output( false, null ); + $this->m->output( "bar", null ); + $this->assertOutputPrePostShutdown( "foobar", false ); + } + + function testOutputWChannelIntermittentEmpty() { + $this->m->output( "foo", "bazChannel" ); + $this->m->output( "", "bazChannel" ); + $this->m->output( "bar", "bazChannel" ); + $this->assertOutputPrePostShutdown( "foobar", true ); + } + + function testOutputWChannelIntermittentFalse() { + $this->m->output( "foo", "bazChannel" ); + $this->m->output( false, "bazChannel" ); + $this->m->output( "bar", "bazChannel" ); + $this->assertOutputPrePostShutdown( "foobar", true ); + } + + // Note that (per documentation) outputChanneled does take strings that end + // in \n, hence we do not test such strings. + + function testOutputChanneledEmpty() { + $this->m->outputChanneled( "" ); + $this->assertOutputPrePostShutdown( "\n", false ); + } + + function testOutputChanneledString() { + $this->m->outputChanneled( "foo" ); + $this->assertOutputPrePostShutdown( "foo\n", false ); + } + + function testOutputChanneledStringString() { + $this->m->outputChanneled( "foo" ); + $this->m->outputChanneled( "bar" ); + $this->assertOutputPrePostShutdown( "foo\nbar\n", false ); + } + + function testOutputChanneledStringNLString() { + $this->m->outputChanneled( "foo\nbar" ); + $this->assertOutputPrePostShutdown( "foo\nbar\n", false ); + } + + function testOutputChanneledStringNLStringNLArbitraryAgain() { + $this->m->outputChanneled( "" ); + $this->m->outputChanneled( "foo" ); + $this->m->outputChanneled( "" ); + $this->m->outputChanneled( "\nb" ); + $this->m->outputChanneled( "a" ); + $this->m->outputChanneled( "" ); + $this->m->outputChanneled( "r" ); + $this->assertOutputPrePostShutdown( "\nfoo\n\n\nb\na\n\nr\n", false ); + } + + function testOutputChanneledWNullChannelEmpty() { + $this->m->outputChanneled( "", null ); + $this->assertOutputPrePostShutdown( "\n", false ); + } + + function testOutputChanneledWNullChannelString() { + $this->m->outputChanneled( "foo", null ); + $this->assertOutputPrePostShutdown( "foo\n", false ); + } + + function testOutputChanneledWNullChannelStringString() { + $this->m->outputChanneled( "foo", null ); + $this->m->outputChanneled( "bar", null ); + $this->assertOutputPrePostShutdown( "foo\nbar\n", false ); + } + + function testOutputChanneledWNullChannelStringNLString() { + $this->m->outputChanneled( "foo\nbar", null ); + $this->assertOutputPrePostShutdown( "foo\nbar\n", false ); + } + + function testOutputChanneledWNullChannelStringNLStringNLArbitraryAgain() { + $this->m->outputChanneled( "", null ); + $this->m->outputChanneled( "foo", null ); + $this->m->outputChanneled( "", null ); + $this->m->outputChanneled( "\nb", null ); + $this->m->outputChanneled( "a", null ); + $this->m->outputChanneled( "", null ); + $this->m->outputChanneled( "r", null ); + $this->assertOutputPrePostShutdown( "\nfoo\n\n\nb\na\n\nr\n", false ); + } + + function testOutputChanneledWChannelString() { + $this->m->outputChanneled( "foo", "bazChannel" ); + $this->assertOutputPrePostShutdown( "foo", true ); + } + + function testOutputChanneledWChannelStringNLString() { + $this->m->outputChanneled( "foo\nbar", "bazChannel" ); + $this->assertOutputPrePostShutdown( "foo\nbar", true ); + } + + function testOutputChanneledWChannelStringString() { + $this->m->outputChanneled( "foo", "bazChannel" ); + $this->m->outputChanneled( "bar", "bazChannel" ); + $this->assertOutputPrePostShutdown( "foobar", true ); + } + + function testOutputChanneledWChannelStringNLStringNLArbitraryAgain() { + $this->m->outputChanneled( "", "bazChannel" ); + $this->m->outputChanneled( "foo", "bazChannel" ); + $this->m->outputChanneled( "", "bazChannel" ); + $this->m->outputChanneled( "\nb", "bazChannel" ); + $this->m->outputChanneled( "a", "bazChannel" ); + $this->m->outputChanneled( "", "bazChannel" ); + $this->m->outputChanneled( "r", "bazChannel" ); + $this->assertOutputPrePostShutdown( "foo\nbar", true ); + } + + function testOutputChanneledWMultipleChannelsChannelChange() { + $this->m->outputChanneled( "foo", "bazChannel" ); + $this->m->outputChanneled( "bar", "bazChannel" ); + $this->m->outputChanneled( "qux", "quuxChannel" ); + $this->m->outputChanneled( "corge", "bazChannel" ); + $this->assertOutputPrePostShutdown( "foobar\nqux\ncorge", true ); + } + + function testOutputChanneledWMultipleChannelsChannelChangeEnclosedNull() { + $this->m->outputChanneled( "foo", "bazChannel" ); + $this->m->outputChanneled( "bar", null ); + $this->m->outputChanneled( "qux", null ); + $this->m->outputChanneled( "corge", "bazChannel" ); + $this->assertOutputPrePostShutdown( "foo\nbar\nqux\ncorge", true ); + } + + function testOutputChanneledWMultipleChannelsChannelAfterNullChange() { + $this->m->outputChanneled( "foo", "bazChannel" ); + $this->m->outputChanneled( "bar", null ); + $this->m->outputChanneled( "qux", null ); + $this->m->outputChanneled( "corge", "quuxChannel" ); + $this->assertOutputPrePostShutdown( "foo\nbar\nqux\ncorge", true ); + } + + function testOutputChanneledWAndWOChannelStringStartWO() { + $this->m->outputChanneled( "foo" ); + $this->m->outputChanneled( "bar", "bazChannel" ); + $this->m->outputChanneled( "qux" ); + $this->m->outputChanneled( "quux", "bazChannel" ); + $this->assertOutputPrePostShutdown( "foo\nbar\nqux\nquux", true ); + } + + function testOutputChanneledWAndWOChannelStringStartW() { + $this->m->outputChanneled( "foo", "bazChannel" ); + $this->m->outputChanneled( "bar" ); + $this->m->outputChanneled( "qux", "bazChannel" ); + $this->m->outputChanneled( "quux" ); + $this->assertOutputPrePostShutdown( "foo\nbar\nqux\nquux\n", false ); + } + + function testOutputChanneledWChannelTypeSwitch() { + $this->m->outputChanneled( "foo", 1 ); + $this->m->outputChanneled( "bar", 1.0 ); + $this->assertOutputPrePostShutdown( "foo\nbar", true ); + } + + function testOutputChanneledWOChannelIntermittentEmpty() { + $this->m->outputChanneled( "foo" ); + $this->m->outputChanneled( "" ); + $this->m->outputChanneled( "bar" ); + $this->assertOutputPrePostShutdown( "foo\n\nbar\n", false ); + } + + function testOutputChanneledWOChannelIntermittentFalse() { + $this->m->outputChanneled( "foo" ); + $this->m->outputChanneled( false ); + $this->m->outputChanneled( "bar" ); + $this->assertOutputPrePostShutdown( "foo\nbar\n", false ); + } + + function testOutputChanneledWNullChannelIntermittentEmpty() { + $this->m->outputChanneled( "foo", null ); + $this->m->outputChanneled( "", null ); + $this->m->outputChanneled( "bar", null ); + $this->assertOutputPrePostShutdown( "foo\n\nbar\n", false ); + } + + function testOutputChanneledWNullChannelIntermittentFalse() { + $this->m->outputChanneled( "foo", null ); + $this->m->outputChanneled( false, null ); + $this->m->outputChanneled( "bar", null ); + $this->assertOutputPrePostShutdown( "foo\nbar\n", false ); + } + + function testOutputChanneledWChannelIntermittentEmpty() { + $this->m->outputChanneled( "foo", "bazChannel" ); + $this->m->outputChanneled( "", "bazChannel" ); + $this->m->outputChanneled( "bar", "bazChannel" ); + $this->assertOutputPrePostShutdown( "foobar", true ); + } + + function testOutputChanneledWChannelIntermittentFalse() { + $this->m->outputChanneled( "foo", "bazChannel" ); + $this->m->outputChanneled( false, "bazChannel" ); + $this->m->outputChanneled( "bar", "bazChannel" ); + $this->assertOutputPrePostShutdown( "foo\nbar", true ); + } + + function testCleanupChanneledClean() { + $this->m->cleanupChanneled(); + $this->assertOutputPrePostShutdown( "", false ); + } + + function testCleanupChanneledAfterOutput() { + $this->m->output( "foo" ); + $this->m->cleanupChanneled(); + $this->assertOutputPrePostShutdown( "foo", false ); + } + + function testCleanupChanneledAfterOutputWNullChannel() { + $this->m->output( "foo", null ); + $this->m->cleanupChanneled(); + $this->assertOutputPrePostShutdown( "foo", false ); + } + + function testCleanupChanneledAfterOutputWChannel() { + $this->m->output( "foo", "bazChannel" ); + $this->m->cleanupChanneled(); + $this->assertOutputPrePostShutdown( "foo\n", false ); + } + + function testCleanupChanneledAfterNLOutput() { + $this->m->output( "foo\n" ); + $this->m->cleanupChanneled(); + $this->assertOutputPrePostShutdown( "foo\n", false ); + } + + function testCleanupChanneledAfterNLOutputWNullChannel() { + $this->m->output( "foo\n", null ); + $this->m->cleanupChanneled(); + $this->assertOutputPrePostShutdown( "foo\n", false ); + } + + function testCleanupChanneledAfterNLOutputWChannel() { + $this->m->output( "foo\n", "bazChannel" ); + $this->m->cleanupChanneled(); + $this->assertOutputPrePostShutdown( "foo\n", false ); + } + + function testCleanupChanneledAfterOutputChanneledWOChannel() { + $this->m->outputChanneled( "foo" ); + $this->m->cleanupChanneled(); + $this->assertOutputPrePostShutdown( "foo\n", false ); + } + + function testCleanupChanneledAfterOutputChanneledWNullChannel() { + $this->m->outputChanneled( "foo", null ); + $this->m->cleanupChanneled(); + $this->assertOutputPrePostShutdown( "foo\n", false ); + } + + function testCleanupChanneledAfterOutputChanneledWChannel() { + $this->m->outputChanneled( "foo", "bazChannel" ); + $this->m->cleanupChanneled(); + $this->assertOutputPrePostShutdown( "foo\n", false ); + } + + function testMultipleMaintenanceObjectsInteractionOutput() { + $m2 = new MaintenanceFixup( $this ); + + $this->m->output( "foo" ); + $m2->output( "bar" ); + + $this->assertEquals( "foobar", $this->getActualOutput(), + "Output before shutdown simulation (m2)" ); + $m2->simulateShutdown(); + $this->assertOutputPrePostShutdown( "foobar", false ); + } + + function testMultipleMaintenanceObjectsInteractionOutputWNullChannel() { + $m2 = new MaintenanceFixup( $this ); + + $this->m->output( "foo", null ); + $m2->output( "bar", null ); + + $this->assertEquals( "foobar", $this->getActualOutput(), + "Output before shutdown simulation (m2)" ); + $m2->simulateShutdown(); + $this->assertOutputPrePostShutdown( "foobar", false ); + } + + function testMultipleMaintenanceObjectsInteractionOutputWChannel() { + $m2 = new MaintenanceFixup( $this ); + + $this->m->output( "foo", "bazChannel" ); + $m2->output( "bar", "bazChannel" ); + + $this->assertEquals( "foobar", $this->getActualOutput(), + "Output before shutdown simulation (m2)" ); + $m2->simulateShutdown(); + $this->assertOutputPrePostShutdown( "foobar\n", true ); + } + + function testMultipleMaintenanceObjectsInteractionOutputWNullChannelNL() { + $m2 = new MaintenanceFixup( $this ); + + $this->m->output( "foo\n", null ); + $m2->output( "bar\n", null ); + + $this->assertEquals( "foo\nbar\n", $this->getActualOutput(), + "Output before shutdown simulation (m2)" ); + $m2->simulateShutdown(); + $this->assertOutputPrePostShutdown( "foo\nbar\n", false ); + } + + function testMultipleMaintenanceObjectsInteractionOutputWChannelNL() { + $m2 = new MaintenanceFixup( $this ); + + $this->m->output( "foo\n", "bazChannel" ); + $m2->output( "bar\n", "bazChannel" ); + + $this->assertEquals( "foobar", $this->getActualOutput(), + "Output before shutdown simulation (m2)" ); + $m2->simulateShutdown(); + $this->assertOutputPrePostShutdown( "foobar\n", true ); + } + + function testMultipleMaintenanceObjectsInteractionOutputChanneled() { + $m2 = new MaintenanceFixup( $this ); + + $this->m->outputChanneled( "foo" ); + $m2->outputChanneled( "bar" ); + + $this->assertEquals( "foo\nbar\n", $this->getActualOutput(), + "Output before shutdown simulation (m2)" ); + $m2->simulateShutdown(); + $this->assertOutputPrePostShutdown( "foo\nbar\n", false ); + } + + function testMultipleMaintenanceObjectsInteractionOutputChanneledWNullChannel() { + $m2 = new MaintenanceFixup( $this ); + + $this->m->outputChanneled( "foo", null ); + $m2->outputChanneled( "bar", null ); + + $this->assertEquals( "foo\nbar\n", $this->getActualOutput(), + "Output before shutdown simulation (m2)" ); + $m2->simulateShutdown(); + $this->assertOutputPrePostShutdown( "foo\nbar\n", false ); + } + + function testMultipleMaintenanceObjectsInteractionOutputChanneledWChannel() { + $m2 = new MaintenanceFixup( $this ); + + $this->m->outputChanneled( "foo", "bazChannel" ); + $m2->outputChanneled( "bar", "bazChannel" ); + + $this->assertEquals( "foobar", $this->getActualOutput(), + "Output before shutdown simulation (m2)" ); + $m2->simulateShutdown(); + $this->assertOutputPrePostShutdown( "foobar\n", true ); + } + + function testMultipleMaintenanceObjectsInteractionCleanupChanneledWChannel() { + $m2 = new MaintenanceFixup( $this ); + + $this->m->outputChanneled( "foo", "bazChannel" ); + $m2->outputChanneled( "bar", "bazChannel" ); + + $this->assertEquals( "foobar", $this->getActualOutput(), + "Output before first cleanup" ); + $this->m->cleanupChanneled(); + $this->assertEquals( "foobar\n", $this->getActualOutput(), + "Output after first cleanup" ); + $m2->cleanupChanneled(); + $this->assertEquals( "foobar\n\n", $this->getActualOutput(), + "Output after second cleanup" ); + + $m2->simulateShutdown(); + $this->assertOutputPrePostShutdown( "foobar\n\n", false ); + } +} diff --git a/tests/phpunit/maintenance/backupPrefetchTest.php b/tests/phpunit/maintenance/backupPrefetchTest.php new file mode 100644 index 00000000..bc2d7375 --- /dev/null +++ b/tests/phpunit/maintenance/backupPrefetchTest.php @@ -0,0 +1,275 @@ +<?php + +require_once __DIR__ . "/../../../maintenance/backupPrefetch.inc"; + +/** + * Tests for BaseDump + * + * @group Dump + */ +class BaseDumpTest extends MediaWikiTestCase { + + /** + * @var BaseDump the BaseDump instance used within a test. + * + * If set, this BaseDump gets automatically closed in tearDown. + */ + private $dump = null; + + protected function tearDown() { + if ( $this->dump !== null ) { + $this->dump->close(); + } + + // Bug 37458, parent teardown need to be done after closing the + // dump or it might cause some permissions errors. + parent::tearDown(); + } + + /** + * asserts that a prefetch yields an expected string + * + * @param $expected string|null: the exepcted result of the prefetch + * @param $page int: the page number to prefetch the text for + * @param $revision int: the revision number to prefetch the text for + */ + private function assertPrefetchEquals( $expected, $page, $revision ) { + $this->assertEquals( $expected, $this->dump->prefetch( $page, $revision ), + "Prefetch of page $page revision $revision" ); + } + + function testSequential() { + $fname = $this->setUpPrefetch(); + $this->dump = new BaseDump( $fname ); + + $this->assertPrefetchEquals( "BackupDumperTestP1Text1", 1, 1 ); + $this->assertPrefetchEquals( "BackupDumperTestP2Text1", 2, 2 ); + $this->assertPrefetchEquals( "BackupDumperTestP2Text4 some additional Text", 2, 5 ); + $this->assertPrefetchEquals( "Talk about BackupDumperTestP1 Text1", 4, 8 ); + } + + function testSynchronizeRevisionMissToRevision() { + $fname = $this->setUpPrefetch(); + $this->dump = new BaseDump( $fname ); + + $this->assertPrefetchEquals( "BackupDumperTestP2Text1", 2, 2 ); + $this->assertPrefetchEquals( null, 2, 3 ); + $this->assertPrefetchEquals( "BackupDumperTestP2Text4 some additional Text", 2, 5 ); + } + + function testSynchronizeRevisionMissToPage() { + $fname = $this->setUpPrefetch(); + $this->dump = new BaseDump( $fname ); + + $this->assertPrefetchEquals( "BackupDumperTestP2Text1", 2, 2 ); + $this->assertPrefetchEquals( null, 2, 40 ); + $this->assertPrefetchEquals( "Talk about BackupDumperTestP1 Text1", 4, 8 ); + } + + function testSynchronizePageMiss() { + $fname = $this->setUpPrefetch(); + $this->dump = new BaseDump( $fname ); + + $this->assertPrefetchEquals( "BackupDumperTestP2Text1", 2, 2 ); + $this->assertPrefetchEquals( null, 3, 40 ); + $this->assertPrefetchEquals( "Talk about BackupDumperTestP1 Text1", 4, 8 ); + } + + function testPageMissAtEnd() { + $fname = $this->setUpPrefetch(); + $this->dump = new BaseDump( $fname ); + + $this->assertPrefetchEquals( "BackupDumperTestP2Text1", 2, 2 ); + $this->assertPrefetchEquals( null, 6, 40 ); + } + + function testRevisionMissAtEnd() { + $fname = $this->setUpPrefetch(); + $this->dump = new BaseDump( $fname ); + + $this->assertPrefetchEquals( "BackupDumperTestP2Text1", 2, 2 ); + $this->assertPrefetchEquals( null, 4, 40 ); + } + + function testSynchronizePageMissAtStart() { + $fname = $this->setUpPrefetch(); + $this->dump = new BaseDump( $fname ); + + $this->assertPrefetchEquals( null, 0, 2 ); + $this->assertPrefetchEquals( "BackupDumperTestP2Text1", 2, 2 ); + } + + function testSynchronizeRevisionMissAtStart() { + $fname = $this->setUpPrefetch(); + $this->dump = new BaseDump( $fname ); + + $this->assertPrefetchEquals( null, 1, -2 ); + $this->assertPrefetchEquals( "BackupDumperTestP2Text1", 2, 2 ); + } + + function testSequentialAcrossFiles() { + $fname1 = $this->setUpPrefetch( array( 1 ) ); + $fname2 = $this->setUpPrefetch( array( 2, 4 ) ); + $this->dump = new BaseDump( $fname1 . ";" . $fname2 ); + + $this->assertPrefetchEquals( "BackupDumperTestP1Text1", 1, 1 ); + $this->assertPrefetchEquals( "BackupDumperTestP2Text1", 2, 2 ); + $this->assertPrefetchEquals( "BackupDumperTestP2Text4 some additional Text", 2, 5 ); + $this->assertPrefetchEquals( "Talk about BackupDumperTestP1 Text1", 4, 8 ); + } + + function testSynchronizeSkipAcrossFile() { + $fname1 = $this->setUpPrefetch( array( 1 ) ); + $fname2 = $this->setUpPrefetch( array( 2 ) ); + $fname3 = $this->setUpPrefetch( array( 4 ) ); + $this->dump = new BaseDump( $fname1 . ";" . $fname2 . ";" . $fname3 ); + + $this->assertPrefetchEquals( "BackupDumperTestP1Text1", 1, 1 ); + $this->assertPrefetchEquals( "Talk about BackupDumperTestP1 Text1", 4, 8 ); + } + + function testSynchronizeMissInWholeFirstFile() { + $fname1 = $this->setUpPrefetch( array( 1 ) ); + $fname2 = $this->setUpPrefetch( array( 2 ) ); + $this->dump = new BaseDump( $fname1 . ";" . $fname2 ); + + $this->assertPrefetchEquals( "BackupDumperTestP2Text1", 2, 2 ); + } + + + /** + * Constructs a temporary file that can be used for prefetching + * + * The temporary file is removed by DumpBackup upon tearDown. + * + * @param $requested_pages Array The indices of the page parts that should + * go into the prefetch file. 1,2,4 are available. + * @return String The file name of the created temporary file + */ + private function setUpPrefetch( $requested_pages = array( 1, 2, 4 ) ) { + // The file name, where we store the prepared prefetch file + $fname = $this->getNewTempFile(); + + // The header of every prefetch file + $header = '<mediawiki xmlns="http://www.mediawiki.org/xml/export-0.7/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.mediawiki.org/xml/export-0.7/ http://www.mediawiki.org/xml/export-0.7.xsd" version="0.7" xml:lang="en"> + <siteinfo> + <sitename>wikisvn</sitename> + <base>http://localhost/wiki-svn/index.php/Main_Page</base> + <generator>MediaWiki 1.21alpha</generator> + <case>first-letter</case> + <namespaces> + <namespace key="-2" case="first-letter">Media</namespace> + <namespace key="-1" case="first-letter">Special</namespace> + <namespace key="0" case="first-letter" /> + <namespace key="1" case="first-letter">Talk</namespace> + <namespace key="2" case="first-letter">User</namespace> + <namespace key="3" case="first-letter">User talk</namespace> + <namespace key="4" case="first-letter">Wikisvn</namespace> + <namespace key="5" case="first-letter">Wikisvn talk</namespace> + <namespace key="6" case="first-letter">File</namespace> + <namespace key="7" case="first-letter">File talk</namespace> + <namespace key="8" case="first-letter">MediaWiki</namespace> + <namespace key="9" case="first-letter">MediaWiki talk</namespace> + <namespace key="10" case="first-letter">Template</namespace> + <namespace key="11" case="first-letter">Template talk</namespace> + <namespace key="12" case="first-letter">Help</namespace> + <namespace key="13" case="first-letter">Help talk</namespace> + <namespace key="14" case="first-letter">Category</namespace> + <namespace key="15" case="first-letter">Category talk</namespace> + </namespaces> + </siteinfo> +'; + + // An array holding the pages that are available for prefetch + $available_pages = array(); + + // Simple plain page + $available_pages[1] = ' <page> + <title>BackupDumperTestP1 + 0 + 1 + + 1 + 2012-04-01T16:46:05Z + + 127.0.0.1 + + BackupDumperTestP1Summary1 + 0bolhl6ol7i6x0e7yq91gxgaan39j87 + BackupDumperTestP1Text1 + 1 + 1 + + +'; + // Page with more than one revisions. Hole in rev ids. + $available_pages[2] = ' + BackupDumperTestP2 + 0 + 2 + + 2 + 2012-04-01T16:46:05Z + + 127.0.0.1 + + BackupDumperTestP2Summary1 + jprywrymfhysqllua29tj3sc7z39dl2 + BackupDumperTestP2Text1 + 1 + 1 + + + 5 + 2 + 2012-04-01T16:46:05Z + + 127.0.0.1 + + BackupDumperTestP2Summary4 extra + 6o1ciaxa6pybnqprmungwofc4lv00wv + BackupDumperTestP2Text4 some additional Text + 1 + 1 + + +'; + // Page with id higher than previous id + 1 + $available_pages[4] = ' + Talk:BackupDumperTestP1 + 1 + 4 + + 8 + 2012-04-01T16:46:05Z + + 127.0.0.1 + + Talk BackupDumperTestP1 Summary1 + nktofwzd0tl192k3zfepmlzxoax1lpe + 1 + 1 + Talk about BackupDumperTestP1 Text1 + + +'; + + // The common ending for all files + $tail = ' +'; + + // Putting together the content of the prefetch files + $content = $header; + foreach ( $requested_pages as $i ) { + $this->assertTrue( array_key_exists( $i, $available_pages ), + "Check for availability of requested page " . $i ); + $content .= $available_pages[$i]; + } + $content .= $tail; + + $this->assertEquals( strlen( $content ), file_put_contents( + $fname, $content ), "Length of prepared prefetch" ); + + return $fname; + } +} diff --git a/tests/phpunit/maintenance/backupTextPassTest.php b/tests/phpunit/maintenance/backupTextPassTest.php new file mode 100644 index 00000000..653a1145 --- /dev/null +++ b/tests/phpunit/maintenance/backupTextPassTest.php @@ -0,0 +1,581 @@ +tablesUsed[] = 'page'; + $this->tablesUsed[] = 'revision'; + $this->tablesUsed[] = 'text'; + + $ns = $this->getDefaultWikitextNS(); + + try { + // Simple page + $title = Title::newFromText( 'BackupDumperTestP1', $ns ); + $page = WikiPage::factory( $title ); + list( $this->revId1_1, $this->textId1_1 ) = $this->addRevision( $page, + "BackupDumperTestP1Text1", "BackupDumperTestP1Summary1" ); + $this->pageId1 = $page->getId(); + + // Page with more than one revision + $title = Title::newFromText( 'BackupDumperTestP2', $ns ); + $page = WikiPage::factory( $title ); + list( $this->revId2_1, $this->textId2_1 ) = $this->addRevision( $page, + "BackupDumperTestP2Text1", "BackupDumperTestP2Summary1" ); + list( $this->revId2_2, $this->textId2_2 ) = $this->addRevision( $page, + "BackupDumperTestP2Text2", "BackupDumperTestP2Summary2" ); + list( $this->revId2_3, $this->textId2_3 ) = $this->addRevision( $page, + "BackupDumperTestP2Text3", "BackupDumperTestP2Summary3" ); + list( $this->revId2_4, $this->textId2_4 ) = $this->addRevision( $page, + "BackupDumperTestP2Text4 some additional Text ", + "BackupDumperTestP2Summary4 extra " ); + $this->pageId2 = $page->getId(); + + // Deleted page. + $title = Title::newFromText( 'BackupDumperTestP3', $ns ); + $page = WikiPage::factory( $title ); + list( $this->revId3_1, $this->textId3_1 ) = $this->addRevision( $page, + "BackupDumperTestP3Text1", "BackupDumperTestP2Summary1" ); + list( $this->revId3_2, $this->textId3_2 ) = $this->addRevision( $page, + "BackupDumperTestP3Text2", "BackupDumperTestP2Summary2" ); + $this->pageId3 = $page->getId(); + $page->doDeleteArticle( "Testing ;)" ); + + // Page from non-default namespace + + if ( $ns === NS_TALK ) { + // @todo work around this. + throw new MWException( "The default wikitext namespace is the talk namespace. " + . " We can't currently deal with that." ); + } + + $title = Title::newFromText( 'BackupDumperTestP1', NS_TALK ); + $page = WikiPage::factory( $title ); + list( $this->revId4_1, $this->textId4_1 ) = $this->addRevision( $page, + "Talk about BackupDumperTestP1 Text1", + "Talk BackupDumperTestP1 Summary1" ); + $this->pageId4 = $page->getId(); + } catch ( Exception $e ) { + // We'd love to pass $e directly. However, ... see + // documentation of exceptionFromAddDBData in + // DumpTestCase + $this->exceptionFromAddDBData = $e; + } + } + + protected function setUp() { + parent::setUp(); + + // Since we will restrict dumping by page ranges (to allow + // working tests, even if the db gets prepopulated by a base + // class), we have to assert, that the page id are consecutively + // increasing + $this->assertEquals( + array( $this->pageId2, $this->pageId3, $this->pageId4 ), + array( $this->pageId1 + 1, $this->pageId2 + 1, $this->pageId3 + 1 ), + "Page ids increasing without holes" ); + } + + function testPlain() { + // Setting up the dump + $nameStub = $this->setUpStub(); + $nameFull = $this->getNewTempFile(); + $dumper = new TextPassDumper( array( "--stub=file:" . $nameStub, + "--output=file:" . $nameFull ) ); + $dumper->reporting = false; + $dumper->setDb( $this->db ); + + // Performing the dump + $dumper->dump( WikiExporter::FULL, WikiExporter::TEXT ); + + // Checking for correctness of the dumped data + $this->assertDumpStart( $nameFull ); + + // Page 1 + $this->assertPageStart( $this->pageId1, NS_MAIN, "BackupDumperTestP1" ); + $this->assertRevision( $this->revId1_1, "BackupDumperTestP1Summary1", + $this->textId1_1, false, "0bolhl6ol7i6x0e7yq91gxgaan39j87", + "BackupDumperTestP1Text1" ); + $this->assertPageEnd(); + + // Page 2 + $this->assertPageStart( $this->pageId2, NS_MAIN, "BackupDumperTestP2" ); + $this->assertRevision( $this->revId2_1, "BackupDumperTestP2Summary1", + $this->textId2_1, false, "jprywrymfhysqllua29tj3sc7z39dl2", + "BackupDumperTestP2Text1" ); + $this->assertRevision( $this->revId2_2, "BackupDumperTestP2Summary2", + $this->textId2_2, false, "b7vj5ks32po5m1z1t1br4o7scdwwy95", + "BackupDumperTestP2Text2", $this->revId2_1 ); + $this->assertRevision( $this->revId2_3, "BackupDumperTestP2Summary3", + $this->textId2_3, false, "jfunqmh1ssfb8rs43r19w98k28gg56r", + "BackupDumperTestP2Text3", $this->revId2_2 ); + $this->assertRevision( $this->revId2_4, "BackupDumperTestP2Summary4 extra", + $this->textId2_4, false, "6o1ciaxa6pybnqprmungwofc4lv00wv", + "BackupDumperTestP2Text4 some additional Text", $this->revId2_3 ); + $this->assertPageEnd(); + + // Page 3 + // -> Page is marked deleted. Hence not visible + + // Page 4 + $this->assertPageStart( $this->pageId4, NS_TALK, "Talk:BackupDumperTestP1" ); + $this->assertRevision( $this->revId4_1, "Talk BackupDumperTestP1 Summary1", + $this->textId4_1, false, "nktofwzd0tl192k3zfepmlzxoax1lpe", + "Talk about BackupDumperTestP1 Text1" ); + $this->assertPageEnd(); + + $this->assertDumpEnd(); + } + + function testPrefetchPlain() { + // The mapping between ids and text, for the hits of the prefetch mock + $prefetchMap = array( + array( $this->pageId1, $this->revId1_1, "Prefetch_________1Text1" ), + array( $this->pageId2, $this->revId2_3, "Prefetch_________2Text3" ) + ); + + // The mock itself + $prefetchMock = $this->getMock( 'BaseDump', array( 'prefetch' ), array(), '', false ); + $prefetchMock->expects( $this->exactly( 6 ) ) + ->method( 'prefetch' ) + ->will( $this->returnValueMap( $prefetchMap ) ); + + // Setting up of the dump + $nameStub = $this->setUpStub(); + $nameFull = $this->getNewTempFile(); + $dumper = new TextPassDumper( array( "--stub=file:" + . $nameStub, "--output=file:" . $nameFull ) ); + $dumper->prefetch = $prefetchMock; + $dumper->reporting = false; + $dumper->setDb( $this->db ); + + // Performing the dump + $dumper->dump( WikiExporter::FULL, WikiExporter::TEXT ); + + // Checking for correctness of the dumped data + $this->assertDumpStart( $nameFull ); + + // Page 1 + $this->assertPageStart( $this->pageId1, NS_MAIN, "BackupDumperTestP1" ); + // Prefetch kicks in. This is still the SHA-1 of the original text, + // But the actual text (with different SHA-1) comes from prefetch. + $this->assertRevision( $this->revId1_1, "BackupDumperTestP1Summary1", + $this->textId1_1, false, "0bolhl6ol7i6x0e7yq91gxgaan39j87", + "Prefetch_________1Text1" ); + $this->assertPageEnd(); + + // Page 2 + $this->assertPageStart( $this->pageId2, NS_MAIN, "BackupDumperTestP2" ); + $this->assertRevision( $this->revId2_1, "BackupDumperTestP2Summary1", + $this->textId2_1, false, "jprywrymfhysqllua29tj3sc7z39dl2", + "BackupDumperTestP2Text1" ); + $this->assertRevision( $this->revId2_2, "BackupDumperTestP2Summary2", + $this->textId2_2, false, "b7vj5ks32po5m1z1t1br4o7scdwwy95", + "BackupDumperTestP2Text2", $this->revId2_1 ); + // Prefetch kicks in. This is still the SHA-1 of the original text, + // But the actual text (with different SHA-1) comes from prefetch. + $this->assertRevision( $this->revId2_3, "BackupDumperTestP2Summary3", + $this->textId2_3, false, "jfunqmh1ssfb8rs43r19w98k28gg56r", + "Prefetch_________2Text3", $this->revId2_2 ); + $this->assertRevision( $this->revId2_4, "BackupDumperTestP2Summary4 extra", + $this->textId2_4, false, "6o1ciaxa6pybnqprmungwofc4lv00wv", + "BackupDumperTestP2Text4 some additional Text", $this->revId2_3 ); + $this->assertPageEnd(); + + // Page 3 + // -> Page is marked deleted. Hence not visible + + // Page 4 + $this->assertPageStart( $this->pageId4, NS_TALK, "Talk:BackupDumperTestP1" ); + $this->assertRevision( $this->revId4_1, "Talk BackupDumperTestP1 Summary1", + $this->textId4_1, false, "nktofwzd0tl192k3zfepmlzxoax1lpe", + "Talk about BackupDumperTestP1 Text1" ); + $this->assertPageEnd(); + + $this->assertDumpEnd(); + } + + /** + * Ensures that checkpoint dumps are used and written, by successively increasing the + * stub size and dumping until the duration crosses a threshold. + * + * @param $checkpointFormat string: Either "file" for plain text or "gzip" for gzipped + * checkpoint files. + */ + private function checkpointHelper( $checkpointFormat = "file" ) { + // Getting temporary names + $nameStub = $this->getNewTempFile(); + $nameOutputDir = $this->getNewTempDirectory(); + + $stderr = fopen( 'php://output', 'a' ); + if ( $stderr === false ) { + $this->fail( "Could not open stream for stderr" ); + } + + $iterations = 32; // We'll start with that many iterations of revisions in stub + $lastDuration = 0; + $minDuration = 2; // We want the dump to take at least this many seconds + $checkpointAfter = 0.5; // Generate checkpoint after this many seconds + + // Until a dump takes at least $minDuration seconds, perform a dump and check + // duration. If the dump did not take long enough increase the iteration + // count, to generate a bigger stub file next time. + while ( $lastDuration < $minDuration ) { + + // Setting up the dump + wfRecursiveRemoveDir( $nameOutputDir ); + $this->assertTrue( wfMkdirParents( $nameOutputDir ), + "Creating temporary output directory " ); + $this->setUpStub( $nameStub, $iterations ); + $dumper = new TextPassDumper( array( "--stub=file:" . $nameStub, + "--output=" . $checkpointFormat . ":" . $nameOutputDir . "/full", + "--maxtime=1" /*This is in minutes. Fixup is below*/, + "--checkpointfile=checkpoint-%s-%s.xml.gz" ) ); + $dumper->setDb( $this->db ); + $dumper->maxTimeAllowed = $checkpointAfter; // Patching maxTime from 1 minute + $dumper->stderr = $stderr; + + // The actual dump and taking time + $ts_before = microtime( true ); + $dumper->dump( WikiExporter::FULL, WikiExporter::TEXT ); + $ts_after = microtime( true ); + $lastDuration = $ts_after - $ts_before; + + // Handling increasing the iteration count for the stubs + if ( $lastDuration < $minDuration ) { + $old_iterations = $iterations; + if ( $lastDuration > 0.2 ) { + // lastDuration is big enough, to allow an educated guess + $factor = ( $minDuration + 0.5 ) / $lastDuration; + if ( ( $factor > 1.1 ) && ( $factor < 100 ) ) { + // educated guess is reasonable + $iterations = (int)( $iterations * $factor ); + } + } + + if ( $old_iterations == $iterations ) { + // Heuristics were not applied, so we just *2. + $iterations *= 2; + } + + $this->assertLessThan( 50000, $iterations, + "Emergency stop against infinitely increasing iteration " + . "count ( last duration: $lastDuration )" ); + } + } + + // The dump (hopefully) did take long enough to produce more than one + // checkpoint file. + // + // We now check all the checkpoint files for validity. + + $files = scandir( $nameOutputDir ); + $this->assertTrue( asort( $files ), "Sorting files in temporary directory" ); + $fileOpened = false; + $lookingForPage = 1; + $checkpointFiles = 0; + + // Each run of the following loop body tries to handle exactly 1 /page/ (not + // iteration of stub content). $i is only increased after having treated page 4. + for ( $i = 0; $i < $iterations; ) { + + // 1. Assuring a file is opened and ready. Skipping across header if + // necessary. + if ( !$fileOpened ) { + $this->assertNotEmpty( $files, "No more existing dump files, " + . "but not yet all pages found" ); + $fname = array_shift( $files ); + while ( $fname == "." || $fname == ".." ) { + $this->assertNotEmpty( $files, "No more existing dump" + . " files, but not yet all pages found" ); + $fname = array_shift( $files ); + } + if ( $checkpointFormat == "gzip" ) { + $this->gunzip( $nameOutputDir . "/" . $fname ); + } + $this->assertDumpStart( $nameOutputDir . "/" . $fname ); + $fileOpened = true; + $checkpointFiles++; + } + + // 2. Performing a single page check + switch ( $lookingForPage ) { + case 1: + // Page 1 + $this->assertPageStart( $this->pageId1 + $i * self::$numOfPages, NS_MAIN, + "BackupDumperTestP1" ); + $this->assertRevision( $this->revId1_1 + $i * self::$numOfRevs, "BackupDumperTestP1Summary1", + $this->textId1_1, false, "0bolhl6ol7i6x0e7yq91gxgaan39j87", + "BackupDumperTestP1Text1" ); + $this->assertPageEnd(); + + $lookingForPage = 2; + break; + + case 2: + // Page 2 + $this->assertPageStart( $this->pageId2 + $i * self::$numOfPages, NS_MAIN, + "BackupDumperTestP2" ); + $this->assertRevision( $this->revId2_1 + $i * self::$numOfRevs, "BackupDumperTestP2Summary1", + $this->textId2_1, false, "jprywrymfhysqllua29tj3sc7z39dl2", + "BackupDumperTestP2Text1" ); + $this->assertRevision( $this->revId2_2 + $i * self::$numOfRevs, "BackupDumperTestP2Summary2", + $this->textId2_2, false, "b7vj5ks32po5m1z1t1br4o7scdwwy95", + "BackupDumperTestP2Text2", $this->revId2_1 + $i * self::$numOfRevs ); + $this->assertRevision( $this->revId2_3 + $i * self::$numOfRevs, "BackupDumperTestP2Summary3", + $this->textId2_3, false, "jfunqmh1ssfb8rs43r19w98k28gg56r", + "BackupDumperTestP2Text3", $this->revId2_2 + $i * self::$numOfRevs ); + $this->assertRevision( $this->revId2_4 + $i * self::$numOfRevs, + "BackupDumperTestP2Summary4 extra", + $this->textId2_4, false, "6o1ciaxa6pybnqprmungwofc4lv00wv", + "BackupDumperTestP2Text4 some additional Text", + $this->revId2_3 + $i * self::$numOfRevs ); + $this->assertPageEnd(); + + $lookingForPage = 4; + break; + + case 4: + // Page 4 + $this->assertPageStart( $this->pageId4 + $i * self::$numOfPages, NS_TALK, + "Talk:BackupDumperTestP1" ); + $this->assertRevision( $this->revId4_1 + $i * self::$numOfRevs, + "Talk BackupDumperTestP1 Summary1", + $this->textId4_1, false, "nktofwzd0tl192k3zfepmlzxoax1lpe", + "Talk about BackupDumperTestP1 Text1" ); + $this->assertPageEnd(); + + $lookingForPage = 1; + + // We dealt with the whole iteration. + $i++; + break; + + default: + $this->fail( "Bad setting for lookingForPage ($lookingForPage)" ); + } + + // 3. Checking for the end of the current checkpoint file + if ( $this->xml->nodeType == XMLReader::END_ELEMENT + && $this->xml->name == "mediawiki" + ) { + $this->assertDumpEnd(); + $fileOpened = false; + } + } + + // Assuring we completely read all files ... + $this->assertFalse( $fileOpened, "Currently read file still open?" ); + $this->assertEmpty( $files, "Remaining unchecked files" ); + + // ... and have dealt with more than one checkpoint file + $this->assertGreaterThan( 1, $checkpointFiles, "expected more than 1 checkpoint to have been created. Checkpoint interval is $checkpointAfter seconds, maybe your computer is too fast?" ); + + $this->expectETAOutput(); + } + + /** + * @group large + */ + function testCheckpointPlain() { + $this->checkpointHelper(); + } + + /** + * tests for working checkpoint generation in gzip format work. + * + * We keep this test in addition to the simpler self::testCheckpointPlain, as there + * were once problems when the used sinks were DumpPipeOutputs. + * + * xmldumps-backup typically uses bzip2 instead of gzip. However, as bzip2 requires + * PHP extensions, we go for gzip instead, which triggers the same relevant code + * paths while still being testable on more systems. + * + * @group large + */ + function testCheckpointGzip() { + $this->checkHasGzip(); + $this->checkpointHelper( "gzip" ); + } + + + /** + * Creates a stub file that is used for testing the text pass of dumps + * + * @param $fname string: (Optional) Absolute name of the file to write + * the stub into. If this parameter is null, a new temporary + * file is generated that is automatically removed upon + * tearDown. + * @param $iterations integer: (Optional) specifies how often the block + * of 3 pages should go into the stub file. The page and + * revision id increase further and further, while the text + * id of the first iteration is reused. The pages and revision + * of iteration > 1 have no corresponding representation in the + * database. + * @return string absolute filename of the stub + */ + private function setUpStub( $fname = null, $iterations = 1 ) { + if ( $fname === null ) { + $fname = $this->getNewTempFile(); + } + $header = ' + + wikisvn + http://localhost/wiki-svn/index.php/Main_Page + MediaWiki 1.21alpha + first-letter + + Media + Special + + Talk + User + User talk + Wikisvn + Wikisvn talk + File + File talk + MediaWiki + MediaWiki talk + Template + Template talk + Help + Help talk + Category + Category talk + + +'; + $tail = ' +'; + + $content = $header; + $iterations = intval( $iterations ); + for ( $i = 0; $i < $iterations; $i++ ) { + + $page1 = ' + BackupDumperTestP1 + 0 + ' . ( $this->pageId1 + $i * self::$numOfPages ) . ' + + ' . ( $this->revId1_1 + $i * self::$numOfRevs ) . ' + 2012-04-01T16:46:05Z + + 127.0.0.1 + + BackupDumperTestP1Summary1 + 0bolhl6ol7i6x0e7yq91gxgaan39j87 + wikitext + text/x-wiki + + + +'; + $page2 = ' + BackupDumperTestP2 + 0 + ' . ( $this->pageId2 + $i * self::$numOfPages ) . ' + + ' . ( $this->revId2_1 + $i * self::$numOfRevs ) . ' + 2012-04-01T16:46:05Z + + 127.0.0.1 + + BackupDumperTestP2Summary1 + jprywrymfhysqllua29tj3sc7z39dl2 + wikitext + text/x-wiki + + + + ' . ( $this->revId2_2 + $i * self::$numOfRevs ) . ' + ' . ( $this->revId2_1 + $i * self::$numOfRevs ) . ' + 2012-04-01T16:46:05Z + + 127.0.0.1 + + BackupDumperTestP2Summary2 + b7vj5ks32po5m1z1t1br4o7scdwwy95 + wikitext + text/x-wiki + + + + ' . ( $this->revId2_3 + $i * self::$numOfRevs ) . ' + ' . ( $this->revId2_2 + $i * self::$numOfRevs ) . ' + 2012-04-01T16:46:05Z + + 127.0.0.1 + + BackupDumperTestP2Summary3 + jfunqmh1ssfb8rs43r19w98k28gg56r + wikitext + text/x-wiki + + + + ' . ( $this->revId2_4 + $i * self::$numOfRevs ) . ' + ' . ( $this->revId2_3 + $i * self::$numOfRevs ) . ' + 2012-04-01T16:46:05Z + + 127.0.0.1 + + BackupDumperTestP2Summary4 extra + 6o1ciaxa6pybnqprmungwofc4lv00wv + wikitext + text/x-wiki + + + +'; + // page 3 not in stub + + $page4 = ' + Talk:BackupDumperTestP1 + 1 + ' . ( $this->pageId4 + $i * self::$numOfPages ) . ' + + ' . ( $this->revId4_1 + $i * self::$numOfRevs ) . ' + 2012-04-01T16:46:05Z + + 127.0.0.1 + + Talk BackupDumperTestP1 Summary1 + nktofwzd0tl192k3zfepmlzxoax1lpe + wikitext + text/x-wiki + + + +'; + $content .= $page1 . $page2 . $page4; + } + $content .= $tail; + $this->assertEquals( strlen( $content ), file_put_contents( + $fname, $content ), "Length of prepared stub" ); + + return $fname; + } +} diff --git a/tests/phpunit/maintenance/backup_LogTest.php b/tests/phpunit/maintenance/backup_LogTest.php new file mode 100644 index 00000000..98d81653 --- /dev/null +++ b/tests/phpunit/maintenance/backup_LogTest.php @@ -0,0 +1,228 @@ +setPerformer( $user ); + $logEntry->setTarget( Title::newFromText( $title, $ns ) ); + if ( $comment !== null ) { + $logEntry->setComment( $comment ); + } + if ( $parameters !== null ) { + $logEntry->setParameters( $parameters ); + } + + return $logEntry->insert(); + } + + function addDBData() { + $this->tablesUsed[] = 'logging'; + $this->tablesUsed[] = 'user'; + + try { + $user1 = User::newFromName( 'BackupDumperLogUserA' ); + $this->userId1 = $user1->getId(); + if ( $this->userId1 === 0 ) { + $user1->addToDatabase(); + $this->userId1 = $user1->getId(); + } + $this->assertGreaterThan( 0, $this->userId1 ); + + $user2 = User::newFromName( 'BackupDumperLogUserB' ); + $this->userId2 = $user2->getId(); + if ( $this->userId2 === 0 ) { + $user2->addToDatabase(); + $this->userId2 = $user2->getId(); + } + $this->assertGreaterThan( 0, $this->userId2 ); + + $this->logId1 = $this->addLogEntry( 'type', 'subtype', + $user1, NS_MAIN, "PageA" ); + $this->assertGreaterThan( 0, $this->logId1 ); + + $this->logId2 = $this->addLogEntry( 'supress', 'delete', + $user2, NS_TALK, "PageB", "SomeComment" ); + $this->assertGreaterThan( 0, $this->logId2 ); + + $this->logId3 = $this->addLogEntry( 'move', 'delete', + $user2, NS_MAIN, "PageA", "SomeOtherComment", + array( 'key1' => 1, 3 => 'value3' ) ); + $this->assertGreaterThan( 0, $this->logId3 ); + } catch ( Exception $e ) { + // We'd love to pass $e directly. However, ... see + // documentation of exceptionFromAddDBData in + // DumpTestCase + $this->exceptionFromAddDBData = $e; + } + } + + + /** + * asserts that the xml reader is at the beginning of a log entry and skips over + * it while analyzing it. + * + * @param $id int: id of the log entry + * @param $user_name string: user name of the log entry's performer + * @param $user_id int: user id of the log entry 's performer + * @param $comment string|null: comment of the log entry. If null, the comment + * text is ignored. + * @param $type string: type of the log entry + * @param $subtype string: subtype of the log entry + * @param $title string: title of the log entry's target + * @param $parameters array: (optional) unserialized data accompanying the log entry + */ + private function assertLogItem( $id, $user_name, $user_id, $comment, $type, + $subtype, $title, $parameters = array() + ) { + + $this->assertNodeStart( "logitem" ); + $this->skipWhitespace(); + + $this->assertTextNode( "id", $id ); + $this->assertTextNode( "timestamp", false ); + + $this->assertNodeStart( "contributor" ); + $this->skipWhitespace(); + $this->assertTextNode( "username", $user_name ); + $this->assertTextNode( "id", $user_id ); + $this->assertNodeEnd( "contributor" ); + $this->skipWhitespace(); + + if ( $comment !== null ) { + $this->assertTextNode( "comment", $comment ); + } + $this->assertTextNode( "type", $type ); + $this->assertTextNode( "action", $subtype ); + $this->assertTextNode( "logtitle", $title ); + + $this->assertNodeStart( "params" ); + $parameters_xml = unserialize( $this->xml->value ); + $this->assertEquals( $parameters, $parameters_xml ); + $this->assertTrue( $this->xml->read(), "Skipping past processed text of params" ); + $this->assertNodeEnd( "params" ); + $this->skipWhitespace(); + + $this->assertNodeEnd( "logitem" ); + $this->skipWhitespace(); + } + + function testPlain() { + global $wgContLang; + + // Preparing the dump + $fname = $this->getNewTempFile(); + $dumper = new BackupDumper( array( "--output=file:" . $fname ) ); + $dumper->startId = $this->logId1; + $dumper->endId = $this->logId3 + 1; + $dumper->reporting = false; + $dumper->setDb( $this->db ); + + // Performing the dump + $dumper->dump( WikiExporter::LOGS, WikiExporter::TEXT ); + + // Analyzing the dumped data + $this->assertDumpStart( $fname ); + + $this->assertLogItem( $this->logId1, "BackupDumperLogUserA", + $this->userId1, null, "type", "subtype", "PageA" ); + + $this->assertNotNull( $wgContLang, "Content language object validation" ); + $namespace = $wgContLang->getNsText( NS_TALK ); + $this->assertInternalType( 'string', $namespace ); + $this->assertGreaterThan( 0, strlen( $namespace ) ); + $this->assertLogItem( $this->logId2, "BackupDumperLogUserB", + $this->userId2, "SomeComment", "supress", "delete", + $namespace . ":PageB" ); + + $this->assertLogItem( $this->logId3, "BackupDumperLogUserB", + $this->userId2, "SomeOtherComment", "move", "delete", + "PageA", array( 'key1' => 1, 3 => 'value3' ) ); + + $this->assertDumpEnd(); + } + + function testXmlDumpsBackupUseCaseLogging() { + global $wgContLang; + + $this->checkHasGzip(); + + // Preparing the dump + $fname = $this->getNewTempFile(); + $dumper = new BackupDumper( array( "--output=gzip:" . $fname, + "--reporting=2" ) ); + $dumper->startId = $this->logId1; + $dumper->endId = $this->logId3 + 1; + $dumper->setDb( $this->db ); + + // xmldumps-backup demands reporting, although this is currently not + // implemented in BackupDumper, when dumping logging data. We + // nevertheless capture the output of the dump process already now, + // to be able to alert (once dumping produces reports) that this test + // needs updates. + $dumper->stderr = fopen( 'php://output', 'a' ); + if ( $dumper->stderr === false ) { + $this->fail( "Could not open stream for stderr" ); + } + + // Performing the dump + $dumper->dump( WikiExporter::LOGS, WikiExporter::TEXT ); + + $this->assertTrue( fclose( $dumper->stderr ), "Closing stderr handle" ); + + // Analyzing the dumped data + $this->gunzip( $fname ); + + $this->assertDumpStart( $fname ); + + $this->assertLogItem( $this->logId1, "BackupDumperLogUserA", + $this->userId1, null, "type", "subtype", "PageA" ); + + $this->assertNotNull( $wgContLang, "Content language object validation" ); + $namespace = $wgContLang->getNsText( NS_TALK ); + $this->assertInternalType( 'string', $namespace ); + $this->assertGreaterThan( 0, strlen( $namespace ) ); + $this->assertLogItem( $this->logId2, "BackupDumperLogUserB", + $this->userId2, "SomeComment", "supress", "delete", + $namespace . ":PageB" ); + + $this->assertLogItem( $this->logId3, "BackupDumperLogUserB", + $this->userId2, "SomeOtherComment", "move", "delete", + "PageA", array( 'key1' => 1, 3 => 'value3' ) ); + + $this->assertDumpEnd(); + + // Currently, no reporting is implemented. Alert via failure, once + // this changes. + // If reporting for log dumps has been implemented, please update + // the following statement to catch good output + $this->expectOutputString( '' ); + } +} diff --git a/tests/phpunit/maintenance/backup_PageTest.php b/tests/phpunit/maintenance/backup_PageTest.php new file mode 100644 index 00000000..99bd2700 --- /dev/null +++ b/tests/phpunit/maintenance/backup_PageTest.php @@ -0,0 +1,404 @@ +setMwGlobals( array( + 'wgLanguageCode' => 'en', + 'wgContLang' => Language::factory( 'en' ), + ) ); + + $this->tablesUsed[] = 'page'; + $this->tablesUsed[] = 'revision'; + $this->tablesUsed[] = 'text'; + + try { + $this->namespace = $this->getDefaultWikitextNS(); + $this->talk_namespace = NS_TALK; + + if ( $this->namespace === $this->talk_namespace ) { + // @todo work around this. + throw new MWException( "The default wikitext namespace is the talk namespace. " + . " We can't currently deal with that." ); + } + + $this->pageTitle1 = Title::newFromText( 'BackupDumperTestP1', $this->namespace ); + $page = WikiPage::factory( $this->pageTitle1 ); + list( $this->revId1_1, $this->textId1_1 ) = $this->addRevision( $page, + "BackupDumperTestP1Text1", "BackupDumperTestP1Summary1" ); + $this->pageId1 = $page->getId(); + + $this->pageTitle2 = Title::newFromText( 'BackupDumperTestP2', $this->namespace ); + $page = WikiPage::factory( $this->pageTitle2 ); + list( $this->revId2_1, $this->textId2_1 ) = $this->addRevision( $page, + "BackupDumperTestP2Text1", "BackupDumperTestP2Summary1" ); + list( $this->revId2_2, $this->textId2_2 ) = $this->addRevision( $page, + "BackupDumperTestP2Text2", "BackupDumperTestP2Summary2" ); + list( $this->revId2_3, $this->textId2_3 ) = $this->addRevision( $page, + "BackupDumperTestP2Text3", "BackupDumperTestP2Summary3" ); + list( $this->revId2_4, $this->textId2_4 ) = $this->addRevision( $page, + "BackupDumperTestP2Text4 some additional Text ", + "BackupDumperTestP2Summary4 extra " ); + $this->pageId2 = $page->getId(); + + $this->pageTitle3 = Title::newFromText( 'BackupDumperTestP3', $this->namespace ); + $page = WikiPage::factory( $this->pageTitle3 ); + list( $this->revId3_1, $this->textId3_1 ) = $this->addRevision( $page, + "BackupDumperTestP3Text1", "BackupDumperTestP2Summary1" ); + list( $this->revId3_2, $this->textId3_2 ) = $this->addRevision( $page, + "BackupDumperTestP3Text2", "BackupDumperTestP2Summary2" ); + $this->pageId3 = $page->getId(); + $page->doDeleteArticle( "Testing ;)" ); + + $this->pageTitle4 = Title::newFromText( 'BackupDumperTestP1', $this->talk_namespace ); + $page = WikiPage::factory( $this->pageTitle4 ); + list( $this->revId4_1, $this->textId4_1 ) = $this->addRevision( $page, + "Talk about BackupDumperTestP1 Text1", + "Talk BackupDumperTestP1 Summary1" ); + $this->pageId4 = $page->getId(); + } catch ( Exception $e ) { + // We'd love to pass $e directly. However, ... see + // documentation of exceptionFromAddDBData in + // DumpTestCase + $this->exceptionFromAddDBData = $e; + } + } + + protected function setUp() { + parent::setUp(); + + // Since we will restrict dumping by page ranges (to allow + // working tests, even if the db gets prepopulated by a base + // class), we have to assert, that the page id are consecutively + // increasing + $this->assertEquals( + array( $this->pageId2, $this->pageId3, $this->pageId4 ), + array( $this->pageId1 + 1, $this->pageId2 + 1, $this->pageId3 + 1 ), + "Page ids increasing without holes" ); + } + + function testFullTextPlain() { + // Preparing the dump + $fname = $this->getNewTempFile(); + $dumper = new BackupDumper( array( "--output=file:" . $fname ) ); + $dumper->startId = $this->pageId1; + $dumper->endId = $this->pageId4 + 1; + $dumper->reporting = false; + $dumper->setDb( $this->db ); + + // Performing the dump + $dumper->dump( WikiExporter::FULL, WikiExporter::TEXT ); + + // Checking the dumped data + $this->assertDumpStart( $fname ); + + // Page 1 + $this->assertPageStart( $this->pageId1, $this->namespace, $this->pageTitle1->getPrefixedText() ); + $this->assertRevision( $this->revId1_1, "BackupDumperTestP1Summary1", + $this->textId1_1, 23, "0bolhl6ol7i6x0e7yq91gxgaan39j87", + "BackupDumperTestP1Text1" ); + $this->assertPageEnd(); + + // Page 2 + $this->assertPageStart( $this->pageId2, $this->namespace, $this->pageTitle2->getPrefixedText() ); + $this->assertRevision( $this->revId2_1, "BackupDumperTestP2Summary1", + $this->textId2_1, 23, "jprywrymfhysqllua29tj3sc7z39dl2", + "BackupDumperTestP2Text1" ); + $this->assertRevision( $this->revId2_2, "BackupDumperTestP2Summary2", + $this->textId2_2, 23, "b7vj5ks32po5m1z1t1br4o7scdwwy95", + "BackupDumperTestP2Text2", $this->revId2_1 ); + $this->assertRevision( $this->revId2_3, "BackupDumperTestP2Summary3", + $this->textId2_3, 23, "jfunqmh1ssfb8rs43r19w98k28gg56r", + "BackupDumperTestP2Text3", $this->revId2_2 ); + $this->assertRevision( $this->revId2_4, "BackupDumperTestP2Summary4 extra", + $this->textId2_4, 44, "6o1ciaxa6pybnqprmungwofc4lv00wv", + "BackupDumperTestP2Text4 some additional Text", $this->revId2_3 ); + $this->assertPageEnd(); + + // Page 3 + // -> Page is marked deleted. Hence not visible + + // Page 4 + $this->assertPageStart( $this->pageId4, $this->talk_namespace, $this->pageTitle4->getPrefixedText() ); + $this->assertRevision( $this->revId4_1, "Talk BackupDumperTestP1 Summary1", + $this->textId4_1, 35, "nktofwzd0tl192k3zfepmlzxoax1lpe", + "Talk about BackupDumperTestP1 Text1" ); + $this->assertPageEnd(); + + $this->assertDumpEnd(); + } + + function testFullStubPlain() { + // Preparing the dump + $fname = $this->getNewTempFile(); + $dumper = new BackupDumper( array( "--output=file:" . $fname ) ); + $dumper->startId = $this->pageId1; + $dumper->endId = $this->pageId4 + 1; + $dumper->reporting = false; + $dumper->setDb( $this->db ); + + // Performing the dump + $dumper->dump( WikiExporter::FULL, WikiExporter::STUB ); + + // Checking the dumped data + $this->assertDumpStart( $fname ); + + // Page 1 + $this->assertPageStart( $this->pageId1, $this->namespace, $this->pageTitle1->getPrefixedText() ); + $this->assertRevision( $this->revId1_1, "BackupDumperTestP1Summary1", + $this->textId1_1, 23, "0bolhl6ol7i6x0e7yq91gxgaan39j87" ); + $this->assertPageEnd(); + + // Page 2 + $this->assertPageStart( $this->pageId2, $this->namespace, $this->pageTitle2->getPrefixedText() ); + $this->assertRevision( $this->revId2_1, "BackupDumperTestP2Summary1", + $this->textId2_1, 23, "jprywrymfhysqllua29tj3sc7z39dl2" ); + $this->assertRevision( $this->revId2_2, "BackupDumperTestP2Summary2", + $this->textId2_2, 23, "b7vj5ks32po5m1z1t1br4o7scdwwy95", false, $this->revId2_1 ); + $this->assertRevision( $this->revId2_3, "BackupDumperTestP2Summary3", + $this->textId2_3, 23, "jfunqmh1ssfb8rs43r19w98k28gg56r", false, $this->revId2_2 ); + $this->assertRevision( $this->revId2_4, "BackupDumperTestP2Summary4 extra", + $this->textId2_4, 44, "6o1ciaxa6pybnqprmungwofc4lv00wv", false, $this->revId2_3 ); + $this->assertPageEnd(); + + // Page 3 + // -> Page is marked deleted. Hence not visible + + // Page 4 + $this->assertPageStart( $this->pageId4, $this->talk_namespace, $this->pageTitle4->getPrefixedText() ); + $this->assertRevision( $this->revId4_1, "Talk BackupDumperTestP1 Summary1", + $this->textId4_1, 35, "nktofwzd0tl192k3zfepmlzxoax1lpe" ); + $this->assertPageEnd(); + + $this->assertDumpEnd(); + } + + function testCurrentStubPlain() { + // Preparing the dump + $fname = $this->getNewTempFile(); + $dumper = new BackupDumper( array( "--output=file:" . $fname ) ); + $dumper->startId = $this->pageId1; + $dumper->endId = $this->pageId4 + 1; + $dumper->reporting = false; + $dumper->setDb( $this->db ); + + // Performing the dump + $dumper->dump( WikiExporter::CURRENT, WikiExporter::STUB ); + + // Checking the dumped data + $this->assertDumpStart( $fname ); + + // Page 1 + $this->assertPageStart( $this->pageId1, $this->namespace, $this->pageTitle1->getPrefixedText() ); + $this->assertRevision( $this->revId1_1, "BackupDumperTestP1Summary1", + $this->textId1_1, 23, "0bolhl6ol7i6x0e7yq91gxgaan39j87" ); + $this->assertPageEnd(); + + // Page 2 + $this->assertPageStart( $this->pageId2, $this->namespace, $this->pageTitle2->getPrefixedText() ); + $this->assertRevision( $this->revId2_4, "BackupDumperTestP2Summary4 extra", + $this->textId2_4, 44, "6o1ciaxa6pybnqprmungwofc4lv00wv", false, $this->revId2_3 ); + $this->assertPageEnd(); + + // Page 3 + // -> Page is marked deleted. Hence not visible + + // Page 4 + $this->assertPageStart( $this->pageId4, $this->talk_namespace, $this->pageTitle4->getPrefixedText() ); + $this->assertRevision( $this->revId4_1, "Talk BackupDumperTestP1 Summary1", + $this->textId4_1, 35, "nktofwzd0tl192k3zfepmlzxoax1lpe" ); + $this->assertPageEnd(); + + $this->assertDumpEnd(); + } + + function testCurrentStubGzip() { + $this->checkHasGzip(); + + // Preparing the dump + $fname = $this->getNewTempFile(); + $dumper = new BackupDumper( array( "--output=gzip:" . $fname ) ); + $dumper->startId = $this->pageId1; + $dumper->endId = $this->pageId4 + 1; + $dumper->reporting = false; + $dumper->setDb( $this->db ); + + // Performing the dump + $dumper->dump( WikiExporter::CURRENT, WikiExporter::STUB ); + + // Checking the dumped data + $this->gunzip( $fname ); + $this->assertDumpStart( $fname ); + + // Page 1 + $this->assertPageStart( $this->pageId1, $this->namespace, $this->pageTitle1->getPrefixedText() ); + $this->assertRevision( $this->revId1_1, "BackupDumperTestP1Summary1", + $this->textId1_1, 23, "0bolhl6ol7i6x0e7yq91gxgaan39j87" ); + $this->assertPageEnd(); + + // Page 2 + $this->assertPageStart( $this->pageId2, $this->namespace, $this->pageTitle2->getPrefixedText() ); + $this->assertRevision( $this->revId2_4, "BackupDumperTestP2Summary4 extra", + $this->textId2_4, 44, "6o1ciaxa6pybnqprmungwofc4lv00wv", false, $this->revId2_3 ); + $this->assertPageEnd(); + + // Page 3 + // -> Page is marked deleted. Hence not visible + + // Page 4 + $this->assertPageStart( $this->pageId4, $this->talk_namespace, $this->pageTitle4->getPrefixedText() ); + $this->assertRevision( $this->revId4_1, "Talk BackupDumperTestP1 Summary1", + $this->textId4_1, 35, "nktofwzd0tl192k3zfepmlzxoax1lpe" ); + $this->assertPageEnd(); + + $this->assertDumpEnd(); + } + + + function testXmlDumpsBackupUseCase() { + // xmldumps-backup typically performs a single dump that that writes + // out three files + // * gzipped stubs of everything (meta-history) + // * gzipped stubs of latest revisions of all pages (meta-current) + // * gzipped stubs of latest revisions of all pages of namespage 0 + // (articles) + // + // We reproduce such a setup with our mini fixture, although we omit + // chunks, and all the other gimmicks of xmldumps-backup. + // + $this->checkHasGzip(); + + $fnameMetaHistory = $this->getNewTempFile(); + $fnameMetaCurrent = $this->getNewTempFile(); + $fnameArticles = $this->getNewTempFile(); + + $dumper = new BackupDumper( array( "--output=gzip:" . $fnameMetaHistory, + "--output=gzip:" . $fnameMetaCurrent, "--filter=latest", + "--output=gzip:" . $fnameArticles, "--filter=latest", + "--filter=notalk", "--filter=namespace:!NS_USER", + "--reporting=1000" ) ); + $dumper->startId = $this->pageId1; + $dumper->endId = $this->pageId4 + 1; + $dumper->setDb( $this->db ); + + // xmldumps-backup uses reporting. We will not check the exact reported + // message, as they are dependent on the processing power of the used + // computer. We only check that reporting does not crash the dumping + // and that something is reported + $dumper->stderr = fopen( 'php://output', 'a' ); + if ( $dumper->stderr === false ) { + $this->fail( "Could not open stream for stderr" ); + } + + // Performing the dump + $dumper->dump( WikiExporter::FULL, WikiExporter::STUB ); + + $this->assertTrue( fclose( $dumper->stderr ), "Closing stderr handle" ); + + // Checking meta-history ------------------------------------------------- + + $this->gunzip( $fnameMetaHistory ); + $this->assertDumpStart( $fnameMetaHistory ); + + // Page 1 + $this->assertPageStart( $this->pageId1, $this->namespace, $this->pageTitle1->getPrefixedText() ); + $this->assertRevision( $this->revId1_1, "BackupDumperTestP1Summary1", + $this->textId1_1, 23, "0bolhl6ol7i6x0e7yq91gxgaan39j87" ); + $this->assertPageEnd(); + + // Page 2 + $this->assertPageStart( $this->pageId2, $this->namespace, $this->pageTitle2->getPrefixedText() ); + $this->assertRevision( $this->revId2_1, "BackupDumperTestP2Summary1", + $this->textId2_1, 23, "jprywrymfhysqllua29tj3sc7z39dl2" ); + $this->assertRevision( $this->revId2_2, "BackupDumperTestP2Summary2", + $this->textId2_2, 23, "b7vj5ks32po5m1z1t1br4o7scdwwy95", false, $this->revId2_1 ); + $this->assertRevision( $this->revId2_3, "BackupDumperTestP2Summary3", + $this->textId2_3, 23, "jfunqmh1ssfb8rs43r19w98k28gg56r", false, $this->revId2_2 ); + $this->assertRevision( $this->revId2_4, "BackupDumperTestP2Summary4 extra", + $this->textId2_4, 44, "6o1ciaxa6pybnqprmungwofc4lv00wv", false, $this->revId2_3 ); + $this->assertPageEnd(); + + // Page 3 + // -> Page is marked deleted. Hence not visible + + // Page 4 + $this->assertPageStart( $this->pageId4, $this->talk_namespace, $this->pageTitle4->getPrefixedText() ); + $this->assertRevision( $this->revId4_1, "Talk BackupDumperTestP1 Summary1", + $this->textId4_1, 35, "nktofwzd0tl192k3zfepmlzxoax1lpe" ); + $this->assertPageEnd(); + + $this->assertDumpEnd(); + + // Checking meta-current ------------------------------------------------- + + $this->gunzip( $fnameMetaCurrent ); + $this->assertDumpStart( $fnameMetaCurrent ); + + // Page 1 + $this->assertPageStart( $this->pageId1, $this->namespace, $this->pageTitle1->getPrefixedText() ); + $this->assertRevision( $this->revId1_1, "BackupDumperTestP1Summary1", + $this->textId1_1, 23, "0bolhl6ol7i6x0e7yq91gxgaan39j87" ); + $this->assertPageEnd(); + + // Page 2 + $this->assertPageStart( $this->pageId2, $this->namespace, $this->pageTitle2->getPrefixedText() ); + $this->assertRevision( $this->revId2_4, "BackupDumperTestP2Summary4 extra", + $this->textId2_4, 44, "6o1ciaxa6pybnqprmungwofc4lv00wv", false, $this->revId2_3 ); + $this->assertPageEnd(); + + // Page 3 + // -> Page is marked deleted. Hence not visible + + // Page 4 + $this->assertPageStart( $this->pageId4, $this->talk_namespace, $this->pageTitle4->getPrefixedText() ); + $this->assertRevision( $this->revId4_1, "Talk BackupDumperTestP1 Summary1", + $this->textId4_1, 35, "nktofwzd0tl192k3zfepmlzxoax1lpe" ); + $this->assertPageEnd(); + + $this->assertDumpEnd(); + + // Checking articles ------------------------------------------------- + + $this->gunzip( $fnameArticles ); + $this->assertDumpStart( $fnameArticles ); + + // Page 1 + $this->assertPageStart( $this->pageId1, $this->namespace, $this->pageTitle1->getPrefixedText() ); + $this->assertRevision( $this->revId1_1, "BackupDumperTestP1Summary1", + $this->textId1_1, 23, "0bolhl6ol7i6x0e7yq91gxgaan39j87" ); + $this->assertPageEnd(); + + // Page 2 + $this->assertPageStart( $this->pageId2, $this->namespace, $this->pageTitle2->getPrefixedText() ); + $this->assertRevision( $this->revId2_4, "BackupDumperTestP2Summary4 extra", + $this->textId2_4, 44, "6o1ciaxa6pybnqprmungwofc4lv00wv", false, $this->revId2_3 ); + $this->assertPageEnd(); + + // Page 3 + // -> Page is marked deleted. Hence not visible + + // Page 4 + // -> Page is not in $this->namespace. Hence not visible + + $this->assertDumpEnd(); + + $this->expectETAOutput(); + } +} diff --git a/tests/phpunit/maintenance/fetchTextTest.php b/tests/phpunit/maintenance/fetchTextTest.php new file mode 100644 index 00000000..e8df199e --- /dev/null +++ b/tests/phpunit/maintenance/fetchTextTest.php @@ -0,0 +1,238 @@ + 0 ); + + + /** + * Data for the fake stdin + * + * @param $stdin String The string to be used instead of stdin + */ + function mockStdin( $stdin ) { + $this->mockStdinText = $stdin; + $this->mockSetUp = true; + } + + /** + * Gets invocation counters for mocked methods. + * + * @return Array An array, whose keys are function names. The corresponding values + * denote the number of times the function has been invoked. + */ + function mockGetInvocations() { + return $this->mockInvocations; + } + + // ----------------------------------------------------------------- + // Mocked functions from FetchText follow. + + function getStdin( $len = null ) { + $this->mockInvocations['getStdin']++; + if ( $len !== null ) { + throw new PHPUnit_Framework_ExpectationFailedException( + "Tried to get stdin with non null parameter" ); + } + + if ( !$this->mockSetUp ) { + throw new PHPUnit_Framework_ExpectationFailedException( + "Tried to get stdin before setting up rerouting" ); + } + + return fopen( 'data://text/plain,' . $this->mockStdinText, 'r' ); + } +} + +/** + * TestCase for FetchText + * + * @group Database + * @group Dump + */ +class FetchTextTest extends MediaWikiTestCase { + + // We add 5 Revisions for this test. Their corresponding text id's + // are stored in the following 5 variables. + private $textId1; + private $textId2; + private $textId3; + private $textId4; + private $textId5; + + + /** + * @var Exception|null As the current MediaWikiTestCase::run is not + * robust enough to recover from thrown exceptions directly, we cannot + * throw frow within addDBData, although it would be appropriate. Hence, + * we catch the exception and store it until we are in setUp and may + * finally rethrow the exception without crashing the test suite. + */ + private $exceptionFromAddDBData; + + /** + * @var FetchText the (mocked) FetchText that is to test + */ + private $fetchText; + + /** + * Adds a revision to a page, while returning the resuting text's id + * + * @param $page WikiPage The page to add the revision to + * @param $text String The revisions text + * @param $text String The revisions summare + * + * @throws MWExcepion + */ + private function addRevision( $page, $text, $summary ) { + $status = $page->doEditContent( ContentHandler::makeContent( $text, $page->getTitle() ), $summary ); + if ( $status->isGood() ) { + $value = $status->getValue(); + $revision = $value['revision']; + $id = $revision->getTextId(); + if ( $id > 0 ) { + return $id; + } + } + throw new MWException( "Could not determine text id" ); + } + + + function addDBData() { + $this->tablesUsed[] = 'page'; + $this->tablesUsed[] = 'revision'; + $this->tablesUsed[] = 'text'; + + $wikitextNamespace = $this->getDefaultWikitextNS(); + + try { + $title = Title::newFromText( 'FetchTextTestPage1', $wikitextNamespace ); + $page = WikiPage::factory( $title ); + $this->textId1 = $this->addRevision( $page, "FetchTextTestPage1Text1", "FetchTextTestPage1Summary1" ); + + $title = Title::newFromText( 'FetchTextTestPage2', $wikitextNamespace ); + $page = WikiPage::factory( $title ); + $this->textId2 = $this->addRevision( $page, "FetchTextTestPage2Text1", "FetchTextTestPage2Summary1" ); + $this->textId3 = $this->addRevision( $page, "FetchTextTestPage2Text2", "FetchTextTestPage2Summary2" ); + $this->textId4 = $this->addRevision( $page, "FetchTextTestPage2Text3", "FetchTextTestPage2Summary3" ); + $this->textId5 = $this->addRevision( $page, "FetchTextTestPage2Text4 some additional Text ", "FetchTextTestPage2Summary4 extra " ); + } catch ( Exception $e ) { + // We'd love to pass $e directly. However, ... see + // documentation of exceptionFromAddDBData + $this->exceptionFromAddDBData = $e; + } + } + + + protected function setUp() { + parent::setUp(); + + // Check if any Exception is stored for rethrowing from addDBData + if ( $this->exceptionFromAddDBData !== null ) { + throw $this->exceptionFromAddDBData; + } + + $this->fetchText = new SemiMockedFetchText(); + } + + + /** + * Helper to relate FetchText's input and output + */ + private function assertFilter( $input, $expectedOutput ) { + $this->fetchText->mockStdin( $input ); + $this->fetchText->execute(); + $invocations = $this->fetchText->mockGetInvocations(); + $this->assertEquals( 1, $invocations['getStdin'], + "getStdin invocation counter" ); + $this->expectOutputString( $expectedOutput ); + } + + + // Instead of the following functions, a data provider would be great. + // However, as data providers are evaluated /before/ addDBData, a data + // provider would not know the required ids. + + function testExistingSimple() { + $this->assertFilter( $this->textId2, + $this->textId2 . "\n23\nFetchTextTestPage2Text1" ); + } + + function testExistingSimpleWithNewline() { + $this->assertFilter( $this->textId2 . "\n", + $this->textId2 . "\n23\nFetchTextTestPage2Text1" ); + } + + function testExistingSeveral() { + $this->assertFilter( "$this->textId1\n$this->textId5\n" + . "$this->textId3\n$this->textId3", + implode( "", array( + $this->textId1 . "\n23\nFetchTextTestPage1Text1", + $this->textId5 . "\n44\nFetchTextTestPage2Text4 " + . "some additional Text", + $this->textId3 . "\n23\nFetchTextTestPage2Text2", + $this->textId3 . "\n23\nFetchTextTestPage2Text2" + ) ) ); + } + + function testEmpty() { + $this->assertFilter( "", null ); + } + + function testNonExisting() { + $this->assertFilter( $this->textId5 + 10, ( $this->textId5 + 10 ) . "\n-1\n" ); + } + + function testNegativeInteger() { + $this->assertFilter( "-42", "-42\n-1\n" ); + } + + function testFloatingPointNumberExisting() { + // float -> int -> revision + $this->assertFilter( $this->textId3 + 0.14159, + $this->textId3 . "\n23\nFetchTextTestPage2Text2" ); + } + + function testFloatingPointNumberNonExisting() { + $this->assertFilter( $this->textId5 + 3.14159, + ( $this->textId5 + 3 ) . "\n-1\n" ); + } + + function testCharacters() { + $this->assertFilter( "abc", "0\n-1\n" ); + } + + function testMix() { + $this->assertFilter( "ab\n" . $this->textId4 . ".5cd\n\nefg\n" . $this->textId2 + . "\n" . $this->textId3, + implode( "", array( + "0\n-1\n", + $this->textId4 . "\n23\nFetchTextTestPage2Text3", + "0\n-1\n", + "0\n-1\n", + $this->textId2 . "\n23\nFetchTextTestPage2Text1", + $this->textId3 . "\n23\nFetchTextTestPage2Text2" + ) ) ); + } +} diff --git a/tests/phpunit/maintenance/getSlaveServerTest.php b/tests/phpunit/maintenance/getSlaveServerTest.php new file mode 100644 index 00000000..2c848862 --- /dev/null +++ b/tests/phpunit/maintenance/getSlaveServerTest.php @@ -0,0 +1,67 @@ +db->getType() === 'sqlite' ) { + // for SQLite, only the empty string is a good server name + return ''; + } + + $octet = '([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])'; + $ip = "(($octet\.){3}$octet)"; + + $label = '([a-zA-Z]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)'; + $hostname = "($label(\.$label)*)"; + + return "($ip|$hostname)(:[0-9]{1,5})?"; + } + + function testPlain() { + $gss = new GetSlaveServer(); + $gss->execute(); + + $this->expectOutputRegex( "/^" . self::getServerRE() . "\n$/D" ); + } + + function testXmlDumpsBackupUseCase() { + global $wgDBprefix; + + global $argv; + $argv = array( null, "--globals" ); + + $gss = new GetSlaveServer(); + $gss->loadParamsAndArgs(); + $gss->execute(); + $gss->globals(); + + // The main answer + $output = $this->getActualOutput(); + $firstLineEndPos = strpos( $output, "\n" ); + if ( $firstLineEndPos === false ) { + $this->fail( "Could not find end of first line of output" ); + } + $firstLine = substr( $output, 0, $firstLineEndPos ); + $this->assertRegExp( "/^" . self::getServerRE() . "$/D", + $firstLine, "DB Server" ); + + // xmldumps-backup relies on the wgDBprefix in the output. + $this->expectOutputRegex( "/^[[:space:]]*\[wgDBprefix\][[:space:]]*=> " + . $wgDBprefix . "$/m" ); + } +} diff --git a/tests/phpunit/mocks/filebackend/MockFSFile.php b/tests/phpunit/mocks/filebackend/MockFSFile.php new file mode 100644 index 00000000..e0463281 --- /dev/null +++ b/tests/phpunit/mocks/filebackend/MockFSFile.php @@ -0,0 +1,69 @@ + $this->exists(), + 'size' => $this->getSize(), + 'file-mime' => $this->getMimeType(), + 'sha1' => $this->getSha1Base36(), + ); + } + + public function getSha1Base36( $recache = false ) { + return '1234567890123456789012345678901'; + } +} diff --git a/tests/phpunit/mocks/filebackend/MockFileBackend.php b/tests/phpunit/mocks/filebackend/MockFileBackend.php new file mode 100644 index 00000000..49aefbd1 --- /dev/null +++ b/tests/phpunit/mocks/filebackend/MockFileBackend.php @@ -0,0 +1,122 @@ + + */ + +/** + * Class simulating a backend store. + * + * @ingroup FileBackend + * @since 1.22 + */ +class MockFileBackend extends FileBackendStore { + + protected $mocked = array(); + + /** Poor man debugging */ + protected function debug( $msg = '' ) { + wfDebug( wfGetCaller() . "$msg\n" ); + } + + public function isPathUsableInternal( $storagePath ) { + return true; + } + + protected function doCreateInternal( array $params ) { + if ( isset( $params['content'] ) ) { + $content = $params['content']; + } else { + $content = 'Default mocked file content'; + } + $this->debug( serialize( $params ) ); + $dst = $params['dst']; + $this->mocked[$dst] = $content; + return Status::newGood(); + } + + protected function doStoreInternal( array $params ) { + $this->debug( serialize( $params ) ); + return $this->doCreateInternal( $params ); + } + + protected function doCopyInternal( array $params ) { + $this->debug( serialize( $params ) ); + $src = $params['src']; + $dst = $params['dst']; + $this->mocked[$dst] = $this->mocked[$src]; + return Status::newGood(); + } + + protected function doDeleteInternal( array $params ) { + $this->debug( serialize( $params ) ); + $src = $params['src']; + unset( $this->mocked[$src] ); + return Status::newGood(); + } + + protected function doGetFileStat( array $params ) { + $src = $params['src']; + if ( array_key_exists( $src, $this->mocked ) ) { + $this->debug( "('$src') found" ); + return array( + 'mtime' => wfTimestamp( TS_MW ), + 'size' => strlen( $this->mocked[$src] ), + # No sha1, stat does not need it. + ); + } else { + $this->debug( "('$src') not found" ); + return false; + } + } + + protected function doGetLocalCopyMulti( array $params ) { + $tmpFiles = array(); // (path => MockFSFile) + + $this->debug( '(' . serialize( $params ) . ')' ); + foreach ( $params['srcs'] as $src ) { + $tmpFiles[$src] = new MockFSFile( + wfTempDir() . '/' . wfRandomString( 32 ) + ); + } + return $tmpFiles; + } + + protected function doDirectoryExists( $container, $dir, array $params ) { + $this->debug(); + return true; + } + + public function getDirectoryListInternal( $container, $dir, array $params ) { + $this->debug(); + return array(); + } + + public function getFileListInternal( $container, $dir, array $params ) { + $this->debug(); + return array(); + } + + protected function directoriesAreVirtual() { + $this->debug(); + return true; + } +} diff --git a/tests/phpunit/mocks/media/MockBitmapHandler.php b/tests/phpunit/mocks/media/MockBitmapHandler.php new file mode 100644 index 00000000..742b41e4 --- /dev/null +++ b/tests/phpunit/mocks/media/MockBitmapHandler.php @@ -0,0 +1,92 @@ +getClientScalingThumbnailImage( $image, $scalerParams ); + } +} +class MockSvgHandler extends SvgHandler { + function doTransform( $image, $dstPath, $dstUrl, $params, $flags = 0 ) { + return MockImageHandler::doFakeTransform( $this, $image, $dstPath, $dstUrl, $params, $flags ); + } +} +/** + * Mock handler for images. + * + * This is really intended for unit testing. + * + * @ingroup Media + */ +class MockImageHandler { + + /** + * Override BitmapHandler::doTransform() making sure we do not generate + * a thumbnail at all. That is merely returning a ThumbnailImage that + * will be consumed by the unit test. There is no need to create a real + * thumbnail on the filesystem. + */ + static function doFakeTransform( $that, $image, $dstPath, $dstUrl, $params, $flags = 0 ) { + # Example of what we receive: + # $image: LocalFile + # $dstPath: /tmp/transform_7d0a7a2f1a09-1.jpg + # $dstUrl : http://example.com/images/thumb/0/09/Bad.jpg/320px-Bad.jpg + # $params: width: 320, descriptionUrl http://trunk.dev/wiki/File:Bad.jpg + + $that->normaliseParams( $image, $params ); + + $scalerParams = array( + # The size to which the image will be resized + 'physicalWidth' => $params['physicalWidth'], + 'physicalHeight' => $params['physicalHeight'], + 'physicalDimensions' => "{$params['physicalWidth']}x{$params['physicalHeight']}", + # The size of the image on the page + 'clientWidth' => $params['width'], + 'clientHeight' => $params['height'], + # Comment as will be added to the EXIF of the thumbnail + 'comment' => isset( $params['descriptionUrl'] ) ? + "File source: {$params['descriptionUrl']}" : '', + # Properties of the original image + 'srcWidth' => $image->getWidth(), + 'srcHeight' => $image->getHeight(), + 'mimeType' => $image->getMimeType(), + 'dstPath' => $dstPath, + 'dstUrl' => $dstUrl, + ); + + # In some cases, we do not bother generating a thumbnail. + if ( !$image->mustRender() && + $scalerParams['physicalWidth'] == $scalerParams['srcWidth'] + && $scalerParams['physicalHeight'] == $scalerParams['srcHeight'] + ) { + wfDebug( __METHOD__ . ": returning unscaled image\n" ); + // getClientScalingThumbnailImage is protected + return $that->doClientImage( $image, $scalerParams ); + } + + return new ThumbnailImage( $image, $dstUrl, false, $params ); + } +} diff --git a/tests/phpunit/phpunit.php b/tests/phpunit/phpunit.php new file mode 100644 index 00000000..1d65e521 --- /dev/null +++ b/tests/phpunit/phpunit.php @@ -0,0 +1,119 @@ +#!/usr/bin/env php +addOption( 'with-phpunitdir', + 'Directory to include PHPUnit from, for example when using a git fetchout from upstream. Path will be prepended to PHP `include_path`.', + false, # not required + true # need arg + ); + } + + public function finalSetup() { + parent::finalSetup(); + + global $wgMainCacheType, $wgMessageCacheType, $wgParserCacheType; + global $wgLanguageConverterCacheType, $wgUseDatabaseMessages; + global $wgLocaltimezone, $wgLocalisationCacheConf; + global $wgDevelopmentWarnings; + + // Inject test autoloader + require_once __DIR__ . '/../TestsAutoLoader.php'; + + // wfWarn should cause tests to fail + $wgDevelopmentWarnings = true; + + $wgMainCacheType = CACHE_NONE; + $wgMessageCacheType = CACHE_NONE; + $wgParserCacheType = CACHE_NONE; + $wgLanguageConverterCacheType = CACHE_NONE; + + $wgUseDatabaseMessages = false; # Set for future resets + + // Assume UTC for testing purposes + $wgLocaltimezone = 'UTC'; + + $wgLocalisationCacheConf['storeClass'] = 'LCStore_Null'; + + // Bug 44192 Do not attempt to send a real e-mail + Hooks::clear( 'AlternateUserMailer' ); + Hooks::register( + 'AlternateUserMailer', + function () { + return false; + } + ); + } + + public function execute() { + global $IP; + + # Make sure we have --configuration or PHPUnit might complain + if ( !in_array( '--configuration', $_SERVER['argv'] ) ) { + //Hack to eliminate the need to use the Makefile (which sucks ATM) + array_splice( $_SERVER['argv'], 1, 0, + array( '--configuration', $IP . '/tests/phpunit/suite.xml' ) ); + } + + # --with-phpunitdir let us override the default PHPUnit version + if ( $phpunitDir = $this->getOption( 'with-phpunitdir' ) ) { + # Sanity checks + if ( !is_dir( $phpunitDir ) ) { + $this->error( "--with-phpunitdir should be set to an existing directory", 1 ); + } + if ( !is_readable( $phpunitDir . "/PHPUnit/Runner/Version.php" ) ) { + $this->error( "No usable PHPUnit installation in $phpunitDir.\nAborting.\n", 1 ); + } + + # Now prepends provided PHPUnit directory + $this->output( "Will attempt loading PHPUnit from `$phpunitDir`\n" ); + set_include_path( $phpunitDir + . PATH_SEPARATOR . get_include_path() ); + + # Cleanup $args array so the option and its value do not + # pollute PHPUnit + $key = array_search( '--with-phpunitdir', $_SERVER['argv'] ); + unset( $_SERVER['argv'][$key] ); // the option + unset( $_SERVER['argv'][$key + 1] ); // its value + $_SERVER['argv'] = array_values( $_SERVER['argv'] ); + } + } + + public function getDbType() { + return Maintenance::DB_ADMIN; + } +} + +$maintClass = 'PHPUnitMaintClass'; +require RUN_MAINTENANCE_IF_MAIN; + +if ( !class_exists( 'PHPUnit_Runner_Version' ) ) { + require_once 'PHPUnit/Runner/Version.php'; +} + +if ( PHPUnit_Runner_Version::id() !== '@package_version@' + && version_compare( PHPUnit_Runner_Version::id(), '3.6.7', '<' ) +) { + die( 'PHPUnit 3.6.7 or later required, you have ' . PHPUnit_Runner_Version::id() . ".\n" ); +} + +if ( !class_exists( 'PHPUnit_TextUI_Command' ) ) { + require_once 'PHPUnit/Autoload.php'; +} +MediaWikiPHPUnitCommand::main(); diff --git a/tests/phpunit/run-tests.bat b/tests/phpunit/run-tests.bat new file mode 100644 index 00000000..e6eb3e0c --- /dev/null +++ b/tests/phpunit/run-tests.bat @@ -0,0 +1 @@ +php phpunit.php --configuration suite.xml %* diff --git a/tests/phpunit/skins/SideBarTest.php b/tests/phpunit/skins/SideBarTest.php new file mode 100644 index 00000000..9cb630fb --- /dev/null +++ b/tests/phpunit/skins/SideBarTest.php @@ -0,0 +1,223 @@ +messages array */ + private function initMessagesHref() { + # List of default messages for the sidebar. The sidebar doesn't care at + # all whether they are full URLs, interwiki links or local titles. + $URL_messages = array( + 'mainpage', + 'portal-url', + 'currentevents-url', + 'recentchanges-url', + 'randompage-url', + 'helppage', + ); + + # We're assuming that isValidURI works as advertised: it's also + # tested separately, in tests/phpunit/includes/HttpTest.php. + foreach ( $URL_messages as $m ) { + $titleName = MessageCache::singleton()->get( $m ); + if ( Http::isValidURI( $titleName ) ) { + $this->messages[$m]['href'] = $titleName; + } else { + $title = Title::newFromText( $titleName ); + $this->messages[$m]['href'] = $title->getLocalURL(); + } + } + } + + protected function setUp() { + parent::setUp(); + $this->initMessagesHref(); + $this->skin = new SkinTemplate(); + $this->skin->getContext()->setLanguage( Language::factory( 'en' ) ); + } + + /** + * Internal helper to test the sidebar + * @param $expected + * @param $text + * @param $message (Default: '') + * @todo this assert method to should be converted to a test using a dataprovider.. + */ + private function assertSideBar( $expected, $text, $message = '' ) { + $bar = array(); + $this->skin->addToSidebarPlain( $bar, $text ); + $this->assertEquals( $expected, $bar, $message ); + } + + /** + * @covers SkinTemplate::addToSidebarPlain + */ + public function testSidebarWithOnlyTwoTitles() { + $this->assertSideBar( + array( + 'Title1' => array(), + 'Title2' => array(), + ), + '* Title1 +* Title2 +' + ); + } + + /** + * @covers SkinTemplate::addToSidebarPlain + */ + public function testExpandMessages() { + $this->setMwGlobals( array( + 'wgNoFollowDomainExceptions' => array( 'mediawiki.org' ), + ) ); + $this->assertSidebar( + array( 'Title' => array( + array( + 'text' => 'Help', + 'href' => $this->messages['helppage']['href'], + 'id' => 'n-help', + 'active' => null + ) + ) ), + '* Title +** helppage|help +' + ); + } + + /** + * @covers SkinTemplate::addToSidebarPlain + */ + public function testExternalUrlsRequireADescription() { + $this->setMwGlobals( array( + 'wgNoFollowLinks' => true, + 'wgNoFollowDomainExceptions' => array(), + 'wgNoFollowNsExceptions' => array(), + ) ); + $this->assertSidebar( + array( 'Title' => array( + # ** http://www.mediawiki.org/| Home + array( + 'text' => 'Home', + 'href' => 'http://www.mediawiki.org/', + 'id' => 'n-Home', + 'active' => null, + 'rel' => 'nofollow', + ), + # ** http://valid.no.desc.org/ + # ... skipped since it is missing a pipe with a description + ) ), + '* Title +** http://www.mediawiki.org/| Home +** http://valid.no.desc.org/ +' + ); + } + + /** + * bug 33321 - Make sure there's a | after transforming. + * @group Database + * @covers SkinTemplate::addToSidebarPlain + */ + public function testTrickyPipe() { + $this->assertSidebar( + array( 'Title' => array( + # The first 2 are skipped + # Doesn't really test the url properly + # because it will vary with $wgArticlePath et al. + # ** Baz|Fred + array( + 'text' => 'Fred', + 'href' => Title::newFromText( 'Baz' )->getLocalURL(), + 'id' => 'n-Fred', + 'active' => null, + ), + array( + 'text' => 'title-to-display', + 'href' => Title::newFromText( 'page-to-go-to' )->getLocalURL(), + 'id' => 'n-title-to-display', + 'active' => null, + ), + ) ), + '* Title +** {{PAGENAME|Foo}} +** Bar +** Baz|Fred +** {{PLURAL:1|page-to-go-to{{int:pipe-separator/en}}title-to-display|branch not taken}} +' + ); + } + + + #### Attributes for external links ########################## + private function getAttribs() { + # Sidebar text we will use everytime + $text = '* Title +** http://www.mediawiki.org/| Home'; + + $bar = array(); + $this->skin->addToSideBarPlain( $bar, $text ); + + return $bar['Title'][0]; + } + + /** + * Simple test to verify our helper assertAttribs() is functional + */ + public function testTestAttributesAssertionHelper() { + $this->setMwGlobals( array( + 'wgNoFollowLinks' => true, + 'wgNoFollowDomainExceptions' => array(), + 'wgNoFollowNsExceptions' => array(), + 'wgExternalLinkTarget' => false, + ) ); + $attribs = $this->getAttribs(); + + $this->assertArrayHasKey( 'rel', $attribs ); + $this->assertEquals( 'nofollow', $attribs['rel'] ); + + $this->assertArrayNotHasKey( 'target', $attribs ); + } + + /** + * Test $wgNoFollowLinks in sidebar + */ + public function testRespectWgnofollowlinks() { + $this->setMwGlobals( 'wgNoFollowLinks', false ); + + $attribs = $this->getAttribs(); + $this->assertArrayNotHasKey( 'rel', $attribs, + 'External URL in sidebar do not have rel=nofollow when $wgNoFollowLinks = false' + ); + } + + /** + * Test $wgExternaLinkTarget in sidebar + * @dataProvider dataRespectExternallinktarget + */ + public function testRespectExternallinktarget( $externalLinkTarget ) { + $this->setMwGlobals( 'wgExternalLinkTarget', $externalLinkTarget ); + + $attribs = $this->getAttribs(); + $this->assertArrayHasKey( 'target', $attribs ); + $this->assertEquals( $attribs['target'], $externalLinkTarget ); + } + + public static function dataRespectExternallinktarget() { + return array( + array( '_blank' ), + array( '_self' ), + ); + } +} diff --git a/tests/phpunit/structure/AutoLoaderTest.php b/tests/phpunit/structure/AutoLoaderTest.php new file mode 100644 index 00000000..205ea360 --- /dev/null +++ b/tests/phpunit/structure/AutoLoaderTest.php @@ -0,0 +1,56 @@ +assertEquals( + $results['expected'], + $results['actual'] + ); + } + + protected static function checkAutoLoadConf() { + global $wgAutoloadLocalClasses, $wgAutoloadClasses, $IP; + $supportsParsekit = function_exists( 'parsekit_compile_file' ); + + // wgAutoloadLocalClasses has precedence, just like in includes/AutoLoader.php + $expected = $wgAutoloadLocalClasses + $wgAutoloadClasses; + $actual = array(); + + $files = array_unique( $expected ); + + foreach ( $files as $file ) { + // Only prefix $IP if it doesn't have it already. + // Generally local classes don't have it, and those from extensions and test suites do. + if ( substr( $file, 0, 1 ) != '/' && substr( $file, 1, 1 ) != ':' ) { + $filePath = "$IP/$file"; + } else { + $filePath = $file; + } + if ( $supportsParsekit ) { + $parseInfo = parsekit_compile_file( "$filePath" ); + $classes = array_keys( $parseInfo['class_table'] ); + } else { + $contents = file_get_contents( "$filePath" ); + $m = array(); + preg_match_all( '/\n\s*(?:final)?\s*(?:abstract)?\s*(?:class|interface)\s+([a-zA-Z0-9_]+)/', $contents, $m, PREG_PATTERN_ORDER ); + $classes = $m[1]; + } + foreach ( $classes as $class ) { + $actual[$class] = $file; + } + } + + return array( + 'expected' => $expected, + 'actual' => $actual, + ); + } +} diff --git a/tests/phpunit/structure/ResourcesTest.php b/tests/phpunit/structure/ResourcesTest.php new file mode 100644 index 00000000..fe823fa4 --- /dev/null +++ b/tests/phpunit/structure/ResourcesTest.php @@ -0,0 +1,131 @@ +assertFileExists( $filename, + "File '$resource' referenced by '$module' must exist." + ); + } + + /** + * This ask the ResouceLoader for all registered files from modules + * created by ResourceLoaderFileModule (or one of its descendants). + * + * + * Since the raw data is stored in protected properties, we have to + * overrride this through ReflectionObject methods. + */ + public static function provideResourceFiles() { + global $wgEnableJavaScriptTest; + + // Test existance of test suite files as well + // (can't use setUp or setMwGlobals because providers are static) + $live_wgEnableJavaScriptTest = $wgEnableJavaScriptTest; + $wgEnableJavaScriptTest = true; + + // Array with arguments for the test function + $cases = array(); + + // Initialize ResourceLoader + $rl = new ResourceLoader(); + + // See also ResourceLoaderFileModule::__construct + $filePathProps = array( + // Lists of file paths + 'lists' => array( + 'scripts', + 'debugScripts', + 'loaderScripts', + 'styles', + ), + + // Collated lists of file paths + 'nested-lists' => array( + 'languageScripts', + 'skinScripts', + 'skinStyles', + ), + ); + + foreach ( $rl->getModuleNames() as $moduleName ) { + $module = $rl->getModule( $moduleName ); + if ( !$module instanceof ResourceLoaderFileModule ) { + continue; + } + + $reflectedModule = new ReflectionObject( $module ); + + $files = array(); + + foreach ( $filePathProps['lists'] as $propName ) { + $property = $reflectedModule->getProperty( $propName ); + $property->setAccessible( true ); + $list = $property->getValue( $module ); + foreach ( $list as $key => $value ) { + // 'scripts' are numeral arrays. + // 'styles' can be numeral or associative. + // In case of associative the key is the file path + // and the value is the 'media' attribute. + if ( is_int( $key ) ) { + $files[] = $value; + } else { + $files[] = $key; + } + } + } + + foreach ( $filePathProps['nested-lists'] as $propName ) { + $property = $reflectedModule->getProperty( $propName ); + $property->setAccessible( true ); + $lists = $property->getValue( $module ); + foreach ( $lists as $list ) { + foreach ( $list as $key => $value ) { + // We need the same filter as for 'lists', + // due to 'skinStyles'. + if ( is_int( $key ) ) { + $files[] = $value; + } else { + $files[] = $key; + } + } + } + } + + // Get method for resolving the paths to full paths + $method = $reflectedModule->getMethod( 'getLocalPath' ); + $method->setAccessible( true ); + + // Populate cases + foreach ( $files as $file ) { + $cases[] = array( + $method->invoke( $module, $file ), + $module->getName(), + $file, + ); + } + } + + // Restore settings + $wgEnableJavaScriptTest = $live_wgEnableJavaScriptTest; + + return $cases; + } +} diff --git a/tests/phpunit/structure/StructureTest.php b/tests/phpunit/structure/StructureTest.php new file mode 100644 index 00000000..df00d4df --- /dev/null +++ b/tests/phpunit/structure/StructureTest.php @@ -0,0 +1,63 @@ +markTestSkipped( 'This test does not work on Windows' ); + } + $rootPath = escapeshellarg( __DIR__ . '/..' ); + $testClassRegex = implode( '|', array( + 'ApiFormatTestBase', + 'ApiTestCase', + 'ApiQueryTestBase', + 'ApiQueryContinueTestBase', + 'MediaWikiLangTestCase', + 'MediaWikiTestCase', + 'PHPUnit_Framework_TestCase', + 'DumpTestCase', + ) ); + $testClassRegex = "^class .* extends ($testClassRegex)"; + $finder = "find $rootPath -name '*.php' '!' -name '*Test.php'" . + " | xargs grep -El '$testClassRegex|function suite\('"; + + $results = null; + $exitCode = null; + exec( $finder, $results, $exitCode ); + + $this->assertEquals( + 0, + $exitCode, + 'Verify find/grep command succeeds.' + ); + + $results = array_filter( + $results, + array( $this, 'filterSuites' ) + ); + $strip = strlen( $rootPath ) - 1; + foreach ( $results as $k => $v ) { + $results[$k] = substr( $v, $strip ); + } + $this->assertEquals( + array(), + $results, + "Unit test file in $rootPath must end with Test." + ); + } + + /** + * Filter to remove testUnitTestFileNamesEndWithTest false positives. + */ + public function filterSuites( $filename ) { + return strpos( $filename, __DIR__ . '/../suites/' ) !== 0; + } +} diff --git a/tests/phpunit/suite.xml b/tests/phpunit/suite.xml new file mode 100644 index 00000000..7a9122fa --- /dev/null +++ b/tests/phpunit/suite.xml @@ -0,0 +1,51 @@ + + + + + + + includes + + + languages + + + skins + + + + maintenance + + + structure + + + suites/UploadFromUrlTestSuite.php + + + suites/ExtensionsTestSuite.php + suites/ExtensionsParserTestSuite.php + + + + + Utility + Broken + ParserFuzz + Stub + + + diff --git a/tests/phpunit/suites/ExtensionsParserTestSuite.php b/tests/phpunit/suites/ExtensionsParserTestSuite.php new file mode 100644 index 00000000..3d68b241 --- /dev/null +++ b/tests/phpunit/suites/ExtensionsParserTestSuite.php @@ -0,0 +1,8 @@ +addTestFile( $file ); + } + if ( !count( $files ) ) { + $this->addTest( new DummyExtensionsTest( 'testNothing' ) ); + } + } + + public static function suite() { + return new self; + } +} + +/** + * Needed to avoid warnings like 'No tests found in class "ExtensionsTestSuite".' + * when no extensions with tests are used. + */ +class DummyExtensionsTest extends MediaWikiTestCase { + public function testNothing() { + $this->assertTrue( true ); + } +} diff --git a/tests/phpunit/suites/UploadFromUrlTestSuite.php b/tests/phpunit/suites/UploadFromUrlTestSuite.php new file mode 100644 index 00000000..7eb599e3 --- /dev/null +++ b/tests/phpunit/suites/UploadFromUrlTestSuite.php @@ -0,0 +1,207 @@ + 'LocalRepo', + 'name' => 'local', + 'url' => 'http://example.com/images', + 'hashLevels' => 2, + 'transformVia404' => false, + 'backend' => new FSFileBackend( array( + 'name' => 'local-backend', + 'lockManager' => 'fsLockManager', + 'containerPaths' => array( + 'local-public' => wfTempDir() . '/test-repo/public', + 'local-thumb' => wfTempDir() . '/test-repo/thumb', + 'local-temp' => wfTempDir() . '/test-repo/temp', + 'local-deleted' => wfTempDir() . '/test-repo/delete', + ) + ) ), + ); + foreach ( $tmpGlobals as $var => $val ) { + if ( array_key_exists( $var, $GLOBALS ) ) { + $this->savedGlobals[$var] = $GLOBALS[$var]; + } + $GLOBALS[$var] = $val; + } + + $wgNamespaceProtection[NS_MEDIAWIKI] = 'editinterface'; + $wgNamespaceAliases['Image'] = NS_FILE; + $wgNamespaceAliases['Image_talk'] = NS_FILE_TALK; + + $wgEnableParserCache = false; + DeferredUpdates::clearPendingUpdates(); + $wgMemc = wfGetMainCache(); + $messageMemc = wfGetMessageCacheStorage(); + $parserMemc = wfGetParserCacheStorage(); + + // $wgContLang = new StubContLang; + $wgUser = new User; + $context = new RequestContext; + $wgLang = $context->getLanguage(); + $wgOut = $context->getOutput(); + $wgParser = new StubObject( 'wgParser', $wgParserConf['class'], array( $wgParserConf ) ); + $wgRequest = $context->getRequest(); + + if ( $wgStyleDirectory === false ) { + $wgStyleDirectory = "$IP/skins"; + } + + RepoGroup::destroySingleton(); + FileBackendGroup::destroySingleton(); + } + + protected function tearDown() { + foreach ( $this->savedGlobals as $var => $val ) { + $GLOBALS[$var] = $val; + } + // Restore backends + RepoGroup::destroySingleton(); + FileBackendGroup::destroySingleton(); + + $this->teardownUploadDir( $this->uploadDir ); + + parent::tearDown(); + } + + private $uploadDir; + private $keepUploads; + + /** + * Remove the dummy uploads directory + */ + private function teardownUploadDir( $dir ) { + if ( $this->keepUploads ) { + return; + } + + // delete the files first, then the dirs. + self::deleteFiles( + array( + "$dir/3/3a/Foobar.jpg", + "$dir/thumb/3/3a/Foobar.jpg/180px-Foobar.jpg", + "$dir/thumb/3/3a/Foobar.jpg/200px-Foobar.jpg", + "$dir/thumb/3/3a/Foobar.jpg/640px-Foobar.jpg", + "$dir/thumb/3/3a/Foobar.jpg/120px-Foobar.jpg", + + "$dir/0/09/Bad.jpg", + ) + ); + + self::deleteDirs( + array( + "$dir/3/3a", + "$dir/3", + "$dir/thumb/6/65", + "$dir/thumb/6", + "$dir/thumb/3/3a/Foobar.jpg", + "$dir/thumb/3/3a", + "$dir/thumb/3", + + "$dir/0/09/", + "$dir/0/", + + "$dir/thumb", + "$dir", + ) + ); + } + + /** + * Delete the specified files, if they exist. + * + * @param $files Array: full paths to files to delete. + */ + private static function deleteFiles( $files ) { + foreach ( $files as $file ) { + if ( file_exists( $file ) ) { + unlink( $file ); + } + } + } + + /** + * Delete the specified directories, if they exist. Must be empty. + * + * @param $dirs Array: full paths to directories to delete. + */ + private static function deleteDirs( $dirs ) { + foreach ( $dirs as $dir ) { + if ( is_dir( $dir ) ) { + rmdir( $dir ); + } + } + } + + /** + * Create a dummy uploads directory which will contain a couple + * of files in order to pass existence tests. + * + * @return String: the directory + */ + private function setupUploadDir() { + global $IP; + + if ( $this->keepUploads ) { + $dir = wfTempDir() . '/mwParser-images'; + + if ( is_dir( $dir ) ) { + return $dir; + } + } else { + $dir = wfTempDir() . "/mwParser-" . mt_rand() . "-images"; + } + + wfDebug( "Creating upload directory $dir\n" ); + + if ( file_exists( $dir ) ) { + wfDebug( "Already exists!\n" ); + + return $dir; + } + + wfMkdirParents( $dir . '/3/3a', null, __METHOD__ ); + copy( "$IP/skins/monobook/headbg.jpg", "$dir/3/3a/Foobar.jpg" ); + + wfMkdirParents( $dir . '/0/09', null, __METHOD__ ); + copy( "$IP/skins/monobook/headbg.jpg", "$dir/0/09/Bad.jpg" ); + + return $dir; + } + + public static function suite() { + // Hack to invoke the autoloader required to get phpunit to recognize + // the UploadFromUrlTest class + class_exists( 'UploadFromUrlTest' ); + $suite = new UploadFromUrlTestSuite( 'UploadFromUrlTest' ); + + return $suite; + } +} -- cgit v1.2.2