diff options
author | Yaco <franco@reevo.org> | 2019-07-31 14:50:50 -0300 |
---|---|---|
committer | Yaco <franco@reevo.org> | 2019-07-31 14:50:50 -0300 |
commit | 3848848fc3bc2db035c824f1453635949505d76e (patch) | |
tree | 71fd898ebb220e7ba034cf2bc1bf708fdd0d6219 /www/wiki/tests | |
parent | 2dfe0b926fe5c6c4f27ad1f9bc1c1377cb091111 (diff) |
ACTUALIZA MW a 1.31.3, SMW a 3.0.2 y extensiones menores
Diffstat (limited to 'www/wiki/tests')
656 files changed, 41050 insertions, 18930 deletions
diff --git a/www/wiki/tests/TestsAutoLoader.php b/www/wiki/tests/TestsAutoLoader.php deleted file mode 100644 index ebb6d901..00000000 --- a/www/wiki/tests/TestsAutoLoader.php +++ /dev/null @@ -1,153 +0,0 @@ -<?php -/** - * AutoLoader for the testing suite. - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along - * with this program; if not, write to the Free Software Foundation, Inc., - * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - * http://www.gnu.org/copyleft/gpl.html - * - * @file - * @ingroup Testing - */ - -global $wgAutoloadClasses; -$testDir = __DIR__; - -$wgAutoloadClasses += [ - - # tests - 'DbTestPreviewer' => "$testDir/testHelpers.inc", - 'DbTestRecorder' => "$testDir/testHelpers.inc", - 'DelayedParserTest' => "$testDir/testHelpers.inc", - 'ParserTestResult' => "$testDir/parser/ParserTestResult.php", - 'TestFileIterator' => "$testDir/testHelpers.inc", - 'TestFileDataProvider' => "$testDir/testHelpers.inc", - 'TestRecorder' => "$testDir/testHelpers.inc", - 'ITestRecorder' => "$testDir/testHelpers.inc", - 'DjVuSupport' => "$testDir/testHelpers.inc", - 'TidySupport' => "$testDir/testHelpers.inc", - - # tests/phpunit - 'MediaWikiTestCase' => "$testDir/phpunit/MediaWikiTestCase.php", - 'MediaWikiPHPUnitTestListener' => "$testDir/phpunit/MediaWikiPHPUnitTestListener.php", - 'MediaWikiLangTestCase' => "$testDir/phpunit/MediaWikiLangTestCase.php", - 'ResourceLoaderTestCase' => "$testDir/phpunit/ResourceLoaderTestCase.php", - 'ResourceLoaderTestModule' => "$testDir/phpunit/ResourceLoaderTestCase.php", - 'ResourceLoaderFileModuleTestModule' => "$testDir/phpunit/ResourceLoaderTestCase.php", - 'TestUser' => "$testDir/phpunit/includes/TestUser.php", - 'LessFileCompilationTest' => "$testDir/phpunit/LessFileCompilationTest.php", - - # tests/phpunit/includes - 'RevisionStorageTest' => "$testDir/phpunit/includes/RevisionStorageTest.php", - 'TestingAccessWrapper' => "$testDir/phpunit/includes/TestingAccessWrapper.php", - 'TestLogger' => "$testDir/phpunit/includes/TestLogger.php", - - # tests/phpunit/includes/api - 'ApiFormatTestBase' => "$testDir/phpunit/includes/api/format/ApiFormatTestBase.php", - 'ApiQueryTestBase' => "$testDir/phpunit/includes/api/query/ApiQueryTestBase.php", - 'ApiQueryContinueTestBase' => "$testDir/phpunit/includes/api/query/ApiQueryContinueTestBase.php", - 'ApiTestCase' => "$testDir/phpunit/includes/api/ApiTestCase.php", - 'ApiTestCaseUpload' => "$testDir/phpunit/includes/api/ApiTestCaseUpload.php", - 'ApiTestContext' => "$testDir/phpunit/includes/api/ApiTestContext.php", - 'MockApi' => "$testDir/phpunit/includes/api/MockApi.php", - 'MockApiQueryBase' => "$testDir/phpunit/includes/api/MockApiQueryBase.php", - 'UserWrapper' => "$testDir/phpunit/includes/api/UserWrapper.php", - 'RandomImageGenerator' => "$testDir/phpunit/includes/api/RandomImageGenerator.php", - - # tests/phpunit/includes/auth - 'MediaWiki\\Auth\\AuthenticationRequestTestCase' => - "$testDir/phpunit/includes/auth/AuthenticationRequestTestCase.php", - - # tests/phpunit/includes/changes - 'TestRecentChangesHelper' => "$testDir/phpunit/includes/changes/TestRecentChangesHelper.php", - - # tests/phpunit/includes/content - 'DummyContentHandlerForTesting' => - "$testDir/phpunit/mocks/content/DummyContentHandlerForTesting.php", - 'DummyContentForTesting' => "$testDir/phpunit/mocks/content/DummyContentForTesting.php", - 'DummyNonTextContentHandler' => "$testDir/phpunit/mocks/content/DummyNonTextContentHandler.php", - 'DummyNonTextContent' => "$testDir/phpunit/mocks/content/DummyNonTextContent.php", - 'ContentHandlerTest' => "$testDir/phpunit/includes/content/ContentHandlerTest.php", - 'JavaScriptContentTest' => "$testDir/phpunit/includes/content/JavaScriptContentTest.php", - 'TextContentTest' => "$testDir/phpunit/includes/content/TextContentTest.php", - 'WikitextContentTest' => "$testDir/phpunit/includes/content/WikitextContentTest.php", - - # tests/phpunit/includes/db - 'DatabaseTestHelper' => "$testDir/phpunit/includes/db/DatabaseTestHelper.php", - - # tests/phpunit/includes/diff - 'FakeDiffOp' => "$testDir/phpunit/includes/diff/FakeDiffOp.php", - - # tests/phpunit/includes/logging - 'LogFormatterTestCase' => "$testDir/phpunit/includes/logging/LogFormatterTestCase.php", - - # tests/phpunit/includes/page - 'WikiPageTest' => "$testDir/phpunit/includes/page/WikiPageTest.php", - - # tests/phpunit/includes/password - 'PasswordTestCase' => "$testDir/phpunit/includes/password/PasswordTestCase.php", - - # tests/phpunit/includes/resourceloader - 'ResourceLoaderImageModuleTest' => - "$testDir/phpunit/includes/resourceloader/ResourceLoaderImageModuleTest.php", - 'ResourceLoaderImageModuleTestable' => - "$testDir/phpunit/includes/resourceloader/ResourceLoaderImageModuleTest.php", - - # tests/phpunit/includes/session - 'MediaWiki\\Session\\TestBagOStuff' => "$testDir/phpunit/includes/session/TestBagOStuff.php", - 'MediaWiki\\Session\\TestUtils' => "$testDir/phpunit/includes/session/TestUtils.php", - - # tests/phpunit/includes/specials - 'SpecialPageTestBase' => "$testDir/phpunit/includes/specials/SpecialPageTestBase.php", - 'SpecialPageExecutor' => "$testDir/phpunit/includes/specials/SpecialPageExecutor.php", - - # tests/phpunit/languages - 'LanguageClassesTestCase' => "$testDir/phpunit/languages/LanguageClassesTestCase.php", - - # tests/phpunit/includes/libs - 'GenericArrayObjectTest' => "$testDir/phpunit/includes/libs/GenericArrayObjectTest.php", - - # tests/phpunit/maintenance - 'DumpTestCase' => "$testDir/phpunit/maintenance/DumpTestCase.php", - - # tests/phpunit/media - 'FakeDimensionFile' => "$testDir/phpunit/includes/media/FakeDimensionFile.php", - 'MediaWikiMediaTestCase' => "$testDir/phpunit/includes/media/MediaWikiMediaTestCase.php", - - # tests/phpunit/mocks - 'MockFSFile' => "$testDir/phpunit/mocks/filebackend/MockFSFile.php", - 'MockFileBackend' => "$testDir/phpunit/mocks/filebackend/MockFileBackend.php", - 'MockBitmapHandler' => "$testDir/phpunit/mocks/media/MockBitmapHandler.php", - 'MockImageHandler' => "$testDir/phpunit/mocks/media/MockImageHandler.php", - 'MockSvgHandler' => "$testDir/phpunit/mocks/media/MockSvgHandler.php", - 'MockDjVuHandler' => "$testDir/phpunit/mocks/media/MockDjVuHandler.php", - 'MockOggHandler' => "$testDir/phpunit/mocks/media/MockOggHandler.php", - 'MockWebRequest' => "$testDir/phpunit/mocks/MockWebRequest.php", - 'MediaWiki\\Session\\DummySessionBackend' - => "$testDir/phpunit/mocks/session/DummySessionBackend.php", - 'DummySessionProvider' => "$testDir/phpunit/mocks/session/DummySessionProvider.php", - - # tests/parser - 'NewParserTest' => "$testDir/phpunit/includes/parser/NewParserTest.php", - 'MediaWikiParserTest' => "$testDir/phpunit/includes/parser/MediaWikiParserTest.php", - 'ParserTest' => "$testDir/parser/parserTest.inc", - 'ParserTestParserHook' => "$testDir/parser/parserTestsParserHook.php", - - # tests/phpunit/includes/site - 'SiteTest' => "$testDir/phpunit/includes/site/SiteTest.php", - 'TestSites' => "$testDir/phpunit/includes/site/TestSites.php", - - # tests/phpunit/includes/specialpage - 'SpecialPageTestHelper' => "$testDir/phpunit/includes/specialpage/SpecialPageTestHelper.php", -]; diff --git a/www/wiki/tests/browser/ci.yml b/www/wiki/tests/browser/ci.yml deleted file mode 100644 index 8c9865e6..00000000 --- a/www/wiki/tests/browser/ci.yml +++ /dev/null @@ -1,8 +0,0 @@ -BROWSER: - - firefox - -MEDIAWIKI_ENVIRONMENT: - - beta - -PLATFORM: - - Linux diff --git a/www/wiki/tests/browser/environments.yml b/www/wiki/tests/browser/environments.yml deleted file mode 100644 index 35eb153f..00000000 --- a/www/wiki/tests/browser/environments.yml +++ /dev/null @@ -1,39 +0,0 @@ -# Customize this configuration as necessary to provide defaults for various -# test environments. -# -# The set of defaults to use is determined by the MEDIAWIKI_ENVIRONMENT -# environment variable. -# -# export MEDIAWIKI_ENVIRONMENT=mw-vagrant-host -# bundle exec cucumber -# -# Additional variables set by the environment will override the corresponding -# defaults defined here. -# -# export MEDIAWIKI_ENVIRONMENT=mw-vagrant-host -# export MEDIAWIKI_USER=Selenium_user2 -# bundle exec cucumber -# -mw-vagrant-host: &default - user_factory: true - mediawiki_url: http://127.0.0.1:8080/wiki/ - -mw-vagrant-guest: - user_factory: true - mediawiki_url: http://127.0.0.1/wiki/ - -beta: - mediawiki_url: http://en.wikipedia.beta.wmflabs.org/wiki/ - mediawiki_user: Selenium_user - # mediawiki_password: SET THIS IN THE ENVIRONMENT! - -test2: - mediawiki_url: http://test2.wikipedia.org/wiki/ - mediawiki_user: Selenium_user - # mediawiki_password: SET THIS IN THE ENVIRONMENT! - -integration: - user_factory: true - # mediawiki_url: THIS WILL BE SET BY JENKINS - -default: *default diff --git a/www/wiki/tests/browser/features/create_account.feature b/www/wiki/tests/browser/features/create_account.feature deleted file mode 100644 index 80291828..00000000 --- a/www/wiki/tests/browser/features/create_account.feature +++ /dev/null @@ -1,17 +0,0 @@ -@chrome @firefox @vagrant -Feature: Create account - - Scenario Outline: Go to Create account page - Given I go to Create account page at <path> - Then form has Create account button - - Examples: - | path | - | Special:CreateAccount | - | Special:UserLogin/signup | - | Special:UserLogin?type=signup | - - Scenario: If no username is entered then an error is displayed - Given I go to Create account page at Special:CreateAccount - When I submit the form - Then an error message is displayed diff --git a/www/wiki/tests/browser/features/create_and_follow_wiki_link.feature b/www/wiki/tests/browser/features/create_and_follow_wiki_link.feature deleted file mode 100644 index 510c467b..00000000 --- a/www/wiki/tests/browser/features/create_and_follow_wiki_link.feature +++ /dev/null @@ -1,9 +0,0 @@ -@chrome @firefox @vagrant -Feature: Create Page With Wiki Link - - Scenario: Create Page With Wiki Link - Given I create page "Link Target Test Page" with content "Link Target Test Page" - And I go to the "Link Source Test Page" page with content "This is a [[Link Target Test Page|link to the test target page]] right here." - When I click the Link Target link - Then I should be on the Link Target Test Page - And the page content should contain "Link Target Test Page" diff --git a/www/wiki/tests/browser/features/edit_page.feature b/www/wiki/tests/browser/features/edit_page.feature deleted file mode 100644 index ade69145..00000000 --- a/www/wiki/tests/browser/features/edit_page.feature +++ /dev/null @@ -1,11 +0,0 @@ -@chrome @firefox @vagrant -Feature: Edit Page - - Scenario: Create and edit page - Given I go to the "Editing Test Page" page with content "This is a page to test editing" - When I click Edit - And I edit the page with "Edited and a random string" - And I click Preview - And I click Show Changes - And I save the edit - Then the edited page content should contain "Edited and a random string" diff --git a/www/wiki/tests/browser/features/file.feature b/www/wiki/tests/browser/features/file.feature deleted file mode 100644 index 0b59c88a..00000000 --- a/www/wiki/tests/browser/features/file.feature +++ /dev/null @@ -1,11 +0,0 @@ -@chrome @firefox @vagrant -Feature: File - - Scenario: Anonymous goes to file that does not exist - Given I am at file that does not exist - Then page should show that no such file exists - - Scenario: Logged-in user goes to file that does not exist - Given I am logged in - And I am at file that does not exist - Then page should show that no such file exists diff --git a/www/wiki/tests/browser/features/login.feature b/www/wiki/tests/browser/features/login.feature deleted file mode 100644 index c18f0872..00000000 --- a/www/wiki/tests/browser/features/login.feature +++ /dev/null @@ -1,30 +0,0 @@ -@chrome @firefox @vagrant -Feature: Log in - - Background: - Given I am at Log in page - - Scenario: Go to Log in page - Then Username element should be there - And Password element should be there - And Log in element should be there - - Scenario: Log in without entering credentials - When I log in without entering credentials - Then error box should be visible - - Scenario: Log in without entering password - When I log in without entering password - Then error box should be visible - - Scenario: Log in with incorrect username - When I log in with incorrect username - Then error box should be visible - - Scenario: Log in with incorrect password - When I log in with incorrect password - Then error box should be visible - - Scenario: Log in with valid credentials - When I log in - Then error box should not be visible diff --git a/www/wiki/tests/browser/features/main_page_links.feature b/www/wiki/tests/browser/features/main_page_links.feature deleted file mode 100644 index 1f3621bb..00000000 --- a/www/wiki/tests/browser/features/main_page_links.feature +++ /dev/null @@ -1,19 +0,0 @@ -@chrome @firefox @vagrant -Feature: Main Page View History Links - - Background: - Given I open the main wiki URL - - Scenario: Main Page View History links exist - Then I should see a link for View History - - Scenario: Main Page Sidebar Links - Then I should see a link for Recent changes - And I should see a link for Random page - And I should see a link for Help - And I should see a link for What links here - And I should see a link for Related changes - And I should see a link for Special pages - And I should see a link for Printable version - And I should see a link for Permanent link - And I should see a link for Page information diff --git a/www/wiki/tests/browser/features/preferences.feature b/www/wiki/tests/browser/features/preferences.feature deleted file mode 100644 index 23663c24..00000000 --- a/www/wiki/tests/browser/features/preferences.feature +++ /dev/null @@ -1,47 +0,0 @@ -@chrome @firefox @vagrant -Feature: Preferences - - Scenario: Preferences Appearance - Given I am logged in - When I navigate to Preferences - And I click Appearance - Then I can select skin Vector - And I can select image size - And I can select thumbnail size - And I can select Threshold for stub link - And I can select underline preferences - And I have advanced options checkboxes - And I can click Save - And I can restore default settings - And I can select date format - And I can see time offset section - And I can see local time - And I can select my time zone - - Scenario: Preferences Editing - Given I am logged in - When I navigate to Preferences - And I click Editing - Then I can select edit area font style - And I can select section editing via edit links - And I can select section editing by right clicking - And I can select section editing by double clicking - And I can select to prompt me when entering a blank edit summary - And I can select to warn me when I leave an edit page with unsaved changes - And I can select show edit toolbar - And I can select show preview on first edit - And I can select show preview before edit box - And I can select live preview - - Scenario: Preferences User profile - Given I am logged in - When I navigate to Preferences - And I click User profile - Then I can see my Basic informations - And I can change my language - And I can change my gender - And I can see my signature - And I can change my signature - And I can see my email - And I can click Save - And I can restore default settings diff --git a/www/wiki/tests/browser/features/step_definitions/create_account_steps.rb b/www/wiki/tests/browser/features/step_definitions/create_account_steps.rb deleted file mode 100644 index fa0570c6..00000000 --- a/www/wiki/tests/browser/features/step_definitions/create_account_steps.rb +++ /dev/null @@ -1,15 +0,0 @@ -Given(/^I go to Create account page at (.+)$/) do |path| - visit(CreateAccountPage, using_params: { page_title: path }) -end - -Then(/^form has Create account button$/) do - expect(on(CreateAccountPage).create_account_element).to exist -end - -When(/^I submit the form$/) do - on(CreateAccountPage).create_account -end - -Then(/^an error message is displayed$/) do - expect(on(CreateAccountPage).error_message_element.class_name).to eq 'errorbox' -end diff --git a/www/wiki/tests/browser/features/step_definitions/create_and_follow_wiki_link_steps.rb b/www/wiki/tests/browser/features/step_definitions/create_and_follow_wiki_link_steps.rb deleted file mode 100644 index 504d3454..00000000 --- a/www/wiki/tests/browser/features/step_definitions/create_and_follow_wiki_link_steps.rb +++ /dev/null @@ -1,26 +0,0 @@ -Given(/^I go to the "(.+)" page with content "(.+)"$/) do |page_title, page_content| - @wikitext = page_content - api.create_page page_title, page_content - step "I am on the #{page_title} page" -end - -Given(/^I am on the (.+) page$/) do |article| - article = article.gsub(/ /, '_') - visit(ZtargetPage, using_params: { article_name: article }) -end - -Given(/^I create page "(.*?)" with content "(.*?)"$/) do |page_title, page_content| - api.create_page page_title, page_content -end - -When(/^I click the Link Target link$/) do - on(ZtargetPage).link_target_page_link -end - -Then(/^I should be on the Link Target Test Page$/) do - expect(@browser.url).to match /Link_Target_Test_Page/ -end - -Then(/^the page content should contain "(.*?)"$/) do |content| - expect(on(ZtargetPage).page_content).to match content -end diff --git a/www/wiki/tests/browser/features/step_definitions/edit_page_steps.rb b/www/wiki/tests/browser/features/step_definitions/edit_page_steps.rb deleted file mode 100644 index 0e0aeb17..00000000 --- a/www/wiki/tests/browser/features/step_definitions/edit_page_steps.rb +++ /dev/null @@ -1,23 +0,0 @@ -When(/^I click Edit$/) do - on(MainPage).edit_link -end - -When(/^I click Preview$/) do - on(EditPage).preview_button -end - -When(/^I click Show Changes$/) do - on(EditPage).show_changes_button -end - -When(/^I edit the page with "(.*?)"$/) do |edit_content| - on(EditPage).edit_page_content_element.send_keys(edit_content + @random_string) -end - -When(/^I save the edit$/) do - on(EditPage).save_button -end - -Then(/^the edited page content should contain "(.*?)"$/) do |content| - expect(on(MainPage).page_content).to match(content + @random_string) -end diff --git a/www/wiki/tests/browser/features/step_definitions/file_steps.rb b/www/wiki/tests/browser/features/step_definitions/file_steps.rb deleted file mode 100644 index 15069b2c..00000000 --- a/www/wiki/tests/browser/features/step_definitions/file_steps.rb +++ /dev/null @@ -1,7 +0,0 @@ -Given(/^I am at file that does not exist$/) do - visit(FileDoesNotExistPage, using_params: { page_name: @random_string }) -end - -Then(/^page should show that no such file exists$/) do - expect(on(FileDoesNotExistPage).file_does_not_exist_message_element).to be_visible -end diff --git a/www/wiki/tests/browser/features/step_definitions/login_steps.rb b/www/wiki/tests/browser/features/step_definitions/login_steps.rb deleted file mode 100644 index bda0faac..00000000 --- a/www/wiki/tests/browser/features/step_definitions/login_steps.rb +++ /dev/null @@ -1,58 +0,0 @@ -Given(/^I am at Log in page$/) do - visit LoginPage -end - -When(/^I log in$/) do - on(LoginPage).login_with(user, password, false) -end - -When(/^I log in with incorrect password$/) do - on(LoginPage).login_with(user, 'incorrect password', false) -end - -When(/^I log in with incorrect username$/) do - on(LoginPage).login_with('incorrect username', password, false) -end - -When(/^I log in without entering credentials$/) do - on(LoginPage).login_with('', '', false) -end - -When(/^I log in without entering password$/) do - on(LoginPage).login_with(user, '', false) -end - -Then(/^error box should be visible$/) do - expect(on(LoginErrorPage).error_box_element).to be_visible -end - -Then(/^error box should not be visible$/) do - expect(on(LoginErrorPage).error_box_element).not_to be_visible -end - -Then(/^feedback should be (.+)$/) do |feedback| - on(LoginPage) do |page| - page.feedback_element.when_present.click - expect(page.feedback).to match Regexp.escape(feedback) - end -end - -Then(/^Log in element should be there$/) do - expect(on(LoginPage).login_element).to exist -end - -Then(/^main page should open$/) do - expect(@browser.url).to eq on(MainPage).class.url -end - -Then(/^Password element should be there$/) do - expect(on(LoginPage).password_element).to exist -end - -Then(/^there should be a link to (.+)$/) do |text| - expect(on(LoginPage).username_displayed_element.when_present.text).to eq text -end - -Then(/^Username element should be there$/) do - expect(on(LoginPage).username_element).to exist -end diff --git a/www/wiki/tests/browser/features/step_definitions/main_page_links_steps.rb b/www/wiki/tests/browser/features/step_definitions/main_page_links_steps.rb deleted file mode 100644 index 7f588c05..00000000 --- a/www/wiki/tests/browser/features/step_definitions/main_page_links_steps.rb +++ /dev/null @@ -1,47 +0,0 @@ -Given(/^I open the main wiki URL$/) do - visit(MainPage) -end - -Then(/^I should see a link for View History$/) do - expect(on(MainPage).view_history_link_element).to be_visible -end - -Then(/^I should see a link for Edit$/) do - expect(on(MainPage).edit_link_element).to be_visible -end - -Then(/^I should see a link for Recent changes$/) do - expect(on(MainPage).recent_changes_link_element).to be_visible -end - -Then(/^I should see a link for Random page$/) do - expect(on(MainPage).random_page_link_element).to be_visible -end - -Then(/^I should see a link for Help$/) do - expect(on(MainPage).help_link_element).to be_visible -end - -Then(/^I should see a link for What links here$/) do - expect(on(MainPage).what_links_here_link_element).to be_visible -end - -Then(/^I should see a link for Related changes$/) do - expect(on(MainPage).related_changes_link_element).to be_visible -end - -Then(/^I should see a link for Special pages$/) do - expect(on(MainPage).special_pages_link_element).to be_visible -end - -Then(/^I should see a link for Printable version$/) do - expect(on(MainPage).printable_version_link_element).to be_visible -end - -Then(/^I should see a link for Permanent link$/) do - expect(on(MainPage).permanent_link_link_element).to be_visible -end - -Then(/^I should see a link for Page information$/) do - expect(on(MainPage).page_information_link_element).to be_visible -end diff --git a/www/wiki/tests/browser/features/step_definitions/preferences_appearance_steps.rb b/www/wiki/tests/browser/features/step_definitions/preferences_appearance_steps.rb deleted file mode 100644 index 8ffdaf1d..00000000 --- a/www/wiki/tests/browser/features/step_definitions/preferences_appearance_steps.rb +++ /dev/null @@ -1,69 +0,0 @@ -When(/^I click Appearance$/) do - visit(PreferencesPage).appearance_link_element.when_present.click -end - -When(/^I navigate to Preferences$/) do - visit(PreferencesPage) -end - -Then(/^I can click Save$/) do - expect(on(PreferencesPage).save_button_element).to exist -end - -Then(/^I can restore default settings$/) do - expect(on(PreferencesAppearancePage).restore_default_link_element).to exist -end - -Then(/^I can see local time$/) do - expect(on(PreferencesAppearancePage).local_time_span_element).to exist -end - -Then(/^I can see time offset section$/) do - expect(on(PreferencesAppearancePage).time_offset_table_element).to be_visible -end - -Then(/^I can select date format$/) do - on(PreferencesAppearancePage) do |page| - expect(page.no_preference_radio_element).to exist - expect(page.mo_day_year_radio_element).to exist - expect(page.day_mo_year_radio_element).to exist - expect(page.year_mo_day_radio_element).to exist - expect(page.iso_8601_radio_element).to exist - end -end - -Then(/^I can select image size$/) do - expect(on(PreferencesAppearancePage).size_select_element).to exist -end - -Then(/^I can select my time zone$/) do - on(PreferencesAppearancePage) do |page| - expect(page.time_offset_select_element).to exist - expect(page.other_offset_element).to exist - end -end - -Then(/^I can select skin Vector$/) do - on(PreferencesAppearancePage) do |page| - expect(page.vector_element).to exist - end -end - -Then(/^I can select Threshold for stub link$/) do - expect(on(PreferencesAppearancePage).threshold_select_element).to exist -end - -Then(/^I can select thumbnail size$/) do - expect(on(PreferencesAppearancePage).thumb_select_element).to exist -end - -Then(/^I can select underline preferences$/) do - expect(on(PreferencesAppearancePage).underline_select_element).to exist -end - -Then(/^I have advanced options checkboxes$/) do - on(PreferencesAppearancePage) do |page| - expect(page.hidden_categories_check_element).to exist - expect(page.auto_number_check_element).to exist - end -end diff --git a/www/wiki/tests/browser/features/step_definitions/preferences_editing_steps.rb b/www/wiki/tests/browser/features/step_definitions/preferences_editing_steps.rb deleted file mode 100644 index f691ffdc..00000000 --- a/www/wiki/tests/browser/features/step_definitions/preferences_editing_steps.rb +++ /dev/null @@ -1,43 +0,0 @@ -When(/^I click Editing$/) do - visit(PreferencesPage).editing_link_element.when_present.click -end - -Then(/^I can select edit area font style$/) do - expect(on(PreferencesEditingPage).edit_area_font_style_select_element.when_present).to exist -end - -Then(/^I can select live preview$/) do - expect(on(PreferencesEditingPage).live_preview_check_element.when_present).to exist -end - -Then(/^I can select section editing by double clicking$/) do - expect(on(PreferencesEditingPage).edit_section_double_click_check_element.when_present).to exist -end - -Then(/^I can select section editing by right clicking$/) do - expect(on(PreferencesEditingPage).edit_section_right_click_check_element.when_present).to exist -end - -Then(/^I can select section editing via edit links$/) do - expect(on(PreferencesEditingPage).edit_section_edit_link_element.when_present).to exist -end - -Then(/^I can select show edit toolbar$/) do - expect(on(PreferencesEditingPage).show_edit_toolbar_check_element.when_present).to exist -end - -Then(/^I can select show preview before edit box$/) do - expect(on(PreferencesEditingPage).preview_on_top_check_element.when_present).to exist -end - -Then(/^I can select show preview on first edit$/) do - expect(on(PreferencesEditingPage).preview_on_first_check_element.when_present).to exist -end - -Then(/^I can select to prompt me when entering a blank edit summary$/) do - expect(on(PreferencesEditingPage).forced_edit_summary_check_element.when_present).to exist -end - -Then(/^I can select to warn me when I leave an edit page with unsaved changes$/) do - expect(on(PreferencesEditingPage).unsaved_changes_check_element.when_present).to exist -end diff --git a/www/wiki/tests/browser/features/step_definitions/preferences_user_profile_steps.rb b/www/wiki/tests/browser/features/step_definitions/preferences_user_profile_steps.rb deleted file mode 100644 index 5660d491..00000000 --- a/www/wiki/tests/browser/features/step_definitions/preferences_user_profile_steps.rb +++ /dev/null @@ -1,31 +0,0 @@ -When(/^I click User profile$/) do - visit(PreferencesPage).user_profile_link_element.when_present.click -end - -Then(/^I can change my gender$/) do - on(PreferencesUserProfilePage) do |page| - expect(page.gender_undefined_radio_element).to exist - expect(page.gender_male_radio_element).to exist - expect(page.gender_female_radio_element).to exist - end -end - -Then(/^I can change my language$/) do - expect(on(PreferencesUserProfilePage).lang_select_element).to exist -end - -Then(/^I can change my signature$/) do - expect(on(PreferencesUserProfilePage).signature_field_element).to exist -end - -Then(/^I can see my Basic informations$/) do - expect(on(PreferencesUserProfilePage).basic_info_table_element).to exist -end - -Then(/^I can see my email$/) do - expect(on(PreferencesUserProfilePage).email_table_element).to exist -end - -Then(/^I can see my signature$/) do - expect(on(PreferencesUserProfilePage).signature_table_element).to exist -end diff --git a/www/wiki/tests/browser/features/step_definitions/view_history_steps.rb b/www/wiki/tests/browser/features/step_definitions/view_history_steps.rb deleted file mode 100644 index d9b93817..00000000 --- a/www/wiki/tests/browser/features/step_definitions/view_history_steps.rb +++ /dev/null @@ -1,7 +0,0 @@ -When(/^I click View History$/) do - on(ViewHistoryPage).view_history_link -end - -Then(/^I should see a link to a previous version of the page$/) do - expect(on(ViewHistoryPage).old_version_link_element).to be_visible -end diff --git a/www/wiki/tests/browser/features/support/env.rb b/www/wiki/tests/browser/features/support/env.rb deleted file mode 100644 index c1072b26..00000000 --- a/www/wiki/tests/browser/features/support/env.rb +++ /dev/null @@ -1,3 +0,0 @@ -require 'mediawiki_selenium/cucumber' -require 'mediawiki_selenium/pages' -require 'mediawiki_selenium/step_definitions' diff --git a/www/wiki/tests/browser/features/support/hooks.rb b/www/wiki/tests/browser/features/support/hooks.rb deleted file mode 100644 index 85309f39..00000000 --- a/www/wiki/tests/browser/features/support/hooks.rb +++ /dev/null @@ -1,2 +0,0 @@ -# Needed for cucumber --dry-run -f stepdefs -require 'page-object' diff --git a/www/wiki/tests/browser/features/support/pages/create_account_page.rb b/www/wiki/tests/browser/features/support/pages/create_account_page.rb deleted file mode 100644 index 9c1c3ba5..00000000 --- a/www/wiki/tests/browser/features/support/pages/create_account_page.rb +++ /dev/null @@ -1,8 +0,0 @@ -class CreateAccountPage - include PageObject - - page_url '<%=params[:page_title]%>' - - button(:create_account, id: 'wpCreateaccount') - div(:error_message, id: 'mw-createacct-status-area') -end diff --git a/www/wiki/tests/browser/features/support/pages/edit_page.rb b/www/wiki/tests/browser/features/support/pages/edit_page.rb deleted file mode 100644 index b0f6bffe..00000000 --- a/www/wiki/tests/browser/features/support/pages/edit_page.rb +++ /dev/null @@ -1,8 +0,0 @@ -class EditPage - include PageObject - - text_area(:edit_page_content, id: 'wpTextbox1') - button(:preview_button, id: 'wpPreview') - button(:show_changes_button, id: 'wpDiff') - button(:save_button, id: 'wpSave') -end diff --git a/www/wiki/tests/browser/features/support/pages/file_does_not_exist_page.rb b/www/wiki/tests/browser/features/support/pages/file_does_not_exist_page.rb deleted file mode 100644 index 632e3037..00000000 --- a/www/wiki/tests/browser/features/support/pages/file_does_not_exist_page.rb +++ /dev/null @@ -1,7 +0,0 @@ -class FileDoesNotExistPage - include PageObject - - page_url 'File:<%=params[:page_name]%>' - - div(:file_does_not_exist_message, id: 'mw-imagepage-nofile') -end diff --git a/www/wiki/tests/browser/features/support/pages/login_error_page.rb b/www/wiki/tests/browser/features/support/pages/login_error_page.rb deleted file mode 100644 index 9a1805f3..00000000 --- a/www/wiki/tests/browser/features/support/pages/login_error_page.rb +++ /dev/null @@ -1,5 +0,0 @@ -class LoginErrorPage - include PageObject - - div(:error_box, class: 'errorbox') -end diff --git a/www/wiki/tests/browser/features/support/pages/login_page.rb b/www/wiki/tests/browser/features/support/pages/login_page.rb deleted file mode 100644 index 8ef1e44c..00000000 --- a/www/wiki/tests/browser/features/support/pages/login_page.rb +++ /dev/null @@ -1,27 +0,0 @@ -require 'page-object' - -class LoginPage - include PageObject - - page_url 'Special:UserLogin' - - div(:feedback, class: 'errorbox') - button(:login, id: 'wpLoginAttempt') - li(:logout, id: 'pt-logout') - text_field(:password, id: 'wpPassword1') - a(:password_strength, text: 'password strength') - a(:phishing, text: 'phishing') - text_field(:username, id: 'wpName1') - a(:username_displayed, title: /Your user page/) - - def logged_in_as_element - @browser.div(id: 'mw-content-text').p.b - end - - def login_with(username, password, wait_for_logout_element = true) - username_element.when_present.send_keys(username) - password_element.when_present.send_keys(password) - login_element.when_present.click - logout_element.when_present(10) if wait_for_logout_element - end -end diff --git a/www/wiki/tests/browser/features/support/pages/main_page.rb b/www/wiki/tests/browser/features/support/pages/main_page.rb deleted file mode 100644 index 3092ab5c..00000000 --- a/www/wiki/tests/browser/features/support/pages/main_page.rb +++ /dev/null @@ -1,18 +0,0 @@ -class MainPage - include PageObject - - page_url '' - - a(:edit_link, css: '#ca-edit a') - li(:help_link, id: 'n-help') - div(:page_content, id: 'content') - li(:page_information_link, id: 't-info') - li(:permanent_link_link, id: 't-permalink') - a(:printable_version_link, css: '#t-print a') - li(:random_page_link, id: 'n-randompage') - li(:recent_changes_link, id: 'n-recentchanges') - li(:related_changes_link, id: 't-recentchangeslinked') - li(:special_pages_link, id: 't-specialpages') - a(:view_history_link, css: '#ca-history a') - li(:what_links_here_link, id: 't-whatlinkshere') -end diff --git a/www/wiki/tests/browser/features/support/pages/preferences_appearance_page.rb b/www/wiki/tests/browser/features/support/pages/preferences_appearance_page.rb deleted file mode 100644 index c871e642..00000000 --- a/www/wiki/tests/browser/features/support/pages/preferences_appearance_page.rb +++ /dev/null @@ -1,25 +0,0 @@ -class PreferencesAppearancePage - include PageObject - - page_url 'Special:Preferences#mw-prefsection-rendering' - - checkbox(:auto_number_check, id: 'mw-input-wpnumberheadings') - radio_button(:day_mo_year_radio, id: 'mw-input-wpdate-dmy') - checkbox(:dont_show_aft_check, id: 'mw-input-wparticlefeedback-disable') - checkbox(:exclude_from_experiments_check, id: 'mw-input-wpvector-noexperiments') - checkbox(:hidden_categories_check, id: 'mw-input-wpshowhiddencats') - radio_button(:iso_8601_radio, id: 'mw-input-wpdate-ISO_8601') - span(:local_time_span, id: 'wpLocalTime') - radio_button(:mo_day_year_radio, id: 'mw-input-wpdate-mdy') - radio_button(:no_preference_radio, id: 'mw-input-wpdate-default') - text_field(:other_offset, id: 'mw-input-wptimecorrection-other') - a(:restore_default_link, id: 'mw-prefs-restoreprefs') - select_list(:size_select, id: 'mw-input-wpimagesize') - select_list(:threshold_select, id: 'mw-input-wpstubthreshold') - select_list(:time_offset_select, id: 'mw-input-wptimecorrection') - table(:time_offset_table, id: 'mw-htmlform-timeoffset') - select_list(:thumb_select, id: 'mw-input-wpthumbsize') - select_list(:underline_select, id: 'mw-input-wpunderline') - radio_button(:vector, id: 'mw-input-wpskin-vector') - radio_button(:year_mo_day_radio, id: 'mw-input-wpdate-ymd') -end diff --git a/www/wiki/tests/browser/features/support/pages/preferences_editing_page.rb b/www/wiki/tests/browser/features/support/pages/preferences_editing_page.rb deleted file mode 100644 index 3b54d458..00000000 --- a/www/wiki/tests/browser/features/support/pages/preferences_editing_page.rb +++ /dev/null @@ -1,16 +0,0 @@ -class PreferencesEditingPage - include PageObject - - page_url 'Special:Preferences#mw-prefsection-rendering' - - select_list(:edit_area_font_style_select, id: 'mw-input-wpeditfont') - checkbox(:edit_section_double_click_check, id: 'mw-input-wpeditondblclick') - checkbox(:edit_section_edit_link, id: 'mw-input-wpeditsectiononrightclick') - checkbox(:edit_section_right_click_check, id: 'mw-input-wpeditsectiononrightclick') - checkbox(:forced_edit_summary_check, id: 'mw-input-wpforceeditsummary') - checkbox(:live_preview_check, id: 'mw-input-wpuselivepreview') - checkbox(:preview_on_first_check, id: 'mw-input-wppreviewonfirst') - checkbox(:preview_on_top_check, id: 'mw-input-wppreviewontop') - checkbox(:show_edit_toolbar_check, id: 'mw-input-wpshowtoolbar') - checkbox(:unsaved_changes_check, id: 'mw-input-wpuseeditwarning') -end diff --git a/www/wiki/tests/browser/features/support/pages/preferences_page.rb b/www/wiki/tests/browser/features/support/pages/preferences_page.rb deleted file mode 100644 index 1d836ea2..00000000 --- a/www/wiki/tests/browser/features/support/pages/preferences_page.rb +++ /dev/null @@ -1,10 +0,0 @@ -class PreferencesPage - include PageObject - - page_url 'Special:Preferences' - - a(:appearance_link, id: 'preftab-rendering') - a(:editing_link, id: 'preftab-editing') - a(:user_profile_link, id: 'preftab-personal') - button(:save_button, id: 'prefcontrol') -end diff --git a/www/wiki/tests/browser/features/support/pages/preferences_user_profile_page.rb b/www/wiki/tests/browser/features/support/pages/preferences_user_profile_page.rb deleted file mode 100644 index ab5eb93e..00000000 --- a/www/wiki/tests/browser/features/support/pages/preferences_user_profile_page.rb +++ /dev/null @@ -1,16 +0,0 @@ -class PreferencesUserProfilePage - include PageObject - - page_url 'Special:Preferences#mw-prefsection-personal' - - table(:basic_info_table, id: 'mw-htmlform-info') - link(:change_password_link, text: 'Change password') - table(:email_table, id: 'mw-htmlform-email') - radio_button(:gender_female_radio, id: 'mw-input-wpgender-male') - radio_button(:gender_male_radio, id: 'mw-input-wpgender-female') - radio_button(:gender_undefined_radio, id: 'mw-input-wpgender-unknown') - select_list(:lang_select, id: 'mw-input-wplanguage') - checkbox(:remember_password_check, id: 'mw-input-wprememberpassword') - text_field(:signature_field, id: 'mw-input-wpnickname') - table(:signature_table, id: 'mw-htmlform-signature') -end diff --git a/www/wiki/tests/browser/features/support/pages/view_history_page.rb b/www/wiki/tests/browser/features/support/pages/view_history_page.rb deleted file mode 100644 index ee4d757a..00000000 --- a/www/wiki/tests/browser/features/support/pages/view_history_page.rb +++ /dev/null @@ -1,6 +0,0 @@ -class ViewHistoryPage - include PageObject - - a(:view_history_link, css: '#ca-history a') - a(:old_version_link, css: '#pagehistory a.mw-changeslist-date') -end diff --git a/www/wiki/tests/browser/features/support/pages/ztargetpage.rb b/www/wiki/tests/browser/features/support/pages/ztargetpage.rb deleted file mode 100644 index da789e5e..00000000 --- a/www/wiki/tests/browser/features/support/pages/ztargetpage.rb +++ /dev/null @@ -1,7 +0,0 @@ -class ZtargetPage < MainPage - include PageObject - - page_url '<%=params[:article_name]%>' - - a(:link_target_page_link, text: 'link to the test target page') -end diff --git a/www/wiki/tests/browser/features/view_history.feature b/www/wiki/tests/browser/features/view_history.feature deleted file mode 100644 index 95136d27..00000000 --- a/www/wiki/tests/browser/features/view_history.feature +++ /dev/null @@ -1,11 +0,0 @@ -@chrome @firefox @vagrant -Feature: View History - - Scenario: Edit page and view history - Given I go to the "History Test Page" page with content "This is a page that will have history" - When I click Edit - And I edit the page with "Edited and a random string" - And I save the edit - And the edited page content should contain "Edited and a random string" - And I click View History - Then I should see a link to a previous version of the page diff --git a/www/wiki/tests/common/TestSetup.php b/www/wiki/tests/common/TestSetup.php index 3733e60f..c176a67f 100644 --- a/www/wiki/tests/common/TestSetup.php +++ b/www/wiki/tests/common/TestSetup.php @@ -39,7 +39,7 @@ class TestSetup { $wgMainStash = 'hash'; // Use memory job queue $wgJobTypeConf = [ - 'default' => [ 'class' => 'JobQueueMemory', 'order' => 'fifo' ], + 'default' => [ 'class' => JobQueueMemory::class, 'order' => 'fifo' ], ]; $wgUseDatabaseMessages = false; # Set for future resets @@ -47,10 +47,10 @@ class TestSetup { // Assume UTC for testing purposes $wgLocaltimezone = 'UTC'; - $wgLocalisationCacheConf['storeClass'] = 'LCStoreNull'; + $wgLocalisationCacheConf['storeClass'] = LCStoreNull::class; // Do not bother updating search tables - $wgSearchType = 'SearchEngineDummy'; + $wgSearchType = SearchEngineDummy::class; // Generic MediaWiki\Session\SessionManager configuration for tests // We use CookieSessionProvider because things might be expecting diff --git a/www/wiki/tests/common/TestsAutoLoader.php b/www/wiki/tests/common/TestsAutoLoader.php index 8f752df6..abf718d0 100644 --- a/www/wiki/tests/common/TestsAutoLoader.php +++ b/www/wiki/tests/common/TestsAutoLoader.php @@ -24,7 +24,7 @@ global $wgAutoloadClasses; $testDir = __DIR__ . "/.."; -// @codingStandardsIgnoreStart Generic.Files.LineLength.TooLong +// phpcs:disable Generic.Files.LineLength $wgAutoloadClasses += [ # tests/common @@ -62,9 +62,14 @@ $wgAutoloadClasses += [ 'TestUser' => "$testDir/phpunit/includes/TestUser.php", 'TestUserRegistry' => "$testDir/phpunit/includes/TestUserRegistry.php", 'LessFileCompilationTest' => "$testDir/phpunit/LessFileCompilationTest.php", + 'MediaWikiCoversValidator' => "$testDir/phpunit/MediaWikiCoversValidator.php", + 'PHPUnit4And6Compat' => "$testDir/phpunit/PHPUnit4And6Compat.php", + 'HamcrestPHPUnitIntegration' => "$testDir/phpunit/HamcrestPHPUnitIntegration.php", # tests/phpunit/includes - 'RevisionStorageTest' => "$testDir/phpunit/includes/RevisionStorageTest.php", + 'RevisionDbTestBase' => "$testDir/phpunit/includes/RevisionDbTestBase.php", + 'RevisionTestModifyableContent' => "$testDir/phpunit/includes/RevisionTestModifyableContent.php", + 'RevisionTestModifyableContentHandler' => "$testDir/phpunit/includes/RevisionTestModifyableContentHandler.php", 'TestLogger' => "$testDir/phpunit/includes/TestLogger.php", # tests/phpunit/includes/api @@ -74,6 +79,7 @@ $wgAutoloadClasses += [ 'ApiTestCase' => "$testDir/phpunit/includes/api/ApiTestCase.php", 'ApiTestCaseUpload' => "$testDir/phpunit/includes/api/ApiTestCaseUpload.php", 'ApiTestContext' => "$testDir/phpunit/includes/api/ApiTestContext.php", + 'ApiUploadTestCase' => "$testDir/phpunit/includes/api/ApiUploadTestCase.php", 'MockApi' => "$testDir/phpunit/includes/api/MockApi.php", 'MockApiQueryBase' => "$testDir/phpunit/includes/api/MockApiQueryBase.php", 'UserWrapper' => "$testDir/phpunit/includes/api/UserWrapper.php", @@ -92,6 +98,8 @@ $wgAutoloadClasses += [ 'DummyContentForTesting' => "$testDir/phpunit/mocks/content/DummyContentForTesting.php", 'DummyNonTextContentHandler' => "$testDir/phpunit/mocks/content/DummyNonTextContentHandler.php", 'DummyNonTextContent' => "$testDir/phpunit/mocks/content/DummyNonTextContent.php", + 'DummySerializeErrorContentHandler' => + "$testDir/phpunit/mocks/content/DummySerializeErrorContentHandler.php", 'ContentHandlerTest' => "$testDir/phpunit/includes/content/ContentHandlerTest.php", 'JavaScriptContentTest' => "$testDir/phpunit/includes/content/JavaScriptContentTest.php", 'TextContentTest' => "$testDir/phpunit/includes/content/TextContentTest.php", @@ -103,11 +111,14 @@ $wgAutoloadClasses += [ # tests/phpunit/includes/diff 'FakeDiffOp' => "$testDir/phpunit/includes/diff/FakeDiffOp.php", + # tests/phpunit/includes/externalstore + 'ExternalStoreForTesting' => "$testDir/phpunit/includes/externalstore/ExternalStoreForTesting.php", + # tests/phpunit/includes/logging 'LogFormatterTestCase' => "$testDir/phpunit/includes/logging/LogFormatterTestCase.php", # tests/phpunit/includes/page - 'WikiPageTest' => "$testDir/phpunit/includes/page/WikiPageTest.php", + 'WikiPageDbTestBase' => "$testDir/phpunit/includes/page/WikiPageDbTestBase.php", # tests/phpunit/includes/parser 'ParserIntegrationTest' => "$testDir/phpunit/includes/parser/ParserIntegrationTest.php", @@ -137,6 +148,10 @@ $wgAutoloadClasses += [ 'SpecialPageTestBase' => "$testDir/phpunit/includes/specials/SpecialPageTestBase.php", 'SpecialPageExecutor' => "$testDir/phpunit/includes/specials/SpecialPageExecutor.php", + # tests/phpunit/includes/Storage + 'MediaWiki\Tests\Storage\RevisionSlotsTest' => "$testDir/phpunit/includes/Storage/RevisionSlotsTest.php", + 'MediaWiki\Tests\Storage\RevisionRecordTests' => "$testDir/phpunit/includes/Storage/RevisionRecordTests.php", + # tests/phpunit/languages 'LanguageClassesTestCase' => "$testDir/phpunit/languages/LanguageClassesTestCase.php", @@ -144,7 +159,8 @@ $wgAutoloadClasses += [ 'GenericArrayObjectTest' => "$testDir/phpunit/includes/libs/GenericArrayObjectTest.php", # tests/phpunit/maintenance - 'DumpTestCase' => "$testDir/phpunit/maintenance/DumpTestCase.php", + 'MediaWiki\Tests\Maintenance\DumpTestCase' => "$testDir/phpunit/maintenance/DumpTestCase.php", + 'MediaWiki\Tests\Maintenance\MaintenanceBaseTestCase' => "$testDir/phpunit/maintenance/MaintenanceBaseTestCase.php", # tests/phpunit/media 'FakeDimensionFile' => "$testDir/phpunit/includes/media/FakeDimensionFile.php", @@ -164,9 +180,43 @@ $wgAutoloadClasses += [ 'MediaWiki\\Session\\DummySessionBackend' => "$testDir/phpunit/mocks/session/DummySessionBackend.php", 'DummySessionProvider' => "$testDir/phpunit/mocks/session/DummySessionProvider.php", + 'MockMessageLocalizer' => "$testDir/phpunit/mocks/MockMessageLocalizer.php", # tests/suites 'ParserTestFileSuite' => "$testDir/phpunit/suites/ParserTestFileSuite.php", 'ParserTestTopLevelSuite' => "$testDir/phpunit/suites/ParserTestTopLevelSuite.php", ]; -// @codingStandardsIgnoreEnd +// phpcs:enable + +/** + * Alias any PHPUnit 4 era PHPUnit_... class + * to it's PHPUnit 6 replacement. For most classes + * this is a direct _ -> \ replacement, but for + * some others we might need to maintain a manual + * mapping. Once we drop support for PHPUnit 4 this + * should be considered deprecated and eventually removed. + */ +spl_autoload_register( function ( $class ) { + if ( strpos( $class, 'PHPUnit_' ) !== 0 ) { + // Skip if it doesn't start with the old prefix + return; + } + + // Classes that don't map 100% + $map = [ + 'PHPUnit_Framework_TestSuite_DataProvider' => 'PHPUnit\Framework\DataProviderTestSuite', + 'PHPUnit_Framework_Error' => 'PHPUnit\Framework\Error\Error', + ]; + + if ( isset( $map[$class] ) ) { + $newForm = $map[$class]; + } else { + $newForm = str_replace( '_', '\\', $class ); + } + + if ( class_exists( $newForm ) || interface_exists( $newForm ) ) { + // If the new class name exists, alias + // the old name to it. + class_alias( $newForm, $class ); + } +} ); diff --git a/www/wiki/tests/integration/includes/http/CurlHttpRequestTest.php b/www/wiki/tests/integration/includes/http/CurlHttpRequestTest.php index 04f80f43..c1884b87 100644 --- a/www/wiki/tests/integration/includes/http/CurlHttpRequestTest.php +++ b/www/wiki/tests/integration/includes/http/CurlHttpRequestTest.php @@ -1,5 +1,9 @@ <?php +/** + * @group large + * @covers CurlHttpRequest + */ class CurlHttpRequestTest extends MWHttpRequestTestCase { protected static $httpEngine = 'curl'; } diff --git a/www/wiki/tests/integration/includes/http/MWHttpRequestTestCase.php b/www/wiki/tests/integration/includes/http/MWHttpRequestTestCase.php index 81473df2..262eb350 100644 --- a/www/wiki/tests/integration/includes/http/MWHttpRequestTestCase.php +++ b/www/wiki/tests/integration/includes/http/MWHttpRequestTestCase.php @@ -2,7 +2,7 @@ use Wikimedia\TestingAccessWrapper; -class MWHttpRequestTestCase extends PHPUnit_Framework_TestCase { +abstract class MWHttpRequestTestCase extends PHPUnit\Framework\TestCase { protected static $httpEngine; protected $oldHttpEngine; @@ -195,6 +195,11 @@ class MWHttpRequestTestCase extends PHPUnit_Framework_TestCase { $this->assertSame( 401, $request->getStatus() ); } + public function testFactoryDefaults() { + $request = MWHttpRequest::factory( 'http://acme.test' ); + $this->assertInstanceOf( MWHttpRequest::class, $request ); + } + // -------------------- /** @@ -242,4 +247,5 @@ class MWHttpRequestTestCase extends PHPUnit_Framework_TestCase { $this->assertArrayNotHasKey( strtolower( $name ), array_change_key_case( $cookieJar->cookie, CASE_LOWER ) ); } + } diff --git a/www/wiki/tests/integration/includes/http/PhpHttpRequestTest.php b/www/wiki/tests/integration/includes/http/PhpHttpRequestTest.php index d0222a5e..8c461f35 100644 --- a/www/wiki/tests/integration/includes/http/PhpHttpRequestTest.php +++ b/www/wiki/tests/integration/includes/http/PhpHttpRequestTest.php @@ -1,5 +1,9 @@ <?php +/** + * @group large + * @covers PhpHttpRequest + */ class PhpHttpRequestTest extends MWHttpRequestTestCase { protected static $httpEngine = 'php'; } diff --git a/www/wiki/tests/integration/includes/shell/FirejailCommandTest.php b/www/wiki/tests/integration/includes/shell/FirejailCommandTest.php new file mode 100644 index 00000000..1e008ee2 --- /dev/null +++ b/www/wiki/tests/integration/includes/shell/FirejailCommandTest.php @@ -0,0 +1,78 @@ +<?php + +use MediaWiki\Shell\FirejailCommand; +use MediaWiki\Shell\Shell; + +/** + * Integration tests to ensure that firejail actually prevents execution. + * Meant to run on vagrant, although will probably work on other setups + * as long as firejail and sudo has similar config. + * + * @group large + * @group Shell + * @covers FirejailCommand + */ +class FirejailCommandIntegrationTest extends PHPUnit\Framework\TestCase { + + public function setUp() { + parent::setUp(); + if ( Shell::command( 'which', 'firejail' )->execute()->getExitCode() ) { + $this->markTestSkipped( 'firejail not installed' ); + } elseif ( wfIsWindows() ) { + $this->markTestSkipped( 'test supports POSIX environments only' ); + } + } + + public function testSanity() { + // Make sure that firejail works at all. + $command = new FirejailCommand( 'firejail' ); + $command + ->unsafeParams( 'ls .' ) + ->restrict( Shell::RESTRICT_DEFAULT ); + $result = $command->execute(); + $this->assertSame( 0, $result->getExitCode() ); + } + + /** + * @coversNothing + * @dataProvider provideExecute + */ + public function testExecute( $testCommand, $flag ) { + if ( preg_match( '/^sudo /', $testCommand ) ) { + if ( Shell::command( 'sudo', '-n', 'ls', '/' )->execute()->getExitCode() ) { + $this->markTestSkipped( 'need passwordless sudo' ); + } + } + + $command = new FirejailCommand( 'firejail' ); + $command + ->unsafeParams( $testCommand ) + // If we don't restrict at all, firejail won't be invoked, + // so the test will give a false positive if firejail breaks + // the command for some non-flag-related reason. Instead, + // set some flag that won't get in the way. + ->restrict( $flag === Shell::NO_NETWORK ? Shell::PRIVATE_DEV : Shell::NO_NETWORK ); + $result = $command->execute(); + $this->assertSame( 0, $result->getExitCode(), 'sanity check' ); + + $command = new FirejailCommand( 'firejail' ); + $command + ->unsafeParams( $testCommand ) + ->restrict( $flag ); + $result = $command->execute(); + $this->assertNotSame( 0, $result->getExitCode(), 'real check' ); + } + + public function provideExecute() { + global $IP; + return [ + [ 'sudo -n ls /', Shell::NO_ROOT ], + [ 'sudo -n ls /', Shell::SECCOMP ], // not a great test but seems to work + [ 'ls /dev/cpu', Shell::PRIVATE_DEV ], + [ 'curl -fsSo /dev/null https://wikipedia.org/', Shell::NO_NETWORK ], + [ 'exec ls /', Shell::NO_EXECVE ], + [ "cat $IP/LocalSettings.php", Shell::NO_LOCALSETTINGS ], + ]; + } + +} diff --git a/www/wiki/tests/parser/ParserTestParserHook.php b/www/wiki/tests/parser/ParserTestParserHook.php index 5bf50ead..5995012b 100644 --- a/www/wiki/tests/parser/ParserTestParserHook.php +++ b/www/wiki/tests/parser/ParserTestParserHook.php @@ -57,8 +57,7 @@ class ParserTestParserHook { $parser->static_tag_buf = null; return $tmp; } else { // wtf? - return - "\nCall this extension as <statictag>string</statictag> or as" . + return "\nCall this extension as <statictag>string</statictag> or as" . " <statictag action=flush/>, not in any other way.\n" . "text: " . var_export( $in, true ) . "\n" . "argv: " . var_export( $argv, true ) . "\n"; diff --git a/www/wiki/tests/parser/ParserTestResultNormalizer.php b/www/wiki/tests/parser/ParserTestResultNormalizer.php index 61aa0d79..fbeed97b 100644 --- a/www/wiki/tests/parser/ParserTestResultNormalizer.php +++ b/www/wiki/tests/parser/ParserTestResultNormalizer.php @@ -25,11 +25,11 @@ class ParserTestResultNormalizer { // guaranteed to give accurate results. For example, it may introduce // differences in the number of line breaks in <pre> tags. - MediaWiki\suppressWarnings(); + Wikimedia\suppressWarnings(); if ( !$this->doc->loadXML( '<html><body>' . $text . '</body></html>' ) ) { $this->invalid = true; } - MediaWiki\restoreWarnings(); + Wikimedia\restoreWarnings(); $this->xpath = new DOMXPath( $this->doc ); $this->body = $this->xpath->query( '//body' )->item( 0 ); } diff --git a/www/wiki/tests/parser/ParserTestRunner.php b/www/wiki/tests/parser/ParserTestRunner.php index 5fe2177d..844a43f3 100644 --- a/www/wiki/tests/parser/ParserTestRunner.php +++ b/www/wiki/tests/parser/ParserTestRunner.php @@ -290,10 +290,10 @@ class ParserTestRunner { // Set up null lock managers $setup['wgLockManagers'] = [ [ 'name' => 'fsLockManager', - 'class' => 'NullLockManager', + 'class' => NullLockManager::class, ], [ 'name' => 'nullLockManager', - 'class' => 'NullLockManager', + 'class' => NullLockManager::class, ] ]; $reset = function () { LockManagerGroup::destroySingletons(); @@ -384,7 +384,7 @@ class ParserTestRunner { // Changing wgExtraNamespaces invalidates caches in MWNamespace and // any live Language object, both on setup and teardown $reset = function () { - MWNamespace::getCanonicalNamespaces( true ); + MWNamespace::clearCaches(); $GLOBALS['wgContLang']->resetNamespaces(); }; $setup[] = $reset; @@ -435,7 +435,7 @@ class ParserTestRunner { return new RepoGroup( [ - 'class' => 'MockLocalRepo', + 'class' => MockLocalRepo::class, 'name' => 'local', 'url' => 'http://example.com/images', 'hashLevels' => 2, @@ -615,9 +615,13 @@ class ParserTestRunner { return false; } );// hooks::register + // Reset the service in case any other tests already cached some prefixes. + MediaWikiServices::getInstance()->resetServiceForTesting( 'InterwikiLookup' ); + return function () { // Tear down Hooks::clear( 'InterwikiLoadPrefix' ); + MediaWikiServices::getInstance()->resetServiceForTesting( 'InterwikiLookup' ); }; } @@ -636,7 +640,6 @@ class ParserTestRunner { /** * Remove last character if it is a newline - * @group utility * @param string $s * @return string */ @@ -708,15 +711,15 @@ class ParserTestRunner { public function meetsRequirements( $requirements ) { foreach ( $requirements as $requirement ) { switch ( $requirement['type'] ) { - case 'hook': - $ok = $this->requireHook( $requirement['name'] ); - break; - case 'functionHook': - $ok = $this->requireFunctionHook( $requirement['name'] ); - break; - case 'transparentHook': - $ok = $this->requireTransparentHook( $requirement['name'] ); - break; + case 'hook': + $ok = $this->requireHook( $requirement['name'] ); + break; + case 'functionHook': + $ok = $this->requireFunctionHook( $requirement['name'] ); + break; + case 'transparentHook': + $ok = $this->requireTransparentHook( $requirement['name'] ); + break; } if ( !$ok ) { return false; @@ -811,10 +814,6 @@ class ParserTestRunner { $options = ParserOptions::newFromContext( $context ); $options->setTimestamp( $this->getFakeTimestamp() ); - if ( !isset( $opts['wrap'] ) ) { - $options->setWrapOutputClass( false ); - } - if ( isset( $opts['tidy'] ) ) { if ( !$this->tidySupport->isEnabled() ) { $this->recorder->skipped( $test, 'tidy extension is not installed' ); @@ -835,6 +834,19 @@ class ParserTestRunner { $parser = $this->getParser( $preprocessor ); $title = Title::newFromText( $titleText ); + if ( isset( $opts['styletag'] ) ) { + // For testing the behavior of <style> (including those deduplicated + // into <link> tags), add tag hooks to allow them to be generated. + $parser->setHook( 'style', function ( $content, $attributes, $parser ) { + $marker = Parser::MARKER_PREFIX . '-style-' . md5( $content ) . Parser::MARKER_SUFFIX; + $parser->mStripState->addNoWiki( $marker, $content ); + return Html::inlineStyle( $marker, 'all', $attributes ); + } ); + $parser->setHook( 'link', function ( $content, $attributes, $parser ) { + return Html::element( 'link', $attributes ); + } ); + } + if ( isset( $opts['pst'] ) ) { $out = $parser->preSaveTransform( $test['input'], $title, $user, $options ); $output = $parser->getOutput(); @@ -853,8 +865,10 @@ class ParserTestRunner { $out = $parser->getPreloadText( $test['input'], $title, $options ); } else { $output = $parser->parse( $test['input'], $title, $options, true, true, 1337 ); - $output->setTOCEnabled( !isset( $opts['notoc'] ) ); - $out = $output->getText(); + $out = $output->getText( [ + 'allowTOC' => !isset( $opts['notoc'] ), + 'unwrap' => !isset( $opts['wrap'] ), + ] ); if ( isset( $opts['tidy'] ) ) { $out = preg_replace( '/\s+$/', '', $out ); } @@ -891,7 +905,7 @@ class ParserTestRunner { if ( isset( $output ) && isset( $opts['showflags'] ) ) { $actualFlags = array_keys( TestingAccessWrapper::newFromObject( $output )->mFlags ); sort( $actualFlags ); - $out .= "\nflags=" . join( ', ', $actualFlags ); + $out .= "\nflags=" . implode( ', ', $actualFlags ); } ScopedCallback::consume( $teardownGuard ); @@ -1098,6 +1112,7 @@ class ParserTestRunner { // Set content language. This invalidates the magic word cache and title services $lang = Language::factory( $langCode ); + $lang->resetNamespaces(); $setup['wgContLang'] = $lang; $reset = function () { MagicWord::clearCache(); @@ -1150,6 +1165,8 @@ class ParserTestRunner { * @return array */ private function listTables() { + global $wgCommentTableSchemaMigrationStage, $wgActorTableSchemaMigrationStage; + $tables = [ 'user', 'user_properties', 'user_former_groups', 'page', 'page_restrictions', 'protected_titles', 'revision', 'ip_changes', 'text', 'pagelinks', 'imagelinks', 'categorylinks', 'templatelinks', 'externallinks', 'langlinks', 'iwlinks', @@ -1159,6 +1176,19 @@ class ParserTestRunner { 'archive', 'user_groups', 'page_props', 'category' ]; + if ( $wgCommentTableSchemaMigrationStage >= MIGRATION_WRITE_BOTH ) { + // The new tables for comments are in use + $tables[] = 'comment'; + $tables[] = 'revision_comment_temp'; + $tables[] = 'image_comment_temp'; + } + + if ( $wgActorTableSchemaMigrationStage >= MIGRATION_WRITE_BOTH ) { + // The new tables for actors are in use + $tables[] = 'actor'; + $tables[] = 'revision_actor_temp'; + } + if ( in_array( $this->db->getType(), [ 'mysql', 'sqlite', 'oracle' ] ) ) { array_push( $tables, 'searchindex' ); } @@ -1592,11 +1622,21 @@ class ParserTestRunner { throw new MWException( "invalid title '$name' at $file:$line\n" ); } + $newContent = ContentHandler::makeContent( $text, $title ); + $page = WikiPage::factory( $title ); $page->loadPageData( 'fromdbmaster' ); if ( $page->exists() ) { - throw new MWException( "duplicate article '$name' at $file:$line\n" ); + $content = $page->getContent( Revision::RAW ); + // Only reject the title, if the content/content model is different. + // This makes it easier to create Template:(( or Template:)) in different extensions + if ( $newContent->equals( $content ) ) { + return; + } + throw new MWException( + "duplicate article '$name' with different content at $file:$line\n" + ); } // Use mock parser, to make debugging of actual parser tests simpler. @@ -1606,7 +1646,7 @@ class ParserTestRunner { $restore = $this->executeSetupSnippets( [ 'wgParser' => new ParserTestMockParser ] ); try { $status = $page->doEditContent( - ContentHandler::makeContent( $text, $title ), + $newContent, '', EDIT_NEW | EDIT_INTERNAL ); diff --git a/www/wiki/tests/parser/PhpunitTestRecorder.php b/www/wiki/tests/parser/PhpunitTestRecorder.php index 2f82ca72..1a2cfc91 100644 --- a/www/wiki/tests/parser/PhpunitTestRecorder.php +++ b/www/wiki/tests/parser/PhpunitTestRecorder.php @@ -3,7 +3,7 @@ class PhpunitTestRecorder extends TestRecorder { private $testCase; - public function setTestCase( PHPUnit_Framework_TestCase $testCase ) { + public function setTestCase( PHPUnit\Framework\TestCase $testCase ) { $this->testCase = $testCase; } diff --git a/www/wiki/tests/parser/TestFileEditor.php b/www/wiki/tests/parser/TestFileEditor.php index 7f646710..1bee31ea 100644 --- a/www/wiki/tests/parser/TestFileEditor.php +++ b/www/wiki/tests/parser/TestFileEditor.php @@ -162,18 +162,18 @@ class TestFileEditor { if ( isset( $changes[$sectionName] ) ) { $change = $changes[$sectionName]; switch ( $change['op'] ) { - case 'rename': - $test[$i]['name'] = $change['value']; - $test[$i]['headingLine'] = "!! {$change['value']}"; - break; - case 'update': - $test[$i]['contents'] = $change['value']; - break; - case 'delete': - $test[$i]['deleted'] = true; - break; - default: - throw new Exception( "Unknown op: ${change['op']}" ); + case 'rename': + $test[$i]['name'] = $change['value']; + $test[$i]['headingLine'] = "!! {$change['value']}"; + break; + case 'update': + $test[$i]['contents'] = $change['value']; + break; + case 'delete': + $test[$i]['deleted'] = true; + break; + default: + throw new Exception( "Unknown op: ${change['op']}" ); } // Acknowledge // Note that we use the old section name for the rename op diff --git a/www/wiki/tests/parser/TidySupport.php b/www/wiki/tests/parser/TidySupport.php index 6aed02f3..559960de 100644 --- a/www/wiki/tests/parser/TidySupport.php +++ b/www/wiki/tests/parser/TidySupport.php @@ -32,7 +32,7 @@ class TidySupport { * @param bool $useConfiguration */ public function __construct( $useConfiguration = false ) { - global $IP, $wgUseTidy, $wgTidyBin, $wgTidyInternal, $wgTidyConfig, + global $wgUseTidy, $wgTidyBin, $wgTidyInternal, $wgTidyConfig, $wgTidyConf, $wgTidyOpts; $this->enabled = true; @@ -55,26 +55,7 @@ class TidySupport { $this->enabled = false; } } else { - $this->config = [ - 'tidyConfigFile' => "$IP/includes/tidy/tidy.conf", - 'tidyCommandLine' => '', - ]; - if ( extension_loaded( 'tidy' ) && ( wfIsHHVM() || class_exists( 'tidy' ) ) ) { - $this->config['driver'] = wfIsHHVM() ? 'RaggettInternalHHVM' : 'RaggettInternalPHP'; - } else { - if ( is_executable( $wgTidyBin ) ) { - $this->config['driver'] = 'RaggettExternal'; - $this->config['tidyBin'] = $wgTidyBin; - } else { - $path = Installer::locateExecutableInDefaultPaths( $wgTidyBin ); - if ( $path !== false ) { - $this->config['driver'] = 'RaggettExternal'; - $this->config['tidyBin'] = $wgTidyBin; - } else { - $this->enabled = false; - } - } - } + $this->config = [ 'driver' => 'RemexHtml' ]; } if ( !$this->enabled ) { $this->config = [ 'driver' => 'disabled' ]; diff --git a/www/wiki/tests/parser/editTests.php b/www/wiki/tests/parser/editTests.php index a9704e69..3e18370a 100644 --- a/www/wiki/tests/parser/editTests.php +++ b/www/wiki/tests/parser/editTests.php @@ -62,10 +62,9 @@ class ParserEditTests extends Maintenance { } protected function setupFileData() { - global $wgParserTestFiles; $this->testFiles = []; $this->testCount = 0; - foreach ( $wgParserTestFiles as $file ) { + foreach ( ParserTestRunner::getParserTestFiles() as $file ) { $fileInfo = TestFileReader::read( $file ); $this->testFiles[$file] = $fileInfo; $this->testCount += count( $fileInfo['tests'] ); @@ -419,8 +418,7 @@ class ParserEditTests extends Maintenance { print "Wrote updated file\n"; } else { print "Cannot write updated file, here is a patch you can paste:\n\n"; - print - "--- {$fileName}\n" . + print "--- {$fileName}\n" . "+++ {$fileName}~\n" . $this->unifiedDiff( $text, $result ) . "\n"; diff --git a/www/wiki/tests/parser/fuzzTest.php b/www/wiki/tests/parser/fuzzTest.php index 9a2a9c93..eb4181c7 100644 --- a/www/wiki/tests/parser/fuzzTest.php +++ b/www/wiki/tests/parser/fuzzTest.php @@ -165,9 +165,9 @@ class ParserFuzzTest extends Maintenance { function guessVarSize( $var ) { $length = 0; try { - MediaWiki\suppressWarnings(); + Wikimedia\suppressWarnings(); $length = strlen( serialize( $var ) ); - MediaWiki\restoreWarnings(); + Wikimedia\restoreWarnings(); } catch ( Exception $e ) { } return $length; diff --git a/www/wiki/tests/parser/parserTest.inc b/www/wiki/tests/parser/parserTest.inc deleted file mode 100644 index cdc21c85..00000000 --- a/www/wiki/tests/parser/parserTest.inc +++ /dev/null @@ -1,1666 +0,0 @@ -<?php -/** - * Helper code for the MediaWiki parser test suite. Some code is duplicated - * in PHPUnit's NewParserTests.php, so you'll probably want to update both - * at the same time. - * - * Copyright © 2004, 2010 Brion Vibber <brion@pobox.com> - * https://www.mediawiki.org/ - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along - * with this program; if not, write to the Free Software Foundation, Inc., - * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - * http://www.gnu.org/copyleft/gpl.html - * - * @todo Make this more independent of the configuration (and if possible the database) - * @todo document - * @file - * @ingroup Testing - */ - -/** - * @ingroup Testing - */ -class ParserTest { - /** - * @var bool $color whereas output should be colorized - */ - private $color; - - /** - * @var bool $showOutput Show test output - */ - private $showOutput; - - /** - * @var bool $useTemporaryTables Use temporary tables for the temporary database - */ - private $useTemporaryTables = true; - - /** - * @var bool $databaseSetupDone True if the database has been set up - */ - private $databaseSetupDone = false; - - /** - * Our connection to the database - * @var DatabaseBase - */ - private $db; - - /** - * Database clone helper - * @var CloneDatabase - */ - private $dbClone; - - /** - * @var DjVuSupport - */ - private $djVuSupport; - - /** - * @var TidySupport - */ - private $tidySupport; - - private $maxFuzzTestLength = 300; - private $fuzzSeed = 0; - private $memoryLimit = 50; - private $uploadDir = null; - - public $regex = ""; - private $savedGlobals = []; - - /** - * Sets terminal colorization and diff/quick modes depending on OS and - * command-line options (--color and --quick). - * @param array $options - */ - public function __construct( $options = [] ) { - # Only colorize output if stdout is a terminal. - $this->color = !wfIsWindows() && Maintenance::posix_isatty( 1 ); - - if ( isset( $options['color'] ) ) { - switch ( $options['color'] ) { - case 'no': - $this->color = false; - break; - case 'yes': - default: - $this->color = true; - break; - } - } - - $this->term = $this->color - ? new AnsiTermColorer() - : new DummyTermColorer(); - - $this->showDiffs = !isset( $options['quick'] ); - $this->showProgress = !isset( $options['quiet'] ); - $this->showFailure = !( - isset( $options['quiet'] ) - && ( isset( $options['record'] ) - || isset( $options['compare'] ) ) ); // redundant output - - $this->showOutput = isset( $options['show-output'] ); - - if ( isset( $options['filter'] ) ) { - $options['regex'] = $options['filter']; - } - - if ( isset( $options['regex'] ) ) { - if ( isset( $options['record'] ) ) { - echo "Warning: --record cannot be used with --regex, disabling --record\n"; - unset( $options['record'] ); - } - $this->regex = $options['regex']; - } else { - # Matches anything - $this->regex = ''; - } - - $this->setupRecorder( $options ); - $this->keepUploads = isset( $options['keep-uploads'] ); - - if ( $this->keepUploads ) { - $this->uploadDir = wfTempDir() . '/mwParser-images'; - } else { - $this->uploadDir = wfTempDir() . "/mwParser-" . mt_rand() . "-images"; - } - - if ( isset( $options['seed'] ) ) { - $this->fuzzSeed = intval( $options['seed'] ) - 1; - } - - $this->runDisabled = isset( $options['run-disabled'] ); - $this->runParsoid = isset( $options['run-parsoid'] ); - - $this->djVuSupport = new DjVuSupport(); - $this->tidySupport = new TidySupport(); - if ( !$this->tidySupport->isEnabled() ) { - echo "Warning: tidy is not installed, skipping some tests\n"; - } - - if ( !extension_loaded( 'gd' ) ) { - echo "Warning: GD extension is not present, thumbnailing tests will probably fail\n"; - } - - $this->hooks = []; - $this->functionHooks = []; - $this->transparentHooks = []; - $this->setUp(); - } - - function setUp() { - global $wgParser, $wgParserConf, $IP, $messageMemc, $wgMemc, - $wgUser, $wgLang, $wgOut, $wgRequest, $wgStyleDirectory, - $wgExtraNamespaces, $wgNamespaceAliases, $wgNamespaceProtection, $wgLocalFileRepo, - $wgExtraInterlanguageLinkPrefixes, $wgLocalInterwikis, - $parserMemc, $wgThumbnailScriptPath, $wgScriptPath, $wgResourceBasePath, - $wgArticlePath, $wgScript, $wgStylePath, $wgExtensionAssetsPath, - $wgMainCacheType, $wgMessageCacheType, $wgParserCacheType, $wgLockManagers; - - $wgScriptPath = ''; - $wgScript = '/index.php'; - $wgStylePath = '/skins'; - $wgResourceBasePath = ''; - $wgExtensionAssetsPath = '/extensions'; - $wgArticlePath = '/wiki/$1'; - $wgThumbnailScriptPath = false; - $wgLockManagers = [ [ - 'name' => 'fsLockManager', - 'class' => 'FSLockManager', - 'lockDirectory' => $this->uploadDir . '/lockdir', - ], [ - 'name' => 'nullLockManager', - 'class' => 'NullLockManager', - ] ]; - $wgLocalFileRepo = [ - 'class' => 'LocalRepo', - 'name' => 'local', - 'url' => 'http://example.com/images', - 'hashLevels' => 2, - 'transformVia404' => false, - 'backend' => new FSFileBackend( [ - 'name' => 'local-backend', - 'wikiId' => wfWikiID(), - 'containerPaths' => [ - 'local-public' => $this->uploadDir . '/public', - 'local-thumb' => $this->uploadDir . '/thumb', - 'local-temp' => $this->uploadDir . '/temp', - 'local-deleted' => $this->uploadDir . '/deleted', - ] - ] ) - ]; - $wgNamespaceProtection[NS_MEDIAWIKI] = 'editinterface'; - $wgNamespaceAliases['Image'] = NS_FILE; - $wgNamespaceAliases['Image_talk'] = NS_FILE_TALK; - # add a namespace shadowing a interwiki link, to test - # proper precedence when resolving links. (bug 51680) - $wgExtraNamespaces[100] = 'MemoryAlpha'; - - // XXX: tests won't run without this (for CACHE_DB) - if ( $wgMainCacheType === CACHE_DB ) { - $wgMainCacheType = CACHE_NONE; - } - if ( $wgMessageCacheType === CACHE_DB ) { - $wgMessageCacheType = CACHE_NONE; - } - if ( $wgParserCacheType === CACHE_DB ) { - $wgParserCacheType = CACHE_NONE; - } - - DeferredUpdates::clearPendingUpdates(); - $wgMemc = wfGetMainCache(); // checks $wgMainCacheType - $messageMemc = wfGetMessageCacheStorage(); - $parserMemc = wfGetParserCacheStorage(); - - RequestContext::resetMain(); - $context = new RequestContext; - $wgUser = new User; - $wgLang = $context->getLanguage(); - $wgOut = $context->getOutput(); - $wgRequest = $context->getRequest(); - $wgParser = new StubObject( 'wgParser', $wgParserConf['class'], [ $wgParserConf ] ); - - if ( $wgStyleDirectory === false ) { - $wgStyleDirectory = "$IP/skins"; - } - - self::setupInterwikis(); - $wgLocalInterwikis = [ 'local', 'mi' ]; - // "extra language links" - // see https://gerrit.wikimedia.org/r/111390 - array_push( $wgExtraInterlanguageLinkPrefixes, 'mul' ); - - // Reset namespace cache - MWNamespace::getCanonicalNamespaces( true ); - Language::factory( 'en' )->resetNamespaces(); - } - - /** - * Insert hardcoded interwiki in the lookup table. - * - * This function insert a set of well known interwikis that are used in - * the parser tests. They can be considered has fixtures are injected in - * the interwiki cache by using the 'InterwikiLoadPrefix' hook. - * Since we are not interested in looking up interwikis in the database, - * the hook completely replace the existing mechanism (hook returns false). - */ - public static function setupInterwikis() { - # Hack: insert a few Wikipedia in-project interwiki prefixes, - # for testing inter-language links - Hooks::register( 'InterwikiLoadPrefix', function ( $prefix, &$iwData ) { - static $testInterwikis = [ - 'local' => [ - 'iw_url' => 'http://doesnt.matter.org/$1', - 'iw_api' => '', - 'iw_wikiid' => '', - 'iw_local' => 0 ], - 'wikipedia' => [ - 'iw_url' => 'http://en.wikipedia.org/wiki/$1', - 'iw_api' => '', - 'iw_wikiid' => '', - 'iw_local' => 0 ], - 'meatball' => [ - 'iw_url' => 'http://www.usemod.com/cgi-bin/mb.pl?$1', - 'iw_api' => '', - 'iw_wikiid' => '', - 'iw_local' => 0 ], - 'memoryalpha' => [ - 'iw_url' => 'http://www.memory-alpha.org/en/index.php/$1', - 'iw_api' => '', - 'iw_wikiid' => '', - 'iw_local' => 0 ], - 'zh' => [ - 'iw_url' => 'http://zh.wikipedia.org/wiki/$1', - 'iw_api' => '', - 'iw_wikiid' => '', - 'iw_local' => 1 ], - 'es' => [ - 'iw_url' => 'http://es.wikipedia.org/wiki/$1', - 'iw_api' => '', - 'iw_wikiid' => '', - 'iw_local' => 1 ], - 'fr' => [ - 'iw_url' => 'http://fr.wikipedia.org/wiki/$1', - 'iw_api' => '', - 'iw_wikiid' => '', - 'iw_local' => 1 ], - 'ru' => [ - 'iw_url' => 'http://ru.wikipedia.org/wiki/$1', - 'iw_api' => '', - 'iw_wikiid' => '', - 'iw_local' => 1 ], - 'mi' => [ - 'iw_url' => 'http://mi.wikipedia.org/wiki/$1', - 'iw_api' => '', - 'iw_wikiid' => '', - 'iw_local' => 1 ], - 'mul' => [ - 'iw_url' => 'http://wikisource.org/wiki/$1', - 'iw_api' => '', - 'iw_wikiid' => '', - 'iw_local' => 1 ], - ]; - if ( array_key_exists( $prefix, $testInterwikis ) ) { - $iwData = $testInterwikis[$prefix]; - } - - // We only want to rely on the above fixtures - return false; - } );// hooks::register - } - - /** - * Remove the hardcoded interwiki lookup table. - */ - public static function tearDownInterwikis() { - Hooks::clear( 'InterwikiLoadPrefix' ); - } - - public function setupRecorder( $options ) { - if ( isset( $options['record'] ) ) { - $this->recorder = new DbTestRecorder( $this ); - $this->recorder->version = isset( $options['setversion'] ) ? - $options['setversion'] : SpecialVersion::getVersion(); - } elseif ( isset( $options['compare'] ) ) { - $this->recorder = new DbTestPreviewer( $this ); - } else { - $this->recorder = new TestRecorder( $this ); - } - } - - /** - * Remove last character if it is a newline - * @group utility - * @param string $s - * @return string - */ - public static function chomp( $s ) { - if ( substr( $s, -1 ) === "\n" ) { - return substr( $s, 0, -1 ); - } else { - return $s; - } - } - - /** - * Run a fuzz test series - * Draw input from a set of test files - * @param array $filenames - */ - function fuzzTest( $filenames ) { - $GLOBALS['wgContLang'] = Language::factory( 'en' ); - $dict = $this->getFuzzInput( $filenames ); - $dictSize = strlen( $dict ); - $logMaxLength = log( $this->maxFuzzTestLength ); - $this->setupDatabase(); - ini_set( 'memory_limit', $this->memoryLimit * 1048576 ); - - $numTotal = 0; - $numSuccess = 0; - $user = new User; - $opts = ParserOptions::newFromUser( $user ); - $title = Title::makeTitle( NS_MAIN, 'Parser_test' ); - - while ( true ) { - // Generate test input - mt_srand( ++$this->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 ); - $fail = false; - } catch ( Exception $exception ) { - $fail = true; - } - - if ( $fail ) { - echo "Test failed with seed {$this->fuzzSeed}\n"; - echo "Input:\n"; - printf( "string(%d) \"%s\"\n\n", strlen( $input ), $input ); - echo "$exception\n"; - } else { - $numSuccess++; - } - - $numTotal++; - $this->teardownGlobals(); - $parser->__destruct(); - - if ( $numTotal % 100 == 0 ) { - $usage = intval( memory_get_usage( true ) / $this->memoryLimit / 1048576 * 100 ); - echo "{$this->fuzzSeed}: $numSuccess/$numTotal (mem: $usage%)\n"; - if ( $usage > 90 ) { - echo "Out of memory:\n"; - $memStats = $this->getMemoryBreakdown(); - - foreach ( $memStats as $name => $usage ) { - echo "$name: $usage\n"; - } - $this->abort(); - } - } - } - } - - /** - * Get an input dictionary from a set of parser test files - * @param array $filenames - * @return string - */ - function getFuzzInput( $filenames ) { - $dict = ''; - - foreach ( $filenames as $filename ) { - $contents = file_get_contents( $filename ); - preg_match_all( - '/!!\s*(input|wikitext)\n(.*?)\n!!\s*(result|html|html\/\*|html\/php)/s', - $contents, - $matches - ); - - foreach ( $matches[1] as $match ) { - $dict .= $match . "\n"; - } - } - - return $dict; - } - - /** - * Get a memory usage breakdown - * @return array - */ - function getMemoryBreakdown() { - $memStats = []; - - 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; - } - - function abort() { - $this->abort(); - } - - /** - * Run a series of tests listed in the given text files. - * Each test consists of a brief description, wikitext input, - * and the expected HTML output. - * - * Prints status updates on stdout and counts up the total - * number and percentage of passed tests. - * - * @param array $filenames Array of strings - * @return bool True if passed all tests, false if any tests failed. - */ - public function runTestsFromFiles( $filenames ) { - $ok = false; - - // be sure, ParserTest::addArticle has correct language set, - // so that system messages gets into the right language cache - $GLOBALS['wgLanguageCode'] = 'en'; - $GLOBALS['wgContLang'] = Language::factory( 'en' ); - - $this->recorder->start(); - try { - $this->setupDatabase(); - $ok = true; - - foreach ( $filenames as $filename ) { - echo "Running parser tests from: $filename\n"; - $tests = new TestFileIterator( $filename, $this ); - $ok = $this->runTests( $tests ) && $ok; - } - - $this->teardownDatabase(); - $this->recorder->report(); - } catch ( DBError $e ) { - echo $e->getMessage(); - } - $this->recorder->end(); - - return $ok; - } - - function runTests( $tests ) { - $ok = true; - - foreach ( $tests as $t ) { - $result = - $this->runTest( $t['test'], $t['input'], $t['result'], $t['options'], $t['config'] ); - $ok = $ok && $result; - $this->recorder->record( $t['test'], $t['subtest'], $result ); - } - - if ( $this->showProgress ) { - print "\n"; - } - - return $ok; - } - - /** - * Get a Parser object - * - * @param string $preprocessor - * @return Parser - */ - function getParser( $preprocessor = null ) { - global $wgParserConf; - - $class = $wgParserConf['class']; - $parser = new $class( [ 'preprocessorClass' => $preprocessor ] + $wgParserConf ); - - foreach ( $this->hooks as $tag => $callback ) { - $parser->setHook( $tag, $callback ); - } - - foreach ( $this->functionHooks as $tag => $bits ) { - list( $callback, $flags ) = $bits; - $parser->setFunctionHook( $tag, $callback, $flags ); - } - - foreach ( $this->transparentHooks as $tag => $callback ) { - $parser->setTransparentTagHook( $tag, $callback ); - } - - Hooks::run( 'ParserTestParser', [ &$parser ] ); - - return $parser; - } - - /** - * Run a given wikitext input through a freshly-constructed wiki parser, - * and compare the output against the expected results. - * Prints status and explanatory messages to stdout. - * - * @param string $desc Test's description - * @param string $input Wikitext to try rendering - * @param string $result Result to output - * @param array $opts Test's options - * @param string $config Overrides for global variables, one per line - * @return bool - */ - public function runTest( $desc, $input, $result, $opts, $config ) { - if ( $this->showProgress ) { - $this->showTesting( $desc ); - } - - $opts = $this->parseOptions( $opts ); - $context = $this->setupGlobals( $opts, $config ); - - $user = $context->getUser(); - $options = ParserOptions::newFromContext( $context ); - - if ( isset( $opts['djvu'] ) ) { - if ( !$this->djVuSupport->isEnabled() ) { - return $this->showSkipped(); - } - } - - if ( isset( $opts['tidy'] ) ) { - if ( !$this->tidySupport->isEnabled() ) { - return $this->showSkipped(); - } else { - $options->setTidy( true ); - } - } - - if ( isset( $opts['title'] ) ) { - $titleText = $opts['title']; - } else { - $titleText = 'Parser test'; - } - - ObjectCache::getMainWANInstance()->clearProcessCache(); - $local = isset( $opts['local'] ); - $preprocessor = isset( $opts['preprocessor'] ) ? $opts['preprocessor'] : null; - $parser = $this->getParser( $preprocessor ); - $title = Title::newFromText( $titleText ); - - 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['tidy'] ) ) { - $out = preg_replace( '/\s+$/', '', $out ); - } - - if ( isset( $opts['showtitle'] ) ) { - if ( $output->getTitleText() ) { - $title = $output->getTitleText(); - } - - $out = "$title\n$out"; - } - - if ( isset( $opts['showindicators'] ) ) { - $indicators = ''; - foreach ( $output->getIndicators() as $id => $content ) { - $indicators .= "$id=$content\n"; - } - $out = $indicators . $out; - } - - if ( isset( $opts['ill'] ) ) { - $out = implode( ' ', $output->getLanguageLinks() ); - } elseif ( isset( $opts['cat'] ) ) { - $outputPage = $context->getOutput(); - $outputPage->addCategoryLinks( $output->getCategories() ); - $cats = $outputPage->getCategoryLinks(); - - if ( isset( $cats['normal'] ) ) { - $out = implode( ' ', $cats['normal'] ); - } else { - $out = ''; - } - } - } - - $this->teardownGlobals(); - - $testResult = new ParserTestResult( $desc ); - $testResult->expected = $result; - $testResult->actual = $out; - - return $this->showTestResult( $testResult ); - } - - /** - * Refactored in 1.22 to use ParserTestResult - * @param ParserTestResult $testResult - * @return bool - */ - function showTestResult( ParserTestResult $testResult ) { - if ( $testResult->isSuccess() ) { - $this->showSuccess( $testResult ); - return true; - } else { - $this->showFailure( $testResult ); - return false; - } - } - - /** - * Use a regex to find out the value of an option - * @param string $key Name of option val to retrieve - * @param array $opts Options array to look in - * @param mixed $default Default value returned if not found - * @return mixed - */ - private static function getOptionValue( $key, $opts, $default ) { - $key = strtolower( $key ); - - if ( isset( $opts[$key] ) ) { - return $opts[$key]; - } else { - return $default; - } - } - - private function parseOptions( $instring ) { - $opts = []; - // foo - // foo=bar - // foo="bar baz" - // foo=[[bar baz]] - // foo=bar,"baz quux" - // foo={...json...} - $defs = '(?(DEFINE) - (?<qstr> # Quoted string - " - (?:[^\\\\"] | \\\\.)* - " - ) - (?<json> - \{ # Open bracket - (?: - [^"{}] | # Not a quoted string or object, or - (?&qstr) | # A quoted string, or - (?&json) # A json object (recursively) - )* - \} # Close bracket - ) - (?<value> - (?: - (?&qstr) # Quoted val - | - \[\[ - [^]]* # Link target - \]\] - | - [\w-]+ # Plain word - | - (?&json) # JSON object - ) - ) - )'; - $regex = '/' . $defs . '\b - (?<k>[\w-]+) # Key - \b - (?:\s* - = # First sub-value - \s* - (?<v> - (?&value) - (?:\s* - , # Sub-vals 1..N - \s* - (?&value) - )* - ) - )? - /x'; - $valueregex = '/' . $defs . '(?&value)/x'; - - if ( preg_match_all( $regex, $instring, $matches, PREG_SET_ORDER ) ) { - foreach ( $matches as $bits ) { - $key = strtolower( $bits['k'] ); - if ( !isset( $bits['v'] ) ) { - $opts[$key] = true; - } else { - preg_match_all( $valueregex, $bits['v'], $vmatches ); - $opts[$key] = array_map( [ $this, 'cleanupOption' ], $vmatches[0] ); - if ( count( $opts[$key] ) == 1 ) { - $opts[$key] = $opts[$key][0]; - } - } - } - } - return $opts; - } - - private function cleanupOption( $opt ) { - if ( substr( $opt, 0, 1 ) == '"' ) { - return stripcslashes( substr( $opt, 1, -1 ) ); - } - - if ( substr( $opt, 0, 2 ) == '[[' ) { - return substr( $opt, 2, -2 ); - } - - if ( substr( $opt, 0, 1 ) == '{' ) { - return FormatJson::decode( $opt, true ); - } - return $opt; - } - - /** - * Set up the global variables for a consistent environment for each test. - * Ideally this should replace the global configuration entirely. - * @param string $opts - * @param string $config - * @return RequestContext - */ - private function setupGlobals( $opts = '', $config = '' ) { - global $IP; - - # 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 ); - - $settings = [ - 'wgServer' => 'http://example.org', - 'wgServerName' => 'example.org', - 'wgScript' => '/index.php', - 'wgScriptPath' => '', - 'wgArticlePath' => '/wiki/$1', - 'wgActionPaths' => [], - 'wgLockManagers' => [ [ - 'name' => 'fsLockManager', - 'class' => 'FSLockManager', - 'lockDirectory' => $this->uploadDir . '/lockdir', - ], [ - 'name' => 'nullLockManager', - 'class' => 'NullLockManager', - ] ], - 'wgLocalFileRepo' => [ - 'class' => 'LocalRepo', - 'name' => 'local', - 'url' => 'http://example.com/images', - 'hashLevels' => 2, - 'transformVia404' => false, - 'backend' => new FSFileBackend( [ - 'name' => 'local-backend', - 'wikiId' => wfWikiID(), - 'containerPaths' => [ - 'local-public' => $this->uploadDir, - 'local-thumb' => $this->uploadDir . '/thumb', - 'local-temp' => $this->uploadDir . '/temp', - 'local-deleted' => $this->uploadDir . '/delete', - ] - ] ) - ], - 'wgEnableUploads' => self::getOptionValue( 'wgEnableUploads', $opts, true ), - 'wgUploadNavigationUrl' => false, - 'wgStylePath' => '/skins', - 'wgSitename' => 'MediaWiki', - 'wgLanguageCode' => $lang, - 'wgDBprefix' => $this->db->getType() != 'oracle' ? 'parsertest_' : 'pt_', - 'wgRawHtml' => self::getOptionValue( 'wgRawHtml', $opts, false ), - 'wgLang' => null, - 'wgContLang' => null, - 'wgNamespacesWithSubpages' => [ 0 => isset( $opts['subpage'] ) ], - 'wgMaxTocLevel' => $maxtoclevel, - 'wgCapitalLinks' => true, - 'wgNoFollowLinks' => true, - 'wgNoFollowDomainExceptions' => [], - 'wgThumbnailScriptPath' => false, - 'wgUseImageResize' => true, - 'wgSVGConverter' => 'null', - 'wgSVGConverters' => [ 'null' => 'echo "1">$output' ], - 'wgLocaltimezone' => 'UTC', - 'wgAllowExternalImages' => self::getOptionValue( 'wgAllowExternalImages', $opts, true ), - 'wgThumbLimits' => [ self::getOptionValue( 'thumbsize', $opts, 180 ) ], - 'wgDefaultLanguageVariant' => $variant, - 'wgVariantArticlePath' => false, - 'wgGroupPermissions' => [ '*' => [ - 'createaccount' => true, - 'read' => true, - 'edit' => true, - 'createpage' => true, - 'createtalk' => true, - ] ], - 'wgNamespaceProtection' => [ NS_MEDIAWIKI => 'editinterface' ], - 'wgDefaultExternalStore' => [], - 'wgForeignFileRepos' => [], - 'wgLinkHolderBatchSize' => $linkHolderBatchSize, - 'wgExperimentalHtmlIds' => false, - 'wgExternalLinkTarget' => false, - 'wgHtml5' => true, - 'wgAdaptiveMessageCache' => true, - 'wgDisableLangConversion' => false, - 'wgDisableTitleConversion' => false, - // Tidy options. - 'wgUseTidy' => isset( $opts['tidy'] ), - 'wgTidyConfig' => null, - 'wgDebugTidy' => false, - 'wgTidyConf' => $IP . '/includes/tidy/tidy.conf', - 'wgTidyOpts' => '', - 'wgTidyInternal' => $this->tidySupport->isInternal(), - ]; - - if ( $config ) { - $configLines = explode( "\n", $config ); - - foreach ( $configLines as $line ) { - list( $var, $value ) = explode( '=', $line, 2 ); - - $settings[$var] = eval( "return $value;" ); - } - } - - $this->savedGlobals = []; - - /** @since 1.20 */ - Hooks::run( 'ParserTestGlobals', [ &$settings ] ); - - foreach ( $settings as $var => $val ) { - if ( array_key_exists( $var, $GLOBALS ) ) { - $this->savedGlobals[$var] = $GLOBALS[$var]; - } - - $GLOBALS[$var] = $val; - } - - // Must be set before $context as user language defaults to $wgContLang - $GLOBALS['wgContLang'] = Language::factory( $lang ); - $GLOBALS['wgMemc'] = new EmptyBagOStuff; - - RequestContext::resetMain(); - $context = RequestContext::getMain(); - $GLOBALS['wgLang'] = $context->getLanguage(); - $GLOBALS['wgOut'] = $context->getOutput(); - $GLOBALS['wgUser'] = $context->getUser(); - - // We (re)set $wgThumbLimits to a single-element array above. - $context->getUser()->setOption( 'thumbsize', 0 ); - - global $wgHooks; - - $wgHooks['ParserTestParser'][] = 'ParserTestParserHook::setup'; - $wgHooks['ParserGetVariableValueTs'][] = 'ParserTest::getFakeTimestamp'; - - MagicWord::clearCache(); - MWTidy::destroySingleton(); - RepoGroup::destroySingleton(); - - return $context; - } - - /** - * List of temporary tables to create, without prefix. - * Some of these probably aren't necessary. - * @return array - */ - private function listTables() { - $tables = [ 'user', 'user_properties', 'user_former_groups', 'page', 'page_restrictions', - 'protected_titles', 'revision', 'text', 'pagelinks', 'imagelinks', - 'categorylinks', 'templatelinks', 'externallinks', 'langlinks', 'iwlinks', - 'site_stats', 'ipblocks', 'image', 'oldimage', - 'recentchanges', 'watchlist', 'interwiki', 'logging', 'log_search', - 'querycache', 'objectcache', 'job', 'l10n_cache', 'redirect', 'querycachetwo', - 'archive', 'user_groups', 'page_props', 'category' - ]; - - if ( in_array( $this->db->getType(), [ 'mysql', 'sqlite', 'oracle' ] ) ) { - array_push( $tables, 'searchindex' ); - } - - // Allow extensions to add to the list of tables to duplicate; - // may be necessary if they hook into page save or other code - // which will require them while running tests. - Hooks::run( 'ParserTestTables', [ &$tables ] ); - - return $tables; - } - - /** - * Set up a temporary set of wiki tables to work with for the tests. - * Currently this will only be done once per run, and any changes to - * the db will be visible to later tests in the run. - */ - public function setupDatabase() { - global $wgDBprefix; - - if ( $this->databaseSetupDone ) { - return; - } - - $this->db = wfGetDB( DB_MASTER ); - $dbType = $this->db->getType(); - - if ( $wgDBprefix === 'parsertest_' || ( $dbType == 'oracle' && $wgDBprefix === 'pt_' ) ) { - throw new MWException( 'setupDatabase should be called before setupGlobals' ); - } - - $this->databaseSetupDone = true; - - # SqlBagOStuff broke when using temporary tables on r40209 (bug 15892). - # It seems to have been fixed since (r55079?), but regressed at some point before r85701. - # This works around it for now... - ObjectCache::$instances[CACHE_DB] = new HashBagOStuff; - - # CREATE TEMPORARY TABLE breaks if there is more than one server - if ( wfGetLB()->getServerCount() != 1 ) { - $this->useTemporaryTables = false; - } - - $temporary = $this->useTemporaryTables || $dbType == 'postgres'; - $prefix = $dbType != 'oracle' ? 'parsertest_' : 'pt_'; - - $this->dbClone = new CloneDatabase( $this->db, $this->listTables(), $prefix ); - $this->dbClone->useTemporaryTables( $temporary ); - $this->dbClone->cloneTableStructure(); - - if ( $dbType == 'oracle' ) { - $this->db->query( 'BEGIN FILL_WIKI_INFO; END;' ); - # Insert 0 user to prevent FK violations - - # Anonymous user - $this->db->insert( 'user', [ - 'user_id' => 0, - 'user_name' => 'Anonymous' ] ); - } - - # Update certain things in site_stats - $this->db->insert( 'site_stats', - [ 'ss_row_id' => 1, 'ss_images' => 2, 'ss_good_articles' => 1 ] ); - - # Reinitialise the LocalisationCache to match the database state - Language::getLocalisationCache()->unloadAll(); - - # Clear the message cache - MessageCache::singleton()->clear(); - - // Remember to update newParserTests.php after changing the below - // (and it uses a slightly different syntax just for teh lulz) - $this->setupUploadDir(); - $user = User::createNew( 'WikiSysop' ); - $image = wfLocalFile( Title::makeTitle( NS_FILE, 'Foobar.jpg' ) ); - # note that the size/width/height/bits/etc of the file - # are actually set by inspecting the file itself; the arguments - # to recordUpload2 have no effect. That said, we try to make things - # match up so it is less confusing to readers of the code & tests. - $image->recordUpload2( '', 'Upload of some lame file', 'Some lame file', [ - 'size' => 7881, - 'width' => 1941, - 'height' => 220, - 'bits' => 8, - 'media_type' => MEDIATYPE_BITMAP, - 'mime' => 'image/jpeg', - 'metadata' => serialize( [] ), - 'sha1' => Wikimedia\base_convert( '1', 16, 36, 31 ), - 'fileExists' => true - ], $this->db->timestamp( '20010115123500' ), $user ); - - $image = wfLocalFile( Title::makeTitle( NS_FILE, 'Thumb.png' ) ); - # again, note that size/width/height below are ignored; see above. - $image->recordUpload2( '', 'Upload of some lame thumbnail', 'Some lame thumbnail', [ - 'size' => 22589, - 'width' => 135, - 'height' => 135, - 'bits' => 8, - 'media_type' => MEDIATYPE_BITMAP, - 'mime' => 'image/png', - 'metadata' => serialize( [] ), - 'sha1' => Wikimedia\base_convert( '2', 16, 36, 31 ), - 'fileExists' => true - ], $this->db->timestamp( '20130225203040' ), $user ); - - $image = wfLocalFile( Title::makeTitle( NS_FILE, 'Foobar.svg' ) ); - $image->recordUpload2( '', 'Upload of some lame SVG', 'Some lame SVG', [ - 'size' => 12345, - 'width' => 240, - 'height' => 180, - 'bits' => 0, - 'media_type' => MEDIATYPE_DRAWING, - 'mime' => 'image/svg+xml', - 'metadata' => serialize( [] ), - 'sha1' => Wikimedia\base_convert( '', 16, 36, 31 ), - 'fileExists' => true - ], $this->db->timestamp( '20010115123500' ), $user ); - - # This image will be blacklisted in [[MediaWiki:Bad image list]] - $image = wfLocalFile( Title::makeTitle( NS_FILE, 'Bad.jpg' ) ); - $image->recordUpload2( '', 'zomgnotcensored', 'Borderline image', [ - 'size' => 12345, - 'width' => 320, - 'height' => 240, - 'bits' => 24, - 'media_type' => MEDIATYPE_BITMAP, - 'mime' => 'image/jpeg', - 'metadata' => serialize( [] ), - 'sha1' => Wikimedia\base_convert( '3', 16, 36, 31 ), - 'fileExists' => true - ], $this->db->timestamp( '20010115123500' ), $user ); - - $image = wfLocalFile( Title::makeTitle( NS_FILE, 'Video.ogv' ) ); - $image->recordUpload2( '', 'A pretty movie', 'Will it play', [ - 'size' => 12345, - 'width' => 320, - 'height' => 240, - 'bits' => 0, - 'media_type' => MEDIATYPE_VIDEO, - 'mime' => 'application/ogg', - 'metadata' => serialize( [] ), - 'sha1' => Wikimedia\base_convert( '', 16, 36, 31 ), - 'fileExists' => true - ], $this->db->timestamp( '20010115123500' ), $user ); - - # A DjVu file - $image = wfLocalFile( Title::makeTitle( NS_FILE, 'LoremIpsum.djvu' ) ); - $image->recordUpload2( '', 'Upload a DjVu', 'A DjVu', [ - 'size' => 3249, - 'width' => 2480, - 'height' => 3508, - 'bits' => 0, - 'media_type' => MEDIATYPE_BITMAP, - 'mime' => 'image/vnd.djvu', - 'metadata' => '<?xml version="1.0" ?> -<!DOCTYPE DjVuXML PUBLIC "-//W3C//DTD DjVuXML 1.1//EN" "pubtext/DjVuXML-s.dtd"> -<DjVuXML> -<HEAD></HEAD> -<BODY><OBJECT height="3508" width="2480"> -<PARAM name="DPI" value="300" /> -<PARAM name="GAMMA" value="2.2" /> -</OBJECT> -<OBJECT height="3508" width="2480"> -<PARAM name="DPI" value="300" /> -<PARAM name="GAMMA" value="2.2" /> -</OBJECT> -<OBJECT height="3508" width="2480"> -<PARAM name="DPI" value="300" /> -<PARAM name="GAMMA" value="2.2" /> -</OBJECT> -<OBJECT height="3508" width="2480"> -<PARAM name="DPI" value="300" /> -<PARAM name="GAMMA" value="2.2" /> -</OBJECT> -<OBJECT height="3508" width="2480"> -<PARAM name="DPI" value="300" /> -<PARAM name="GAMMA" value="2.2" /> -</OBJECT> -</BODY> -</DjVuXML>', - 'sha1' => Wikimedia\base_convert( '', 16, 36, 31 ), - 'fileExists' => true - ], $this->db->timestamp( '20010115123600' ), $user ); - } - - public function teardownDatabase() { - if ( !$this->databaseSetupDone ) { - $this->teardownGlobals(); - return; - } - $this->teardownUploadDir( $this->uploadDir ); - - $this->dbClone->destroy(); - $this->databaseSetupDone = false; - - if ( $this->useTemporaryTables ) { - if ( $this->db->getType() == 'sqlite' ) { - # Under SQLite the searchindex table is virtual and need - # to be explicitly destroyed. See bug 29912 - # See also MediaWikiTestCase::destroyDB() - wfDebug( __METHOD__ . " explicitly destroying sqlite virtual table parsertest_searchindex\n" ); - $this->db->query( "DROP TABLE `parsertest_searchindex`" ); - } - # Don't need to do anything - $this->teardownGlobals(); - return; - } - - $tables = $this->listTables(); - - foreach ( $tables as $table ) { - if ( $this->db->getType() == 'oracle' ) { - $this->db->query( "DROP TABLE pt_$table DROP CONSTRAINTS" ); - } else { - $this->db->query( "DROP TABLE `parsertest_$table`" ); - } - } - - if ( $this->db->getType() == 'oracle' ) { - $this->db->query( 'BEGIN FILL_WIKI_INFO; END;' ); - } - - $this->teardownGlobals(); - } - - /** - * 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; - - $dir = $this->uploadDir; - if ( $this->keepUploads && is_dir( $dir ) ) { - return; - } - - // wfDebug( "Creating upload directory $dir\n" ); - if ( file_exists( $dir ) ) { - wfDebug( "Already exists!\n" ); - return; - } - - wfMkdirParents( $dir . '/3/3a', null, __METHOD__ ); - copy( "$IP/tests/phpunit/data/parser/headbg.jpg", "$dir/3/3a/Foobar.jpg" ); - wfMkdirParents( $dir . '/e/ea', null, __METHOD__ ); - copy( "$IP/tests/phpunit/data/parser/wiki.png", "$dir/e/ea/Thumb.png" ); - wfMkdirParents( $dir . '/0/09', null, __METHOD__ ); - copy( "$IP/tests/phpunit/data/parser/headbg.jpg", "$dir/0/09/Bad.jpg" ); - wfMkdirParents( $dir . '/f/ff', null, __METHOD__ ); - file_put_contents( "$dir/f/ff/Foobar.svg", - '<?xml version="1.0" encoding="utf-8"?>' . - '<svg xmlns="http://www.w3.org/2000/svg"' . - ' version="1.1" width="240" height="180"/>' ); - wfMkdirParents( $dir . '/5/5f', null, __METHOD__ ); - copy( "$IP/tests/phpunit/data/parser/LoremIpsum.djvu", "$dir/5/5f/LoremIpsum.djvu" ); - wfMkdirParents( $dir . '/0/00', null, __METHOD__ ); - copy( "$IP/tests/phpunit/data/parser/320x240.ogv", "$dir/0/00/Video.ogv" ); - - return; - } - - /** - * Restore default values and perform any necessary clean-up - * after each test runs. - */ - private function teardownGlobals() { - RepoGroup::destroySingleton(); - FileBackendGroup::destroySingleton(); - LockManagerGroup::destroySingletons(); - LinkCache::singleton()->clear(); - MWTidy::destroySingleton(); - - foreach ( $this->savedGlobals as $var => $val ) { - $GLOBALS[$var] = $val; - } - } - - /** - * Remove the dummy uploads directory - * @param string $dir - */ - private function teardownUploadDir( $dir ) { - if ( $this->keepUploads ) { - return; - } - - // delete the files first, then the dirs. - self::deleteFiles( - [ - "$dir/3/3a/Foobar.jpg", - "$dir/thumb/3/3a/Foobar.jpg/*.jpg", - "$dir/e/ea/Thumb.png", - "$dir/0/09/Bad.jpg", - "$dir/5/5f/LoremIpsum.djvu", - "$dir/thumb/5/5f/LoremIpsum.djvu/*-LoremIpsum.djvu.jpg", - "$dir/f/ff/Foobar.svg", - "$dir/thumb/f/ff/Foobar.svg/*-Foobar.svg.png", - "$dir/math/f/a/5/fa50b8b616463173474302ca3e63586b.png", - "$dir/0/00/Video.ogv", - "$dir/thumb/0/00/Video.ogv/120px--Video.ogv.jpg", - "$dir/thumb/0/00/Video.ogv/180px--Video.ogv.jpg", - "$dir/thumb/0/00/Video.ogv/240px--Video.ogv.jpg", - "$dir/thumb/0/00/Video.ogv/320px--Video.ogv.jpg", - "$dir/thumb/0/00/Video.ogv/270px--Video.ogv.jpg", - "$dir/thumb/0/00/Video.ogv/320px-seek=2-Video.ogv.jpg", - "$dir/thumb/0/00/Video.ogv/320px-seek=3.3666666666667-Video.ogv.jpg", - ] - ); - - self::deleteDirs( - [ - "$dir/3/3a", - "$dir/3", - "$dir/thumb/3/3a/Foobar.jpg", - "$dir/thumb/3/3a", - "$dir/thumb/3", - "$dir/e/ea", - "$dir/e", - "$dir/f/ff/", - "$dir/f/", - "$dir/thumb/f/ff/Foobar.svg", - "$dir/thumb/f/ff/", - "$dir/thumb/f/", - "$dir/0/00/", - "$dir/0/09/", - "$dir/0/", - "$dir/5/5f", - "$dir/5", - "$dir/thumb/0/00/Video.ogv", - "$dir/thumb/0/00", - "$dir/thumb/0", - "$dir/thumb/5/5f/LoremIpsum.djvu", - "$dir/thumb/5/5f", - "$dir/thumb/5", - "$dir/thumb", - "$dir/math/f/a/5", - "$dir/math/f/a", - "$dir/math/f", - "$dir/math", - "$dir/lockdir", - "$dir", - ] - ); - } - - /** - * Delete the specified files, if they exist. - * @param array $files Full paths to files to delete. - */ - private static function deleteFiles( $files ) { - foreach ( $files as $pattern ) { - foreach ( glob( $pattern ) as $file ) { - if ( file_exists( $file ) ) { - unlink( $file ); - } - } - } - } - - /** - * Delete the specified directories, if they exist. Must be empty. - * @param array $dirs Full paths to directories to delete. - */ - private static function deleteDirs( $dirs ) { - foreach ( $dirs as $dir ) { - if ( is_dir( $dir ) ) { - rmdir( $dir ); - } - } - } - - /** - * "Running test $desc..." - * @param string $desc - */ - protected function showTesting( $desc ) { - print "Running test $desc... "; - } - - /** - * Print a happy success message. - * - * Refactored in 1.22 to use ParserTestResult - * - * @param ParserTestResult $testResult - * @return bool - */ - protected function showSuccess( ParserTestResult $testResult ) { - if ( $this->showProgress ) { - print $this->term->color( '1;32' ) . 'PASSED' . $this->term->reset() . "\n"; - } - - return true; - } - - /** - * Print a failure message and provide some explanatory output - * about what went wrong if so configured. - * - * Refactored in 1.22 to use ParserTestResult - * - * @param ParserTestResult $testResult - * @return bool - */ - protected function showFailure( ParserTestResult $testResult ) { - if ( $this->showFailure ) { - if ( !$this->showProgress ) { - # In quiet mode we didn't show the 'Testing' message before the - # test, in case it succeeded. Show it now: - $this->showTesting( $testResult->description ); - } - - print $this->term->color( '31' ) . 'FAILED!' . $this->term->reset() . "\n"; - - if ( $this->showOutput ) { - print "--- Expected ---\n{$testResult->expected}\n"; - print "--- Actual ---\n{$testResult->actual}\n"; - } - - if ( $this->showDiffs ) { - print $this->quickDiff( $testResult->expected, $testResult->actual ); - if ( !$this->wellFormed( $testResult->actual ) ) { - print "XML error: $this->mXmlError\n"; - } - } - } - - return false; - } - - /** - * Print a skipped message. - * - * @return bool - */ - protected function showSkipped() { - if ( $this->showProgress ) { - print $this->term->color( '1;33' ) . 'SKIPPED' . $this->term->reset() . "\n"; - } - - return true; - } - - /** - * Run given strings through a diff and return the (colorized) output. - * Requires writable /tmp directory and a 'diff' command in the PATH. - * - * @param string $input - * @param string $output - * @param string $inFileTail Tailing for the input file name - * @param string $outFileTail Tailing for the output file name - * @return string - */ - protected function quickDiff( $input, $output, - $inFileTail = 'expected', $outFileTail = 'actual' - ) { - # Windows, or at least the fc utility, is retarded - $slash = wfIsWindows() ? '\\' : '/'; - $prefix = wfTempDir() . "{$slash}mwParser-" . mt_rand(); - - $infile = "$prefix-$inFileTail"; - $this->dumpToFile( $input, $infile ); - - $outfile = "$prefix-$outFileTail"; - $this->dumpToFile( $output, $outfile ); - - $shellInfile = wfEscapeShellArg( $infile ); - $shellOutfile = wfEscapeShellArg( $outfile ); - - global $wgDiff3; - // we assume that people with diff3 also have usual diff - $shellCommand = ( wfIsWindows() && !$wgDiff3 ) ? 'fc' : 'diff -au'; - - $diff = wfShellExec( "$shellCommand $shellInfile $shellOutfile" ); - - unlink( $infile ); - unlink( $outfile ); - - return $this->colorDiff( $diff ); - } - - /** - * Write the given string to a file, adding a final newline. - * - * @param string $data - * @param string $filename - */ - private function dumpToFile( $data, $filename ) { - $file = fopen( $filename, "wt" ); - fwrite( $file, $data . "\n" ); - fclose( $file ); - } - - /** - * Colorize unified diff output if set for ANSI color output. - * Subtractions are colored blue, additions red. - * - * @param string $text - * @return string - */ - protected function colorDiff( $text ) { - return preg_replace( - [ '/^(-.*)$/m', '/^(\+.*)$/m' ], - [ $this->term->color( 34 ) . '$1' . $this->term->reset(), - $this->term->color( 31 ) . '$1' . $this->term->reset() ], - $text ); - } - - /** - * Show "Reading tests from ..." - * - * @param string $path - */ - public function showRunFile( $path ) { - print $this->term->color( 1 ) . - "Reading tests from \"$path\"..." . - $this->term->reset() . - "\n"; - } - - /** - * Insert a temporary test article - * @param string $name The title, including any prefix - * @param string $text The article text - * @param int|string $line The input line number, for reporting errors - * @param bool|string $ignoreDuplicate Whether to silently ignore duplicate pages - * @throws Exception - * @throws MWException - */ - public static function addArticle( $name, $text, $line = 'unknown', $ignoreDuplicate = '' ) { - global $wgCapitalLinks; - - $oldCapitalLinks = $wgCapitalLinks; - $wgCapitalLinks = true; // We only need this from SetupGlobals() See r70917#c8637 - - $text = self::chomp( $text ); - $name = self::chomp( $name ); - - $title = Title::newFromText( $name ); - - if ( is_null( $title ) ) { - throw new MWException( "invalid title '$name' at line $line\n" ); - } - - $page = WikiPage::factory( $title ); - $page->loadPageData( 'fromdbmaster' ); - - if ( $page->exists() ) { - if ( $ignoreDuplicate == 'ignoreduplicate' ) { - return; - } else { - throw new MWException( "duplicate article '$name' at line $line\n" ); - } - } - - $page->doEditContent( ContentHandler::makeContent( $text, $title ), '', EDIT_NEW ); - - $wgCapitalLinks = $oldCapitalLinks; - } - - /** - * 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 string $name - * @return bool True if tag hook is present - */ - public function requireHook( $name ) { - global $wgParser; - - $wgParser->firstCallInit(); // make sure hooks are loaded. - - if ( isset( $wgParser->mTagHooks[$name] ) ) { - $this->hooks[$name] = $wgParser->mTagHooks[$name]; - } else { - echo " This test suite requires the '$name' hook extension, skipping.\n"; - return false; - } - - return true; - } - - /** - * 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 string $name - * @return bool True if function hook is present - */ - public function requireFunctionHook( $name ) { - global $wgParser; - - $wgParser->firstCallInit(); // make sure hooks are loaded. - - if ( isset( $wgParser->mFunctionHooks[$name] ) ) { - $this->functionHooks[$name] = $wgParser->mFunctionHooks[$name]; - } else { - echo " This test suite requires the '$name' function hook extension, skipping.\n"; - return false; - } - - return true; - } - - /** - * 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 string $name - * @return bool True if function hook is present - */ - public function requireTransparentHook( $name ) { - global $wgParser; - - $wgParser->firstCallInit(); // make sure hooks are loaded. - - if ( isset( $wgParser->mTransparentTagHooks[$name] ) ) { - $this->transparentHooks[$name] = $wgParser->mTransparentTagHooks[$name]; - } else { - echo " This test suite requires the '$name' transparent hook extension, skipping.\n"; - return false; - } - - return true; - } - - private function wellFormed( $text ) { - $html = - Sanitizer::hackDocType() . - '<html>' . - $text . - '</html>'; - - $parser = xml_parser_create( "UTF-8" ); - - # case folding violates XML standard, turn it off - xml_parser_set_option( $parser, XML_OPTION_CASE_FOLDING, false ); - - if ( !xml_parse( $parser, $html, true ) ) { - $err = xml_error_string( xml_get_error_code( $parser ) ); - $position = xml_get_current_byte_index( $parser ); - $fragment = $this->extractFragment( $html, $position ); - $this->mXmlError = "$err at byte $position:\n$fragment"; - xml_parser_free( $parser ); - - return false; - } - - xml_parser_free( $parser ); - - return true; - } - - private function extractFragment( $text, $position ) { - $start = max( 0, $position - 10 ); - $before = $position - $start; - $fragment = '...' . - $this->term->color( 34 ) . - substr( $text, $start, $before ) . - $this->term->color( 0 ) . - $this->term->color( 31 ) . - $this->term->color( 1 ) . - substr( $text, $position, 1 ) . - $this->term->color( 0 ) . - $this->term->color( 34 ) . - substr( $text, $position + 1, 9 ) . - $this->term->color( 0 ) . - '...'; - $display = str_replace( "\n", ' ', $fragment ); - $caret = ' ' . - str_repeat( ' ', $before ) . - $this->term->color( 31 ) . - '^' . - $this->term->color( 0 ); - - return "$display\n$caret"; - } - - static function getFakeTimestamp( &$parser, &$ts ) { - $ts = 123; // parsed as '1970-01-01T00:02:03Z' - return true; - } -} diff --git a/www/wiki/tests/parser/parserTests.php b/www/wiki/tests/parser/parserTests.php index 2735f93e..6a423d5c 100644 --- a/www/wiki/tests/parser/parserTests.php +++ b/www/wiki/tests/parser/parserTests.php @@ -30,6 +30,8 @@ define( 'MW_PARSER_TEST', true ); require __DIR__ . '/../../maintenance/Maintenance.php'; +use MediaWiki\MediaWikiServices; + class ParserTestsMaintenance extends Maintenance { function __construct() { parent::__construct(); @@ -145,7 +147,8 @@ class ParserTestsMaintenance extends Maintenance { $recorderLB = false; if ( $record || $compare ) { - $recorderLB = wfGetLBFactory()->newMainLB(); + $lbFactory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory(); + $recorderLB = $lbFactory->newMainLB(); // This connection will have the wiki's table prefix, not parsertest_ $recorderDB = $recorderLB->getConnection( DB_MASTER ); diff --git a/www/wiki/tests/parser/parserTests.txt b/www/wiki/tests/parser/parserTests.txt index 50560f84..451c50f5 100644 --- a/www/wiki/tests/parser/parserTests.txt +++ b/www/wiki/tests/parser/parserTests.txt @@ -280,12 +280,6 @@ Template:EmptyTRWithHTMLAttrTest !!endarticle !! article -Template:CircularRef -!! text -<ref>{{CircularRef}}</ref> -!! endarticle - -!! article Template:With: Colon !! text Template with colon @@ -294,6 +288,7 @@ Template with colon ### ### Basic tests ### + !! test Blank input !! wikitext @@ -301,16 +296,6 @@ Blank input !! end !! test -CircularRef -!! wikitext -{{CircularRef}} -<references /> -!! html/parsoid -<p><span about="#mwt1" class="mw-ref" id="cite_ref-1" rel="dc:references" typeof="mw:Transclusion mw:Extension/ref" data-parsoid='{"pi":[[]]}' data-mw='{"parts":[{"template":{"target":{"wt":"CircularRef","href":"./Template:CircularRef"},"params":{},"i":0}}]}'><a href="./Main_Page#cite_note-1" style="counter-reset: mw-Ref 1;"><span class="mw-reflink-text">[1]</span></a></span></p> -<ol class="mw-references references" typeof="mw:Extension/references" about="#mwt6" data-mw='{"name":"references","attrs":{}}'><li about="#cite_note-1" id="cite_note-1"><a href="./Main_Page#cite_ref-1" rel="mw:referencedBy"><span class="mw-linkback-text">↑ </span></a> <span id="mw-reference-text-cite_note-1" class="mw-reference-text" data-parsoid="{}">Error: Expansion loop detected at <a data-parsoid='{"a":{"href":null},"sa":{"href":"Template:CircularRef"}}'>Template:CircularRef</a></span></li></ol> -!! end - -!! test Simple paragraph !! wikitext This is a simple paragraph. @@ -546,16 +531,20 @@ Extra newlines between heading and content are swallowed Heading with line break in nowiki !! options parsoid=wt2html +!! config +wgFragmentMode=[ 'html5', 'legacy' ] !! wikitext -== A <nowiki>B -C</nowiki> == -!! html -<h2><span class="mw-headline" id="A_B.0AC">A B +==A <nowiki>B +C</nowiki>== +!! html/php +<h2><span id="A_B.0AC"></span><span class="mw-headline" id="A_B +C">A B C</span><span class="mw-editsection"><span class="mw-editsection-bracket">[</span><a href="/index.php?title=Parser_test&action=edit&section=1" title="Edit section: A B C">edit</a><span class="mw-editsection-bracket">]</span></span></h2> !! html/parsoid -<h2 id="A_B.0AC">A <span typeof="mw:Nowiki">B -C</span> </h2> +<h2 id="A_B +C"><span id="A_B.0AC" typeof="mw:FallbackId"></span>A <span typeof="mw:Nowiki">B +C</span></h2> !! end !! test @@ -568,58 +557,51 @@ http://fr.wikipedia.org/wiki/🍺 </p> !! end -# Note that the html+tidy output removes the spaces after the <li>, -# which is a bug (https://sourceforge.net/p/tidy/bugs/945/, etc). -# This is an issue for all tests with lists. We intentionally do -# *not* add html+tidy clauses for these, as we don't want to -# document/test the broken behavior. (Parsoid matches the non-tidy -# output in these cases.) - !! test Simple list !! wikitext -* Item 1 -* Item 2 +*Item 1 +*Item 2 !! html -<ul><li> Item 1</li> -<li> Item 2</li></ul> +<ul><li>Item 1</li> +<li>Item 2</li></ul> !! end !! test Italics and bold !! wikitext -* plain -* plain''italic''plain -* plain''italic''plain''italic''plain -* plain'''bold'''plain -* plain'''bold'''plain'''bold'''plain -* plain''italic''plain'''bold'''plain -* plain'''bold'''plain''italic''plain -* plain''italic'''bold-italic'''italic''plain -* plain'''bold''bold-italic''bold'''plain -* plain'''''bold-italic'''italic''plain -* plain'''''bold-italic''bold'''plain -* plain''italic'''bold-italic'''''plain -* plain'''bold''bold-italic'''''plain -* plain l'''italic''plain -* plain l''''bold''' plain -!! html -<ul><li> plain</li> -<li> plain<i>italic</i>plain</li> -<li> plain<i>italic</i>plain<i>italic</i>plain</li> -<li> plain<b>bold</b>plain</li> -<li> plain<b>bold</b>plain<b>bold</b>plain</li> -<li> plain<i>italic</i>plain<b>bold</b>plain</li> -<li> plain<b>bold</b>plain<i>italic</i>plain</li> -<li> plain<i>italic<b>bold-italic</b>italic</i>plain</li> -<li> plain<b>bold<i>bold-italic</i>bold</b>plain</li> -<li> plain<i><b>bold-italic</b>italic</i>plain</li> -<li> plain<b><i>bold-italic</i>bold</b>plain</li> -<li> plain<i>italic<b>bold-italic</b></i>plain</li> -<li> plain<b>bold<i>bold-italic</i></b>plain</li> -<li> plain l'<i>italic</i>plain</li> -<li> plain l'<b>bold</b> plain</li></ul> +*plain +*plain''italic''plain +*plain''italic''plain''italic''plain +*plain'''bold'''plain +*plain'''bold'''plain'''bold'''plain +*plain''italic''plain'''bold'''plain +*plain'''bold'''plain''italic''plain +*plain''italic'''bold-italic'''italic''plain +*plain'''bold''bold-italic''bold'''plain +*plain'''''bold-italic'''italic''plain +*plain'''''bold-italic''bold'''plain +*plain''italic'''bold-italic'''''plain +*plain'''bold''bold-italic'''''plain +*plain l'''italic''plain +*plain l''''bold''' plain +!! html +<ul><li>plain</li> +<li>plain<i>italic</i>plain</li> +<li>plain<i>italic</i>plain<i>italic</i>plain</li> +<li>plain<b>bold</b>plain</li> +<li>plain<b>bold</b>plain<b>bold</b>plain</li> +<li>plain<i>italic</i>plain<b>bold</b>plain</li> +<li>plain<b>bold</b>plain<i>italic</i>plain</li> +<li>plain<i>italic<b>bold-italic</b>italic</i>plain</li> +<li>plain<b>bold<i>bold-italic</i>bold</b>plain</li> +<li>plain<i><b>bold-italic</b>italic</i>plain</li> +<li>plain<b><i>bold-italic</i>bold</b>plain</li> +<li>plain<i>italic<b>bold-italic</b></i>plain</li> +<li>plain<b>bold<i>bold-italic</i></b>plain</li> +<li>plain l'<i>italic</i>plain</li> +<li>plain l'<b>bold</b> plain</li></ul> !! end @@ -1145,8 +1127,7 @@ The ''[[Main Page]]'''s talk page. !! end !! test -Parsoid only: Quote balancing context should be restricted to td/th cells on the same wikitext line -(Requires tidy for PHP parser output to be fixed up) +Quote balancing context should be restricted to td/th cells on the same wikitext line !! options parsoid=wt2html,wt2wt !! wikitext @@ -1154,20 +1135,15 @@ parsoid=wt2html,wt2wt !''a!!''b |''a||''b |} -!! html/php+tidy +!! html+tidy <table> -<tr> +<tbody><tr> <th><i>a</i></th> -<th><i>b</i></th> +<th><i>b</i> +</th> <td><i>a</i></td> -<td><i>b</i></td> -</tr> -</table> -!! html/parsoid -<table> -<tbody><tr><th><i>a</i></th><th><i>b</i></th> -<td><i>a</i></td><td><i>b</i></td></tr> -</tbody></table> +<td><i>b</i> +</td></tr></tbody></table> !! end ### @@ -1271,32 +1247,33 @@ Text-level semantic html elements in wikitext !! test Ruby markup (W3C-style) !! wikitext -; Mono-ruby for individual base characters -: <ruby>日<rt>に</rt>本<rt>ほん</rt>語<rt>ご</rt></ruby> -; Group ruby -: <ruby>今日<rt>きょう</rt></ruby> -; Jukugo ruby -: <ruby>法<rb>華</rb><rb>経</rb><rt>ほ</rt><rt>け</rt><rt>きょう</rt></ruby> -; Inline ruby -: <ruby>東<rb>京</rb><rp>(</rp><rt>とう</rt><rt>きょう</rt><rp>)</rp></ruby> -; Double-sided ruby -: <ruby><rb>旧</rb><rb>金</rb><rb>山</rb><rt>jiù</rt><rt>jīn</rt><rt>shān</rt><rtc>San Francisco</rtc></ruby> +;Mono-ruby for individual base characters +:<ruby>日<rt>に</rt>本<rt>ほん</rt>語<rt>ご</rt></ruby> +;Group ruby +:<ruby>今日<rt>きょう</rt></ruby> +;Jukugo ruby +:<ruby>法<rb>華</rb><rb>経</rb><rt>ほ</rt><rt>け</rt><rt>きょう</rt></ruby> +;Inline ruby +:<ruby>東<rb>京</rb><rp>(</rp><rt>とう</rt><rt>きょう</rt><rp>)</rp></ruby> +;Double-sided ruby +:<ruby><rb>旧</rb><rb>金</rb><rb>山</rb><rt>jiù</rt><rt>jīn</rt><rt>shān</rt><rtc>San Francisco</rtc></ruby> + <ruby> <rb>♥</rb><rtc><rt>Heart</rt></rtc><rtc lang="fr"><rt>Cœur</rt></rtc> <rb>☘</rb><rtc><rt>Shamrock</rt></rtc><rtc lang="fr"><rt>Trèfle</rt></rtc> <rb>✶</rb><rtc><rt>Star</rt></rtc><rtc lang="fr"><rt>Étoile</rt></rtc> </ruby> !! html -<dl><dt> Mono-ruby for individual base characters</dt> -<dd> <ruby>日<rt>に</rt>本<rt>ほん</rt>語<rt>ご</rt></ruby></dd> -<dt> Group ruby</dt> -<dd> <ruby>今日<rt>きょう</rt></ruby></dd> -<dt> Jukugo ruby</dt> -<dd> <ruby>法<rb>華</rb><rb>経</rb><rt>ほ</rt><rt>け</rt><rt>きょう</rt></ruby></dd> -<dt> Inline ruby</dt> -<dd> <ruby>東<rb>京</rb><rp>(</rp><rt>とう</rt><rt>きょう</rt><rp>)</rp></ruby></dd> -<dt> Double-sided ruby</dt> -<dd> <ruby><rb>旧</rb><rb>金</rb><rb>山</rb><rt>jiù</rt><rt>jīn</rt><rt>shān</rt><rtc>San Francisco</rtc></ruby></dd></dl> +<dl><dt>Mono-ruby for individual base characters</dt> +<dd><ruby>日<rt>に</rt>本<rt>ほん</rt>語<rt>ご</rt></ruby></dd> +<dt>Group ruby</dt> +<dd><ruby>今日<rt>きょう</rt></ruby></dd> +<dt>Jukugo ruby</dt> +<dd><ruby>法<rb>華</rb><rb>経</rb><rt>ほ</rt><rt>け</rt><rt>きょう</rt></ruby></dd> +<dt>Inline ruby</dt> +<dd><ruby>東<rb>京</rb><rp>(</rp><rt>とう</rt><rt>きょう</rt><rp>)</rp></ruby></dd> +<dt>Double-sided ruby</dt> +<dd><ruby><rb>旧</rb><rb>金</rb><rb>山</rb><rt>jiù</rt><rt>jīn</rt><rt>shān</rt><rtc>San Francisco</rtc></ruby></dd></dl> <p><ruby> <rb>♥</rb><rtc><rt>Heart</rt></rtc><rtc lang="fr"><rt>Cœur</rt></rtc> <rb>☘</rb><rtc><rt>Shamrock</rt></rtc><rtc lang="fr"><rt>Trèfle</rt></rtc> @@ -1330,11 +1307,8 @@ Non-word characters don't terminate tag names (T19663, T42670, T54022) </p> !! end -# There is a tidy bug here: https://sourceforge.net/p/tidy/bugs/946/ -# If the non-word-character tag made it through the sanitizer, tidy -# would munge it up. !! test -Non-word characters don't terminate tag names + tidy +Non-word characters don't terminate tag names !! wikitext <blockquote|>a</blockquote> @@ -1348,12 +1322,13 @@ Non-word characters don't terminate tag names + tidy <sub-ID#1> !! html+tidy -<p><blockquote|>a</p> -<p><b→> doesn't terminate </b→></p> -<p><bä> doesn't terminate </bä></p> -<p><boo> doesn't terminate </boo></p> -<p><s.foo> doesn't terminate </s.foo></p> -<p><sub-ID#1></p> +<p><blockquote|>a +</p><p><b→> doesn't terminate </b→> +</p><p><bä> doesn't terminate </bä> +</p><p><boo> doesn't terminate </boo> +</p><p><s.foo> doesn't terminate </s.foo> +</p><p><sub-ID#1> +</p> !! end ### @@ -1386,7 +1361,9 @@ parsoid=wt2html <s.foo>s</s> !! html/php+tidy -<p><s.foo>s</p> +<p class="mw-empty-elt"> +</p><p><s.foo>s +</p> !! html/parsoid <p><s.foo>s</p> !! end @@ -1514,7 +1491,8 @@ Entities inside template parameters !! wikitext {{echo|–}} !! html/php+tidy -<p>–</p> +<p>– +</p> !! html/parsoid <p><span typeof="mw:Transclusion mw:Entity" data-mw='{"parts":[{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"&ndash;"}},"i":0}}]}'>–</span></p> !! end @@ -1884,6 +1862,7 @@ IE conditional comments ### ### paragraph wrapping tests ### + !! test No block tags !! wikitext @@ -1907,9 +1886,38 @@ a <div>foo</div> <p>b </p> !! html+tidy -<p>a</p> -<div>foo</div> -<p>b</p> +<p>a </p><div>foo</div> +<p>b +</p> +!! end + +# Remex wraps empty tag runs with p-tags. +# Parsoid strips them out during p-wrapping. +!! test +No p-wrappable content +!! wikitext +<span><div>x</div></span> +<span><s><div>x</div></s></span> +<small><em></em></small><span><s><div>x</div></s></span> +!! html/php+tidy +<span><div>x</div></span> +<span><s><div>x</div></s></span> +<p><small><em></em></small></p><span><s><div>x</div></s></span> +!! html/parsoid +<span><div>x</div></span> +<span><s><div>x</div></s></span> +<small><em></em></small><span><s><div>x</div></s></span> +!! end + +# T177612: Parsoid-only test +!! test +Transclusion meta tags shouldn't trip Parsoid's useless p-wrapper stripping code +!! wikitext +{{echo|<span><div>x</div></span>}} +x +!! html/parsoid +<span about="#mwt1" typeof="mw:Transclusion" data-parsoid='{"stx":"html","pi":[[{"k":"1"}]]}' data-mw='{"parts":[{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"<span><div>x</div></span>"}},"i":0}}]}'><div>x</div></span> +<p>x</p> !! end !! test @@ -1923,11 +1931,9 @@ a <blockquote>foo</blockquote> <p>b </p> !! html+tidy -<p>a</p> -<blockquote> -<p>foo</p> -</blockquote> -<p>b</p> +<p>a </p><blockquote><p>foo</p></blockquote> +<p>b +</p> !! end !! test @@ -1941,10 +1947,8 @@ a <div>foo</div> b <div>foo</div> !! html+tidy -<p>a</p> -<div>foo</div> -<p>b</p> -<div>foo</div> +<p>a </p><div>foo</div><p> +b </p><div>foo</div> !! end !! test @@ -1958,14 +1962,8 @@ a <blockquote>foo</blockquote> b <blockquote>foo</blockquote> !! html+tidy -<p>a</p> -<blockquote> -<p>foo</p> -</blockquote> -<p>b</p> -<blockquote> -<p>foo</p> -</blockquote> +<p>a </p><blockquote><p>foo</p></blockquote><p> +b </p><blockquote><p>foo</p></blockquote> !! end !! test @@ -1985,19 +1983,21 @@ d e x <div>foo</div> z !! html+tidy -<div>foo</div> -<p>a</p> -<p>b c d e</p> -<p>x</p> -<div>foo</div> -<p>z</p> +<div>foo</div><p> a +</p><p>b +c +d e +</p><p> +x </p><div>foo</div><p> z +</p> !! end -# Tidy strips out the empty <div> tags. Parsoid doesn't. -# So, we have a separate section for Parsoid. We don't want -# to mimic this stripping behavior in Parsoid. It affects -# editing experience and also requires us to maintain additional -# info for RT-ing. +# The difference between Parsoid & Remex here +# is because of Parsoid's Tidy-emulation code +# for p-wrapping. We'll start work to remove this +# emulation code in Parsoid sooner than later. +# Remex wraps empty tag runs with p-tags. +# Parsoid strips them out in a separate pass. !! test Empty lines between lines with block tags !! wikitext @@ -2027,14 +2027,16 @@ b <div>e</div> !! html+tidy -<p><br /></p> -<p>a</p> -<p>b</p> -<div>a</div> -<p>b</p> -<div>b</div> -<p>d</p> -<p><br /></p> +<div></div> +<p><br /> +</p> +<div></div><p>a +</p><p>b +</p> +<div>a</div><p>b +</p><div>b</div><p>d +</p><p><br /> +</p> <div>e</div> !! html/parsoid <div data-parsoid='{"stx":"html"}'></div> @@ -2051,7 +2053,6 @@ b <div data-parsoid='{"stx":"html"}'>e</div> !! end -## PHP parser emits output which is broken !! test Unclosed HTML p-tags should be handled properly !! wikitext @@ -2060,11 +2061,10 @@ a b !! html/php+tidy -<div> -<p>foo</p> -</div> -<p>a</p> -<p>b</p> +<div><p>foo</p></div> +<p>a +</p><p>b +</p> !! html/parsoid <div data-parsoid='{"stx":"html"}'><p data-parsoid='{"stx":"html", "autoInsertedEnd":true}'>foo</p></div> <p>a</p> @@ -2097,9 +2097,55 @@ parsoid=wt2html <link rel="mw:PageProp/Category" href="./Category:A1"/><p>a</p> !! end +!! test +No paragraph necessary for SOL transparent template +!! wikitext +<span><div>foo</div></span> +[[Category:Foo]] + +<span><div>foo</div></span> +{{echo|[[Category:Foo]]}} +!! html/php +<span><div>foo</div></span> +<span><div>foo</div></span> + +!! html/parsoid +<span data-parsoid='{"stx":"html"}'><div data-parsoid='{"stx":"html"}'>foo</div></span> +<link rel="mw:PageProp/Category" href="./Category:Foo"/> + +<span data-parsoid='{"stx":"html"}'><div data-parsoid='{"stx":"html"}'>foo</div></span> +<link rel="mw:PageProp/Category" href="./Category:Foo" about="#mwt1" typeof="mw:Transclusion" data-mw='{"parts":[{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"[[Category:Foo]]"}},"i":0}}]}'/> +!! end + +!! test +Avoid expanding multiline sol transparent template ranges unnecessarily +!! wikitext +hi + + +{{echo|<br/> +}} + +[[Category:Ho]] +!! html/php +<p>hi +</p><p><br /> +<br /> +</p> +!! html/parsoid +<p>hi</p> + +<p><br /> +<br about="#mwt1" typeof="mw:Transclusion" data-parsoid='{}' data-mw='{"parts":[{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"<br/>\n"}},"i":0}}]}'/><span about="#mwt1"> +</span></p> + +<link rel="mw:PageProp/Category" href="./Category:Ho" /> +!! end + ### ### Preformatted text ### + !! test Preformatted text !! wikitext @@ -2243,11 +2289,13 @@ Foo <del>bar</del> <ins>baz</ins> quux </p> </blockquote> +!! html+tidy +<blockquote> +<p>Foo <del>bar</del> <ins>baz</ins> quux +</p> +</blockquote> !! end -# Note that the p-wrapping is newline sensitive, which could be -# considered a bug: tidy will wrap only the 'Foo' in the example -# below in a <p> tag. (see comment 23-25 of T8200) !! test T17491: <ins>/<del> in blockquote (2) !! wikitext @@ -2258,9 +2306,8 @@ T17491: <ins>/<del> in blockquote (2) </blockquote> !! html+tidy -<blockquote> -<p>Foo</p> -<del>bar</del> <ins>baz</ins> quux</blockquote> +<blockquote><p>Foo <del>bar</del> <ins>baz</ins> quux +</p></blockquote> !! end !! test @@ -2395,7 +2442,6 @@ parsoid=wt2html </p> !! end -# Parsoid doesn't strip empty tags, like Tidy does. !! test Empty pre; pre inside other HTML tags (T56946) !! wikitext @@ -2405,20 +2451,12 @@ a foo </pre></div> <pre></pre> -!! html/php +!! html/php+tidy <p>a </p> -<div><pre> -foo +<div><pre>foo </pre></div> <pre></pre> - -!! html/php+tidy -<p>a</p> -<div> -<pre> -foo -</pre></div> !! html/parsoid <p>a</p> @@ -2438,16 +2476,12 @@ HTML pre followed by indent-pre </pre> !! end -# Note that tidy removes the empty <p> tags from the start and end. -# Parsoid does not, by design. !! test Block tag pre !! wikitext <p><pre>foo</pre></p> !! html/php+tidy -<pre> -foo -</pre> +<p class="mw-empty-elt"></p><pre>foo</pre><p class="mw-empty-elt"></p> !! html/parsoid <p class='mw-empty-elt' data-parsoid='{"stx":"html","autoInsertedEnd":true}'></p><pre typeof="mw:Extension/pre" about="#mwt2" data-parsoid='{"stx":"html"}' data-mw='{"name":"pre","attrs":{},"body":{"extsrc":"foo"}}'>foo</pre><p class='mw-empty-elt' data-parsoid='{"autoInsertedStart":true,"stx":"html"}'></p> !! end @@ -2610,10 +2644,8 @@ parsoid=wt2html <table><pre </table> !! html/php+tidy -<pre> -x -</pre> -<p><pre</p> +<pre>x</pre> +<pre <table></table> !! html/parsoid <pre about="#mwt1" typeof="mw:Transclusion mw:Extension/pre" data-parsoid='{"a":{"<pre":null},"sa":{"<pre":""},"stx":"html","pi":[[{"k":"1"}]]}' data-mw='{"parts":[{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"<pre <pre>x</pre>"}},"i":0}}]}'>x</pre> @@ -2664,6 +2696,17 @@ parsoid=wt2html !! end !! test +Self-closed pre +!! wikitext +<pre /> +!! html/php +<pre></pre> + +!! html/parsoid +<pre typeof="mw:Extension/pre" about="#mwt2" data-mw='{"name":"pre","attrs":{},"body":null}'></pre> +!! end + +!! test Parsoid: Don't paragraph-wrap fosterable content even if table syntax is unbalanced !! options parsoid=wt2html @@ -2714,7 +2757,7 @@ Templates: Strip leading and trailing whitespace from named-param values </p><p>b </p><p>c </p> -<ul><li> d</li></ul> +<ul><li>d</li></ul> !! end @@ -2735,7 +2778,7 @@ Templates: Don't strip whitespace from positional-param values e}} {{echo| -* f}} +*f}} {{echo| }}g @@ -2755,7 +2798,7 @@ Templates: Don't strip whitespace from positional-param values </pre> <p><br /> </p> -<ul><li> f</li></ul> +<ul><li>f</li></ul> <p><br /> </p> <pre>g @@ -2907,7 +2950,8 @@ Templates: Parsoid parameter escaping test 1 !! wikitext {{echo|[foo]|{{echo|[bar]}}}} !! html/php+tidy -<p>[foo]</p> +<p>[foo] +</p> !! html/parsoid <p about="#mwt1" typeof="mw:Transclusion" data-mw='{"parts":[{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"[foo]"},"2":{"wt":"{{echo|[bar]}}"}},"i":0}}]}'>[foo]</p> @@ -2918,9 +2962,10 @@ Parsoid: Pipes in external links in template parameter !! wikitext {{echo|[{{echo|http://example.com}} link]}} !! html/php+tidy -<p><a rel="nofollow" class="external text" href="http://example.com">link</a></p> +<p><a rel="nofollow" class="external text" href="http://example.com">link</a> +</p> !! html/parsoid -<p><a rel="mw:ExtLink" href="http://example.com" about="#mwt31" typeof="mw:Transclusion" data-mw='{"parts":[{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"[{{echo|http://example.com}} link]"}},"i":0}}]}'>link</a></p> +<p><a rel="mw:ExtLink" class="external text" href="http://example.com" about="#mwt31" typeof="mw:Transclusion" data-mw='{"parts":[{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"[{{echo|http://example.com}} link]"}},"i":0}}]}'>link</a></p> !! end !! test @@ -2928,11 +2973,10 @@ Parsoid: pipe in transclusion parameter !! wikitext {{echo|http://foo.com/a|b}} !! html/php+tidy -<p><a rel="nofollow" class="external free" href="http://foo.com/a%7Cb">http://foo.com/a%7Cb</a></p> +<p><a rel="nofollow" class="external free" href="http://foo.com/a%7Cb">http://foo.com/a%7Cb</a> +</p> !! html/parsoid -<p><a rel="mw:ExtLink" href="http://foo.com/a%7Cb" about="#mwt1" -typeof="mw:Transclusion" -data-mw='{"parts":[{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"http://foo.com/a&#124;b"}},"i":0}}]}'>http://foo.com/a%7Cb</a></p> +<p><a rel="mw:ExtLink" class="external free" href="http://foo.com/a%7Cb" about="#mwt1" typeof="mw:Transclusion" data-mw='{"parts":[{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"http://foo.com/a&#124;b"}},"i":0}}]}'>http://foo.com/a%7Cb</a></p> !! end !! test @@ -2942,7 +2986,8 @@ parsoid=html2wt,wt2wt !! wikitext {{echo|[http://foo.com/a|b a|b]}} !! html/php+tidy -<p><a rel="nofollow" class="external text" href="http://foo.com/a%7Cb">a|b</a></p> +<p><a rel="nofollow" class="external text" href="http://foo.com/a%7Cb">a|b</a> +</p> !! html/parsoid <p><a rel="mw:ExtLink" href="http://foo.com/a|b" about="#mwt1" typeof="mw:Transclusion" @@ -2969,7 +3014,10 @@ parsoid=html2wt,wt2wt {{echo|<nowiki><div></nowiki>}} {{echo|<nowiki></nowiki>}} !! html/php+tidy -<p>foo|bar <div></p> +<p>foo|bar +<div> + +</p> !! html/parsoid <p><span about="#mwt1" typeof="mw:Transclusion" data-mw='{"parts":[{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"foo<nowiki>|</nowiki>bar"}},"i":0}}]}'}'>foo</span><span typeof="mw:Nowiki" about="#mwt1">|</span><span about="#mwt1">bar</span> <span typeof="mw:Transclusion mw:Nowiki" about="#mwt2" data-mw='{"parts":[{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"<nowiki>&lt;div&gt;</nowiki>"}},"i":0}}]}'><span typeof="mw:Entity"><</span>div<span typeof="mw:Entity">></span></span> @@ -2985,7 +3033,8 @@ parsoid=html2wt,wt2wt !! wikitext {{echo|{{echo|1=bar}}}} !! html/php+tidy -<p>bar</p> +<p>bar +</p> !! html/parsoid <p about="#mwt1" typeof="mw:Transclusion" data-mw='{"parts":[{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"{{echo|1=bar}}"}},"i":0}}]}'>bar</p> !! end @@ -2996,7 +3045,8 @@ Templates parameters with special tokenizing behavior dont get modified because !! wikitext {{echo|a : b}} !! html/php+tidy -<p>a : b</p> +<p>a : b +</p> !! html/parsoid <p about="#mwt1" typeof="mw:Transclusion" data-mw='{"parts":[{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"a : b"}},"i":0}}]}'>a<span typeof="mw:DisplaySpace mw:Placeholder" data-parsoid='{"isDisplayHack":true}'> </span>: b</p> !! end @@ -3007,7 +3057,8 @@ Templates: Preserve blank parameter names !! wikitext {{echo|=foo}} !! html/php+tidy -<p>{{{1}}}</p> +<p>{{{1}}} +</p> !! html/parsoid <p about="#mwt1" typeof="mw:Transclusion" data-mw='{"parts":[{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"":{"wt":"foo"}},"i":0}}]}'>{{{1}}}</p> !! end @@ -3017,7 +3068,9 @@ Templates: Preserve blank parameter names in other positions !! wikitext {{blank_param|bar|=foo}} !! html/php+tidy -<p>bar foo</p> +<p>bar +foo +</p> !! html/parsoid <p about="#mwt1" typeof="mw:Transclusion" data-parsoid='{"pi":[[{"k":"1"},{"k":"","named":true}]]}' data-mw='{"parts":[{"template":{"target":{"wt":"blank_param","href":"./Template:Blank_param"},"params":{"1":{"wt":"bar"},"":{"wt":"foo"}},"i":0}}]}'>bar foo</p> @@ -3121,15 +3174,15 @@ c 2c. Indent-Pre and tables (T44252) !! wikitext {| - |+ foo - ! | bar + |+foo + ! |bar |} !! html <table> -<caption> foo +<caption>foo </caption> <tr> -<th> bar +<th>bar </th></tr></table> !!end @@ -3139,14 +3192,14 @@ c !! wikitext a {| - | b + |b |} !! html/php <pre>a </pre> <table> <tr> -<td> b +<td>b </td></tr></table> !! html/parsoid @@ -3229,17 +3282,11 @@ a <p>c </p><blockquote data-parsoid='{"stx":"html"}'> foo </blockquote> <pre><span> foo </span> </pre> -!! html+tidy -<p>a</p> -<p>foo</p> -<p>b</p> -<div>foo</div> -<p>c</p> -<blockquote> -<p>foo</p> -</blockquote> -<pre> -<span> foo </span> +!! html/php+tidy +<p> a </p><p> foo </p><p> + b </p><div> foo </div><p> + c </p><blockquote><p> foo </p></blockquote> +<pre><span> foo </span> </pre> !! end @@ -3256,12 +3303,10 @@ a !! html/parsoid <pre>a <span data-parsoid='{"stx":"html"}'>foo</span></pre> b <div data-parsoid='{"stx":"html"}'> foo </div> -!! html+tidy -<pre> -a <span>foo</span> -</pre> -<p>b</p> -<div>foo</div> +!! html/php+tidy +<pre>a <span>foo</span> +</pre><p> + b </p><div> foo </div> !!end !!test @@ -3496,7 +3541,8 @@ a <!-- foo --> b !! html/php+tidy -<p>a b</p> +<p>a b +</p> !! html/parsoid <p>a <!-- foo @@ -3527,11 +3573,8 @@ foo foo </pre> !! html/php+tidy -<pre> -foo -</pre> -<pre> -foo +<pre>foo</pre> +<pre>foo </pre> <pre> @@ -3662,19 +3705,19 @@ HTML-pre: 3: other wikitext !! test Simple definition !! wikitext -; name : Definition +;name :Definition !! html -<dl><dt> name </dt> -<dd> Definition</dd></dl> +<dl><dt>name </dt> +<dd>Definition</dd></dl> !! end !! test Definition list for indentation only !! wikitext -: Indented text +:Indented text !! html -<dl><dd> Indented text</dd></dl> +<dl><dd>Indented text</dd></dl> !! end @@ -3691,10 +3734,10 @@ Definition list with no space !! test Definition list with URL link !! wikitext -; http://example.com/ : definition +;http://example.com/ :definition !! html -<dl><dt> <a rel="nofollow" class="external free" href="http://example.com/">http://example.com/</a> </dt> -<dd> definition</dd></dl> +<dl><dt><a rel="nofollow" class="external free" href="http://example.com/">http://example.com/</a> </dt> +<dd>definition</dd></dl> !! end @@ -3711,10 +3754,10 @@ Definition list with bracketed URL link !! test Definition list with wikilink containing colon !! wikitext -; [[Help:FAQ]]: The least-read page on Wikipedia +; [[Help:FAQ]]:The least-read page on Wikipedia !! html -<dl><dt> <a href="/index.php?title=Help:FAQ&action=edit&redlink=1" class="new" title="Help:FAQ (page does not exist)">Help:FAQ</a></dt> -<dd> The least-read page on Wikipedia</dd></dl> +<dl><dt><a href="/index.php?title=Help:FAQ&action=edit&redlink=1" class="new" title="Help:FAQ (page does not exist)">Help:FAQ</a></dt> +<dd>The least-read page on Wikipedia</dd></dl> !! end @@ -3722,13 +3765,13 @@ Definition list with wikilink containing colon !! test Definition list with news link containing colon !! wikitext -; news:alt.wikipedia.rox: This isn't even a real newsgroup! +;news:alt.wikipedia.rox: This isn't even a real newsgroup! !! html/php -<dl><dt> <a rel="nofollow" class="external free" href="news:alt.wikipedia.rox">news:alt.wikipedia.rox</a></dt> -<dd> This isn't even a real newsgroup!</dd></dl> +<dl><dt><a rel="nofollow" class="external free" href="news:alt.wikipedia.rox">news:alt.wikipedia.rox</a></dt> +<dd>This isn't even a real newsgroup!</dd></dl> !! html/parsoid -<dl><dt> <a rel="mw:ExtLink" href="news:alt.wikipedia.rox" data-parsoid='{"stx":"url"}'>news:alt.wikipedia.rox</a></dt><dd data-parsoid='{"stx":"row"}'> This isn't even a real newsgroup!</dd></dl> +<dl><dt> <a rel="mw:ExtLink" class="external free" href="news:alt.wikipedia.rox" data-parsoid='{"stx":"url"}'>news:alt.wikipedia.rox</a></dt><dd data-parsoid='{"stx":"row"}'>This isn't even a real newsgroup!</dd></dl> !! end !! test @@ -3736,17 +3779,17 @@ Malformed definition list with colon !! wikitext ; news:alt.wikipedia.rox -- don't crash or enter an infinite loop !! html -<dl><dt> <a rel="nofollow" class="external free" href="news:alt.wikipedia.rox">news:alt.wikipedia.rox</a> -- don't crash or enter an infinite loop</dt></dl> +<dl><dt><a rel="nofollow" class="external free" href="news:alt.wikipedia.rox">news:alt.wikipedia.rox</a> -- don't crash or enter an infinite loop</dt></dl> !! end !! test Definition lists: colon in external link text !! wikitext -; [http://www.wikipedia2.org/ Wikipedia : The Next Generation]: OK, I made that up +;[http://www.wikipedia2.org/ Wikipedia :The Next Generation] :OK, I made that up !! html -<dl><dt> <a rel="nofollow" class="external text" href="http://www.wikipedia2.org/">Wikipedia : The Next Generation</a></dt> -<dd> OK, I made that up</dd></dl> +<dl><dt><a rel="nofollow" class="external text" href="http://www.wikipedia2.org/">Wikipedia :The Next Generation</a> </dt> +<dd>OK, I made that up</dd></dl> !! end @@ -3762,32 +3805,30 @@ Definition lists: colon in HTML attribute !! test Definition lists: self-closed tag !! wikitext -;one<br/>two : two-line fun +;one<br/>two :two-line fun !! html <dl><dt>one<br />two </dt> -<dd> two-line fun</dd></dl> +<dd>two-line fun</dd></dl> !! end !! test Definition lists: ignore colons inside tags !! wikitext -;one <b>two : tag <i>fun:</i>:</b>: def +;one <b>two : tag <i>fun:</i>:</b>:def !! html <dl><dt>one <b>two : tag <i>fun:</i>:</b></dt> -<dd> def</dd></dl> +<dd>def</dd></dl> !! end !! test Definition lists: excess closed tags !! wikitext -;one</b>two : bad tag fun +;one</b>two :bad tag fun !! html/php+tidy -<dl> -<dt>onetwo </dt> -<dd>bad tag fun</dd> -</dl> +<dl><dt>onetwo </dt> +<dd>bad tag fun</dd></dl> !! html/parsoid <dl> <dt>onetwo</dt> @@ -3818,14 +3859,14 @@ T13748: Literal closing tags Definition and unordered list using wiki syntax nested in unordered list using html tags. !! wikitext <ul><li> -; term : description -* unordered +;term :description +*unordered </li></ul> !! html <ul><li> -<dl><dt> term </dt> -<dd> description</dd></dl> -<ul><li> unordered</li></ul> +<dl><dt>term </dt> +<dd>description</dd></dl> +<ul><li>unordered</li></ul> </li></ul> !! end @@ -3833,10 +3874,11 @@ Definition and unordered list using wiki syntax nested in unordered list using h !! test Definition list with empty definition and following paragraph !! wikitext -; term: +;term: + Paragraph text !! html -<dl><dt> term</dt> +<dl><dt>term</dt> <dd></dd></dl> <p>Paragraph text </p> @@ -3923,6 +3965,29 @@ should be left alone !! end !! test +Definition Lists: Hacky use to indent tables (with content following table) +!! wikitext +:{| +|foo +|bar +|} <!--c1--> this text should be part of the dl +!! html/php+tidy +<dl><dd><table> +<tbody><tr> +<td>foo +</td> +<td>bar +</td></tr></tbody></table> this text should be part of the dl</dd></dl> +!! html/parsoid +<dl><dd><table> +<tbody><tr> +<td>foo +</td> +<td>bar +</td></tr></tbody></table> <!--c1--> this text should be part of the dl</dd></dl> +!! end + +!! test Definition Lists: Hacky use to indent tables, with comments (T65979) !! wikitext <!-- foo --> @@ -4014,22 +4079,24 @@ Table / list interaction: indented table with lists in table contents !! wikitext :{| |- -| a -* b +|a + +*b |- -| c -* d +|c + +*d |} !! html <dl><dd><table> <tr> -<td> a -<ul><li> b</li></ul> +<td>a +<ul><li>b</li></ul> </td></tr> <tr> -<td> c -<ul><li> d</li></ul> +<td>c +<ul><li>d</li></ul> </td></tr></table></dd></dl> !! end @@ -4066,13 +4133,11 @@ Table / list interaction: lists nested in tables nested in indented lists !! test Definition Lists: Nesting: Multi-level (Parsoid only) -!! options -parsoid !! wikitext ;t1 :d1 ;;t2 ::d2 ;;;t3 :::d3 -!! html +!! html/parsoid <dl> <dt>t1 </dt> <dd>d1</dd> @@ -4095,72 +4160,26 @@ parsoid !! test -Definition Lists: Nesting: Test 2 (Parsoid only) +Definition Lists: Nesting: Test 2 !! wikitext ;t1 ::d2 -!! html/php+tidy -<dl> -<dt>t1</dt> +!! html+tidy +<dl><dt>t1</dt> <dd> -<dl> -<dd>d2</dd> -</dl> -</dd> -</dl> -!! html/parsoid -<dl> - <dt>t1</dt> - <dd> - <dl> - <dd>d2</dd> - </dl> - </dd> -</dl> - +<dl><dd>d2</dd></dl></dd></dl> !! end !! test -Definition Lists: Nesting: Test 3 (Parsoid only) +Definition Lists: Nesting: Test 3 !! wikitext :;t1 ::::d2 -!! html/php+tidy -<dl> -<dd> -<dl> -<dt>t1</dt> -<dd> -<dl> +!! html+tidy +<dl><dd><dl><dt>t1</dt> <dd> -<dl> -<dd>d2</dd> -</dl> -</dd> -</dl> -</dd> -</dl> -</dd> -</dl> -!! html/parsoid -<dl> - <dd> - <dl> - <dt>t1</dt> - <dd> - <dl> - <dd> - <dl> - <dd>d2</dd> - </dl> - </dd> - </dl> - </dd> - </dl> - </dd> -</dl> - +<dl><dd><dl><dd>d2</dd></dl></dd></dl></dd></dl></dd></dl> !! end @@ -4185,42 +4204,30 @@ Definition Lists: Nesting: Test 4 !! test Definition Lists: Mixed Lists: Test 1 !! wikitext -:;* foo -::* bar -:; baz +:;*foo +::*bar +:;baz !! html/php -<dl><dd><dl><dt><ul><li> foo</li> -<li> bar</li></ul></dt></dl> -<dl><dt> baz</dt></dl></dd></dl> +<dl><dd><dl><dt><ul><li>foo</li> +<li>bar</li></ul></dt></dl> +<dl><dt>baz</dt></dl></dd></dl> !! html/php+tidy -<dl> -<dd> -<dl> -<dd> -<ul> -<li>foo</li> -<li>bar</li> -</ul> -</dd> -</dl> -<dl> -<dt>baz</dt> -</dl> -</dd> -</dl> +<dl><dd><dl><dt><ul><li>foo</li> +<li>bar</li></ul></dt></dl> +<dl><dt>baz</dt></dl></dd></dl> !! html/parsoid <dl> <dd><dl> <dt><ul> -<li> foo +<li>foo </li> </ul></dt> <dd><ul> -<li> bar +<li>bar </li> </ul></dd> -<dt> baz</dt> +<dt>baz</dt> </dl></dd> </dl> !! end @@ -4228,11 +4235,11 @@ Definition Lists: Mixed Lists: Test 1 !! test Definition Lists: Mixed Lists: Test 2 !! wikitext -*: d1 -*: d2 +*:d1 +*:d2 !! html -<ul><li><dl><dd> d1</dd> -<dd> d2</dd></dl></li></ul> +<ul><li><dl><dd>d1</dd> +<dd>d2</dd></dl></li></ul> !! end @@ -4240,11 +4247,11 @@ Definition Lists: Mixed Lists: Test 2 !! test Definition Lists: Mixed Lists: Test 3 !! wikitext -*::: d1 -*::: d2 +*:::d1 +*:::d2 !! html -<ul><li><dl><dd><dl><dd><dl><dd> d1</dd> -<dd> d2</dd></dl></dd></dl></dd></dl></li></ul> +<ul><li><dl><dd><dl><dd><dl><dd>d1</dd> +<dd>d2</dd></dl></dd></dl></dd></dl></li></ul> !! end @@ -4267,10 +4274,10 @@ Definition Lists: Mixed Lists: Test 4 Definition Lists: Mixed Lists: Test 5 !! wikitext *:d1 -*:: d2 +*::d2 !! html <ul><li><dl><dd>d1 -<dl><dd> d2</dd></dl></dd></dl></li></ul> +<dl><dd>d2</dd></dl></dd></dl></li></ul> !! end @@ -4279,10 +4286,10 @@ Definition Lists: Mixed Lists: Test 5 Definition Lists: Mixed Lists: Test 6 !! wikitext #*:d1 -#*::: d3 +#*:::d3 !! html <ol><li><ul><li><dl><dd>d1 -<dl><dd><dl><dd> d3</dd></dl></dd></dl></dd></dl></li></ul></li></ol> +<dl><dd><dl><dd>d3</dd></dl></dd></dl></dd></dl></li></ul></li></ol> !! end @@ -4290,11 +4297,11 @@ Definition Lists: Mixed Lists: Test 6 !! test Definition Lists: Mixed Lists: Test 7 !! wikitext -:* d1 -:* d2 +:*d1 +:*d2 !! html -<dl><dd><ul><li> d1</li> -<li> d2</li></ul></dd></dl> +<dl><dd><ul><li>d1</li> +<li>d2</li></ul></dd></dl> !! end @@ -4302,11 +4309,11 @@ Definition Lists: Mixed Lists: Test 7 !! test Definition Lists: Mixed Lists: Test 8 !! wikitext -:* d1 -::* d2 +:*d1 +::*d2 !! html -<dl><dd><ul><li> d1</li></ul> -<dl><dd><ul><li> d2</li></ul></dd></dl></dd></dl> +<dl><dd><ul><li>d1</li></ul> +<dl><dd><ul><li>d2</li></ul></dd></dl></dd></dl> !! end @@ -4332,27 +4339,28 @@ Definition Lists: Mixed Lists: Test 10 !! end +# The Parsoid team disagrees with the PHP parser's seemingly-random +# rules regarding dd/dt on the next few tests. Parsoid is more +# consistent, and recognizes the shared nesting and keeps the +# still-open tags around until the nesting is complete. + # This is a regression test for T175099 -# html/php+tidy is insufficient since Tidy covers up the bug. -# But once Tidy is replaced with RemexHTML, html/php+tidy is good enough !! test Definition Lists: Mixed Lists: Test 11 !! wikitext -; a -:* b -!! html/* -<dl><dt> a</dt> +;a +:*b +!! html/php +<dl><dt>a</dt> <dd> -<ul><li> b</li></ul></dd></dl> +<ul><li>b</li></ul></dd></dl> +!! html/parsoid +<dl><dt>a +<dd><ul><li>b</li></ul></dd></dl> !! end -# The Parsoid team disagrees with the PHP parser's seemingly-random -# rules regarding dd/dt on the next two tests. Parsoid is more -# consistent, and recognizes the shared nesting and keeps the -# still-open tags around until the nesting is complete. -# (And tidy again converts <dt> to <dd> before 'bar'.) - +# FIXME: Maybe get rid of this test? !! test Definition Lists: Mixed Lists: Test 12 !! wikitext @@ -4365,42 +4373,10 @@ Definition Lists: Mixed Lists: Test 12 <dd>baz</dd></dl></li></ol></li></ul></li></ol></li></ul> !! html/php+tidy -<ul> -<li> -<ol> -<li> -<ul> -<li> -<ol> -<li> -<dl> -<dt>foo </dt> -<dd> -<ul> -<li> -<dl> -<dd> -<dl> -<dt>bar</dt> -</dl> -</dd> -</dl> -</li> -</ul> -</dd> -</dl> -<dl> -<dt>boo </dt> -<dd>baz</dd> -</dl> -</li> -</ol> -</li> -</ul> -</li> -</ol> -</li> -</ul> +<ul><li><ol><li><ul><li><ol><li><dl><dt>foo </dt> +<dd><ul><li><dl><dt><dl><dt>bar</dt></dl></dt></dl></li></ul></dd></dl></li></ol></li></ul> +<dl><dt>boo </dt> +<dd>baz</dd></dl></li></ol></li></ul> !! html/parsoid <ul> <li> @@ -4431,52 +4407,17 @@ Definition Lists: Mixed Lists: Test 12 </ul> !! end - -# Another case where tidy converts a <dt> to a <dd> (but Parsoid doesn't). +# FIXME: Maybe get rid of this test? # From whitelist: # * The test is wrong, there are two colons where there should be :; # * The PHP parser is wrong to close the <dl> after the <dt> containing the <ul>. !! test Definition Lists: Weird Ones: Test 1 !! wikitext -*#;*::;; foo : bar (who uses this?) -!! html/php -<ul><li><ol><li><dl><dt> foo </dt> -<dd><ul><li><dl><dd><dl><dd><dl><dt><dl><dt> bar (who uses this?)</dt></dl></dd></dl></dd></dl></dd></dl></li></ul></dd></dl></li></ol></li></ul> - +*#;*::;;foo :bar (who uses this?) !! html/php+tidy -<ul> -<li> -<ol> -<li> -<dl> -<dt>foo </dt> -<dd> -<ul> -<li> -<dl> -<dd> -<dl> -<dd> -<dl> -<dd> -<dl> -<dt>bar (who uses this?)</dt> -</dl> -</dd> -</dl> -</dd> -</dl> -</dd> -</dl> -</li> -</ul> -</dd> -</dl> -</li> -</ol> -</li> -</ul> +<ul><li><ol><li><dl><dt>foo </dt> +<dd><ul><li><dl><dd><dl><dd><dl><dt><dl><dt>bar (who uses this?)</dt></dl></dt></dl></dd></dl></dd></dl></li></ul></dd></dl></li></ol></li></ul> !! html/parsoid <ul> <li> @@ -4493,8 +4434,8 @@ Definition Lists: Weird Ones: Test 1 <dl> <dt> <dl> -<dt> foo<span typeof="mw:DisplaySpace mw:Placeholder" data-parsoid='{"src":" ","isDisplayHack":true}'> </span></dt> -<dd data-parsoid='{"stx":"row"}'> bar (who uses this?)</dd> +<dt>foo<span typeof="mw:DisplaySpace mw:Placeholder" data-parsoid='{"src":" ","isDisplayHack":true}'> </span></dt> +<dd data-parsoid='{"stx":"row"}'>bar (who uses this?)</dd> </dl></dt> </dl></dd> </dl></dd> @@ -4519,37 +4460,18 @@ Definition Lists: colons occurring in tags ;{{echo|''a:b''}} ;;;''a:b'' !! html+tidy -<dl> -<dt>a</dt> +<dl><dt>a</dt> <dd>b</dd> <dt><b>a:b</b></dt> <dt><i>a:b</i></dt> <dt><span>a:b</span></dt> -<dd> -<div>a:b</div> -</dd> -<dd> -<div>a -<dl> +<dt><div>a:b</div></dt> +<dt><div>a</div></dt> <dd>b</dd> -</dl> -</div> -</dd> <dt>a</dt> <dd>b</dd> -<dt><i>a:b</i></dt> -</dl> -<dl> -<dd> -<dl> -<dd> -<dl> -<dt><i>a:b</i></dt> -</dl> -</dd> -</dl> -</dd> -</dl> +<dt><i>a:b</i></dt></dl> +<dl><dt><dl><dt><dl><dt><i>a:b</i></dt></dl></dt></dl></dt></dl> !! html/parsoid <dl><dt>a</dt><dd data-parsoid='{"stx":"row"}'>b</dd> <dt><b>a:b</b></dt> @@ -4563,52 +4485,40 @@ Definition Lists: colons occurring in tags <dl><dt><dl><dt><i>a:b</i></dt></dl></dt></dl></dt></dl> !! end +# Parsoid's output differs here again because it shares +# nesting between the two lists unlike the PHP parser. +# Unsure which is more desirable. !! test Definition Lists: colons and tables 1 !! wikitext :{| -| x +|x |} :{| -| y +|y |} -!! html +!! html/php <dl><dd><table> <tr> -<td> x +<td>x </td></tr></table></dd></dl> <dl><dd><table> <tr> -<td> y +<td>y </td></tr></table></dd></dl> -!! end - -# Parsoid's output (as documented below) differs from php's in this case. -# This is probably a bug. If we fixup parsoid to match php's output, the -# above test should pass and the below test case can be removed. It is -# unclear which output is more desirable. - -!! test -Definition Lists: colons and tables 2 -!! wikitext -:{| -| x -|} -:{| -| y -|} !! html/parsoid <dl><dd><table> <tr> -<td> x +<td>x </td></tr></table></dd> <dd><table> <tr> -<td> y +<td>y </td></tr></table></dd></dl> !! end +# FIXME: Does this need a html/php section? !! test Definition Lists: template interaction !! wikitext @@ -4657,9 +4567,9 @@ Numbered: <a rel="nofollow" class="external autonumber" href="http://example.net Numbered: <a rel="nofollow" class="external autonumber" href="http://example.com">[3]</a> </p> !! html/parsoid -<p>Numbered: <a rel="mw:ExtLink" href="http://example.com"></a> -Numbered: <a rel="mw:ExtLink" href="http://example.net"></a> -Numbered: <a rel="mw:ExtLink" href="http://example.com"></a></p> +<p>Numbered: <a rel="mw:ExtLink" class="external autonumber" href="http://example.com"></a> +Numbered: <a rel="mw:ExtLink" class="external autonumber" href="http://example.net"></a> +Numbered: <a rel="mw:ExtLink" class="external autonumber" href="http://example.com"></a></p> !!end !! test @@ -4698,7 +4608,7 @@ External links: dollar sign in URL (autonumber) <p><a rel="nofollow" class="external autonumber" href="http://example.com/1$2345">[1]</a> </p> !! html/parsoid -<p><a rel="mw:ExtLink" href="http://example.com/1$2345"></a></p> +<p><a rel="mw:ExtLink" class="external autonumber" href="http://example.com/1$2345"></a></p> !!end !! test @@ -4711,7 +4621,7 @@ http://example.com/1[2345 <p><a rel="nofollow" class="external free" href="http://example.com/1">http://example.com/1</a>[2345 </p> !! html/parsoid -<p><a rel="mw:ExtLink" href="http://example.com/1">http://example.com/1</a>[2345</p> +<p><a rel="mw:ExtLink" class="external free" href="http://example.com/1">http://example.com/1</a>[2345</p> !! end !! test @@ -4724,7 +4634,7 @@ parsoid=wt2html,html2html <p><a rel="nofollow" class="external text" href="http://example.com/1">[2345</a> </p> !! html/parsoid -<p><a rel="mw:ExtLink" href="http://example.com/1">[2345</a></p> +<p><a rel="mw:ExtLink" class="external text" href="http://example.com/1">[2345</a></p> !!end # parsoid adds a space before the link name @@ -4785,7 +4695,7 @@ External links: protocol-relative URL in brackets without text <p><a rel="nofollow" class="external autonumber" href="//example.com">[1]</a> </p> !! html/parsoid -<p><a rel="mw:ExtLink" href="//example.com"></a></p> +<p><a rel="mw:ExtLink" class="external autonumber" href="//example.com"></a></p> !! end !! test @@ -4806,8 +4716,11 @@ foo//example.com/Foo </p> !! end +## html2wt and html2html will fail because we will prefer the :en: interwiki prefix over wikipedia: !! test External links: with no contents +!! options +parsoid=wt2html,wt2wt !! wikitext [http://en.wikipedia.org/wiki/Foo] @@ -4820,9 +4733,9 @@ External links: with no contents </p><p><a href="http://en.wikipedia.org/wiki/Foo" class="extiw" title="wikipedia:Foo"><span>Bar</span></a> </p> !! html/parsoid -<p><a rel="mw:ExtLink" href="http://en.wikipedia.org/wiki/Foo"></a></p> -<p><a rel="mw:ExtLink" href="http://en.wikipedia.org/wiki/Foo" title="wikipedia:Foo">Bar</a></p> -<p><a rel="mw:ExtLink" href="http://en.wikipedia.org/wiki/Foo" title="wikipedia:Foo"><span>Bar</span></a></p> +<p><a rel="mw:ExtLink" class="external autonumber" href="http://en.wikipedia.org/wiki/Foo"></a></p> +<p><a rel="mw:WikiLink/Interwiki" href="http://en.wikipedia.org/wiki/Foo" title="wikipedia:Foo">Bar</a></p> +<p><a rel="mw:WikiLink/Interwiki" href="http://en.wikipedia.org/wiki/Foo" title="wikipedia:Foo"><span>Bar</span></a></p> !! end !! test @@ -4869,25 +4782,25 @@ http://example.com/url_with_entity< <a rel="nofollow" class="external free" href="http://example.com/url_with_entity">http://example.com/url_with_entity</a>< </p> !! html/parsoid -<p><a rel="mw:ExtLink" href="http://example.com">http://example.com</a>, -<a rel="mw:ExtLink" href="http://example.com">http://example.com</a>; -<a rel="mw:ExtLink" href="http://example.com">http://example.com</a>\ -<a rel="mw:ExtLink" href="http://example.com">http://example.com</a>. -<a rel="mw:ExtLink" href="http://example.com">http://example.com</a>: -<a rel="mw:ExtLink" href="http://example.com">http://example.com</a>! -<a rel="mw:ExtLink" href="http://example.com">http://example.com</a>? -<a rel="mw:ExtLink" href="http://example.com">http://example.com</a>) -<a rel="mw:ExtLink" href="http://example.com/url_with_(brackets)">http://example.com/url_with_(brackets)</a> -(<a rel="mw:ExtLink" href="http://example.com/url_without_brackets">http://example.com/url_without_brackets</a>) -<a rel="mw:ExtLink" href="http://example.com/url_with_entity&">http://example.com/url_with_entity&</a> -<a rel="mw:ExtLink" href="http://example.com/url_with_entity&">http://example.com/url_with_entity&</a> -<a rel="mw:ExtLink" href="http://example.com/url_with_entity&">http://example.com/url_with_entity&</a> -<a rel="mw:ExtLink" href="http://example.com/url_with_entity">http://example.com/url_with_entity</a><span typeof="mw:Entity" data-parsoid='{"src":"&nbsp;","srcContent":" "}'> </span> -<a rel="mw:ExtLink" href="http://example.com/url_with_entity">http://example.com/url_with_entity</a><span typeof="mw:Entity" data-parsoid='{"src":"&#xA0;","srcContent":" "}'> </span> -<a rel="mw:ExtLink" href="http://example.com/url_with_entity">http://example.com/url_with_entity</a><span typeof="mw:Entity" data-parsoid='{"src":"&#160;","srcContent":" "}'> </span> -<a rel="mw:ExtLink" href="http://example.com/url_with_entity">http://example.com/url_with_entity</a><span typeof="mw:Entity" data-parsoid='{"src":"&lt;","srcContent":"<"}'><</span> -<a rel="mw:ExtLink" href="http://example.com/url_with_entity">http://example.com/url_with_entity</a><span typeof="mw:Entity" data-parsoid='{"src":"&#x3C;","srcContent":"<"}'><</span> -<a rel="mw:ExtLink" href="http://example.com/url_with_entity">http://example.com/url_with_entity</a><span typeof="mw:Entity" data-parsoid='{"src":"&#60;","srcContent":"<"}'><</span></p> +<p><a rel="mw:ExtLink" class="external free" href="http://example.com">http://example.com</a>, +<a rel="mw:ExtLink" class="external free" href="http://example.com">http://example.com</a>; +<a rel="mw:ExtLink" class="external free" href="http://example.com">http://example.com</a>\ +<a rel="mw:ExtLink" class="external free" href="http://example.com">http://example.com</a>. +<a rel="mw:ExtLink" class="external free" href="http://example.com">http://example.com</a>: +<a rel="mw:ExtLink" class="external free" href="http://example.com">http://example.com</a>! +<a rel="mw:ExtLink" class="external free" href="http://example.com">http://example.com</a>? +<a rel="mw:ExtLink" class="external free" href="http://example.com">http://example.com</a>) +<a rel="mw:ExtLink" class="external free" href="http://example.com/url_with_(brackets)">http://example.com/url_with_(brackets)</a> +(<a rel="mw:ExtLink" class="external free" href="http://example.com/url_without_brackets">http://example.com/url_without_brackets</a>) +<a rel="mw:ExtLink" class="external free" href="http://example.com/url_with_entity&">http://example.com/url_with_entity&</a> +<a rel="mw:ExtLink" class="external free" href="http://example.com/url_with_entity&">http://example.com/url_with_entity&</a> +<a rel="mw:ExtLink" class="external free" href="http://example.com/url_with_entity&">http://example.com/url_with_entity&</a> +<a rel="mw:ExtLink" class="external free" href="http://example.com/url_with_entity">http://example.com/url_with_entity</a><span typeof="mw:Entity" data-parsoid='{"src":"&nbsp;","srcContent":" "}'> </span> +<a rel="mw:ExtLink" class="external free" href="http://example.com/url_with_entity">http://example.com/url_with_entity</a><span typeof="mw:Entity" data-parsoid='{"src":"&#xA0;","srcContent":" "}'> </span> +<a rel="mw:ExtLink" class="external free" href="http://example.com/url_with_entity">http://example.com/url_with_entity</a><span typeof="mw:Entity" data-parsoid='{"src":"&#160;","srcContent":" "}'> </span> +<a rel="mw:ExtLink" class="external free" href="http://example.com/url_with_entity">http://example.com/url_with_entity</a><span typeof="mw:Entity" data-parsoid='{"src":"&lt;","srcContent":"<"}'><</span> +<a rel="mw:ExtLink" class="external free" href="http://example.com/url_with_entity">http://example.com/url_with_entity</a><span typeof="mw:Entity" data-parsoid='{"src":"&#x3C;","srcContent":"<"}'><</span> +<a rel="mw:ExtLink" class="external free" href="http://example.com/url_with_entity">http://example.com/url_with_entity</a><span typeof="mw:Entity" data-parsoid='{"src":"&#60;","srcContent":"<"}'><</span></p> !! end !! test @@ -4900,7 +4813,7 @@ http://example.com/url_with_entity&amp; <p><a rel="nofollow" class="external free" href="http://example.com/url_with_entity&amp">http://example.com/url_with_entity&amp</a>; </p> !! html/parsoid -<p><a rel="mw:ExtLink" href="http://example.com/url_with_entity&amp">http://example.com/url_with_entity&amp</a>;</p> +<p><a rel="mw:ExtLink" class="external free" href="http://example.com/url_with_entity&amp">http://example.com/url_with_entity&amp</a>;</p> !! end !! test @@ -4915,7 +4828,7 @@ news:'a'b''c''d e </p> !! html/parsoid <p><b>News:</b> Stuff here</p> -<p><a rel="mw:ExtLink" href="news:'a'b">news:'a'b</a><i>c</i>d e</p> +<p><a rel="mw:ExtLink" class="external free" href="news:'a'b">news:'a'b</a><i>c</i>d e</p> !! end !! test @@ -4926,7 +4839,7 @@ External links: with entity <p><a rel="nofollow" class="external text" href="http://+www.librarieswithoutborders.org">Libraries without borders</a> </p> !! html/parsoid -<p><a rel="mw:ExtLink" href="http://+www.librarieswithoutborders.org" data-parsoid='{"a":{"href":"http://+www.librarieswithoutborders.org"},"sa":{"href":"http://&#x20;www.librarieswithoutborders.org"}}'>Libraries without borders</a></p> +<p><a rel="mw:ExtLink" class="external text" href="http://+www.librarieswithoutborders.org" data-parsoid='{"a":{"href":"http://+www.librarieswithoutborders.org"},"sa":{"href":"http://&#x20;www.librarieswithoutborders.org"}}'>Libraries without borders</a></p> !! end !! test @@ -5056,10 +4969,10 @@ parsoid=wt2html !! wikitext URL in text: [http://example.com http://example.com] !! html/php -<p>URL in text: <a rel="nofollow" class="external free" href="http://example.com">http://example.com</a> +<p>URL in text: <a rel="nofollow" class="external text" href="http://example.com">http://example.com</a> </p> !! html/parsoid -<p>URL in text: <a rel="mw:ExtLink" href="http://example.com">http://example.com</a></p> +<p>URL in text: <a rel="mw:ExtLink" class="external free" href="http://example.com">http://example.com</a></p> !! end !! test @@ -5070,7 +4983,7 @@ ja-style clickable images: [http://example.com http://meta.wikimedia.org/upload/ <p>ja-style clickable images: <a rel="nofollow" class="external text" href="http://example.com"><img src="http://meta.wikimedia.org/upload/f/f1/Ncwikicol.png" alt="Ncwikicol.png"/></a> </p> !! html/parsoid -<p>ja-style clickable images: <a rel="mw:ExtLink" href="http://example.com"><img src="http://meta.wikimedia.org/upload/f/f1/Ncwikicol.png" alt="Ncwikicol.png" data-parsoid='{"type":"extlink"}'/></a></p> +<p>ja-style clickable images: <a rel="mw:ExtLink" class="external text" href="http://example.com"><img src="http://meta.wikimedia.org/upload/f/f1/Ncwikicol.png" alt="Ncwikicol.png" data-parsoid='{"type":"extlink"}'/></a></p> !! end !! test @@ -5090,7 +5003,7 @@ Old & use: http://x&y <p>Old & use: <a rel="nofollow" class="external free" href="http://x&y">http://x&y</a> </p> !! html/parsoid -<p>Old <span typeof="mw:Entity">&</span> use: <a rel="mw:ExtLink" href="http://x&y">http://x&y</a></p> +<p>Old <span typeof="mw:Entity">&</span> use: <a rel="mw:ExtLink" class="external free" href="http://x&y">http://x&y</a></p> !! end !! test @@ -5101,7 +5014,7 @@ http://example.com/?foo=bar <p><a rel="nofollow" class="external free" href="http://example.com/?foo=bar">http://example.com/?foo=bar</a> </p> !! html/parsoid -<p><a rel="mw:ExtLink" href="http://example.com/?foo=bar">http://example.com/?foo=bar</a></p> +<p><a rel="mw:ExtLink" class="external free" href="http://example.com/?foo=bar">http://example.com/?foo=bar</a></p> !! end ## @@ -5118,7 +5031,7 @@ Old & use: [http://x&y] <p>Old & use: <a rel="nofollow" class="external autonumber" href="http://x&y">[1]</a> </p> !! html/parsoid -<p>Old <span typeof="mw:Entity">&</span> use: <a rel="mw:ExtLink" href="http://x&y"></a></p> +<p>Old <span typeof="mw:Entity">&</span> use: <a rel="mw:ExtLink" class="external autonumber" href="http://x&y"></a></p> !! end # note that parsoid html is identical to [raw ampersand] case; so html2wt @@ -5133,7 +5046,7 @@ Old & use: [http://x&y] <p>Old & use: <a rel="nofollow" class="external autonumber" href="http://x&y">[1]</a> </p> !! html/parsoid -<p>Old <span typeof="mw:Entity">&</span> use: <a rel="mw:ExtLink" href="http://x&y"></a></p> +<p>Old <span typeof="mw:Entity">&</span> use: <a rel="mw:ExtLink" class="external autonumber" href="http://x&y"></a></p> !! end !! test @@ -5144,7 +5057,7 @@ External links: [raw equals] <p><a rel="nofollow" class="external autonumber" href="http://example.com/?foo=bar">[1]</a> </p> !! html/parsoid -<p><a rel="mw:ExtLink" href="http://example.com/?foo=bar"></a></p> +<p><a rel="mw:ExtLink" class="external autonumber" href="http://example.com/?foo=bar"></a></p> !! end # note that parsoid html is identical to [raw equals] case; so html2wt @@ -5159,7 +5072,7 @@ parsoid=wt2html,wt2wt,html2html <p><a rel="nofollow" class="external autonumber" href="http://example.com/?foo=bar">[1]</a> </p> !! html/parsoid -<p><a rel="mw:ExtLink" href="http://example.com/?foo=bar"></a></p> +<p><a rel="mw:ExtLink" class="external autonumber" href="http://example.com/?foo=bar"></a></p> !! end # xxx parsoid strips the IDN character, so the round-trip tests will @@ -5174,7 +5087,7 @@ parsoid=wt2html,wt2wt,html2html <p><a rel="nofollow" class="external autonumber" href="http://example.com/">[1]</a> </p> !! html/parsoid -<p><a rel="mw:ExtLink" href="http://example.com/"></a></p> +<p><a rel="mw:ExtLink" class="external autonumber" href="http://example.com/"></a></p> !! end # FIXME: This test (the IDN characters in the text of a link) is an inconsistency. @@ -5206,7 +5119,7 @@ http://e‌xample.com/ <p><a rel="nofollow" class="external free" href="http://example.com/">http://example.com/</a> </p> !! html/parsoid -<p><a rel="mw:ExtLink" href="http://example.com/">http://example.com/</a></p> +<p><a rel="mw:ExtLink" class="external free" href="http://example.com/">http://example.com/</a></p> !! end !! test @@ -5227,7 +5140,7 @@ External links: URL within URL (T2002) <p><a rel="nofollow" class="external autonumber" href="http://www.unausa.org/newindex.asp?place=http://www.unausa.org/programs/mun.asp">[1]</a> </p> !! html/parsoid -<p><a rel="mw:ExtLink" href="http://www.unausa.org/newindex.asp?place=http://www.unausa.org/programs/mun.asp"></a></p> +<p><a rel="mw:ExtLink" class="external autonumber" href="http://www.unausa.org/newindex.asp?place=http://www.unausa.org/programs/mun.asp"></a></p> !! end !! test @@ -5265,7 +5178,7 @@ http://www.example.com/<b>html</b> <p><a rel="nofollow" class="external free" href="http://www.example.com/">http://www.example.com/</a><b>html</b> </p> !! html/parsoid -<p><a rel="mw:ExtLink" href="http://www.example.com/" data-parsoid='{"stx":"url"}'>http://www.example.com/</a><b data-parsoid='{"stx":"html"}'>html</b></p> +<p><a rel="mw:ExtLink" class="external free" href="http://www.example.com/" data-parsoid='{"stx":"url"}'>http://www.example.com/</a><b data-parsoid='{"stx":"html"}'>html</b></p> !! end !! test @@ -5324,17 +5237,22 @@ External links: link text with spaces </p> !! end +# Note edge case difference between PHP and Parsoid here. !! test External links: wiki links within external link (T5695) !! options parsoid=wt2html,html2html !! wikitext [http://example.com [[wikilink]] embedded in ext link] + +[http://example.com test [[wikilink]] embedded in ext link] !! html/php <p><a rel="nofollow" class="external text" href="http://example.com"></a><a href="/index.php?title=Wikilink&action=edit&redlink=1" class="new" title="Wikilink (page does not exist)">wikilink</a><a rel="nofollow" class="external text" href="http://example.com"> embedded in ext link</a> +</p><p><a rel="nofollow" class="external text" href="http://example.com">test </a><a href="/index.php?title=Wikilink&action=edit&redlink=1" class="new" title="Wikilink (page does not exist)">wikilink</a><a rel="nofollow" class="external text" href="http://example.com"> embedded in ext link</a> </p> !! html/parsoid -<p><a rel="mw:ExtLink" href="http://example.com"></a><a rel="mw:WikiLink" href="./Wikilink" title="Wikilink">wikilink</a><span> embedded in ext link</span></p> +<p><a rel="mw:ExtLink" class="external autonumber" href="http://example.com"></a><a rel="mw:WikiLink" href="./Wikilink" title="Wikilink">wikilink</a><span> embedded in ext link</span></p> +<p><a rel="mw:ExtLink" class="external text" href="http://example.com">test </a><a rel="mw:WikiLink" href="./Wikilink" title="Wikilink">wikilink</a><span> embedded in ext link</span></p> !! end !! test @@ -5378,8 +5296,8 @@ parsoid=wt2html </p><p>{{echo|[[Foo}} </p> !! html/parsoid -<p>[<a rel="mw:ExtLink" href="http://example.com">http://example.com</a> x</p> -<p typeof="mw:Transclusion" data-mw='{"parts":[{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"[http://example.com x"}},"i":0}}]}'>[<a rel="mw:ExtLink" href="http://example.com">http://example.com</a> x</p> +<p>[<a rel="mw:ExtLink" class="external free" href="http://example.com">http://example.com</a> x</p> +<p typeof="mw:Transclusion" data-mw='{"parts":[{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"[http://example.com x"}},"i":0}}]}'>[<a rel="mw:ExtLink" class="external free" href="http://example.com">http://example.com</a> x</p> <p>[[Foo</p> <p>{{echo|[[Foo}}</p> !! end @@ -5442,7 +5360,7 @@ http://www.example.com/?title=AT%26T <p><a rel="nofollow" class="external free" href="http://www.example.com/?title=AT%26T">http://www.example.com/?title=AT%26T</a> </p> !! html/parsoid -<p><a rel="mw:ExtLink" href="http://www.example.com/?title=AT%26T">http://www.example.com/?title=AT%26T</a></p> +<p><a rel="mw:ExtLink" class="external free" href="http://www.example.com/?title=AT%26T">http://www.example.com/?title=AT%26T</a></p> !! end # According to https://www.w3.org/TR/2011/WD-html5-20110525/Overview.html#parsing-urls a plain @@ -5455,7 +5373,7 @@ http://www.example.com/?title=100%25_Bran <p><a rel="nofollow" class="external free" href="http://www.example.com/?title=100%25_Bran">http://www.example.com/?title=100%25_Bran</a> </p> !! html/parsoid -<p><a rel="mw:ExtLink" href="http://www.example.com/?title=100%25_Bran">http://www.example.com/?title=100%25_Bran</a></p> +<p><a rel="mw:ExtLink" class="external free" href="http://www.example.com/?title=100%25_Bran">http://www.example.com/?title=100%25_Bran</a></p> !! end !! test @@ -5466,7 +5384,7 @@ http://www.example.com/?title=Ben-Hur_%281959_film%29 <p><a rel="nofollow" class="external free" href="http://www.example.com/?title=Ben-Hur_%281959_film%29">http://www.example.com/?title=Ben-Hur_%281959_film%29</a> </p> !! html/parsoid -<p><a rel="mw:ExtLink" href="http://www.example.com/?title=Ben-Hur_%281959_film%29">http://www.example.com/?title=Ben-Hur_%281959_film%29</a></p> +<p><a rel="mw:ExtLink" class="external free" href="http://www.example.com/?title=Ben-Hur_%281959_film%29">http://www.example.com/?title=Ben-Hur_%281959_film%29</a></p> !! end @@ -5478,7 +5396,7 @@ T6781: %26 in autonumber URL <p><a rel="nofollow" class="external autonumber" href="http://www.example.com/?title=AT%26T">[1]</a> </p> !! html/parsoid -<p><a rel="mw:ExtLink" href="http://www.example.com/?title=AT%26T"></a></p> +<p><a rel="mw:ExtLink" class="external autonumber" href="http://www.example.com/?title=AT%26T"></a></p> !! end !! test @@ -5489,7 +5407,7 @@ T6781, T7267: %26 in autonumber URL <p><a rel="nofollow" class="external autonumber" href="http://www.example.com/?title=100%25_Bran">[1]</a> </p> !! html/parsoid -<p><a rel="mw:ExtLink" href="http://www.example.com/?title=100%25_Bran"></a></p> +<p><a rel="mw:ExtLink" class="external autonumber" href="http://www.example.com/?title=100%25_Bran"></a></p> !! end !! test @@ -5500,7 +5418,7 @@ T6781, T7267: %28, %29 in autonumber URL <p><a rel="nofollow" class="external autonumber" href="http://www.example.com/?title=Ben-Hur_%281959_film%29">[1]</a> </p> !! html/parsoid -<p><a rel="mw:ExtLink" href="http://www.example.com/?title=Ben-Hur_%281959_film%29"></a></p> +<p><a rel="mw:ExtLink" class="external autonumber" href="http://www.example.com/?title=Ben-Hur_%281959_film%29"></a></p> !! end @@ -5512,7 +5430,7 @@ T6781: %26 in bracketed URL <p><a rel="nofollow" class="external text" href="http://www.example.com/?title=AT%26T">link</a> </p> !! html/parsoid -<p><a rel="mw:ExtLink" href="http://www.example.com/?title=AT%26T">link</a></p> +<p><a rel="mw:ExtLink" class="external text" href="http://www.example.com/?title=AT%26T">link</a></p> !! end !! test @@ -5532,7 +5450,7 @@ T6781, T7267: %28, %29 in bracketed URL <p><a rel="nofollow" class="external text" href="http://www.example.com/?title=Ben-Hur_%281959_film%29">link</a> </p> !! html/parsoid -<p><a rel="mw:ExtLink" href="http://www.example.com/?title=Ben-Hur_%281959_film%29">link</a></p> +<p><a rel="mw:ExtLink" class="external text" href="http://www.example.com/?title=Ben-Hur_%281959_film%29">link</a></p> !! end !! test @@ -5546,8 +5464,8 @@ External link containing a period in the anchor. (T65947) </p><p><a rel="nofollow" class="external text" href="//foo.org/bar.">bang</a> </p> !! html/parsoid -<p><a rel="mw:ExtLink" href="//foo.org/bar#baz.">bang</a></p> -<p><a rel="mw:ExtLink" href="//foo.org/bar.">bang</a></p> +<p><a rel="mw:ExtLink" class="external text" href="//foo.org/bar#baz.">bang</a></p> +<p><a rel="mw:ExtLink" class="external text" href="//foo.org/bar.">bang</a></p> !! end !! test @@ -5561,8 +5479,8 @@ External link containing a single quote. (T65947) </p><p><a rel="nofollow" class="external text" href="//foo.org/bar'baz">bang</a> </p> !! html/parsoid -<p><a rel="mw:ExtLink" href="//foo.org/bar'baz"></a></p> -<p><a rel="mw:ExtLink" href="//foo.org/bar'baz">bang</a></p> +<p><a rel="mw:ExtLink" class="external autonumber" href="//foo.org/bar'baz"></a></p> +<p><a rel="mw:ExtLink" class="external text" href="//foo.org/bar'baz">bang</a></p> !! end !! test @@ -5583,17 +5501,16 @@ External link containing double-single-quotes in text embedded in italics (T6598 </p> !! end +# Don't add the html/php section since the output is broken and there isn't any reason to spec it !! test External link containing double-single-quotes with no space separating the url from text in italics !! wikitext [http://www.musee-picasso.fr/pages/page_id18528_u1l2.htm''La muerte de Casagemas'' (1901) en el sitio de [[Museo Picasso (París)|Museo Picasso]].] -!! html/php -<p><a rel="nofollow" class="external text" href="http://www.musee-picasso.fr/pages/page_id18528_u1l2.htm"><i>La muerte de Casagemas</i> (1901) en el sitio de <a href="/index.php?title=Museo_Picasso_(Par%C3%ADs)&action=edit&redlink=1" class="new" title="Museo Picasso (París) (page does not exist)">Museo Picasso</a>.</a> -</p> !! html/php+tidy -<p><a rel="nofollow" class="external text" href="http://www.musee-picasso.fr/pages/page_id18528_u1l2.htm"><i>La muerte de Casagemas</i> (1901) en el sitio de</a> <a href="/index.php?title=Museo_Picasso_(Par%C3%ADs)&action=edit&redlink=1" class="new" title="Museo Picasso (París) (page does not exist)">Museo Picasso</a>.</p> +<p><a rel="nofollow" class="external text" href="http://www.musee-picasso.fr/pages/page_id18528_u1l2.htm"><i>La muerte de Casagemas</i> (1901) en el sitio de </a><a href="/index.php?title=Museo_Picasso_(Par%C3%ADs)&action=edit&redlink=1" class="new" title="Museo Picasso (París) (page does not exist)">Museo Picasso</a>. +</p> !! html/parsoid -<p><a rel="mw:ExtLink" href="http://www.musee-picasso.fr/pages/page_id18528_u1l2.htm"><i>La muerte de Casagemas</i> (1901) en el sitio de </a><a rel="mw:WikiLink" href="./Museo_Picasso_(París)" title="Museo Picasso (París)">Museo Picasso</a><span>.</span></p> +<p><a rel="mw:ExtLink" class="external text" href="http://www.musee-picasso.fr/pages/page_id18528_u1l2.htm"><i>La muerte de Casagemas</i> (1901) en el sitio de </a><a rel="mw:WikiLink" href="./Museo_Picasso_(París)" title="Museo Picasso (París)">Museo Picasso</a><span>.</span></p> !! end !! test @@ -5604,7 +5521,7 @@ External link with comments in link text <p><a rel="nofollow" class="external text" href="http://www.google.com">Google </a> </p> !! html/parsoid -<p><a rel="mw:ExtLink" href="http://www.google.com">Google <!-- comment --></a></p> +<p><a rel="mw:ExtLink" class="external text" href="http://www.google.com">Google <!-- comment --></a></p> !! end !! test @@ -5615,7 +5532,7 @@ External link to bare IPv4 address <p><a rel="nofollow" class="external text" href="http://192.168.0.1">Link</a> </p> !! html/parsoid -<p><a rel="mw:ExtLink" href="http://192.168.0.1">Link</a></p> +<p><a rel="mw:ExtLink" class="external text" href="http://192.168.0.1">Link</a></p> !! end !! test @@ -5647,9 +5564,9 @@ http://example.com/index.php?foozoid[]=bar </p><p><a rel="nofollow" class="external free" href="http://example.com/index.php?foozoid%5B%5D=bar">http://example.com/index.php?foozoid%5B%5D=bar</a> </p> !! html/parsoid -<p><a rel="mw:ExtLink" href="http://example.com/index.php?foozoid%5B%5D=bar">http://example.com/index.php?foozoid%5B%5D=bar</a></p> +<p><a rel="mw:ExtLink" class="external free" href="http://example.com/index.php?foozoid%5B%5D=bar">http://example.com/index.php?foozoid%5B%5D=bar</a></p> -<p><a rel="mw:ExtLink" href="http://example.com/index.php?foozoid%5B%5D=bar" data-parsoid='{"stx":"url","a":{"href":"http://example.com/index.php?foozoid%5B%5D=bar"},"sa":{"href":"http://example.com/index.php?foozoid&#x5B;&#x5D;=bar"}}'>http://example.com/index.php?foozoid%5B%5D=bar</a></p> +<p><a rel="mw:ExtLink" class="external free" href="http://example.com/index.php?foozoid%5B%5D=bar" data-parsoid='{"stx":"url","a":{"href":"http://example.com/index.php?foozoid%5B%5D=bar"},"sa":{"href":"http://example.com/index.php?foozoid&#x5B;&#x5D;=bar"}}'>http://example.com/index.php?foozoid%5B%5D=bar</a></p> !! end !! test @@ -5658,61 +5575,62 @@ IPv6 urls, autolink format (T23261) http://[2404:130:0:1000::187:2]/index.php Examples from RFC 2373, section 2.2: -* http://[1080::8:800:200C:417A]/unicast -* http://[FF01::101]/multicast -* http://[::1]/loopback -* http://[::]/unspecified -* http://[::13.1.68.3]/ipv4compat -* http://[::FFFF:129.144.52.38]/ipv4compat + +*http://[1080::8:800:200C:417A]/unicast +*http://[FF01::101]/multicast +*http://[::1]/loopback +*http://[::]/unspecified +*http://[::13.1.68.3]/ipv4compat +*http://[::FFFF:129.144.52.38]/ipv4compat Examples from RFC 2732, section 2: -* http://[FEDC:BA98:7654:3210:FEDC:BA98:7654:3210]:80/index.html -* http://[1080:0:0:0:8:800:200C:417A]/index.html -* http://[3ffe:2a00:100:7031::1] -* http://[1080::8:800:200C:417A]/foo -* http://[::192.9.5.5]/ipng -* http://[::FFFF:129.144.52.38]:80/index.html -* http://[2010:836B:4179::836B:4179] +*http://[FEDC:BA98:7654:3210:FEDC:BA98:7654:3210]:80/index.html +*http://[1080:0:0:0:8:800:200C:417A]/index.html +*http://[3ffe:2a00:100:7031::1] +*http://[1080::8:800:200C:417A]/foo +*http://[::192.9.5.5]/ipng +*http://[::FFFF:129.144.52.38]:80/index.html +*http://[2010:836B:4179::836B:4179] !! html/php <p><a rel="nofollow" class="external free" href="http://[2404:130:0:1000::187:2]/index.php">http://[2404:130:0:1000::187:2]/index.php</a> -</p><p>Examples from <a class="external mw-magiclink-rfc" rel="nofollow" href="//tools.ietf.org/html/rfc2373">RFC 2373</a>, section 2.2: -</p> -<ul><li> <a rel="nofollow" class="external free" href="http://[1080::8:800:200C:417A]/unicast">http://[1080::8:800:200C:417A]/unicast</a></li> -<li> <a rel="nofollow" class="external free" href="http://[FF01::101]/multicast">http://[FF01::101]/multicast</a></li> -<li> <a rel="nofollow" class="external free" href="http://[::1]/loopback">http://[::1]/loopback</a></li> -<li> <a rel="nofollow" class="external free" href="http://[::]/unspecified">http://[::]/unspecified</a></li> -<li> <a rel="nofollow" class="external free" href="http://[::13.1.68.3]/ipv4compat">http://[::13.1.68.3]/ipv4compat</a></li> -<li> <a rel="nofollow" class="external free" href="http://[::FFFF:129.144.52.38]/ipv4compat">http://[::FFFF:129.144.52.38]/ipv4compat</a></li></ul> -<p>Examples from <a class="external mw-magiclink-rfc" rel="nofollow" href="//tools.ietf.org/html/rfc2732">RFC 2732</a>, section 2: -</p> -<ul><li> <a rel="nofollow" class="external free" href="http://[FEDC:BA98:7654:3210:FEDC:BA98:7654:3210]:80/index.html">http://[FEDC:BA98:7654:3210:FEDC:BA98:7654:3210]:80/index.html</a></li> -<li> <a rel="nofollow" class="external free" href="http://[1080:0:0:0:8:800:200C:417A]/index.html">http://[1080:0:0:0:8:800:200C:417A]/index.html</a></li> -<li> <a rel="nofollow" class="external free" href="http://[3ffe:2a00:100:7031::1]">http://[3ffe:2a00:100:7031::1]</a></li> -<li> <a rel="nofollow" class="external free" href="http://[1080::8:800:200C:417A]/foo">http://[1080::8:800:200C:417A]/foo</a></li> -<li> <a rel="nofollow" class="external free" href="http://[::192.9.5.5]/ipng">http://[::192.9.5.5]/ipng</a></li> -<li> <a rel="nofollow" class="external free" href="http://[::FFFF:129.144.52.38]:80/index.html">http://[::FFFF:129.144.52.38]:80/index.html</a></li> -<li> <a rel="nofollow" class="external free" href="http://[2010:836B:4179::836B:4179]">http://[2010:836B:4179::836B:4179]</a></li></ul> - -!! html/parsoid -<p><a rel="mw:ExtLink" href="http://[2404:130:0:1000::187:2]/index.php">http://[2404:130:0:1000::187:2]/index.php</a></p> - -<p>Examples from <a href="//tools.ietf.org/html/rfc2373" rel="mw:ExtLink">RFC 2373</a>, section 2.2:</p> -<ul><li> <a rel="mw:ExtLink" href="http://[1080::8:800:200C:417A]/unicast">http://[1080::8:800:200C:417A]/unicast</a></li> -<li> <a rel="mw:ExtLink" href="http://[FF01::101]/multicast">http://[FF01::101]/multicast</a></li> -<li> <a rel="mw:ExtLink" href="http://[::1]/loopback">http://[::1]/loopback</a></li> -<li> <a rel="mw:ExtLink" href="http://[::]/unspecified">http://[::]/unspecified</a></li> -<li> <a rel="mw:ExtLink" href="http://[::13.1.68.3]/ipv4compat">http://[::13.1.68.3]/ipv4compat</a></li> -<li> <a rel="mw:ExtLink" href="http://[::FFFF:129.144.52.38]/ipv4compat">http://[::FFFF:129.144.52.38]/ipv4compat</a></li></ul> - -<p>Examples from <a href="//tools.ietf.org/html/rfc2732" rel="mw:ExtLink">RFC 2732</a>, section 2:</p> -<ul><li> <a rel="mw:ExtLink" href="http://[FEDC:BA98:7654:3210:FEDC:BA98:7654:3210]:80/index.html">http://[FEDC:BA98:7654:3210:FEDC:BA98:7654:3210]:80/index.html</a></li> -<li> <a rel="mw:ExtLink" href="http://[1080:0:0:0:8:800:200C:417A]/index.html">http://[1080:0:0:0:8:800:200C:417A]/index.html</a></li> -<li> <a rel="mw:ExtLink" href="http://[3ffe:2a00:100:7031::1]">http://[3ffe:2a00:100:7031::1]</a></li> -<li> <a rel="mw:ExtLink" href="http://[1080::8:800:200C:417A]/foo">http://[1080::8:800:200C:417A]/foo</a></li> -<li> <a rel="mw:ExtLink" href="http://[::192.9.5.5]/ipng">http://[::192.9.5.5]/ipng</a></li> -<li> <a rel="mw:ExtLink" href="http://[::FFFF:129.144.52.38]:80/index.html">http://[::FFFF:129.144.52.38]:80/index.html</a></li> -<li> <a rel="mw:ExtLink" href="http://[2010:836B:4179::836B:4179]">http://[2010:836B:4179::836B:4179]</a></li></ul> +</p><p>Examples from <a class="external mw-magiclink-rfc" rel="nofollow" href="https://tools.ietf.org/html/rfc2373">RFC 2373</a>, section 2.2: +</p> +<ul><li><a rel="nofollow" class="external free" href="http://[1080::8:800:200C:417A]/unicast">http://[1080::8:800:200C:417A]/unicast</a></li> +<li><a rel="nofollow" class="external free" href="http://[FF01::101]/multicast">http://[FF01::101]/multicast</a></li> +<li><a rel="nofollow" class="external free" href="http://[::1]/loopback">http://[::1]/loopback</a></li> +<li><a rel="nofollow" class="external free" href="http://[::]/unspecified">http://[::]/unspecified</a></li> +<li><a rel="nofollow" class="external free" href="http://[::13.1.68.3]/ipv4compat">http://[::13.1.68.3]/ipv4compat</a></li> +<li><a rel="nofollow" class="external free" href="http://[::FFFF:129.144.52.38]/ipv4compat">http://[::FFFF:129.144.52.38]/ipv4compat</a></li></ul> +<p>Examples from <a class="external mw-magiclink-rfc" rel="nofollow" href="https://tools.ietf.org/html/rfc2732">RFC 2732</a>, section 2: +</p> +<ul><li><a rel="nofollow" class="external free" href="http://[FEDC:BA98:7654:3210:FEDC:BA98:7654:3210]:80/index.html">http://[FEDC:BA98:7654:3210:FEDC:BA98:7654:3210]:80/index.html</a></li> +<li><a rel="nofollow" class="external free" href="http://[1080:0:0:0:8:800:200C:417A]/index.html">http://[1080:0:0:0:8:800:200C:417A]/index.html</a></li> +<li><a rel="nofollow" class="external free" href="http://[3ffe:2a00:100:7031::1]">http://[3ffe:2a00:100:7031::1]</a></li> +<li><a rel="nofollow" class="external free" href="http://[1080::8:800:200C:417A]/foo">http://[1080::8:800:200C:417A]/foo</a></li> +<li><a rel="nofollow" class="external free" href="http://[::192.9.5.5]/ipng">http://[::192.9.5.5]/ipng</a></li> +<li><a rel="nofollow" class="external free" href="http://[::FFFF:129.144.52.38]:80/index.html">http://[::FFFF:129.144.52.38]:80/index.html</a></li> +<li><a rel="nofollow" class="external free" href="http://[2010:836B:4179::836B:4179]">http://[2010:836B:4179::836B:4179]</a></li></ul> + +!! html/parsoid +<p><a rel="mw:ExtLink" class="external free" href="http://[2404:130:0:1000::187:2]/index.php">http://[2404:130:0:1000::187:2]/index.php</a></p> + +<p>Examples from <a href="https://tools.ietf.org/html/rfc2373" rel="mw:ExtLink" class="external text">RFC 2373</a>, section 2.2:</p> +<ul><li><a rel="mw:ExtLink" class="external free" href="http://[1080::8:800:200C:417A]/unicast">http://[1080::8:800:200C:417A]/unicast</a></li> +<li><a rel="mw:ExtLink" class="external free" href="http://[FF01::101]/multicast">http://[FF01::101]/multicast</a></li> +<li><a rel="mw:ExtLink" class="external free" href="http://[::1]/loopback">http://[::1]/loopback</a></li> +<li><a rel="mw:ExtLink" class="external free" href="http://[::]/unspecified">http://[::]/unspecified</a></li> +<li><a rel="mw:ExtLink" class="external free" href="http://[::13.1.68.3]/ipv4compat">http://[::13.1.68.3]/ipv4compat</a></li> +<li><a rel="mw:ExtLink" class="external free" href="http://[::FFFF:129.144.52.38]/ipv4compat">http://[::FFFF:129.144.52.38]/ipv4compat</a></li></ul> + +<p>Examples from <a href="https://tools.ietf.org/html/rfc2732" rel="mw:ExtLink" class="external text">RFC 2732</a>, section 2:</p> +<ul><li><a rel="mw:ExtLink" class="external free" href="http://[FEDC:BA98:7654:3210:FEDC:BA98:7654:3210]:80/index.html">http://[FEDC:BA98:7654:3210:FEDC:BA98:7654:3210]:80/index.html</a></li> +<li><a rel="mw:ExtLink" class="external free" href="http://[1080:0:0:0:8:800:200C:417A]/index.html">http://[1080:0:0:0:8:800:200C:417A]/index.html</a></li> +<li><a rel="mw:ExtLink" class="external free" href="http://[3ffe:2a00:100:7031::1]">http://[3ffe:2a00:100:7031::1]</a></li> +<li><a rel="mw:ExtLink" class="external free" href="http://[1080::8:800:200C:417A]/foo">http://[1080::8:800:200C:417A]/foo</a></li> +<li><a rel="mw:ExtLink" class="external free" href="http://[::192.9.5.5]/ipng">http://[::192.9.5.5]/ipng</a></li> +<li><a rel="mw:ExtLink" class="external free" href="http://[::FFFF:129.144.52.38]:80/index.html">http://[::FFFF:129.144.52.38]:80/index.html</a></li> +<li><a rel="mw:ExtLink" class="external free" href="http://[2010:836B:4179::836B:4179]">http://[2010:836B:4179::836B:4179]</a></li></ul> !! end !! test @@ -5721,61 +5639,62 @@ IPv6 urls, bracketed format (T23261) [http://[2404:130:0:1000::187:2]/index.php test] Examples from RFC 2373, section 2.2: -* [http://[1080::8:800:200C:417A] unicast] -* [http://[FF01::101] multicast] -* [http://[::1]/ loopback] -* [http://[::] unspecified] -* [http://[::13.1.68.3] ipv4compat] -* [http://[::FFFF:129.144.52.38] ipv4compat] + +*[http://[1080::8:800:200C:417A] unicast] +*[http://[FF01::101] multicast] +*[http://[::1]/ loopback] +*[http://[::] unspecified] +*[http://[::13.1.68.3] ipv4compat] +*[http://[::FFFF:129.144.52.38] ipv4compat] Examples from RFC 2732, section 2: -* [http://[FEDC:BA98:7654:3210:FEDC:BA98:7654:3210]:80/index.html 1] -* [http://[1080:0:0:0:8:800:200C:417A]/index.html 2] -* [http://[3ffe:2a00:100:7031::1] 3] -* [http://[1080::8:800:200C:417A]/foo 4] -* [http://[::192.9.5.5]/ipng 5] -* [http://[::FFFF:129.144.52.38]:80/index.html 6] -* [http://[2010:836B:4179::836B:4179] 7] +*[http://[FEDC:BA98:7654:3210:FEDC:BA98:7654:3210]:80/index.html 1] +*[http://[1080:0:0:0:8:800:200C:417A]/index.html 2] +*[http://[3ffe:2a00:100:7031::1] 3] +*[http://[1080::8:800:200C:417A]/foo 4] +*[http://[::192.9.5.5]/ipng 5] +*[http://[::FFFF:129.144.52.38]:80/index.html 6] +*[http://[2010:836B:4179::836B:4179] 7] !! html/php <p><a rel="nofollow" class="external text" href="http://[2404:130:0:1000::187:2]/index.php">test</a> -</p><p>Examples from <a class="external mw-magiclink-rfc" rel="nofollow" href="//tools.ietf.org/html/rfc2373">RFC 2373</a>, section 2.2: -</p> -<ul><li> <a rel="nofollow" class="external text" href="http://[1080::8:800:200C:417A]">unicast</a></li> -<li> <a rel="nofollow" class="external text" href="http://[FF01::101]">multicast</a></li> -<li> <a rel="nofollow" class="external text" href="http://[::1]/">loopback</a></li> -<li> <a rel="nofollow" class="external text" href="http://[::]">unspecified</a></li> -<li> <a rel="nofollow" class="external text" href="http://[::13.1.68.3]">ipv4compat</a></li> -<li> <a rel="nofollow" class="external text" href="http://[::FFFF:129.144.52.38]">ipv4compat</a></li></ul> -<p>Examples from <a class="external mw-magiclink-rfc" rel="nofollow" href="//tools.ietf.org/html/rfc2732">RFC 2732</a>, section 2: -</p> -<ul><li> <a rel="nofollow" class="external text" href="http://[FEDC:BA98:7654:3210:FEDC:BA98:7654:3210]:80/index.html">1</a></li> -<li> <a rel="nofollow" class="external text" href="http://[1080:0:0:0:8:800:200C:417A]/index.html">2</a></li> -<li> <a rel="nofollow" class="external text" href="http://[3ffe:2a00:100:7031::1]">3</a></li> -<li> <a rel="nofollow" class="external text" href="http://[1080::8:800:200C:417A]/foo">4</a></li> -<li> <a rel="nofollow" class="external text" href="http://[::192.9.5.5]/ipng">5</a></li> -<li> <a rel="nofollow" class="external text" href="http://[::FFFF:129.144.52.38]:80/index.html">6</a></li> -<li> <a rel="nofollow" class="external text" href="http://[2010:836B:4179::836B:4179]">7</a></li></ul> - -!! html/parsoid -<p><a rel="mw:ExtLink" href="http://[2404:130:0:1000::187:2]/index.php">test</a></p> - -<p>Examples from <a href="//tools.ietf.org/html/rfc2373" rel="mw:ExtLink">RFC 2373</a>, section 2.2:</p> -<ul><li> <a rel="mw:ExtLink" href="http://[1080::8:800:200C:417A]">unicast</a></li> -<li> <a rel="mw:ExtLink" href="http://[FF01::101]">multicast</a></li> -<li> <a rel="mw:ExtLink" href="http://[::1]/">loopback</a></li> -<li> <a rel="mw:ExtLink" href="http://[::]">unspecified</a></li> -<li> <a rel="mw:ExtLink" href="http://[::13.1.68.3]">ipv4compat</a></li> -<li> <a rel="mw:ExtLink" href="http://[::FFFF:129.144.52.38]">ipv4compat</a></li></ul> - -<p>Examples from <a href="//tools.ietf.org/html/rfc2732" rel="mw:ExtLink">RFC 2732</a>, section 2:</p> -<ul><li> <a rel="mw:ExtLink" href="http://[FEDC:BA98:7654:3210:FEDC:BA98:7654:3210]:80/index.html">1</a></li> -<li> <a rel="mw:ExtLink" href="http://[1080:0:0:0:8:800:200C:417A]/index.html">2</a></li> -<li> <a rel="mw:ExtLink" href="http://[3ffe:2a00:100:7031::1]">3</a></li> -<li> <a rel="mw:ExtLink" href="http://[1080::8:800:200C:417A]/foo">4</a></li> -<li> <a rel="mw:ExtLink" href="http://[::192.9.5.5]/ipng">5</a></li> -<li> <a rel="mw:ExtLink" href="http://[::FFFF:129.144.52.38]:80/index.html">6</a></li> -<li> <a rel="mw:ExtLink" href="http://[2010:836B:4179::836B:4179]">7</a></li></ul> +</p><p>Examples from <a class="external mw-magiclink-rfc" rel="nofollow" href="https://tools.ietf.org/html/rfc2373">RFC 2373</a>, section 2.2: +</p> +<ul><li><a rel="nofollow" class="external text" href="http://[1080::8:800:200C:417A]">unicast</a></li> +<li><a rel="nofollow" class="external text" href="http://[FF01::101]">multicast</a></li> +<li><a rel="nofollow" class="external text" href="http://[::1]/">loopback</a></li> +<li><a rel="nofollow" class="external text" href="http://[::]">unspecified</a></li> +<li><a rel="nofollow" class="external text" href="http://[::13.1.68.3]">ipv4compat</a></li> +<li><a rel="nofollow" class="external text" href="http://[::FFFF:129.144.52.38]">ipv4compat</a></li></ul> +<p>Examples from <a class="external mw-magiclink-rfc" rel="nofollow" href="https://tools.ietf.org/html/rfc2732">RFC 2732</a>, section 2: +</p> +<ul><li><a rel="nofollow" class="external text" href="http://[FEDC:BA98:7654:3210:FEDC:BA98:7654:3210]:80/index.html">1</a></li> +<li><a rel="nofollow" class="external text" href="http://[1080:0:0:0:8:800:200C:417A]/index.html">2</a></li> +<li><a rel="nofollow" class="external text" href="http://[3ffe:2a00:100:7031::1]">3</a></li> +<li><a rel="nofollow" class="external text" href="http://[1080::8:800:200C:417A]/foo">4</a></li> +<li><a rel="nofollow" class="external text" href="http://[::192.9.5.5]/ipng">5</a></li> +<li><a rel="nofollow" class="external text" href="http://[::FFFF:129.144.52.38]:80/index.html">6</a></li> +<li><a rel="nofollow" class="external text" href="http://[2010:836B:4179::836B:4179]">7</a></li></ul> + +!! html/parsoid +<p><a rel="mw:ExtLink" class="external text" href="http://[2404:130:0:1000::187:2]/index.php">test</a></p> + +<p>Examples from <a href="https://tools.ietf.org/html/rfc2373" rel="mw:ExtLink" class="external text">RFC 2373</a>, section 2.2:</p> +<ul><li><a rel="mw:ExtLink" class="external text" href="http://[1080::8:800:200C:417A]">unicast</a></li> +<li><a rel="mw:ExtLink" class="external text" href="http://[FF01::101]">multicast</a></li> +<li><a rel="mw:ExtLink" class="external text" href="http://[::1]/">loopback</a></li> +<li><a rel="mw:ExtLink" class="external text" href="http://[::]">unspecified</a></li> +<li><a rel="mw:ExtLink" class="external text" href="http://[::13.1.68.3]">ipv4compat</a></li> +<li><a rel="mw:ExtLink" class="external text" href="http://[::FFFF:129.144.52.38]">ipv4compat</a></li></ul> + +<p>Examples from <a href="https://tools.ietf.org/html/rfc2732" rel="mw:ExtLink" class="external text">RFC 2732</a>, section 2:</p> +<ul><li><a rel="mw:ExtLink" class="external text" href="http://[FEDC:BA98:7654:3210:FEDC:BA98:7654:3210]:80/index.html">1</a></li> +<li><a rel="mw:ExtLink" class="external text" href="http://[1080:0:0:0:8:800:200C:417A]/index.html">2</a></li> +<li><a rel="mw:ExtLink" class="external text" href="http://[3ffe:2a00:100:7031::1]">3</a></li> +<li><a rel="mw:ExtLink" class="external text" href="http://[1080::8:800:200C:417A]/foo">4</a></li> +<li><a rel="mw:ExtLink" class="external text" href="http://[::192.9.5.5]/ipng">5</a></li> +<li><a rel="mw:ExtLink" class="external text" href="http://[::FFFF:129.144.52.38]:80/index.html">6</a></li> +<li><a rel="mw:ExtLink" class="external text" href="http://[2010:836B:4179::836B:4179]">7</a></li></ul> !! end !! test @@ -5815,13 +5734,13 @@ Non-extlinks in brackets [foo <i>bar</i>] [fool's] errand [fool's errand] -[<span typeof="mw:Placeholder" data-parsoid='{"src":"{{echo|foo}}"}'>foo</span>] -[<span typeof="mw:Placeholder" data-parsoid='{"src":"{{echo|foo}}"}'>foo</span> bar] -[<span typeof="mw:Placeholder" data-parsoid='{"src":"{{echo|foo}}"}'>foo</span> <i>bar</i>] -[<span typeof="mw:Placeholder" data-parsoid='{"src":"{{echo|foo}}l's"}'>fool's</span>] errand -[<span typeof="mw:Placeholder" data-parsoid='{"src":"{{echo|foo}}l's"}'>fool's</span> errand] -[<span typeof="mw:Placeholder" data-parsoid='{"src":"url={{echo|foo}}"}'>url=foo</span>] -[url=<a rel="mw:ExtLink" href="http://example.com">http://example.com</a>] +[<span about="#mwt19" typeof="mw:Transclusion" data-mw='{"parts":[{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"foo"}},"i":0}}]}'>foo</span>] +[<span about="#mwt20" typeof="mw:Transclusion" data-mw='{"parts":[{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"foo"}},"i":0}}]}'>foo</span> bar] +[<span about="#mwt21" typeof="mw:Transclusion" data-mw='{"parts":[{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"foo"}},"i":0}}]}'>foo</span> <i>bar</i>] +[<span about="#mwt22" typeof="mw:Transclusion" data-mw='{"parts":[{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"foo"}},"i":0}}]}'>foo</span>l's] errand +[<span about="#mwt23" typeof="mw:Transclusion" data-mw='{"parts":[{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"foo"}},"i":0}}]}'>foo</span>l's errand] +[url=<span about="#mwt24" typeof="mw:Transclusion" data-mw='{"parts":[{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"foo"}},"i":0}}]}'>foo</span>] +[url=<a rel="mw:ExtLink" class="external free" href="http://example.com">http://example.com</a>] [http:// bare protocols don't count]</p> !! end @@ -5833,8 +5752,7 @@ Percent encoding in external links <p><a rel="nofollow" class="external text" href="https://github.com/search?l=&q=ResourceLoader+%40wikimedia">Search</a> </p> !! html/parsoid -<p><a rel="mw:ExtLink" -href="https://github.com/search?l=&q=ResourceLoader+%40wikimedia">Search</a></p> +<p><a rel="mw:ExtLink" class="external text" href="https://github.com/search?l=&q=ResourceLoader+%40wikimedia">Search</a></p> !! end !! test @@ -5845,7 +5763,7 @@ http://example.com <p><a rel="nofollow" class="external free" href="http://example.com">http://example.com</a> </p> !! html/parsoid -<p><a rel="mw:ExtLink" href="http://example.com">http://example.com</a></p> +<p><a rel="mw:ExtLink" class="external free" href="http://example.com">http://example.com</a></p> !! end !! test @@ -5877,14 +5795,14 @@ http://example.com/a)b </p><p><a rel="nofollow" class="external text" href="http://example.com)">foo</a> </p> !! html/parsoid -<p><a rel="mw:ExtLink" href="http://example.com">http://example.com</a>)</p> -<p><a rel="mw:ExtLink" href="http://example.com/test">http://example.com/test</a>)</p> -<p><a rel="mw:ExtLink" href="http://example.com/(test)">http://example.com/(test)</a></p> -<p><a rel="mw:ExtLink" href="http://example.com/((test)">http://example.com/((test)</a></p> -<p>(<a rel="mw:ExtLink" href="http://example.com/(test))">http://example.com/(test))</a></p> -<p>(<a rel="mw:ExtLink" href="http://example.com/(test)))))">http://example.com/(test)))))</a></p> -<p><a rel="mw:ExtLink" href="http://example.com/a)b">http://example.com/a)b</a></p> -<p><a rel="mw:ExtLink" href="http://example.com)">foo</a></p> +<p><a rel="mw:ExtLink" class="external free" href="http://example.com">http://example.com</a>)</p> +<p><a rel="mw:ExtLink" class="external free" href="http://example.com/test">http://example.com/test</a>)</p> +<p><a rel="mw:ExtLink" class="external free" href="http://example.com/(test)">http://example.com/(test)</a></p> +<p><a rel="mw:ExtLink" class="external free" href="http://example.com/((test)">http://example.com/((test)</a></p> +<p>(<a rel="mw:ExtLink" class="external free" href="http://example.com/(test))">http://example.com/(test))</a></p> +<p>(<a rel="mw:ExtLink" class="external free" href="http://example.com/(test)))))">http://example.com/(test)))))</a></p> +<p><a rel="mw:ExtLink" class="external free" href="http://example.com/a)b">http://example.com/a)b</a></p> +<p><a rel="mw:ExtLink" class="external text" href="http://example.com)">foo</a></p> !! end !! test @@ -5898,9 +5816,9 @@ Parenthesis in external links, w/ transclusion or comment </p><p>(<a rel="nofollow" class="external free" href="http://example.com">http://example.com</a>) </p> !! html/parsoid -<p>(<a typeof="mw:ExpandedAttrs" about="#mwt2" rel="mw:ExtLink" href="http://example.com/hi" data-parsoid='{"stx":"url","a":{"href":"http://example.com/hi"},"sa":{"href":"http://example.com/{{echo|hi}}"}}' data-mw='{"attribs":[[{"txt":"href"},{"html":"http://example.com/<span about=\"#mwt1\" typeof=\"mw:Transclusion\" data-parsoid='{\"pi\":[[{\"k\":\"1\"}]],\"dsr\":[20,31,null,null]}' data-mw='{\"parts\":[{\"template\":{\"target\":{\"wt\":\"echo\",\"href\":\"./Template:Echo\"},\"params\":{\"1\":{\"wt\":\"hi\"}},\"i\":0}}]}'>hi</span>"}]]}'>http://example.com/hi</a>)</p> +<p>(<a typeof="mw:ExpandedAttrs" about="#mwt2" rel="mw:ExtLink" class="external free" href="http://example.com/hi" data-parsoid='{"stx":"url","a":{"href":"http://example.com/hi"},"sa":{"href":"http://example.com/{{echo|hi}}"}}' data-mw='{"attribs":[[{"txt":"href"},{"html":"http://example.com/<span about=\"#mwt1\" typeof=\"mw:Transclusion\" data-parsoid='{\"pi\":[[{\"k\":\"1\"}]],\"dsr\":[20,31,null,null]}' data-mw='{\"parts\":[{\"template\":{\"target\":{\"wt\":\"echo\",\"href\":\"./Template:Echo\"},\"params\":{\"1\":{\"wt\":\"hi\"}},\"i\":0}}]}'>hi</span>"}]]}'>http://example.com/hi</a>)</p> -<p>(<a rel="mw:ExtLink" href="http://example.com" data-parsoid='{"stx":"url","a":{"href":"http://example.com"},"sa":{"href":"http://example.com<!-- hi -->"}}'>http://example.com</a>)</p> +<p>(<a rel="mw:ExtLink" class="external free" href="http://example.com" data-parsoid='{"stx":"url","a":{"href":"http://example.com"},"sa":{"href":"http://example.com<!-- hi -->"}}'>http://example.com</a>)</p> !! end !! test @@ -5935,11 +5853,11 @@ parsoid=html2wt !! wikitext [[Foo|Bar]] [[Foo|Bar]] -[[wikipedia:Foo|Bar]] -[[wikipedia:Foo|Bar]] +[[:en:Foo|Bar]] +[[:en:Foo|Bar]] -[[wikipedia:European_Robin|European Robin]] -[[wikipedia:European_Robin|European Robin]] +[[:en:European_Robin|European Robin]] +[[:en:European_Robin|European Robin]] !! end !! test @@ -6104,15 +6022,15 @@ A table with no data (take 2) A table with nothing but a caption !! wikitext {| -|+ caption +|+caption |} !! html/php <table> -<caption> caption +<caption>caption </caption><tr><td></td></tr></table> !! html/parsoid -<table><caption> caption</caption></table> +<table><caption>caption</caption></table> !! end !! test @@ -6121,14 +6039,14 @@ A table with caption with default-spaced attributes and a table row {| |+ style="color: red;" | caption1 |- -| foo +|foo |} !! html <table> -<caption style="color: red;"> caption1 +<caption style="color: red;">caption1 </caption> <tr> -<td> foo +<td>foo </td></tr></table> !! end @@ -6138,18 +6056,18 @@ A table with captions with non-default spaced attributes and a table row !! wikitext {| |+style="color: red;"|caption2 -|+ style="color: red;"| caption3 +|+ style="color: red;"|caption3 |- -| foo +|foo |} !! html <table> <caption style="color: red;">caption2 </caption> -<caption style="color: red;"> caption3 +<caption style="color: red;">caption3 </caption> <tr> -<td> foo +<td>foo </td></tr></table> !! end @@ -6158,23 +6076,23 @@ A table with captions with non-default spaced attributes and a table row Table td-cell syntax variations !! wikitext {| -| foo bar foo | baz -| foo bar foo || baz -| style='color:red;' | baz -| style='color:red;' || baz +|foo bar foo|baz +|foo bar foo||baz +|style='color:red;'|baz +|style='color:red;'||baz |} !! html <table> <tr> -<td> baz +<td>baz </td> -<td> foo bar foo </td> -<td> baz +<td>foo bar foo</td> +<td>baz </td> -<td style="color:red;"> baz +<td style="color:red;">baz </td> -<td> style='color:red;' </td> -<td> baz +<td>style='color:red;'</td> +<td>baz </td></tr></table> !! end @@ -6183,19 +6101,19 @@ Table td-cell syntax variations Simple table !! wikitext {| -| 1 || 2 +|1||2 |- -| 3 || 4 +|3||4 |} !! html <table> <tr> -<td> 1 </td> -<td> 2 +<td>1</td> +<td>2 </td></tr> <tr> -<td> 3 </td> -<td> 4 +<td>3</td> +<td>4 </td></tr></table> !! end @@ -6204,17 +6122,17 @@ Simple table Simple table but with multiple dashes for row wikitext !! wikitext {| -| foo +|foo |----- -| bar +|bar |} !! html <table> <tr> -<td> foo +<td>foo </td></tr> <tr> -<td> bar +<td>bar </td></tr></table> !! end @@ -6225,67 +6143,67 @@ Multiplication table {| border="1" cellpadding="2" |+Multiplication table |- -! × !! 1 !! 2 !! 3 +!×!!1!!2!!3 |- -! 1 -| 1 || 2 || 3 +!1 +|1||2||3 |- -! 2 -| 2 || 4 || 6 +!2 +|2||4||6 |- -! 3 -| 3 || 6 || 9 +!3 +|3||6||9 |- -! 4 -| 4 || 8 || 12 +!4 +|4||8||12 |- -! 5 -| 5 || 10 || 15 +!5 +|5||10||15 |} !! html <table border="1" cellpadding="2"> <caption>Multiplication table </caption> <tr> -<th> × </th> -<th> 1 </th> -<th> 2 </th> -<th> 3 +<th>×</th> +<th>1</th> +<th>2</th> +<th>3 </th></tr> <tr> -<th> 1 +<th>1 </th> -<td> 1 </td> -<td> 2 </td> -<td> 3 +<td>1</td> +<td>2</td> +<td>3 </td></tr> <tr> -<th> 2 +<th>2 </th> -<td> 2 </td> -<td> 4 </td> -<td> 6 +<td>2</td> +<td>4</td> +<td>6 </td></tr> <tr> -<th> 3 +<th>3 </th> -<td> 3 </td> -<td> 6 </td> -<td> 9 +<td>3</td> +<td>6</td> +<td>9 </td></tr> <tr> -<th> 4 +<th>4 </th> -<td> 4 </td> -<td> 8 </td> -<td> 12 +<td>4</td> +<td>8</td> +<td>12 </td></tr> <tr> -<th> 5 +<th>5 </th> -<td> 5 </td> -<td> 10 </td> -<td> 15 +<td>5</td> +<td>10</td> +<td>15 </td></tr></table> !! end @@ -6294,13 +6212,13 @@ Multiplication table Accept "||" in table headings !! wikitext {| -!h1 || h2 +!h1||h2 |} !! html <table> <tr> -<th>h1 </th> -<th> h2 +<th>h1</th> +<th>h2 </th></tr></table> !! end @@ -6309,18 +6227,18 @@ Accept "||" in table headings Accept "!!" in table data !! wikitext {| -| Foo!! || +|Foo!!|| |} !! html <table> <tr> -<td> Foo!! </td> +<td>Foo!!</td> <td> </td></tr></table> !! html/parsoid <table> -<tbody><tr data-parsoid='{"autoInsertedEnd":true,"autoInsertedStart":true}'><td data-parsoid='{"autoInsertedEnd":true}'> Foo!! </td><td data-parsoid='{"stx_v":"row","autoInsertedEnd":true}'></td></tr> +<tbody><tr data-parsoid='{"autoInsertedEnd":true,"autoInsertedStart":true}'><td data-parsoid='{"autoInsertedEnd":true}'> Foo!! </td><td data-parsoid='{"stx":"row","autoInsertedEnd":true}'></td></tr> </tbody></table> !! end @@ -6328,13 +6246,13 @@ Accept "!!" in table data Accept "||" in indented table headings !! wikitext :{| -!h1 || h2 +!h1||h2 |} !! html <dl><dd><table> <tr> -<th>h1 </th> -<th> h2 +<th>h1</th> +<th>h2 </th></tr></table></dd></dl> !! end @@ -6386,15 +6304,12 @@ Accept "!!" in table data of mixed wikitext / html syntax !a <tr><td>b!!c</td></tr> |} -!! html+tidy +!! html/php+tidy <table> -<tr> -<th>a</th> -</tr> -<tr> -<td>b!!c</td> -</tr> -</table> +<tbody><tr> +<th>a +</th></tr><tr><td>b!!c</td></tr> +</tbody></table> !! html/parsoid <table> <tbody><tr><th>a</th></tr> @@ -6412,9 +6327,9 @@ Accept empty attributes in td/th cells (td/th cells starting with leading ||) !! html <table> <tr> -<th> h1 +<th>h1 </th> -<td> a +<td>a </td></tr></table> !! end @@ -6424,13 +6339,13 @@ Accept "| !" at start of line in tables (ignore !-attribute) !! wikitext {| |- -| !style="color:red" | bar +|!style="color:red"|bar |} !! html <table> <tr> -<td> bar +<td>bar </td></tr></table> !!end @@ -6443,8 +6358,8 @@ Allow +/- in 2nd and later cells in a row, in 1st cell when td-attrs are present |style='color:red;'|+1 |style='color:blue;'|-1 |- -| 1 || 2 || 3 -| 1 ||+2 ||-3 +|1||2||3 +|1||+2||-3 |- | +1 | -1 @@ -6458,18 +6373,18 @@ Allow +/- in 2nd and later cells in a row, in 1st cell when td-attrs are present <td style="color:blue;">-1 </td></tr> <tr> -<td> 1 </td> -<td> 2 </td> -<td> 3 +<td>1</td> +<td>2</td> +<td>3 </td> -<td> 1 </td> -<td>+2 </td> +<td>1</td> +<td>+2</td> <td>-3 </td></tr> <tr> -<td> +1 +<td>+1 </td> -<td> -1 +<td>-1 </td></tr></table> !!end @@ -6478,26 +6393,26 @@ Allow +/- in 2nd and later cells in a row, in 1st cell when td-attrs are present Table rowspan !! wikitext {| border=1 -| Cell 1, row 1 -|rowspan=2| Cell 2, row 1 (and 2) -| Cell 3, row 1 +|Cell 1, row 1 +|rowspan=2|Cell 2, row 1 (and 2) +|Cell 3, row 1 |- -| Cell 1, row 2 -| Cell 3, row 2 +|Cell 1, row 2 +|Cell 3, row 2 |} !! html <table border="1"> <tr> -<td> Cell 1, row 1 +<td>Cell 1, row 1 </td> -<td rowspan="2"> Cell 2, row 1 (and 2) +<td rowspan="2">Cell 2, row 1 (and 2) </td> -<td> Cell 3, row 1 +<td>Cell 3, row 1 </td></tr> <tr> -<td> Cell 1, row 2 +<td>Cell 1, row 2 </td> -<td> Cell 3, row 2 +<td>Cell 3, row 2 </td></tr></table> !! end @@ -6518,7 +6433,7 @@ Nested table !! html <table border="1"> <tr> -<td> α +<td>α </td> <td> <table bgcolor="#ABCDEF" border="2"> @@ -6563,7 +6478,7 @@ Table cell attributes: Pipes protected by nowikis should be treated as a plain c </td> <td title="foo|">bar </td> -<td> title="foo|" bar +<td>title="foo|" bar </td></tr></table> !! html/parsoid @@ -6596,24 +6511,24 @@ parsoid=wt2html,html2html !! html/parsoid <table><tbody> <tr> -<td data-parsoid='{"startTagSrc":"| ","attrSepSrc":"|","autoInsertedEnd":true}'>[<a rel="mw:ExtLink" href="ftp://%7Cx" data-parsoid='{"stx":"url","a":{"href":"ftp://%7Cx"},"sa":{"href":"ftp://|x"}}'>ftp://%7Cx</a></td><td data-parsoid='{"stx_v":"row","autoInsertedEnd":true}'>]" onmouseover="alert(document.cookie)">test</td></tr></tbody></table> +<td data-parsoid='{"startTagSrc":"| ","attrSepSrc":"|","autoInsertedEnd":true}'>[<a rel="mw:ExtLink" class="external free" href="ftp://%7Cx" data-parsoid='{"stx":"url","a":{"href":"ftp://%7Cx"},"sa":{"href":"ftp://|x"}}'>ftp://%7Cx</a></td><td data-parsoid='{"stx":"row","autoInsertedEnd":true}'>]" onmouseover="alert(document.cookie)">test</td></tr></tbody></table> !! end !! test Element attributes with double ! should not be broken up by <th> !! wikitext {| -! hi <div class="!!">ha</div> ho +!hi <div class="!!">ha</div> ho |} !! html/php <table> <tr> -<th> hi <div class="!!">ha</div> ho +<th>hi <div class="!!">ha</div> ho </th></tr></table> !! html/parsoid <table> -<tbody><tr><th> hi <div class="!!" data-parsoid='{"stx":"html"}'>ha</div> ho</th></tr> +<tbody><tr><th>hi <div class="!!" data-parsoid='{"stx":"html"}'>ha</div> ho</th></tr> </tbody></table> !! end @@ -6621,17 +6536,17 @@ Element attributes with double ! should not be broken up by <th> ! and || in element attributes should not be parsed as <th>/<td> !! wikitext {| -| <div style="color: red !important;" data-contrived="put this here ||">hi</div> +|<div style="color: red !important;" data-contrived="put this here ||">hi</div> |} !! html/php <table> <tr> -<td> <div style="color: red !important;" data-contrived="put this here ||">hi</div> +<td><div style="color: red !important;" data-contrived="put this here ||">hi</div> </td></tr></table> !! html/parsoid <table> -<tbody><tr><td> <div style="color: red !important;" data-contrived="put this here ||" data-parsoid='{"stx":"html"}'>hi</div></td></tr> +<tbody><tr><td><div style="color: red !important;" data-contrived="put this here ||" data-parsoid='{"stx":"html"}'>hi</div></td></tr> </tbody></table> !! end @@ -6642,18 +6557,18 @@ Element attributes with double ! should not be broken up by <th> parsoid=wt2html !! wikitext {| -| style="color: red !important;" data-contrived="put this here ||" | foo +|style="color: red !important;" data-contrived="put this here ||"|foo |} !! html/php <table> <tr> -<td> style="color: red !important;" data-contrived="put this here </td> -<td> foo +<td>style="color: red !important;" data-contrived="put this here</td> +<td>foo </td></tr></table> !! html/parsoid <table> -<tbody><tr><td> style="color: red !important;" data-contrived="put this here </td><td data-parsoid='{"stx_v":"row","a":{"\"":null},"sa":{"\"":""},"autoInsertedEnd":true}'> foo</td></tr> +<tbody><tr><td>style="color: red !important;" data-contrived="put this here</td><td data-parsoid='{"stx":"row","a":{"\"":null},"sa":{"\"":""},"autoInsertedEnd":true}'>foo</td></tr> </tbody></table> !! end @@ -6685,9 +6600,9 @@ Don't break on | in extension attribute in template <references /> !! html/parsoid -<p><span about="#mwt2" class="mw-ref" id="cite_ref-hi.7Cho_1-0" rel="dc:references" typeof="mw:Transclusion mw:Extension/ref" data-parsoid='{"pi":[[{"k":"1"}]]}' data-mw='{"parts":[{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"<ref name=\"hi|ho\">ha</ref>"}},"i":0}}]}'><a href="./Main_Page#cite_note-hi.7Cho-1" style="counter-reset: mw-Ref 1;"><span class="mw-reflink-text">[1]</span></a></span></p> +<p><sup about="#mwt2" class="mw-ref" id="cite_ref-hi|ho_1-0" rel="dc:references" typeof="mw:Transclusion mw:Extension/ref" data-mw='{"parts":[{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"<ref name=\"hi|ho\">ha</ref>"}},"i":0}}]}'><a href="./Main_Page#cite_note-hi|ho-1" style="counter-reset: mw-Ref 1;"><span class="mw-reflink-text">[1]</span></a></sup></p> -<ol class="mw-references references" typeof="mw:Extension/references" about="#mwt5" data-mw='{"name":"references","attrs":{}}'><li about="#cite_note-hi.7Cho-1" id="cite_note-hi.7Cho-1"><a href="./Main_Page#cite_ref-hi.7Cho_1-0" rel="mw:referencedBy"><span class="mw-linkback-text">↑ </span></a> <span id="mw-reference-text-cite_note-hi.7Cho-1" class="mw-reference-text">ha</span></li></ol> +<ol class="mw-references references" typeof="mw:Extension/references" about="#mwt5" data-mw='{"name":"references","attrs":{}}'><li about="#cite_note-hi|ho-1" id="cite_note-hi|ho-1"><a href="./Main_Page#cite_ref-hi|ho_1-0" rel="mw:referencedBy"><span class="mw-linkback-text">↑ </span></a> <span id="mw-reference-text-cite_note-hi|ho-1" class="mw-reference-text">ha</span></li></ol> !! end ## We don't support roundtripping of these attributes in Parsoid. @@ -6699,22 +6614,22 @@ Invalid text in table attributes should be discarded parsoid=wt2html !! wikitext {| <span>boo</span> style='border:1px solid black' -| <span>boo</span> style='color:blue' | 1 -|<span>boo</span> style='color:blue'| 2 +| <span>boo</span> style='color:blue' |1 +|<span>boo</span> style='color:blue'|2 |} !! html/php <table style="border:1px solid black"> <tr> -<td style="color:blue"> 1 +<td style="color:blue">1 </td> -<td style="color:blue"> 2 +<td style="color:blue">2 </td></tr></table> !! html/parsoid <table style="border:1px solid black"> <tr> -<td style="color:blue"> 1</td> -<td style="color:blue"> 2</td> +<td style="color:blue">1</td> +<td style="color:blue">2</td> </tr> </table> !! end @@ -6759,7 +6674,7 @@ parsoid={ </td> <td style="color:red;">Foo </td> -<td> style="color:red;"</td> +<td>style="color:red;"</td> <td>Bar </td> <td style="color:red;">Foo @@ -6860,7 +6775,7 @@ T107652: <ref>s in templates that also generate table cell attributes should be <references /> !! html/parsoid <table> -<tbody><tr><td style="background:#f9f9f9;" typeof="mw:Transclusion" about="#mwt1" data-mw='{"parts":["|",{"template":{"target":{"wt":"table_attribs_7","href":"./Template:Table_attribs_7"},"params":{},"i":0}}]}'>Foo<span class="mw-ref" id="cite_ref-1" rel="dc:references" data-mw='{"name":"ref","body":{"id":"mw-reference-text-cite_note-1"},"attrs":{}}'><a href="./Main_Page#cite_note-1" style="counter-reset: mw-Ref 1;"><span class="mw-reflink-text">[1]</span></a></span></td></tr> +<tbody><tr><td style="background:#f9f9f9;" typeof="mw:Transclusion" about="#mwt1" data-mw='{"parts":["|",{"template":{"target":{"wt":"table_attribs_7","href":"./Template:Table_attribs_7"},"params":{},"i":0}}]}'>Foo<sup class="mw-ref" id="cite_ref-1" rel="dc:references" typeof="mw:Extension/ref" data-mw='{"name":"ref","body":{"id":"mw-reference-text-cite_note-1"},"attrs":{}}'><a href="./Main_Page#cite_note-1" style="counter-reset: mw-Ref 1;"><span class="mw-reflink-text">[1]</span></a></s></td></tr> </tbody></table> <ol class="mw-references references" typeof="mw:Extension/references" about="#mwt5" data-mw='{"name":"references","attrs":{}}'><li about="#cite_note-1" id="cite_note-1"><a href="./Main_Page#cite_ref-1" rel="mw:referencedBy"><span class="mw-linkback-text">↑ </span></a> <span id="mw-reference-text-cite_note-1" class="mw-reference-text" data-parsoid="{}">foo</span></li></ol> !! end @@ -6873,14 +6788,14 @@ parsoid=wt2html,html2html {| |- -! foo +!foo |} !! html/* <table> <tr> -<th> foo +<th>foo </th></tr></table> !! end @@ -6893,14 +6808,14 @@ parsoid=wt2html,html2html {| |- -| foo +|foo |} !! html/* <table> <tr> -<td> foo +<td>foo </td></tr></table> !! end @@ -6911,17 +6826,17 @@ Table attributes with empty value parsoid=wt2html,html2html !! wikitext {| -| style=| hello +| style=|hello |} !! html/php <table> <tr> -<td style=""> hello +<td style="">hello </td></tr></table> !! html/parsoid <table> -<tbody><tr><td style=""> hello</td></tr> +<tbody><tr><td style="">hello</td></tr> </tbody></table> !! end @@ -6930,7 +6845,7 @@ Wikitext table with a lot of comments !! wikitext {| <!-- c0 --> -| foo +|foo <!-- c1 --> |-<!-- c2 --> <!-- c3 --> @@ -6940,7 +6855,7 @@ Wikitext table with a lot of comments !! html <table> <tr> -<td> foo +<td>foo </td></tr> <tr> <td> @@ -6953,18 +6868,18 @@ Wikitext table comments represented in parsoid dom !! wikitext {|<!--c1--><!--c2--> |-<!--c3--> -| x +|x |} !! html/php+tidy <table> -<tr> -<td>x</td> -</tr> -</table> + +<tbody><tr> +<td>x +</td></tr></tbody></table> !! html/parsoid <table><!--c1--><!--c2--> <tbody><tr data-parsoid='{"startTagSrc":"|-","autoInsertedEnd":true}'><!--c3--> -<td data-parsoid='{"autoInsertedEnd":true}'> x</td></tr> +<td data-parsoid='{"autoInsertedEnd":true}'>x</td></tr> </tbody></table> !! end @@ -6990,14 +6905,14 @@ Table cell with a single comment !! wikitext {| | <!-- c1 --> -| a +|a |} !! html <table> <tr> <td> </td> -<td> a +<td>a </td></tr></table> !! end @@ -7008,21 +6923,21 @@ Table-cell after a comment-only-empty-line {| |a <!--c1--> -<!--c2-->| b +<!--c2-->|b |} !! html <table> <tr> <td>a </td> -<td> b +<td>b </td></tr></table> !! html/parsoid <table> <tbody><tr data-parsoid='{"autoInsertedEnd":true,"autoInsertedStart":true}'><td data-parsoid='{"autoInsertedEnd":true}'>a</td> <!--c1--> -<!--c2--><td data-parsoid='{"autoInsertedEnd":true}'> b</td></tr> +<!--c2--><td data-parsoid='{"autoInsertedEnd":true}'>b</td></tr> </tbody></table> !! end @@ -7031,21 +6946,21 @@ Table-cell after a comment-only-empty-line Build table with {{!}} !! wikitext {{{!}} class="wikitable" -! header -! second header +!header +!second header {{!}}- style="color:red;" -{{!}} data {{!}}{{!}} style="color:red;" {{!}} second data +{{!}}data{{!}}{{!}} style="color:red;" {{!}}second data {{!}}} !! html <table class="wikitable"> <tr> -<th> header +<th>header </th> -<th> second header +<th>second header </th></tr> <tr style="color:red;"> -<td> data </td> -<td style="color:red;"> second data +<td>data</td> +<td style="color:red;">second data </td></tr></table> !! end @@ -7054,33 +6969,33 @@ Build table with {{!}} Build table with pipe as data !! wikitext {| class="wikitable" -! header -! second header +!header +!second header |- style="color:red;" -| data || style="color:red;" | second data +|data|| style="color:red;" |second data |- -| style="color:red;" | data with | || style="color:red;" | second data with | +| style="color:red;" |data with | || style="color:red;" | second data with | |- -|| data with | ||| second data with | +||data with | |||second data with | |} !! html <table class="wikitable"> <tr> -<th> header +<th>header </th> -<th> second header +<th>second header </th></tr> <tr style="color:red;"> -<td> data </td> -<td style="color:red;"> second data +<td>data</td> +<td style="color:red;">second data </td></tr> <tr> -<td style="color:red;"> data with | </td> -<td style="color:red;"> second data with | +<td style="color:red;">data with |</td> +<td style="color:red;">second data with | </td></tr> <tr> -<td> data with | </td> -<td> second data with | +<td>data with |</td> +<td>second data with | </td></tr></table> !! end @@ -7089,25 +7004,25 @@ Build table with pipe as data Build table with wikilink !! wikitext {| class="wikitable" -! header || second header +!header||second header |- style="color:red;" -| data [[Main Page|linktext]] || second data [[Main Page|linktext]] +|data [[Main Page|linktext]]||second data [[Main Page|linktext]] |- -| data || second data [[Main Page|link|text with pipe]] +|data||second data [[Main Page|link|text with pipe]] |} !! html <table class="wikitable"> <tr> -<th> header </th> -<th> second header +<th>header</th> +<th>second header </th></tr> <tr style="color:red;"> -<td> data <a href="/wiki/Main_Page" title="Main Page">linktext</a> </td> -<td> second data <a href="/wiki/Main_Page" title="Main Page">linktext</a> +<td>data <a href="/wiki/Main_Page" title="Main Page">linktext</a></td> +<td>second data <a href="/wiki/Main_Page" title="Main Page">linktext</a> </td></tr> <tr> -<td> data </td> -<td> second data <a href="/wiki/Main_Page" title="Main Page">link|text with pipe</a> +<td>data</td> +<td>second data <a href="/wiki/Main_Page" title="Main Page">link|text with pipe</a> </td></tr></table> !! end @@ -7130,7 +7045,7 @@ Wikitext table with html-syntax row !! end !! test -Implicit <td> after a |- +Fostered content in tables: Plain text !! options parsoid=wt2html,html2html !! wikitext @@ -7145,7 +7060,10 @@ a </table> !! html/php+tidy -<p>a</p> + + +a +<table></table> !! html/parsoid <p data-parsoid='{"fostered":true,"autoInsertedEnd":true}'>a</p><table> <tbody><tr data-parsoid='{"startTagSrc":"|-","autoInsertedEnd":true}'> @@ -7154,7 +7072,7 @@ a !! end !! test -Lists should be recognized in an implicit <td> context +Fostered content in tables: Lists !! options parsoid=wt2html,html2html !! wikitext @@ -7169,9 +7087,10 @@ parsoid=wt2html,html2html </table> !! html/php+tidy -<ul> -<li>a</li> -</ul> +<ul><li>a</li></ul><table> + + +</table> !! html/parsoid <ul data-parsoid='{"fostered":true,"autoInsertedEnd":true}'><li>a</li></ul><table> <tbody><tr data-parsoid='{"startTagSrc":"|-","autoInsertedEnd":true}'> @@ -7180,24 +7099,24 @@ parsoid=wt2html,html2html !! end !! test -Table cells not properly parsed in an implicit-td context +Template generated table cell with attributes !! wikitext {| |- -{{table_attribs_4}} || a || b +{{table_attribs_4}} ||a||b |} !! html/php+tidy <table> -<tr> + +<tbody><tr> <td style="background-color:#DC241f;" width="10px"></td> <td>a</td> -<td>b</td> -</tr> -</table> +<td>b +</td></tr></tbody></table> !! html/parsoid <table> <tbody><tr data-parsoid='{"startTagSrc":"|-","autoInsertedEnd":true}'> -<td style="background-color:#DC241f;" width="10px" about="#mwt1" typeof="mw:Transclusion" data-parsoid='{"autoInsertedEnd":true,"pi":[[]]}' data-mw='{"parts":[{"template":{"target":{"wt":"table_attribs_4","href":"./Template:Table_attribs_4"},"params":{},"i":0}}," || a || b"]}'> </td><td about="#mwt1"> a </td><td about="#mwt1"> b</td></tr> +<td style="background-color:#DC241f;" width="10px" about="#mwt1" typeof="mw:Transclusion" data-parsoid='{"autoInsertedEnd":true,"pi":[[]]}' data-mw='{"parts":[{"template":{"target":{"wt":"table_attribs_4","href":"./Template:Table_attribs_4"},"params":{},"i":0}}," ||a||b"]}'></td><td about="#mwt1">a</td><td about="#mwt1">b</td></tr> !! end !! test @@ -7214,17 +7133,14 @@ parsoid=wt2html,wt2wt |}<b>quux</b> !! html+tidy <table> -<tr> -<td>foo</td> -</tr> -</table> -<p>bar</p> -<table> -<tr> -<td>baz</td> -</tr> -</table> -<p><b>quux</b></p> +<tbody><tr> +<td>foo +</td></tr></tbody></table><p> bar +</p><table> +<tbody><tr> +<td>baz +</td></tr></tbody></table><p><b>quux</b> +</p> !! end !! test @@ -7265,17 +7181,17 @@ Parsoid: Row-syntax table headings followed by comment & table cells parsoid=wt2html,wt2wt !! wikitext {| -! foo || bar -<!-- foo --> || baz || quux +!foo||bar +<!-- foo --> ||baz||quux |} !! html/php <table> <tr> -<th> foo </th> -<th> bar +<th>foo</th> +<th>bar </th> -<td> baz </td> -<td> quux +<td>baz</td> +<td>quux </td></tr></table> !! html/parsoid @@ -7296,12 +7212,11 @@ foo |} !!html/php+tidy <table class="foo"> -<tr> +<tbody><tr> <td class="bar"> -<p>foo</p> -</td> -</tr> -</table> +<p>foo +</p> +</td></tr></tbody></table> !!html/parsoid <table class="foo"> <tr> @@ -7375,24 +7290,28 @@ parsoid=html2wt |} !! html/php+tidy <table> -<caption>Test</caption> -<tr> -<th>Month</th> -<th>Savings</th> -</tr> +<caption>Test +</caption> +<tbody><tr> +<th>Month +</th> +<th>Savings +</th></tr> <tr> -<td>January</td> -<td>$100</td> -</tr> +<td>January +</td> +<td>$100 +</td></tr> <tr> -<td>February</td> -<td>$80</td> -</tr> +<td>February +</td> +<td>$80 +</td></tr> <tr> -<td>Sum</td> -<td>$180</td> -</tr> -</table> +<td>Sum +</td> +<td>$180 +</td></tr></tbody></table> !! end # T137406: No whitespace in the HTML @@ -7795,13 +7714,15 @@ Link with multiple pipes !! test Anchor containing a #. (T65430) +!! config +wgFragmentMode=[ 'html5', 'legacy' ] !! wikitext [[Main Page#And#Link]] !! html/php -<p><a href="/wiki/Main_Page#And.23Link" title="Main Page">Main Page#And#Link</a> +<p><a href="/wiki/Main_Page#And#Link" title="Main Page">Main Page#And#Link</a> </p> !! html/parsoid -<p><a rel="mw:WikiLink" href="./Main_Page#And.23Link" title="Main Page" data-parsoid='{"stx":"simple","a":{"href":"./Main_Page#And.23Link"},"sa":{"href":"Main Page#And#Link"}}'>Main Page#And#Link</a></p> +<p><a rel="mw:WikiLink" href="./Main_Page#And#Link" title="Main Page" data-parsoid='{"stx":"simple","a":{"href":"./Main_Page#And#Link"},"sa":{"href":"Main Page#And#Link"}}'>Main Page#And#Link</a></p> !! end !! test @@ -7919,13 +7840,27 @@ Link containing % as a double hex sequence interpreted to hex sequence ## Example for such a section: == < == !! test Link containing "#<" and "#>" % as a hex sequences- these are valid section anchors +!! config +wgFragmentMode=[ 'html5', 'legacy' ] !! wikitext [[%23%3c]][[%23%3e]] !! html/php -<p><a href="#.3C">#<</a><a href="#.3E">#></a> +<p><a href="#<">#<</a><a href="#>">#></a> </p> !! html/parsoid -<p><a rel="mw:WikiLink" href="./Main_Page#.3C" title="Main Page" data-parsoid='{"stx":"simple","a":{"href":"./Main_Page#.3C"},"sa":{"href":"%23%3c"}}'>#<</a><a rel="mw:WikiLink" href="./Main_Page#.3E" title="Main Page" data-parsoid='{"stx":"simple","a":{"href":"./Main_Page#.3E"},"sa":{"href":"%23%3e"}}'>#></a></p> +<p><a rel="mw:WikiLink" href="./Main_Page#<" title="Main Page" data-parsoid='{"stx":"simple","a":{"href":"./Main_Page#<"},"sa":{"href":"%23%3c"}}'>#<</a><a rel="mw:WikiLink" href="./Main_Page#>" title="Main Page" data-parsoid='{"stx":"simple","a":{"href":"./Main_Page#>"},"sa":{"href":"%23%3e"}}'>#></a></p> +!! end + +## Example for such a section: == < == +!! test +Link containing "#<" and "#>" % as a hex sequences- these are valid section anchors (legacy) +!! config +wgFragmentMode=[ 'legacy' ] +!! wikitext +[[%23%3c]][[%23%3e]] +!! html/php +<p><a href="#.3C">#<</a><a href="#.3E">#></a> +</p> !! end !! test @@ -7987,7 +7922,7 @@ Link containing double quotes and spaces <p><a href="/index.php?title=Cool_%22Gator%22&action=edit&redlink=1" class="new" title="Cool "Gator" (page does not exist)">Cool "Gator"</a> </p> !! html/parsoid -<p><a rel="mw:WikiLink" href="./Cool_%22Gator%22" title='Cool "Gator"'>Cool "Gator"</a></p> +<p><a rel="mw:WikiLink" href='./Cool_"Gator"' title='Cool "Gator"'>Cool "Gator"</a></p> !! end !! test @@ -7995,7 +7930,7 @@ File containing double quotes and spaces !! wikitext [[File:Cool "Gator".png]] !! html/parsoid -<p><span class="mw-default-size" typeof="mw:Error mw:Image" data-parsoid='{"optList":[]}' data-mw='{"errors":[{"key":"apierror-filedoesnotexist","message":"This image does not exist."}]}'><a href="./File:Cool_%22Gator%22.png" data-parsoid='{"a":{"href":"./File:Cool_%22Gator%22.png"},"sa":{"href":"File:Cool \"Gator\".png"}}'><img resource='./File:Cool_"Gator".png' src="./Special:FilePath/Cool_%22Gator%22.png" height="220" width="220" data-parsoid='{"a":{"resource":"./File:Cool_\"Gator\".png","height":"220","width":"220","src":"./Special:FilePath/Cool_%22Gator%22.png"},"sa":{"resource":"File:Cool \"Gator\".png","src":"./Special:FilePath/Cool_\"Gator\".png"}}'/></a></span></p> +<p><figure-inline class="mw-default-size" typeof="mw:Error mw:Image" data-parsoid='{"optList":[]}' data-mw='{"errors":[{"key":"apierror-filedoesnotexist","message":"This image does not exist."}]}'><a href="./File:Cool_%22Gator%22.png" data-parsoid='{"a":{"href":"./File:Cool_%22Gator%22.png"},"sa":{"href":"File:Cool \"Gator\".png"}}'><img resource='./File:Cool_"Gator".png' src="./Special:FilePath/Cool_%22Gator%22.png" height="220" width="220" data-parsoid='{"a":{"resource":"./File:Cool_\"Gator\".png","height":"220","width":"220","src":"./Special:FilePath/Cool_%22Gator%22.png"},"sa":{"resource":"File:Cool \"Gator\".png","src":"./Special:FilePath/Cool_\"Gator\".png"}}'/></a></figure-inline></p> !! end !! test @@ -8043,7 +7978,7 @@ Link with double quotes in title part (literal) and alternate part (interpreted) </p><p><a href="/index.php?title=%27%27Pentecoste%27%27&action=edit&redlink=1" class="new" title="''Pentecoste'' (page does not exist)"><i>Pentecoste</i></a> </p> !! html/parsoid -<p><span class="mw-default-size" typeof="mw:Error mw:Image" data-mw='{"errors":[{"key":"apierror-filedoesnotexist","message":"This image does not exist."}]}'><a href="./File:Denys_Savchenko_''Pentecoste''.jpg"><img resource="./File:Denys_Savchenko_''Pentecoste''.jpg" src="./Special:FilePath/Denys_Savchenko_''Pentecoste''.jpg" height="220" width="220"/></a></span></p> +<p><figure-inline class="mw-default-size" typeof="mw:Error mw:Image" data-mw='{"errors":[{"key":"apierror-filedoesnotexist","message":"This image does not exist."}]}'><a href="./File:Denys_Savchenko_''Pentecoste''.jpg"><img resource="./File:Denys_Savchenko_''Pentecoste''.jpg" src="./Special:FilePath/Denys_Savchenko_''Pentecoste''.jpg" height="220" width="220"/></a></figure-inline></p> <p><a rel="mw:WikiLink" href="./''Pentecoste''" title="''Pentecoste''">''Pentecoste''</a></p> <p><a rel="mw:WikiLink" href="./''Pentecoste''" title="''Pentecoste''">Pentecoste</a></p> <p><a rel="mw:WikiLink" href="./''Pentecoste''" title="''Pentecoste''"><i>Pentecoste</i></a></p> @@ -8063,10 +7998,10 @@ Broken image links with HTML captions (T41700) <a href="/index.php?title=Special:Upload&wpDestFile=Nonexistent" class="new" title="File:Nonexistent">abc</a> </p> !! html/parsoid -<p><span class="mw-default-size" typeof="mw:Error mw:Image" data-parsoid='{"optList":[{"ck":"caption","ak":"<script></script>"}]}' data-mw='{"errors":[{"key":"apierror-filedoesnotexist","message":"This image does not exist."}],"caption":"&lt;script>&lt;/script>"}'><a href="./File:Nonexistent" data-parsoid='{"a":{"href":"./File:Nonexistent"},"sa":{}}'><img resource="./File:Nonexistent" src="./Special:FilePath/Nonexistent" height="220" width="220" data-parsoid='{"a":{"resource":"./File:Nonexistent","height":"220","width":"220"},"sa":{"resource":"File:Nonexistent"}}'/></a></span> -<span typeof="mw:Error mw:Image" data-parsoid='{"optList":[{"ck":"width","ak":"100x100px"},{"ck":"caption","ak":"<script></script>"}]}' data-mw='{"errors":[{"key":"apierror-filedoesnotexist","message":"This image does not exist."}],"caption":"&lt;script>&lt;/script>"}'><a href="./File:Nonexistent" data-parsoid='{"a":{"href":"./File:Nonexistent"},"sa":{}}'><img resource="./File:Nonexistent" src="./Special:FilePath/Nonexistent" height="100" width="100" data-parsoid='{"a":{"resource":"./File:Nonexistent","height":"100","width":"100"},"sa":{"resource":"File:Nonexistent"}}'/></a></span> -<span class="mw-default-size" typeof="mw:Error mw:Image" data-parsoid='{"optList":[{"ck":"caption","ak":"&lt;"}]}' data-mw='{"errors":[{"key":"apierror-filedoesnotexist","message":"This image does not exist."}],"caption":"<span typeof=\"mw:Entity\" data-parsoid='{\"src\":\"&amp;lt;\",\"srcContent\":\"&lt;\",\"dsr\":[107,111,null,null]}'>&lt;</span>"}'><a href="./File:Nonexistent" data-parsoid='{"a":{"href":"./File:Nonexistent"},"sa":{}}'><img resource="./File:Nonexistent" src="./Special:FilePath/Nonexistent" height="220" width="220" data-parsoid='{"a":{"resource":"./File:Nonexistent","height":"220","width":"220"},"sa":{"resource":"File:Nonexistent"}}'/></a></span> -<span class="mw-default-size" typeof="mw:Error mw:Image" data-parsoid='{"optList":[{"ck":"caption","ak":"a<i>b</i>c"}]}' data-mw='{"errors":[{"key":"apierror-filedoesnotexist","message":"This image does not exist."}],"caption":"a<i data-parsoid='{\"stx\":\"html\",\"dsr\":[134,142,3,4]}'>b</i>c"}'><a href="./File:Nonexistent" data-parsoid='{"a":{"href":"./File:Nonexistent"},"sa":{}}'><img resource="./File:Nonexistent" src="./Special:FilePath/Nonexistent" height="220" width="220" data-parsoid='{"a":{"resource":"./File:Nonexistent","height":"220","width":"220"},"sa":{"resource":"File:Nonexistent"}}'/></a></span></p> +<p><figure-inline class="mw-default-size" typeof="mw:Error mw:Image" data-parsoid='{"optList":[{"ck":"caption","ak":"<script></script>"}]}' data-mw='{"errors":[{"key":"apierror-filedoesnotexist","message":"This image does not exist."}],"caption":"&lt;script>&lt;/script>"}'><a href="./File:Nonexistent" data-parsoid='{"a":{"href":"./File:Nonexistent"},"sa":{}}'><img resource="./File:Nonexistent" src="./Special:FilePath/Nonexistent" height="220" width="220" data-parsoid='{"a":{"resource":"./File:Nonexistent","height":"220","width":"220"},"sa":{"resource":"File:Nonexistent"}}'/></a></figure-inline> +<figure-inline typeof="mw:Error mw:Image" data-parsoid='{"optList":[{"ck":"width","ak":"100x100px"},{"ck":"caption","ak":"<script></script>"}]}' data-mw='{"errors":[{"key":"apierror-filedoesnotexist","message":"This image does not exist."}],"caption":"&lt;script>&lt;/script>"}'><a href="./File:Nonexistent" data-parsoid='{"a":{"href":"./File:Nonexistent"},"sa":{}}'><img resource="./File:Nonexistent" src="./Special:FilePath/Nonexistent" height="100" width="100" data-parsoid='{"a":{"resource":"./File:Nonexistent","height":"100","width":"100"},"sa":{"resource":"File:Nonexistent"}}'/></a></figure-inline> +<figure-inline class="mw-default-size" typeof="mw:Error mw:Image" data-parsoid='{"optList":[{"ck":"caption","ak":"&lt;"}]}' data-mw='{"errors":[{"key":"apierror-filedoesnotexist","message":"This image does not exist."}],"caption":"<span typeof=\"mw:Entity\" data-parsoid='{\"src\":\"&amp;lt;\",\"srcContent\":\"&lt;\",\"dsr\":[107,111,null,null]}'>&lt;</span>"}'><a href="./File:Nonexistent" data-parsoid='{"a":{"href":"./File:Nonexistent"},"sa":{}}'><img resource="./File:Nonexistent" src="./Special:FilePath/Nonexistent" height="220" width="220" data-parsoid='{"a":{"resource":"./File:Nonexistent","height":"220","width":"220"},"sa":{"resource":"File:Nonexistent"}}'/></a></figure-inline> +<figure-inline class="mw-default-size" typeof="mw:Error mw:Image" data-parsoid='{"optList":[{"ck":"caption","ak":"a<i>b</i>c"}]}' data-mw='{"errors":[{"key":"apierror-filedoesnotexist","message":"This image does not exist."}],"caption":"a<i data-parsoid='{\"stx\":\"html\",\"dsr\":[134,142,3,4]}'>b</i>c"}'><a href="./File:Nonexistent" data-parsoid='{"a":{"href":"./File:Nonexistent"},"sa":{}}'><img resource="./File:Nonexistent" src="./Special:FilePath/Nonexistent" height="220" width="220" data-parsoid='{"a":{"resource":"./File:Nonexistent","height":"220","width":"220"},"sa":{"resource":"File:Nonexistent"}}'/></a></figure-inline></p> !! end !! test @@ -8077,7 +8012,7 @@ Plain link to URL <p>[<a rel="nofollow" class="external autonumber" href="http://www.example.com">[1]</a>] </p> !! html/parsoid -<p>[<a rel="mw:ExtLink" href="http://www.example.com"></a>]</p> +<p>[<a rel="mw:ExtLink" class="external autonumber" href="http://www.example.com"></a>]</p> !! end !! test @@ -8097,7 +8032,7 @@ Plain link to protocol-relative URL <p>[<a rel="nofollow" class="external autonumber" href="//www.example.com">[1]</a>] </p> !! html/parsoid -<p>[<a rel="mw:ExtLink" href="//www.example.com"></a>]</p> +<p>[<a rel="mw:ExtLink" class="external autonumber" href="//www.example.com"></a>]</p> !! end !! test @@ -8140,7 +8075,7 @@ Piped link to URL: [[http://www.example.com|an example URL]] <p>Piped link to URL: [<a rel="nofollow" class="external text" href="http://www.example.com%7Can">example URL</a>] </p> !! html/parsoid -<p>Piped link to URL: [<a rel="mw:ExtLink" href="http://www.example.com%7Can" data-parsoid='{"a":{"href":"http://www.example.com%7Can"},"sa":{"href":"http://www.example.com|an"}}'>example URL</a>]</p> +<p>Piped link to URL: [<a rel="mw:ExtLink" class="external text" href="http://www.example.com%7Can" data-parsoid='{"a":{"href":"http://www.example.com%7Can"},"sa":{"href":"http://www.example.com|an"}}'>example URL</a>]</p> !! end !! test @@ -8162,13 +8097,13 @@ parsoid=wt2html </p><p>[<a rel="nofollow" class="external free" href="http://www.example.com">http://www.example.com</a> </p> !! html/parsoid -<p about="#mwt1" typeof="mw:Transclusion" data-mw='{"parts":[{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"[http://www.example.com "},"2":{"wt":"123]"}},"i":0}}]}'>[<a rel="mw:ExtLink" href="http://www.example.com">http://www.example.com</a> </p> +<p about="#mwt1" typeof="mw:Transclusion" data-mw='{"parts":[{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"[http://www.example.com "},"2":{"wt":"123]"}},"i":0}}]}'>[<a rel="mw:ExtLink" class="external free" href="http://www.example.com">http://www.example.com</a> </p> -<p about="#mwt2" typeof="mw:Transclusion" data-mw='{"parts":[{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"[[http://www.example.com |123]]"}},"i":0}}]}'>[<a rel="mw:ExtLink" href="http://www.example.com">|123</a>]</p> +<p about="#mwt2" typeof="mw:Transclusion" data-mw='{"parts":[{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"[[http://www.example.com |123]]"}},"i":0}}]}'>[<a rel="mw:ExtLink" class="external text" href="http://www.example.com">|123</a>]</p> -<p>{{echo|[<a rel="mw:ExtLink" href="http://www.example.com" data-parsoid='{"targetOff":114,"contentOffsets":[114,118],"dsr":[90,119,24,1]}'>|123</a>}}</p> +<p>{{echo|[<a rel="mw:ExtLink" class="external text" href="http://www.example.com" data-parsoid='{"targetOff":114,"contentOffsets":[114,118],"dsr":[90,119,24,1]}'>|123</a>}}</p> -<p about="#mwt3" typeof="mw:Transclusion" data-mw='{"parts":[{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"[http://www.example.com "},"2":{"wt":"123]]"}},"i":0}}]}'>[<a rel="mw:ExtLink" href="http://www.example.com">http://www.example.com</a> </p> +<p about="#mwt3" typeof="mw:Transclusion" data-mw='{"parts":[{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"[http://www.example.com "},"2":{"wt":"123]]"}},"i":0}}]}'>[<a rel="mw:ExtLink" class="external free" href="http://www.example.com">http://www.example.com</a> </p> !! end !! test @@ -8189,11 +8124,9 @@ T2337: Escaped self-links should be bold title=[[Bug462]] !! wikitext [[Bug462]] [[Bug462]] -!! html/php +!! html/php+tidy <p><a class="mw-selflink selflink">Bug462</a> <a class="mw-selflink selflink">Bug462</a> </p> -!! html/php+tidy -<p><a class="mw-selflink selflink">Bug462</a> <a class="mw-selflink selflink">Bug462</a></p> !! html/parsoid <p><a rel="mw:WikiLink" href="./Bug462" title="Bug462">Bug462</a> <a rel="mw:WikiLink" href="./Bug462" title="Bug462">Bug462</a></p> !! end @@ -8518,6 +8451,31 @@ Aðrir mótmælenda<nowiki/>[[söfnuður]] !! end !! test +Parsoid link bracket escaping +!! options +parsoid=html2wt,html2html +!! html/parsoid +<p><a rel="mw:WikiLink" href="./Test" title="Test">Test</a></p> +<p>[<a rel="mw:WikiLink" href="./Test" title="Test">Test</a>]</p> +<p>[[<a rel="mw:WikiLink" href="./Test" title="Test">Test</a>]]</p> +<p>[[[<a rel="mw:WikiLink" href="./Test" title="Test">Test</a>]]]</p> +<p>[[[[<a rel="mw:WikiLink" href="./Test" title="Test">Test</a>]]]]</p> +<p>[[[[[<a rel="mw:WikiLink" href="./Test" title="Test">Test</a>]]]]]</p> +!! wikitext +[[Test]] + +[<nowiki/>[[Test]]] + +[[[[Test]]]] + +[[[<nowiki/>[[Test]]]]] + +[[[[[[Test]]]]]] + +[[[[[<nowiki/>[[Test]]]]]]] +!! end + +!! test Parsoid-centric test: Whitespace in ext- and wiki-links should be preserved !! wikitext [[Foo| bar]] @@ -8545,13 +8503,26 @@ Parsoid: Scoped parsing should handle mixed transclusions and plain text !! test Link with angle bracket after anchor +!! config +wgFragmentMode=[ 'html5', 'legacy' ] !! wikitext [[Foo#<bar>]] !! html/php -<p><a href="/wiki/Foo#.3Cbar.3E" title="Foo">Foo#<bar></a> +<p><a href="/wiki/Foo#<bar>" title="Foo">Foo#<bar></a> </p> !! html/parsoid -<p><a rel="mw:WikiLink" href="./Foo#.3Cbar.3E" title="Foo" data-parsoid='{"stx":"simple","a":{"href":"./Foo#.3Cbar.3E"},"sa":{"href":"Foo#<bar>"}}'>Foo#<bar></a></p> +<p><a rel="mw:WikiLink" href="./Foo#<bar>" title="Foo" data-parsoid='{"stx":"simple","a":{"href":"./Foo#<bar>"},"sa":{"href":"Foo#<bar>"}}'>Foo#<bar></a></p> +!! end + +!! test +Link with angle bracket after anchor (legacy) +!! config +wgFragmentMode=[ 'legacy' ] +!! wikitext +[[Foo#<bar>]] +!! html/php +<p><a href="/wiki/Foo#.3Cbar.3E" title="Foo">Foo#<bar></a> +</p> !! end ### @@ -8568,7 +8539,7 @@ parsoid=wt2html,wt2wt,html2html <p><a href="http://www.usemod.com/cgi-bin/mb.pl?SoftSecurity" class="extiw" title="meatball:SoftSecurity">MeatBall:SoftSecurity</a> </p> !! html/parsoid -<p><a rel="mw:ExtLink" href="http://www.usemod.com/cgi-bin/mb.pl?SoftSecurity" title="meatball:SoftSecurity">MeatBall:SoftSecurity</a></p> +<p><a rel="mw:WikiLink/Interwiki" href="http://www.usemod.com/cgi-bin/mb.pl?SoftSecurity" title="meatball:SoftSecurity">MeatBall:SoftSecurity</a></p> !! end !! test @@ -8581,11 +8552,14 @@ parsoid=wt2html,wt2wt,html2html <p><a href="http://www.usemod.com/cgi-bin/mb.pl" class="extiw" title="meatball:">MeatBall:</a> </p> !! html/parsoid -<p><a rel="mw:ExtLink" href="http://www.usemod.com/cgi-bin/mb.pl?" title="meatball:">MeatBall:</a></p> +<p><a rel="mw:WikiLink/Interwiki" href="http://www.usemod.com/cgi-bin/mb.pl?" title="meatball:">MeatBall:</a></p> !! end +## html2wt and html2html will fail because we will prefer the :en: interwiki prefix over wikipedia: !! test Interwiki link encoding conversion (T3636) +!! options +parsoid=wt2html,wt2wt !! wikitext *[[Wikipedia:ro:Olteniţa]] *[[Wikipedia:ro:Olteniţa]] @@ -8593,11 +8567,16 @@ Interwiki link encoding conversion (T3636) <ul><li><a href="http://en.wikipedia.org/wiki/ro:Olteni%C5%A3a" class="extiw" title="wikipedia:ro:Olteniţa">Wikipedia:ro:Olteniţa</a></li> <li><a href="http://en.wikipedia.org/wiki/ro:Olteni%C5%A3a" class="extiw" title="wikipedia:ro:Olteniţa">Wikipedia:ro:Olteniţa</a></li></ul> -!! html+tidy +!! html/php+tidy <ul> <li><a href="http://en.wikipedia.org/wiki/ro:Olteni%C5%A3a" class="extiw" title="wikipedia:ro:Olteniţa">Wikipedia:ro:Olteniţa</a></li> <li><a href="http://en.wikipedia.org/wiki/ro:Olteni%C5%A3a" class="extiw" title="wikipedia:ro:Olteniţa">Wikipedia:ro:Olteniţa</a></li> </ul> +!! html/parsoid +<ul> +<li><a rel="mw:WikiLink/Interwiki" href="http://en.wikipedia.org/wiki/ro:Olteniţa" title="wikipedia:ro:Olteniţa">Wikipedia:ro:Olteniţa</a></li> +<li><a rel="mw:WikiLink/Interwiki" href="http://en.wikipedia.org/wiki/ro:Olteniţa" title="wikipedia:ro:Olteniţa">Wikipedia:ro:Olteniţa</a></li> +</ul> !! end !! test @@ -8611,6 +8590,27 @@ Interwiki link with fragment (T4130) !! test Link scenarios with escaped fragments +!! config +wgFragmentMode=[ 'html5', 'legacy' ] +!! wikitext +[[#Is this great?]] +[[Foo#Is this great?]] +[[meatball:Foo#Is this great?]] +!! html/php +<p><a href="#Is_this_great?">#Is this great?</a> +<a href="/wiki/Foo#Is_this_great?" title="Foo">Foo#Is this great?</a> +<a href="http://www.usemod.com/cgi-bin/mb.pl?Foo#Is_this_great.3F" class="extiw" title="meatball:Foo">meatball:Foo#Is this great?</a> +</p> +!! html/parsoid +<p><a rel="mw:WikiLink" href="./Main_Page#Is_this_great?" data-parsoid='{"stx":"simple","a":{"href":"./Main_Page#Is_this_great?"},"sa":{"href":"#Is this great?"}}'>#Is this great?</a> +<a rel="mw:WikiLink" href="./Foo#Is_this_great?" title="Foo" data-parsoid='{"stx":"simple","a":{"href":"./Foo#Is_this_great?"},"sa":{"href":"Foo#Is this great?"}}'>Foo#Is this great?</a> +<a rel="mw:WikiLink/Interwiki" href="http://www.usemod.com/cgi-bin/mb.pl?Foo#Is_this_great?" title="meatball:Foo" data-parsoid='{"stx":"simple","a":{"href":"http://www.usemod.com/cgi-bin/mb.pl?Foo#Is_this_great?"},"sa":{"href":"meatball:Foo#Is this great?"},"isIW":true}'>meatball:Foo#Is this great?</a></p> +!! end + +!! test +Link scenarios with escaped fragments (legacy) +!! config +wgFragmentMode=[ 'legacy' ] !! wikitext [[#Is this great?]] [[Foo#Is this great?]] @@ -8620,10 +8620,6 @@ Link scenarios with escaped fragments <a href="/wiki/Foo#Is_this_great.3F" title="Foo">Foo#Is this great?</a> <a href="http://www.usemod.com/cgi-bin/mb.pl?Foo#Is_this_great.3F" class="extiw" title="meatball:Foo">meatball:Foo#Is this great?</a> </p> -!! html/parsoid -<p><a rel="mw:WikiLink" href="./Main_Page#Is_this_great.3F" data-parsoid='{"stx":"simple","a":{"href":"./Main_Page#Is_this_great.3F"},"sa":{"href":"#Is this great?"}}'>#Is this great?</a> -<a rel="mw:WikiLink" href="./Foo#Is_this_great.3F" title="Foo" data-parsoid='{"stx":"simple","a":{"href":"./Foo#Is_this_great.3F"},"sa":{"href":"Foo#Is this great?"}}'>Foo#Is this great?</a> -<a rel="mw:ExtLink" href="http://www.usemod.com/cgi-bin/mb.pl?Foo#Is_this_great.3F" title="meatball:Foo" data-parsoid='{"stx":"simple","a":{"href":"http://www.usemod.com/cgi-bin/mb.pl?Foo#Is_this_great.3F"},"sa":{"href":"meatball:Foo#Is this great?"},"isIW":true}'>meatball:Foo#Is this great?</a></p> !! end # Ideally the wikipedia: prefix here should be proto-relative too @@ -8648,19 +8644,19 @@ Different interwiki prefixes mapping to the same URL [[ wikiPEdia :Foo]] !! html/parsoid -<p><a rel="mw:ExtLink" href="//en.wikipedia.org/wiki/Foo" data-parsoid='{"stx":"simple","a":{"href":"//en.wikipedia.org/wiki/Foo"},"sa":{"href":":en:Foo"},"isIW":true}' title="en:Foo">en:Foo</a></p> +<p><a rel="mw:WikiLink/Interwiki" href="//en.wikipedia.org/wiki/Foo" data-parsoid='{"stx":"simple","a":{"href":"//en.wikipedia.org/wiki/Foo"},"sa":{"href":":en:Foo"},"isIW":true}' title="en:Foo">en:Foo</a></p> -<p><a rel="mw:ExtLink" href="//en.wikipedia.org/wiki/Foo" data-parsoid='{"stx":"piped","a":{"href":"//en.wikipedia.org/wiki/Foo"},"sa":{"href":":en:Foo"},"isIW":true}' title="en:Foo">Foo</a></p> +<p><a rel="mw:WikiLink/Interwiki" href="//en.wikipedia.org/wiki/Foo" data-parsoid='{"stx":"piped","a":{"href":"//en.wikipedia.org/wiki/Foo"},"sa":{"href":":en:Foo"},"isIW":true}' title="en:Foo">Foo</a></p> -<p><a rel="mw:ExtLink" href="http://en.wikipedia.org/wiki/Foo" data-parsoid='{"stx":"simple","a":{"href":"http://en.wikipedia.org/wiki/Foo"},"sa":{"href":"wikipedia:Foo"},"isIW":true}' title="wikipedia:Foo">wikipedia:Foo</a></p> +<p><a rel="mw:WikiLink/Interwiki" href="http://en.wikipedia.org/wiki/Foo" data-parsoid='{"stx":"simple","a":{"href":"http://en.wikipedia.org/wiki/Foo"},"sa":{"href":"wikipedia:Foo"},"isIW":true}' title="wikipedia:Foo">wikipedia:Foo</a></p> -<p><a rel="mw:ExtLink" href="http://en.wikipedia.org/wiki/Foo" data-parsoid='{"stx":"piped","a":{"href":"http://en.wikipedia.org/wiki/Foo"},"sa":{"href":":wikipedia:Foo"},"isIW":true}' title="wikipedia:Foo">Foo</a></p> +<p><a rel="mw:WikiLink/Interwiki" href="http://en.wikipedia.org/wiki/Foo" data-parsoid='{"stx":"piped","a":{"href":"http://en.wikipedia.org/wiki/Foo"},"sa":{"href":":wikipedia:Foo"},"isIW":true}' title="wikipedia:Foo">Foo</a></p> -<p><a rel="mw:ExtLink" href="http://en.wikipedia.org/wiki/en:Foo" data-parsoid='{"stx":"simple","a":{"href":"http://en.wikipedia.org/wiki/en:Foo"},"sa":{"href":"wikipedia:en:Foo"},"isIW":true}' title="wikipedia:en:Foo">wikipedia:en:Foo</a></p> +<p><a rel="mw:WikiLink/Interwiki" href="http://en.wikipedia.org/wiki/en:Foo" data-parsoid='{"stx":"simple","a":{"href":"http://en.wikipedia.org/wiki/en:Foo"},"sa":{"href":"wikipedia:en:Foo"},"isIW":true}' title="wikipedia:en:Foo">wikipedia:en:Foo</a></p> -<p><a rel="mw:ExtLink" href="http://en.wikipedia.org/wiki/en:Foo" data-parsoid='{"stx":"simple","a":{"href":"http://en.wikipedia.org/wiki/en:Foo"},"sa":{"href":":wikipedia:en:Foo"},"isIW":true}' title="wikipedia:en:Foo">wikipedia:en:Foo</a></p> +<p><a rel="mw:WikiLink/Interwiki" href="http://en.wikipedia.org/wiki/en:Foo" data-parsoid='{"stx":"simple","a":{"href":"http://en.wikipedia.org/wiki/en:Foo"},"sa":{"href":":wikipedia:en:Foo"},"isIW":true}' title="wikipedia:en:Foo">wikipedia:en:Foo</a></p> -<p><a rel="mw:ExtLink" href="http://en.wikipedia.org/wiki/Foo" data-parsoid='{"stx":"simple","a":{"href":"http://en.wikipedia.org/wiki/Foo"},"sa":{"href":" wikiPEdia :Foo"},"isIW":true}' title="wikipedia:Foo"> wikiPEdia :Foo</a></p> +<p><a rel="mw:WikiLink/Interwiki" href="http://en.wikipedia.org/wiki/Foo" data-parsoid='{"stx":"simple","a":{"href":"http://en.wikipedia.org/wiki/Foo"},"sa":{"href":" wikiPEdia :Foo"},"isIW":true}' title="wikipedia:Foo"> wikiPEdia :Foo</a></p> !! end !! test @@ -8680,11 +8676,11 @@ Interwiki links that cannot be represented in wiki syntax <a rel="nofollow" class="external text" href="http://de.wikipedia.org/wiki/#foo">is just fragment</a> </p> !! html/parsoid -<p><a rel="mw:ExtLink" href="http://www.usemod.com/cgi-bin/mb.pl?ok" title="meatball:ok">meatball:ok</a> -<a rel="mw:ExtLink" href="http://www.usemod.com/cgi-bin/mb.pl?ok#foo" title="meatball:ok">ok with fragment</a> -<a rel="mw:ExtLink" href="http://www.usemod.com/cgi-bin/mb.pl?ok_as_well?" title="meatball:ok as well?">ok ending with ? mark</a> -<a rel="mw:ExtLink" href="http://de.wikipedia.org/wiki/Foo?action=history">has query</a> -<a rel="mw:ExtLink" href="http://de.wikipedia.org/wiki/#foo">is just fragment</a></p> +<p><a rel="mw:WikiLink/Interwiki" href="http://www.usemod.com/cgi-bin/mb.pl?ok" title="meatball:ok">meatball:ok</a> +<a rel="mw:WikiLink/Interwiki" href="http://www.usemod.com/cgi-bin/mb.pl?ok#foo" title="meatball:ok">ok with fragment</a> +<a rel="mw:WikiLink/Interwiki" href="http://www.usemod.com/cgi-bin/mb.pl?ok_as_well?" title="meatball:ok as well?">ok ending with ? mark</a> +<a rel="mw:ExtLink" class="external text" href="http://de.wikipedia.org/wiki/Foo?action=history">has query</a> +<a rel="mw:ExtLink" class="external text" href="http://de.wikipedia.org/wiki/#foo">is just fragment</a></p> !! end !! test @@ -8695,7 +8691,7 @@ Interwiki links: trail <p><a href="http://en.wikipedia.org/wiki/Foo" class="extiw" title="wikipedia:Foo">Bar</a> </p> !! html/parsoid -<p><a rel="mw:ExtLink" href="http://en.wikipedia.org/wiki/Foo" data-parsoid='{"stx":"piped","a":{"href":"http://en.wikipedia.org/wiki/Foo"},"sa":{"href":"wikipedia:Foo"},"isIW":true,"tail":"r"}' title="wikipedia:Foo">Bar</a></p> +<p><a rel="mw:WikiLink/Interwiki" href="http://en.wikipedia.org/wiki/Foo" data-parsoid='{"stx":"piped","a":{"href":"http://en.wikipedia.org/wiki/Foo"},"sa":{"href":"wikipedia:Foo"},"isIW":true,"tail":"r"}' title="wikipedia:Foo">Bar</a></p> !! end !! test @@ -8749,7 +8745,7 @@ parsoid=wt2html,wt2wt,html2html <p><a href="http://www.usemod.com/cgi-bin/mb.pl?Hello" class="extiw" title="meatball:Hello">local:meatball:Hello</a> </p> !! html/parsoid -<p><a rel="mw:ExtLink" href="http://www.usemod.com/cgi-bin/mb.pl?Hello" title="meatball:Hello">local:meatball:Hello</a></p> +<p><a rel="mw:WikiLink/Interwiki" href="http://www.usemod.com/cgi-bin/mb.pl?Hello" title="meatball:Hello">local:meatball:Hello</a></p> !! end !! test @@ -8847,8 +8843,8 @@ Blah blah blah </p> !! html/parsoid <p>Blah blah blah -<a rel="mw:ExtLink" href="http://es.wikipedia.org/wiki/Spanish" title="es:Spanish">es:Spanish</a> -<a rel="mw:ExtLink" href="http://zh.wikipedia.org/wiki/Chinese" title="zh:Chinese"> zh : Chinese </a></p> +<a rel="mw:WikiLink/Interwiki" href="http://es.wikipedia.org/wiki/Spanish" title="es:Spanish">es:Spanish</a> +<a rel="mw:WikiLink/Interwiki" href="http://zh.wikipedia.org/wiki/Chinese" title="zh:Chinese"> zh : Chinese </a></p> !! end !! test @@ -8865,7 +8861,7 @@ parsoid=wt2html [[:::es:Spanish]] </p> !! html/parsoid -<p><a rel="mw:ExtLink" href="http://es.wikipedia.org/wiki/Spanish" title="es:Spanish">es:Spanish</a> +<p><a rel="mw:WikiLink/Interwiki" href="http://es.wikipedia.org/wiki/Spanish" title="es:Spanish">es:Spanish</a> [[::es:Spanish]] [[:::es:Spanish]]</p> !! end @@ -8942,7 +8938,7 @@ parsoid=wt2html,wt2wt,html2html Blah blah blah [[zh:Chinese]] !! html/parsoid -<p>Blah blah blah <a rel="mw:ExtLink" href="http://zh.wikipedia.org/wiki/Chinese" title="zh:Chinese">zh:Chinese</a></p> +<p>Blah blah blah <a rel="mw:WikiLink/Interwiki" href="http://zh.wikipedia.org/wiki/Chinese" title="zh:Chinese">zh:Chinese</a></p> !! end ## PHP parser tests script needs an update @@ -8956,7 +8952,7 @@ parsoid=wt2html,wt2wt,html2html Blah blah blah [[zh:Chinese]] !! html/parsoid -<p>Blah blah blah <a rel="mw:ExtLink" href="http://zh.wikipedia.org/wiki/Chinese" title="zh:Chinese">zh:Chinese</a></p> +<p>Blah blah blah <a rel="mw:WikiLink/Interwiki" href="http://zh.wikipedia.org/wiki/Chinese" title="zh:Chinese">zh:Chinese</a></p> !! end !! test @@ -9043,7 +9039,7 @@ parsoid=wt2html,wt2wt,html2html </p><p><a href="/wiki/Ko:" title="Ko:">ko:</a> </p> !! html/parsoid -<p><a rel="mw:ExtLink" href="http://es.wikipedia.org/wiki/" title="es:">es:</a></p> +<p><a rel="mw:WikiLink/Interwiki" href="http://es.wikipedia.org/wiki/" title="es:">es:</a></p> <p><a rel="mw:WikiLink" href="./Ko:" title="Ko:">ko:</a></p> !! end @@ -9071,7 +9067,7 @@ Blah blah blah </p> !! html/parsoid <p>Blah blah blah -<a rel="mw:ExtLink" href="http://es.wikipedia.org/wiki/Spanish" title="es:Spanish">local:es:Spanish</a></p> +<a rel="mw:WikiLink/Interwiki" href="http://es.wikipedia.org/wiki/Spanish" title="es:Spanish">local:es:Spanish</a></p> !! end !! test @@ -9114,10 +9110,12 @@ Blah blah blah # This tests the Parsoid bail-out code. !! test 3. Other redirect variants +!! options +parsoid=wt2html !! wikitext #REDIRECT [[<nowiki>[[Bar]]</nowiki>]] !! html/parsoid -<ol><li data-parsoid>REDIRECT [[[[Bar]]]]</li></ol> +<ol><li>REDIRECT [[<span typeof="mw:Nowiki">[[Bar]]</span>]]</li></ol> !! end !! test @@ -9265,6 +9263,7 @@ language=is Redirect syntax under text isn't considered a redirect !! wikitext some text + #redirect [[Main Page]] !! html/parsoid <p>some text</p> @@ -9287,9 +9286,9 @@ Redirect followed by block on the same line !! options parsoid=wt2html !! wikitext -#REDIRECT [[Main Page]]<!-- haha -->== hi == +#REDIRECT [[Main Page]]<!-- haha -->==hi== !! html/parsoid -<link rel="mw:PageProp/redirect" href="./Main_Page"/><!-- haha --><h2 id="hi"> hi </h2> +<link rel="mw:PageProp/redirect" href="./Main_Page"/><!-- haha --><h2 id="hi">hi</h2> !! end !! test @@ -9360,8 +9359,9 @@ parsoid=wt2html <br/ > !! html+tidy -<p><br /></p> -<p><br /></p> +<p><br /> +</p><p><br /> +</p> !! end !! test @@ -9411,7 +9411,7 @@ Handling html with a div self-closing tag !! html/parsoid <div title="" data-parsoid='{"stx":"html","selfClose":true}'></div> <div title="" data-parsoid='{"stx":"html","selfClose":true}'></div> -<div title="" data-parsoid='{"stx":"html","selfClose":true,"brokenHTMLTag":true}'></div> +<div title="" data-parsoid='{"stx":"html","selfClose":true}'></div> <div title="bar" data-parsoid='{"stx":"html","selfClose":true}'></div> <div title="bar" data-parsoid='{"stx":"html","selfClose":true}'></div> <div title="bar/" data-parsoid='{"stx":"html","autoInsertedEnd":true}'></div> @@ -9453,10 +9453,9 @@ foo <hr > bar !! html+tidy <hr /> -<hr /> -<p>foo</p> -<hr /> -<p>bar</p> +<hr /><p> +foo </p><hr /><p> bar +</p> !! end !! test @@ -9505,8 +9504,8 @@ Horizontal ruler -- Supports content following dashes on same line <hr /> Foo !! html+tidy -<hr /> -<p>Foo</p> +<hr /><p> Foo +</p> !! end ### @@ -9516,11 +9515,11 @@ Horizontal ruler -- Supports content following dashes on same line Common list !! wikitext *Common list -* item 2 +*item 2 *item 3 !! html <ul><li>Common list</li> -<li> item 2</li> +<li>item 2</li> <li>item 3</li></ul> !! end @@ -9530,21 +9529,22 @@ Numbered list !! wikitext #Numbered list #item 2 -# item 3 +#item 3 !! html <ol><li>Numbered list</li> <li>item 2</li> -<li> item 3</li></ol> +<li>item 3</li></ol> !! end +# the switch from level 3 to ordered should not introduce a newline between !! test Mixed list !! wikitext *Mixed list -*# with numbers -** and bullets -*# and numbers +*#with numbers +**and bullets +*#and numbers *bullets again **bullet level 2 ***bullet level 3 @@ -9554,13 +9554,13 @@ Mixed list **#Number on level 3 *#number level 2 *Level 1 -*** Level 3 -#** Level 3, but ordered +***Level 3 +#**Level 3, but ordered !! html <ul><li>Mixed list -<ol><li> with numbers</li></ol> -<ul><li> and bullets</li></ul> -<ol><li> and numbers</li></ol></li> +<ol><li>with numbers</li></ol> +<ul><li>and bullets</li></ul> +<ol><li>and numbers</li></ol></li> <li>bullets again <ul><li>bullet level 2 <ul><li>bullet level 3 @@ -9570,43 +9570,43 @@ Mixed list <li>Number on level 3</li></ol></li></ul> <ol><li>number level 2</li></ol></li> <li>Level 1 -<ul><li><ul><li> Level 3</li></ul></li></ul></li></ul> -<ol><li><ul><li><ul><li> Level 3, but ordered</li></ul></li></ul></li></ol> +<ul><li><ul><li>Level 3</li></ul></li></ul></li></ul> +<ol><li><ul><li><ul><li>Level 3, but ordered</li></ul></li></ul></li></ol> !! end !! test 1. Nested mixed wikitext and html list !! wikitext -* hi -* <ul><li>ho</li></ul> -* hi -** ho +*hi +*<ul><li>ho</li></ul> +*hi +**ho !! html/php -<ul><li> hi</li> -<li> <ul><li>ho</li></ul></li> -<li> hi -<ul><li> ho</li></ul></li></ul> +<ul><li>hi</li> +<li><ul><li>ho</li></ul></li> +<li>hi +<ul><li>ho</li></ul></li></ul> !! html/parsoid -<ul><li> hi</li> -<li> <ul data-parsoid='{"stx":"html"}'><li data-parsoid='{"stx":"html"}'>ho</li></ul></li> -<li> hi -<ul><li> ho</li></ul></li></ul> +<ul><li>hi</li> +<li><ul data-parsoid='{"stx":"html"}'><li data-parsoid='{"stx":"html"}'>ho</li></ul></li> +<li>hi +<ul><li>ho</li></ul></li></ul> !! end !! test 2. Nested mixed wikitext and html list (incompatible) !! wikitext -; hi -: {{echo|<li>ho</li>}} +;hi +:{{echo|<li>ho</li>}} !! html/php -<dl><dt> hi</dt> -<dd> <li>ho</li></dd></dl> +<dl><dt>hi</dt> +<dd><li>ho</li></dd></dl> !! html/parsoid -<dl><dt> hi</dt> -<dd> <li about="#mwt1" typeof="mw:Transclusion" data-parsoid='{"stx":"html","pi":[[{"k":"1"}]]}' data-mw='{"parts":[{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"<li>ho</li>"}},"i":0}}]}'>ho</li></dd></dl> +<dl><dt>hi</dt> +<dd><li about="#mwt1" typeof="mw:Transclusion" data-parsoid='{"stx":"html","pi":[[{"k":"1"}]]}' data-mw='{"parts":[{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"<li>ho</li>"}},"i":0}}]}'>ho</li></dd></dl> !! end !! test @@ -9678,24 +9678,24 @@ Nested lists 6 (both elements empty) !! test Nested lists 7 (skip initial nesting levels) !! wikitext -*** foo +***foo !! html -<ul><li><ul><li><ul><li> foo</li></ul></li></ul></li></ul> +<ul><li><ul><li><ul><li>foo</li></ul></li></ul></li></ul> !! end !! test Nested lists 8 (multiple nesting transitions) !! wikitext -* foo -*** bar -** baz -* boo +*foo +***bar +**baz +*boo !! html -<ul><li> foo -<ul><li><ul><li> bar</li></ul></li> -<li> baz</li></ul></li> -<li> boo</li></ul> +<ul><li>foo +<ul><li><ul><li>bar</li></ul></li> +<li>baz</li></ul></li> +<li>boo</li></ul> !! end @@ -9736,60 +9736,61 @@ parsoid !! test List items are not parsed correctly following a <pre> block (T2785) !! wikitext -* <pre>foo</pre> -* <pre>bar</pre> -* zar +*<pre>foo</pre> +*<pre>bar</pre> +*zar !! html/php -<ul><li> <pre>foo</pre></li> -<li> <pre>bar</pre></li> -<li> zar</li></ul> +<ul><li><pre>foo</pre></li> +<li><pre>bar</pre></li> +<li>zar</li></ul> !! html/parsoid -<ul><li> <pre typeof="mw:Extension/pre" about="#mwt2" data-mw='{"name":"pre","attrs":{},"body":{"extsrc":"foo"}}'>foo</pre></li> -<li> <pre typeof="mw:Extension/pre" about="#mwt4" data-mw='{"name":"pre","attrs":{},"body":{"extsrc":"bar"}}'>bar</pre></li> -<li> zar</li></ul> +<ul><li><pre typeof="mw:Extension/pre" about="#mwt2" data-mw='{"name":"pre","attrs":{},"body":{"extsrc":"foo"}}'>foo</pre></li> +<li><pre typeof="mw:Extension/pre" about="#mwt4" data-mw='{"name":"pre","attrs":{},"body":{"extsrc":"bar"}}'>bar</pre></li> +<li>zar</li></ul> !! end +# FIXME: Might benefit from a html/parsoid since this has a template !! test List items from template !! wikitext {{inner list}} -* item 2 +*item 2 -* item 0 +*item 0 {{inner list}} -* item 2 +*item 2 -* item 0 -* notSOL{{inner list}} -* item 2 +*item 0 +*notSOL{{inner list}} +*item 2 !! html -<ul><li> item 1</li> -<li> item 2</li></ul> -<ul><li> item 0</li> -<li> item 1</li> -<li> item 2</li></ul> -<ul><li> item 0</li> -<li> notSOL</li> -<li> item 1</li> -<li> item 2</li></ul> +<ul><li>item 1</li> +<li>item 2</li></ul> +<ul><li>item 0</li> +<li>item 1</li> +<li>item 2</li></ul> +<ul><li>item 0</li> +<li>notSOL</li> +<li>item 1</li> +<li>item 2</li></ul> !! end !! test List interrupted by empty line or heading !! wikitext -* foo +*foo -** bar -== A heading == -* Another list item +**bar +==A heading== +*Another list item !! html -<ul><li> foo</li></ul> -<ul><li><ul><li> bar</li></ul></li></ul> +<ul><li>foo</li></ul> +<ul><li><ul><li>bar</li></ul></li></ul> <h2><span class="mw-headline" id="A_heading">A heading</span><span class="mw-editsection"><span class="mw-editsection-bracket">[</span><a href="/index.php?title=Parser_test&action=edit&section=1" title="Edit section: A heading">edit</a><span class="mw-editsection-bracket">]</span></span></h2> -<ul><li> Another list item</li></ul> +<ul><li>Another list item</li></ul> !!end @@ -9807,11 +9808,14 @@ Multiple list tags generated by templates </li> !! html+tidy -<ul> -<li>a</li> -<li>b</li> -<li>c</li> -</ul> +<li>a +</li><li>b +</li><li>c +</li> +!! html/parsoid +<li about="#mwt1" typeof="mw:Transclusion" data-parsoid='{"stx":"html","autoInsertedEnd":true,"dsr":[0,44,null,null],"pi":[[{"k":"1"}],[{"k":"1"}],[{"k":"1"}]]}' data-mw='{"parts":[{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"<li>"}},"i":0}},"a\n",{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"<li>"}},"i":1}},"b\n",{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"<li>"}},"i":2}},"c"]}'>a +</li><li about="#mwt1">b +</li><li about="#mwt1" data-parsoid='{"stx":"html","autoInsertedEnd":true,"dsr":[null,44,null,0]}'>c</li> !!end !!test @@ -9851,36 +9855,36 @@ Replacing whitespace with tabs still doesn't break the list (gerrit 78327) !!end +# FIXME: Parsoid has a dedicated DOM pass to mimic this Tidy-specific li-hack +# That pass could possibly be removed. !!test -Test the li-hack -(The PHP parser relies on Tidy for the hack) +Test the li-hack (a hack from Tidy days, but doesn't work as advertised with Remex) !!options parsoid=wt2html,wt2wt !! wikitext -* foo -* <li>li-hack -* {{echo|<li>templated li-hack}} -* <!--foo--> <li> unsupported li-hack with preceding comments +*foo +*<li>li-hack +*{{echo|<li>templated li-hack}} +*<!--foo--><li> unsupported li-hack with preceding comments <ul> <li><li>not a li-hack </li> </ul> !! html+tidy +<ul><li>foo</li> +<li class="mw-empty-elt"></li><li>li-hack</li> +<li class="mw-empty-elt"></li><li>templated li-hack</li> +<li class="mw-empty-elt"></li><li> unsupported li-hack with preceding comments</li></ul> <ul> -<li>foo</li> -<li>li-hack</li> -<li>templated li-hack</li> -<li>unsupported li-hack with preceding comments</li> -</ul> -<ul> -<li>not a li-hack</li> +<li class="mw-empty-elt"></li><li>not a li-hack +</li> </ul> !! html/parsoid <ul><li> foo</li> -<li data-parsoid='{"stx":"html","autoInsertedEnd":true,"liHackSrc":"* "}'>li-hack</li> -<li about="#mwt1" typeof="mw:Transclusion" data-parsoid='{"stx":"html","autoInsertedEnd":true,,"pi":[[{"k":"1"}]]}' data-mw='{"parts":["* ",{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"<li>templated li-hack"}},"i":0}}]}'>templated li-hack</li> -<li data-parsoid='{"autoInsertedEnd":true}'> <!--foo--> </li><li data-parsoid='{"stx":"html","autoInsertedEnd":true}'> unsupported li-hack with preceding comments</li></ul> +<li data-parsoid='{"stx":"html","autoInsertedEnd":true,"liHackSrc":"*"}'>li-hack</li> +<li about="#mwt1" typeof="mw:Transclusion" data-parsoid='{"stx":"html","autoInsertedEnd":true,,"pi":[[{"k":"1"}]]}' data-mw='{"parts":["*",{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"<li>templated li-hack"}},"i":0}}]}'>templated li-hack</li> +<li data-parsoid='{"autoInsertedEnd":true}'><!--foo--></li><li data-parsoid='{"stx":"html","autoInsertedEnd":true}'>unsupported li-hack with preceding comments</li></ul> <ul data-parsoid='{"stx":"html"}'> <li class="mw-empty-elt" data-parsoid='{"stx":"html","autoInsertedEnd":true}'></li><li data-parsoid='{"stx":"html"}'>not a li-hack @@ -9894,24 +9898,26 @@ Parsoid: Make sure nested lists are serialized on their own line even if HTML co !! options parsoid !! wikitext -# foo -## bar -* foo -** bar -: foo -:: bar +#foo +##bar + +*foo +**bar + +:foo +::bar !! html <ol> -<li> foo<ol> -<li> bar</li> +<li>foo<ol> +<li>bar</li> </ol></li> </ol><ul> -<li> foo<ul> -<li> bar</li> +<li>foo<ul> +<li>bar</li> </ul></li> </ul><dl> -<dd> foo<dl> -<dd> bar</dd> +<dd>foo<dl> +<dd>bar</dd> </dl></dd> </dl> !! end @@ -9933,101 +9939,103 @@ parsoid # tags (parse, minimize scope of fixup, and roundtrip back) # ------------------------------------------------------------------------ +# Remex and Parsoid output stems from list handling diffs because Parsoid & PHP parser. +# Parsoid's list handling is more aware of block structure. !! test Unbalanced closing block tags break a list -(php parser relies on Tidy to fix up) !! wikitext <div> *a</div><div> *b</div> !! html+tidy <div> -<ul> +<ul><li>a</li></ul></div><div> +<li>b</li></div> +!! html/parsoid +<div><ul> <li>a</li> -</ul> -</div> -<div> -<ul> +</ul></div> +<div><ul> <li>b</li> -</ul> -</div> +</ul></div> !! end -# Parsoid fails this test, but it might be tricky to support properly. -# See T70395. !! test Unbalanced closing non-block tags don't break a list -(php parser relies on Tidy to fix up) !! wikitext <span> *a</span><span> *b</span> !! html/php+tidy -<ul> -<li><span>a</span></li> -<li><span>b</span></li> -</ul> +<p><span> +</span></p> +<ul><li>a<span></span></li> +<li>b</li></ul> !! html/parsoid <span> <ul> -<li>a<span></span> -</li> -<li>b -</li> +<li>a<span></span></li> +<li>b</li> </ul> </span> !! end +# Parsoid does some post-dom-building cleanup +# which is why its output differs from Remex. !! test Unclosed formatting tags that straddle lists are closed and reopened -(php parser relies on Tidy to fix up) !! options parsoid=wt2html,wt2wt,html2html !! wikitext -# <s> a -# b </s> +#<s> a +#b </s> !! html/php+tidy -<ol> -<li><s>a</s></li> -<li><s>b</s></li> -</ol> +<ol><li><s> a</s></li><s> +</s><li><s>b </s></li></ol> !! html/parsoid -<ol><li> <s> a</s></li> -<li><s> b </s></li></ol> +<ol><li><s> a</s></li> +<li><s>b </s></li></ol> !! end +# Output is ugly because of all the misnested tag fixups. +# Remex is wrapping p-tags around empty elements. +# Parsoid has special-case handling of this pattern of +# wrapping lists in formatting tags. +# FIXME: Should we remove this code from Parsoid? Or add +# special support in Remex? If the latter, maybe just wait +# for Parsoid to become the default parser. # See T70395. !!test 1. List embedded in a formatting tag !! wikitext <small> -* foo +*foo </small> !! html/php+tidy -<ul> -<li><small>foo</small></li> -</ul> +<p><small> +</small></p><small><ul><li>foo</li></ul></small><small></small><p><small></small> +</p> !! html/parsoid <small> <ul> -<li> foo</li> +<li>foo</li> </ul> </small> !!end -## Ugly Parsoid output here -## Not sure what the right output is. +# Output is ugly because of all the misnested tag fixups +# Remex is wrapping p-tags around empty elements. +# Parsoid has code that strips useless p-tags. !!test -2. List embedded in a formatting tag +2. List embedded in a formatting tag in a misnested way !! wikitext <small> *a *b</small> !! html/php+tidy -<ul> -<li><small>a</small></li> -<li><small>b</small></li> -</ul> +<p><small> +</small></p><small></small><ul><small><li>a</li> +</small><li><small>b</small></li></ul> !! html/parsoid <small></small> <ul><small> @@ -10037,31 +10045,6 @@ parsoid=wt2html,wt2wt,html2html </ul> !!end -# Ugly Parsoid and PHP parser output here -# Not sure if we want to make this a test! -# -## !!test -## 3. Unclosed formatting tags in list elements -## !! wikitext -## *<small>a -## *<small>b -## !! html/php+tidy -## <ul> -## <li><small>a</small></li> -## <li><small><small>b</small></small></li> -## </ul> -## !! html/parsoid -## <ul> -## <li><small>a</small></li> -## <small> -## <li><small>b</small></li> -## </small></ul> -## !!end - -# This is a bug in the PHP parser + tidy combination. -# (The </tr> tag gets parsed as text and html-escaped by PHP, -# and then fostered out of the table by tidy.) -# We believe the Parsoid output to be correct. !! test Table with missing opening <tr> tag !! options @@ -10073,10 +10056,9 @@ parsoid=wt2html,wt2wt </table> !! html+tidy <table> -<tr> -<td>foo</td> +<tbody><tr><td>foo</td> </tr> -</table> +</tbody></table> !! end ### @@ -10217,35 +10199,35 @@ Magic Word: {{CURRENTTIMESTAMP}} !! test Magic Words LOCAL (UTC) !! wikitext -* {{LOCALMONTH}} -* {{LOCALMONTH1}} -* {{LOCALMONTHNAME}} -* {{LOCALMONTHNAMEGEN}} -* {{LOCALMONTHABBREV}} -* {{LOCALDAY}} -* {{LOCALDAY2}} -* {{LOCALDAYNAME}} -* {{LOCALYEAR}} -* {{LOCALTIME}} -* {{LOCALHOUR}} -* {{LOCALWEEK}} -* {{LOCALDOW}} -* {{LOCALTIMESTAMP}} -!! html -<ul><li> 01</li> -<li> 1</li> -<li> January</li> -<li> January</li> -<li> Jan</li> -<li> 1</li> -<li> 01</li> -<li> Thursday</li> -<li> 1970</li> -<li> 00:02</li> -<li> 00</li> -<li> 1</li> -<li> 4</li> -<li> 19700101000203</li></ul> +*{{LOCALMONTH}} +*{{LOCALMONTH1}} +*{{LOCALMONTHNAME}} +*{{LOCALMONTHNAMEGEN}} +*{{LOCALMONTHABBREV}} +*{{LOCALDAY}} +*{{LOCALDAY2}} +*{{LOCALDAYNAME}} +*{{LOCALYEAR}} +*{{LOCALTIME}} +*{{LOCALHOUR}} +*{{LOCALWEEK}} +*{{LOCALDOW}} +*{{LOCALTIMESTAMP}} +!! html +<ul><li>01</li> +<li>1</li> +<li>January</li> +<li>January</li> +<li>Jan</li> +<li>1</li> +<li>01</li> +<li>Thursday</li> +<li>1970</li> +<li>00:02</li> +<li>00</li> +<li>1</li> +<li>4</li> +<li>19700101000203</li></ul> !! end @@ -10554,11 +10536,9 @@ title=[['foo & bar = baz']] parsoid={ "modes": ["wt2html","wt2wt"], "normalizePhp": true } !! wikitext ''{{PAGENAME}}'' -!! html/php +!! html+tidy <p><i>'foo & bar = baz'</i> </p> -!! html+tidy -<p><i>'foo & bar = baz'</i></p> !! end !! test @@ -10568,11 +10548,9 @@ title=[[*RFC 1234 http://example.com/]] parsoid={ "modes": ["wt2html","wt2wt"], "normalizePhp": true } !! wikitext {{PAGENAME}} -!! html/php +!! html+tidy <p>*RFC 1234 http://example.com/ </p> -!! html+tidy -<p>*RFC 1234 http://example.com/</p> !! end !! test @@ -10594,11 +10572,9 @@ title=[[*RFC 1234 http://example.com/]] parsoid={ "modes": ["wt2html","wt2wt"], "normalizePhp": true } !! wikitext {{PAGENAMEE}} -!! html/php +!! html+tidy <p>*RFC_1234_http://example.com/ </p> -!! html+tidy -<p>*RFC_1234_http://example.com/</p> !! end !! test @@ -10935,10 +10911,10 @@ Magic links: RFC (T2479) !! wikitext RFC 822 !! html/php -<p><a class="external mw-magiclink-rfc" rel="nofollow" href="//tools.ietf.org/html/rfc822">RFC 822</a> +<p><a class="external mw-magiclink-rfc" rel="nofollow" href="https://tools.ietf.org/html/rfc822">RFC 822</a> </p> !! html/parsoid -<p><a href="//tools.ietf.org/html/rfc822" rel="mw:ExtLink">RFC 822</a></p> +<p><a href="https://tools.ietf.org/html/rfc822" rel="mw:ExtLink" class="external text">RFC 822</a></p> !! end !! test @@ -10946,10 +10922,10 @@ Magic links: RFC (T67278) !! wikitext This is RFC 822 but thisRFC 822 is not RFC 822linked. !! html/php -<p>This is <a class="external mw-magiclink-rfc" rel="nofollow" href="//tools.ietf.org/html/rfc822">RFC 822</a> but thisRFC 822 is not RFC 822linked. +<p>This is <a class="external mw-magiclink-rfc" rel="nofollow" href="https://tools.ietf.org/html/rfc822">RFC 822</a> but thisRFC 822 is not RFC 822linked. </p> !! html/parsoid -<p>This is <a href="//tools.ietf.org/html/rfc822" rel="mw:ExtLink">RFC 822</a> but thisRFC 822 is not RFC 822linked.</p> +<p>This is <a href="https://tools.ietf.org/html/rfc822" rel="mw:ExtLink" class="external text">RFC 822</a> but thisRFC 822 is not RFC 822linked.</p> !! end !! test @@ -10959,12 +10935,12 @@ RFC      822 RFC 822 !! html/php -<p><a class="external mw-magiclink-rfc" rel="nofollow" href="//tools.ietf.org/html/rfc822">RFC 822</a> +<p><a class="external mw-magiclink-rfc" rel="nofollow" href="https://tools.ietf.org/html/rfc822">RFC 822</a> RFC 822 </p> !! html/parsoid -<p><a href="//tools.ietf.org/html/rfc822" rel="mw:ExtLink">RFC <span typeof="mw:Entity" data-parsoid='{"src":"&nbsp;","srcContent":" "}'> </span><span typeof="mw:Entity" data-parsoid='{"src":"&#160;","srcContent":" "}'> </span><span typeof="mw:Entity" data-parsoid='{"src":"&#0160;","srcContent":" "}'> </span><span typeof="mw:Entity" data-parsoid='{"src":"&#xA0;","srcContent":" "}'> </span><span typeof="mw:Entity" data-parsoid='{"src":"&#Xa0;","srcContent":" "}'> </span> 822</a> +<p><a href="https://tools.ietf.org/html/rfc822" rel="mw:ExtLink" class="external text">RFC <span typeof="mw:Entity" data-parsoid='{"src":"&nbsp;","srcContent":" "}'> </span><span typeof="mw:Entity" data-parsoid='{"src":"&#160;","srcContent":" "}'> </span><span typeof="mw:Entity" data-parsoid='{"src":"&#0160;","srcContent":" "}'> </span><span typeof="mw:Entity" data-parsoid='{"src":"&#xA0;","srcContent":" "}'> </span><span typeof="mw:Entity" data-parsoid='{"src":"&#Xa0;","srcContent":" "}'> </span> 822</a> RFC 822</p> !! end @@ -11022,7 +10998,7 @@ PMID 1234 <p><a class="external mw-magiclink-pmid" rel="nofollow" href="//www.ncbi.nlm.nih.gov/pubmed/1234?dopt=Abstract">PMID 1234</a> </p> !! html/parsoid -<p><a href="//www.ncbi.nlm.nih.gov/pubmed/1234?dopt=Abstract" rel="mw:ExtLink">PMID 1234</a></p> +<p><a href="//www.ncbi.nlm.nih.gov/pubmed/1234?dopt=Abstract" rel="mw:ExtLink" class="external text">PMID 1234</a></p> !! end !! test @@ -11033,7 +11009,7 @@ This is PMID 1234 but thisPMID 1234 is not PMID 1234linked. <p>This is <a class="external mw-magiclink-pmid" rel="nofollow" href="//www.ncbi.nlm.nih.gov/pubmed/1234?dopt=Abstract">PMID 1234</a> but thisPMID 1234 is not PMID 1234linked. </p> !! html/parsoid -<p>This is <a href="//www.ncbi.nlm.nih.gov/pubmed/1234?dopt=Abstract" rel="mw:ExtLink">PMID 1234</a> but thisPMID 1234 is not PMID 1234linked.</p> +<p>This is <a href="//www.ncbi.nlm.nih.gov/pubmed/1234?dopt=Abstract" rel="mw:ExtLink" class="external text">PMID 1234</a> but thisPMID 1234 is not PMID 1234linked.</p> !! end !! test @@ -11048,7 +11024,7 @@ PMID 1234 </p> !! html/parsoid -<p><a href="//www.ncbi.nlm.nih.gov/pubmed/1234?dopt=Abstract" rel="mw:ExtLink">PMID <span typeof="mw:Entity" data-parsoid='{"src":"&nbsp;","srcContent":" "}'> </span><span typeof="mw:Entity" data-parsoid='{"src":"&#160;","srcContent":" "}'> </span><span typeof="mw:Entity" data-parsoid='{"src":"&#0160;","srcContent":" "}'> </span><span typeof="mw:Entity" data-parsoid='{"src":"&#xA0;","srcContent":" "}'> </span><span typeof="mw:Entity" data-parsoid='{"src":"&#Xa0;","srcContent":" "}'> </span> 1234</a> +<p><a href="//www.ncbi.nlm.nih.gov/pubmed/1234?dopt=Abstract" rel="mw:ExtLink" class="external text">PMID <span typeof="mw:Entity" data-parsoid='{"src":"&nbsp;","srcContent":" "}'> </span><span typeof="mw:Entity" data-parsoid='{"src":"&#160;","srcContent":" "}'> </span><span typeof="mw:Entity" data-parsoid='{"src":"&#0160;","srcContent":" "}'> </span><span typeof="mw:Entity" data-parsoid='{"src":"&#xA0;","srcContent":" "}'> </span><span typeof="mw:Entity" data-parsoid='{"src":"&#Xa0;","srcContent":" "}'> </span> 1234</a> PMID 1234</p> !! end @@ -11060,14 +11036,14 @@ Magic links: use appropriate serialization for "almost" magic links. !! wikitext X[[Special:BookSources/0978739256|foo]] -X[//tools.ietf.org/html/rfc1234 foo] +X[https://tools.ietf.org/html/rfc1234 foo] !! html/php <p>X<a href="/wiki/Special:BookSources/0978739256" title="Special:BookSources/0978739256">foo</a> -</p><p>X<a rel="nofollow" class="external text" href="//tools.ietf.org/html/rfc1234">foo</a> +</p><p>X<a rel="nofollow" class="external text" href="https://tools.ietf.org/html/rfc1234">foo</a> </p> !! html/parsoid <p>X<a rel="mw:WikiLink" href="./Special:BookSources/0978739256" title="Special:BookSources/0978739256">foo</a></p> -<p>X<a rel="mw:ExtLink" href="//tools.ietf.org/html/rfc1234">foo</a></p> +<p>X<a rel="mw:ExtLink" class="external text" href="https://tools.ietf.org/html/rfc1234">foo</a></p> !! end !! test @@ -11317,11 +11293,20 @@ Templates with templated name !! html <p>foo </p> -<ul><li> item 1</li></ul> +<ul><li>item 1</li></ul> !! html/parsoid <p about="#mwt2" typeof="mw:Transclusion" data-mw='{"parts":[{"template":{"target":{"wt":"{{echo|echo}}","href":"./Template:Echo"},"params":{"1":{"wt":"foo"}},"i":0}}]}'>foo</p> -<ul about="#mwt4" typeof="mw:Transclusion" data-mw='{"parts":[{"template":{"target":{"wt":"{{echo|inner list}} ","href":"./Template:Inner_list"},"params":{},"i":0}}]}'><li> item 1</li></ul> +<ul about="#mwt4" typeof="mw:Transclusion" data-mw='{"parts":[{"template":{"target":{"wt":"{{echo|inner list}} ","href":"./Template:Inner_list"},"params":{},"i":0}}]}'><li>item 1</li></ul> +!! end + +## Regression test; the output here isn't really that interesting. +!! test +Templates with templated name and top level template args +!! wikitext +{{1{{2{{{3}}}|4=5}}}} +!! html/parsoid +<p about="#mwt2" typeof="mw:Transclusion" data-mw='{"parts":[{"template":{"target":{"wt":"1{{2{{{3}}}|4=5}}"},"params":{},"i":0}}]}'>{{1{{2{{{3}}}|4=5}}}}</p> !! end # Parsoid markup is deliberate "broken". This is an edge case. @@ -11350,12 +11335,7 @@ Template with thumb image (with link in description) This is a test template with parameter <div class="thumb tright"><div class="thumbinner" style="width:182px;"><a href="/index.php?title=Special:Upload&wpDestFile=Noimage.png" class="new" title="File:Noimage.png">File:Noimage.png</a> <div class="thumbcaption"><a href="/index.php?title=No_link&action=edit&redlink=1" class="new" title="No link (page does not exist)">link</a> <a href="/index.php?title=No_link&action=edit&redlink=1" class="new" title="No link (page does not exist)">caption</a></div></div></div> !! html+tidy -<p>This is a test template with parameter</p> -<div class="thumb tright"> -<div class="thumbinner" style="width:182px;"><a href="/index.php?title=Special:Upload&wpDestFile=Noimage.png" class="new" title="File:Noimage.png">File:Noimage.png</a> -<div class="thumbcaption"><a href="/index.php?title=No_link&action=edit&redlink=1" class="new" title="No link (page does not exist)">link</a> <a href="/index.php?title=No_link&action=edit&redlink=1" class="new" title="No link (page does not exist)">caption</a></div> -</div> -</div> +<p>This is a test template with parameter </p><div class="thumb tright"><div class="thumbinner" style="width:182px;"><a href="/index.php?title=Special:Upload&wpDestFile=Noimage.png" class="new" title="File:Noimage.png">File:Noimage.png</a> <div class="thumbcaption"><a href="/index.php?title=No_link&action=edit&redlink=1" class="new" title="No link (page does not exist)">link</a> <a href="/index.php?title=No_link&action=edit&redlink=1" class="new" title="No link (page does not exist)">caption</a></div></div></div> !! html/parsoid <p about="#mwt1" typeof="mw:Transclusion" data-mw='{"parts":[{"template":{"target":{"wt":"paramtest","href":"./Template:Paramtest"},"params":{"param":{"wt":"[[Image:noimage.png|thumb|[[no link|link]] [[no link|caption]]]]"}},"i":0}}]}'>This is a test template with parameter </p><figure class="mw-default-size" typeof="mw:Error mw:Image/Thumb" about="#mwt1" data-mw='{"errors":[{"key":"apierror-filedoesnotexist","message":"This image does not exist."}]}'><a href="./File:Noimage.png" ><img resource="./File:Noimage.png" src="./Special:FilePath/Noimage.png" height="220" width="220"/></a><figcaption><a rel="mw:WikiLink" href="./No_link" title="No link">link</a> <a rel="mw:WikiLink" href="./No_link" title="No link">caption</a></figcaption></figure> !! end @@ -11401,28 +11381,28 @@ T2553: link with two variables in a piped link Abort table cell attribute parsing on wikilink !! wikitext {| -| testing [[one|two]] | three || four -| testing one two | three || four -| testing="[[one|two]]" | three || four +|testing [[one|two]] |three||four +|testing one two |three||four +|testing="[[one|two]]" |three||four |} !! html/php <table> <tr> -<td> testing <a href="/index.php?title=One&action=edit&redlink=1" class="new" title="One (page does not exist)">two</a> | three </td> -<td> four +<td>testing <a href="/index.php?title=One&action=edit&redlink=1" class="new" title="One (page does not exist)">two</a> |three</td> +<td>four </td> -<td> three </td> -<td> four +<td>three</td> +<td>four </td> -<td> testing="<a href="/index.php?title=One&action=edit&redlink=1" class="new" title="One (page does not exist)">two</a>" | three </td> -<td> four +<td>testing="<a href="/index.php?title=One&action=edit&redlink=1" class="new" title="One (page does not exist)">two</a>" |three</td> +<td>four </td></tr></table> !! html/parsoid <table> -<tbody><tr data-parsoid='{"autoInsertedEnd":true,"autoInsertedStart":true}'><td data-parsoid='{"autoInsertedEnd":true}'> testing <a rel="mw:WikiLink" href="./One" title="One" data-parsoid='{"stx":"piped","a":{"href":"./One"},"sa":{"href":"one"}}'>two</a> | three </td><td data-parsoid='{"stx_v":"row","autoInsertedEnd":true}'> four</td> -<td data-parsoid='{"a":{"testing":null,"one":null,"two":null},"sa":{"testing":"","one":"","two":""},"autoInsertedEnd":true}'> three </td><td data-parsoid='{"stx_v":"row","autoInsertedEnd":true}'> four</td> -<td> testing="<a rel="mw:WikiLink" href="./One" title="One" data-parsoid='{"stx":"piped","a":{"href":"./One"},"sa":{"href":"one"}}'>two</a>" | three </td><td data-parsoid='{"stx_v":"row","autoInsertedEnd":true}'> four</td></tr> +<tbody><tr data-parsoid='{"autoInsertedEnd":true,"autoInsertedStart":true}'><td data-parsoid='{"autoInsertedEnd":true}'>testing <a rel="mw:WikiLink" href="./One" title="One" data-parsoid='{"stx":"piped","a":{"href":"./One"},"sa":{"href":"one"}}'>two</a> |three</td><td data-parsoid='{"stx":"row","autoInsertedEnd":true}'>four</td> +<td data-parsoid='{"a":{"testing":null,"one":null,"two":null},"sa":{"testing":"","one":"","two":""},"autoInsertedEnd":true}'>three</td><td data-parsoid='{"stx":"row","autoInsertedEnd":true}'>four</td> +<td>testing="<a rel="mw:WikiLink" href="./One" title="One" data-parsoid='{"stx":"piped","a":{"href":"./One"},"sa":{"href":"one"}}'>two</a>" |three</td><td data-parsoid='{"stx":"row","autoInsertedEnd":true}'>four</td></tr> </tbody></table> !! end @@ -11430,11 +11410,11 @@ Abort table cell attribute parsing on wikilink Don't abort table cell attribute parsing if wikilink is found in template arg !! wikitext {| -| Test {{#tag:ref|One two "[[three]]" four}} +|Test {{#tag:ref|One two "[[three]]" four}} |} !! html/parsoid <table> -<tbody><tr><td> Test <ref about="#mwt1" typeof="mw:Transclusion" data-parsoid='{"pi":[[{"k":"1"}]]}' data-mw='{"parts":[{"template":{"target":{"wt":"#tag:ref","function":"tag"},"params":{"1":{"wt":"One two \"[[three]]\" four"}},"i":0}}]}'>One two "<a rel="mw:WikiLink" href="./Three" title="Three">three</a>" four</ref></td></tr> +<tbody><tr><td>Test <ref about="#mwt1" typeof="mw:Transclusion" data-parsoid='{"pi":[[{"k":"1"}]]}' data-mw='{"parts":[{"template":{"target":{"wt":"#tag:ref","function":"tag"},"params":{"1":{"wt":"One two \"[[three]]\" four"}},"i":0}}]}'>One two "<a rel="mw:WikiLink" href="./Three" title="Three">three</a>" four</ref></td></tr> </tbody></table> !! end @@ -11548,12 +11528,12 @@ foo {{table}} </p> <table> <tr> -<td> 1 </td> -<td> 2 +<td>1</td> +<td>2 </td></tr> <tr> -<td> 3 </td> -<td> 4 +<td>3</td> +<td>4 </td></tr></table> !! end @@ -11568,12 +11548,12 @@ foo </p> <table> <tr> -<td> 1 </td> -<td> 2 +<td>1</td> +<td>2 </td></tr> <tr> -<td> 3 </td> -<td> 4 +<td>3</td> +<td>4 </td></tr></table> !! end @@ -11639,21 +11619,13 @@ Templates with intersecting and overlapping ranges {{echo|{{!}}hi}} |} !! html/php+tidy -<p>ha</p> -<p>ho</p> -<table> -<tr> -<td></td> -</tr> -<tr> -<td>hi</td> -</tr> -</table> -<table> -<tr> -<td></td> -</tr> -</table> +<p>ha</p><table> + +</table><p>ho</p><table> + +<tbody><tr> +<td>hi +</td></tr></tbody></table> !! html/parsoid <p about="#mwt1" typeof="mw:Transclusion" data-parsoid='{"stx":"html","autoInsertedEnd":true,"pi":[[{"k":"1"}],[{"k":"1"}],[{"k":"1"}]],"firstWikitextNode":"table"}' data-mw='{"parts":["{|",{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"\n<p>ha</p>"}},"i":0}},"\n","{|",{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"\n<p>ho</p>"}},"i":1}},"\n",{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"{{!}}hi"}},"i":2}},"\n|}"]}'>ha</p><table about="#mwt1" typeof="mw:ExpandedAttrs" data-mw='{"attribs":[[{"txt":"","html":""},{"html":""}]]}'> @@ -11899,32 +11871,32 @@ Includes and comments at SOL !! options parsoid=wt2html,html2html !! wikitext -<!-- comment --><noinclude><!-- comment --></noinclude><!-- comment -->== hu == +<!-- comment --><noinclude><!-- comment --></noinclude><!-- comment -->==hu== <noinclude> some -</noinclude>* stuff -* here +</noinclude>*stuff +*here -<includeonly>can have stuff</includeonly>=== here === +<includeonly>can have stuff</includeonly>===here=== !! html/php <h2><span class="mw-headline" id="hu">hu</span></h2> <p>some </p> -<ul><li> stuff</li> -<li> here</li></ul> +<ul><li>stuff</li> +<li>here</li></ul> <h3><span class="mw-headline" id="here">here</span></h3> !! html/parsoid -<!-- comment --><meta typeof="mw:Includes/NoInclude" data-parsoid='{"src":"<noinclude>"}'/><!-- comment --><meta typeof="mw:Includes/NoInclude/End" data-parsoid='{"src":"</noinclude>"}'/><!-- comment --><h2> hu </h2> +<!-- comment --><meta typeof="mw:Includes/NoInclude" data-parsoid='{"src":"<noinclude>"}'/><!-- comment --><meta typeof="mw:Includes/NoInclude/End" data-parsoid='{"src":"</noinclude>"}'/><!-- comment --><h2 id="hu">hu</h2> <meta typeof="mw:Includes/NoInclude" data-parsoid='{"src":"<noinclude>"}'/> <p>some</p> -<meta typeof="mw:Includes/NoInclude/End" data-parsoid='{"src":"</noinclude>"}'/><ul><li> stuff</li> -<li> here</li></ul> +<meta typeof="mw:Includes/NoInclude/End" data-parsoid='{"src":"</noinclude>"}'/><ul><li>stuff</li> +<li>here</li></ul> -<meta typeof="mw:Includes/IncludeOnly" data-parsoid='{"src":"<includeonly>can have stuff</includeonly>"}'/><meta typeof="mw:Includes/IncludeOnly/End" data-parsoid='{"src":""}'/><h3> here </h3> +<meta typeof="mw:Includes/IncludeOnly" data-parsoid='{"src":"<includeonly>can have stuff</includeonly>"}'/><meta typeof="mw:Includes/IncludeOnly/End" data-parsoid='{"src":""}'/><h3 id="here">here</h3> !! end @@ -12143,10 +12115,10 @@ Preprocessor precedence 5: tplarg takes precedence over template !! wikitext {{Precedence5|Bullet}} !! html/php -<ul><li> Bar</li></ul> +<ul><li>Bar</li></ul> !! html/parsoid -<ul typeof="mw:Transclusion" data-mw='{"parts":[{"template":{"target":{"wt":"Precedence5","href":"./Template:Precedence5"},"params":{"1":{"wt":"Bullet"}},"i":0}}]}'><li> Bar</li></ul> +<ul typeof="mw:Transclusion" data-mw='{"parts":[{"template":{"target":{"wt":"Precedence5","href":"./Template:Precedence5"},"params":{"1":{"wt":"Bullet"}},"i":0}}]}'><li>Bar</li></ul> !! end !! test @@ -12236,14 +12208,14 @@ Preprocessor precedence 9: groups of braces {{Preprocessor precedence 9|Four|Bullet|1|2}} !! html/php <dl><dt>4</dt> -<dd> {Four}</dd> +<dd>{Four}</dd> <dt>5</dt> -<dd> </dd></dl> -<ul><li> Bar</li></ul> +<dd></dd></dl> +<ul><li>Bar</li></ul> <dl><dt>6</dt> -<dd> Four</dd> +<dd>Four</dd> <dt>7</dt> -<dd> {Bullet}</dd></dl> +<dd>{Bullet}</dd></dl> !! html/parsoid <dl about="#mwt1" typeof="mw:Transclusion" data-mw='{"parts":[{"template":{"target":{"wt":"Preprocessor precedence 9","href":"./Template:Preprocessor_precedence_9"},"params":{"1":{"wt":"Four"},"2":{"wt":"Bullet"},"3":{"wt":"1"},"4":{"wt":"2"}},"i":0}}]}'> @@ -12281,21 +12253,21 @@ language=zh {{Preprocessor precedence 10|Three|raw2|Bullet|1|2}} !! html/php <dl><dt>1</dt> -<dd> raw</dd> +<dd>raw</dd> <dt>2</dt> -<dd> -</dd></dl> -<ul><li> Bar-</li></ul> +<dd>-</dd></dl> +<ul><li>Bar-</li></ul> <dl><dt>3</dt> -<dd> -Three-</dd> +<dd>-Three-</dd> <dt>4</dt> -<dd> raw2</dd> +<dd>raw2</dd> <dt>5</dt> -<dd> -</dd></dl> -<ul><li> Bar-</li></ul> +<dd>-</dd></dl> +<ul><li>Bar-</li></ul> <dl><dt>6</dt> -<dd> -Three-</dd> +<dd>-Three-</dd> <dt>7</dt> -<dd> raw2</dd></dl> +<dd>raw2</dd></dl> !! html/parsoid <dl about="#mwt1" typeof="mw:Transclusion" data-mw='{"parts":[{"template":{"target":{"wt":"Preprocessor precedence 10","href":"./Template:Preprocessor_precedence_10"},"params":{"1":{"wt":"Three"},"2":{"wt":"raw2"},"3":{"wt":"Bullet"},"4":{"wt":"1"},"5":{"wt":"2"}},"i":0}}]}'> @@ -12349,43 +12321,34 @@ Preprocessor precedence 12: broken language converter closed by brace. parsoid=wt2html !! wikitext This form breaks the template, which is unfortunate: -* {{echo|foo-{bar}bat}} +*{{echo|foo-{bar}bat}} But if the broken language converter markup is inside an extension tag, nothing bad happens: -* <nowiki>foo-{bar}bat</nowiki> -* {{echo|<nowiki>foo-{bar}bat</nowiki>}} -* <pre>foo-{bar}bat</pre> -* {{echo|<pre>foo-{bar}bat</pre>}} +*<nowiki>foo-{bar}bat</nowiki> +*{{echo|<nowiki>foo-{bar}bat</nowiki>}} +*<pre>foo-{bar}bat</pre> +*{{echo|<pre>foo-{bar}bat</pre>}} <tag>foo-{bar}bat</tag> {{echo|<tag>foo-{bar}bat</tag>}} !! html/php+tidy -<p>This form breaks the template, which is unfortunate:</p> -<ul> -<li>{{echo|foo-{bar}bat}}</li> -</ul> -<p>But if the broken language converter markup is inside an extension tag, nothing bad happens:</p> -<ul> -<li>foo-{bar}bat</li> -<li>foo-{bar}bat</li> -<li> -<pre> -foo-{bar}bat -</pre></li> -<li> -<pre> -foo-{bar}bat -</pre></li> -</ul> -<pre> -'foo-{bar}bat' +<p>This form breaks the template, which is unfortunate: +</p> +<ul><li>{{echo|foo-{bar}bat}}</li></ul> +<p>But if the broken language converter markup is inside an extension +tag, nothing bad happens: +</p> +<ul><li>foo-{bar}bat</li> +<li>foo-{bar}bat</li> +<li><pre>foo-{bar}bat</pre></li> +<li><pre>foo-{bar}bat</pre></li></ul> +<pre>'foo-{bar}bat' array ( ) </pre> -<pre> -'foo-{bar}bat' +<pre>'foo-{bar}bat' array ( ) </pre> @@ -12409,45 +12372,43 @@ Preprocessor precedence 13: broken language converter in external link !! options parsoid=wt2html !! wikitext -* [http://example.com/-{foo Example in URL] -* [http://example.com Example in -{link} description] -* {{echo|[http://example.com/-{foo Breaks template, however]}} +*[http://example.com/-{foo Example in URL] +*[http://example.com Example in -{link} description] +*{{echo|[http://example.com/-{foo Breaks template, however]}} !! html/php+tidy -<ul> -<li><a rel="nofollow" class="external text" href="http://example.com/-{foo">Example in URL</a></li> +<ul><li><a rel="nofollow" class="external text" href="http://example.com/-{foo">Example in URL</a></li> <li><a rel="nofollow" class="external text" href="http://example.com">Example in -{link} description</a></li> -<li>{{echo|<a rel="nofollow" class="external text" href="http://example.com/-{foo">Breaks template, however</a>}}</li> -</ul> +<li>{{echo|<a rel="nofollow" class="external text" href="http://example.com/-{foo">Breaks template, however</a>}}</li></ul> !! html/parsoid <ul> -<li><a rel="mw:ExtLink" href="http://example.com/-{foo">Example in URL</a></li> -<li><a rel="mw:ExtLink" href="http://example.com">Example in -{link} description</a></li> -<li>{{echo|<a rel="mw:ExtLink" href="http://example.com/-{foo">Breaks template, however</a>}}</li> +<li><a rel="mw:ExtLink" class="external text" href="http://example.com/-{foo">Example in URL</a></li> +<li><a rel="mw:ExtLink" class="external text" href="http://example.com">Example in -{link} description</a></li> +<li>{{echo|<a rel="mw:ExtLink" class="external text" href="http://example.com/-{foo">Breaks template, however</a>}}</li> </ul> !! end !! test Preprocessor precedence 14: broken language converter in comment !! wikitext -* <!--{{foo}}--> ...should be ok -* <!---{{foo}}--> ...extra dashes -* {{echo|foo<!-- -{bar} -->bat}} ...should be ok +*<!--{{foo}}-->...should be ok +*<!---{{foo}}-->...extra dashes +*{{echo|foo<!-- -{bar} -->bat}}...should be ok !! html/php+tidy -<ul> -<li>...should be ok</li> +<ul><li>...should be ok</li> <li>...extra dashes</li> -<li>foobat ...should be ok</li> -</ul> +<li>foobat...should be ok</li></ul> !! html/parsoid <ul> -<li><!--{{foo}}--> ...should be ok</li> -<li><!---{{foo}}--> ...extra dashes</li> -<li><span about="#mwt1" typeof="mw:Transclusion" data-mw='{"parts":[{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"foo<!-- -{bar} -->bat"}},"i":0}}]}'>foo</span><span about="#mwt1"><!-- -{bar} --></span><span about="#mwt1">bat</span> ...should be ok</li> +<li><!--{{foo}}-->...should be ok</li> +<li><!---{{foo}}-->...extra dashes</li> +<li><span about="#mwt1" typeof="mw:Transclusion" data-mw='{"parts":[{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"foo<!-- -{bar} -->bat"}},"i":0}}]}'>foo</span><span about="#mwt1"><!-- -{bar} --></span><span about="#mwt1">bat</span>...should be ok</li> </ul> !! end !! test Preprocessor precedence 15: broken brace markup in headings +!! config +wgFragmentMode=[ 'html5', 'legacy' ] !! options parsoid=wt2html !! wikitext @@ -12465,32 +12426,37 @@ __NOTOC__ __NOEDITSECTION__ ===6 foo-{bar 6=== 6 !! html/php+tidy -<h3><span class="mw-headline" id="1_foo.5Bbar_1">1 foo[bar 1</span></h3> -<p>1</p> -<h3><span class="mw-headline" id="2_foo.5B.5Bbar_2">2 foo[[bar 2</span></h3> -<p>2</p> -<h3><span class="mw-headline" id="3_foo.7Bbar_3">3 foo{bar 3</span></h3> -<p>3</p> -<h3><span class="mw-headline" id="4_foo.7B.7Bbar_4">4 foo{{bar 4</span></h3> -<p>4</p> -<h3><span class="mw-headline" id="5_foo.7B.7B.7Bbar_5">5 foo{{{bar 5</span></h3> -<p>5</p> -<h3><span class="mw-headline" id="6_foo-.7Bbar_6">6 foo-{bar 6</span></h3> -<p>6</p> +<h3><span id="1_foo.5Bbar_1"></span><span class="mw-headline" id="1_foo[bar_1">1 foo[bar 1</span></h3> +<p>1 +</p> +<h3><span id="2_foo.5B.5Bbar_2"></span><span class="mw-headline" id="2_foo[[bar_2">2 foo[[bar 2</span></h3> +<p>2 +</p> +<h3><span id="3_foo.7Bbar_3"></span><span class="mw-headline" id="3_foo{bar_3">3 foo{bar 3</span></h3> +<p>3 +</p> +<h3><span id="4_foo.7B.7Bbar_4"></span><span class="mw-headline" id="4_foo{{bar_4">4 foo{{bar 4</span></h3> +<p>4 +</p> +<h3><span id="5_foo.7B.7B.7Bbar_5"></span><span class="mw-headline" id="5_foo{{{bar_5">5 foo{{{bar 5</span></h3> +<p>5 +</p> +<h3><span id="6_foo-.7Bbar_6"></span><span class="mw-headline" id="6_foo-{bar_6">6 foo-{bar 6</span></h3> +<p>6 +</p> !! html/parsoid -<meta property="mw:PageProp/notoc"/> <meta property="mw:PageProp/noeditsection"/ -> -<h3>1 foo[bar 1</h3> +<meta property="mw:PageProp/notoc"/> <meta property="mw:PageProp/noeditsection"/> +<h3 id="1_foo[bar_1"><span id="1_foo.5Bbar_1" typeof="mw:FallbackId"></span>1 foo[bar 1</h3> <p>1</p> -<h3>2 foo[[bar 2</h3> +<h3 id="2_foo[[bar_2"><span id="2_foo.5B.5Bbar_2" typeof="mw:FallbackId"></span>2 foo[[bar 2</h3> <p>2</p> -<h3>3 foo{bar 3</h3> +<h3 id="3_foo{bar_3"><span id="3_foo.7Bbar_3" typeof="mw:FallbackId"></span>3 foo{bar 3</h3> <p>3</p> -<h3>4 foo{{bar 4</h3> +<h3 id="4_foo{{bar_4"><span id="4_foo.7B.7Bbar_4" typeof="mw:FallbackId"></span>4 foo{{bar 4</h3> <p>4</p> -<h3>5 foo{{{bar 5</h3> +<h3 id="5_foo{{{bar_5"><span id="5_foo.7B.7B.7Bbar_5" typeof="mw:FallbackId"></span>5 foo{{{bar 5</h3> <p>5</p> -<h3>6 foo-{bar 6</h3> +<h3 id="6_foo-{bar_6"><span id="6_foo-.7Bbar_6" typeof="mw:FallbackId"></span>6 foo-{bar 6</h3> <p>6</p> !! end @@ -12508,6 +12474,45 @@ parsoid=wt2html <p><span typeof="mw:LanguageVariant" data-mw-variant='{"disabled":{"t":"<span about=\"#mwt1\" typeof=\"mw:Transclusion\" data-parsoid='{\"pi\":[[{\"k\":\"1\"}]],\"dsr\":[2,14,null,null]}' data-mw='{\"parts\":[{\"template\":{\"target\":{\"wt\":\"echo\",\"href\":\"./Template:Echo\"},\"params\":{\"1\":{\"wt\":\"foo\"}},\"i\":0}}]}'>foo</span>bar"}}'></span></p> !! end +!! test +Preprocessor precedence 17: template w/o target shouldn't prevent closing +!! options +parsoid=wt2html +!! wikitext +{{echo|hi {{}}}} +!! html/php +<p>hi {{}} +</p> +!! html/parsoid +<p about="#mwt1" typeof="mw:Transclusion" data-mw='{"parts":[{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"hi {{}}"}},"i":0}}]}'>hi {{}}</p> +!! end + +!! test +Preprocessor precedence 18: another rightmost wins scenario +!! options +parsoid=wt2html +!! wikitext +{{ -{{{{1|tplarg}}} }} }- +!! html/php +<p>{{ -{tplarg }} }- +</p> +!! html/parsoid +<p>{{ -{<span about="#mwt1" typeof="mw:Param" data-mw='{"parts":[{"templatearg":{"target":{"wt":"1"},"params":{"1":{"wt":"tplarg"}},"i":0}}]}'>tplarg</span> }} }-</p> +!! end + +!! test +Preprocessor precedence 19: break syntax +!! options +parsoid=wt2html +!! wikitext +-{{ +!! html/php +<p>-{{ +</p> +!! html/parsoid +<p>-{{</p> +!! end + ### ### Token Stream Patcher tests ### @@ -12637,9 +12642,7 @@ Templates: 2. Inside a block tag !! html+tidy <div>Foo</div> -<blockquote> -<p>Foo</p> -</blockquote> +<blockquote><p>Foo</p></blockquote> !!end !!test @@ -12678,9 +12681,9 @@ Templates: P-wrapping: 1c. Templates on consecutive lines bar <div>baz</div> !! html+tidy -<p>Foo</p> -<p>bar</p> -<div>baz</div> +<p>Foo +</p><p> +bar </p><div>baz</div> !! end !!test @@ -12974,17 +12977,17 @@ Templates: Support for templates generating attributes and content 4. Entities and nowikis inside templated attributes should be handled correctly inside templated tables !! wikitext {| -| {{table_attribs_6}} hi +|{{table_attribs_6}} hi |} !! html/php <table> <tr> -<td style="background: red;"> hi +<td style="background: red;">hi </td></tr></table> !! html/parsoid <table> -<tbody><tr><td style="background: red;" typeof="mw:Transclusion" about="#mwt1" data-parsoid='{"autoInsertedEnd":true,"pi":[[]]}' data-mw='{"parts":["| ",{"template":{"target":{"wt":"table_attribs_6","href":"./Template:Table_attribs_6"},"params":{},"i":0}}," hi"]}'> hi</td></tr> +<tbody><tr><td style="background: red;" typeof="mw:Transclusion" about="#mwt1" data-parsoid='{"autoInsertedEnd":true,"pi":[[]]}' data-mw='{"parts":["|",{"template":{"target":{"wt":"table_attribs_6","href":"./Template:Table_attribs_6"},"params":{},"i":0}}," hi"]}'> hi</td></tr> </tbody></table> !! end @@ -13103,12 +13106,13 @@ Templates: Wiki Tables: 1a. Fostering of entire template content a <tr><td></td></tr></table> -!! html+tidy -<p>a</p> -<table> -<tr> -<td></td> -</tr> +!! html/php+tidy + +a +<table><tbody><tr><td></td></tr></tbody></table> +!! html/parsoid +<p about="#mwt2" typeof="mw:Transclusion" data-parsoid='{"fostered":true,"autoInsertedEnd":true,"firstWikitextNode":"TABLE","pi":[[{"k":"1"}]]}' data-mw='{"parts":["{|\n",{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"a"}},"i":0}},"\n|}"]}'>a</p><table about="#mwt2"> + </table> !! end @@ -13128,14 +13132,18 @@ foo </div> <tr><td></td></tr></table> -!! html+tidy +!! html/php+tidy <div> +<p>foo +</p> +</div><table> + +<tbody><tr><td></td></tr></tbody></table> +!! html/parsoid +<div about="#mwt3" typeof="mw:Transclusion" data-parsoid='{"stx":"html","fostered":true,"autoInsertedEnd":true,"firstWikitextNode":"TABLE","pi":[[{"k":"1"}],[{"k":"1"}]]}' data-mw='{"parts":["{|\n",{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"<div>"}},"i":0}},"\nfoo\n",{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"</div>"}},"i":1}},"\n|}"]}'> <p>foo</p> -</div> -<table> -<tr> -<td></td> -</tr> +</div><table about="#mwt3"> + </table> !! end @@ -13152,13 +13160,15 @@ a <div>b</div> <tr><td></td></tr></table> -!! html+tidy -<p>a</p> -<div>b</div> -<table> -<tr> -<td></td> -</tr> +!! html/php+tidy + +a +<div>b</div><table> +<tbody><tr><td></td></tr></tbody></table> +!! html/parsoid +<p about="#mwt2" typeof="mw:Transclusion" data-parsoid='{"fostered":true,"autoInsertedEnd":true,"firstWikitextNode":"TABLE","pi":[[{"k":"1"}]]}' data-mw='{"parts":["{|\n",{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"a\n<div>b</div>"}},"i":0}},"\n|}"]}'>a</p><div about="#mwt2">b</div><table about="#mwt2"> + + </table> !! end @@ -13228,7 +13238,7 @@ Templates: Wiki Tables: 7. Fosterable <ref>s should get fostered <references /> !!html/parsoid -<span about="#mwt2" class="mw-ref" id="cite_ref-1" rel="dc:references" typeof="mw:Transclusion mw:Extension/ref" data-mw='{"parts":[{"template":{"target":{"wt":"PartialTable","href":"./Template:PartialTable"},"params":{},"i":0}},"<ref>foo</ref>\n|}"]}'><a href="./Main_Page#cite_note-1"><span class="mw-reflink-text">[1]</span></a></span><table about="#mwt2"> +<sup about="#mwt2" class="mw-ref" id="cite_ref-1" rel="dc:references" typeof="mw:Transclusion mw:Extension/ref" data-mw='{"parts":[{"template":{"target":{"wt":"PartialTable","href":"./Template:PartialTable"},"params":{},"i":0}},"<ref>foo</ref>\n|}"]}'><a href="./Main_Page#cite_note-1"><span class="mw-reflink-text">[1]</span></a></sup><table about="#mwt2"> <tbody> </tbody></table> @@ -13307,78 +13317,10 @@ a<div>b{{echo|c</div>d}}e a<div>bc</div>de !! html+tidy -<p>a</p> -<div>bc</div> -<p>de</p> +<p>a</p><div>bc</div><p>de +</p> !! end -!!test -Templates: Ugly templates: 1. Navbox template parses badly leading to table misnesting -(Parsoid-centric) -!! options -parsoid -!! wikitext -{| -|{{echo|foo</table>}} -|bar -|} -!! html -<table about="#mwt1" typeof="mw:Transclusion" data-mw='{"parts":["{|\n|",{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"foo</table>"}},"i":0}},"\n|bar\n|}"]}'> - -<tbody> -<tr> -<td>foo</td></tr></tbody></table><span about="#mwt1"> -</span><span about="#mwt1">|bar</span><span about="#mwt1"> -|}</span> -!!end - -!!test -Templates: Ugly templates: 2. Navbox template parses badly leading to table misnesting -(Parsoid-centric) -!! options -parsoid -!! wikitext -<table> - <tr> - <td> - <table> - <tr> - <td>1. {{echo|foo </table>}}</td> - <td> bar </td> - <td>2. {{echo|baz </table>}}</td> - </tr> - <tr> - <td>abc</td> - </tr> - </table> - </td> - </tr> - <tr> - <td>xyz</td> - </tr> -</table> -!! html -<table about="#mwt2" typeof="mw:Transclusion" data-mw='{"parts":["<table>\n <tr>\n <td>\n <table>\n <tr>\n <td>1. ",{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"foo </table>"}},"i":0}},"</td>\n <td> bar </td>\n <td>2. ",{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"baz </table>"}},"i":1}},"</td>\n </tr>\n <tr>\n <td>abc</td>\n </tr>\n </table>\n </td>\n </tr>\n <tr>\n <td>xyz</td>\n </tr>\n</table>"]}'> - <tbody><tr> - <td> - <table> - <tbody><tr> - <td>1. foo </td></tr></tbody></table></td> - <td> bar </td> - <td>2. baz </td></tr></tbody></table><span about="#mwt2"> - </span><span about="#mwt2"> - </span><span about="#mwt2"> - </span><span about="#mwt2">abc</span><span about="#mwt2"> - </span><span about="#mwt2"> - </span><span about="#mwt2"> - </span><span about="#mwt2"> - </span><span about="#mwt2"> - </span><span about="#mwt2"> - </span><span about="#mwt2">xyz</span><span about="#mwt2"> - </span><span about="#mwt2"> -</span> -!!end - !! test Templates: Ugly templates: 3. newline-only template parameter !! wikitext @@ -14192,15 +14134,15 @@ parsoid=wt2html,wt2wt,html2html <p><a href="/wiki/File:Foobar.jpg" class="image"><img alt="Foobar.jpg" src="http://example.com/images/3/3a/Foobar.jpg" width="1941" height="220" /></a> </p> !! html/parsoid -<p><span class="mw-default-size" typeof="mw:Image"><a href="./File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="220" width="1941"/></a></span></p> +<p><figure-inline class="mw-default-size" typeof="mw:Image"><a href="./File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="220" width="1941"/></a></figure-inline></p> !! end !! test -Serialize simple image with figure-inline wrapper +Serialize simple image with span wrapper !! options parsoid=html2wt !! html/parsoid -<p><figure-inline class="mw-default-size" typeof="mw:Image"><a href="./File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="220" width="1941"/></a></figure-inline></p> +<p><span class="mw-default-size" typeof="mw:Image"><a href="./File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="220" width="1941"/></a></span></p> !! wikitext [[File:Foobar.jpg]] !! end @@ -14213,7 +14155,7 @@ Simple image (using File: namespace, now canonical) <p><a href="/wiki/File:Foobar.jpg" class="image"><img alt="Foobar.jpg" src="http://example.com/images/3/3a/Foobar.jpg" width="1941" height="220" /></a> </p> !! html/parsoid -<p><span class="mw-default-size" typeof="mw:Image"><a href="./File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="220" width="1941"/></a></span></p> +<p><figure-inline class="mw-default-size" typeof="mw:Image"><a href="./File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="220" width="1941"/></a></figure-inline></p> !! end !! test @@ -14311,6 +14253,8 @@ Titles in unlinked images (T23454) !! html/php <p><img alt="stuff" src="http://example.com/images/3/3a/Foobar.jpg" title="stuff" width="1941" height="220" /> </p> +!! html/parsoid +<p><figure-inline class="mw-default-size" typeof="mw:Image" data-mw='{"caption":"stuff"}'><span><img resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="220" width="1941"/></span></figure-inline></p> !! end !! test @@ -14330,7 +14274,7 @@ Linktrails should not work for images: [[File:Foobar.jpg]]s <p>Linktrails should not work for images: <a href="/wiki/File:Foobar.jpg" class="image"><img alt="Foobar.jpg" src="http://example.com/images/3/3a/Foobar.jpg" width="1941" height="220" /></a>s </p> !! html/parsoid -<p>Linktrails should not work for images: <span class="mw-default-size" typeof="mw:Image"><a href="./File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="220" width="1941"/></a></span>s</p> +<p>Linktrails should not work for images: <figure-inline class="mw-default-size" typeof="mw:Image"><a href="./File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="220" width="1941"/></a></figure-inline>s</p> !! end !! test @@ -14376,7 +14320,7 @@ parsoid=wt2html,wt2wt,html2html <p><a href="/wiki/File:Foobar.jpg" class="image"><img alt="Foobar.jpg" src="http://example.com/images/thumb/3/3a/Foobar.jpg/50px-Foobar.jpg" width="50" height="6" srcset="http://example.com/images/thumb/3/3a/Foobar.jpg/75px-Foobar.jpg 1.5x, http://example.com/images/thumb/3/3a/Foobar.jpg/100px-Foobar.jpg 2x" /></a> </p> !! html/parsoid -<p><span typeof="mw:Image mw:ExpandedAttrs" about="#mwt2" data-parsoid='{"optList":[{"ck":"width","ak":"{{echo|50px}}"}]}' data-mw='{"attribs":[["width",{"html":"<span about=\"#mwt1\" typeof=\"mw:Transclusion\" data-parsoid='{\"pi\":[[{\"k\":\"1\"}]],\"dsr\":[18,31,null,null]}' data-mw='{\"parts\":[{\"template\":{\"target\":{\"wt\":\"echo\",\"href\":\"./Template:Echo\"},\"params\":{\"1\":{\"wt\":\"50px\"}},\"i\":0}}]}'>50px</span>"}]]}'><a href="./File:Foobar.jpg" data-parsoid='{"a":{"href":"./File:Foobar.jpg"},"sa":{}}'><img resource="./File:Foobar.jpg" src="//example.com/images/thumb/3/3a/Foobar.jpg/50px-Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="6" width="50" data-parsoid='{"a":{"resource":"./File:Foobar.jpg","height":"6","width":"50"},"sa":{"resource":"File:Foobar.jpg"}}'/></a></span></p> +<p><figure-inline typeof="mw:Image mw:ExpandedAttrs" about="#mwt2" data-parsoid='{"optList":[{"ck":"width","ak":"{{echo|50px}}"}]}' data-mw='{"attribs":[["width",{"html":"<span about=\"#mwt1\" typeof=\"mw:Transclusion\" data-parsoid='{\"pi\":[[{\"k\":\"1\"}]],\"dsr\":[18,31,null,null]}' data-mw='{\"parts\":[{\"template\":{\"target\":{\"wt\":\"echo\",\"href\":\"./Template:Echo\"},\"params\":{\"1\":{\"wt\":\"50px\"}},\"i\":0}}]}'>50px</span>"}]]}'><a href="./File:Foobar.jpg" data-parsoid='{"a":{"href":"./File:Foobar.jpg"},"sa":{}}'><img resource="./File:Foobar.jpg" src="//example.com/images/thumb/3/3a/Foobar.jpg/50px-Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="6" width="50" data-parsoid='{"a":{"resource":"./File:Foobar.jpg","height":"6","width":"50"},"sa":{"resource":"File:Foobar.jpg"}}'/></a></figure-inline></p> !! end ## Parsoid does not provide editing support for images where templates produce multiple image attributes. @@ -14407,20 +14351,13 @@ thumbsize=220 123<div class="thumb tright"><div class="thumbinner" style="width:222px;"><a href="/wiki/File:Foobar.jpg" class="image"><img alt="Foobar.jpg" src="http://example.com/images/thumb/3/3a/Foobar.jpg/220px-Foobar.jpg" width="220" height="25" class="thumbimage" srcset="http://example.com/images/thumb/3/3a/Foobar.jpg/330px-Foobar.jpg 1.5x, http://example.com/images/thumb/3/3a/Foobar.jpg/440px-Foobar.jpg 2x" /></a> <div class="thumbcaption"><div class="magnify"><a href="/wiki/File:Foobar.jpg" class="internal" title="Enlarge"></a></div></div></div></div>456 !! html/php+tidy -<p>123<a href="/wiki/File:Foobar.jpg" class="image"><img alt="Foobar.jpg" src="http://example.com/images/3/3a/Foobar.jpg" width="1941" height="220" /></a>456</p> -<p>123</p> -<div class="floatright"><a href="/wiki/File:Foobar.jpg" class="image"><img alt="Foobar.jpg" src="http://example.com/images/3/3a/Foobar.jpg" width="1941" height="220" /></a></div> -<p>456 123</p> -<div class="thumb tright"> -<div class="thumbinner" style="width:222px;"><a href="/wiki/File:Foobar.jpg" class="image"><img alt="Foobar.jpg" src="http://example.com/images/thumb/3/3a/Foobar.jpg/220px-Foobar.jpg" width="220" height="25" class="thumbimage" srcset="http://example.com/images/thumb/3/3a/Foobar.jpg/330px-Foobar.jpg 1.5x, http://example.com/images/thumb/3/3a/Foobar.jpg/440px-Foobar.jpg 2x" /></a> -<div class="thumbcaption"> -<div class="magnify"><a href="/wiki/File:Foobar.jpg" class="internal" title="Enlarge"></a></div> -</div> -</div> -</div> -<p>456</p> +<p>123<a href="/wiki/File:Foobar.jpg" class="image"><img alt="Foobar.jpg" src="http://example.com/images/3/3a/Foobar.jpg" width="1941" height="220" /></a>456 +</p><p> +123</p><div class="floatright"><a href="/wiki/File:Foobar.jpg" class="image"><img alt="Foobar.jpg" src="http://example.com/images/3/3a/Foobar.jpg" width="1941" height="220" /></a></div><p>456 +123</p><div class="thumb tright"><div class="thumbinner" style="width:222px;"><a href="/wiki/File:Foobar.jpg" class="image"><img alt="Foobar.jpg" src="http://example.com/images/thumb/3/3a/Foobar.jpg/220px-Foobar.jpg" width="220" height="25" class="thumbimage" srcset="http://example.com/images/thumb/3/3a/Foobar.jpg/330px-Foobar.jpg 1.5x, http://example.com/images/thumb/3/3a/Foobar.jpg/440px-Foobar.jpg 2x" /></a> <div class="thumbcaption"><div class="magnify"><a href="/wiki/File:Foobar.jpg" class="internal" title="Enlarge"></a></div></div></div></div><p>456 +</p> !! html/parsoid -<p>123<span class="mw-default-size" typeof="mw:Image"><a href="./File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="220" width="1941"/></a></span>456</p> +<p>123<figure-inline class="mw-default-size" typeof="mw:Image"><a href="./File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="220" width="1941"/></a></figure-inline>456</p> <p>123</p><figure class="mw-default-size mw-halign-right" typeof="mw:Image"><a href="./File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="220" width="1941"/></a></figure><p>456</p> <p>123</p><figure class="mw-default-size" typeof="mw:Image/Thumb"><a href="./File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/thumb/3/3a/Foobar.jpg/220px-Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="25" width="220"/></a></figure><p>456</p> !! end @@ -14444,7 +14381,7 @@ Image with multiple widths -- use last <p><a href="/wiki/File:Foobar.jpg" class="image" title="caption"><img alt="caption" src="http://example.com/images/thumb/3/3a/Foobar.jpg/300px-Foobar.jpg" width="300" height="34" srcset="http://example.com/images/thumb/3/3a/Foobar.jpg/450px-Foobar.jpg 1.5x, http://example.com/images/thumb/3/3a/Foobar.jpg/600px-Foobar.jpg 2x" /></a> </p> !! html/parsoid -<p><span typeof="mw:Image" data-mw='{"caption":"caption"}'><a href="./File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/thumb/3/3a/Foobar.jpg/300px-Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="34" width="300"/></a></span></p> +<p><figure-inline typeof="mw:Image" data-mw='{"caption":"caption"}'><a href="./File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/thumb/3/3a/Foobar.jpg/300px-Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="34" width="300"/></a></figure-inline></p> !! end !! test @@ -14461,7 +14398,7 @@ thumbsize=220 </p> !! html/parsoid <figure class="mw-default-size mw-halign-left" typeof="mw:Image/Thumb"><a href="./File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/thumb/3/3a/Foobar.jpg/220px-Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="25" width="220"/></a><figcaption>caption</figcaption></figure> -<p><span class="mw-default-size mw-valign-middle" typeof="mw:Image" data-mw='{"caption":"caption"}'><a href="./File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="220" width="1941"/></a></span></p> +<p><figure-inline class="mw-default-size mw-valign-middle" typeof="mw:Image" data-mw='{"caption":"caption"}'><a href="./File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="220" width="1941"/></a></figure-inline></p> !! end !! test @@ -14494,7 +14431,7 @@ parsoid=wt2html,wt2wt,html2html <a href="/wiki/File:Foobar.jpg" class="image"><img alt="Foobar.jpg" src="http://example.com/images/thumb/3/3a/Foobar.jpg/177px-Foobar.jpg" width="177" height="20" srcset="http://example.com/images/thumb/3/3a/Foobar.jpg/265px-Foobar.jpg 1.5x, http://example.com/images/thumb/3/3a/Foobar.jpg/353px-Foobar.jpg 2x" /></a> </p> !! html/parsoid -<p><span typeof="mw:Image"><a href="./File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/thumb/3/3a/Foobar.jpg/20px-Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="2" width="20"/></a></span> <span typeof="mw:Image"><a href="./File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/thumb/3/3a/Foobar.jpg/177px-Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="20" width="177"/></a></span></p> +<p><figure-inline typeof="mw:Image"><a href="./File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/thumb/3/3a/Foobar.jpg/20px-Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="2" width="20"/></a></figure-inline> <figure-inline typeof="mw:Image"><a href="./File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/thumb/3/3a/Foobar.jpg/177px-Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="20" width="177"/></a></figure-inline></p> !! end !! test @@ -14505,7 +14442,7 @@ Image with link parameter, wiki target <p><a href="/wiki/Main_Page" title="Main Page"><img alt="Foobar.jpg" src="http://example.com/images/3/3a/Foobar.jpg" width="1941" height="220" /></a> </p> !! html/parsoid -<p><span class="mw-default-size" typeof="mw:Image"><a href="./Main_Page"><img resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="220" width="1941"/></a></span></p> +<p><figure-inline class="mw-default-size" typeof="mw:Image"><a href="./Main_Page"><img resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="220" width="1941"/></a></figure-inline></p> !! end # parsoid T51293 (part 1) @@ -14517,7 +14454,7 @@ Image with link parameter, URL target <p><a href="http://example.com/" rel="nofollow"><img alt="Foobar.jpg" src="http://example.com/images/3/3a/Foobar.jpg" width="1941" height="220" /></a> </p> !! html/parsoid -<p><span class="mw-default-size" typeof="mw:Image"><a href="http://example.com/"><img resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="220" width="1941"/></a></span></p> +<p><figure-inline class="mw-default-size" typeof="mw:Image"><a href="http://example.com/"><img resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="220" width="1941"/></a></figure-inline></p> !! end # parsoid T51293 (part 2) @@ -14529,7 +14466,7 @@ Image with link parameter, protocol-less URL target <p><a href="//example.com/" rel="nofollow"><img alt="Foobar.jpg" src="http://example.com/images/3/3a/Foobar.jpg" width="1941" height="220" /></a> </p> !! html/parsoid -<p><span class="mw-default-size" typeof="mw:Image"><a href="//example.com/"><img resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="220" width="1941"/></a></span></p> +<p><figure-inline class="mw-default-size" typeof="mw:Image"><a href="//example.com/"><img resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="220" width="1941"/></a></figure-inline></p> !! end !! test @@ -14565,7 +14502,7 @@ Image with link parameter, wgNoFollowLinks set to false [[Image:foobar.jpg|link=http://example.com/]] !! config wgNoFollowLinks=false -!! html +!! html/php <p><a href="http://example.com/"><img alt="Foobar.jpg" src="http://example.com/images/3/3a/Foobar.jpg" width="1941" height="220" /></a> </p> !! end @@ -14576,7 +14513,7 @@ Image with link parameter, wgNoFollowDomainExceptions [[Image:foobar.jpg|link=http://example.com/]] !! config wgNoFollowDomainExceptions='example.com' -!! html +!! html/php <p><a href="http://example.com/"><img alt="Foobar.jpg" src="http://example.com/images/3/3a/Foobar.jpg" width="1941" height="220" /></a> </p> !! end @@ -14601,7 +14538,7 @@ Image with empty link parameter <p><img alt="Foobar.jpg" src="http://example.com/images/3/3a/Foobar.jpg" width="1941" height="220" /> </p> !! html/parsoid -<p><span class="mw-default-size" typeof="mw:Image"><span><img resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="220" width="1941"/></span></span></p> +<p><figure-inline class="mw-default-size" typeof="mw:Image"><span><img resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="220" width="1941"/></span></figure-inline></p> !! end !! test @@ -14612,7 +14549,7 @@ Image with link parameter (wiki target) and unnamed parameter <p><a href="/wiki/Main_Page" title="Title"><img alt="Title" src="http://example.com/images/3/3a/Foobar.jpg" width="1941" height="220" /></a> </p> !! html/parsoid -<p><span class="mw-default-size" typeof="mw:Image" data-mw='{"caption":"Title"}'><a href="./Main_Page"><img resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="220" width="1941"/></a></span></p> +<p><figure-inline class="mw-default-size" typeof="mw:Image" data-mw='{"caption":"Title"}'><a href="./Main_Page"><img resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="220" width="1941"/></a></figure-inline></p> !! end !! test @@ -14623,7 +14560,7 @@ Image with link parameter (URL target) and unnamed parameter <p><a href="http://example.com/" title="Title" rel="nofollow"><img alt="Title" src="http://example.com/images/3/3a/Foobar.jpg" width="1941" height="220" /></a> </p> !! html/parsoid -<p><span class="mw-default-size" typeof="mw:Image" data-mw='{"caption":"Title"}'><a href="http://example.com/"><img resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="220" width="1941"/></a></span></p> +<p><figure-inline class="mw-default-size" typeof="mw:Image" data-mw='{"caption":"Title"}'><a href="http://example.com/"><img resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="220" width="1941"/></a></figure-inline></p> !! end !! test @@ -14746,9 +14683,9 @@ Image with wiki markup in implicit alt </p><p><a href="/wiki/File:Foobar.jpg" class="image"><img alt="testing bold in alt" src="http://example.com/images/3/3a/Foobar.jpg" width="1941" height="220" /></a> </p> !! html/parsoid -<p><span class="mw-default-size" typeof="mw:Image" data-parsoid='{"optList":[{"ck":"caption","ak":"testing '''bold''' in alt"}]}' data-mw='{"caption":"testing <b data-parsoid='{\"dsr\":[27,37,3,3]}'>bold</b> in alt"}'><a href="./File:Foobar.jpg" data-parsoid='{"a":{"href":"./File:Foobar.jpg"},"sa":{}}'><img resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="220" width="1941" data-parsoid='{"a":{"resource":"./File:Foobar.jpg","height":"220","width":"1941"},"sa":{"resource":"Image:Foobar.jpg"}}'/></a></span></p> +<p><figure-inline class="mw-default-size" typeof="mw:Image" data-parsoid='{"optList":[{"ck":"caption","ak":"testing '''bold''' in alt"}]}' data-mw='{"caption":"testing <b data-parsoid='{\"dsr\":[27,37,3,3]}'>bold</b> in alt"}'><a href="./File:Foobar.jpg" data-parsoid='{"a":{"href":"./File:Foobar.jpg"},"sa":{}}'><img resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="220" width="1941" data-parsoid='{"a":{"resource":"./File:Foobar.jpg","height":"220","width":"1941"},"sa":{"resource":"Image:Foobar.jpg"}}'/></a></figure-inline></p> -<p><span class="mw-default-size" typeof="mw:Image" data-parsoid='{"optList":[{"ck":"alt","ak":"alt=testing '''bold''' in alt"}]}'><a href="./File:Foobar.jpg" data-parsoid='{"a":{"href":"./File:Foobar.jpg"},"sa":{}}'><img alt="testing bold in alt" resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="220" width="1941" data-parsoid='{"a":{"alt":"testing bold in alt","resource":"./File:Foobar.jpg","height":"220","width":"1941"},"sa":{"alt":"alt=testing '''bold''' in alt","resource":"Image:Foobar.jpg"}}'/></a></span></p> +<p><figure-inline class="mw-default-size" typeof="mw:Image" data-parsoid='{"optList":[{"ck":"alt","ak":"alt=testing '''bold''' in alt"}]}'><a href="./File:Foobar.jpg" data-parsoid='{"a":{"href":"./File:Foobar.jpg"},"sa":{}}'><img alt="testing bold in alt" resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="220" width="1941" data-parsoid='{"a":{"alt":"testing bold in alt","resource":"./File:Foobar.jpg","height":"220","width":"1941"},"sa":{"alt":"alt=testing '''bold''' in alt","resource":"Image:Foobar.jpg"}}'/></a></figure-inline></p> !! end !! test @@ -14759,7 +14696,65 @@ Alt image option should handle most kinds of wikitext without barfing <div class="thumb tright"><div class="thumbinner" style="width:182px;"><a href="/wiki/File:Foobar.jpg" class="image"><img alt="This is a link and a bold template." src="http://example.com/images/thumb/3/3a/Foobar.jpg/180px-Foobar.jpg" width="180" height="20" class="thumbimage" srcset="http://example.com/images/thumb/3/3a/Foobar.jpg/270px-Foobar.jpg 1.5x, http://example.com/images/thumb/3/3a/Foobar.jpg/360px-Foobar.jpg 2x" /></a> <div class="thumbcaption"><div class="magnify"><a href="/wiki/File:Foobar.jpg" class="internal" title="Enlarge"></a></div>This is the image caption</div></div></div> !! html/parsoid -<figure class="mw-default-size" typeof="mw:Image/Thumb mw:ExpandedAttrs" about="#mwt2" data-parsoid='{"optList":[{"ck":"thumbnail","ak":"thumb"},{"ck":"caption","ak":"This is the image caption"},{"ck":"alt","ak":"alt=This is a [[link]] and a {{echo|''bold template''}}."}]}' data-mw='{"attribs":[["thumbnail",{"html":"thumb"}],["alt",{"html":"alt=This is a <a rel=\"mw:WikiLink\" href=\"./Link\" title=\"Link\" data-parsoid='{\"stx\":\"simple\",\"a\":{\"href\":\"./Link\"},\"sa\":{\"href\":\"link\"},\"dsr\":[65,73,2,2]}'>link</a> and a <i about=\"#mwt1\" typeof=\"mw:Transclusion\" data-parsoid='{\"dsr\":[80,106,null,null],\"pi\":[[{\"k\":\"1\"}]]}' data-mw='{\"parts\":[{\"template\":{\"target\":{\"wt\":\"echo\",\"href\":\"./Template:Echo\"},\"params\":{\"1\":{\"wt\":\"&#39;&#39;bold template&#39;&#39;\"}},\"i\":0}}]}'>bold template</i>."}]]}'><a href="./File:Foobar.jpg" data-parsoid='{"a":{"href":"./File:Foobar.jpg"},"sa":{}}'><img alt="This is a link and a bold template." resource="./File:Foobar.jpg" src="//example.com/images/thumb/3/3a/Foobar.jpg/220px-Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="25" width="220" data-parsoid='{"a":{"alt":"This is a link and a bold template.","resource":"./File:Foobar.jpg","height":"25","width":"220"},"sa":{"alt":"alt=This is a [[link]] and a {{echo|''bold template''}}.","resource":"Image:Foobar.jpg"}}'/></a><figcaption>This is the image caption</figcaption></figure> +<figure class="mw-default-size" typeof="mw:Image/Thumb mw:ExpandedAttrs" about="#mwt2" data-parsoid='{"optList":[{"ck":"thumbnail","ak":"thumb"},{"ck":"caption","ak":"This is the image caption"},{"ck":"alt","ak":"alt=This is a [[link]] and a {{echo|''bold template''}}."}]}' data-mw='{"attribs":[["thumbnail",{"html":"thumb"}],["alt",{"html":"alt=This is a <a rel=\"mw:WikiLink\" href=\"./Link\" title=\"Link\" data-parsoid='{\"stx\":\"simple\",\"a\":{\"href\":\"./Link\"},\"sa\":{\"href\":\"link\"},\"dsr\":[65,73,2,2]}'>link</a> and a <i about=\"#mwt1\" typeof=\"mw:Transclusion\" data-parsoid='{\"dsr\":[80,106,null,null],\"pi\":[[{\"k\":\"1\"}]]}' data-mw='{\"parts\":[{\"template\":{\"target\":{\"wt\":\"echo\",\"href\":\"./Template:Echo\"},\"params\":{\"1\":{\"wt\":\"&apos;&apos;bold template&apos;&apos;\"}},\"i\":0}}]}'>bold template</i>."}]]}'><a href="./File:Foobar.jpg" data-parsoid='{"a":{"href":"./File:Foobar.jpg"},"sa":{}}'><img alt="This is a link and a bold template." resource="./File:Foobar.jpg" src="//example.com/images/thumb/3/3a/Foobar.jpg/220px-Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="25" width="220" data-parsoid='{"a":{"alt":"This is a link and a bold template.","resource":"./File:Foobar.jpg","height":"25","width":"220"},"sa":{"alt":"alt=This is a [[link]] and a {{echo|''bold template''}}.","resource":"Image:Foobar.jpg"}}'/></a><figcaption>This is the image caption</figcaption></figure> +!! end + +!! test +Image with table with attributes in caption +!! options +parsoid=wt2html,html2html +!! wikitext +[[File:Foobar.jpg|thumb| +{| class="123" | +|- class="456" | +| ha +|} +]] +!! html/parsoid +<figure class="mw-default-size" typeof="mw:Image/Thumb" data-parsoid='{"optList":[{"ck":"thumbnail","ak":"thumb"},{"ck":"caption","ak":"\n{| class=\"123\" |\n|- class=\"456\" |\n| ha\n|}\n"}]}'><a href="./File:Foobar.jpg" data-parsoid='{"a":{"href":"./File:Foobar.jpg"},"sa":{"href":"File:Foobar.jpg"}}'><img resource="./File:Foobar.jpg" src="//example.com/images/thumb/3/3a/Foobar.jpg/220px-Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="25" width="220" data-parsoid='{"a":{"resource":"./File:Foobar.jpg","height":"25","width":"220"},"sa":{"resource":"File:Foobar.jpg"}}'/></a><figcaption> +<table class="123"> +<tbody><tr class="456" data-parsoid='{"startTagSrc":"|-"}'> +<td> ha</td></tr> +</tbody></table> +</figcaption></figure> +!! end + +!! test +Image with table with rows from templates in caption +!! wikitext +[[File:Foobar.jpg|thumb| +{| +{{echo|{{!}} hi}} +|} +]] +!! html/parsoid +<figure class="mw-default-size" typeof="mw:Image/Thumb" data-parsoid='{"optList":[{"ck":"thumbnail","ak":"thumb"},{"ck":"caption","ak":"\n{|\n{{echo|{{!}} hi}}\n|}\n"}]}'><a href="./File:Foobar.jpg" data-parsoid='{"a":{"href":"./File:Foobar.jpg"},"sa":{"href":"File:Foobar.jpg"}}'><img resource="./File:Foobar.jpg" src="//example.com/images/thumb/3/3a/Foobar.jpg/220px-Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="25" width="220" data-parsoid='{"a":{"resource":"./File:Foobar.jpg","height":"25","width":"220"},"sa":{"resource":"File:Foobar.jpg"}}'/></a><figcaption> +<table> +<tbody about="#mwt4" typeof="mw:Transclusion" data-mw='{"parts":[{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"{{!}} hi"}},"i":0}},"\n"]}'><tr><td> hi</td></tr> +</tbody></table> +</figcaption></figure> +!! end + +!! test +Image with nested tables in caption +!! wikitext +[[File:Foobar.jpg|thumb|Foo<br /> +{| +| +{| +|z +|} +|} +]] +!! html/parsoid +<figure class="mw-default-size" typeof="mw:Image/Thumb" data-parsoid='{"optList":[{"ck":"thumbnail","ak":"thumb"},{"ck":"caption","ak":"Foo<br/>\n{|\n|\n{|\n|z\n|}\n|}\n"}]}'><a href="./File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/thumb/3/3a/Foobar.jpg/220px-Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="25" width="220" data-parsoid='{"a":{"resource":"./File:Foobar.jpg","height":"25","width":"220"},"sa":{"resource":"File:Foobar.jpg"}}'/></a><figcaption data-parsoid='{"dsr":[null,50,null,null]}'>Foo<br data-parsoid='{"stx":"html","selfClose":true}'/> +<table> +<tbody><tr><td> +<table> +<tbody><tr><td>z</td></tr> +</tbody></table></td></tr> +</tbody></table> +</figcaption></figure> !! end ################### @@ -14783,9 +14778,9 @@ parsoid=wt2html,wt2wt,html2html </p><p><a href="/wiki/File:Foobar.jpg" class="image" title="caption"><img alt="caption" src="http://example.com/images/thumb/3/3a/Foobar.jpg/180px-Foobar.jpg" width="180" height="20" srcset="http://example.com/images/thumb/3/3a/Foobar.jpg/270px-Foobar.jpg 1.5x, http://example.com/images/thumb/3/3a/Foobar.jpg/360px-Foobar.jpg 2x" /></a> </p> !! html/parsoid -<p><span class="mw-default-size" typeof="mw:Image/Frameless" data-mw='{"caption":"caption"}'><a href="./File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/thumb/3/3a/Foobar.jpg/220px-Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="25" width="220"/></a></span></p> -<p><span class="mw-default-size" typeof="mw:Image/Frameless" data-mw='{"caption":"caption"}'><a href="./File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/thumb/3/3a/Foobar.jpg/220px-Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="25" width="220"/></a></span></p> -<p><span class="mw-default-size" typeof="mw:Image/Frameless" data-mw='{"caption":"caption"}'><a href="./File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/thumb/3/3a/Foobar.jpg/220px-Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="25" width="220"/></a></span></p> +<p><figure-inline class="mw-default-size" typeof="mw:Image/Frameless" data-mw='{"caption":"caption"}'><a href="./File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/thumb/3/3a/Foobar.jpg/220px-Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="25" width="220"/></a></figure-inline></p> +<p><figure-inline class="mw-default-size" typeof="mw:Image/Frameless" data-mw='{"caption":"caption"}'><a href="./File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/thumb/3/3a/Foobar.jpg/220px-Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="25" width="220"/></a></figure-inline></p> +<p><figure-inline class="mw-default-size" typeof="mw:Image/Frameless" data-mw='{"caption":"caption"}'><a href="./File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/thumb/3/3a/Foobar.jpg/220px-Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="25" width="220"/></a></figure-inline></p> !! end !! test @@ -14850,8 +14845,8 @@ parsoid=wt2html,wt2wt,html2html </p><p><a href="/wiki/File:Foobar.jpg" class="image"><img alt="Foobar.jpg" src="http://example.com/images/3/3a/Foobar.jpg" width="2000" height="227" class="thumbborder" /></a> </p> !! html/parsoid -<p><span typeof="mw:Image"><a href="./File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="227" width="2000"/></a></span></p> -<p><span class="mw-image-border" typeof="mw:Image"><a href="./File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="227" width="2000"/></a></span></p> +<p><figure-inline typeof="mw:Image"><a href="./File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="227" width="2000"/></a></figure-inline></p> +<p><figure-inline class="mw-image-border" typeof="mw:Image"><a href="./File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="227" width="2000"/></a></figure-inline></p> !! end !! test @@ -14867,8 +14862,8 @@ parsoid=wt2html,wt2wt,html2html </p><p><a href="/wiki/File:Foobar.jpg" class="image"><img alt="Foobar.jpg" src="http://example.com/images/thumb/3/3a/Foobar.jpg/1000px-Foobar.jpg" width="1000" height="113" class="thumbborder" srcset="http://example.com/images/thumb/3/3a/Foobar.jpg/1500px-Foobar.jpg 1.5x, http://example.com/images/3/3a/Foobar.jpg 2x" /></a> </p> !! html/parsoid -<p><span typeof="mw:Image"><a href="./File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/thumb/3/3a/Foobar.jpg/1000px-Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="113" width="1000"/></a></span></p> -<p><span class="mw-image-border" typeof="mw:Image"><a href="./File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/thumb/3/3a/Foobar.jpg/1000px-Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="113" width="1000"/></a></span></p> +<p><figure-inline typeof="mw:Image"><a href="./File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/thumb/3/3a/Foobar.jpg/1000px-Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="113" width="1000"/></a></figure-inline></p> +<p><figure-inline class="mw-image-border" typeof="mw:Image"><a href="./File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/thumb/3/3a/Foobar.jpg/1000px-Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="113" width="1000"/></a></figure-inline></p> !! end !! test @@ -14911,7 +14906,7 @@ parsoid=wt2html,wt2wt,html2html <p><a href="/wiki/File:Foobar.jpg" class="image"><img alt="Foobar.jpg" src="http://example.com/images/thumb/3/3a/Foobar.jpg/50px-Foobar.jpg" width="50" height="6" srcset="http://example.com/images/thumb/3/3a/Foobar.jpg/75px-Foobar.jpg 1.5x, http://example.com/images/thumb/3/3a/Foobar.jpg/100px-Foobar.jpg 2x" /></a> </p> !! html/parsoid -<p><span typeof="mw:Image/Frameless"><a href="./File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/thumb/3/3a/Foobar.jpg/50px-Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="6" width="50"/></a></span></p> +<p><figure-inline typeof="mw:Image/Frameless"><a href="./File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/thumb/3/3a/Foobar.jpg/50px-Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="6" width="50"/></a></figure-inline></p> !! end !! test @@ -14927,8 +14922,8 @@ parsoid=wt2html,wt2wt,html2html </p><p><a href="/wiki/File:Foobar.svg" class="image"><img alt="Foobar.svg" src="http://example.com/images/thumb/f/ff/Foobar.svg/2000px-Foobar.svg.png" width="2000" height="1500" srcset="http://example.com/images/thumb/f/ff/Foobar.svg/3000px-Foobar.svg.png 1.5x, http://example.com/images/thumb/f/ff/Foobar.svg/4000px-Foobar.svg.png 2x" /></a> </p> !! html/parsoid -<p><span typeof="mw:Image/Frameless"><a href="./File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="220" width="1941"/></a></span></p> -<p><span typeof="mw:Image/Frameless"><a href="./File:Foobar.svg"><img resource="./File:Foobar.svg" src="//example.com/images/thumb/f/ff/Foobar.svg/2000px-Foobar.svg.png" data-file-width="240" data-file-height="180" data-file-type="drawing" height="1500" width="2000"/></a></span></p> +<p><figure-inline typeof="mw:Image/Frameless"><a href="./File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="220" width="1941"/></a></figure-inline></p> +<p><figure-inline typeof="mw:Image/Frameless"><a href="./File:Foobar.svg"><img resource="./File:Foobar.svg" src="//example.com/images/thumb/f/ff/Foobar.svg/2000px-Foobar.svg.png" data-file-width="240" data-file-height="180" data-file-type="drawing" height="1500" width="2000"/></a></figure-inline></p> !! end !! test @@ -14986,7 +14981,7 @@ Frameless image caption with a free URL <p><a href="/wiki/File:Foobar.jpg" class="image" title="http://example.com"><img alt="http://example.com" src="http://example.com/images/3/3a/Foobar.jpg" width="1941" height="220" /></a> </p> !! html/parsoid -<p><span class="mw-default-size" typeof="mw:Image" data-parsoid='{"optList":[{"ck":"caption","ak":"http://example.com"}]}' data-mw='{"caption":"<a rel=\"mw:ExtLink\" href=\"http://example.com\" data-parsoid='{\"stx\":\"url\",\"dsr\":[18,36,0,0]}'>http://example.com</a>"}'><a href="./File:Foobar.jpg" data-parsoid='{"a":{"href":"./File:Foobar.jpg"},"sa":{}}'><img resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="220" width="1941" data-parsoid='{"a":{"resource":"./File:Foobar.jpg","height":"220","width":"1941"},"sa":{"resource":"File:Foobar.jpg"}}'/></a></span></p> +<p><figure-inline class="mw-default-size" typeof="mw:Image" data-parsoid='{"optList":[{"ck":"caption","ak":"http://example.com"}]}' data-mw='{"caption":"<a rel=\"mw:ExtLink\" href=\"http://example.com\" data-parsoid='{\"stx\":\"url\",\"dsr\":[18,36,0,0]}'>http://example.com</a>"}'><a href="./File:Foobar.jpg" data-parsoid='{"a":{"href":"./File:Foobar.jpg"},"sa":{}}'><img resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="220" width="1941" data-parsoid='{"a":{"resource":"./File:Foobar.jpg","height":"220","width":"1941"},"sa":{"resource":"File:Foobar.jpg"}}'/></a></figure-inline></p> !! end !! test @@ -14999,7 +14994,7 @@ thumbsize=220 <div class="thumb tright"><div class="thumbinner" style="width:222px;"><a href="/wiki/File:Foobar.jpg" class="image"><img alt="" src="http://example.com/images/thumb/3/3a/Foobar.jpg/220px-Foobar.jpg" width="220" height="25" class="thumbimage" srcset="http://example.com/images/thumb/3/3a/Foobar.jpg/330px-Foobar.jpg 1.5x, http://example.com/images/thumb/3/3a/Foobar.jpg/440px-Foobar.jpg 2x" /></a> <div class="thumbcaption"><div class="magnify"><a href="/wiki/File:Foobar.jpg" class="internal" title="Enlarge"></a></div><a rel="nofollow" class="external free" href="http://example.com">http://example.com</a></div></div></div> !! html/parsoid -<figure class="mw-default-size" typeof="mw:Image/Thumb"><a href="./File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/thumb/3/3a/Foobar.jpg/220px-Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="25" width="220"/></a><figcaption><a rel="mw:ExtLink" href="http://example.com">http://example.com</a></figcaption></figure> +<figure class="mw-default-size" typeof="mw:Image/Thumb"><a href="./File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/thumb/3/3a/Foobar.jpg/220px-Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="25" width="220"/></a><figcaption><a rel="mw:ExtLink" class="external free" href="http://example.com">http://example.com</a></figcaption></figure> !! end !! test @@ -15013,7 +15008,7 @@ parsoid=wt2html,wt2wt,html2html <div class="thumb tright"><div class="thumbinner" style="width:222px;"><a href="/wiki/File:Foobar.jpg" class="image"><img alt="Alteration" src="http://example.com/images/thumb/3/3a/Foobar.jpg/220px-Foobar.jpg" width="220" height="25" class="thumbimage" srcset="http://example.com/images/thumb/3/3a/Foobar.jpg/330px-Foobar.jpg 1.5x, http://example.com/images/thumb/3/3a/Foobar.jpg/440px-Foobar.jpg 2x" /></a> <div class="thumbcaption"><div class="magnify"><a href="/wiki/File:Foobar.jpg" class="internal" title="Enlarge"></a></div><a rel="nofollow" class="external free" href="http://example.com">http://example.com</a></div></div></div> !! html/parsoid -<figure class="mw-default-size" typeof="mw:Image/Thumb"><a href="./File:Foobar.jpg"><img alt="Alteration" resource="./File:Foobar.jpg" src="//example.com/images/thumb/3/3a/Foobar.jpg/220px-Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="25" width="220"/></a><figcaption><a rel="mw:ExtLink" href="http://example.com">http://example.com</a></figcaption></figure> +<figure class="mw-default-size" typeof="mw:Image/Thumb"><a href="./File:Foobar.jpg"><img alt="Alteration" resource="./File:Foobar.jpg" src="//example.com/images/thumb/3/3a/Foobar.jpg/220px-Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="25" width="220"/></a><figcaption><a rel="mw:ExtLink" class="external free" href="http://example.com">http://example.com</a></figcaption></figure> !! end !! test @@ -15046,12 +15041,12 @@ SVG thumbnails with invalid language code !! options parsoid=wt2html,wt2wt,html2html !! wikitext -[[File:Foobar.svg|thumb|caption|lang=invalid.language.code]] +[[File:Foobar.svg|thumb|caption|lang=invalid:language:code]] !! html/php -<div class="thumb tright"><div class="thumbinner" style="width:182px;"><a href="/wiki/File:Foobar.svg" class="image"><img alt="" src="http://example.com/images/thumb/f/ff/Foobar.svg/180px-Foobar.svg.png" width="180" height="135" class="thumbimage" srcset="http://example.com/images/thumb/f/ff/Foobar.svg/270px-Foobar.svg.png 1.5x, http://example.com/images/thumb/f/ff/Foobar.svg/360px-Foobar.svg.png 2x" /></a> <div class="thumbcaption"><div class="magnify"><a href="/wiki/File:Foobar.svg" class="internal" title="Enlarge"></a></div>lang=invalid.language.code</div></div></div> +<div class="thumb tright"><div class="thumbinner" style="width:182px;"><a href="/wiki/File:Foobar.svg" class="image"><img alt="" src="http://example.com/images/thumb/f/ff/Foobar.svg/180px-Foobar.svg.png" width="180" height="135" class="thumbimage" srcset="http://example.com/images/thumb/f/ff/Foobar.svg/270px-Foobar.svg.png 1.5x, http://example.com/images/thumb/f/ff/Foobar.svg/360px-Foobar.svg.png 2x" /></a> <div class="thumbcaption"><div class="magnify"><a href="/wiki/File:Foobar.svg" class="internal" title="Enlarge"></a></div>lang=invalid:language:code</div></div></div> !! html/parsoid -<figure class="mw-default-size" typeof="mw:Image/Thumb"><a href="./File:Foobar.svg"><img resource="./File:Foobar.svg" src="//example.com/images/f/ff/Foobar.svg" data-file-width="240" data-file-height="180" data-file-type="drawing" height="165" width="220"/></a><figcaption>lang=invalid.language.code</figcaption></figure> +<figure class="mw-default-size" typeof="mw:Image/Thumb"><a href="./File:Foobar.svg"><img resource="./File:Foobar.svg" src="//example.com/images/thumb/f/ff/Foobar.svg/220px-Foobar.svg.png" data-file-width="240" data-file-height="180" data-file-type="drawing" height="165" width="220"/></a><figcaption>lang=invalid:language:code</figcaption></figure> !! end !! test @@ -15070,10 +15065,10 @@ T3887: A RFC with a thumbnail !! wikitext [[File:Foobar.jpg|thumb|This is RFC 12354]] !! html/php -<div class="thumb tright"><div class="thumbinner" style="width:182px;"><a href="/wiki/File:Foobar.jpg" class="image"><img alt="" src="http://example.com/images/thumb/3/3a/Foobar.jpg/180px-Foobar.jpg" width="180" height="20" class="thumbimage" srcset="http://example.com/images/thumb/3/3a/Foobar.jpg/270px-Foobar.jpg 1.5x, http://example.com/images/thumb/3/3a/Foobar.jpg/360px-Foobar.jpg 2x" /></a> <div class="thumbcaption"><div class="magnify"><a href="/wiki/File:Foobar.jpg" class="internal" title="Enlarge"></a></div>This is <a class="external mw-magiclink-rfc" rel="nofollow" href="//tools.ietf.org/html/rfc12354">RFC 12354</a></div></div></div> +<div class="thumb tright"><div class="thumbinner" style="width:182px;"><a href="/wiki/File:Foobar.jpg" class="image"><img alt="" src="http://example.com/images/thumb/3/3a/Foobar.jpg/180px-Foobar.jpg" width="180" height="20" class="thumbimage" srcset="http://example.com/images/thumb/3/3a/Foobar.jpg/270px-Foobar.jpg 1.5x, http://example.com/images/thumb/3/3a/Foobar.jpg/360px-Foobar.jpg 2x" /></a> <div class="thumbcaption"><div class="magnify"><a href="/wiki/File:Foobar.jpg" class="internal" title="Enlarge"></a></div>This is <a class="external mw-magiclink-rfc" rel="nofollow" href="https://tools.ietf.org/html/rfc12354">RFC 12354</a></div></div></div> !! html/parsoid -<figure class="mw-default-size" typeof="mw:Image/Thumb"><a href="./File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/thumb/3/3a/Foobar.jpg/220px-Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="25" width="220"/></a><figcaption>This is <a href="//tools.ietf.org/html/rfc12354" rel="mw:ExtLink">RFC 12354</a></figcaption></figure> +<figure class="mw-default-size" typeof="mw:Image/Thumb"><a href="./File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/thumb/3/3a/Foobar.jpg/220px-Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="25" width="220"/></a><figcaption>This is <a href="https://tools.ietf.org/html/rfc12354" rel="mw:ExtLink" class="external text">RFC 12354</a></figcaption></figure> !! end !! test @@ -15084,7 +15079,7 @@ T3887: A mailto link with a thumbnail <div class="thumb tright"><div class="thumbinner" style="width:182px;"><a href="/wiki/File:Foobar.jpg" class="image"><img alt="" src="http://example.com/images/thumb/3/3a/Foobar.jpg/180px-Foobar.jpg" width="180" height="20" class="thumbimage" srcset="http://example.com/images/thumb/3/3a/Foobar.jpg/270px-Foobar.jpg 1.5x, http://example.com/images/thumb/3/3a/Foobar.jpg/360px-Foobar.jpg 2x" /></a> <div class="thumbcaption"><div class="magnify"><a href="/wiki/File:Foobar.jpg" class="internal" title="Enlarge"></a></div>Please <a rel="nofollow" class="external free" href="mailto:nobody@example.com">mailto:nobody@example.com</a></div></div></div> !! html/parsoid -<figure class="mw-default-size" typeof="mw:Image/Thumb"><a href="./File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/thumb/3/3a/Foobar.jpg/220px-Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="25" width="220"/></a><figcaption>Please <a rel="mw:ExtLink" href="mailto:nobody@example.com">mailto:nobody@example.com</a></figcaption></figure> +<figure class="mw-default-size" typeof="mw:Image/Thumb"><a href="./File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/thumb/3/3a/Foobar.jpg/220px-Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="25" width="220"/></a><figcaption>Please <a rel="mw:ExtLink" class="external free" href="mailto:nobody@example.com">mailto:nobody@example.com</a></figcaption></figure> !! end # Pending resolution to T2368 @@ -15096,7 +15091,7 @@ T2648: Frameless image caption with a link <p><a href="/wiki/File:Foobar.jpg" class="image" title="text with a link in it"><img alt="text with a link in it" src="http://example.com/images/3/3a/Foobar.jpg" width="1941" height="220" /></a> </p> !! html/parsoid -<p><span class="mw-default-size" typeof="mw:Image" data-parsoid='{"optList":[{"ck":"caption","ak":"text with a [[link]] in it"}]}' data-mw='{"caption":"text with a <a rel=\"mw:WikiLink\" href=\"./Link\" title=\"Link\" data-parsoid='{\"stx\":\"simple\",\"a\":{\"href\":\"./Link\"},\"sa\":{\"href\":\"link\"},\"dsr\":[30,38,2,2]}'>link</a> in it"}'><a href="./File:Foobar.jpg" data-parsoid='{"a":{"href":"./File:Foobar.jpg"},"sa":{}}'><img resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="220" width="1941" data-parsoid='{"a":{"resource":"./File:Foobar.jpg","height":"220","width":"1941"},"sa":{"resource":"File:Foobar.jpg"}}'/></a></span></p> +<p><figure-inline class="mw-default-size" typeof="mw:Image" data-parsoid='{"optList":[{"ck":"caption","ak":"text with a [[link]] in it"}]}' data-mw='{"caption":"text with a <a rel=\"mw:WikiLink\" href=\"./Link\" title=\"Link\" data-parsoid='{\"stx\":\"simple\",\"a\":{\"href\":\"./Link\"},\"sa\":{\"href\":\"link\"},\"dsr\":[30,38,2,2]}'>link</a> in it"}'><a href="./File:Foobar.jpg" data-parsoid='{"a":{"href":"./File:Foobar.jpg"},"sa":{}}'><img resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="220" width="1941" data-parsoid='{"a":{"resource":"./File:Foobar.jpg","height":"220","width":"1941"},"sa":{"resource":"File:Foobar.jpg"}}'/></a></figure-inline></p> !! end !! test @@ -15107,7 +15102,7 @@ T2648: Frameless image caption with a link (suffix) <p><a href="/wiki/File:Foobar.jpg" class="image" title="text with a linkfoo in it"><img alt="text with a linkfoo in it" src="http://example.com/images/3/3a/Foobar.jpg" width="1941" height="220" /></a> </p> !! html/parsoid -<p><span class="mw-default-size" typeof="mw:Image" data-parsoid='{"optList":[{"ck":"caption","ak":"text with a [[link]]foo in it"}]}' data-mw='{"caption":"text with a <a rel=\"mw:WikiLink\" href=\"./Link\" title=\"Link\" data-parsoid='{\"stx\":\"simple\",\"a\":{\"href\":\"./Link\"},\"sa\":{\"href\":\"link\"},\"dsr\":[30,41,2,5],\"tail\":\"foo\"}'>linkfoo</a> in it"}'><a href="./File:Foobar.jpg" data-parsoid='{"a":{"href":"./File:Foobar.jpg"},"sa":{}}'><img resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="220" width="1941" data-parsoid='{"a":{"resource":"./File:Foobar.jpg","height":"220","width":"1941"},"sa":{"resource":"File:Foobar.jpg"}}'/></a></span></p> +<p><figure-inline class="mw-default-size" typeof="mw:Image" data-parsoid='{"optList":[{"ck":"caption","ak":"text with a [[link]]foo in it"}]}' data-mw='{"caption":"text with a <a rel=\"mw:WikiLink\" href=\"./Link\" title=\"Link\" data-parsoid='{\"stx\":\"simple\",\"a\":{\"href\":\"./Link\"},\"sa\":{\"href\":\"link\"},\"dsr\":[30,41,2,5],\"tail\":\"foo\"}'>linkfoo</a> in it"}'><a href="./File:Foobar.jpg" data-parsoid='{"a":{"href":"./File:Foobar.jpg"},"sa":{}}'><img resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="220" width="1941" data-parsoid='{"a":{"resource":"./File:Foobar.jpg","height":"220","width":"1941"},"sa":{"resource":"File:Foobar.jpg"}}'/></a></figure-inline></p> !! end !! test @@ -15118,7 +15113,7 @@ T2648: Frameless image caption with an interwiki link <p><a href="/wiki/File:Foobar.jpg" class="image" title="text with a MeatBall:Link in it"><img alt="text with a MeatBall:Link in it" src="http://example.com/images/3/3a/Foobar.jpg" width="1941" height="220" /></a> </p> !! html/parsoid -<p><span class="mw-default-size" typeof="mw:Image" data-parsoid='{"optList":[{"ck":"caption","ak":"text with a [[MeatBall:Link]] in it"}]}' data-mw='{"caption":"text with a <a rel=\"mw:ExtLink\" href=\"http://www.usemod.com/cgi-bin/mb.pl?Link\" title=\"meatball:Link\" data-parsoid='{\"stx\":\"simple\",\"a\":{\"href\":\"http://www.usemod.com/cgi-bin/mb.pl?Link\"},\"sa\":{\"href\":\"MeatBall:Link\"},\"isIW\":true,\"dsr\":[30,47,2,2]}'>MeatBall:Link</a> in it"}'><a href="./File:Foobar.jpg" data-parsoid='{"a":{"href":"./File:Foobar.jpg"},"sa":{}}'><img resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="220" width="1941" data-parsoid='{"a":{"resource":"./File:Foobar.jpg","height":"220","width":"1941"},"sa":{"resource":"File:Foobar.jpg"}}'/></a></span></p> +<p><figure-inline class="mw-default-size" typeof="mw:Image" data-parsoid='{"optList":[{"ck":"caption","ak":"text with a [[MeatBall:Link]] in it"}]}' data-mw='{"caption":"text with a <a rel=\"mw:WikiLink/Interwiki\" href=\"http://www.usemod.com/cgi-bin/mb.pl?Link\" title=\"meatball:Link\" data-parsoid='{\"stx\":\"simple\",\"a\":{\"href\":\"http://www.usemod.com/cgi-bin/mb.pl?Link\"},\"sa\":{\"href\":\"MeatBall:Link\"},\"isIW\":true,\"dsr\":[30,47,2,2]}'>MeatBall:Link</a> in it"}'><a href="./File:Foobar.jpg" data-parsoid='{"a":{"href":"./File:Foobar.jpg"},"sa":{}}'><img resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="220" width="1941" data-parsoid='{"a":{"resource":"./File:Foobar.jpg","height":"220","width":"1941"},"sa":{"resource":"File:Foobar.jpg"}}'/></a></figure-inline></p> !! end !! test @@ -15129,7 +15124,7 @@ T2648: Frameless image caption with a piped interwiki link <p><a href="/wiki/File:Foobar.jpg" class="image" title="text with a link in it"><img alt="text with a link in it" src="http://example.com/images/3/3a/Foobar.jpg" width="1941" height="220" /></a> </p> !! html/parsoid -<p><span class="mw-default-size" typeof="mw:Image" data-parsoid='{"optList":[{"ck":"caption","ak":"text with a [[MeatBall:Link|link]] in it"}]}' data-mw='{"caption":"text with a <a rel=\"mw:ExtLink\" href=\"http://www.usemod.com/cgi-bin/mb.pl?Link\" title=\"meatball:Link\" data-parsoid='{\"stx\":\"piped\",\"a\":{\"href\":\"http://www.usemod.com/cgi-bin/mb.pl?Link\"},\"sa\":{\"href\":\"MeatBall:Link\"},\"isIW\":true,\"dsr\":[30,52,16,2]}'>link</a> in it"}'><a href="./File:Foobar.jpg" data-parsoid='{"a":{"href":"./File:Foobar.jpg"},"sa":{}}'><img resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="220" width="1941" data-parsoid='{"a":{"resource":"./File:Foobar.jpg","height":"220","width":"1941"},"sa":{"resource":"File:Foobar.jpg"}}'/></a></span></p> +<p><figure-inline class="mw-default-size" typeof="mw:Image" data-parsoid='{"optList":[{"ck":"caption","ak":"text with a [[MeatBall:Link|link]] in it"}]}' data-mw='{"caption":"text with a <a rel=\"mw:WikiLink/Interwiki\" href=\"http://www.usemod.com/cgi-bin/mb.pl?Link\" title=\"meatball:Link\" data-parsoid='{\"stx\":\"piped\",\"a\":{\"href\":\"http://www.usemod.com/cgi-bin/mb.pl?Link\"},\"sa\":{\"href\":\"MeatBall:Link\"},\"isIW\":true,\"dsr\":[30,52,16,2]}'>link</a> in it"}'><a href="./File:Foobar.jpg" data-parsoid='{"a":{"href":"./File:Foobar.jpg"},"sa":{}}'><img resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="220" width="1941" data-parsoid='{"a":{"resource":"./File:Foobar.jpg","height":"220","width":"1941"},"sa":{"resource":"File:Foobar.jpg"}}'/></a></figure-inline></p> !! end !! test @@ -15137,7 +15132,7 @@ T107474: Frameless image caption with <nowiki> !! wikitext [[File:Foobar.jpg|<nowiki>text with a [[MeatBall:Link|link]] in it</nowiki>]] !! html/parsoid -<p><span class="mw-default-size" typeof="mw:Image" data-parsoid='{"optList":[{"ck":"caption","ak":"<nowiki>text with a [[MeatBall:Link|link]] in it</nowiki>"}]}' data-mw='{"caption":"<span typeof=\"mw:Nowiki\" data-parsoid='{\"dsr\":[18,75,8,9]}'>text with a [[MeatBall:Link|link]] in it</span>"}'><a href="./File:Foobar.jpg" data-parsoid='{"a":{"href":"./File:Foobar.jpg"},"sa":{}}'><img resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="220" width="1941" data-parsoid='{"a":{"resource":"./File:Foobar.jpg","height":"220","width":"1941"},"sa":{"resource":"File:Foobar.jpg"}}'/></a></span></p> +<p><figure-inline class="mw-default-size" typeof="mw:Image" data-parsoid='{"optList":[{"ck":"caption","ak":"<nowiki>text with a [[MeatBall:Link|link]] in it</nowiki>"}]}' data-mw='{"caption":"<span typeof=\"mw:Nowiki\" data-parsoid='{\"dsr\":[18,75,8,9]}'>text with a [[MeatBall:Link|link]] in it</span>"}'><a href="./File:Foobar.jpg" data-parsoid='{"a":{"href":"./File:Foobar.jpg"},"sa":{}}'><img resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="220" width="1941" data-parsoid='{"a":{"resource":"./File:Foobar.jpg","height":"220","width":"1941"},"sa":{"resource":"File:Foobar.jpg"}}'/></a></figure-inline></p> !! end !! test @@ -15148,7 +15143,7 @@ Escape HTML special chars in image alt text <p><a href="/wiki/File:Foobar.jpg" class="image" title="& < > ""><img alt="& < > "" src="http://example.com/images/3/3a/Foobar.jpg" width="1941" height="220" /></a> </p> !! html/parsoid -<p><span class="mw-default-size" typeof="mw:Image" data-parsoid='{"optList":[{"ck":"caption","ak":"& < > \""}]}' data-mw='{"caption":"&amp; &lt; > \""}'><a href="./File:Foobar.jpg" data-parsoid='{"a":{"href":"./File:Foobar.jpg"},"sa":{}}'><img resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="220" width="1941" data-parsoid='{"a":{"resource":"./File:Foobar.jpg","height":"220","width":"1941"},"sa":{"resource":"File:Foobar.jpg"}}'/></a></span></p> +<p><figure-inline class="mw-default-size" typeof="mw:Image" data-parsoid='{"optList":[{"ck":"caption","ak":"& < > \""}]}' data-mw='{"caption":"&amp; &lt; > \""}'><a href="./File:Foobar.jpg" data-parsoid='{"a":{"href":"./File:Foobar.jpg"},"sa":{}}'><img resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="220" width="1941" data-parsoid='{"a":{"resource":"./File:Foobar.jpg","height":"220","width":"1941"},"sa":{"resource":"File:Foobar.jpg"}}'/></a></figure-inline></p> !! end !! test @@ -15161,7 +15156,7 @@ language=zh <p><a href="/wiki/File:Foobar.jpg" class="image" title="& < > ""><img alt="& < > "" src="http://example.com/images/3/3a/Foobar.jpg" width="1941" height="220" /></a> </p> !! html/parsoid -<p><span class="mw-default-size" typeof="mw:Image" data-parsoid='{"optList":[{"ck":"caption","ak":"& < > \""}]}' data-mw='{"caption":"&amp; &lt; > \""}'><a href="./File:Foobar.jpg" data-parsoid='{"a":{"href":"./File:Foobar.jpg"},"sa":{}}'><img resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="220" width="1941" data-parsoid='{"a":{"resource":"./File:Foobar.jpg","height":"220","width":"1941"},"sa":{"resource":"File:Foobar.jpg"}}'/></a></span></p> +<p><figure-inline class="mw-default-size" typeof="mw:Image" data-parsoid='{"optList":[{"ck":"caption","ak":"& < > \""}]}' data-mw='{"caption":"&amp; &lt; > \""}'><a href="./File:Foobar.jpg" data-parsoid='{"a":{"href":"./File:Foobar.jpg"},"sa":{}}'><img resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="220" width="1941" data-parsoid='{"a":{"resource":"./File:Foobar.jpg","height":"220","width":"1941"},"sa":{"resource":"File:Foobar.jpg"}}'/></a></figure-inline></p> !! end !! test @@ -15172,7 +15167,7 @@ Entities in file name and attributes <p><a href="/index.php?title=Special:Upload&wpDestFile=7%25_solution.gif" class="new" title="File:7% solution.gif">7% solution</a> </p> !! html/parsoid -<p><span class="mw-default-size" typeof="mw:Error mw:Image" data-parsoid='{"optList":[{"ck":"bogus","ak":"manualthumb=7%25 solution.gif"},{"ck":"link","ak":"link=7%25 solution"},{"ck":"caption","ak":"[[7%25 solution]]"}]}' data-mw='{"errors":[{"key":"apierror-filedoesnotexist","message":"This image does not exist."}],"caption":"<a rel=\"mw:WikiLink\" href=\"./7%25_solution\" title=\"7% solution\" data-parsoid='{\"stx\":\"simple\",\"a\":{\"href\":\"./7%25_solution\"},\"sa\":{\"href\":\"7%25 solution\"},\"dsr\":[74,91,2,2]}'>7% solution</a>"}'><a href="./7%25_solution" data-parsoid='{"a":{"href":"./7%25_solution"},"sa":{"href":"link=7%25 solution"}}'><img resource="./File:7%25_solution.gif" src="./Special:FilePath/7%25_solution.gif" height="220" width="220" data-parsoid='{"a":{"resource":"./File:7%25_solution.gif","height":"220","width":"220"},"sa":{"resource":"File:7%25 solution.gif"}}'/></a></span></p> +<p><figure-inline class="mw-default-size" typeof="mw:Error mw:Image" data-parsoid='{"optList":[{"ck":"bogus","ak":"manualthumb=7%25 solution.gif"},{"ck":"link","ak":"link=7%25 solution"},{"ck":"caption","ak":"[[7%25 solution]]"}]}' data-mw='{"errors":[{"key":"apierror-filedoesnotexist","message":"This image does not exist."}],"caption":"<a rel=\"mw:WikiLink\" href=\"./7%25_solution\" title=\"7% solution\" data-parsoid='{\"stx\":\"simple\",\"a\":{\"href\":\"./7%25_solution\"},\"sa\":{\"href\":\"7%25 solution\"},\"dsr\":[74,91,2,2]}'>7% solution</a>"}'><a href="./7%25_solution" data-parsoid='{"a":{"href":"./7%25_solution"},"sa":{"href":"link=7%25 solution"}}'><img resource="./File:7%25_solution.gif" src="./Special:FilePath/7%25_solution.gif" height="220" width="220" data-parsoid='{"a":{"resource":"./File:7%25_solution.gif","height":"220","width":"220"},"sa":{"resource":"File:7%25 solution.gif"}}'/></a></figure-inline></p> !! end !! test @@ -15183,7 +15178,7 @@ T2499: Alt text should have Ӓ, not &1234; <p><a href="/wiki/File:Foobar.jpg" class="image" title="♀"><img alt="♀" src="http://example.com/images/3/3a/Foobar.jpg" width="1941" height="220" /></a> </p> !! html/parsoid -<p><span class="mw-default-size" typeof="mw:Image" data-parsoid='{"optList":[{"ck":"caption","ak":"&#9792;"}]}' data-mw='{"caption":"<span typeof=\"mw:Entity\" data-parsoid='{\"src\":\"&amp;#9792;\",\"srcContent\":\"♀\",\"dsr\":[18,25,null,null]}'>♀</span>"}'><a href="./File:Foobar.jpg" data-parsoid='{"a":{"href":"./File:Foobar.jpg"},"sa":{}}'><img resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="220" width="1941" data-parsoid='{"a":{"resource":"./File:Foobar.jpg","height":"220","width":"1941"},"sa":{"resource":"File:Foobar.jpg"}}'/></a></span></p> +<p><figure-inline class="mw-default-size" typeof="mw:Image" data-parsoid='{"optList":[{"ck":"caption","ak":"&#9792;"}]}' data-mw='{"caption":"<span typeof=\"mw:Entity\" data-parsoid='{\"src\":\"&amp;#9792;\",\"srcContent\":\"♀\",\"dsr\":[18,25,null,null]}'>♀</span>"}'><a href="./File:Foobar.jpg" data-parsoid='{"a":{"href":"./File:Foobar.jpg"},"sa":{}}'><img resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="220" width="1941" data-parsoid='{"a":{"resource":"./File:Foobar.jpg","height":"220","width":"1941"},"sa":{"resource":"File:Foobar.jpg"}}'/></a></figure-inline></p> !! end !! test @@ -15207,7 +15202,7 @@ Image caption containing another image <div class="thumb tright"><div class="thumbinner" style="width:182px;"><a href="/wiki/File:Foobar.jpg" class="image"><img alt="" src="http://example.com/images/thumb/3/3a/Foobar.jpg/180px-Foobar.jpg" width="180" height="20" class="thumbimage" srcset="http://example.com/images/thumb/3/3a/Foobar.jpg/270px-Foobar.jpg 1.5x, http://example.com/images/thumb/3/3a/Foobar.jpg/360px-Foobar.jpg 2x" /></a> <div class="thumbcaption"><div class="magnify"><a href="/wiki/File:Foobar.jpg" class="internal" title="Enlarge"></a></div>This is a caption with another <a href="/wiki/File:Thumb.png" class="image" title="image"><img alt="image" src="http://example.com/images/e/ea/Thumb.png" width="135" height="135" /></a> inside it!</div></div></div> !! html/parsoid -<figure class="mw-default-size" typeof="mw:Image/Thumb"><a href="./File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/thumb/3/3a/Foobar.jpg/220px-Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="25" width="220"/></a><figcaption>This is a caption with another <span class="mw-default-size" typeof="mw:Image" data-mw='{"caption":"image"}'><a href="./File:Thumb.png"><img resource="./File:Thumb.png" src="//example.com/images/e/ea/Thumb.png" data-file-width="135" data-file-height="135" data-file-type="bitmap" height="135" width="135"/></a></span> inside it!</figcaption></figure> +<figure class="mw-default-size" typeof="mw:Image/Thumb"><a href="./File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/thumb/3/3a/Foobar.jpg/220px-Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="25" width="220"/></a><figcaption>This is a caption with another <figure-inline class="mw-default-size" typeof="mw:Image" data-mw='{"caption":"image"}'><a href="./File:Thumb.png"><img resource="./File:Thumb.png" src="//example.com/images/e/ea/Thumb.png" data-file-width="135" data-file-height="135" data-file-type="bitmap" height="135" width="135"/></a></figure-inline> inside it!</figcaption></figure> !! end !! test @@ -15219,7 +15214,7 @@ Image: caption containing a newline <p><a href="/wiki/File:Foobar.jpg" class="image" title="This *is some text"><img alt="This *is some text" src="http://example.com/images/3/3a/Foobar.jpg" width="1941" height="220" /></a> </p> !! html/parsoid -<p><span class="mw-default-size" typeof="mw:Image" data-mw='{"caption":"This\n*is some text"}'><a href="./File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="220" width="1941"/></a></span></p> +<p><figure-inline class="mw-default-size" typeof="mw:Image" data-mw='{"caption":"This\n*is some text"}'><a href="./File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="220" width="1941"/></a></figure-inline></p> !!end !!test @@ -15234,6 +15229,10 @@ Image: caption containing leading space <figure class="mw-default-size" typeof="mw:Image/Thumb"><a href="./File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/thumb/3/3a/Foobar.jpg/220px-Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="25" width="220"/></a><figcaption> bar</figcaption></figure> !!end +# html/php output not have newlines after table, td, th, etc. because +# Linker::makeThumbLink2() replaces the newlines with spaces since +# the table is inside a caption. +# FIXME: Verify if that circa 2004 fix is still required. !! test Image: caption containing a table !! options @@ -15241,21 +15240,21 @@ parsoid=wt2html,wt2wt,html2html !! wikitext [[Image:Foobar.jpg|thumb|200px|This is an example image thumbnail caption with a table {| -! Foo !! Bar +!Foo!!Bar |- -| Foo1 || Bar1 +|Foo1||Bar1 |} and some more text.]] !! html/php -<div class="thumb tright"><div class="thumbinner" style="width:202px;"><a href="/wiki/File:Foobar.jpg" class="image"><img alt="" src="http://example.com/images/thumb/3/3a/Foobar.jpg/200px-Foobar.jpg" width="200" height="23" class="thumbimage" srcset="http://example.com/images/thumb/3/3a/Foobar.jpg/300px-Foobar.jpg 1.5x, http://example.com/images/thumb/3/3a/Foobar.jpg/400px-Foobar.jpg 2x" /></a> <div class="thumbcaption"><div class="magnify"><a href="/wiki/File:Foobar.jpg" class="internal" title="Enlarge"></a></div>This is an example image thumbnail caption with a table <table> <tr> <th> Foo </th> <th> Bar </th></tr> <tr> <td> Foo1 </td> <td> Bar1 </td></tr></table> and some more text.</div></div></div> +<div class="thumb tright"><div class="thumbinner" style="width:202px;"><a href="/wiki/File:Foobar.jpg" class="image"><img alt="" src="http://example.com/images/thumb/3/3a/Foobar.jpg/200px-Foobar.jpg" width="200" height="23" class="thumbimage" srcset="http://example.com/images/thumb/3/3a/Foobar.jpg/300px-Foobar.jpg 1.5x, http://example.com/images/thumb/3/3a/Foobar.jpg/400px-Foobar.jpg 2x" /></a> <div class="thumbcaption"><div class="magnify"><a href="/wiki/File:Foobar.jpg" class="internal" title="Enlarge"></a></div>This is an example image thumbnail caption with a table <table> <tr> <th>Foo</th> <th>Bar </th></tr> <tr> <td>Foo1</td> <td>Bar1 </td></tr></table> and some more text.</div></div></div> !! html/parsoid <figure typeof="mw:Image/Thumb"><a href="./File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/thumb/3/3a/Foobar.jpg/200px-Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="23" width="200"/></a><figcaption>This is an example image thumbnail caption with a table <table> <tbody> -<tr><th>Foo </th><th>Bar</th></tr> +<tr><th>Foo</th><th>Bar</th></tr> <tr> -<td>Foo1 </td> +<td>Foo1</td> <td>Bar1</td></tr></tbody></table>and some more text.</figcaption></figure> !! end @@ -15267,7 +15266,7 @@ T5090: External links other than http: in image captions <div class="thumb tright"><div class="thumbinner" style="width:202px;"><a href="/wiki/File:Foobar.jpg" class="image"><img alt="" src="http://example.com/images/thumb/3/3a/Foobar.jpg/200px-Foobar.jpg" width="200" height="23" class="thumbimage" srcset="http://example.com/images/thumb/3/3a/Foobar.jpg/300px-Foobar.jpg 1.5x, http://example.com/images/thumb/3/3a/Foobar.jpg/400px-Foobar.jpg 2x" /></a> <div class="thumbcaption"><div class="magnify"><a href="/wiki/File:Foobar.jpg" class="internal" title="Enlarge"></a></div>This caption has <a rel="nofollow" class="external text" href="irc://example.net">irc</a> and <a rel="nofollow" class="external text" href="https://example.com">Secure</a> ext links in it.</div></div></div> !! html/parsoid -<figure typeof="mw:Image/Thumb"><a href="./File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/thumb/3/3a/Foobar.jpg/200px-Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="23" width="200"/></a><figcaption>This caption has <a rel="mw:ExtLink" href="irc://example.net">irc</a> and <a rel="mw:ExtLink" href="https://example.com">Secure</a> ext links in it.</figcaption></figure> +<figure typeof="mw:Image/Thumb"><a href="./File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/thumb/3/3a/Foobar.jpg/200px-Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="23" width="200"/></a><figcaption>This caption has <a rel="mw:ExtLink" class="external text" href="irc://example.net">irc</a> and <a rel="mw:ExtLink" class="external text" href="https://example.com">Secure</a> ext links in it.</figcaption></figure> !! end !! test @@ -15280,7 +15279,7 @@ parsoid=wt2html,wt2wt,html2html <p><a href="/wiki/File:Foobar.jpg" class="image" title="a"><img alt="a" src="http://example.com/images/3/3a/Foobar.jpg" width="1941" height="220" class="b" /></a> </p> !! html/parsoid -<p><span class="mw-default-size b" typeof="mw:Image" data-mw='{"caption":"a"}'><a href="./File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="220" width="1941"/></a></span></p> +<p><figure-inline class="mw-default-size b" typeof="mw:Image" data-mw='{"caption":"a"}'><a href="./File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="220" width="1941"/></a></figure-inline></p> !! end !! test @@ -15334,7 +15333,7 @@ parsoid=wt2html,wt2wt,html2html <p><a href="/wiki/File:Foobar.jpg" class="image" title="caption"><img alt="caption" src="http://example.com/images/thumb/3/3a/Foobar.jpg/220px-Foobar.jpg" width="220" height="25" class="extra thumbborder" srcset="http://example.com/images/thumb/3/3a/Foobar.jpg/330px-Foobar.jpg 1.5x, http://example.com/images/thumb/3/3a/Foobar.jpg/440px-Foobar.jpg 2x" /></a> </p> !! html/parsoid -<p><span class="mw-default-size mw-image-border extra" typeof="mw:Image/Frameless" data-mw='{"caption":"caption"}'><a href="./File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/thumb/3/3a/Foobar.jpg/220px-Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="25" width="220"/></a></span></p> +<p><figure-inline class="mw-default-size mw-image-border extra" typeof="mw:Image/Frameless" data-mw='{"caption":"caption"}'><a href="./File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/thumb/3/3a/Foobar.jpg/220px-Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="25" width="220"/></a></figure-inline></p> !! end # Note that 'right' is the default alignment, despite the misspelled 'righ' below @@ -15387,7 +15386,7 @@ wgEnableUploads=0 <p><a href="/wiki/File:Foobaz.jpg" title="File:Foobaz.jpg">File:Foobaz.jpg</a> </p> !! html/parsoid -<p><span class="mw-default-size" typeof="mw:Error mw:Image" data-mw='{"errors":[{"key":"apierror-filedoesnotexist","message":"This image does not exist."}]}'><a href="./File:Foobaz.jpg"><img resource="./File:Foobaz.jpg" src="./Special:FilePath/Foobaz.jpg" height="220" width="220"/></a></span></p> +<p><figure-inline class="mw-default-size" typeof="mw:Error mw:Image" data-mw='{"errors":[{"key":"apierror-filedoesnotexist","message":"This image does not exist."}]}'><a href="./File:Foobaz.jpg"><img resource="./File:Foobaz.jpg" src="./Special:FilePath/Foobaz.jpg" height="220" width="220"/></a></figure-inline></p> !! end # Parsoid-specific testing for images @@ -15402,7 +15401,7 @@ Parsoid-specific image handling - simple image with size and middle alignment !! wikitext [[File:Foobar.jpg|middle|50px]] !! html/parsoid -<p><span class="mw-valign-middle" typeof="mw:Image"><a href="./File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/thumb/3/3a/Foobar.jpg/50px-Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="6" width="50"/></a></span></p> +<p><figure-inline class="mw-valign-middle" typeof="mw:Image"><a href="./File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/thumb/3/3a/Foobar.jpg/50px-Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="6" width="50"/></a></figure-inline></p> !! end !! test @@ -15413,7 +15412,7 @@ parsoid=wt2wt,wt2html,html2html !! wikitext [[Image:Foobar.jpg|middle|50px]] !! html/parsoid -<p><span class="mw-valign-middle" typeof="mw:Image"><a href="./File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/thumb/3/3a/Foobar.jpg/50px-Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="6" width="50"/></a></span></p> +<p><figure-inline class="mw-valign-middle" typeof="mw:Image"><a href="./File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/thumb/3/3a/Foobar.jpg/50px-Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="6" width="50"/></a></figure-inline></p> !! end !! test @@ -15422,7 +15421,7 @@ Parsoid-specific image handling - simple image with size and middle alignment !! wikitext [[File:Foobar.jpg|50px|middle]] !! html/parsoid -<p><span class="mw-valign-middle" typeof="mw:Image" data-parsoid='{"optList":[{"ck":"width","ak":"50px"},{"ck":"middle","ak":"middle"}]}'><a href="./File:Foobar.jpg" data-parsoid='{"a":{"href":"./File:Foobar.jpg"}}'><img resource="./File:Foobar.jpg" src="//example.com/images/thumb/3/3a/Foobar.jpg/50px-Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="6" width="50" data-parsoid='{"a":{"resource":"./File:Foobar.jpg","height":"6","width":"50"},"sa":{"resource":"File:Foobar.jpg"}}'/></a></span></p> +<p><figure-inline class="mw-valign-middle" typeof="mw:Image" data-parsoid='{"optList":[{"ck":"width","ak":"50px"},{"ck":"middle","ak":"middle"}]}'><a href="./File:Foobar.jpg" data-parsoid='{"a":{"href":"./File:Foobar.jpg"}}'><img resource="./File:Foobar.jpg" src="//example.com/images/thumb/3/3a/Foobar.jpg/50px-Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="6" width="50" data-parsoid='{"a":{"resource":"./File:Foobar.jpg","height":"6","width":"50"},"sa":{"resource":"File:Foobar.jpg"}}'/></a></figure-inline></p> !! end !! test @@ -15433,7 +15432,7 @@ parsoid=wt2html,wt2wt,html2html !! wikitext [[Image:Foobar.jpg|50px|middle]] !! html/parsoid -<p><span class="mw-valign-middle" typeof="mw:Image"><a href="./File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/thumb/3/3a/Foobar.jpg/50px-Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="6" width="50"/></a></span></p> +<p><figure-inline class="mw-valign-middle" typeof="mw:Image"><a href="./File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/thumb/3/3a/Foobar.jpg/50px-Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="6" width="50"/></a></figure-inline></p> !! end !! test @@ -15441,7 +15440,7 @@ Parsoid-specific image handling - simple image with both sizes, a baseline align !! wikitext [[File:Foobar.jpg|500x10px|baseline|caption]] !! html/parsoid -<p><span class="mw-valign-baseline" typeof="mw:Image" data-mw='{"caption":"caption"}' data-parsoid='{"optList":[{"ck":"width","ak":"500x10px"},{"ck":"baseline","ak":"baseline"},{"ck":"caption","ak":"caption"}],"size":"500x10"}'><a href="./File:Foobar.jpg" data-parsoid='{"a":{"href":"./File:Foobar.jpg"}}'><img resource="./File:Foobar.jpg" src="//example.com/images/thumb/3/3a/Foobar.jpg/89px-Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="10" width="89" data-parsoid='{"a":{"resource":"./File:Foobar.jpg","height":"10","width":"89"},"sa":{"resource":"File:Foobar.jpg"}}'/></a></span></p> +<p><figure-inline class="mw-valign-baseline" typeof="mw:Image" data-mw='{"caption":"caption"}' data-parsoid='{"optList":[{"ck":"width","ak":"500x10px"},{"ck":"baseline","ak":"baseline"},{"ck":"caption","ak":"caption"}],"size":"500x10"}'><a href="./File:Foobar.jpg" data-parsoid='{"a":{"href":"./File:Foobar.jpg"}}'><img resource="./File:Foobar.jpg" src="//example.com/images/thumb/3/3a/Foobar.jpg/89px-Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="10" width="89" data-parsoid='{"a":{"resource":"./File:Foobar.jpg","height":"10","width":"89"},"sa":{"resource":"File:Foobar.jpg"}}'/></a></figure-inline></p> !! end !! test @@ -15449,7 +15448,7 @@ Parsoid-specific image handling - simple image with border and size spec !! wikitext [[File:Foobar.jpg|50px|border|caption]] !! html/parsoid -<p><span class="mw-image-border" typeof="mw:Image" data-mw='{"caption":"caption"}' data-parsoid='{"optList":[{"ck":"width","ak":"50px"},{"ck":"border","ak":"border"},{"ck":"caption","ak":"caption"}]}'><a href="./File:Foobar.jpg" data-parsoid='{"a":{"href":"./File:Foobar.jpg"}}'><img resource="./File:Foobar.jpg" src="//example.com/images/thumb/3/3a/Foobar.jpg/50px-Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="6" width="50" data-parsoid='{"a":{"resource":"./File:Foobar.jpg","height":"6","width":"50"},"sa":{"resource":"File:Foobar.jpg"}}'/></a></span></p> +<p><figure-inline class="mw-image-border" typeof="mw:Image" data-mw='{"caption":"caption"}' data-parsoid='{"optList":[{"ck":"width","ak":"50px"},{"ck":"border","ak":"border"},{"ck":"caption","ak":"caption"}]}'><a href="./File:Foobar.jpg" data-parsoid='{"a":{"href":"./File:Foobar.jpg"}}'><img resource="./File:Foobar.jpg" src="//example.com/images/thumb/3/3a/Foobar.jpg/50px-Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="6" width="50" data-parsoid='{"a":{"resource":"./File:Foobar.jpg","height":"6","width":"50"},"sa":{"resource":"File:Foobar.jpg"}}'/></a></figure-inline></p> !! end !! test @@ -15513,7 +15512,7 @@ Parsoid-specific image handling - frameless image with specific size, border, an !! wikitext [[File:Foobar.jpg|frameless|442x50px|border|caption]] !! html/parsoid -<p><span class="mw-image-border" typeof="mw:Image/Frameless" data-mw='{"caption":"caption"}' data-parsoid='{"optList":[{"ck":"frameless","ak":"frameless"},{"ck":"width","ak":"442x50px"},{"ck":"border","ak":"border"},{"ck":"caption","ak":"caption"}],"size":"442x50"}'><a href="./File:Foobar.jpg" data-parsoid='{"a":{"href":"./File:Foobar.jpg"}}'><img resource="./File:Foobar.jpg" src="//example.com/images/thumb/3/3a/Foobar.jpg/442px-Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="50" width="442" data-parsoid='{"a":{"resource":"./File:Foobar.jpg","height":"50","width":"442"},"sa":{"resource":"File:Foobar.jpg"}}'/></a></span></p> +<p><figure-inline class="mw-image-border" typeof="mw:Image/Frameless" data-mw='{"caption":"caption"}' data-parsoid='{"optList":[{"ck":"frameless","ak":"frameless"},{"ck":"width","ak":"442x50px"},{"ck":"border","ak":"border"},{"ck":"caption","ak":"caption"}],"size":"442x50"}'><a href="./File:Foobar.jpg" data-parsoid='{"a":{"href":"./File:Foobar.jpg"}}'><img resource="./File:Foobar.jpg" src="//example.com/images/thumb/3/3a/Foobar.jpg/442px-Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="50" width="442" data-parsoid='{"a":{"resource":"./File:Foobar.jpg","height":"50","width":"442"},"sa":{"resource":"File:Foobar.jpg"}}'/></a></figure-inline></p> !! end !! test @@ -15521,7 +15520,7 @@ Parsoid-specific image handling - simple image with a formatted caption !! wikitext [[File:Foobar.jpg|<table><tr><td>a</td><td>b</td></tr><tr><td>c</td></tr></table>]] !! html/parsoid -<p><span class="mw-default-size" typeof="mw:Image" data-parsoid='{"optList":[{"ck":"caption","ak":"<table><tr><td>a</td><td>b</td></tr><tr><td>c</td></tr></table>"}]}' data-mw='{"caption":"<table data-parsoid='{\"stx\":\"html\",\"dsr\":[18,81,7,8]}'><tbody data-parsoid='{\"dsr\":[25,73,0,0]}'><tr data-parsoid='{\"stx\":\"html\",\"dsr\":[25,54,4,5]}'><td data-parsoid='{\"stx\":\"html\",\"dsr\":[29,39,4,5]}'>a</td><td data-parsoid='{\"stx\":\"html\",\"dsr\":[39,49,4,5]}'>b</td></tr><tr data-parsoid='{\"stx\":\"html\",\"dsr\":[54,73,4,5]}'><td data-parsoid='{\"stx\":\"html\",\"dsr\":[58,68,4,5]}'>c</td></tr></tbody></table>"}'><a href="./File:Foobar.jpg" data-parsoid='{"a":{"href":"./File:Foobar.jpg"},"sa":{}}'><img resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="220" width="1941" data-parsoid='{"a":{"resource":"./File:Foobar.jpg","height":"220","width":"1941"},"sa":{"resource":"File:Foobar.jpg"}}'/></a></span></p> +<p><figure-inline class="mw-default-size" typeof="mw:Image" data-parsoid='{"optList":[{"ck":"caption","ak":"<table><tr><td>a</td><td>b</td></tr><tr><td>c</td></tr></table>"}]}' data-mw='{"caption":"<table data-parsoid='{\"stx\":\"html\",\"dsr\":[18,81,7,8]}'><tbody data-parsoid='{\"dsr\":[25,73,0,0]}'><tr data-parsoid='{\"stx\":\"html\",\"dsr\":[25,54,4,5]}'><td data-parsoid='{\"stx\":\"html\",\"dsr\":[29,39,4,5]}'>a</td><td data-parsoid='{\"stx\":\"html\",\"dsr\":[39,49,4,5]}'>b</td></tr><tr data-parsoid='{\"stx\":\"html\",\"dsr\":[54,73,4,5]}'><td data-parsoid='{\"stx\":\"html\",\"dsr\":[58,68,4,5]}'>c</td></tr></tbody></table>"}'><a href="./File:Foobar.jpg" data-parsoid='{"a":{"href":"./File:Foobar.jpg"},"sa":{}}'><img resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="220" width="1941" data-parsoid='{"a":{"resource":"./File:Foobar.jpg","height":"220","width":"1941"},"sa":{"resource":"File:Foobar.jpg"}}'/></a></figure-inline></p> !! end !! test @@ -15591,7 +15590,7 @@ foo bar !! html/parsoid <p>foo -<span class="mw-default-size" typeof="mw:Image" data-mw='{"caption":"caption"}'><a href="./File:Foobar.svg"><img resource="./File:Foobar.svg" src="//example.com/images/f/ff/Foobar.svg" lang="de" data-file-width="240" data-file-height="180" data-file-type="drawing" height="180" width="240"/></a></span> +<figure-inline class="mw-default-size" typeof="mw:Image" data-mw='{"caption":"caption"}'><a href="./File:Foobar.svg"><img resource="./File:Foobar.svg" src="//example.com/images/f/ff/Foobar.svg" lang="de" data-file-width="240" data-file-height="180" data-file-type="drawing" height="180" width="240"/></a></figure-inline> bar</p> !! end @@ -15603,7 +15602,7 @@ T93580: 1. Templated <ref> inside block images <references /> !! html/parsoid -<figure class="mw-default-size" typeof="mw:Image/Thumb" data-parsoid='{"optList":[{"ck":"thumbnail","ak":"thumb"},{"ck":"caption","ak":"Caption with templated ref: {{echo|<ref>foo</ref>}}"}]}'><a href="./File:Foobar.jpg" data-parsoid='{"a":{"href":"./File:Foobar.jpg"},"sa":{}}'><img resource="./File:Foobar.jpg" src="//example.com/images/thumb/3/3a/Foobar.jpg/220px-Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="25" width="220" data-parsoid='{"a":{"resource":"./File:Foobar.jpg","height":"25","width":"220"},"sa":{"resource":"File:Foobar.jpg"}}'/></a><figcaption>Caption with templated ref: <span about="#mwt5" class="mw-ref" id="cite_ref-1" rel="dc:references" typeof="mw:Transclusion mw:Extension/ref" data-mw='{"parts":[{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"<ref>foo</ref>"}},"i":0}}]}'><a href="./Main_Page#cite_note-1" style="counter-reset: mw-Ref 1;"><span class="mw-reflink-text">[1]</span></a></span></figcaption></figure> +<figure class="mw-default-size" typeof="mw:Image/Thumb" data-parsoid='{"optList":[{"ck":"thumbnail","ak":"thumb"},{"ck":"caption","ak":"Caption with templated ref: {{echo|<ref>foo</ref>}}"}]}'><a href="./File:Foobar.jpg" data-parsoid='{"a":{"href":"./File:Foobar.jpg"},"sa":{}}'><img resource="./File:Foobar.jpg" src="//example.com/images/thumb/3/3a/Foobar.jpg/220px-Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="25" width="220" data-parsoid='{"a":{"resource":"./File:Foobar.jpg","height":"25","width":"220"},"sa":{"resource":"File:Foobar.jpg"}}'/></a><figcaption>Caption with templated ref: <sup about="#mwt5" class="mw-ref" id="cite_ref-1" rel="dc:references" typeof="mw:Transclusion mw:Extension/ref" data-mw='{"parts":[{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"<ref>foo</ref>"}},"i":0}}]}'><a href="./Main_Page#cite_note-1" style="counter-reset: mw-Ref 1;"><span class="mw-reflink-text">[1]</span></a></sup></figcaption></figure> <ol class="mw-references references" typeof="mw:Extension/references" about="#mwt6" data-mw='{"name":"references","attrs":{}}'><li about="#cite_note-1" id="cite_note-1"><a href="./Main_Page#cite_ref-1" rel="mw:referencedBy"><span class="mw-linkback-text">↑ </span></a> <span id="mw-reference-text-cite_note-1" class="mw-reference-text" data-parsoid="{}">foo</span></li></ol> !! end @@ -15615,9 +15614,9 @@ T93580: 2. <ref> inside inline images <references /> !! html/parsoid -<p><span class="mw-default-size" typeof="mw:Image" data-parsoid='{"optList":[{"ck":"caption","ak":"Undisplayed caption in inline image with ref: <ref>foo</ref>"}]}' data-mw='{"caption":"Undisplayed caption in inline image with ref: <span about=\"#mwt2\" class=\"mw-ref\" id=\"cite_ref-1\" rel=\"dc:references\" typeof=\"mw:Extension/ref\" data-parsoid='{\"dsr\":[64,78,5,6]}' data-mw='{\"name\":\"ref\",\"body\":{\"id\":\"mw-reference-text-cite_note-1\"},\"attrs\":{}}'><a href=\"./Main_Page#cite_note-1\" style=\"counter-reset: mw-Ref 1;\" data-parsoid=\"{}\"><span class=\"mw-reflink-text\" data-parsoid=\"{}\">[1]</span></a></span><meta typeof=\"mw:Extension/ref/Marker\" about=\"#mwt2\" data-parsoid='{\"group\":\"\",\"name\":\"\",\"content\":\"foo\",\"hasRefInRef\":false,\"dsr\":[64,78,5,6]}'/>"}'><a href="./File:Foobar.jpg" data-parsoid='{"a":{"href":"./File:Foobar.jpg"},"sa":{}}'><img resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="220" width="1941" data-parsoid='{"a":{"resource":"./File:Foobar.jpg","height":"220","width":"1941"},"sa":{"resource":"File:Foobar.jpg"}}'/></a></span></p> +<p><figure-inline class="mw-default-size" typeof="mw:Image" data-parsoid='{"optList":[{"ck":"caption","ak":"Undisplayed caption in inline image with ref: <ref>foo</ref>"}]}' data-mw='{"caption":"Undisplayed caption in inline image with ref: <sup about=\"#mwt2\" class=\"mw-ref\" id=\"cite_ref-1\" rel=\"dc:references\" typeof=\"mw:Extension/ref\" data-parsoid='{\"dsr\":[64,78,5,6]}' data-mw='{\"name\":\"ref\",\"body\":{\"id\":\"mw-reference-text-cite_note-1\"},\"attrs\":{}}'><a href=\"./Main_Page#cite_note-1\" style=\"counter-reset: mw-Ref 1;\" data-parsoid=\"{}\"><span class=\"mw-reflink-text\" data-parsoid=\"{}\">[1]</span></a></sup>"}'><a href="./File:Foobar.jpg" data-parsoid='{"a":{"href":"./File:Foobar.jpg"},"sa":{"href":"File:Foobar.jpg"}}'><img resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="220" width="1941" data-parsoid='{"a":{"resource":"./File:Foobar.jpg","height":"220","width":"1941"},"sa":{"resource":"File:Foobar.jpg"}}'/></a></figure-inline></p> -<ol class="mw-references references" typeof="mw:Extension/references" about="#mwt4" data-mw='{"name":"references","attrs":{}}'><li about="#cite_note-1" id="cite_note-1"><a href="./Main_Page#cite_ref-1" rel="mw:referencedBy"><span class="mw-linkback-text">↑ </span></a> <span id="mw-reference-text-cite_note-1" class="mw-reference-text" data-parsoid="{}">foo</span></li></ol> +<ol class="mw-references references" typeof="mw:Extension/references" about="#mwt4" data-mw='{"name":"references","attrs":{}}'><li about="#cite_note-1" id="cite_note-1"><a href="./Main_Page#cite_ref-1" rel="mw:referencedBy"><span class="mw-linkback-text">↑ </span></a> <span id="mw-reference-text-cite_note-1" class="mw-reference-text">foo</span></li></ol> !! end !! test @@ -15627,9 +15626,9 @@ T93580: 3. Templated <ref> inside inline images <references /> !! html/parsoid -<p><span class="mw-default-size" typeof="mw:Image" data-parsoid='{"optList":[{"ck":"caption","ak":"Undisplayed caption in inline image with ref: {{echo|<ref>{{echo|foo}}</ref>}}"}]}' data-mw='{"caption":"Undisplayed caption in inline image with ref: <span about=\"#mwt2\" class=\"mw-ref\" id=\"cite_ref-1\" rel=\"dc:references\" typeof=\"mw:Transclusion mw:Extension/ref\" data-parsoid='{\"dsr\":[64,96,null,null],\"pi\":[[{\"k\":\"1\"}]]}' data-mw='{\"parts\":[{\"template\":{\"target\":{\"wt\":\"echo\",\"href\":\"./Template:Echo\"},\"params\":{\"1\":{\"wt\":\"&lt;ref>{{echo|foo}}&lt;/ref>\"}},\"i\":0}}]}'><a href=\"./Main_Page#cite_note-1\" style=\"counter-reset: mw-Ref 1;\" data-parsoid=\"{}\"><span class=\"mw-reflink-text\" data-parsoid=\"{}\">[1]</span></a></span><meta typeof=\"mw:Transclusion mw:Extension/ref/Marker\" about=\"#mwt2\" data-parsoid='{\"group\":\"\",\"name\":\"\",\"content\":\"foo\",\"hasRefInRef\":false,\"dsr\":[64,96,null,null],\"pi\":[[{\"k\":\"1\"}]]}' data-mw='{\"parts\":[{\"template\":{\"target\":{\"wt\":\"echo\",\"href\":\"./Template:Echo\"},\"params\":{\"1\":{\"wt\":\"&lt;ref>{{echo|foo}}&lt;/ref>\"}},\"i\":0}}]}'/>"}'><a href="./File:Foobar.jpg" data-parsoid='{"a":{"href":"./File:Foobar.jpg"},"sa":{}}'><img resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="220" width="1941" data-parsoid='{"a":{"resource":"./File:Foobar.jpg","height":"220","width":"1941"},"sa":{"resource":"File:Foobar.jpg"}}'/></a></span></p> +<p><figure-inline class="mw-default-size" typeof="mw:Image" data-parsoid='{"optList":[{"ck":"caption","ak":"Undisplayed caption in inline image with ref: {{echo|<ref>{{echo|foo}}</ref>}}"}]}' data-mw='{"caption":"Undisplayed caption in inline image with ref: <sup about=\"#mwt2\" class=\"mw-ref\" id=\"cite_ref-1\" rel=\"dc:references\" typeof=\"mw:Transclusion mw:Extension/ref\" data-parsoid='{\"dsr\":[64,96,null,null],\"pi\":[[{\"k\":\"1\"}]]}' data-mw='{\"parts\":[{\"template\":{\"target\":{\"wt\":\"echo\",\"href\":\"./Template:Echo\"},\"params\":{\"1\":{\"wt\":\"&lt;ref>{{echo|foo}}&lt;/ref>\"}},\"i\":0}}]}'><a href=\"./Main_Page#cite_note-1\" style=\"counter-reset: mw-Ref 1;\" data-parsoid=\"{}\"><span class=\"mw-reflink-text\" data-parsoid=\"{}\">[1]</span></a></sup>"}'><a href="./File:Foobar.jpg" data-parsoid='{"a":{"href":"./File:Foobar.jpg"},"sa":{"href":"File:Foobar.jpg"}}'><img resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="220" width="1941" data-parsoid='{"a":{"resource":"./File:Foobar.jpg","height":"220","width":"1941"},"sa":{"resource":"File:Foobar.jpg"}}'/></a></figure-inline></p> -<ol class="mw-references references" typeof="mw:Extension/references" about="#mwt6" data-mw='{"name":"references","attrs":{}}'><li about="#cite_note-1" id="cite_note-1"><a href="./Main_Page#cite_ref-1" rel="mw:referencedBy"><span class="mw-linkback-text">↑ </span></a> <span id="mw-reference-text-cite_note-1" class="mw-reference-text" data-parsoid="{}">foo</span></li></ol> +<ol class="mw-references references" typeof="mw:Extension/references" about="#mwt6" data-mw='{"name":"references","attrs":{}}'><li about="#cite_note-1" id="cite_note-1"><a href="./Main_Page#cite_ref-1" rel="mw:referencedBy"><span class="mw-linkback-text">↑ </span></a> <span id="mw-reference-text-cite_note-1" class="mw-reference-text">foo</span></li></ol> !! end ### @@ -15768,7 +15767,7 @@ Render invalid page names as plain text (T53090) [[.]] [[..]] [[foo././bar]] -[[foo<a rel="mw:ExtLink" href="http://example.com"></a>xyz]]</p> +[[foo<a rel="mw:ExtLink" class="external autonumber" href="http://example.com"></a>xyz]]</p> <p>[[<span typeof="mw:Transclusion" data-mw='{"parts":[{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"./../foo"}},"i":0}}]}'>./../foo</span>|bar]] [[<span typeof="mw:Transclusion" data-mw='{"parts":[{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"foo/."}},"i":0}}]}'>foo/.</span>|bar]] @@ -15981,7 +15980,8 @@ Bar <p>Foo <link rel="mw:PageProp/Category" href="./Category:Baz"/> Bar</p> <p>Foo <link rel="mw:PageProp/Category" href="./Category:Baz"/> Bar</p> <p>Foo <link rel="mw:PageProp/Category" href="./Category:Baz"/> Bar</p> -<p>Foo <link rel="mw:PageProp/Category" href="./Category:Baz"/> <link rel="mw:PageProp/Category" href="./Category:Baz"/> <link rel="mw:PageProp/Category" href="./Category:Baz"/> Bar <link rel="mw:PageProp/Category" href="./Category:Baz"/> <link rel="mw:PageProp/Category" href="./Category:Baz"/> <link rel="mw:PageProp/Category" href="./Category:Baz"/> <link rel="mw:PageProp/Category" href="./Category:Baz"/> <link rel="mw:PageProp/Category" href="./Category:Baz" typeof="mw:Transclusion" data-mw='{"parts":[{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"[[Category:Baz]]"}},"i":0}}]}'/></p> +<p>Foo <link rel="mw:PageProp/Category" href="./Category:Baz"/> <link rel="mw:PageProp/Category" href="./Category:Baz"/> <link rel="mw:PageProp/Category" href="./Category:Baz"/> Bar</p> +<link rel="mw:PageProp/Category" href="./Category:Baz"/> <link rel="mw:PageProp/Category" href="./Category:Baz"/> <link rel="mw:PageProp/Category" href="./Category:Baz"/> <link rel="mw:PageProp/Category" href="./Category:Baz"/> <link rel="mw:PageProp/Category" href="./Category:Baz" typeof="mw:Transclusion" data-mw='{"parts":[{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"[[Category:Baz]]"}},"i":0}}]}'/> <link rel="mw:PageProp/Category" href="./Category:Baz"/> !! end @@ -16143,6 +16143,68 @@ parsoid=wt2html !! end !! test +9. Categories and newlines: should behave properly with linkprefix (T87753) +!! options +language=ar +!! wikitext +foo bar +foo bar +[[تصنيف:Foo]] +[[تصنيف:Bar]] +!! html/php +<p>foo bar +foo bar +</p> +!! html/parsoid +<p>foo bar +foo bar</p> +<link rel="mw:PageProp/Category" href="./تصنيف:Foo"/> +<link rel="mw:PageProp/Category" href="./تصنيف:Bar"/> +!! end + +!! test +10. No regressions on internal links following category (T174639) +!! options +parsoid=wt2html,html2html +!! wikitext +[[Category:Foo]]<div>a + +[[Foo]]</div> +!! html/php +<div>a +<a href="/wiki/Foo" title="Foo">Foo</a></div> + +!! html/parsoid +<link rel="mw:PageProp/Category" href="./Category:Foo"/><div>a + +<a rel="mw:WikiLink" href="./Foo" title="Foo">Foo</a></div> +!! end + +# Note that Parsoid differs slightly from PHP due to T175421 +!! test +11. Special case where only newlines separate links (T175416) +!! options +parsoid=wt2html,html2html +!! wikitext +[[Category:Foo]] + +[[Foo]][[es:Alimento]] + +[[Foo]] +!! html/php +<p><br /> +<a href="/wiki/Foo" title="Foo">Foo</a> +</p><p><a href="/wiki/Foo" title="Foo">Foo</a> +</p> +!! html/parsoid +<link rel="mw:PageProp/Category" href="./Category:Foo"/> + +<p><a rel="mw:WikiLink" href="./Foo" title="Foo">Foo</a></p><link rel="mw:PageProp/Language" href="http://es.wikipedia.org/wiki/Alimento"/> + +<p><a rel="mw:WikiLink" href="./Foo" title="Foo">Foo</a></p> +!! end + +!! test Category links with multiple namespaces !! wikitext [[Category:Project:Foo]] @@ -16190,6 +16252,20 @@ x[[Category:Foo]]y !! end !! test +Link prefix/suffixes aren't applied to language links +!! options +parsoid=wt2html +language=is +!! wikitext +x[[es:Foo]]y +!! html/php +<p>xy +</p> +!! html/parsoid +<p>x<link rel="mw:PageProp/Language" href="http://es.wikipedia.org/wiki/Foo" data-parsoid=""/>y</p> +!! end + +!! test Parsoid: Serialize link to file page with colon escape !! options parsoid @@ -16288,7 +16364,7 @@ es:1 fr:1 !! test Basic section headings !! wikitext -== Headline 1 == +==Headline 1== Some text ==Headline 2== @@ -16310,16 +16386,16 @@ Blah blah !! test Section headings with TOC !! wikitext -== Headline 1 == -=== Subheadline 1 === -===== Skipping a level ===== -====== Skipping a level ====== +==Headline 1== +===Subheadline 1=== +=====Skipping a level===== +======Skipping a level====== -== Headline 2 == +==Headline 2== Some text ===Another headline=== !! html -<div id="toc" class="toc"><div class="toctitle"><h2>Contents</h2></div> +<div id="toc" class="toc"><div class="toctitle" lang="en" dir="ltr"><h2>Contents</h2></div> <ul> <li class="toclevel-1 tocsection-1"><a href="#Headline_1"><span class="tocnumber">1</span> <span class="toctext">Headline 1</span></a> <ul> @@ -16357,12 +16433,12 @@ Some text TOC anchors don't collide !! wikitext __FORCETOC__ -== Headline 2 == -== Headline == -== Headline 2 == -== Headline == +==Headline 2== +==Headline== +==Headline 2== +==Headline== !! html/php -<div id="toc" class="toc"><div class="toctitle"><h2>Contents</h2></div> +<div id="toc" class="toc"><div class="toctitle" lang="en" dir="ltr"><h2>Contents</h2></div> <ul> <li class="toclevel-1 tocsection-1"><a href="#Headline_2"><span class="tocnumber">1</span> <span class="toctext">Headline 2</span></a></li> <li class="toclevel-1 tocsection-2"><a href="#Headline"><span class="tocnumber">2</span> <span class="toctext">Headline</span></a></li> @@ -16379,21 +16455,24 @@ __FORCETOC__ !! end # perl -e 'print "="x$_," Level $_ heading","="x$_,"\n" for 1..10' +# Parsoid html2wt direction adds <nowiki> for level 7 and up. !! test Handling of sections up to level 6 and beyond +!! options +parsoid=wt2html !! wikitext -= Level 1 Heading= -== Level 2 Heading== -=== Level 3 Heading=== -==== Level 4 Heading==== -===== Level 5 Heading===== -====== Level 6 Heading====== -======= Level 7 Heading======= -======== Level 8 Heading======== -========= Level 9 Heading========= -========== Level 10 Heading========== -!! html -<div id="toc" class="toc"><div class="toctitle"><h2>Contents</h2></div> +=Level 1 Heading= +==Level 2 Heading== +===Level 3 Heading=== +====Level 4 Heading==== +=====Level 5 Heading===== +======Level 6 Heading====== +=======Level 7 Heading======= +========Level 8 Heading======== +=========Level 9 Heading========= +==========Level 10 Heading========== +!! html/php +<div id="toc" class="toc"><div class="toctitle" lang="en" dir="ltr"><h2>Contents</h2></div> <ul> <li class="toclevel-1 tocsection-1"><a href="#Level_1_Heading"><span class="tocnumber">1</span> <span class="toctext">Level 1 Heading</span></a> <ul> @@ -16406,10 +16485,10 @@ Handling of sections up to level 6 and beyond <li class="toclevel-5 tocsection-5"><a href="#Level_5_Heading"><span class="tocnumber">1.1.1.1.1</span> <span class="toctext">Level 5 Heading</span></a> <ul> <li class="toclevel-6 tocsection-6"><a href="#Level_6_Heading"><span class="tocnumber">1.1.1.1.1.1</span> <span class="toctext">Level 6 Heading</span></a></li> -<li class="toclevel-6 tocsection-7"><a href="#.3D_Level_7_Heading.3D"><span class="tocnumber">1.1.1.1.1.2</span> <span class="toctext">= Level 7 Heading=</span></a></li> -<li class="toclevel-6 tocsection-8"><a href="#.3D.3D_Level_8_Heading.3D.3D"><span class="tocnumber">1.1.1.1.1.3</span> <span class="toctext">== Level 8 Heading==</span></a></li> -<li class="toclevel-6 tocsection-9"><a href="#.3D.3D.3D_Level_9_Heading.3D.3D.3D"><span class="tocnumber">1.1.1.1.1.4</span> <span class="toctext">=== Level 9 Heading===</span></a></li> -<li class="toclevel-6 tocsection-10"><a href="#.3D.3D.3D.3D_Level_10_Heading.3D.3D.3D.3D"><span class="tocnumber">1.1.1.1.1.5</span> <span class="toctext">==== Level 10 Heading====</span></a></li> +<li class="toclevel-6 tocsection-7"><a href="#.3DLevel_7_Heading.3D"><span class="tocnumber">1.1.1.1.1.2</span> <span class="toctext">=Level 7 Heading=</span></a></li> +<li class="toclevel-6 tocsection-8"><a href="#.3D.3DLevel_8_Heading.3D.3D"><span class="tocnumber">1.1.1.1.1.3</span> <span class="toctext">==Level 8 Heading==</span></a></li> +<li class="toclevel-6 tocsection-9"><a href="#.3D.3D.3DLevel_9_Heading.3D.3D.3D"><span class="tocnumber">1.1.1.1.1.4</span> <span class="toctext">===Level 9 Heading===</span></a></li> +<li class="toclevel-6 tocsection-10"><a href="#.3D.3D.3D.3DLevel_10_Heading.3D.3D.3D.3D"><span class="tocnumber">1.1.1.1.1.5</span> <span class="toctext">====Level 10 Heading====</span></a></li> </ul> </li> </ul> @@ -16429,24 +16508,35 @@ Handling of sections up to level 6 and beyond <h4><span class="mw-headline" id="Level_4_Heading">Level 4 Heading</span><span class="mw-editsection"><span class="mw-editsection-bracket">[</span><a href="/index.php?title=Parser_test&action=edit&section=4" title="Edit section: Level 4 Heading">edit</a><span class="mw-editsection-bracket">]</span></span></h4> <h5><span class="mw-headline" id="Level_5_Heading">Level 5 Heading</span><span class="mw-editsection"><span class="mw-editsection-bracket">[</span><a href="/index.php?title=Parser_test&action=edit&section=5" title="Edit section: Level 5 Heading">edit</a><span class="mw-editsection-bracket">]</span></span></h5> <h6><span class="mw-headline" id="Level_6_Heading">Level 6 Heading</span><span class="mw-editsection"><span class="mw-editsection-bracket">[</span><a href="/index.php?title=Parser_test&action=edit&section=6" title="Edit section: Level 6 Heading">edit</a><span class="mw-editsection-bracket">]</span></span></h6> -<h6><span class="mw-headline" id=".3D_Level_7_Heading.3D">= Level 7 Heading=</span><span class="mw-editsection"><span class="mw-editsection-bracket">[</span><a href="/index.php?title=Parser_test&action=edit&section=7" title="Edit section: = Level 7 Heading=">edit</a><span class="mw-editsection-bracket">]</span></span></h6> -<h6><span class="mw-headline" id=".3D.3D_Level_8_Heading.3D.3D">== Level 8 Heading==</span><span class="mw-editsection"><span class="mw-editsection-bracket">[</span><a href="/index.php?title=Parser_test&action=edit&section=8" title="Edit section: == Level 8 Heading==">edit</a><span class="mw-editsection-bracket">]</span></span></h6> -<h6><span class="mw-headline" id=".3D.3D.3D_Level_9_Heading.3D.3D.3D">=== Level 9 Heading===</span><span class="mw-editsection"><span class="mw-editsection-bracket">[</span><a href="/index.php?title=Parser_test&action=edit&section=9" title="Edit section: === Level 9 Heading===">edit</a><span class="mw-editsection-bracket">]</span></span></h6> -<h6><span class="mw-headline" id=".3D.3D.3D.3D_Level_10_Heading.3D.3D.3D.3D">==== Level 10 Heading====</span><span class="mw-editsection"><span class="mw-editsection-bracket">[</span><a href="/index.php?title=Parser_test&action=edit&section=10" title="Edit section: ==== Level 10 Heading====">edit</a><span class="mw-editsection-bracket">]</span></span></h6> +<h6><span class="mw-headline" id=".3DLevel_7_Heading.3D">=Level 7 Heading=</span><span class="mw-editsection"><span class="mw-editsection-bracket">[</span><a href="/index.php?title=Parser_test&action=edit&section=7" title="Edit section: =Level 7 Heading=">edit</a><span class="mw-editsection-bracket">]</span></span></h6> +<h6><span class="mw-headline" id=".3D.3DLevel_8_Heading.3D.3D">==Level 8 Heading==</span><span class="mw-editsection"><span class="mw-editsection-bracket">[</span><a href="/index.php?title=Parser_test&action=edit&section=8" title="Edit section: ==Level 8 Heading==">edit</a><span class="mw-editsection-bracket">]</span></span></h6> +<h6><span class="mw-headline" id=".3D.3D.3DLevel_9_Heading.3D.3D.3D">===Level 9 Heading===</span><span class="mw-editsection"><span class="mw-editsection-bracket">[</span><a href="/index.php?title=Parser_test&action=edit&section=9" title="Edit section: ===Level 9 Heading===">edit</a><span class="mw-editsection-bracket">]</span></span></h6> +<h6><span class="mw-headline" id=".3D.3D.3D.3DLevel_10_Heading.3D.3D.3D.3D">====Level 10 Heading====</span><span class="mw-editsection"><span class="mw-editsection-bracket">[</span><a href="/index.php?title=Parser_test&action=edit&section=10" title="Edit section: ====Level 10 Heading====">edit</a><span class="mw-editsection-bracket">]</span></span></h6> +!! html/parsoid +<h1 id="Level_1_Heading" data-parsoid='{}'>Level 1 Heading</h1> +<h2 id="Level_2_Heading" data-parsoid='{}'>Level 2 Heading</h2> +<h3 id="Level_3_Heading" data-parsoid='{}'>Level 3 Heading</h3> +<h4 id="Level_4_Heading" data-parsoid='{}'>Level 4 Heading</h4> +<h5 id="Level_5_Heading" data-parsoid='{}'>Level 5 Heading</h5> +<h6 id="Level_6_Heading" data-parsoid='{}'>Level 6 Heading</h6> +<h6 id="=Level_7_Heading=" data-parsoid='{}'><span id=".3DLevel_7_Heading.3D" typeof="mw:FallbackId"></span>=Level 7 Heading=</h6> +<h6 id="==Level_8_Heading==" data-parsoid='{}'><span id=".3D.3DLevel_8_Heading.3D.3D" typeof="mw:FallbackId"></span>==Level 8 Heading==</h6> +<h6 id="===Level_9_Heading===" data-parsoid='{}'><span id=".3D.3D.3DLevel_9_Heading.3D.3D.3D" typeof="mw:FallbackId"></span>===Level 9 Heading===</h6> +<h6 id="====Level_10_Heading====" data-parsoid='{}'><span id=".3D.3D.3D.3DLevel_10_Heading.3D.3D.3D.3D" typeof="mw:FallbackId"></span>====Level 10 Heading====</h6> !! end !! test TOC regression (T11764) !! wikitext -== title 1 == -=== title 1.1 === -==== title 1.1.1 ==== -=== title 1.2 === -== title 2 == -=== title 2.1 === +==title 1== +===title 1.1=== +====title 1.1.1==== +===title 1.2=== +==title 2== +===title 2.1=== !! html -<div id="toc" class="toc"><div class="toctitle"><h2>Contents</h2></div> +<div id="toc" class="toc"><div class="toctitle" lang="en" dir="ltr"><h2>Contents</h2></div> <ul> <li class="toclevel-1 tocsection-1"><a href="#title_1"><span class="tocnumber">1</span> <span class="toctext">title 1</span></a> <ul> @@ -16481,7 +16571,7 @@ TOC for heading containing <span id="..."></span> (T96153) __FORCETOC__ ==<span id="old-anchor"></span>New title== !! html/php -<div id="toc" class="toc"><div class="toctitle"><h2>Contents</h2></div> +<div id="toc" class="toc"><div class="toctitle" lang="en" dir="ltr"><h2>Contents</h2></div> <ul> <li class="toclevel-1 tocsection-1"><a href="#New_title"><span class="tocnumber">1</span> <span class="toctext">New title</span></a></li> </ul> @@ -16496,14 +16586,14 @@ TOC with wgMaxTocLevel=3 (T8204) !! options wgMaxTocLevel=3 !! wikitext -== title 1 == -=== title 1.1 === -==== title 1.1.1 ==== -=== title 1.2 === -== title 2 == -=== title 2.1 === +==title 1== +===title 1.1=== +====title 1.1.1==== +===title 1.2=== +==title 2== +===title 2.1=== !! html -<div id="toc" class="toc"><div class="toctitle"><h2>Contents</h2></div> +<div id="toc" class="toc"><div class="toctitle" lang="en" dir="ltr"><h2>Contents</h2></div> <ul> <li class="toclevel-1 tocsection-1"><a href="#title_1"><span class="tocnumber">1</span> <span class="toctext">title 1</span></a> <ul> @@ -16539,7 +16629,7 @@ wgMaxTocLevel=3 ====Section 1.1.1.1==== ==Section 2== !! html -<div id="toc" class="toc"><div class="toctitle"><h2>Contents</h2></div> +<div id="toc" class="toc"><div class="toctitle" lang="en" dir="ltr"><h2>Contents</h2></div> <ul> <li class="toclevel-1 tocsection-1"><a href="#Section_1"><span class="tocnumber">1</span> <span class="toctext">Section 1</span></a> <ul> @@ -16562,8 +16652,8 @@ wgMaxTocLevel=3 !! test Resolving duplicate section names !! wikitext -== Foo bar == -== Foo bar == +==Foo bar== +==Foo bar== !! html <h2><span class="mw-headline" id="Foo_bar">Foo bar</span><span class="mw-editsection"><span class="mw-editsection-bracket">[</span><a href="/index.php?title=Parser_test&action=edit&section=1" title="Edit section: Foo bar">edit</a><span class="mw-editsection-bracket">]</span></span></h2> <h2><span class="mw-headline" id="Foo_bar_2">Foo bar</span><span class="mw-editsection"><span class="mw-editsection-bracket">[</span><a href="/index.php?title=Parser_test&action=edit&section=2" title="Edit section: Foo bar">edit</a><span class="mw-editsection-bracket">]</span></span></h2> @@ -16573,8 +16663,8 @@ Resolving duplicate section names !! test Resolving duplicate section names with differing case (T12721) !! wikitext -== Foo bar == -== Foo Bar == +==Foo bar== +==Foo Bar== !! html <h2><span class="mw-headline" id="Foo_bar">Foo bar</span><span class="mw-editsection"><span class="mw-editsection-bracket">[</span><a href="/index.php?title=Parser_test&action=edit&section=1" title="Edit section: Foo bar">edit</a><span class="mw-editsection-bracket">]</span></span></h2> <h2><span class="mw-headline" id="Foo_Bar_2">Foo Bar</span><span class="mw-editsection"><span class="mw-editsection-bracket">[</span><a href="/index.php?title=Parser_test&action=edit&section=2" title="Edit section: Foo Bar">edit</a><span class="mw-editsection-bracket">]</span></span></h2> @@ -16628,11 +16718,11 @@ Link inside a section heading TOC regression (T14077) !! wikitext __TOC__ -== title 1 == -=== title 1.1 === -== title 2 == +==title 1== +===title 1.1=== +==title 2== !! html -<div id="toc" class="toc"><div class="toctitle"><h2>Contents</h2></div> +<div id="toc" class="toc"><div class="toctitle" lang="en" dir="ltr"><h2>Contents</h2></div> <ul> <li class="toclevel-1 tocsection-1"><a href="#title_1"><span class="tocnumber">1</span> <span class="toctext">title 1</span></a> <ul> @@ -16657,24 +16747,33 @@ http://example.com [[File:Foobar.jpg]] <p><a rel="nofollow" class="external free" href="http://example.com">http://example.com</a> <a href="/wiki/File:Foobar.jpg" class="image"><img alt="Foobar.jpg" src="http://example.com/images/3/3a/Foobar.jpg" width="1941" height="220" /></a> </p> !! html/parsoid -<p><a rel="mw:ExtLink" href="http://example.com">http://example.com</a> <span class="mw-default-size" typeof="mw:Image"><a href="./File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="220" width="1941"/></a></span></p> +<p><a rel="mw:ExtLink" class="external free" href="http://example.com">http://example.com</a> <figure-inline class="mw-default-size" typeof="mw:Image"><a href="./File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="220" width="1941"/></a></figure-inline></p> !!end +# Parsoid doesn't wt2wt this cleanly because it adds <nowiki>s. !! test Short headings with trailing space should match behavior of Parser::doHeadings (T21910) +!! options +parsoid=wt2html,html2html !! wikitext === The line above must have a trailing space! === <!-- --> <!-- --> But just in case it doesn't... -!! html +!! html/php <h1><span class="mw-headline" id=".3D">=</span><span class="mw-editsection"><span class="mw-editsection-bracket">[</span><a href="/index.php?title=Parser_test&action=edit&section=1" title="Edit section: =">edit</a><span class="mw-editsection-bracket">]</span></span></h1> <p>The line above must have a trailing space! </p> <h1><span class="mw-headline" id=".3D_2">=</span><span class="mw-editsection"><span class="mw-editsection-bracket">[</span><a href="/index.php?title=Parser_test&action=edit&section=2" title="Edit section: =">edit</a><span class="mw-editsection-bracket">]</span></span></h1> <p>But just in case it doesn't... </p> +!! html/parsoid +<h1 id="="><span id=".3D" typeof="mw:FallbackId"></span>=</h1> +<p>The line above must have a trailing space!</p> +<h1 id="=_2"><span id=".3D_2" typeof="mw:FallbackId"></span>=</h1> <!-- +--> <!-- --> +<p>But just in case it doesn't...</p> !! end !! test @@ -16682,24 +16781,24 @@ Header with special characters (T27462) !! wikitext The tooltips shall not show entities to the user (ie. be double escaped) -== text > text == +==text > text== section 1 -== text < text == +==text < text== section 2 -== text & text == +==text & text== section 3 -== text ' text == +==text ' text== section 4 -== text " text == +==text " text== section 5 -!! html +!! html/php <p>The tooltips shall not show entities to the user (ie. be double escaped) </p> -<div id="toc" class="toc"><div class="toctitle"><h2>Contents</h2></div> +<div id="toc" class="toc"><div class="toctitle" lang="en" dir="ltr"><h2>Contents</h2></div> <ul> <li class="toclevel-1 tocsection-1"><a href="#text_.3E_text"><span class="tocnumber">1</span> <span class="toctext">text > text</span></a></li> <li class="toclevel-1 tocsection-2"><a href="#text_.3C_text"><span class="tocnumber">2</span> <span class="toctext">text < text</span></a></li> @@ -16724,6 +16823,23 @@ section 5 <h2><span class="mw-headline" id="text_.22_text">text " text</span><span class="mw-editsection"><span class="mw-editsection-bracket">[</span><a href="/index.php?title=Parser_test&action=edit&section=5" title="Edit section: text " text">edit</a><span class="mw-editsection-bracket">]</span></span></h2> <p>section 5 </p> +!! html/parsoid +<p>The tooltips shall not show entities to the user (ie. be double escaped)</p> + +<h2 id="text_>_text"><span id="text_.3E_text" typeof="mw:FallbackId"></span>text > text</h2> +<p>section 1</p> + +<h2 id="text_<_text"><span id="text_.3C_text" typeof="mw:FallbackId"></span>text < text</h2> +<p>section 2</p> + +<h2 id="text_&_text"><span id="text_.26_text" typeof="mw:FallbackId"></span>text & text</h2> +<p>section 3</p> + +<h2 id="text_'_text"><span id="text_.27_text" typeof="mw:FallbackId"></span>text ' text</h2> +<p>section 4</p> + +<h2 id='text_"_text'><span id="text_.22_text" typeof="mw:FallbackId"></span>text " text</h2> +<p>section 5</p> !! end !! test @@ -16731,22 +16847,22 @@ Header with space, plus and underscore as entity !! wikitext Id should not contain + for spaces -== Space between Text == +==Space between Text== section 1 -== Space-Entity between Text == +==Space-Entity between Text== section 2 -== Plus+between+Text == +==Plus+between+Text== section 3 -== Plus-Entity+between+Text == +==Plus-Entity+between+Text== section 4 -== Underscore_between_Text == +==Underscore_between_Text== section 5 -== Underscore-Entity_between_Text == +==Underscore-Entity_between_Text== section 6 [[#Space between Text]] @@ -16755,10 +16871,10 @@ section 6 [[#Plus-Entity+between+Text]] [[#Underscore_between_Text]] [[#Underscore-Entity_between_Text]] -!! html +!! html/php <p>Id should not contain + for spaces </p> -<div id="toc" class="toc"><div class="toctitle"><h2>Contents</h2></div> +<div id="toc" class="toc"><div class="toctitle" lang="en" dir="ltr"><h2>Contents</h2></div> <ul> <li class="toclevel-1 tocsection-1"><a href="#Space_between_Text"><span class="tocnumber">1</span> <span class="toctext">Space between Text</span></a></li> <li class="toclevel-1 tocsection-2"><a href="#Space-Entity_between_Text"><span class="tocnumber">2</span> <span class="toctext">Space-Entity between Text</span></a></li> @@ -16793,18 +16909,48 @@ section 6 <a href="#Underscore_between_Text">#Underscore_between_Text</a> <a href="#Underscore-Entity_between_Text">#Underscore-Entity_between_Text</a> </p> +!! html/parsoid +<p>Id should not contain + for spaces</p> + +<h2 id="Space_between_Text">Space between Text</h2> +<p>section 1</p> + +<h2 id="Space-Entity_between_Text">Space-Entity<span typeof="mw:Entity" data-parsoid='{"src":"&#32;","srcContent":" "}'> </span>between<span typeof="mw:Entity" data-parsoid='{"src":"&#32;","srcContent":" "}'> </span>Text</h2> +<p>section 2</p> + +<h2 id="Plus+between+Text"><span id="Plus.2Bbetween.2BText" typeof="mw:FallbackId"></span>Plus+between+Text</h2> +<p>section 3</p> + +<h2 id="Plus-Entity+between+Text"><span id="Plus-Entity.2Bbetween.2BText" typeof="mw:FallbackId"></span>Plus-Entity<span typeof="mw:Entity" data-parsoid='{"src":"&#43;","srcContent":"+"}'>+</span>between<span typeof="mw:Entity" data-parsoid='{"src":"&#43;","srcContent":"+"}'>+</span>Text</h2> +<p>section 4</p> + +<h2 id="Underscore_between_Text">Underscore_between_Text</h2> +<p>section 5</p> + +<h2 id="Underscore-Entity_between_Text">Underscore-Entity<span typeof="mw:Entity" data-parsoid='{"src":"&#95;","srcContent":"_"}'>_</span>between<span typeof="mw:Entity" data-parsoid='{"src":"&#95;","srcContent":"_"}'>_</span>Text</h2> +<p>section 6</p> + +<p><a rel="mw:WikiLink" href="./Main_Page#Space_between_Text" data-parsoid='{"stx":"simple","a":{"href":"./Main_Page#Space_between_Text"},"sa":{"href":"#Space between Text"}}'>#Space between Text</a> +<a rel="mw:WikiLink" href="./Main_Page#Space-Entity_between_Text" data-parsoid='{"stx":"simple","a":{"href":"./Main_Page#Space-Entity_between_Text"},"sa":{"href":"#Space-Entity&#32;between&#32;Text"}}'>#Space-Entity between Text</a> +<a rel="mw:WikiLink" href="./Main_Page#Plus+between+Text" data-parsoid='{"stx":"simple","a":{"href":"./Main_Page#Plus+between+Text"},"sa":{"href":"#Plus+between+Text"}}'>#Plus+between+Text</a> +<a rel="mw:WikiLink" href="./Main_Page#Plus-Entity+between+Text" data-parsoid='{"stx":"simple","a":{"href":"./Main_Page#Plus-Entity+between+Text"},"sa":{"href":"#Plus-Entity&#43;between&#43;Text"}}'>#Plus-Entity+between+Text</a> +<a rel="mw:WikiLink" href="./Main_Page#Underscore_between_Text" data-parsoid='{"stx":"simple","a":{"href":"./Main_Page#Underscore_between_Text"},"sa":{"href":"#Underscore_between_Text"}}'>#Underscore_between_Text</a> +<a rel="mw:WikiLink" href="./Main_Page#Underscore-Entity_between_Text" data-parsoid='{"stx":"simple","a":{"href":"./Main_Page#Underscore-Entity_between_Text"},"sa":{"href":"#Underscore-Entity&#95;between&#95;Text"}}'>#Underscore-Entity_between_Text</a></p> !! end +# Parsoid html2wt disabled because it adds padding spaces around = !! test Headers with excess '=' characters (Are similar tests necessary beyond the 1st level?) +!! options +parsoid=wt2html,wt2wt,html2html !! wikitext =foo== ==foo= =''italic'' heading== ==''italic'' heading= -!! html -<div id="toc" class="toc"><div class="toctitle"><h2>Contents</h2></div> +!! html/php +<div id="toc" class="toc"><div class="toctitle" lang="en" dir="ltr"><h2>Contents</h2></div> <ul> <li class="toclevel-1 tocsection-1"><a href="#foo.3D"><span class="tocnumber">1</span> <span class="toctext">foo=</span></a></li> <li class="toclevel-1 tocsection-2"><a href="#.3Dfoo"><span class="tocnumber">2</span> <span class="toctext">=foo</span></a></li> @@ -16818,6 +16964,11 @@ Headers with excess '=' characters <h1><span class="mw-headline" id="italic_heading.3D"><i>italic</i> heading=</span><span class="mw-editsection"><span class="mw-editsection-bracket">[</span><a href="/index.php?title=Parser_test&action=edit&section=3" title="Edit section: italic heading=">edit</a><span class="mw-editsection-bracket">]</span></span></h1> <h1><span class="mw-headline" id=".3Ditalic_heading">=<i>italic</i> heading</span><span class="mw-editsection"><span class="mw-editsection-bracket">[</span><a href="/index.php?title=Parser_test&action=edit&section=4" title="Edit section: =italic heading">edit</a><span class="mw-editsection-bracket">]</span></span></h1> +!! html/parsoid +<h1 id="foo="><span id="foo.3D" typeof="mw:FallbackId"></span>foo=</h1> +<h1 id="=foo"><span id=".3Dfoo" typeof="mw:FallbackId"></span>=foo</h1> +<h1 id="italic_heading="><span id="italic_heading.3D" typeof="mw:FallbackId"></span><i>italic</i> heading=</h1> +<h1 id="=italic_heading"><span id=".3Ditalic_heading" typeof="mw:FallbackId"></span>=<i>italic</i> heading</h1> !! end !! test @@ -16825,16 +16976,16 @@ HTML headers vs TOC (T25393) (__NOEDITSECTION__ for clearer output, doesn't matter here) !! wikitext <h1>Header 1</h1> -== Header 1.1 == -== Header 1.2 == +==Header 1.1== +==Header 1.2== <h1>Header 2 </h1> -== Header 2.1 == -== Header 2.2 == +==Header 2.1== +==Header 2.2== __NOEDITSECTION__ -!! html -<div id="toc" class="toc"><div class="toctitle"><h2>Contents</h2></div> +!! html/php +<div id="toc" class="toc"><div class="toctitle" lang="en" dir="ltr"><h2>Contents</h2></div> <ul> <li class="toclevel-1"><a href="#Header_1"><span class="tocnumber">1</span> <span class="toctext">Header 1</span></a> <ul> @@ -16854,10 +17005,21 @@ __NOEDITSECTION__ <h1><span class="mw-headline" id="Header_1">Header 1</span></h1> <h2><span class="mw-headline" id="Header_1.1">Header 1.1</span></h2> <h2><span class="mw-headline" id="Header_1.2">Header 1.2</span></h2> -<h1><span class="mw-headline" id="Header_2">Header 2</span></h1> +<h1><span class="mw-headline" id="Header_2">Header 2 +</span></h1> <h2><span class="mw-headline" id="Header_2.1">Header 2.1</span></h2> <h2><span class="mw-headline" id="Header_2.2">Header 2.2</span></h2> +!! html/parsoid +<h1 id="Header_1" data-parsoid='{"stx":"html"}'>Header 1</h1> +<h2 id="Header_1.1" data-parsoid='{}'>Header 1.1</h2> +<h2 id="Header_1.2" data-parsoid='{}'>Header 1.2</h2> + +<h1 id="Header_2" data-parsoid='{"stx":"html"}'>Header 2 +</h1> +<h2 id="Header_2.1" data-parsoid='{}'>Header 2.1</h2> +<h2 id="Header_2.2" data-parsoid='{}'>Header 2.2</h2> +<meta property="mw:PageProp/noeditsection"/> !! end !! test @@ -16870,11 +17032,17 @@ parsoid=wt2html,wt2wt ==baz==<!-- c2 c3--> -!! html -<h2><span class="mw-headline" id="foo">foo</span></h2> -<h2><span class="mw-headline" id="bar">bar</span></h2> -<h2><span class="mw-headline" id="baz">baz</span></h2> +!! html/php +<h2><span class="mw-headline" id="foo">foo</span><span class="mw-editsection"><span class="mw-editsection-bracket">[</span><a href="/index.php?title=Parser_test&action=edit&section=1" title="Edit section: foo">edit</a><span class="mw-editsection-bracket">]</span></span></h2> +<h2><span class="mw-headline" id="bar">bar</span><span class="mw-editsection"><span class="mw-editsection-bracket">[</span><a href="/index.php?title=Parser_test&action=edit&section=2" title="Edit section: bar">edit</a><span class="mw-editsection-bracket">]</span></span></h2> +<h2><span class="mw-headline" id="baz">baz</span><span class="mw-editsection"><span class="mw-editsection-bracket">[</span><a href="/index.php?title=Parser_test&action=edit&section=3" title="Edit section: baz">edit</a><span class="mw-editsection-bracket">]</span></span></h2> +!! html/parsoid +<h2 id="foo">foo</h2><!----> +<h2 id="bar">bar</h2><!--c1--> +<h2 id="baz">baz</h2><!-- +c2 +c3--> !! end !! test @@ -16885,7 +17053,7 @@ http://example.com[[File:Foobar.jpg]] <p><a rel="nofollow" class="external free" href="http://example.com">http://example.com</a><a href="/wiki/File:Foobar.jpg" class="image"><img alt="Foobar.jpg" src="http://example.com/images/3/3a/Foobar.jpg" width="1941" height="220" /></a> </p> !! html/parsoid -<p><a rel="mw:ExtLink" href="http://example.com">http://example.com</a><span class="mw-default-size" typeof="mw:Image"><a href="./File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="220" width="1941"/></a></span></p> +<p><a rel="mw:ExtLink" class="external free" href="http://example.com">http://example.com</a><figure-inline class="mw-default-size" typeof="mw:Image"><a href="./File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="220" width="1941"/></a></figure-inline></p> !!end !! test @@ -16981,15 +17149,31 @@ parsoid=wt2html,html2html !! test div with multiple empty attribute values +!! config +wgFragmentMode=[ 'html5', 'legacy' ] !! options parsoid=wt2html,html2html !! wikitext <div id= title=>HTML rocks</div> !! html/php -<div id="title.3D">HTML rocks</div> +<div id="title=">HTML rocks</div> + +!! html/parsoid +<div id="title=" data-parsoid='{"stx":"html"}'>HTML rocks</div> +!! end +# FIXME Parsoid doesn't actually match PHP here. +# Probably we should use the synthetic <foo /> or <indicator> +# extensions for this test, which are enabled when running parser tests. +!! test +Extension tag in attribute value +!! wikitext +<span title="<translate>123</translate>">ok</span> +!! html/php+disabled +<p><span title="<translate>123</translate>">ok</span> +</p> !! html/parsoid -<div id="title.3D" data-parsoid='{"stx":"html"}'>HTML rocks</div> +<p><span title="123" about="#mwt4" typeof="mw:ExpandedAttrs" data-parsoid='{"stx":"html","a":{"title":"123"},"sa":{"title":"<translate>123</translate>"}}' data-mw='{"attribs":[[{"txt":"title"},{"html":"<translate typeof=\"mw:Extension/translate\" about=\"#mwt3\" data-parsoid='{\"dsr\":[13,39,2,2]}' data-mw='{\"name\":\"translate\",\"attrs\":{},\"body\":{\"extsrc\":\"123\"}}'>123</translate>"}]]}'>ok</span></p> !! end !! test @@ -16998,17 +17182,17 @@ table with multiple empty attribute values parsoid=wt2html,html2html !! wikitext {| title= id= -| hi +|hi |} !! html/php <table title="id="> <tr> -<td> hi +<td>hi </td></tr></table> !! html/parsoid <table title="id="> -<tbody><tr><td> hi</td></tr> +<tbody><tr><td>hi</td></tr> </tbody></table> !! end @@ -17049,12 +17233,12 @@ HTML multiple attributes correction Table multiple attributes correction !! wikitext {| -!+ class="error" class="awesome"| status +!+ class="error" class="awesome"|status |} !! html <table> <tr> -<th class="awesome"> status +<th class="awesome">status </th></tr></table> !!end @@ -17099,11 +17283,9 @@ Remember AT&T? text with character entity: eacute !! wikitext I always thought é was a cute letter. -!! html +!! html+tidy <p>I always thought é was a cute letter. </p> -!! html+tidy -<p>I always thought é was a cute letter.</p> !! end !! test @@ -17181,12 +17363,11 @@ Ensure that HTML adoption agency algorithm is properly implemented. !! end # This was T43545 in the PHP parser. -# Note that tidy doesn't handle this correctly. !! test Nesting of <kbd> !! wikitext <kbd>X<kbd>Y</kbd>Z</kbd> -!! html +!! html+tidy <p><kbd>X<kbd>Y</kbd>Z</kbd> </p> !! end @@ -17195,22 +17376,20 @@ Nesting of <kbd> # Note that there are some other nestable tags (b, i, etc) which are # not covered; see T53081 for discussion. -# Note that tidy doesn't handle this correctly. !! test Nesting of <em> !! wikitext <em>X<em>Y</em>Z</em> -!! html +!! html+tidy <p><em>X<em>Y</em>Z</em> </p> !! end -# Note that tidy doesn't handle this correctly. !! test Nesting of <strong> !! wikitext <strong>X<strong>Y</strong>Z</strong> -!! html +!! html+tidy <p><strong>X<strong>Y</strong>Z</strong> </p> !! end @@ -17220,10 +17399,10 @@ Nesting of <q> !! wikitext <q>X<q>Y</q>Z</q> !! html+tidy -<p><q>X<q>Y</q>Z</q></p> +<p><q>X<q>Y</q>Z</q> +</p> !! end -# Note that tidy doesn't handle this correctly. !! test Nesting of <ruby> !! wikitext @@ -17233,7 +17412,6 @@ Nesting of <ruby> </p> !! end -# Note that tidy doesn't handle this correctly. !! test Nesting of <bdo> !! wikitext @@ -17278,6 +17456,7 @@ Media link with text # FIXME: this is still bad HTML tag nesting # FIXME: doBlockLevels won't wrap this in a paragraph because it contains a div +# Parsoid & Remex fix the p-wrapping since they operate on the DOM. !! test Media link with nasty text !! wikitext @@ -17285,9 +17464,8 @@ Media link with nasty text !! html/php <a href="http://example.com/images/3/3a/Foobar.jpg" class="internal" title="Foobar.jpg">Safe Link<div style="display:none">" onmouseover="alert(document.cookie)" onfoo="</div></a> -!! html+php/tidy -<p><a href="http://example.com/images/3/3a/Foobar.jpg" class="internal" title="Foobar.jpg">Safe Link</a></p> -<div style="display:none">" onmouseover="alert(document.cookie)" onfoo="</div> +!! html/php+tidy +<p><a href="http://example.com/images/3/3a/Foobar.jpg" class="internal" title="Foobar.jpg">Safe Link</a></p><a href="http://example.com/images/3/3a/Foobar.jpg" class="internal" title="Foobar.jpg"><div style="display:none">" onmouseover="alert(document.cookie)" onfoo="</div></a> !! html/parsoid <p><a rel="mw:MediaLink" href="//example.com/images/3/3a/Foobar.jpg" title="Foobar.jpg" data-parsoid='{"autoInsertedEnd":true}'>Safe Link</a></p><div style="display:none" data-parsoid='{"stx":"html"}'><a rel="mw:MediaLink" href="//example.com/images/3/3a/Foobar.jpg" title="Foobar.jpg" data-parsoid='{"autoInsertedEnd":true,"autoInsertedStart":true}'>" onmouseover="alert(document.cookie)" onfoo="</a></div> @@ -17315,7 +17493,7 @@ Image link to nonexistent file (T3850 - good) <p><a href="/index.php?title=Special:Upload&wpDestFile=No_such.jpg" class="new" title="File:No such.jpg">File:No such.jpg</a> </p> !! html/parsoid -<p><span class="mw-default-size" typeof="mw:Error mw:Image" data-mw='{"errors":[{"key":"apierror-filedoesnotexist","message":"This image does not exist."}]}'><a href="./File:No_such.jpg"><img resource="./File:No_such.jpg" src="./Special:FilePath/No_such.jpg" height="220" width="220"/></a></span></p> +<p><figure-inline class="mw-default-size" typeof="mw:Error mw:Image" data-mw='{"errors":[{"key":"apierror-filedoesnotexist","message":"This image does not exist."}]}'><a href="./File:No_such.jpg"><img resource="./File:No_such.jpg" src="./Special:FilePath/No_such.jpg" height="220" width="220"/></a></figure-inline></p> !! end !! test @@ -17567,9 +17745,11 @@ T4304: HTML attribute safety (unsafe breakout parameter 2; 2309) T4304: HTML attribute safety (link) !! wikitext <div title="[[Main Page]]"></div> -!! html -<div title="[[Main Page]]"></div> +!! html/php +<div title="[[Main Page]]"></div> +!! html/parsoid +<div title="[[Main Page]]"></div> !! end !! test @@ -17630,9 +17810,11 @@ T4304: HTML attribute safety (web link) T4304: HTML attribute safety (named web link) !! wikitext <div title="[http://example.com/ link]"></div> -!! html -<div title="[http://example.com/ link]"></div> +!! html/php +<div title="[http://example.com/ link]"></div> +!! html/parsoid +<div title="[http://example.com/ link]"></div> !! end !! test @@ -17807,12 +17989,12 @@ MSIE 6 CSS safety test: Repetition markers (T57332) Table attribute legitimate extension !! wikitext {| -!+ style="<nowiki>color:blue</nowiki>"| status +!+ style="<nowiki>color:blue</nowiki>"|status |} !! html <table> <tr> -<th style="color:blue"> status +<th style="color:blue">status </th></tr></table> !!end @@ -17821,12 +18003,12 @@ Table attribute legitimate extension Table attribute safety !! wikitext {| -!+ style="<nowiki>border-width:expression(0+alert(document.cookie))</nowiki>"| status +!+ style="<nowiki>border-width:expression(0+alert(document.cookie))</nowiki>"|status |} !! html <table> <tr> -<th style="/* insecure input */"> status +<th style="/* insecure input */">status </th></tr></table> !! end @@ -17887,11 +18069,12 @@ Expansion of multi-line templates in attribute values (T8255 sanity check 2) !! end !! test -Tags which are hidden from Tidy cannot pass through the Sanitizer +Tags which are hidden from tidiers cannot pass through the Sanitizer !! wikitext <mw:toc><script>alert();</script></mw:toc> !! html+tidy -<p><mw:toc><script>alert();</script></mw:toc></p> +<p><mw:toc><script>alert();</script></mw:toc> +</p> !! end ### @@ -18135,6 +18318,27 @@ this is a '''test''' <p>this is a <b>test</b></p> !! end +!! test +Parser hook: horizontal rule inside extension tag that outputs <pre> +!! wikitext +<tag> +Hello +<hr/> +Goodbye +</tag> +!! html/php +<pre> +' +Hello +<hr/> +Goodbye +' +array ( +) +</pre> + +!! end + ### ### (see tests/parser/parserTestsParserHook.php for the <statictag> extension) ### @@ -18196,18 +18400,16 @@ Nested template calls ### Sanitizer ### -# HTML+Tidy effectively strips out the empty tags completely -# But since Parsoid doesn't it wraps the <s></s> tags in p-tags -# which Tidy would have done for the PHP parser had there been content inside it. +# Remex wraps empty tag runs with p-tags. +# Parsoid strips them out during p-wrapping. !! test Sanitizer: Closing of open tags !! wikitext <s></s><table></table> -!! html -<s></s><table></table> - -!! html/parsoid +!! html/php+tidy <p><s></s></p><table></table> +!! html/parsoid +<s></s><table></table> !! end !! test @@ -18226,6 +18428,8 @@ parsoid=wt2html !! wikitext </s> !! html/php+tidy +<p class="mw-empty-elt"> +</p> !! html/parsoid !! end @@ -18235,21 +18439,33 @@ Sanitizer: Closing of closed but not open table tags parsoid=wt2html !! wikitext Table not started</td></tr></table> -!! html/php+tidy -<p>Table not started</p> -!! html/parsoid -<p>Table not started</p> +!! html+tidy +<p>Table not started +</p> !! end !! test Sanitizer: Escaping of spaces, multibyte characters, colons & other stuff in id="" +!! config +wgFragmentMode=[ 'html5', 'legacy' ] !! wikitext <span id="æ: v">byte</span>[[#æ: v|backlink]] !! html/php -<p><span id=".C3.A6:_v">byte</span><a href="#.C3.A6:_v">backlink</a> +<p><span id="æ:_v">byte</span><a href="#æ:_v">backlink</a> </p> !! html/parsoid -<p><span id=".C3.A6:_v" data-parsoid='{"stx":"html","a":{"id":".C3.A6:_v"},"sa":{"id":"æ: v"}}'>byte</span><a rel="mw:WikiLink" href="./Main_Page#.C3.A6:_v" data-parsoid='{"stx":"piped","a":{"href":"./Main_Page#.C3.A6:_v"},"sa":{"href":"#æ: v"}}'>backlink</a></p> +<p><span id="æ:_v" data-parsoid='{"stx":"html","a":{"id":"æ:_v"},"sa":{"id":"æ: v"}}'>byte</span><a rel="mw:WikiLink" href="./Main_Page#æ:_v" data-parsoid='{"stx":"piped","a":{"href":"./Main_Page#æ:_v"},"sa":{"href":"#æ: v"}}'>backlink</a></p> +!! end + +!! test +Sanitizer: Escaping of spaces, multibyte characters, colons & other stuff in id="" (legacy) +!! config +wgFragmentMode=[ 'legacy' ] +!! wikitext +<span id="æ: v">byte</span>[[#æ: v|backlink]] +!! html/php +<p><span id=".C3.A6:_v">byte</span><a href="#.C3.A6:_v">backlink</a> +</p> !! end # In HTML5, the restrictions are that id must contain at least one character, @@ -18313,6 +18529,37 @@ parsoid=wt2html,wt2wt !! end !! test +Sanitizer: Avoid unnecessary percent encoded characters in interwiki links +!! wikitext +[[meatball:Soft"Security]] +!! html/php +<p><a href="http://www.usemod.com/cgi-bin/mb.pl?Soft%22Security" class="extiw" title="meatball:Soft"Security">meatball:Soft"Security</a> +</p> +!! html/parsoid +<p><a rel="mw:WikiLink/Interwiki" href='http://www.usemod.com/cgi-bin/mb.pl?Soft"Security' title='meatball:Soft"Security'>meatball:Soft"Security</a></p> +!! end + +!! test +Sanitizer: angle brackets are invalid, even in interwiki links (T182338) +!! wikitext +[[meatball:Foo<Bar]] +[[meatball:Foo>Bar]] +[[meatball:Foo<bar]] +[[meatball:Foo>bar]] +!! html/php +<p>[[meatball:Foo<Bar]] +[[meatball:Foo>Bar]] +[[meatball:Foo<bar]] +[[meatball:Foo>bar]] +</p> +!! html/parsoid +<p>[[meatball:Foo<Bar]] +[[meatball:Foo>Bar]] +[[meatball:Foo<span typeof="mw:Entity" data-parsoid='{"src":"&lt;","srcContent":"<"}'><</span>bar]] +[[meatball:Foo<span typeof="mw:Entity" data-parsoid='{"src":"&gt;","srcContent":">"}'>></span>bar]]</p> +!! end + +!! test Language converter: output gets cut off unexpectedly (T7757) !! options language=zh @@ -18345,10 +18592,14 @@ language=sr variant=sr-el -{H|foAjrjvi=>sr-el:" onload="alert(1)" data-foo="}- [[File:Foobar.jpg|alt=-{}-foAjrjvi-{}-]] -!! html +!! html/php <p> </p><p><a href="/wiki/%D0%94%D0%B0%D1%82%D0%BE%D1%82%D0%B5%D0%BA%D0%B0:Foobar.jpg" class="image"><img alt="" onload="alert(1)" data-foo="" src="http://example.com/images/3/3a/Foobar.jpg" width="1941" height="220" /></a> </p> +!! html/parsoid +<p><meta typeof="mw:LanguageVariant" data-mw-variant='{"add":true,"oneway":[{"f":"foAjrjvi","l":"sr-el","t":"\" onload=\"alert(1)\" data-foo=\""}]}'/></p> + +<p><figure-inline class="mw-default-size" typeof="mw:Image"><a href="./Датотека:Foobar.jpg"><img alt="foAjrjvi" resource="./Датотека:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="220" width="1941" data-parsoid='{"a":{"alt":"foAjrjvi","resource":"./Датотека:Foobar.jpg","height":"220","width":"1941"},"sa":{"alt":"alt=-{}-foAjrjvi-{}-","resource":"File:Foobar.jpg"}}'/></a></figure-inline></p> !! end !! test @@ -18361,10 +18612,6 @@ Self closed html pairs (T7487) <div><font id="bug2"></font>In div text</div> !! end -# -# -# - !! test Punctuation: nbsp before exclamation !! wikitext @@ -18422,9 +18669,9 @@ HTML bullet list, unclosed tags (T7497) </ul> !! html/php+tidy <ul> -<li>One</li> -<li>Two</li> -</ul> +<li>One +</li><li>Two +</li></ul> !! html/parsoid <ul data-parsoid='{"stx":"html"}'> <li data-parsoid='{"stx":"html","autoInsertedEnd":true}'>One</li> @@ -18464,9 +18711,9 @@ HTML ordered list, unclosed tags (T7497) </ol> !! html/php+tidy <ol> -<li>One</li> -<li>Two</li> -</ol> +<li>One +</li><li>Two +</li></ol> !! html/parsoid <ol data-parsoid='{"stx":"html"}'> <li data-parsoid='{"stx":"html","autoInsertedEnd":true}'>One</li> @@ -18521,30 +18768,15 @@ HTML nested bullet list, open tags (T7497) <li>Sub-two </ul> </ul> -!! html/php+tidy -<ul> -<li>One</li> -<li>Two: -<ul> -<li>Sub-one</li> -<li>Sub-two</li> -</ul> -</li> -</ul> -!! html/parsoid +!! html+tidy <ul> <li>One -</li> -<li>Two: +</li><li>Two: <ul> <li>Sub-one -</li> -<li>Sub-two -</li> -</ul> -</li> -</ul> - +</li><li>Sub-two +</li></ul> +</li></ul> !! end !! test @@ -18641,11 +18873,11 @@ mailto:inline@mail.tld </p><p><a rel="nofollow" class="external free" href="mailto:inline@mail.tld">mailto:inline@mail.tld</a> </p> !! html/parsoid -<p><a rel="mw:ExtLink" href="http://first/"></a> <a rel="mw:ExtLink" href="http://second"></a> <a rel="mw:ExtLink" href="ftp://ftp"></a></p> -<p><a rel="mw:ExtLink" href="ftp://inlineftp">ftp://inlineftp</a></p> -<p><a rel="mw:ExtLink" href="mailto:enclosed@mail.tld">With target</a></p> -<p><a rel="mw:ExtLink" href="mailto:enclosed@mail.tld"></a></p> -<p><a rel="mw:ExtLink" href="mailto:inline@mail.tld">mailto:inline@mail.tld</a></p> +<p><a rel="mw:ExtLink" class="external autonumber" href="http://first/"></a> <a rel="mw:ExtLink" class="external autonumber" href="http://second"></a> <a rel="mw:ExtLink" class="external autonumber" href="ftp://ftp"></a></p> +<p><a rel="mw:ExtLink" class="external free" href="ftp://inlineftp">ftp://inlineftp</a></p> +<p><a rel="mw:ExtLink" class="external text" href="mailto:enclosed@mail.tld">With target</a></p> +<p><a rel="mw:ExtLink" class="external autonumber" href="mailto:enclosed@mail.tld"></a></p> +<p><a rel="mw:ExtLink" class="external free" href="mailto:inline@mail.tld">mailto:inline@mail.tld</a></p> !! end @@ -18669,32 +18901,33 @@ Fuzz testing: Parser13 !! end +# Note that Parsoid output differs from the PHP parser here: the PHP +# parser breaks the URL for the magic word, while in Parsoid the URL +# production takes precedence. !! test Fuzz testing: Parser14 !! wikitext -== onmouseover= == +==onmouseover=== http://__TOC__ -!! html +!! html/php <h2><span class="mw-headline" id="onmouseover.3D">onmouseover=</span><span class="mw-editsection"><span class="mw-editsection-bracket">[</span><a href="/index.php?title=Parser_test&action=edit&section=1" title="Edit section: onmouseover=">edit</a><span class="mw-editsection-bracket">]</span></span></h2> -http://<div id="toc" class="toc"><div class="toctitle"><h2>Contents</h2></div> +http://<div id="toc" class="toc"><div class="toctitle" lang="en" dir="ltr"><h2>Contents</h2></div> <ul> <li class="toclevel-1 tocsection-1"><a href="#onmouseover.3D"><span class="tocnumber">1</span> <span class="toctext">onmouseover=</span></a></li> </ul> </div> -!! html+tidy -<h2><span class="mw-headline" id="onmouseover.3D">onmouseover=</span><span class="mw-editsection"><span class="mw-editsection-bracket">[</span><a href="/index.php?title=Parser_test&action=edit&section=1" title="Edit section: onmouseover=">edit</a><span class="mw-editsection-bracket">]</span></span></h2> -<p>http://</p> -<div id="toc" class="toc"> -<div class="toctitle"> -<h2>Contents</h2> -</div> +!! html/php+tidy +<h2><span class="mw-headline" id="onmouseover.3D">onmouseover=</span><span class="mw-editsection"><span class="mw-editsection-bracket">[</span><a href="/index.php?title=Parser_test&action=edit&section=1" title="Edit section: onmouseover=">edit</a><span class="mw-editsection-bracket">]</span></span></h2><p> +http://</p><div id="toc" class="toc"><div class="toctitle" lang="en" dir="ltr"><h2>Contents</h2></div> <ul> <li class="toclevel-1 tocsection-1"><a href="#onmouseover.3D"><span class="tocnumber">1</span> <span class="toctext">onmouseover=</span></a></li> </ul> </div> -<p></p> +!! html/parsoid +<h2 id="onmouseover="><span id="onmouseover.3D" typeof="mw:FallbackId"></span>onmouseover=</h2> +<p><a rel="mw:ExtLink" class="external free" href="http://__TOC__" data-parsoid='{"stx":"url"}'>http://__TOC__</a></p> !! end !! test @@ -18718,48 +18951,39 @@ parsoid=wt2html,html2html </tr> </table> !! html/parsoid -<h2>a</h2> +<h2 id="a">a</h2> <table style="__TOC__"></table> !! end # Known to produce bogus xml (extra </td>) +# Don't add the html/php section since it generates broken HTML !! test Fuzz testing: Parser16 !! wikitext {| !https://|||||| -!! html +!! html+tidy <table> -<tr> +<tbody><tr> <th>https://</th> <th></th> <th></th> <th> -</td> -</tr> -</table> -!! html+tidy -<table> -<tr> -<th>https://</th> -<th></th> -<th></th> -<th></th> -</tr> -</table> +</th></tr> +</tbody></table> !! end !! test Fuzz testing: Parser21 !! wikitext {| -! irc://{{ftp://a" onmouseover="alert('hello world');" +!irc://{{ftp://a" onmouseover="alert('hello world');" | !! html <table> <tr> -<th> <a rel="nofollow" class="external free" href="irc://{{ftp://a">irc://{{ftp://a</a>" onmouseover="alert('hello world');" +<th><a rel="nofollow" class="external free" href="irc://{{ftp://a">irc://{{ftp://a</a>" onmouseover="alert('hello world');" </th> <td> </td> @@ -18852,7 +19076,7 @@ http://example.com <nowiki>junk</nowiki> <p><a rel="nofollow" class="external free" href="http://example.com">http://example.com</a> junk </p> !! html/parsoid -<p><a rel="mw:ExtLink" href="http://example.com" data-parsoid='{"stx":"url"}'>http://example.com</a> <span typeof="mw:Nowiki">junk</span></p> +<p><a rel="mw:ExtLink" class="external free" href="http://example.com" data-parsoid='{"stx":"url"}'>http://example.com</a> <span typeof="mw:Nowiki">junk</span></p> !! end !!test @@ -18863,7 +19087,7 @@ http://example.com<nowiki>junk</nowiki> <p><a rel="nofollow" class="external free" href="http://example.com">http://example.com</a>junk </p> !! html/parsoid -<p><a rel="mw:ExtLink" href="http://example.com" data-parsoid='{"stx":"url"}'>http://example.com</a><span typeof="mw:Nowiki">junk</span></p> +<p><a rel="mw:ExtLink" class="external free" href="http://example.com" data-parsoid='{"stx":"url"}'>http://example.com</a><span typeof="mw:Nowiki">junk</span></p> !! end !! test @@ -18874,12 +19098,9 @@ http://example.com<pre>junk</pre> <a rel="nofollow" class="external free" href="http://example.com">http://example.com</a><pre>junk</pre> !! html/php+tidy -<p><a rel="nofollow" class="external free" href="http://example.com">http://example.com</a></p> -<pre> -junk -</pre> +<p><a rel="nofollow" class="external free" href="http://example.com">http://example.com</a></p><pre>junk</pre> !! html/parsoid -<p><a rel="mw:ExtLink" href="http://example.com" data-parsoid='{"stx":"url"}'>http://example.com</a></p><pre typeof="mw:Extension/pre" about="#mwt2" data-mw='{"name":"pre","attrs":{},"body":{"extsrc":"junk"}}'>junk</pre> +<p><a rel="mw:ExtLink" class="external free" href="http://example.com" data-parsoid='{"stx":"url"}'>http://example.com</a></p><pre typeof="mw:Extension/pre" about="#mwt2" data-mw='{"name":"pre","attrs":{},"body":{"extsrc":"junk"}}'>junk</pre> !! end !! test @@ -18893,15 +19114,45 @@ Fuzz testing: image with bogus manual thumbnail <figure class="mw-default-size" typeof="mw:Error mw:Image/Thumb" data-parsoid='{"optList":[{"ck":"manualthumb","ak":"thumbnail= "}]}' data-mw='{"errors":[{"key":"apierror-invalidtitle","message":"Invalid thumbnail title.","params":{"name":""}}],"thumb":""}'><a href="./File:Foobar.jpg" data-parsoid='{"a":{"href":"./File:Foobar.jpg"},"sa":{"href":"Image:foobar.jpg"}}'><img resource="./File:Foobar.jpg" src="./Special:FilePath/Foobar.jpg" height="220" width="220" data-parsoid='{"a":{"resource":"./File:Foobar.jpg","height":"220","width":"220"},"sa":{"resource":"Image:foobar.jpg"}}'/></a></figure> !! end +# Parsoid will emit the newline literally in wt2wt; see next test case. !! test Fuzz testing: encoded newline in generated HTML replacements (T8577) +!! options +parsoid=wt2html !! wikitext <pre dir=" "></pre> !! html/php <pre dir=" "></pre> !! html/parsoid -<pre typeof="mw:Extension/pre" about="#mwt2" dir="&#10;" data-mw='{"name":"pre","attrs":{"dir":"&#10;"},"body":{"extsrc":""}}'></pre> +<pre typeof="mw:Extension/pre" about="#mwt2" dir=" +" data-mw='{"name":"pre","attrs":{"dir":"\n"},"body":{"extsrc":""}}'></pre> +!! end + +!! test +Fuzz testing: encoded newline in generated HTML replacements, html2wt (T8577) +!! options +parsoid=html2wt +!! html/parsoid +<pre typeof="mw:Extension/pre" about="#mwt2" dir=" +" data-mw='{"name":"pre","attrs":{"dir":"\n"},"body":{"extsrc":""}}'></pre> +!! wikitext +<pre dir=" +"></pre> +!! html/php +<pre dir=""></pre> + +!! end + +!! test +Templates in extension attributes are not expanded +!! wikitext +<pre dir="{{echo|ltr}}"></pre> +!! html/php +<pre dir="{{echo|ltr}}"></pre> + +!! html/parsoid +<pre typeof="mw:Extension/pre" about="#mwt2" dir="{{echo|ltr}}" data-mw='{"name":"pre","attrs":{"dir":"{{echo|ltr}}"},"body":{"extsrc":""}}'></pre> !! end !! test @@ -19903,23 +20154,23 @@ xxx !! test Handling of 
 in URLs !! wikitext -** irc://
a +*irc://
a !! html/php -<ul><li><ul><li> <a rel="nofollow" class="external free" href="irc://%0Aa">irc://%0Aa</a></li></ul></li></ul> +<ul><li><a rel="nofollow" class="external free" href="irc://%0Aa">irc://%0Aa</a></li></ul> !! html/parsoid -<ul><li><ul><li> <a rel="mw:ExtLink" href="irc://%0Aa" data-parsoid='{"stx":"url","a":{"href":"irc://%0Aa"},"sa":{"href":"irc://&#x0A;a"}}'>irc://%0Aa</a></li></ul></li></ul> +<ul><li><a rel="mw:ExtLink" class="external free" href="irc://%0Aa" data-parsoid='{"stx":"url","a":{"href":"irc://%0Aa"},"sa":{"href":"irc://&#x0A;a"}}'>irc://%0Aa</a></li></ul> !! end !! test Handling of %0A in URLs !! wikitext -** irc://%0Aa +*irc://%0Aa !! html/php -<ul><li><ul><li> <a rel="nofollow" class="external free" href="irc://%0Aa">irc://%0Aa</a></li></ul></li></ul> +<ul><li><a rel="nofollow" class="external free" href="irc://%0Aa">irc://%0Aa</a></li></ul> !! html/parsoid -<ul><li><ul><li> <a rel="mw:ExtLink" href="irc://%0Aa">irc://%0Aa</a></li></ul></li></ul> +<ul><li><a rel="mw:ExtLink" class="external free" href="irc://%0Aa">irc://%0Aa</a></li></ul> !! end # The PHP parser strips the empty tags out for giggles; parsoid doesn't. @@ -19931,7 +20182,7 @@ parsoid=wt2html ''''' !! html/php !! html/parsoid -<p><b><i></i></b></p> +<b><i></i></b> !! end # same html as previous, but wikitext adjusted to match parsoid html2wt @@ -19989,51 +20240,51 @@ Say the magic word !! options title=[[Parser test]] !! wikitext -* {{PAGENAME}} -* {{PAGENAMEE}} -* {{FULLPAGENAME}} -* {{FULLPAGENAMEE}} -* {{BASEPAGENAME}} -* {{BASEPAGENAMEE}} -* {{SUBPAGENAME}} -* {{SUBPAGENAMEE}} -* {{ROOTPAGENAME}} -* {{ROOTPAGENAMEE}} -* {{TALKPAGENAME}} -* {{TALKPAGENAMEE}} -* {{SUBJECTPAGENAME}} -* {{SUBJECTPAGENAMEE}} -* {{NAMESPACEE}} -* {{NAMESPACE}} -* {{NAMESPACENUMBER}} -* {{TALKSPACE}} -* {{TALKSPACEE}} -* {{SUBJECTSPACE}} -* {{SUBJECTSPACEE}} -* {{Dynamic|{{NUMBEROFUSERS}}|{{NUMBEROFPAGES}}|{{CURRENTVERSION}}|{{CONTENTLANGUAGE}}|{{DIRECTIONMARK}}|{{CURRENTTIMESTAMP}}|{{NUMBEROFARTICLES}}}} -!! html -<ul><li> Parser test</li> -<li> Parser_test</li> -<li> Parser test</li> -<li> Parser_test</li> -<li> Parser test</li> -<li> Parser_test</li> -<li> Parser test</li> -<li> Parser_test</li> -<li> Parser test</li> -<li> Parser_test</li> -<li> Talk:Parser test</li> -<li> Talk:Parser_test</li> -<li> Parser test</li> -<li> Parser_test</li> -<li> </li> -<li> </li> -<li> 0</li> -<li> Talk</li> -<li> Talk</li> -<li> </li> -<li> </li> -<li> <a href="/index.php?title=Template:Dynamic&action=edit&redlink=1" class="new" title="Template:Dynamic (page does not exist)">Template:Dynamic</a></li></ul> +*{{PAGENAME}} +*{{PAGENAMEE}} +*{{FULLPAGENAME}} +*{{FULLPAGENAMEE}} +*{{BASEPAGENAME}} +*{{BASEPAGENAMEE}} +*{{SUBPAGENAME}} +*{{SUBPAGENAMEE}} +*{{ROOTPAGENAME}} +*{{ROOTPAGENAMEE}} +*{{TALKPAGENAME}} +*{{TALKPAGENAMEE}} +*{{SUBJECTPAGENAME}} +*{{SUBJECTPAGENAMEE}} +*{{NAMESPACEE}} +*{{NAMESPACE}} +*{{NAMESPACENUMBER}} +*{{TALKSPACE}} +*{{TALKSPACEE}} +*{{SUBJECTSPACE}} +*{{SUBJECTSPACEE}} +*{{Dynamic|{{NUMBEROFUSERS}}|{{NUMBEROFPAGES}}|{{CURRENTVERSION}}|{{CONTENTLANGUAGE}}|{{DIRECTIONMARK}}|{{CURRENTTIMESTAMP}}|{{NUMBEROFARTICLES}}}} +!! html +<ul><li>Parser test</li> +<li>Parser_test</li> +<li>Parser test</li> +<li>Parser_test</li> +<li>Parser test</li> +<li>Parser_test</li> +<li>Parser test</li> +<li>Parser_test</li> +<li>Parser test</li> +<li>Parser_test</li> +<li>Talk:Parser test</li> +<li>Talk:Parser_test</li> +<li>Parser test</li> +<li>Parser_test</li> +<li></li> +<li></li> +<li>0</li> +<li>Talk</li> +<li>Talk</li> +<li></li> +<li></li> +<li><a href="/index.php?title=Template:Dynamic&action=edit&redlink=1" class="new" title="Template:Dynamic (page does not exist)">Template:Dynamic</a></li></ul> !! end ### Note: Above tests excludes the "{{NUMBEROFADMINS}}" magic word because it generates a MySQL error when included. @@ -20055,7 +20306,7 @@ File:File:Foobar.jpg !! html/parsoid <ul class="gallery mw-gallery-traditional" type="123" typeof="mw:Extension/gallery" about="#mwt2" data-mw='{"name":"gallery","attrs":{"type":"123","summary":"345"},"body":{"extsrc":"\nFile:File:Foobar.jpg\n"}}'> -<li class="gallerybox" style="width: 155px;"><div class="thumb" style="width: 150px; height: 150px;"><span typeof="mw:Error mw:Image"><a href="./File:File:Foobar.jpg"><img resource="./File:File:Foobar.jpg" src="./Special:FilePath/File:Foobar.jpg" height="120" width="120"/></a></span></div><div class="gallerytext"></div></li> +<li class="gallerybox" style="width: 155px;"><div class="thumb" style="width: 150px; height: 150px;"><figure-inline typeof="mw:Error mw:Image"><a href="./File:File:Foobar.jpg"><img resource="./File:File:Foobar.jpg" src="./Special:FilePath/File:Foobar.jpg" height="120" width="120"/></a></figure-inline></div><div class="gallerytext"></div></li> </ul> !! end @@ -20118,12 +20369,12 @@ image4 |300px| centre !! html/parsoid <ul class="gallery mw-gallery-traditional" typeof="mw:Extension/gallery" about="#mwt3" data-mw='{"name":"gallery","attrs":{},"body":{}}'> -<li class="gallerybox" style="width: 155px;"><div class="thumb" style="width: 150px; height: 150px;"><span typeof="mw:Error mw:Image"><a href="./File:Image1.png"><img resource="./File:Image1.png" src="./Special:FilePath/Image1.png" height="120" width="120"/></a></span></div><div class="gallerytext"></div></li> -<li class="gallerybox" style="width: 155px;"><div class="thumb" style="width: 150px; height: 150px;"><span typeof="mw:Error mw:Image"><a href="./File:Image2.gif"><img resource="./File:Image2.gif" src="./Special:FilePath/Image2.gif" height="120" width="120"/></a></span></div><div class="gallerytext"></div></li> -<li class="gallerybox" style="width: 155px;"><div class="thumb" style="width: 150px; height: 150px;"><span typeof="mw:Error mw:Image"><a href="./File:Image3"><img resource="./File:Image3" src="./Special:FilePath/Image3" height="120" width="120"/></a></span></div><div class="gallerytext"></div></li> -<li class="gallerybox" style="width: 155px;"><div class="thumb" style="width: 150px; height: 150px;"><span typeof="mw:Error mw:Image"><a href="./File:Image4"><img resource="./File:Image4" src="./Special:FilePath/Image4" height="300" width="300"/></a></span></div><div class="gallerytext"></div></li> -<li class="gallerybox" style="width: 155px;"><div class="thumb" style="width: 150px; height: 150px;"><span typeof="mw:Error mw:Image"><a href="./File:Image5.svg"><img resource="./File:Image5.svg" src="./Special:FilePath/Image5.svg" height="120" width="120"/></a></span></div><div class="gallerytext"> <a rel="mw:ExtLink" href="http://///////">http://///////</a></div></li> -<li class="gallerybox" style="width: 155px;"><div class="thumb" style="width: 150px; height: 150px;"><span typeof="mw:Error mw:Image"><a href="./File:*_image6"><img resource="./File:*_image6" src="./Special:FilePath/*_image6" height="120" width="120"/></a></span></div><div class="gallerytext"></div></li> +<li class="gallerybox" style="width: 155px;"><div class="thumb" style="width: 150px; height: 150px;"><figure-inline typeof="mw:Error mw:Image"><a href="./File:Image1.png"><img resource="./File:Image1.png" src="./Special:FilePath/Image1.png" height="120" width="120"/></a></figure-inline></div><div class="gallerytext"></div></li> +<li class="gallerybox" style="width: 155px;"><div class="thumb" style="width: 150px; height: 150px;"><figure-inline typeof="mw:Error mw:Image"><a href="./File:Image2.gif"><img resource="./File:Image2.gif" src="./Special:FilePath/Image2.gif" height="120" width="120"/></a></figure-inline></div><div class="gallerytext"></div></li> +<li class="gallerybox" style="width: 155px;"><div class="thumb" style="width: 150px; height: 150px;"><figure-inline typeof="mw:Error mw:Image"><a href="./File:Image3"><img resource="./File:Image3" src="./Special:FilePath/Image3" height="120" width="120"/></a></figure-inline></div><div class="gallerytext"></div></li> +<li class="gallerybox" style="width: 155px;"><div class="thumb" style="width: 150px; height: 150px;"><figure-inline typeof="mw:Error mw:Image"><a href="./File:Image4"><img resource="./File:Image4" src="./Special:FilePath/Image4" height="300" width="300"/></a></figure-inline></div><div class="gallerytext"></div></li> +<li class="gallerybox" style="width: 155px;"><div class="thumb" style="width: 150px; height: 150px;"><figure-inline typeof="mw:Error mw:Image"><a href="./File:Image5.svg"><img resource="./File:Image5.svg" src="./Special:FilePath/Image5.svg" height="120" width="120"/></a></figure-inline></div><div class="gallerytext"> <a rel="mw:ExtLink" class="external free" href="http://///////">http://///////</a></div></li> +<li class="gallerybox" style="width: 155px;"><div class="thumb" style="width: 150px; height: 150px;"><figure-inline typeof="mw:Error mw:Image"><a href="./File:*_image6"><img resource="./File:*_image6" src="./Special:FilePath/*_image6" height="120" width="120"/></a></figure-inline></div><div class="gallerytext"></div></li> </ul> !! end @@ -20181,11 +20432,11 @@ image:foobar.jpg|Blabla|alt=This is a foo-bar.|blabla. !! html/parsoid <ul class="gallery mw-gallery-traditional" style="max-width: 226px; _width: 226px;" typeof="mw:Extension/gallery" about="#mwt3" data-mw='{"name":"gallery","attrs":{"widths":"70px","heights":"40px","perrow":"2"},"body":{}}'> <li class="gallerycaption">Foo <a rel="mw:WikiLink" href="./Main_Page" title="Main Page">Main Page</a></li> -<li class="gallerybox" style="width: 105px;"><div class="thumb" style="width: 100px; height: 70px;"><span typeof="mw:Error mw:Image"><a href="./File:Nonexistent.jpg"><img resource="./File:Nonexistent.jpg" src="./Special:FilePath/Nonexistent.jpg" height="40" width="70"/></a></span></div><div class="gallerytext">caption</div></li> -<li class="gallerybox" style="width: 105px;"><div class="thumb" style="width: 100px; height: 70px;"><span typeof="mw:Error mw:Image"><a href="./File:Nonexistent.jpg"><img resource="./File:Nonexistent.jpg" src="./Special:FilePath/Nonexistent.jpg" height="40" width="70"/></a></span></div><div class="gallerytext"></div></li> -<li class="gallerybox" style="width: 105px;"><div class="thumb" style="width: 100px; height: 70px;"><span typeof="mw:Image"><a href="./File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/thumb/3/3a/Foobar.jpg/70px-Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="8" width="70"/></a></span></div><div class="gallerytext">some <b>caption</b> <a rel="mw:WikiLink" href="./Main_Page" title="Main Page">Main Page</a></div></li> -<li class="gallerybox" style="width: 105px;"><div class="thumb" style="width: 100px; height: 70px;"><span typeof="mw:Image"><a href="./File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/thumb/3/3a/Foobar.jpg/70px-Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="8" width="70"/></a></span></div><div class="gallerytext"></div></li> -<li class="gallerybox" style="width: 105px;"><div class="thumb" style="width: 100px; height: 70px;"><span typeof="mw:Image"><a href="./File:Foobar.jpg"><img alt="This is a foo-bar." resource="./File:Foobar.jpg" src="//example.com/images/thumb/3/3a/Foobar.jpg/70px-Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="8" width="70"/></a></span></div><div class="gallerytext">blabla.</div></li> +<li class="gallerybox" style="width: 105px;"><div class="thumb" style="width: 100px; height: 70px;"><figure-inline typeof="mw:Error mw:Image"><a href="./File:Nonexistent.jpg"><img resource="./File:Nonexistent.jpg" src="./Special:FilePath/Nonexistent.jpg" height="40" width="70"/></a></figure-inline></div><div class="gallerytext">caption</div></li> +<li class="gallerybox" style="width: 105px;"><div class="thumb" style="width: 100px; height: 70px;"><figure-inline typeof="mw:Error mw:Image"><a href="./File:Nonexistent.jpg"><img resource="./File:Nonexistent.jpg" src="./Special:FilePath/Nonexistent.jpg" height="40" width="70"/></a></figure-inline></div><div class="gallerytext"></div></li> +<li class="gallerybox" style="width: 105px;"><div class="thumb" style="width: 100px; height: 70px;"><figure-inline typeof="mw:Image"><a href="./File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/thumb/3/3a/Foobar.jpg/70px-Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="8" width="70"/></a></figure-inline></div><div class="gallerytext">some <b>caption</b> <a rel="mw:WikiLink" href="./Main_Page" title="Main Page">Main Page</a></div></li> +<li class="gallerybox" style="width: 105px;"><div class="thumb" style="width: 100px; height: 70px;"><figure-inline typeof="mw:Image"><a href="./File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/thumb/3/3a/Foobar.jpg/70px-Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="8" width="70"/></a></figure-inline></div><div class="gallerytext"></div></li> +<li class="gallerybox" style="width: 105px;"><div class="thumb" style="width: 100px; height: 70px;"><figure-inline typeof="mw:Image"><a href="./File:Foobar.jpg"><img alt="This is a foo-bar." resource="./File:Foobar.jpg" src="//example.com/images/thumb/3/3a/Foobar.jpg/70px-Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="8" width="70"/></a></figure-inline></div><div class="gallerytext">blabla.</div></li> </ul> !! end @@ -20242,11 +20493,53 @@ image:foobar.jpg|Blabla|alt=This is a foo-bar.|blabla. !! html/parsoid <ul class="gallery mw-gallery-traditional" style="max-width: 226px; _width: 226px;" typeof="mw:Extension/gallery" about="#mwt3" data-mw='{"name":"gallery","attrs":{"widths":"70px","heights":"40px","perrow":"2","caption":"Foo [[Main Page]]"},"body":{"extsrc":"\nFile:Nonexistent.jpg|caption\nFile:Nonexistent.jpg\nimage:foobar.jpg|some '''caption''' [[Main Page]]\nimage:foobar.jpg\nimage:foobar.jpg|Blabla|alt=This is a foo-bar.|blabla.\n"}}'> <li class="gallerycaption">Foo <a rel="mw:WikiLink" href="./Main_Page" title="Main Page">Main Page</a></li> -<li class="gallerybox" style="width: 105px;"><div class="thumb" style="width: 100px; height: 70px;"><span typeof="mw:Error mw:Image"><a href="./File:Nonexistent.jpg"><img resource="./File:Nonexistent.jpg" src="./Special:FilePath/Nonexistent.jpg" height="40" width="70"/></a></span></div><div class="gallerytext">caption</div></li> -<li class="gallerybox" style="width: 105px;"><div class="thumb" style="width: 100px; height: 70px;"><span typeof="mw:Error mw:Image"><a href="./File:Nonexistent.jpg"><img resource="./File:Nonexistent.jpg" src="./Special:FilePath/Nonexistent.jpg" height="40" width="70"/></a></span></div><div class="gallerytext"></div></li> -<li class="gallerybox" style="width: 105px;"><div class="thumb" style="width: 100px; height: 70px;"><span typeof="mw:Image"><a href="./File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/thumb/3/3a/Foobar.jpg/70px-Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="8" width="70"/></a></span></div><div class="gallerytext">some <b>caption</b> <a rel="mw:WikiLink" href="./Main_Page" title="Main Page">Main Page</a></div></li> -<li class="gallerybox" style="width: 105px;"><div class="thumb" style="width: 100px; height: 70px;"><span typeof="mw:Image"><a href="./File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/thumb/3/3a/Foobar.jpg/70px-Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="8" width="70"/></a></span></div><div class="gallerytext"></div></li> -<li class="gallerybox" style="width: 105px;"><div class="thumb" style="width: 100px; height: 70px;"><span typeof="mw:Image"><a href="./File:Foobar.jpg"><img alt="This is a foo-bar." resource="./File:Foobar.jpg" src="//example.com/images/thumb/3/3a/Foobar.jpg/70px-Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="8" width="70"/></a></span></div><div class="gallerytext">blabla.</div></li> +<li class="gallerybox" style="width: 105px;"><div class="thumb" style="width: 100px; height: 70px;"><figure-inline typeof="mw:Error mw:Image"><a href="./File:Nonexistent.jpg"><img resource="./File:Nonexistent.jpg" src="./Special:FilePath/Nonexistent.jpg" height="40" width="70"/></a></figure-inline></div><div class="gallerytext">caption</div></li> +<li class="gallerybox" style="width: 105px;"><div class="thumb" style="width: 100px; height: 70px;"><figure-inline typeof="mw:Error mw:Image"><a href="./File:Nonexistent.jpg"><img resource="./File:Nonexistent.jpg" src="./Special:FilePath/Nonexistent.jpg" height="40" width="70"/></a></figure-inline></div><div class="gallerytext"></div></li> +<li class="gallerybox" style="width: 105px;"><div class="thumb" style="width: 100px; height: 70px;"><figure-inline typeof="mw:Image"><a href="./File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/thumb/3/3a/Foobar.jpg/70px-Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="8" width="70"/></a></figure-inline></div><div class="gallerytext">some <b>caption</b> <a rel="mw:WikiLink" href="./Main_Page" title="Main Page">Main Page</a></div></li> +<li class="gallerybox" style="width: 105px;"><div class="thumb" style="width: 100px; height: 70px;"><figure-inline typeof="mw:Image"><a href="./File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/thumb/3/3a/Foobar.jpg/70px-Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="8" width="70"/></a></figure-inline></div><div class="gallerytext"></div></li> +<li class="gallerybox" style="width: 105px;"><div class="thumb" style="width: 100px; height: 70px;"><figure-inline typeof="mw:Image"><a href="./File:Foobar.jpg"><img alt="This is a foo-bar." resource="./File:Foobar.jpg" src="//example.com/images/thumb/3/3a/Foobar.jpg/70px-Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="8" width="70"/></a></figure-inline></div><div class="gallerytext">blabla.</div></li> +</ul> +!! end + +!! test +Gallery (without px units) +!! wikitext +<gallery widths="70" heights="40"> +File:Foobar.jpg +</gallery> +!! html/php +<ul class="gallery mw-gallery-traditional"> + <li class="gallerybox" style="width: 105px"><div style="width: 105px"> + <div class="thumb" style="width: 100px;"><div style="margin:31px auto;"><a href="/wiki/File:Foobar.jpg" class="image"><img alt="Foobar.jpg" src="http://example.com/images/thumb/3/3a/Foobar.jpg/70px-Foobar.jpg" width="70" height="8" srcset="http://example.com/images/thumb/3/3a/Foobar.jpg/105px-Foobar.jpg 1.5x, http://example.com/images/thumb/3/3a/Foobar.jpg/140px-Foobar.jpg 2x" /></a></div></div> + <div class="gallerytext"> + </div> + </div></li> +</ul> + +!! html/parsoid +<ul class="gallery mw-gallery-traditional" typeof="mw:Extension/gallery" about="#mwt2" data-mw='{"name":"gallery","attrs":{"widths":"70","heights":"40"},"body":{"extsrc":"\nFile:Foobar.jpg\n"}}'> +<li class="gallerybox" style="width: 105px;"><div class="thumb" style="width: 100px; height: 70px;"><figure-inline typeof="mw:Image"><a href="./File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/thumb/3/3a/Foobar.jpg/70px-Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="8" width="70"/></a></figure-inline></div><div class="gallerytext"></div></li> +</ul> +!! end + +!! test +Gallery (with invalid units) +!! wikitext +<gallery widths="70em" heights="40em"> +File:Foobar.jpg +</gallery> +!! html/php +<ul class="gallery mw-gallery-traditional"> + <li class="gallerybox" style="width: 155px"><div style="width: 155px"> + <div class="thumb" style="width: 150px;"><div style="margin:68px auto;"><a href="/wiki/File:Foobar.jpg" class="image"><img alt="Foobar.jpg" src="http://example.com/images/thumb/3/3a/Foobar.jpg/120px-Foobar.jpg" width="120" height="14" srcset="http://example.com/images/thumb/3/3a/Foobar.jpg/180px-Foobar.jpg 1.5x, http://example.com/images/thumb/3/3a/Foobar.jpg/240px-Foobar.jpg 2x" /></a></div></div> + <div class="gallerytext"> + </div> + </div></li> +</ul> + +!! html/parsoid +<ul class="gallery mw-gallery-traditional" typeof="mw:Extension/gallery" about="#mwt2" data-mw='{"name":"gallery","attrs":{"widths":"70em","heights":"40em"},"body":{"extsrc":"\nFile:Foobar.jpg\n"}}'> +<li class="gallerybox" style="width: 155px;"><div class="thumb" style="width: 150px; height: 150px;"><figure-inline typeof="mw:Image"><a href="./File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/thumb/3/3a/Foobar.jpg/120px-Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="14" width="120"/></a></figure-inline></div><div class="gallerytext"></div></li> </ul> !! end @@ -20286,9 +20579,9 @@ image:foobar.jpg|link=Main Page#section|caption !! html/parsoid <ul class="gallery mw-gallery-traditional" typeof="mw:Extension/gallery" about="#mwt2" data-mw='{"name":"gallery","attrs":{},"body":{}}'> -<li class="gallerybox" style="width: 155px;"><div class="thumb" style="width: 150px; height: 150px;"><span typeof="mw:Image"><a href="./Main_Page"><img resource="./File:Foobar.jpg" src="//example.com/images/thumb/3/3a/Foobar.jpg/120px-Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="14" width="120"/></a></span></div><div class="gallerytext"></div></li> -<li class="gallerybox" style="width: 155px;"><div class="thumb" style="width: 150px; height: 150px;"><span typeof="mw:Image"><a href="./Main_Page#section"><img resource="./File:Foobar.jpg" src="//example.com/images/thumb/3/3a/Foobar.jpg/120px-Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="14" width="120"/></a></span></div><div class="gallerytext"></div></li> -<li class="gallerybox" style="width: 155px;"><div class="thumb" style="width: 150px; height: 150px;"><span typeof="mw:Image"><a href="./Main_Page#section"><img resource="./File:Foobar.jpg" src="//example.com/images/thumb/3/3a/Foobar.jpg/120px-Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="14" width="120"/></a></span></div><div class="gallerytext">caption</div></li> +<li class="gallerybox" style="width: 155px;"><div class="thumb" style="width: 150px; height: 150px;"><figure-inline typeof="mw:Image"><a href="./Main_Page"><img resource="./File:Foobar.jpg" src="//example.com/images/thumb/3/3a/Foobar.jpg/120px-Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="14" width="120"/></a></figure-inline></div><div class="gallerytext"></div></li> +<li class="gallerybox" style="width: 155px;"><div class="thumb" style="width: 150px; height: 150px;"><figure-inline typeof="mw:Image"><a href="./Main_Page#section"><img resource="./File:Foobar.jpg" src="//example.com/images/thumb/3/3a/Foobar.jpg/120px-Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="14" width="120"/></a></figure-inline></div><div class="gallerytext"></div></li> +<li class="gallerybox" style="width: 155px;"><div class="thumb" style="width: 150px; height: 150px;"><figure-inline typeof="mw:Image"><a href="./Main_Page#section"><img resource="./File:Foobar.jpg" src="//example.com/images/thumb/3/3a/Foobar.jpg/120px-Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="14" width="120"/></a></figure-inline></div><div class="gallerytext">caption</div></li> </ul> !! end @@ -20318,7 +20611,7 @@ File:Foobar.jpg|{{echo|ho}} !! html/parsoid <ul class="gallery mw-gallery-traditional" typeof="mw:Extension/gallery" about="#mwt6" data-mw='{"name":"gallery","attrs":{},"body":{}}'> <li class="gallerycaption"><span about="#mwt3" typeof="mw:Transclusion" data-mw='{"parts":[{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"hi"}},"i":0}}]}'>hi</span></li> -<li class="gallerybox" style="width: 155px;"><div class="thumb" style="width: 150px; height: 150px;"><span typeof="mw:Image"><a href="./File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/thumb/3/3a/Foobar.jpg/120px-Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="14" width="120"/></a></span></div><div class="gallerytext"><span about="#mwt5" typeof="mw:Transclusion" data-mw='{"parts":[{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"ho"}},"i":0}}]}'>ho</span></div></li> +<li class="gallerybox" style="width: 155px;"><div class="thumb" style="width: 150px; height: 150px;"><figure-inline typeof="mw:Image"><a href="./File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/thumb/3/3a/Foobar.jpg/120px-Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="14" width="120"/></a></figure-inline></div><div class="gallerytext"><span about="#mwt5" typeof="mw:Transclusion" data-mw='{"parts":[{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"ho"}},"i":0}}]}'>ho</span></div></li> </ul> !! end @@ -20353,8 +20646,8 @@ File:Foobar.jpg|alt=galleryalt|{{Test|unamedParam|alt=param}} !! html/parsoid <ul class="gallery mw-gallery-traditional" typeof="mw:Extension/gallery" about="#mwt6" data-mw='{"name":"gallery","attrs":{},"body":{}}'> -<li class="gallerybox" style="width: 155px;"><div class="thumb" style="width: 150px; height: 150px;"><span typeof="mw:Image"><a href="./File:Foobar.jpg"><img alt="galleryalt" resource="./File:Foobar.jpg" src="//example.com/images/thumb/3/3a/Foobar.jpg/120px-Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="14" width="120"/></a></span></div><div class="gallerytext"><span typeof="mw:Image" data-mw='{"caption":"desc"}'><a href="./File:Foobar.jpg"><img alt="inneralt" resource="./File:Foobar.jpg" src="//example.com/images/thumb/3/3a/Foobar.jpg/20px-Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="2" width="20"/></a></span></div></li> -<li class="gallerybox" style="width: 155px;"><div class="thumb" style="width: 150px; height: 150px;"><span typeof="mw:Image"><a href="./File:Foobar.jpg"><img alt="galleryalt" resource="./File:Foobar.jpg" src="//example.com/images/thumb/3/3a/Foobar.jpg/120px-Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="14" width="120"/></a></span></div><div class="gallerytext"><span about="#mwt4" typeof="mw:Transclusion" data-mw='{"parts":[{"template":{"target":{"wt":"Test","href":"./Template:Test"},"params":{"1":{"wt":"unamedParam"},"alt":{"wt":"param"}},"i":0}}]}'>This is a test template</span></div></li> +<li class="gallerybox" style="width: 155px;"><div class="thumb" style="width: 150px; height: 150px;"><figure-inline typeof="mw:Image"><a href="./File:Foobar.jpg"><img alt="galleryalt" resource="./File:Foobar.jpg" src="//example.com/images/thumb/3/3a/Foobar.jpg/120px-Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="14" width="120"/></a></figure-inline></div><div class="gallerytext"><figure-inline typeof="mw:Image" data-mw='{"caption":"desc"}'><a href="./File:Foobar.jpg"><img alt="inneralt" resource="./File:Foobar.jpg" src="//example.com/images/thumb/3/3a/Foobar.jpg/20px-Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="2" width="20"/></a></figure-inline></div></li> +<li class="gallerybox" style="width: 155px;"><div class="thumb" style="width: 150px; height: 150px;"><figure-inline typeof="mw:Image"><a href="./File:Foobar.jpg"><img alt="galleryalt" resource="./File:Foobar.jpg" src="//example.com/images/thumb/3/3a/Foobar.jpg/120px-Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="14" width="120"/></a></figure-inline></div><div class="gallerytext"><span about="#mwt4" typeof="mw:Transclusion" data-mw='{"parts":[{"template":{"target":{"wt":"Test","href":"./Template:Test"},"params":{"1":{"wt":"unamedParam"},"alt":{"wt":"param"}},"i":0}}]}'>This is a test template</span></div></li> </ul> !! end @@ -20407,10 +20700,10 @@ some <b>caption</b> <a href="/wiki/Main_Page" title="Main Page">Main Page</a> !! html/parsoid <ul class="gallery mw-gallery-traditional" typeof="mw:Extension/gallery" about="#mwt3" data-mw='{"name":"gallery","attrs":{"showfilename":""},"body":{}}'> -<li class="gallerybox" style="width: 155px;"><div class="thumb" style="width: 150px; height: 150px;"><span typeof="mw:Error mw:Image"><a href="./File:Nonexistent.jpg"><img resource="./File:Nonexistent.jpg" src="./Special:FilePath/Nonexistent.jpg" height="120" width="120"/></a></span></div><div class="gallerytext"><a href="./File:Nonexistent.jpg" class="galleryfilename galleryfilename-truncate" title="File:Nonexistent.jpg">File:Nonexistent.jpg</a>caption</div></li> -<li class="gallerybox" style="width: 155px;"><div class="thumb" style="width: 150px; height: 150px;"><span typeof="mw:Error mw:Image"><a href="./File:Nonexistent.jpg"><img resource="./File:Nonexistent.jpg" src="./Special:FilePath/Nonexistent.jpg" height="120" width="120"/></a></span></div><div class="gallerytext"><a href="./File:Nonexistent.jpg" class="galleryfilename galleryfilename-truncate" title="File:Nonexistent.jpg">File:Nonexistent.jpg</a></div></li> -<li class="gallerybox" style="width: 155px;"><div class="thumb" style="width: 150px; height: 150px;"><span typeof="mw:Image"><a href="./File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/thumb/3/3a/Foobar.jpg/120px-Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="14" width="120"/></a></span></div><div class="gallerytext"><a href="./File:Foobar.jpg" class="galleryfilename galleryfilename-truncate" title="File:Foobar.jpg">File:Foobar.jpg</a>some <b>caption</b> <a rel="mw:WikiLink" href="./Main_Page" title="Main Page">Main Page</a></div></li> -<li class="gallerybox" style="width: 155px;"><div class="thumb" style="width: 150px; height: 150px;"><span typeof="mw:Image"><a href="./File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/thumb/3/3a/Foobar.jpg/120px-Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="14" width="120"/></a></span></div><div class="gallerytext"><a href="./File:Foobar.jpg" class="galleryfilename galleryfilename-truncate" title="File:Foobar.jpg">File:Foobar.jpg</a></div></li> +<li class="gallerybox" style="width: 155px;"><div class="thumb" style="width: 150px; height: 150px;"><figure-inline typeof="mw:Error mw:Image"><a href="./File:Nonexistent.jpg"><img resource="./File:Nonexistent.jpg" src="./Special:FilePath/Nonexistent.jpg" height="120" width="120"/></a></figure-inline></div><div class="gallerytext"><a href="./File:Nonexistent.jpg" class="galleryfilename galleryfilename-truncate" title="File:Nonexistent.jpg">File:Nonexistent.jpg</a>caption</div></li> +<li class="gallerybox" style="width: 155px;"><div class="thumb" style="width: 150px; height: 150px;"><figure-inline typeof="mw:Error mw:Image"><a href="./File:Nonexistent.jpg"><img resource="./File:Nonexistent.jpg" src="./Special:FilePath/Nonexistent.jpg" height="120" width="120"/></a></figure-inline></div><div class="gallerytext"><a href="./File:Nonexistent.jpg" class="galleryfilename galleryfilename-truncate" title="File:Nonexistent.jpg">File:Nonexistent.jpg</a></div></li> +<li class="gallerybox" style="width: 155px;"><div class="thumb" style="width: 150px; height: 150px;"><figure-inline typeof="mw:Image"><a href="./File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/thumb/3/3a/Foobar.jpg/120px-Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="14" width="120"/></a></figure-inline></div><div class="gallerytext"><a href="./File:Foobar.jpg" class="galleryfilename galleryfilename-truncate" title="File:Foobar.jpg">File:Foobar.jpg</a>some <b>caption</b> <a rel="mw:WikiLink" href="./Main_Page" title="Main Page">Main Page</a></div></li> +<li class="gallerybox" style="width: 155px;"><div class="thumb" style="width: 150px; height: 150px;"><figure-inline typeof="mw:Image"><a href="./File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/thumb/3/3a/Foobar.jpg/120px-Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="14" width="120"/></a></figure-inline></div><div class="gallerytext"><a href="./File:Foobar.jpg" class="galleryfilename galleryfilename-truncate" title="File:Foobar.jpg">File:Foobar.jpg</a></div></li> </ul> !! end @@ -20455,35 +20748,35 @@ foobar.jpg !! html/parsoid <ul class="gallery mw-gallery-traditional" typeof="mw:Extension/gallery" about="#mwt2" data-mw='{"name":"gallery","attrs":{},"body":{}}'> -<li class="gallerybox" style="width: 155px;"><div class="thumb" style="width: 150px; height: 150px;"><span typeof="mw:Error mw:Image"><a href="./File:Nonexistent.jpg"><img resource="./File:Nonexistent.jpg" src="./Special:FilePath/Nonexistent.jpg" height="120" width="120"/></a></span></div><div class="gallerytext"></div></li> -<li class="gallerybox" style="width: 155px;"><div class="thumb" style="width: 150px; height: 150px;"><span typeof="mw:Error mw:Image"><a href="./File:Nonexistent.jpg"><img resource="./File:Nonexistent.jpg" src="./Special:FilePath/Nonexistent.jpg" height="120" width="120"/></a></span></div><div class="gallerytext"></div></li> -<li class="gallerybox" style="width: 155px;"><div class="thumb" style="width: 150px; height: 150px;"><span typeof="mw:Image"><a href="./File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/thumb/3/3a/Foobar.jpg/120px-Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="14" width="120"/></a></span></div><div class="gallerytext"></div></li> -<li class="gallerybox" style="width: 155px;"><div class="thumb" style="width: 150px; height: 150px;"><span typeof="mw:Image"><a href="./File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/thumb/3/3a/Foobar.jpg/120px-Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="14" width="120"/></a></span></div><div class="gallerytext"></div></li> +<li class="gallerybox" style="width: 155px;"><div class="thumb" style="width: 150px; height: 150px;"><figure-inline typeof="mw:Error mw:Image"><a href="./File:Nonexistent.jpg"><img resource="./File:Nonexistent.jpg" src="./Special:FilePath/Nonexistent.jpg" height="120" width="120"/></a></figure-inline></div><div class="gallerytext"></div></li> +<li class="gallerybox" style="width: 155px;"><div class="thumb" style="width: 150px; height: 150px;"><figure-inline typeof="mw:Error mw:Image"><a href="./File:Nonexistent.jpg"><img resource="./File:Nonexistent.jpg" src="./Special:FilePath/Nonexistent.jpg" height="120" width="120"/></a></figure-inline></div><div class="gallerytext"></div></li> +<li class="gallerybox" style="width: 155px;"><div class="thumb" style="width: 150px; height: 150px;"><figure-inline typeof="mw:Image"><a href="./File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/thumb/3/3a/Foobar.jpg/120px-Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="14" width="120"/></a></figure-inline></div><div class="gallerytext"></div></li> +<li class="gallerybox" style="width: 155px;"><div class="thumb" style="width: 150px; height: 150px;"><figure-inline typeof="mw:Image"><a href="./File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/thumb/3/3a/Foobar.jpg/120px-Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="14" width="120"/></a></figure-inline></div><div class="gallerytext"></div></li> </ul> !! end !! test -Gallery override link with WikiLink (T36852) +Gallery override link with wikilink (T36852) !! options parsoid={ "nativeGallery": true } !! wikitext <gallery> -File:Foobar.jpg|alt=galleryalt|link=InterWikiLink +File:Foobar.jpg|alt=galleryalt|link=Wikilink </gallery> !! html/php <ul class="gallery mw-gallery-traditional"> <li class="gallerybox" style="width: 155px"><div style="width: 155px"> - <div class="thumb" style="width: 150px;"><div style="margin:68px auto;"><a href="/wiki/InterWikiLink"><img alt="galleryalt" src="http://example.com/images/thumb/3/3a/Foobar.jpg/120px-Foobar.jpg" width="120" height="14" srcset="http://example.com/images/thumb/3/3a/Foobar.jpg/180px-Foobar.jpg 1.5x, http://example.com/images/thumb/3/3a/Foobar.jpg/240px-Foobar.jpg 2x" /></a></div></div> + <div class="thumb" style="width: 150px;"><div style="margin:68px auto;"><a href="/wiki/Wikilink"><img alt="galleryalt" src="http://example.com/images/thumb/3/3a/Foobar.jpg/120px-Foobar.jpg" width="120" height="14" srcset="http://example.com/images/thumb/3/3a/Foobar.jpg/180px-Foobar.jpg 1.5x, http://example.com/images/thumb/3/3a/Foobar.jpg/240px-Foobar.jpg 2x" /></a></div></div> <div class="gallerytext"> </div> </div></li> </ul> !! html/parsoid -<ul class="gallery mw-gallery-traditional" typeof="mw:Extension/gallery" about="#mwt2" data-parsoid='{"dsr":[0,70,2,2]}' data-mw='{"name":"gallery","attrs":{},"body":{}}'> -<li class="gallerybox" style="width: 155px;"><div class="thumb" style="width: 150px; height: 150px;"><span typeof="mw:Image"><a href="./InterWikiLink"><img alt="galleryalt" resource="./File:Foobar.jpg" src="//example.com/images/thumb/3/3a/Foobar.jpg/120px-Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="14" width="120"/></a></span></div><div class="gallerytext"></div></li> +<ul class="gallery mw-gallery-traditional" typeof="mw:Extension/gallery" about="#mwt2" data-mw='{"name":"gallery","attrs":{},"body":{}}'> +<li class="gallerybox" style="width: 155px;"><div class="thumb" style="width: 150px; height: 150px;"><figure-inline typeof="mw:Image"><a href="./Wikilink"><img alt="galleryalt" resource="./File:Foobar.jpg" src="//example.com/images/thumb/3/3a/Foobar.jpg/120px-Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="14" width="120"/></a></figure-inline></div><div class="gallerytext"></div></li> </ul> !! end @@ -20508,7 +20801,7 @@ File:Foobar.jpg|alt=galleryalt|link=http://www.example.org !! html/parsoid <ul class="gallery mw-gallery-traditional" typeof="mw:Extension/gallery" about="#mwt2" data-mw='{"name":"gallery","attrs":{},"body":{}}'> -<li class="gallerybox" style="width: 155px;"><div class="thumb" style="width: 150px; height: 150px;"><span typeof="mw:Image"><a href="http://www.example.org"><img alt="galleryalt" resource="./File:Foobar.jpg" src="//example.com/images/thumb/3/3a/Foobar.jpg/120px-Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="14" width="120"/></a></span></div><div class="gallerytext"></div></li> +<li class="gallerybox" style="width: 155px;"><div class="thumb" style="width: 150px; height: 150px;"><figure-inline typeof="mw:Image"><a href="http://www.example.org"><img alt="galleryalt" resource="./File:Foobar.jpg" src="//example.com/images/thumb/3/3a/Foobar.jpg/120px-Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="14" width="120"/></a></figure-inline></div><div class="gallerytext"></div></li> </ul> !! end @@ -20516,11 +20809,11 @@ File:Foobar.jpg|alt=galleryalt|link=http://www.example.org Gallery override link with absolute external link with LanguageConverter !! options language=zh -!! input +!! wikitext <gallery> File:foobar.jpg|caption|alt=galleryalt|link=http://www.example.org </gallery> -!! result +!! html/php <ul class="gallery mw-gallery-traditional"> <li class="gallerybox" style="width: 155px"><div style="width: 155px"> <div class="thumb" style="width: 150px;"><div style="margin:68px auto;"><a href="http://www.example.org"><img alt="galleryalt" src="http://example.com/images/thumb/3/3a/Foobar.jpg/120px-Foobar.jpg" width="120" height="14" srcset="http://example.com/images/thumb/3/3a/Foobar.jpg/180px-Foobar.jpg 1.5x, http://example.com/images/thumb/3/3a/Foobar.jpg/240px-Foobar.jpg 2x" /></a></div></div> @@ -20531,6 +20824,10 @@ File:foobar.jpg|caption|alt=galleryalt|link=http://www.example.org </div></li> </ul> +!! html/parsoid +<ul class="gallery mw-gallery-traditional" typeof="mw:Extension/gallery" about="#mwt2" data-mw='{"name":"gallery","attrs":{},"body":{"extsrc":"\nFile:foobar.jpg|caption|alt=galleryalt|link=http://www.example.org\n"}}'> +<li class="gallerybox" style="width: 155px;"><div class="thumb" style="width: 150px; height: 150px;"><figure-inline typeof="mw:Image"><a href="http://www.example.org"><img alt="galleryalt" resource="./File:Foobar.jpg" src="//example.com/images/thumb/3/3a/Foobar.jpg/120px-Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="14" width="120"/></a></figure-inline></div><div class="gallerytext">caption</div></li> +</ul> !! end !! test @@ -20555,7 +20852,7 @@ File:Foobar.jpg|alt=galleryalt|link=" onclick="alert('malicious javascript code! !! html/parsoid <ul class="gallery mw-gallery-traditional" typeof="mw:Extension/gallery" about="#mwt2" data-mw='{"name":"gallery","attrs":{},"body":{}}'> -<li class="gallerybox" style="width: 155px;"><div class="thumb" style="width: 150px; height: 150px;"><span typeof="mw:Image"><a href="./%22_onclick=%22alert('malicious_javascript_code!');"><img alt="galleryalt" resource="./File:Foobar.jpg" src="//example.com/images/thumb/3/3a/Foobar.jpg/120px-Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="14" width="120"/></a></span></div><div class="gallerytext"></div></li> +<li class="gallerybox" style="width: 155px;"><div class="thumb" style="width: 150px; height: 150px;"><figure-inline typeof="mw:Image"><a href="./%22_onclick=%22alert('malicious_javascript_code!');"><img alt="galleryalt" resource="./File:Foobar.jpg" src="//example.com/images/thumb/3/3a/Foobar.jpg/120px-Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="14" width="120"/></a></figure-inline></div><div class="gallerytext"></div></li> </ul> !! end @@ -20582,7 +20879,7 @@ File:Foobar.jpg|link=< !! html/parsoid <ul class="gallery mw-gallery-traditional" typeof="mw:Extension/gallery" about="#mwt2" data-mw='{"name":"gallery","attrs":{},"body":{}}'> -<li class="gallerybox" style="width: 155px;"><div class="thumb" style="width: 150px; height: 150px;"><span typeof="mw:Image"><a href="./File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/thumb/3/3a/Foobar.jpg/120px-Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="14" width="120"/></a></span></div><div class="gallerytext">link=<</div></li> +<li class="gallerybox" style="width: 155px;"><div class="thumb" style="width: 150px; height: 150px;"><figure-inline typeof="mw:Image"><a href="./File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/thumb/3/3a/Foobar.jpg/120px-Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="14" width="120"/></a></figure-inline></div><div class="gallerytext">link=<</div></li> </ul> !! end @@ -20625,7 +20922,7 @@ File:Foobar.jpg !! html/parsoid <ul class="gallery mw-gallery-traditional center" style="text-align: center;" typeof="mw:Extension/gallery" about="#mwt2" data-mw='{"name":"gallery","attrs":{"class":"center","style":"text-align: center;"},"body":{}}'> -<li class="gallerybox" style="width: 155px;"><div class="thumb" style="width: 150px; height: 150px;"><span typeof="mw:Image"><a href="./File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/thumb/3/3a/Foobar.jpg/120px-Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="14" width="120"/></a></span></div><div class="gallerytext"></div></li> +<li class="gallerybox" style="width: 155px;"><div class="thumb" style="width: 150px; height: 150px;"><figure-inline typeof="mw:Image"><a href="./File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/thumb/3/3a/Foobar.jpg/120px-Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="14" width="120"/></a></figure-inline></div><div class="gallerytext"></div></li> </ul> !! end @@ -20650,7 +20947,7 @@ File:Foobar.jpg !! html/parsoid <ul class="gallery mw-gallery-slideshow" data-showthumbnails="1" typeof="mw:Extension/gallery" about="#mwt2" data-mw='{"name":"gallery","attrs":{"mode":"slideshow","showthumbnails":""},"body":{}}'> -<li class="gallerybox" style="width: 155px;"><div class="thumb" style="width: 150px;"><span typeof="mw:Image"><a href="./File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/thumb/3/3a/Foobar.jpg/120px-Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="14" width="120"/></a></span></div><div class="gallerytext"></div></li> +<li class="gallerybox" style="width: 155px;"><div class="thumb" style="width: 150px;"><figure-inline typeof="mw:Image"><a href="./File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/thumb/3/3a/Foobar.jpg/120px-Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="14" width="120"/></a></figure-inline></div><div class="gallerytext"></div></li> </ul> !! end @@ -20663,8 +20960,6 @@ parsoid=wt2html,wt2wt,html2html !! html/php <p>JavaScript </p> -!! html/php+tidy -<p>JavaScript</p> !! html/parsoid <p><span typeof="mw:Entity">J</span><span typeof="mw:Entity">a</span><span typeof="mw:Entity">v</span><span typeof="mw:Entity">a</span><span typeof="mw:Entity">S</span><span typeof="mw:Entity">c</span><span typeof="mw:Entity">r</span><span typeof="mw:Entity">i</span><span typeof="mw:Entity">p</span><span typeof="mw:Entity">t</span></p> !! end @@ -20673,11 +20968,9 @@ parsoid=wt2html,wt2wt,html2html HTML Hex character encoding bogus encoding (T28437 regression check) !! wikitext &#xsee;&#XSEE; -!! html/php +!! html <p>&#xsee;&#XSEE; </p> -!! html/parsoid -<p>&#xsee;&#XSEE;</p> !! end !! test @@ -20689,8 +20982,6 @@ parsoid=wt2html,wt2wt,html2html !! html/php <p>îî </p> -!! html/php+tidy -<p>îî</p> !! html/parsoid <p><span typeof="mw:Entity">î</span><span typeof="mw:Entity">î</span></p> !! end @@ -20709,8 +21000,7 @@ Illegal character references (T106578) ; Surrogate: �� ; This is an okay astral character: 💩 !! html+tidy -<dl> -<dt>Null</dt> +<dl><dt>Null</dt> <dd>&#00;</dd> <dt>FF</dt> <dd>&#xC;</dd> @@ -20723,8 +21013,7 @@ Illegal character references (T106578) <dt>Surrogate</dt> <dd>&#xD83D;&#xDCA9;</dd> <dt>This is an okay astral character</dt> -<dd>💩</dd> -</dl> +<dd>💩</dd></dl> !! end !! test @@ -20741,11 +21030,9 @@ __FORCETOC__ ISBN code coverage !! wikitext ISBN 978-0-1234-56 789 -!! html +!! html/php <p><a href="/wiki/Special:BookSources/9780123456" class="internal mw-magiclink-isbn">ISBN 978-0-1234-56</a> 789 </p> -!! html+tidy -<p><a href="/wiki/Special:BookSources/9780123456" class="internal mw-magiclink-isbn">ISBN 978-0-1234-56</a> 789</p> !! html/parsoid <p><a href="./Special:BookSources/9780123456" rel="mw:WikiLink" data-parsoid='{"stx":"magiclink"}'>ISBN 978-0-1234-56</a><span typeof="mw:Entity" data-parsoid='{"src":"&#x20;","srcContent":" "}'> </span>789</p> !! end @@ -20816,7 +21103,7 @@ T24905: <abbr> followed by ISBN followed by </a> <p><abbr>(fr)</abbr> <a href="/wiki/Special:BookSources/2753300917" class="internal mw-magiclink-isbn">ISBN 2753300917</a> <a rel="nofollow" class="external text" href="http://www.example.com">example.com</a> </p> !! html/parsoid -<p><abbr data-parsoid='{"stx":"html"}'>(fr)</abbr> <a href="./Special:BookSources/2753300917" rel="mw:WikiLink" data-parsoid='{"stx":"magiclink"}'>ISBN 2753300917</a> <a rel="mw:ExtLink" href="http://www.example.com">example.com</a></p> +<p><abbr data-parsoid='{"stx":"html"}'>(fr)</abbr> <a href="./Special:BookSources/2753300917" rel="mw:WikiLink" data-parsoid='{"stx":"magiclink"}'>ISBN 2753300917</a> <a rel="mw:ExtLink" class="external text" href="http://www.example.com">example.com</a></p> !! end !! test @@ -20824,7 +21111,7 @@ Double RFC !! wikitext RFC RFC 1234 !! html -<p>RFC <a class="external mw-magiclink-rfc" rel="nofollow" href="//tools.ietf.org/html/rfc1234">RFC 1234</a> +<p>RFC <a class="external mw-magiclink-rfc" rel="nofollow" href="https://tools.ietf.org/html/rfc1234">RFC 1234</a> </p> !! end @@ -20841,66 +21128,78 @@ RFC [[RFC 1234]] RFC code coverage !! wikitext RFC 983 987 -!! html -<p><a class="external mw-magiclink-rfc" rel="nofollow" href="//tools.ietf.org/html/rfc983">RFC 983</a> 987 +!! html/php +<p><a class="external mw-magiclink-rfc" rel="nofollow" href="https://tools.ietf.org/html/rfc983">RFC 983</a> 987 </p> -!! html+tidy -<p><a class="external mw-magiclink-rfc" rel="nofollow" href="//tools.ietf.org/html/rfc983">RFC 983</a> 987</p> +!! html/parsoid +<p><a href="https://tools.ietf.org/html/rfc983" rel="mw:ExtLink" class="external text" data-parsoid='{"stx":"magiclink"}'>RFC 983</a><span typeof="mw:Entity" data-parsoid='{"src":"&#x20;","srcContent":" "}'> </span>987</p> !! end !! test Centre-aligned image !! wikitext [[Image:foobar.jpg|centre]] -!! html +!! html/php <div class="center"><div class="floatnone"><a href="/wiki/File:Foobar.jpg" class="image"><img alt="Foobar.jpg" src="http://example.com/images/3/3a/Foobar.jpg" width="1941" height="220" /></a></div></div> -!!end +!! html/parsoid +<figure class="mw-default-size mw-halign-center" typeof="mw:Image" data-parsoid='{"optList":[{"ck":"center","ak":"centre"}]}'><a href="./File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="220" width="1941" data-parsoid='{"a":{"resource":"./File:Foobar.jpg","height":"220","width":"1941"},"sa":{"resource":"Image:foobar.jpg"}}'/></a></figure> +!! end !! test None-aligned image !! wikitext [[Image:foobar.jpg|none]] -!! html +!! html/php <div class="floatnone"><a href="/wiki/File:Foobar.jpg" class="image"><img alt="Foobar.jpg" src="http://example.com/images/3/3a/Foobar.jpg" width="1941" height="220" /></a></div> -!!end +!! html/parsoid +<figure class="mw-default-size mw-halign-none" typeof="mw:Image"><a href="./File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="220" width="1941" data-parsoid='{"a":{"resource":"./File:Foobar.jpg","height":"220","width":"1941"},"sa":{"resource":"Image:foobar.jpg"}}'/></a></figure> +!! end !! test Width + Height sized image (using px) (height is ignored) !! wikitext [[Image:foobar.jpg|640x480px]] -!! html +!! html/php <p><a href="/wiki/File:Foobar.jpg" class="image"><img alt="Foobar.jpg" src="http://example.com/images/thumb/3/3a/Foobar.jpg/640px-Foobar.jpg" width="640" height="73" srcset="http://example.com/images/thumb/3/3a/Foobar.jpg/960px-Foobar.jpg 1.5x, http://example.com/images/thumb/3/3a/Foobar.jpg/1280px-Foobar.jpg 2x" /></a> </p> -!!end +!! html/parsoid +<p><figure-inline typeof="mw:Image" data-parsoid='{"optList":[{"ck":"width","ak":"640x480px"}]}'><a href="./File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/thumb/3/3a/Foobar.jpg/640px-Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="73" width="640" data-parsoid='{"a":{"resource":"./File:Foobar.jpg","height":"73","width":"640"},"sa":{"resource":"Image:foobar.jpg"}}'/></a></figure-inline></p> +!! end !! test Width-sized image (using px, no following whitespace) !! wikitext [[Image:foobar.jpg|640px]] -!! html +!! html/php <p><a href="/wiki/File:Foobar.jpg" class="image"><img alt="Foobar.jpg" src="http://example.com/images/thumb/3/3a/Foobar.jpg/640px-Foobar.jpg" width="640" height="73" srcset="http://example.com/images/thumb/3/3a/Foobar.jpg/960px-Foobar.jpg 1.5x, http://example.com/images/thumb/3/3a/Foobar.jpg/1280px-Foobar.jpg 2x" /></a> </p> -!!end +!! html/parsoid +<p><figure-inline typeof="mw:Image" data-parsoid='{"optList":[{"ck":"width","ak":"640px"}]}'><a href="./File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/thumb/3/3a/Foobar.jpg/640px-Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="73" width="640" data-parsoid='{"a":{"resource":"./File:Foobar.jpg","height":"73","width":"640"},"sa":{"resource":"Image:foobar.jpg"}}'/></a></figure-inline></p> +!! end !! test Width-sized image (using px, with following whitespace - test regression from r39467) !! wikitext [[Image:foobar.jpg|640px ]] -!! html +!! html/php <p><a href="/wiki/File:Foobar.jpg" class="image"><img alt="Foobar.jpg" src="http://example.com/images/thumb/3/3a/Foobar.jpg/640px-Foobar.jpg" width="640" height="73" srcset="http://example.com/images/thumb/3/3a/Foobar.jpg/960px-Foobar.jpg 1.5x, http://example.com/images/thumb/3/3a/Foobar.jpg/1280px-Foobar.jpg 2x" /></a> </p> +!! html/parsoid +<p><figure-inline typeof="mw:Image" data-parsoid='{"optList":[{"ck":"width","ak":"640px "}]}'><a href="./File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/thumb/3/3a/Foobar.jpg/640px-Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="73" width="640" data-parsoid='{"a":{"resource":"./File:Foobar.jpg","height":"73","width":"640"},"sa":{"resource":"Image:foobar.jpg"}}'/></a></figure-inline></p> !!end !! test Width-sized image (using px, with preceding whitespace - test regression from r39467) !! wikitext [[Image:foobar.jpg| 640px]] -!! html +!! html/php <p><a href="/wiki/File:Foobar.jpg" class="image"><img alt="Foobar.jpg" src="http://example.com/images/thumb/3/3a/Foobar.jpg/640px-Foobar.jpg" width="640" height="73" srcset="http://example.com/images/thumb/3/3a/Foobar.jpg/960px-Foobar.jpg 1.5x, http://example.com/images/thumb/3/3a/Foobar.jpg/1280px-Foobar.jpg 2x" /></a> </p> -!!end +!! html/parsoid +<p><figure-inline typeof="mw:Image" data-parsoid='{"optList":[{"ck":"width","ak":" 640px"}]}'><a href="./File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/thumb/3/3a/Foobar.jpg/640px-Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="73" width="640" data-parsoid='{"a":{"resource":"./File:Foobar.jpg","height":"73","width":"640"},"sa":{"resource":"Image:foobar.jpg"}}'/></a></figure-inline></p> +!! end !! test Image with page parameter @@ -20912,7 +21211,7 @@ djvu <p><a href="/index.php?title=File:LoremIpsum.djvu&page=2" class="image"><img alt="LoremIpsum.djvu" src="http://example.com/images/thumb/5/5f/LoremIpsum.djvu/page2-2480px-LoremIpsum.djvu.jpg" width="2480" height="3508" srcset="http://example.com/images/thumb/5/5f/LoremIpsum.djvu/page2-3720px-LoremIpsum.djvu.jpg 1.5x, http://example.com/images/thumb/5/5f/LoremIpsum.djvu/page2-4960px-LoremIpsum.djvu.jpg 2x" /></a> </p> !! html/parsoid -<p><span class="mw-default-size" typeof="mw:Image" data-parsoid='{"optList":[{"ck":"page","ak":"page=2"}]}' data-mw='{"page":"2"}'><a href="./File:LoremIpsum.djvu" data-parsoid='{"a":{"href":"./File:LoremIpsum.djvu"},"sa":{"href":"File:LoremIpsum.djvu"}}'><img resource="./File:LoremIpsum.djvu" src="//example.com/images/5/5f/LoremIpsum.djvu" data-file-width="2480" data-file-height="3508" data-file-type="bitmap" height="3508" width="2480" data-parsoid='{"a":{"resource":"./File:LoremIpsum.djvu","height":"3508","width":"2480"},"sa":{"resource":"File:LoremIpsum.djvu"}}'/></a></span></p> +<p><figure-inline class="mw-default-size" typeof="mw:Image" data-parsoid='{"optList":[{"ck":"page","ak":"page=2"}]}' data-mw='{"page":"2"}'><a href="./File:LoremIpsum.djvu" data-parsoid='{"a":{"href":"./File:LoremIpsum.djvu"},"sa":{"href":"File:LoremIpsum.djvu"}}'><img resource="./File:LoremIpsum.djvu" src="//example.com/images/5/5f/LoremIpsum.djvu" data-file-width="2480" data-file-height="3508" data-file-type="bitmap" height="3508" width="2480" data-parsoid='{"a":{"resource":"./File:LoremIpsum.djvu","height":"3508","width":"2480"},"sa":{"resource":"File:LoremIpsum.djvu"}}'/></a></figure-inline></p> !! end !! test @@ -20946,7 +21245,7 @@ Images with the "|" character in the comment <div class="thumb tright"><div class="thumbinner" style="width:182px;"><a href="/wiki/File:Foobar.jpg" class="image"><img alt="" src="http://example.com/images/thumb/3/3a/Foobar.jpg/180px-Foobar.jpg" width="180" height="20" class="thumbimage" srcset="http://example.com/images/thumb/3/3a/Foobar.jpg/270px-Foobar.jpg 1.5x, http://example.com/images/thumb/3/3a/Foobar.jpg/360px-Foobar.jpg 2x" /></a> <div class="thumbcaption"><div class="magnify"><a href="/wiki/File:Foobar.jpg" class="internal" title="Enlarge"></a></div>An <a rel="nofollow" class="external text" href="http://test/?param1=%7Cleft%7C&param2=%7Cx">external</a> URL</div></div></div> !! html/parsoid -<figure class="mw-default-size" typeof="mw:Image/Thumb"><a href="./File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/thumb/3/3a/Foobar.jpg/220px-Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="25" width="220"/></a><figcaption>An <a rel="mw:ExtLink" href="http://test/?param1=%7Cleft%7C&param2=%7Cx" data-parsoid='{"a":{"href":"http://test/?param1=%7Cleft%7C&param2=%7Cx"},"sa":{"href":"http://test/?param1=|left|&param2=|x"}}'>external</a> URL</figcaption></figure> +<figure class="mw-default-size" typeof="mw:Image/Thumb"><a href="./File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/thumb/3/3a/Foobar.jpg/220px-Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="25" width="220"/></a><figcaption>An <a rel="mw:ExtLink" class="external text" href="http://test/?param1=%7Cleft%7C&param2=%7Cx" data-parsoid='{"a":{"href":"http://test/?param1=%7Cleft%7C&param2=%7Cx"},"sa":{"href":"http://test/?param1=|left|&param2=|x"}}'>external</a> URL</figcaption></figure> !! end !! test @@ -21078,20 +21377,20 @@ parsoid=wt2html !! test Definition list code coverage !! wikitext -; title : def -; title : def +;title : def +;title : def ;title: def !! html/php -<dl><dt> title  </dt> -<dd> def</dd> -<dt> title </dt> -<dd> def</dd> +<dl><dt>title  </dt> +<dd>def</dd> +<dt>title </dt> +<dd>def</dd> <dt>title</dt> -<dd> def</dd></dl> +<dd>def</dd></dl> !! html/parsoid -<dl><dt> title <span typeof="mw:Placeholder"> </span></dt><dd> def</dd> -<dt> title<span typeof="mw:Placeholder"> </span></dt><dd> def</dd> +<dl><dt>title <span typeof="mw:Placeholder"> </span></dt><dd> def</dd> +<dt>title<span typeof="mw:Placeholder"> </span></dt><dd> def</dd> <dt>title</dt><dd> def</dd></dl> !! end @@ -21170,7 +21469,7 @@ Out-of-order TOC heading levels =====5===== ==2== !! html -<div id="toc" class="toc"><div class="toctitle"><h2>Contents</h2></div> +<div id="toc" class="toc"><div class="toctitle" lang="en" dir="ltr"><h2>Contents</h2></div> <ul> <li class="toclevel-1 tocsection-1"><a href="#2"><span class="tocnumber">1</span> <span class="toctext">2</span></a> <ul> @@ -21270,47 +21569,94 @@ ISBN 1 234 56789 0 - 2006 !! test anchorencode +!! config +wgFragmentMode=[ 'html5', 'legacy' ] !! wikitext {{anchorencode:foo bar©#%n}} -!! html +!! html/php +<p>foo_bar©#%n +</p> +!! html/parsoid +<p about="#mwt1" typeof="mw:Transclusion" data-mw='{"parts":[{"template":{"target":{"wt":"anchorencode:foo bar©#%n","function":"anchorencode"},"params":{},"i":0}}]}'>foo_bar©#%n</p> +!! end + +!! test +anchorencode (legacy) +!! config +wgFragmentMode=[ 'legacy' ] +!! wikitext +{{anchorencode:foo bar©#%n}} +!! html/php <p>foo_bar.C2.A9.23.25n </p> !! end !! test anchorencode trims spaces +!! config +wgFragmentMode=[ 'html5', 'legacy' ] !! wikitext {{anchorencode: __pretty__please__}} -!! html +!! html/php <p>pretty_please </p> +!! html/parsoid +<p about="#mwt1" typeof="mw:Transclusion" data-mw='{"parts":[{"template":{"target":{"wt":"anchorencode: __pretty__please__","function":"anchorencode"},"params":{},"i":0}}]}'>pretty_please</p> !! end !! test anchorencode deals with links +!! config +wgFragmentMode=[ 'html5', 'legacy' ] !! wikitext {{anchorencode: [[hello|world]] [[hi]]}} -!! html +!! html/php <p>world_hi </p> +!! html/parsoid +<p about="#mwt1" typeof="mw:Transclusion" data-mw='{"parts":[{"template":{"target":{"wt":"anchorencode: [[hello|world]] [[hi]]","function":"anchorencode"},"params":{},"i":0}}]}'>world_hi</p> !! end !! test anchorencode deals with templates +!! config +wgFragmentMode=[ 'html5', 'legacy' ] !! wikitext -{{anchorencode: {{Foo}} }} -!! html -<p>FOO +{{anchorencode: {{Foo}} x}} +!! html/php +<p>FOO_x </p> +!! html/parsoid +<p about="#mwt2" typeof="mw:Transclusion" data-mw='{"parts":[{"template":{"target":{"wt":"anchorencode: {{Foo}} x","function":"anchorencode"},"params":{},"i":0}}]}'>FOO_x</p> !! end !! test anchorencode encodes like the TOC generator: (T20431) +!! config +wgFragmentMode=[ 'html5', 'legacy' ] !! wikitext -=== _ +:.3A%3A&&]] === +===_ +:.3A%3A _ &&]] x=== +{{anchorencode: _ +:.3A%3A _ &&]] x}} +__NOEDITSECTION__ +!! html/php +<h3><span id=".2B:.3A.253A_.26.26.5D.5D_x"></span><span class="mw-headline" id="+:.3A%3A_&&]]_x">_ +:.3A%3A _ &&]] x</span></h3> +<p>+:.3A%3A_&&]]_x +</p> +!! html/parsoid +<h3 id="+:.3A%3A_&&]]_x"><span id=".2B:.3A.253A_.26.26.5D.5D_x" typeof="mw:FallbackId"></span>_ +:.3A%3A _ &<span typeof="mw:Entity" data-parsoid='{"src":"&amp;","srcContent":"&","dsr":[18,23,null,null]}'>&</span>]] x</h3> +<p about="#mwt1" typeof="mw:Transclusion" data-mw='{"parts":[{"template":{"target":{"wt":"anchorencode: _ +:.3A%3A _ &&amp;]] x","function":"anchorencode"},"params":{},"i":0}}]}'>+:.3A%3A_&&<span typeof="mw:Entity">]</span><span typeof="mw:Entity">]</span>_x</p> +<meta property="mw:PageProp/noeditsection"/> +!! end + +!! test +anchorencode encodes like the TOC generator: (T20431) (legacy) +!! config +wgFragmentMode=[ 'legacy' ] +!! wikitext +===_ +:.3A%3A&&]]=== {{anchorencode: _ +:.3A%3A&&]] }} __NOEDITSECTION__ -!! html +!! html/php <h3><span class="mw-headline" id=".2B:.3A.253A.26.26.5D.5D">_ +:.3A%3A&&]]</span></h3> <p>.2B:.3A.253A.26.26.5D.5D </p> @@ -21586,24 +21932,26 @@ language=sr variant=sr-ec !! test -{}- tags within headlines (within html for parserConvert()) +!! config +wgFragmentMode=[ 'html5', 'legacy' ] !! options language=sr variant=sr-ec !! wikitext -== -{Naslov}- == +==-{Naslov}-== Note that even an unprotected headline ID is not affected by language conversion: -== Latinski == +==Latinski== !! html/php -<h2><span class="mw-headline" id="-.7BNaslov.7D-">Naslov</span><span class="mw-editsection"><span class="mw-editsection-bracket">[</span><a href="/index.php?title=Parser_test&action=edit&section=1" title="Уреди одељак „Naslov“">уреди</a><span class="mw-editsection-bracket">]</span></span></h2> +<h2><span id="-.7BNaslov.7D-"></span><span class="mw-headline" id="-{Naslov}-">Naslov</span><span class="mw-editsection"><span class="mw-editsection-bracket">[</span><a href="/index.php?title=Parser_test&action=edit&section=1" title="Уреди одељак „Naslov“">уреди</a><span class="mw-editsection-bracket">]</span></span></h2> <p>Ноте тхат евен ан унпротецтед хеадлине ИД ис нот аффецтед бy лангуаге цонверсион: </p> <h2><span class="mw-headline" id="Latinski">Латински</span><span class="mw-editsection"><span class="mw-editsection-bracket">[</span><a href="/index.php?title=Parser_test&action=edit&section=2" title="Уреди одељак „Латински“">уреди</a><span class="mw-editsection-bracket">]</span></span></h2> !! html/parsoid -<h2 id="-.7BNaslov.7D-"><span typeof="mw:LanguageVariant" data-mw-variant='{"disabled":{"t":"Naslov"}}'></span></h2> +<h2 id="-{Naslov}-"><span id="-.7BNaslov.7D-" typeof="mw:FallbackId"></span><span typeof="mw:LanguageVariant" data-mw-variant='{"disabled":{"t":"Naslov"}}'></span></h2> <p>Note that even an unprotected headline ID is not affected by language conversion:</p> @@ -22046,7 +22394,7 @@ Nested raw: -{R|nested {{echo|hi}} templates}- Strings evaluating false shouldn't be ignored by Language converter (T51072) !! options language=zh variant=zh-cn -!! input +!! wikitext -{zh-cn:0;zh-sg:1;zh-tw:2;zh-hk:3}- !! html/php <p>0 @@ -22059,7 +22407,7 @@ language=zh variant=zh-cn Conversion rules from [numeric-only string] to [something else] (T48634) !! options language=zh variant=zh-cn -!! input +!! wikitext -{H|0=>zh-cn:B}--{H|0=>zh-cn:C;0=>zh-cn:D}--{H|0=>zh-hans:A}-012345-{A|zh-tw:0;zh-cn:E;}-012345 !! html/php <p>D12345EE12345 @@ -22072,7 +22420,7 @@ language=zh variant=zh-cn Two-way converter rule entries with an empty value should be ignored (T53551) !! options language=zh variant=zh-cn -!! input +!! wikitext -{H|zh-cn:foo;zh-tw:;}-foobar !! html/php <p>foobar @@ -22085,7 +22433,7 @@ language=zh variant=zh-cn One-way converter rule entries with an empty "from" string should be ignored (T53551) !! options language=zh variant=zh-cn -!! input +!! wikitext -{H|=>zh-cn:foo;}-foobar !! html/php <p>foobar @@ -22098,7 +22446,7 @@ language=zh variant=zh-cn Empty converter rule entries shouldn't be inserted into the conversion table (T53551) !! options language=zh variant=zh-cn -!! input +!! wikitext -{H|}-foobar !! html/php <p>foobar @@ -22117,7 +22465,7 @@ Nested: -{zh-hans:Hi -{zh-cn:China;zh-sg:Singapore;}-;zh-hant:Hello -{zh-tw:Taiw <p>Nested: Hello Hong Kong! </p> !! html/parsoid -<p>Nested: <span typeof="mw:LanguageVariant" data-parsoid='{"tSp":[7]}' data-mw-variant='{"twoway":[{"l":"zh-hans","t":"Hi <span typeof=\"mw:LanguageVariant\" data-mw-variant='{\"twoway\":[{\"l\":\"zh-cn\",\"t\":\"China\"},{\"l\":\"zh-sg\",\"t\":\"Singapore\"}]}' data-parsoid='{\"fl\":[],\"tSp\":[7],\"dsr\":[21,53,null,2]}'></span>"},{"l":"zh-hant","t":"Hello <span typeof=\"mw:LanguageVariant\" data-mw-variant='{\"twoway\":[{\"l\":\"zh-tw\",\"t\":\"Taiwan\"},{\"l\":\"zh-hk\",\"t\":\"H&lt;span typeof=\\\"mw:LanguageVariant\\\" data-mw-variant=&#39;{\\\"disabled\\\":{\\\"t\\\":\\\"ong\\\"}}&#39; data-parsoid=&#39;{\\\"fl\\\":[],\\\"dsr\\\":[90,97,null,2]}&#39;>&lt;/span> K&lt;span typeof=\\\"mw:LanguageVariant\\\" data-mw-variant=&#39;{\\\"disabled\\\":{\\\"t\\\":\\\"\\\"}}&#39; data-parsoid=&#39;{\\\"fl\\\":[],\\\"dsr\\\":[99,103,null,2]}&#39;>&lt;/span>ong\"}]}' data-parsoid='{\"fl\":[],\"tSp\":[7],\"dsr\":[68,109,null,2]}'></span>"}]}'></span>!</p> +<p>Nested: <span typeof="mw:LanguageVariant" data-parsoid='{"tSp":[7]}' data-mw-variant='{"twoway":[{"l":"zh-hans","t":"Hi <span typeof=\"mw:LanguageVariant\" data-mw-variant='{\"twoway\":[{\"l\":\"zh-cn\",\"t\":\"China\"},{\"l\":\"zh-sg\",\"t\":\"Singapore\"}]}' data-parsoid='{\"fl\":[],\"tSp\":[7],\"dsr\":[21,53,null,2]}'></span>"},{"l":"zh-hant","t":"Hello <span typeof=\"mw:LanguageVariant\" data-mw-variant='{\"twoway\":[{\"l\":\"zh-tw\",\"t\":\"Taiwan\"},{\"l\":\"zh-hk\",\"t\":\"H&lt;span typeof=\\\"mw:LanguageVariant\\\" data-mw-variant=&apos;{\\\"disabled\\\":{\\\"t\\\":\\\"ong\\\"}}&apos; data-parsoid=&apos;{\\\"fl\\\":[],\\\"dsr\\\":[90,97,null,2]}&apos;>&lt;/span> K&lt;span typeof=\\\"mw:LanguageVariant\\\" data-mw-variant=&apos;{\\\"disabled\\\":{\\\"t\\\":\\\"\\\"}}&apos; data-parsoid=&apos;{\\\"fl\\\":[],\\\"dsr\\\":[99,103,null,2]}&apos;>&lt;/span>ong\"}]}' data-parsoid='{\"fl\":[],\"tSp\":[7],\"dsr\":[68,109,null,2]}'></span>"}]}'></span>!</p> !! end !! test @@ -22130,7 +22478,7 @@ language=zh variant=zh-cn <p><span title="X">A</span> </p> !! html/parsoid -<p><span typeof="mw:LanguageVariant" data-mw-variant='{"filter":{"l":["zh","zh-hans","zh-hant"],"t":"<span title=\"\" about=\"#mwt1\" typeof=\"mw:ExpandedAttrs\" data-parsoid='{\"stx\":\"html\",\"a\":{\"title\":\"\"},\"sa\":{\"title\":\"-{X}-\"},\"dsr\":[21,49,20,7]}' data-mw='{\"attribs\":[[{\"txt\":\"title\"},{\"html\":\"&lt;span typeof=\\\"mw:LanguageVariant\\\" data-mw-variant=&#39;{\\\"disabled\\\":{\\\"t\\\":\\\"X\\\"}}&#39; data-parsoid=&#39;{\\\"fl\\\":[],\\\"dsr\\\":[34,39,null,2]}&#39;>&lt;/span>\"}]]}'>A</span>"}}'></span></p> +<p><span typeof="mw:LanguageVariant" data-mw-variant='{"filter":{"l":["zh","zh-hans","zh-hant"],"t":"<span title=\"\" about=\"#mwt1\" typeof=\"mw:ExpandedAttrs\" data-parsoid='{\"stx\":\"html\",\"a\":{\"title\":\"\"},\"sa\":{\"title\":\"-{X}-\"},\"dsr\":[21,49,20,7]}' data-mw='{\"attribs\":[[{\"txt\":\"title\"},{\"html\":\"&lt;span typeof=\\\"mw:LanguageVariant\\\" data-mw-variant=&apos;{\\\"disabled\\\":{\\\"t\\\":\\\"X\\\"}}&apos; data-parsoid=&apos;{\\\"fl\\\":[],\\\"dsr\\\":[34,39,null,2]}&apos;>&lt;/span>\"}]]}'>A</span>"}}'></span></p> !! end !! test @@ -22143,7 +22491,7 @@ language=zh variant=zh-cn <p><span title="X">A</span> </p> !! html/parsoid -<p><span typeof="mw:LanguageVariant" data-mw-variant='{"disabled":{"t":"<span title=\"\" about=\"#mwt1\" typeof=\"mw:ExpandedAttrs\" data-parsoid='{\"stx\":\"html\",\"a\":{\"title\":\"\"},\"sa\":{\"title\":\"-{X}-\"},\"dsr\":[2,30,20,7]}' data-mw='{\"attribs\":[[{\"txt\":\"title\"},{\"html\":\"&lt;span typeof=\\\"mw:LanguageVariant\\\" data-mw-variant=&#39;{\\\"disabled\\\":{\\\"t\\\":\\\"X\\\"}}&#39; data-parsoid=&#39;{\\\"fl\\\":[],\\\"dsr\\\":[15,20,null,2]}&#39;>&lt;/span>\"}]]}'>A</span>"}}'></span></p> +<p><span typeof="mw:LanguageVariant" data-mw-variant='{"disabled":{"t":"<span title=\"\" about=\"#mwt1\" typeof=\"mw:ExpandedAttrs\" data-parsoid='{\"stx\":\"html\",\"a\":{\"title\":\"\"},\"sa\":{\"title\":\"-{X}-\"},\"dsr\":[2,30,20,7]}' data-mw='{\"attribs\":[[{\"txt\":\"title\"},{\"html\":\"&lt;span typeof=\\\"mw:LanguageVariant\\\" data-mw-variant=&apos;{\\\"disabled\\\":{\\\"t\\\":\\\"X\\\"}}&apos; data-parsoid=&apos;{\\\"fl\\\":[],\\\"dsr\\\":[15,20,null,2]}&apos;>&lt;/span>\"}]]}'>A</span>"}}'></span></p> !! end # Parsoid and PHP disagree on how to parse this example: Parsoid @@ -22160,10 +22508,10 @@ language=zh variant=zh-cn <span>a-{H|0=>zh-cn:x<span>y;0=>zh-tw:b<div>c}-d !! html/php+tidy -<p><span>ab</span></p> -<div><span>cd <span>ab</span></span> -<div><span>cd <span>ad</span></span></div> -</div> +<span>ab<div>cd +<span>ab<div>cd +<span>ad +</span></div></span></div></span> !! html/parsoid <p><span data-parsoid='{"stx":"html","autoInsertedEnd":true}'>a</span></p><div typeof="mw:LanguageVariant" data-mw-variant='{"disabled":{"t":"b<div data-parsoid='{\"stx\":\"html\",\"autoInsertedEnd\":true,\"dsr\":[10,16,5,0]}'>c</div>"}}'></div><p>d</p> @@ -22236,7 +22584,7 @@ parsoid={ |} !! end -# Tests LanguageVariantText._fromSelser +# Tests LanguageVariantText._fromSelSer !! test LanguageConverter selser (4) !! options @@ -22282,20 +22630,20 @@ gopher://www.google.com !! html/php <p><a rel="nofollow" class="external free" href="http://www.google.com">http://www.google.com</a> <a rel="nofollow" class="external free" href="gopher://www.google.com">gopher://www.google.com</a> -<a rel="nofollow" class="external free" href="http://www.google.com">http://www.google.com</a> -<a rel="nofollow" class="external free" href="gopher://www.google.com">gopher://www.google.com</a> +<a rel="nofollow" class="external text" href="http://www.google.com">http://www.google.com</a> +<a rel="nofollow" class="external text" href="gopher://www.google.com">gopher://www.google.com</a> <a rel="nofollow" class="external text" href="https://www.google.com">irc://www.google.com</a> <a rel="nofollow" class="external text" href="ftp://www.google.com">www.гоогле.цом/фтп://дир</a> <a rel="nofollow" class="external text" href="//www.google.com">www.гоогле.цом</a> </p> !! html/parsoid -<p><a rel="mw:ExtLink" href="http://www.google.com">http://www.google.com</a> -<a rel="mw:ExtLink" href="gopher://www.google.com">gopher://www.google.com</a> -<a rel="mw:ExtLink" href="http://www.google.com">http://www.google.com</a> -<a rel="mw:ExtLink" href="gopher://www.google.com">gopher://www.google.com</a> -<a rel="mw:ExtLink" href="https://www.google.com">irc://www.google.com</a> -<a rel="mw:ExtLink" href="ftp://www.google.com">www.google.com/ftp://dir</a> -<a rel="mw:ExtLink" href="//www.google.com">www.google.com</a></p> +<p><a rel="mw:ExtLink" class="external free" href="http://www.google.com">http://www.google.com</a> +<a rel="mw:ExtLink" class="external free" href="gopher://www.google.com">gopher://www.google.com</a> +<a rel="mw:ExtLink" class="external free" href="http://www.google.com">http://www.google.com</a> +<a rel="mw:ExtLink" class="external free" href="gopher://www.google.com">gopher://www.google.com</a> +<a rel="mw:ExtLink" class="external text" href="https://www.google.com">irc://www.google.com</a> +<a rel="mw:ExtLink" class="external text" href="ftp://www.google.com">www.google.com/ftp://dir</a> +<a rel="mw:ExtLink" class="external text" href="//www.google.com">www.google.com</a></p> !! end !! test @@ -22416,15 +22764,9 @@ File:foobar.jpg|{{Test|unamedParam|alt=-{R|param}-}}|alt=galleryalt </ul> !! html/parsoid -<ul class="gallery mw-gallery-traditional" typeof="mw:Extension/gallery" data-mw='{"name":"gallery","attrs":{},"body":{"extsrc":"\nFile:foobar.jpg|[[File:foobar.jpg|20px|desc|alt=-{R|foo}-|-{R|bar}-]]|alt=-{R|bat}-\nFile:foobar.jpg|{{Test|unamedParam|alt=-{R|param}-}}|alt=galleryalt\n"}}'> -<li class="gallerybox"> -<div class="thumb"><span typeof="mw:Image"><a href="./File:Foobar.jpg"><img alt="" resource="./File:Foobar.jpg" src="//example.com/images/thumb/3/3a/Foobar.jpg/120px-Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="14" width="120"/></a></span></div> -<div class="gallerytext"><span typeof="mw:Image" data-mw='{"caption":"<span typeof=\"mw:LanguageVariant\" data-mw-variant='{\"disabled\":{\"t\":\"bar\"}}' data-parsoid='{\"fl\":[\"R\"],\"dsr\":[68,77,null,2]}'></span>"}'><a href="./File:Foobar.jpg"><img alt="" resource="./File:Foobar.jpg" src="//example.com/images/thumb/3/3a/Foobar.jpg/20px-Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="2" width="20"/></a></span></div> -</li> -<li class="gallerybox"> -<div class="thumb"><span typeof="mw:Image"><a href="./File:Foobar.jpg"><img alt="galleryalt" resource="./File:Foobar.jpg" src="//example.com/images/thumb/3/3a/Foobar.jpg/120px-Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="14" width="120"/></a></span></div> -<div class="gallerytext"><span typeof="mw:Transclusion" data-mw='{"parts":[{"template":{"target":{"wt":"Test","href":"./Template:Test"},"params":{"1":{"wt":"unamedParam"},"alt":{"wt":"-{R|param}-"}},"i":0}}]}'>This is a test template</span></div> -</li> +<ul class="gallery mw-gallery-traditional" typeof="mw:Extension/gallery" about="#mwt6" data-mw='{"name":"gallery","attrs":{},"body":{"extsrc":"\nFile:foobar.jpg|[[File:foobar.jpg|20px|desc|alt=-{R|foo}-|-{R|bar}-]]|alt=-{R|bat}-\nFile:foobar.jpg|{{Test|unamedParam|alt=-{R|param}-}}|alt=galleryalt\n"}}'> +<li class="gallerybox" style="width: 155px;"><div class="thumb" style="width: 150px; height: 150px;"><figure-inline typeof="mw:Image"><a href="./File:Foobar.jpg"><img alt="" resource="./File:Foobar.jpg" src="//example.com/images/thumb/3/3a/Foobar.jpg/120px-Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="14" width="120"/></a></figure-inline></div><div class="gallerytext"><figure-inline typeof="mw:Image" data-mw='{"caption":"<span typeof=\"mw:LanguageVariant\" data-mw-variant='{\"disabled\":{\"t\":\"bar\"}}' data-parsoid='{\"fl\":[\"R\"],\"dsr\":[68,77,null,2]}'></span>"}'><a href="./File:Foobar.jpg"><img alt="" resource="./File:Foobar.jpg" src="//example.com/images/thumb/3/3a/Foobar.jpg/20px-Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="2" width="20"/></a></figure-inline></div></li> +<li class="gallerybox" style="width: 155px;"><div class="thumb" style="width: 150px; height: 150px;"><figure-inline typeof="mw:Image"><a href="./File:Foobar.jpg"><img alt="galleryalt" resource="./File:Foobar.jpg" src="//example.com/images/thumb/3/3a/Foobar.jpg/120px-Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="14" width="120"/></a></figure-inline></div><div class="gallerytext"><span about="#mwt4" typeof="mw:Transclusion" data-mw='{"parts":[{"template":{"target":{"wt":"Test","href":"./Template:Test"},"params":{"1":{"wt":"unamedParam"},"alt":{"wt":"-{R|param}-"}},"i":0}}]}'>This is a test template</span></div></li> </ul> !! end @@ -22455,10 +22797,9 @@ language=zh variant=zh-cn ;<b>foo:bar ;-{zh-cn:AAA !! html/php+tidy -<dl> -<dt><b>foo:bar</b></dt> -<dt><b>-{zh-cn:AAA</b></dt> -</dl> +<dl><dt><b>foo:bar</b></dt><b> +<dt>-{zh-cn:AAA</dt></b></dl><p><b> +</b></p> !! html/parsoid <dl><dt data-parsoid='{"dsr":[0,11,1,0]}'><b data-parsoid='{"stx":"html","autoInsertedEnd":true}'>foo:bar</b></dt><b data-parsoid='{"stx":"html","autoInsertedEnd":true,"autoInsertedStart":true}'> <dt data-parsoid='{"dsr":[12,20,1,0]}'>-{zh-cn</dt> @@ -22497,7 +22838,7 @@ parsoid=wt2html,wt2wt,html2html <table> <tr> -<td> B +<td>B </td></tr></table> !! html/parsoid @@ -22611,28 +22952,37 @@ a:b=>c xyz !! end !! test +T179579: Nowiki and lc interaction +!! options +parsoid=wt2html +language=sr +!! wikitext +-{</nowiki>123}- + +-{123<nowiki>|</nowiki>456}- +!! html/parsoid +<p><span typeof="mw:LanguageVariant" data-mw-variant='{"disabled":{"t":"&lt;/nowiki>123"}}' data-parsoid='{"fl":[],"src":"-{</nowiki>123}-"}'></span></p> + +<p><span typeof="mw:LanguageVariant" data-mw-variant='{"disabled":{"t":"123<span typeof=\"mw:Nowiki\" data-parsoid='{\"dsr\":[23,41,8,9]}'>|</span>456"}}' data-parsoid='{"fl":[],"src":"-{123<nowiki>|</nowiki>456}-"}'></span></p> +!! end + +!! test T2529: Uncovered bullet !! wikitext -* Foo {{bullet}} +*Foo {{bullet}} !! html -<ul><li> Foo </li> -<li> Bar</li></ul> +<ul><li>Foo</li> +<li>Bar</li></ul> !! end -# Plain MediaWiki does not remove empty lists, but tidy actually does. -# Templates in Wikipedia rely on this behavior, as tidy has always been -# enabled there. These tests are normally run *without* tidy, so specify the -# full output here. -# To test realistic parsing behavior, apply a tidy-like transformation to both -# the expected output and your parser's output. !! test -T2529: Uncovered bullet leaving empty list, normally removed by tidy +T2529: Uncovered bullet in a deeply nested list !! wikitext -******* Foo {{bullet}} +*******Foo {{bullet}} !! html -<ul><li><ul><li><ul><li><ul><li><ul><li><ul><li><ul><li> Foo </li></ul></li></ul></li></ul></li></ul></li></ul></li></ul></li> -<li> Bar</li></ul> +<ul><li><ul><li><ul><li><ul><li><ul><li><ul><li><ul><li>Foo</li></ul></li></ul></li></ul></li></ul></li></ul></li></ul></li> +<li>Bar</li></ul> !! end @@ -22648,12 +22998,12 @@ y </p> <table> <tr> -<td> 1 </td> -<td> 2 +<td>1</td> +<td>2 </td></tr> <tr> -<td> 3 </td> -<td> 4 +<td>3</td> +<td>4 </td></tr></table> <p>y </p> @@ -22662,10 +23012,10 @@ y !! test T2529: Uncovered bullet in parser function result !! wikitext -* Foo {{lc:{{bullet}} }} +*Foo {{lc:{{bullet}} }} !! html -<ul><li> Foo </li> -<li> bar</li></ul> +<ul><li>Foo</li> +<li>bar</li></ul> !! end @@ -22818,14 +23168,17 @@ Line two !! end +# doBlockLevels screws up this output and Remex cleans up as much as it can. +# Parsoid seems to do a better job here since its p-wrapper is probably smarter. !! test Nesting tags, paragraphs on lines which begin with <div> !! wikitext <div></div><strong>A B</strong> !! html/php+tidy -<p><strong>A</strong></p> -<p><strong>B</strong></p> +<div></div><p><strong>A +</strong></p><strong></strong><p><strong>B</strong> +</p> !! html/parsoid <div></div> <p><strong>A @@ -22845,9 +23198,8 @@ Line two</blockquote> Line two</blockquote> !! html+tidy -<blockquote> -<p>Line one Line two</p> -</blockquote> +<blockquote><p>Line one +Line two</p></blockquote> !! end !! test @@ -22865,10 +23217,12 @@ Line two</blockquote> !! html+tidy <blockquote> -<p>Line one</p> -Line two</blockquote> +<p>Line one +</p><p> +Line two</p></blockquote> !! end +# Parsoid's output is broken on this because of Tidy-compatibility cruft !! test T8200: paragraphs inside blockquotes (extra line break on close) !! wikitext @@ -22883,9 +23237,9 @@ Line two </blockquote> !! html+tidy -<blockquote> -<p>Line one</p> -<p>Line two</p> +<blockquote><p>Line one +</p><p>Line two +</p> </blockquote> !! end @@ -22904,13 +23258,9 @@ Line two </p> </blockquote> -!! html+tidy -<blockquote> -<p>Line one</p> -<p>Line two</p> -</blockquote> !! end +# FIXME: Why does/should the blockquote+div combo suppress p-wrapping here? !! test Paragraphs inside blockquotes/divs (no extra line breaks) !! wikitext @@ -22989,9 +23339,11 @@ wgLinkHolderBatchSize=0 Free external link invading image caption !! wikitext [[Image:Foobar.jpg|thumb|http://x|hello]] -!! html +!! html/php <div class="thumb tright"><div class="thumbinner" style="width:182px;"><a href="/wiki/File:Foobar.jpg" class="image"><img alt="" src="http://example.com/images/thumb/3/3a/Foobar.jpg/180px-Foobar.jpg" width="180" height="20" class="thumbimage" srcset="http://example.com/images/thumb/3/3a/Foobar.jpg/270px-Foobar.jpg 1.5x, http://example.com/images/thumb/3/3a/Foobar.jpg/360px-Foobar.jpg 2x" /></a> <div class="thumbcaption"><div class="magnify"><a href="/wiki/File:Foobar.jpg" class="internal" title="Enlarge"></a></div>hello</div></div></div> +!! html/parsoid +<figure class="mw-default-size" typeof="mw:Image/Thumb" data-parsoid='{"optList":[{"ck":"thumbnail","ak":"thumb"},{"ck":"bogus","ak":"http://x"},{"ck":"caption","ak":"hello"}]}'><a href="./File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/thumb/3/3a/Foobar.jpg/220px-Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="25" width="220" data-parsoid='{"a":{"resource":"./File:Foobar.jpg","height":"25","width":"220"},"sa":{"resource":"Image:Foobar.jpg"}}'/></a><figcaption>hello</figcaption></figure> !! end !! test @@ -23004,25 +23356,29 @@ language=fa <p><a rel="nofollow" class="external autonumber" href="http://en.wikipedia.org/">[۱]</a> </p> !! html/parsoid -<p><a rel="mw:ExtLink" href="http://en.wikipedia.org/"></a></p> +<p><a rel="mw:ExtLink" class="external autonumber" href="http://en.wikipedia.org/"></a></p> !! end !! test Multibyte character in padleft !! wikitext {{padleft:-Hello|7|Æ}} -!! html +!! html/php <p>Æ-Hello </p> +!! html/parsoid +<p typeof="mw:Transclusion" data-mw='{"parts":[{"template":{"target":{"wt":"padleft:-Hello","function":"padleft"},"params":{"1":{"wt":"7"},"2":{"wt":"Æ"}},"i":0}}]}'>Æ-Hello</p> !! end !! test Multibyte character in padright !! wikitext {{padright:Hello-|7|Æ}} -!! html +!! html/php <p>Hello-Æ </p> +!! html/parsoid +<p typeof="mw:Transclusion" data-mw='{"parts":[{"template":{"target":{"wt":"padright:Hello-","function":"padright"},"params":{"1":{"wt":"7"},"2":{"wt":"Æ"}},"i":0}}]}'>Hello-Æ</p> !! end !!test @@ -23262,7 +23618,7 @@ comment Bad images - basic functionality !! wikitext [[File:Bad.jpg]] -!! DISABLED/html/php +!! html/php+disabled !! html/parsoid <p><span class="mw-default-size" typeof="mw:Error mw:Image" data-mw='{"errors":[{"key":"bad-image","message":"This image is blacklisted in this context."}]}'><a href="./File:Bad.jpg"><img resource="./File:Bad.jpg" height="220" width="220"/></a></span></p> !! end @@ -23273,7 +23629,7 @@ Bad images - T18039: text after bad image disappears Foo bar [[File:Bad.jpg]] Bar foo -!! DISABLED/html/php +!! html/php+disabled <p>Foo bar </p><p>Bar foo </p> @@ -23451,13 +23807,13 @@ showindicators <indicator name="02">[[Main Page]]</indicator> <indicator name="03">[[File:Foobar.jpg|25px|link=]]</indicator> <indicator name="04">[[File:Foobar.jpg|25px]]</indicator> -<indicator name="05">* foo -* bar</indicator> +<indicator name="05">*foo +*bar</indicator> <indicator name="06"><nowiki>foo</nowiki></indicator> <indicator name="07"> Preformatted</indicator> <indicator name="08"><div>Broken tag</indicator> <indicator name="09">{| class=wikitable -| cell +|cell |}</indicator> <indicator name="10">Two @@ -23467,8 +23823,8 @@ paragraphs</indicator> 02=<a href="/wiki/Main_Page" title="Main Page">Main Page</a> 03=<img alt="Foobar.jpg" src="http://example.com/images/thumb/3/3a/Foobar.jpg/25px-Foobar.jpg" width="25" height="3" srcset="http://example.com/images/thumb/3/3a/Foobar.jpg/38px-Foobar.jpg 1.5x, http://example.com/images/thumb/3/3a/Foobar.jpg/50px-Foobar.jpg 2x" /> 04=<a href="/wiki/File:Foobar.jpg" class="image"><img alt="Foobar.jpg" src="http://example.com/images/thumb/3/3a/Foobar.jpg/25px-Foobar.jpg" width="25" height="3" srcset="http://example.com/images/thumb/3/3a/Foobar.jpg/38px-Foobar.jpg 1.5x, http://example.com/images/thumb/3/3a/Foobar.jpg/50px-Foobar.jpg 2x" /></a> -05=<ul><li> foo</li> -<li> bar</li></ul> +05=<ul><li>foo</li> +<li>bar</li></ul> 06=foo 07=<pre>Preformatted @@ -23477,7 +23833,7 @@ paragraphs</indicator> 09=<table class="wikitable"> <tr> -<td> cell +<td>cell </td></tr></table> 10=<p>Two @@ -23594,7 +23950,7 @@ percent-encoding and + signs in internal links (T28410) !! html/parsoid <p><a rel="mw:WikiLink" href="./User:+%25" title="User:+%" data-parsoid='{"stx":"simple","a":{"href":"./User:+%25"},"sa":{"href":"User:+%"}}'>User:+%</a> <a rel="mw:WikiLink" href="./Page+title%25" title="Page+title%" data-parsoid='{"stx":"simple","a":{"href":"./Page+title%25"},"sa":{"href":"Page+title%"}}'>Page+title%</a> <a rel="mw:WikiLink" href="./%25+" title="%+" data-parsoid='{"stx":"simple","a":{"href":"./%25+"},"sa":{"href":"%+"}}'>%+</a> <a rel="mw:WikiLink" href="./%25+" title="%+" data-parsoid='{"stx":"piped","a":{"href":"./%25+"},"sa":{"href":"%+"}}'>%20</a> <a rel="mw:WikiLink" href="./%25+" title="%+" data-parsoid='{"stx":"simple","a":{"href":"./%25+"},"sa":{"href":"%+ "}}'>%+ </a> <a rel="mw:WikiLink" href="./%25+r" title="%+r" data-parsoid='{"stx":"simple","a":{"href":"./%25+r"},"sa":{"href":"%+r"}}'>%+r</a> -<a rel="mw:WikiLink" href="./%25" title="%" data-parsoid='{"stx":"simple","a":{"href":"./%25"},"sa":{"href":"%"}}'>%</a> <a rel="mw:WikiLink" href="./+" title="+" data-parsoid='{"stx":"simple","a":{"href":"./+"},"sa":{"href":"+"}}'>+</a> <span class="mw-default-size" typeof="mw:Error mw:Image" data-parsoid='{"optList":[{"ck":"bogus","ak":"foo"},{"ck":"caption","ak":"[[bar]]"}]}' data-mw='{"errors":[{"key":"apierror-filedoesnotexist","message":"This image does not exist."}],"caption":"<a rel=\"mw:WikiLink\" href=\"./Bar\" title=\"Bar\" data-parsoid='{\"stx\":\"simple\",\"a\":{\"href\":\"./Bar\"},\"sa\":{\"href\":\"bar\"},\"dsr\":[94,101,2,2]}'>bar</a>"}'><a href="./File:%25+abc9" data-parsoid='{"a":{"href":"./File:%25+abc9"},"sa":{}}'><img resource="./File:%25+abc9" src="./Special:FilePath/%25+abc9" height="220" width="220" data-parsoid='{"a":{"resource":"./File:%25+abc9","height":"220","width":"220"},"sa":{"resource":"File:%+abc%39"}}'/></a></span> +<a rel="mw:WikiLink" href="./%25" title="%" data-parsoid='{"stx":"simple","a":{"href":"./%25"},"sa":{"href":"%"}}'>%</a> <a rel="mw:WikiLink" href="./+" title="+" data-parsoid='{"stx":"simple","a":{"href":"./+"},"sa":{"href":"+"}}'>+</a> <figure-inline class="mw-default-size" typeof="mw:Error mw:Image" data-parsoid='{"optList":[{"ck":"bogus","ak":"foo"},{"ck":"caption","ak":"[[bar]]"}]}' data-mw='{"errors":[{"key":"apierror-filedoesnotexist","message":"This image does not exist."}],"caption":"<a rel=\"mw:WikiLink\" href=\"./Bar\" title=\"Bar\" data-parsoid='{\"stx\":\"simple\",\"a\":{\"href\":\"./Bar\"},\"sa\":{\"href\":\"bar\"},\"dsr\":[94,101,2,2]}'>bar</a>"}'><a href="./File:%25+abc9" data-parsoid='{"a":{"href":"./File:%25+abc9"},"sa":{}}'><img resource="./File:%25+abc9" src="./Special:FilePath/%25+abc9" height="220" width="220" data-parsoid='{"a":{"resource":"./File:%25+abc9","height":"220","width":"220"},"sa":{"resource":"File:%+abc%39"}}'/></a></figure-inline> <a rel="mw:WikiLink" href="./3E" title="3E" data-parsoid='{"stx":"simple","a":{"href":"./3E"},"sa":{"href":"%33%45"}}'>3E</a> <a rel="mw:WikiLink" href="./3E+" title="3E+" data-parsoid='{"stx":"simple","a":{"href":"./3E+"},"sa":{"href":"%33%45+"}}'>3E+</a></p> !! end @@ -23608,8 +23964,8 @@ Special characters in embedded file links (T29679) <a href="/index.php?title=Special:Upload&wpDestFile=Does_not_exist.jpg" class="new" title="File:Does not exist.jpg">Title with & ampersand</a> </p> !! html/parsoid -<p><span class="mw-default-size" typeof="mw:Error mw:Image" data-mw='{"errors":[{"key":"apierror-filedoesnotexist","message":"This image does not exist."}]}'><a href="./File:Contains_&_ampersand.jpg"><img resource="./File:Contains_&_ampersand.jpg" src="./Special:FilePath/Contains_&_ampersand.jpg" height="220" width="220"/></a></span> -<span class="mw-default-size" typeof="mw:Error mw:Image" data-mw='{"errors":[{"key":"apierror-filedoesnotexist","message":"This image does not exist."}],"caption":"Title with &amp; ampersand"}'><a href="./File:Does_not_exist.jpg"><img resource="./File:Does_not_exist.jpg" src="./Special:FilePath/Does_not_exist.jpg" height="220" width="220"/></a></span></p> +<p><figure-inline class="mw-default-size" typeof="mw:Error mw:Image" data-mw='{"errors":[{"key":"apierror-filedoesnotexist","message":"This image does not exist."}]}'><a href="./File:Contains_&_ampersand.jpg"><img resource="./File:Contains_&_ampersand.jpg" src="./Special:FilePath/Contains_&_ampersand.jpg" height="220" width="220"/></a></figure-inline> +<figure-inline class="mw-default-size" typeof="mw:Error mw:Image" data-mw='{"errors":[{"key":"apierror-filedoesnotexist","message":"This image does not exist."}],"caption":"Title with &amp; ampersand"}'><a href="./File:Does_not_exist.jpg"><img resource="./File:Does_not_exist.jpg" src="./Special:FilePath/Does_not_exist.jpg" height="220" width="220"/></a></figure-inline></p> !! end !! test @@ -23744,9 +24100,9 @@ T28375: TOC with italics title=[[Main Page]] !! wikitext __TOC__ -== ''Lost'' episodes == +==''Lost'' episodes== !! html/php -<div id="toc" class="toc"><div class="toctitle"><h2>Contents</h2></div> +<div id="toc" class="toc"><div class="toctitle" lang="en" dir="ltr"><h2>Contents</h2></div> <ul> <li class="toclevel-1 tocsection-1"><a href="#Lost_episodes"><span class="tocnumber">1</span> <span class="toctext"><i>Lost</i> episodes</span></a></li> </ul> @@ -23756,7 +24112,7 @@ __TOC__ !! html/parsoid <meta property="mw:PageProp/toc" data-parsoid='{}'/> -<h2 data-parsoid='{}'> <i>Lost</i> episodes </h2> +<h2 id="Lost_episodes" data-parsoid='{}'><i>Lost</i> episodes</h2> !! end !! test @@ -23765,9 +24121,9 @@ T28375: TOC with bold title=[[Main Page]] !! wikitext __TOC__ -== '''should be bold''' then normal text == +=='''should be bold''' then normal text== !! html/php -<div id="toc" class="toc"><div class="toctitle"><h2>Contents</h2></div> +<div id="toc" class="toc"><div class="toctitle" lang="en" dir="ltr"><h2>Contents</h2></div> <ul> <li class="toclevel-1 tocsection-1"><a href="#should_be_bold_then_normal_text"><span class="tocnumber">1</span> <span class="toctext"><b>should be bold</b> then normal text</span></a></li> </ul> @@ -23777,7 +24133,7 @@ __TOC__ !! html/parsoid <meta property="mw:PageProp/toc" data-parsoid='{}'/> -<h2 data-parsoid='{}'> <b>should be bold</b> then normal text </h2> +<h2 id="should_be_bold_then_normal_text" data-parsoid='{}'><b>should be bold</b> then normal text</h2> !! end !! test @@ -23786,9 +24142,9 @@ T35845: Headings become cursive in TOC when they contain an image title=[[Main Page]] !! wikitext __TOC__ -== Image [[Image:foobar.jpg]] == +==Image [[Image:foobar.jpg]]== !! html/php -<div id="toc" class="toc"><div class="toctitle"><h2>Contents</h2></div> +<div id="toc" class="toc"><div class="toctitle" lang="en" dir="ltr"><h2>Contents</h2></div> <ul> <li class="toclevel-1 tocsection-1"><a href="#Image"><span class="tocnumber">1</span> <span class="toctext">Image</span></a></li> </ul> @@ -23798,7 +24154,7 @@ __TOC__ !! html/parsoid <meta property="mw:PageProp/toc" data-parsoid='{}'/> -<h2 data-parsoid='{}'> Image <span class="mw-default-size" typeof="mw:Image"><a href="./File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="220" width="1941" data-parsoid='{"a":{"resource":"./File:Foobar.jpg","height":"220","width":"1941"},"sa":{"resource":"Image:foobar.jpg"}}'/></a></span> </h2> +<h2 id="Image" data-parsoid='{}'>Image <figure-inline class="mw-default-size" typeof="mw:Image"><a href="./File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="220" width="1941" data-parsoid='{"a":{"resource":"./File:Foobar.jpg","height":"220","width":"1941"},"sa":{"resource":"Image:foobar.jpg"}}'/></a></figure-inline></h2> !! end !! test @@ -23807,9 +24163,9 @@ T35845 (2): Headings become bold in TOC when they contain a blockquote title=[[Main Page]] !! wikitext __TOC__ -== <blockquote>Quote</blockquote> == +==<blockquote>Quote</blockquote>== !! html/php -<div id="toc" class="toc"><div class="toctitle"><h2>Contents</h2></div> +<div id="toc" class="toc"><div class="toctitle" lang="en" dir="ltr"><h2>Contents</h2></div> <ul> <li class="toclevel-1 tocsection-1"><a href="#Quote"><span class="tocnumber">1</span> <span class="toctext">Quote</span></a></li> </ul> @@ -23818,49 +24174,43 @@ __TOC__ <h2><span class="mw-headline" id="Quote"><blockquote>Quote</blockquote></span><span class="mw-editsection"><span class="mw-editsection-bracket">[</span><a href="/index.php?title=Main_Page&action=edit&section=1" title="Edit section: Quote">edit</a><span class="mw-editsection-bracket">]</span></span></h2> !! html/php+tidy -<p></p> -<div id="toc" class="toc"> -<div class="toctitle"> -<h2>Contents</h2> -</div> +<div id="toc" class="toc"><div class="toctitle" lang="en" dir="ltr"><h2>Contents</h2></div> <ul> <li class="toclevel-1 tocsection-1"><a href="#Quote"><span class="tocnumber">1</span> <span class="toctext">Quote</span></a></li> </ul> </div> -<p></p> -<h2><span class="mw-headline" id="Quote"></span></h2> -<blockquote> -<p><span class="mw-headline" id="Quote">Quote</span></p> -</blockquote> -<p><span class="mw-editsection"><span class="mw-editsection-bracket">[</span><a href="/index.php?title=Main_Page&action=edit&section=1" title="Edit section: Quote">edit</a><span class="mw-editsection-bracket">]</span></span></p> + +<h2><span class="mw-headline" id="Quote"><blockquote><p>Quote</p></blockquote></span><span class="mw-editsection"><span class="mw-editsection-bracket">[</span><a href="/index.php?title=Main_Page&action=edit&section=1" title="Edit section: Quote">edit</a><span class="mw-editsection-bracket">]</span></span></h2> !! html/parsoid <meta property="mw:PageProp/toc" data-parsoid='{}'/> -<h2 data-parsoid='{}'> <blockquote>Quote</blockquote> </h2> +<h2 id="Quote" data-parsoid='{}'><blockquote>Quote</blockquote></h2> !! end !! test Unclosed tags in TOC +!! config +wgFragmentMode=[ 'html5', 'legacy' ] !! options title=[[Main Page]] !! wikitext __TOC__ -== Proof: 2 < 3 == +==Proof: 2 < 3== <small>Hanc marginis exiguitas non caperet.</small> QED !! html/php -<div id="toc" class="toc"><div class="toctitle"><h2>Contents</h2></div> +<div id="toc" class="toc"><div class="toctitle" lang="en" dir="ltr"><h2>Contents</h2></div> <ul> -<li class="toclevel-1 tocsection-1"><a href="#Proof:_2_.3C_3"><span class="tocnumber">1</span> <span class="toctext">Proof: 2 < 3</span></a></li> +<li class="toclevel-1 tocsection-1"><a href="#Proof:_2_<_3"><span class="tocnumber">1</span> <span class="toctext">Proof: 2 < 3</span></a></li> </ul> </div> -<h2><span class="mw-headline" id="Proof:_2_.3C_3">Proof: 2 < 3</span><span class="mw-editsection"><span class="mw-editsection-bracket">[</span><a href="/index.php?title=Main_Page&action=edit&section=1" title="Edit section: Proof: 2 < 3">edit</a><span class="mw-editsection-bracket">]</span></span></h2> +<h2><span id="Proof:_2_.3C_3"></span><span class="mw-headline" id="Proof:_2_<_3">Proof: 2 < 3</span><span class="mw-editsection"><span class="mw-editsection-bracket">[</span><a href="/index.php?title=Main_Page&action=edit&section=1" title="Edit section: Proof: 2 < 3">edit</a><span class="mw-editsection-bracket">]</span></span></h2> <p><small>Hanc marginis exiguitas non caperet.</small> QED </p> !! html/parsoid <meta property="mw:PageProp/toc" data-parsoid='{}'/> -<h2 data-parsoid='{}'> Proof: 2 < 3 </h2> +<h2 id="Proof:_2_<_3" data-parsoid='{}'><span id="Proof:_2_.3C_3" typeof="mw:FallbackId"></span>Proof: 2 < 3</h2> <p><small>Hanc marginis exiguitas non caperet.</small> QED</p> !! end @@ -23869,11 +24219,11 @@ QED</p> Multiple tags in TOC !! wikitext __TOC__ -== <i>Foo</i> <b>Bar</b> == +==<i>Foo</i> <b>Bar</b>== -== <i>Foo</i> <blockquote>Bar</blockquote> == +==<i>Foo</i> <blockquote>Bar</blockquote>== !! html/php -<div id="toc" class="toc"><div class="toctitle"><h2>Contents</h2></div> +<div id="toc" class="toc"><div class="toctitle" lang="en" dir="ltr"><h2>Contents</h2></div> <ul> <li class="toclevel-1 tocsection-1"><a href="#Foo_Bar"><span class="tocnumber">1</span> <span class="toctext"><i>Foo</i> <b>Bar</b></span></a></li> <li class="toclevel-1 tocsection-2"><a href="#Foo_Bar_2"><span class="tocnumber">2</span> <span class="toctext"><i>Foo</i> Bar</span></a></li> @@ -23884,27 +24234,20 @@ __TOC__ <h2><span class="mw-headline" id="Foo_Bar_2"><i>Foo</i> <blockquote>Bar</blockquote></span><span class="mw-editsection"><span class="mw-editsection-bracket">[</span><a href="/index.php?title=Parser_test&action=edit&section=2" title="Edit section: Foo Bar">edit</a><span class="mw-editsection-bracket">]</span></span></h2> !! html/php+tidy -<p></p> -<div id="toc" class="toc"> -<div class="toctitle"> -<h2>Contents</h2> -</div> +<div id="toc" class="toc"><div class="toctitle" lang="en" dir="ltr"><h2>Contents</h2></div> <ul> <li class="toclevel-1 tocsection-1"><a href="#Foo_Bar"><span class="tocnumber">1</span> <span class="toctext"><i>Foo</i> <b>Bar</b></span></a></li> <li class="toclevel-1 tocsection-2"><a href="#Foo_Bar_2"><span class="tocnumber">2</span> <span class="toctext"><i>Foo</i> Bar</span></a></li> </ul> </div> -<p></p> + <h2><span class="mw-headline" id="Foo_Bar"><i>Foo</i> <b>Bar</b></span><span class="mw-editsection"><span class="mw-editsection-bracket">[</span><a href="/index.php?title=Parser_test&action=edit&section=1" title="Edit section: Foo Bar">edit</a><span class="mw-editsection-bracket">]</span></span></h2> -<h2><span class="mw-headline" id="Foo_Bar_2"><i>Foo</i></span></h2> -<blockquote> -<p><span class="mw-headline" id="Foo_Bar_2">Bar</span></p> -</blockquote> -<p><span class="mw-editsection"><span class="mw-editsection-bracket">[</span><a href="/index.php?title=Parser_test&action=edit&section=2" title="Edit section: Foo Bar">edit</a><span class="mw-editsection-bracket">]</span></span></p> +<h2><span class="mw-headline" id="Foo_Bar_2"><i>Foo</i> <blockquote><p>Bar</p></blockquote></span><span class="mw-editsection"><span class="mw-editsection-bracket">[</span><a href="/index.php?title=Parser_test&action=edit&section=2" title="Edit section: Foo Bar">edit</a><span class="mw-editsection-bracket">]</span></span></h2> !! html/parsoid <meta property="mw:PageProp/toc" data-parsoid='{}'/> -<h2 data-parsoid='{}'> <i data-parsoid='{"stx":"html"}'>Foo</i> <b data-parsoid='{"stx":"html"}'>Bar</b> </h2> -<h2> <i data-parsoid='{"stx":"html"}'>Foo</i> <blockquote>Bar</blockquote> </h2> +<h2 id="Foo_Bar" data-parsoid='{}'><i data-parsoid='{"stx":"html"}'>Foo</i> <b data-parsoid='{"stx":"html"}'>Bar</b></h2> + +<h2 id="Foo_Bar_2" data-parsoid='{}'><i data-parsoid='{"stx":"html"}'>Foo</i> <blockquote>Bar</blockquote></h2> !! end # Don't expect Parsoid to roundtrip this until the php parser comes closer to @@ -23915,11 +24258,11 @@ Tags with parameters in TOC parsoid=wt2html !! wikitext __TOC__ -== <sup class="in-h2">Hello</sup> == +==<sup class="in-h2">Hello</sup>== -== <sup class="a > b">Evilbye</sup> == +==<sup class="a > b">Evilbye</sup>== !! html/php -<div id="toc" class="toc"><div class="toctitle"><h2>Contents</h2></div> +<div id="toc" class="toc"><div class="toctitle" lang="en" dir="ltr"><h2>Contents</h2></div> <ul> <li class="toclevel-1 tocsection-1"><a href="#Hello"><span class="tocnumber">1</span> <span class="toctext"><sup>Hello</sup></span></a></li> <li class="toclevel-1 tocsection-2"><a href="#b.22.3EEvilbye"><span class="tocnumber">2</span> <span class="toctext"><sup> b">Evilbye</sup></span></a></li> @@ -23931,26 +24274,26 @@ __TOC__ !! html/parsoid <meta property="mw:PageProp/toc" /> -<h2> <sup class="in-h2" data-parsoid='{"stx":"html"}'>Hello</sup> </h2> +<h2 id="Hello"><sup class="in-h2" data-parsoid='{"stx":"html"}'>Hello</sup></h2> -<h2> <sup class="a " data-parsoid='{"stx":"html"}'> b">Evilbye</sup> </h2> +<h2 id='b">Evilbye'><span id="b.22.3EEvilbye" typeof="mw:FallbackId"></span><sup class="a " data-parsoid='{"stx":"html"}'> b">Evilbye</sup></h2> !! end !! test span tags with directionality in TOC !! wikitext __TOC__ -== <span dir="ltr">C++</span> == +==<span dir="ltr">C++</span>== -== <span dir="rtl">זבנג!</span> == +==<span dir="rtl">זבנג!</span>== -== <span style="font-style: italic">The attributes on these span tags must be deleted from the TOC</span> == +==<span style="font-style: italic">The attributes on these span tags must be deleted from the TOC</span>== -== <span style="font-style: italic" dir="ltr">All attributes on these span tags must be deleted from the TOC</span> == +==<span style="font-style: italic" dir="ltr">All attributes on these span tags must be deleted from the TOC</span>== -== <span dir="ltr" style="font-style: italic">Attributes after dir on these span tags must be deleted from the TOC</span> == +==<span dir="ltr" style="font-style: italic">Attributes after dir on these span tags must be deleted from the TOC</span>== !! html/php -<div id="toc" class="toc"><div class="toctitle"><h2>Contents</h2></div> +<div id="toc" class="toc"><div class="toctitle" lang="en" dir="ltr"><h2>Contents</h2></div> <ul> <li class="toclevel-1 tocsection-1"><a href="#C.2B.2B"><span class="tocnumber">1</span> <span class="toctext"><span dir="ltr">C++</span></span></a></li> <li class="toclevel-1 tocsection-2"><a href="#.D7.96.D7.91.D7.A0.D7.92.21"><span class="tocnumber">2</span> <span class="toctext"><span dir="rtl">זבנג!</span></span></a></li> @@ -23968,20 +24311,20 @@ __TOC__ !! html/parsoid <meta property="mw:PageProp/toc" data-parsoid='{}'/> -<h2 data-parsoid='{}'> <span dir="ltr">C++</span> </h2> -<h2> <span dir="rtl">זבנג!</span> </h2> -<h2> <span style="font-style: italic">The attributes on these span tags must be deleted from the TOC</span> </h2> -<h2> <span style="font-style: italic" dir="ltr">All attributes on these span tags must be deleted from the TOC</span> </h2> -<h2> <span dir="ltr" style="font-style: italic">Attributes after dir on these span tags must be deleted from the TOC</span> </h2> +<h2 id="C++" data-parsoid='{}'><span id="C.2B.2B" typeof="mw:FallbackId"></span><span dir="ltr">C++</span></h2> +<h2 id="זבנג!"><span id=".D7.96.D7.91.D7.A0.D7.92.21" typeof="mw:FallbackId"></span><span dir="rtl">זבנג!</span></h2> +<h2 id="The_attributes_on_these_span_tags_must_be_deleted_from_the_TOC"><span style="font-style: italic">The attributes on these span tags must be deleted from the TOC</span></h2> +<h2 id="All_attributes_on_these_span_tags_must_be_deleted_from_the_TOC"><span style="font-style: italic" dir="ltr">All attributes on these span tags must be deleted from the TOC</span></h2> +<h2 id="Attributes_after_dir_on_these_span_tags_must_be_deleted_from_the_TOC"><span dir="ltr" style="font-style: italic">Attributes after dir on these span tags must be deleted from the TOC</span></h2> !! end !! test T74884: bdi element in ToC !! wikitext __TOC__ -== <bdi>test</bdi> == +==<bdi>test</bdi>== !! html/php -<div id="toc" class="toc"><div class="toctitle"><h2>Contents</h2></div> +<div id="toc" class="toc"><div class="toctitle" lang="en" dir="ltr"><h2>Contents</h2></div> <ul> <li class="toclevel-1 tocsection-1"><a href="#test"><span class="tocnumber">1</span> <span class="toctext"><bdi>test</bdi></span></a></li> </ul> @@ -23991,16 +24334,16 @@ __TOC__ !! html/parsoid <meta property="mw:PageProp/toc" data-parsoid='{}'/> -<h2 data-parsoid='{}'> <bdi>test</bdi> </h2> +<h2 id="test" data-parsoid='{}'><bdi>test</bdi></h2> !! end !! test T35715: s/strike element in ToC !! wikitext __TOC__ -== <s>test</s> test <strike>test</strike> == +==<s>test</s> test <strike>test</strike>== !! html/php -<div id="toc" class="toc"><div class="toctitle"><h2>Contents</h2></div> +<div id="toc" class="toc"><div class="toctitle" lang="en" dir="ltr"><h2>Contents</h2></div> <ul> <li class="toclevel-1 tocsection-1"><a href="#test_test_test"><span class="tocnumber">1</span> <span class="toctext"><s>test</s> test <strike>test</strike></span></a></li> </ul> @@ -24010,19 +24353,16 @@ __TOC__ !! html/parsoid <meta property="mw:PageProp/toc" data-parsoid='{}'/> -<h2 data-parsoid='{}'> <s>test</s> test <strike>test</strike> </h2> +<h2 id="test_test_test" data-parsoid='{}'><s>test</s> test <strike>test</strike></h2> !! end -# Note that the html output does not have the <p></p>, but the -# html+tidy output *does*. This is because the empty <p></p> is -# removed by the sanitizer, but only when tidy is *not* enabled (!). !! test Empty <p> tag in TOC, removed by Sanitizer (T92892) !! wikitext __TOC__ -== x == +==x== !! html/php -<div id="toc" class="toc"><div class="toctitle"><h2>Contents</h2></div> +<div id="toc" class="toc"><div class="toctitle" lang="en" dir="ltr"><h2>Contents</h2></div> <ul> <li class="toclevel-1 tocsection-1"><a href="#x"><span class="tocnumber">1</span> <span class="toctext">x</span></a></li> </ul> @@ -24030,21 +24370,9 @@ __TOC__ <h2><span class="mw-headline" id="x">x</span><span class="mw-editsection"><span class="mw-editsection-bracket">[</span><a href="/index.php?title=Parser_test&action=edit&section=1" title="Edit section: x">edit</a><span class="mw-editsection-bracket">]</span></span></h2> -!! html/php+tidy -<p></p> -<div id="toc" class="toc"> -<div class="toctitle"> -<h2>Contents</h2> -</div> -<ul> -<li class="toclevel-1 tocsection-1"><a href="#x"><span class="tocnumber">1</span> <span class="toctext">x</span></a></li> -</ul> -</div> -<p></p> -<h2><span class="mw-headline" id="x">x</span><span class="mw-editsection"><span class="mw-editsection-bracket">[</span><a href="/index.php?title=Parser_test&action=edit&section=1" title="Edit section: x">edit</a><span class="mw-editsection-bracket">]</span></span></h2> !! html/parsoid <meta property="mw:PageProp/toc" data-parsoid='{}'/> -<h2 data-parsoid='{}'> x </h2> +<h2 id="x" data-parsoid='{}'>x</h2> !! end !! article @@ -24167,9 +24495,11 @@ Strip marker in padright Strip marker in anchorencode !! wikitext {{anchorencode:x<nowiki/>y}} -!! html +!! html/php <p>xy </p> +!! html/parsoid +<p about="#mwt2" typeof="mw:Transclusion" data-parsoid='{"pi":[[]]}' data-mw='{"parts":[{"template":{"target":{"wt":"anchorencode:x<nowiki/>y","function":"anchorencode"},"params":{},"i":0}}]}'>xy</p> !! end !! test @@ -24194,17 +24524,17 @@ new support for bdi element (T33817) Ignore pipe between table row attributes !! wikitext {| -| quux +|quux |- id=foo | style='color: red' -| bar +|bar |} !! html <table> <tr> -<td> quux +<td>quux </td></tr> <tr id="foo" style="color: red"> -<td> bar +<td>bar </td></tr></table> !! end @@ -24219,14 +24549,45 @@ Language parser function !! end !!test +Padleft and padright (default 0-padding) +!! wikitext +{{padleft:xyz|5}} +{{padright:xyz|5}} +!! html/php +<p>00xyz +xyz00 +</p> +!! html/parsoid +<p><span typeof="mw:Transclusion" data-mw='{"parts":[{"template":{"target":{"wt":"padleft:xyz","function":"padleft"},"params":{"1":{"wt":"5"}},"i":0}}]}'>00xyz</span> +<span typeof="mw:Transclusion" data-mw='{"parts":[{"template":{"target":{"wt":"padright:xyz","function":"padright"},"params":{"1":{"wt":"5"}},"i":0}}]}'>xyz00</span></p> +!! end + +!!test +Padleft and padright (partial fill) +!! wikitext +{{padleft:xyz|6|ab}} +{{padright:xyz|6|ab}} +!! html/php +<p>abaxyz +xyzaba +</p> +!! html/parsoid +<p><span typeof="mw:Transclusion" data-mw='{"parts":[{"template":{"target":{"wt":"padleft:xyz","function":"padleft"},"params":{"1":{"wt":"6"},"2":{"wt":"ab"}},"i":0}}]}'>abaxyz</span> +<span typeof="mw:Transclusion" data-mw='{"parts":[{"template":{"target":{"wt":"padright:xyz","function":"padright"},"params":{"1":{"wt":"6"},"2":{"wt":"ab"}},"i":0}}]}'>xyzaba</span></p> +!! end + +!!test Padleft and padright as substr !! wikitext {{padleft:|3|abcde}} {{padright:|3|abcde}} -!! html +!! html/php <p>abc abc </p> +!! html/parsoid +<p><span typeof="mw:Transclusion" data-mw='{"parts":[{"template":{"target":{"wt":"padleft:","function":"padleft"},"params":{"1":{"wt":"3"},"2":{"wt":"abcde"}},"i":0}}]}'>abc</span> +<span typeof="mw:Transclusion" data-mw='{"parts":[{"template":{"target":{"wt":"padright:","function":"padright"},"params":{"1":{"wt":"3"},"2":{"wt":"abcde"}},"i":0}}]}'>abc</span></p> !! end !! test @@ -24261,7 +24622,7 @@ T36939 - Case insensitive link parsing ([HttP://]) <p><a rel="nofollow" class="external autonumber" href="HttP://MediaWiki.Org/">[1]</a> </p> !! html/parsoid -<p><a rel="mw:ExtLink" href="HttP://MediaWiki.Org/"></a></p> +<p><a rel="mw:ExtLink" class="external autonumber" href="HttP://MediaWiki.Org/"></a></p> !! end !!test @@ -24281,7 +24642,7 @@ HttP://MediaWiki.Org/ <p><a rel="nofollow" class="external free" href="HttP://MediaWiki.Org/">HttP://MediaWiki.Org/</a> </p> !! html/parsoid -<p><a rel="mw:ExtLink" href="HttP://MediaWiki.Org/">HttP://MediaWiki.Org/</a></p> +<p><a rel="mw:ExtLink" class="external free" href="HttP://MediaWiki.Org/">HttP://MediaWiki.Org/</a></p> !! end !!test @@ -24290,11 +24651,11 @@ Disable TOC notoc !! wikitext Lead -== Section 1 == -== Section 2 == -== Section 3 == -== Section 4 == -== Section 5 == +==Section 1== +==Section 2== +==Section 3== +==Section 4== +==Section 5== !! html <p>Lead </p> @@ -24397,9 +24758,7 @@ parsoid=wt2html,wt2wt !! wikitext '''<small>[[Image:Foobar.jpg|right|300px]]</small>''' !! html/parsoid -<p><b><small></small></b></p> -<figure class="mw-halign-right" typeof="mw:Image"><a href="./File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/thumb/3/3a/Foobar.jpg/300px-Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="34" width="300"/></a></figure> -<p></p> +<b><small><figure class="mw-halign-right" typeof="mw:Image"><a href="./File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/thumb/3/3a/Foobar.jpg/300px-Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="34" width="300"/></a></figure></small></b> !! end #### ---------------------------------------------------------------- @@ -24466,6 +24825,7 @@ Empty LI and TR nodes should not be stripped from top-level content * a * * b + {| |- |- @@ -24641,17 +25001,60 @@ Headings: 4b. No escaping needed (inside p-tags) !! options parsoid=html2wt !! html/parsoid -<p>=== -=foo= x +<p>=foo= x =foo= <s></s> </p> !! wikitext -=== =foo= x =foo= <s></s> +!! html/php +<p>=foo= x +=foo= <s></s> +</p> !!end !! test +Headings: 4c. Short headings (1) +!! options +parsoid=html2wt +!! html/parsoid +<p>=== +</p> +!! wikitext +<nowiki>===</nowiki> +!! html/php +<p>=== +</p> +!! end + +# in the html2wt direction we emit '= = =' or '=<nowiki>=</nowiki>=' +!! test +Headings: 4d. Short headings (2) +!! options +parsoid=wt2html,html2html +!! wikitext += +== +=== +==== +===== +!! html/php +<p>= +== +</p> +<h1><span class="mw-headline" id=".3D">=</span><span class="mw-editsection"><span class="mw-editsection-bracket">[</span><a href="/index.php?title=Parser_test&action=edit&section=1" title="Edit section: =">edit</a><span class="mw-editsection-bracket">]</span></span></h1> +<h1><span class="mw-headline" id=".3D.3D">==</span><span class="mw-editsection"><span class="mw-editsection-bracket">[</span><a href="/index.php?title=Parser_test&action=edit&section=2" title="Edit section: ==">edit</a><span class="mw-editsection-bracket">]</span></span></h1> +<h2><span class="mw-headline" id=".3D_2">=</span><span class="mw-editsection"><span class="mw-editsection-bracket">[</span><a href="/index.php?title=Parser_test&action=edit&section=3" title="Edit section: =">edit</a><span class="mw-editsection-bracket">]</span></span></h2> + +!! html/parsoid +<p>= +==</p> +<h1 id="="><span id=".3D" typeof="mw:FallbackId"></span>=</h1> +<h1 id="=="><span id=".3D.3D" typeof="mw:FallbackId"></span>==</h1> +<h2 id="=_2"><span id=".3D_2" typeof="mw:FallbackId"></span>=</h2> +!! end + +!! test Headings: 5. Empty headings !! options parsoid=html2wt @@ -24777,6 +25180,21 @@ a !! end +!! test +Headings: Used as horizontal rule +!! config +wgFragmentMode=[ 'html5', 'legacy' ] +!! options +parsoid=wt2html +!! wikitext +=============== +!! html/php +<h6><span id=".3D.3D.3D"></span><span class="mw-headline" id="===">===</span><span class="mw-editsection"><span class="mw-editsection-bracket">[</span><a href="/index.php?title=Parser_test&action=edit&section=1" title="Edit section: ===">edit</a><span class="mw-editsection-bracket">]</span></span></h6> + +!! html/parsoid +<h6 id="==="><span id=".3D.3D.3D" typeof="mw:FallbackId"></span>===</h6> +!! end + #### --------------- Lists --------------- #### 0. Outside nests (*foo, etc.) #### 1. Nested inside html <ul><li>*foo</li></ul> @@ -25083,15 +25501,12 @@ parsoid=html2wt |} !! html/php+tidy <table> +<tbody><tr> +<td>foo|bar +</td></tr> <tr> -<td>foo|bar</td> -</tr> -<tr> -<td>x -<div>a|b</div> -</td> -</tr> -</table> +<td>x<div>a|b</div> +</td></tr></tbody></table> !! end !! test @@ -25366,9 +25781,9 @@ parsoid=html2wt !! html/php <table> <tr> -<td> <foo +<td><foo </td> -<td> bar> +<td>bar> </td></tr></table> !! end @@ -25598,9 +26013,9 @@ Links 8. Add <nowiki/>s between text-nodes and RFC-links when required (T66300) !! options parsoid=html2wt !! html/parsoid -<p><a href="//tools.ietf.org/html/rfc123" rel="mw:ExtLink" data-parsoid='{"stx":"magiclink"}'>RFC 123</a>4 -<a href="//tools.ietf.org/html/rfc123" rel="mw:ExtLink" data-parsoid='{"stx":"magiclink"}'>RFC 123</a>y -X<a href="//tools.ietf.org/html/rfc123" rel="mw:ExtLink" data-parsoid='{"stx":"magiclink"}'>RFC 123</a>y</p> +<p><a href="https://tools.ietf.org/html/rfc123" rel="mw:ExtLink" data-parsoid='{"stx":"magiclink"}'>RFC 123</a>4 +<a href="https://tools.ietf.org/html/rfc123" rel="mw:ExtLink" data-parsoid='{"stx":"magiclink"}'>RFC 123</a>y +X<a href="https://tools.ietf.org/html/rfc123" rel="mw:ExtLink" data-parsoid='{"stx":"magiclink"}'>RFC 123</a>y</p> !! wikitext RFC 123<nowiki/>4 RFC 123<nowiki/>y @@ -25612,18 +26027,18 @@ Links 9. Don't add spurious <nowiki/>s between text-nodes and RFC-links (T66300) !! options parsoid=html2wt !! html/parsoid -<p><a href="//tools.ietf.org/html/rfc123" rel="mw:ExtLink" data-parsoid='{"stx":"magiclink"}'>RFC 123</a>?foo -<a href="//tools.ietf.org/html/rfc123" rel="mw:ExtLink" data-parsoid='{"stx":"magiclink"}'>RFC 123</a>&foo --<a href="//tools.ietf.org/html/rfc123" rel="mw:ExtLink" data-parsoid='{"stx":"magiclink"}'>RFC 123</a>- +<p><a href="https://tools.ietf.org/html/rfc123" rel="mw:ExtLink" data-parsoid='{"stx":"magiclink"}'>RFC 123</a>?foo +<a href="https://tools.ietf.org/html/rfc123" rel="mw:ExtLink" data-parsoid='{"stx":"magiclink"}'>RFC 123</a>&foo +-<a href="https://tools.ietf.org/html/rfc123" rel="mw:ExtLink" data-parsoid='{"stx":"magiclink"}'>RFC 123</a>- </p> !! wikitext RFC 123?foo RFC 123&foo -RFC 123- !! html/php -<p><a class="external mw-magiclink-rfc" rel="nofollow" href="//tools.ietf.org/html/rfc123">RFC 123</a>?foo -<a class="external mw-magiclink-rfc" rel="nofollow" href="//tools.ietf.org/html/rfc123">RFC 123</a>&foo --<a class="external mw-magiclink-rfc" rel="nofollow" href="//tools.ietf.org/html/rfc123">RFC 123</a>- +<p><a class="external mw-magiclink-rfc" rel="nofollow" href="https://tools.ietf.org/html/rfc123">RFC 123</a>?foo +<a class="external mw-magiclink-rfc" rel="nofollow" href="https://tools.ietf.org/html/rfc123">RFC 123</a>&foo +-<a class="external mw-magiclink-rfc" rel="nofollow" href="https://tools.ietf.org/html/rfc123">RFC 123</a>- </p> !! end @@ -26200,8 +26615,8 @@ parsoid=wt2html,html2html !! wikitext <div title="Hello world />Foo !! html/php+tidy -<div title="Hello world"></div> -<p>Foo</p> +<div title="Hello world"></div><p>Foo +</p> !! html/parsoid <div title="Hello world " data-parsoid='{"stx":"html","selfClose":true}'></div><p>Foo</p> !! end @@ -26258,12 +26673,12 @@ parsoid=wt2html,html2html Accept empty td cell attribute !! wikitext {| -| align="center" | foo || | +| align="center" |foo|| | |} !! html <table> <tr> -<td align="center"> foo </td> +<td align="center">foo</td> <td> </td></tr></table> @@ -26273,13 +26688,13 @@ Accept empty td cell attribute Non-empty attributes in th-cells !! wikitext {| -! Foo !! style="color: red" | Bar +!Foo!! style="color: red" |Bar |} !! html <table> <tr> -<th> Foo </th> -<th style="color: red"> Bar +<th>Foo</th> +<th style="color: red">Bar </th></tr></table> !!end @@ -26288,13 +26703,13 @@ Non-empty attributes in th-cells Accept empty attributes in th-cells !! wikitext {| -!| foo !!| bar +!|foo!!|bar |} !! html <table> <tr> -<th> foo </th> -<th> bar +<th>foo</th> +<th>bar </th></tr></table> !!end @@ -26303,17 +26718,17 @@ Accept empty attributes in th-cells Empty table rows go away !! wikitext {| -| Hello -| there +|Hello +|there |- class="foo" |- |} !! html <table> <tr> -<td> Hello +<td>Hello </td> -<td> there +<td>there </td></tr> </table> @@ -26343,11 +26758,9 @@ RT-ed inter-element separators should be valid separators </tbody></table> !!end -# Parsoid-only since PHP parser relies on Tidy for correct output +# Parsoid-only test of a DOM pass !!test Trailing newlines in a deep dom-subtree that ends a wikitext line should be migrated out -!!options -parsoid !! wikitext {| |<small>foo @@ -26357,7 +26770,7 @@ bar {| |<small>foo<small> |} -!! html +!! html/parsoid <table> <tbody><tr data-parsoid='{"autoInsertedEnd":true,"autoInsertedStart":true}'><td data-parsoid='{"autoInsertedEnd":true}'><small data-parsoid='{"stx":"html","autoInsertedEnd":true}'>foo <p>bar</p></small></td></tr> @@ -26457,13 +26870,13 @@ Indent and comment before table row !! wikitext {| <!--hi-->|- - | there + |there |} !! html/php <table> <tr> -<td> there +<td>there </td></tr></table> !! html/parsoid @@ -27237,7 +27650,7 @@ Image: upright option is ignored on inline and frame images (parsoid) !! wikitext [[File:Foobar.jpg|500x500px|upright=0.5|caption]] !! html/parsoid -<p><span typeof="mw:Image" data-mw='{"caption":"caption"}'><a href="./File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/thumb/3/3a/Foobar.jpg/500px-Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="57" width="500"/></a></span></p> +<p><figure-inline typeof="mw:Image" data-mw='{"caption":"caption"}'><a href="./File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/thumb/3/3a/Foobar.jpg/500px-Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="57" width="500"/></a></figure-inline></p> !! end !! test @@ -27245,7 +27658,7 @@ Image: in template parameter with empty parameter !! wikitext {{echo|[[File:Foobar.jpg|link=]]}} !! html/parsoid -<p><span class="mw-default-size" typeof="mw:Transclusion mw:Image" about="#mwt1" data-mw='{"parts":[{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"[[File:Foobar.jpg|link=]]"}},"i":0}}]}'><span><img resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="220" width="1941"/></span></span></p> +<p><figure-inline class="mw-default-size" typeof="mw:Transclusion mw:Image" about="#mwt1" data-mw='{"parts":[{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"[[File:Foobar.jpg|link=]]"}},"i":0}}]}'><span><img resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="220" width="1941"/></span></figure-inline></p> !! end !! test @@ -27298,7 +27711,7 @@ Image: Invalid title as link <p><a href="/wiki/File:Foobar.jpg" class="image" title="link=<"><img alt="link=<" src="http://example.com/images/3/3a/Foobar.jpg" width="1941" height="220" /></a> </p> !! html/parsoid -<p><span class="mw-default-size" typeof="mw:Image" data-parsoid='{"optList":[{"ck":"link","ak":"link=<"}]}' data-mw='{"caption":"link=&lt;"}'><a href="./File:Foobar.jpg" data-parsoid='{"a":{"href":"./File:Foobar.jpg"},"sa":{}}'><img resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="220" width="1941" data-parsoid='{"a":{"resource":"./File:Foobar.jpg","height":"220","width":"1941"},"sa":{"resource":"File:Foobar.jpg"}}'/></a></span></p> +<p><figure-inline class="mw-default-size" typeof="mw:Image" data-parsoid='{"optList":[{"ck":"link","ak":"link=<"}]}' data-mw='{"caption":"link=&lt;"}'><a href="./File:Foobar.jpg" data-parsoid='{"a":{"href":"./File:Foobar.jpg"},"sa":{}}'><img resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="220" width="1941" data-parsoid='{"a":{"resource":"./File:Foobar.jpg","height":"220","width":"1941"},"sa":{"resource":"File:Foobar.jpg"}}'/></a></figure-inline></p> !! end !! test @@ -27396,7 +27809,7 @@ parsoid=html2wt !! html/parsoid <ul><li>a<br>b</li><li>c</li></ul> !! wikitext -* a<br>b +* a<br />b * c !! end @@ -27884,9 +28297,9 @@ Edited RFC links not serializable as RFC links should serialize as extlinks !! options parsoid=html2wt !! html/parsoid -<a href="//tools.ietf.org/html/rfc123" rel="mw:ExtLink">New RFC</a> +<a href="https://tools.ietf.org/html/rfc123" rel="mw:ExtLink">New RFC</a> !! wikitext -[//tools.ietf.org/html/rfc123 New RFC] +[https://tools.ietf.org/html/rfc123 New RFC] !! end !! test @@ -27929,7 +28342,7 @@ WTS of autolinks with nowikis (round-trip) !! wikitext x<nowiki/>http://cscott.net<nowiki/>x !! html/parsoid -<p>x<a rel="mw:ExtLink" href="http://cscott.net">http://cscott.net</a>x</p> +<p>x<a rel="mw:ExtLink" class="external free" href="http://cscott.net">http://cscott.net</a>x</p> !! end # this is the "easy" test because it leaves in place all the @@ -27997,18 +28410,25 @@ Magic links inside links (not autolinked) [http://foo.com PMID 1234] [http://foo.com ISBN 123456789x] !! html+tidy -<p><a href="/wiki/Foo" title="Foo">http://example.com</a> <a href="/wiki/Foo" title="Foo">RFC 1234</a> <a href="/wiki/Foo" title="Foo">PMID 1234</a> <a href="/wiki/Foo" title="Foo">ISBN 123456789x</a></p> -<p><a rel="nofollow" class="external text" href="http://foo.com">http://example.com</a> <a rel="nofollow" class="external text" href="http://foo.com">RFC 1234</a> <a rel="nofollow" class="external text" href="http://foo.com">PMID 1234</a> <a rel="nofollow" class="external text" href="http://foo.com">ISBN 123456789x</a></p> +<p><a href="/wiki/Foo" title="Foo">http://example.com</a> +<a href="/wiki/Foo" title="Foo">RFC 1234</a> +<a href="/wiki/Foo" title="Foo">PMID 1234</a> +<a href="/wiki/Foo" title="Foo">ISBN 123456789x</a> +</p><p><a rel="nofollow" class="external text" href="http://foo.com">http://example.com</a> +<a rel="nofollow" class="external text" href="http://foo.com">RFC 1234</a> +<a rel="nofollow" class="external text" href="http://foo.com">PMID 1234</a> +<a rel="nofollow" class="external text" href="http://foo.com">ISBN 123456789x</a> +</p> !! html/parsoid <p><a rel="mw:WikiLink" href="./Foo" title="Foo">http://example.com</a> <a rel="mw:WikiLink" href="./Foo" title="Foo">RFC 1234</a> <a rel="mw:WikiLink" href="./Foo" title="Foo">PMID 1234</a> <a rel="mw:WikiLink" href="./Foo" title="Foo">ISBN 123456789x</a></p> -<p><a rel="mw:ExtLink" href="http://foo.com">http://example.com</a> -<a rel="mw:ExtLink" href="http://foo.com">RFC 1234</a> -<a rel="mw:ExtLink" href="http://foo.com">PMID 1234</a> -<a rel="mw:ExtLink" href="http://foo.com">ISBN 123456789x</a></p> +<p><a rel="mw:ExtLink" class="external text" href="http://foo.com">http://example.com</a> +<a rel="mw:ExtLink" class="external text" href="http://foo.com">RFC 1234</a> +<a rel="mw:ExtLink" class="external text" href="http://foo.com">PMID 1234</a> +<a rel="mw:ExtLink" class="external text" href="http://foo.com">ISBN 123456789x</a></p> !! end !! test @@ -28019,38 +28439,14 @@ Magic links inside image captions (autolinked) [[File:Foobar.jpg|thumb|PMID 1234]] [[File:Foobar.jpg|thumb|ISBN 123456789x]] !! html+tidy -<div class="thumb tright"> -<div class="thumbinner" style="width:182px;"><a href="/wiki/File:Foobar.jpg" class="image"><img alt="" src="http://example.com/images/thumb/3/3a/Foobar.jpg/180px-Foobar.jpg" width="180" height="20" class="thumbimage" srcset="http://example.com/images/thumb/3/3a/Foobar.jpg/270px-Foobar.jpg 1.5x, http://example.com/images/thumb/3/3a/Foobar.jpg/360px-Foobar.jpg 2x" /></a> -<div class="thumbcaption"> -<div class="magnify"><a href="/wiki/File:Foobar.jpg" class="internal" title="Enlarge"></a></div> -<a rel="nofollow" class="external free" href="http://example.com">http://example.com</a></div> -</div> -</div> -<div class="thumb tright"> -<div class="thumbinner" style="width:182px;"><a href="/wiki/File:Foobar.jpg" class="image"><img alt="" src="http://example.com/images/thumb/3/3a/Foobar.jpg/180px-Foobar.jpg" width="180" height="20" class="thumbimage" srcset="http://example.com/images/thumb/3/3a/Foobar.jpg/270px-Foobar.jpg 1.5x, http://example.com/images/thumb/3/3a/Foobar.jpg/360px-Foobar.jpg 2x" /></a> -<div class="thumbcaption"> -<div class="magnify"><a href="/wiki/File:Foobar.jpg" class="internal" title="Enlarge"></a></div> -<a class="external mw-magiclink-rfc" rel="nofollow" href="//tools.ietf.org/html/rfc1234">RFC 1234</a></div> -</div> -</div> -<div class="thumb tright"> -<div class="thumbinner" style="width:182px;"><a href="/wiki/File:Foobar.jpg" class="image"><img alt="" src="http://example.com/images/thumb/3/3a/Foobar.jpg/180px-Foobar.jpg" width="180" height="20" class="thumbimage" srcset="http://example.com/images/thumb/3/3a/Foobar.jpg/270px-Foobar.jpg 1.5x, http://example.com/images/thumb/3/3a/Foobar.jpg/360px-Foobar.jpg 2x" /></a> -<div class="thumbcaption"> -<div class="magnify"><a href="/wiki/File:Foobar.jpg" class="internal" title="Enlarge"></a></div> -<a class="external mw-magiclink-pmid" rel="nofollow" href="//www.ncbi.nlm.nih.gov/pubmed/1234?dopt=Abstract">PMID 1234</a></div> -</div> -</div> -<div class="thumb tright"> -<div class="thumbinner" style="width:182px;"><a href="/wiki/File:Foobar.jpg" class="image"><img alt="" src="http://example.com/images/thumb/3/3a/Foobar.jpg/180px-Foobar.jpg" width="180" height="20" class="thumbimage" srcset="http://example.com/images/thumb/3/3a/Foobar.jpg/270px-Foobar.jpg 1.5x, http://example.com/images/thumb/3/3a/Foobar.jpg/360px-Foobar.jpg 2x" /></a> -<div class="thumbcaption"> -<div class="magnify"><a href="/wiki/File:Foobar.jpg" class="internal" title="Enlarge"></a></div> -<a href="/wiki/Special:BookSources/123456789X" class="internal mw-magiclink-isbn">ISBN 123456789x</a></div> -</div> -</div> -!! html/parsoid -<figure class="mw-default-size" typeof="mw:Image/Thumb"><a href="./File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/thumb/3/3a/Foobar.jpg/220px-Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="25" width="220"/></a><figcaption><a rel="mw:ExtLink" href="http://example.com">http://example.com</a></figcaption></figure> -<figure class="mw-default-size" typeof="mw:Image/Thumb"><a href="./File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/thumb/3/3a/Foobar.jpg/220px-Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="25" width="220"/></a><figcaption><a href="//tools.ietf.org/html/rfc1234" rel="mw:ExtLink">RFC 1234</a></figcaption></figure> -<figure class="mw-default-size" typeof="mw:Image/Thumb"><a href="./File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/thumb/3/3a/Foobar.jpg/220px-Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="25" width="220"/></a><figcaption><a href="//www.ncbi.nlm.nih.gov/pubmed/1234?dopt=Abstract" rel="mw:ExtLink">PMID 1234</a></figcaption></figure> +<div class="thumb tright"><div class="thumbinner" style="width:182px;"><a href="/wiki/File:Foobar.jpg" class="image"><img alt="" src="http://example.com/images/thumb/3/3a/Foobar.jpg/180px-Foobar.jpg" width="180" height="20" class="thumbimage" srcset="http://example.com/images/thumb/3/3a/Foobar.jpg/270px-Foobar.jpg 1.5x, http://example.com/images/thumb/3/3a/Foobar.jpg/360px-Foobar.jpg 2x" /></a> <div class="thumbcaption"><div class="magnify"><a href="/wiki/File:Foobar.jpg" class="internal" title="Enlarge"></a></div><a rel="nofollow" class="external free" href="http://example.com">http://example.com</a></div></div></div> +<div class="thumb tright"><div class="thumbinner" style="width:182px;"><a href="/wiki/File:Foobar.jpg" class="image"><img alt="" src="http://example.com/images/thumb/3/3a/Foobar.jpg/180px-Foobar.jpg" width="180" height="20" class="thumbimage" srcset="http://example.com/images/thumb/3/3a/Foobar.jpg/270px-Foobar.jpg 1.5x, http://example.com/images/thumb/3/3a/Foobar.jpg/360px-Foobar.jpg 2x" /></a> <div class="thumbcaption"><div class="magnify"><a href="/wiki/File:Foobar.jpg" class="internal" title="Enlarge"></a></div><a class="external mw-magiclink-rfc" rel="nofollow" href="https://tools.ietf.org/html/rfc1234">RFC 1234</a></div></div></div> +<div class="thumb tright"><div class="thumbinner" style="width:182px;"><a href="/wiki/File:Foobar.jpg" class="image"><img alt="" src="http://example.com/images/thumb/3/3a/Foobar.jpg/180px-Foobar.jpg" width="180" height="20" class="thumbimage" srcset="http://example.com/images/thumb/3/3a/Foobar.jpg/270px-Foobar.jpg 1.5x, http://example.com/images/thumb/3/3a/Foobar.jpg/360px-Foobar.jpg 2x" /></a> <div class="thumbcaption"><div class="magnify"><a href="/wiki/File:Foobar.jpg" class="internal" title="Enlarge"></a></div><a class="external mw-magiclink-pmid" rel="nofollow" href="//www.ncbi.nlm.nih.gov/pubmed/1234?dopt=Abstract">PMID 1234</a></div></div></div> +<div class="thumb tright"><div class="thumbinner" style="width:182px;"><a href="/wiki/File:Foobar.jpg" class="image"><img alt="" src="http://example.com/images/thumb/3/3a/Foobar.jpg/180px-Foobar.jpg" width="180" height="20" class="thumbimage" srcset="http://example.com/images/thumb/3/3a/Foobar.jpg/270px-Foobar.jpg 1.5x, http://example.com/images/thumb/3/3a/Foobar.jpg/360px-Foobar.jpg 2x" /></a> <div class="thumbcaption"><div class="magnify"><a href="/wiki/File:Foobar.jpg" class="internal" title="Enlarge"></a></div><a href="/wiki/Special:BookSources/123456789X" class="internal mw-magiclink-isbn">ISBN 123456789x</a></div></div></div> +!! html/parsoid +<figure class="mw-default-size" typeof="mw:Image/Thumb"><a href="./File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/thumb/3/3a/Foobar.jpg/220px-Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="25" width="220"/></a><figcaption><a rel="mw:ExtLink" class="external free" href="http://example.com">http://example.com</a></figcaption></figure> +<figure class="mw-default-size" typeof="mw:Image/Thumb"><a href="./File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/thumb/3/3a/Foobar.jpg/220px-Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="25" width="220"/></a><figcaption><a href="https://tools.ietf.org/html/rfc1234" rel="mw:ExtLink" class="external text">RFC 1234</a></figcaption></figure> +<figure class="mw-default-size" typeof="mw:Image/Thumb"><a href="./File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/thumb/3/3a/Foobar.jpg/220px-Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="25" width="220"/></a><figcaption><a href="//www.ncbi.nlm.nih.gov/pubmed/1234?dopt=Abstract" rel="mw:ExtLink" class="external text">PMID 1234</a></figcaption></figure> <figure class="mw-default-size" typeof="mw:Image/Thumb"><a href="./File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/thumb/3/3a/Foobar.jpg/220px-Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="25" width="220"/></a><figcaption><a href="./Special:BookSources/123456789X" rel="mw:WikiLink">ISBN 123456789x</a></figcaption></figure> !! end @@ -28113,31 +28509,34 @@ parsoid=html2wt,wt2wt |<nowiki>- </nowiki> |- |<small>-</small> -|<br> +|<br /> - -|<br> +|<br /> - |} !! html/php+tidy <table> +<tbody><tr> +<th>- +</th> +<th>- +</th></tr> <tr> -<th>-</th> -<th>-</th> -</tr> -<tr> -<td>-</td> -<td>-</td> -</tr> +<td>- +</td> +<td>- +</td></tr> <tr> -<td><small>-</small></td> -<td><br /> -<p>-</p> +<td><small>-</small> </td> <td><br /> -<p>-</p> +<p>- +</p> </td> -</tr> -</table> +<td><br /> +<p>- +</p> +</td></tr></tbody></table> !! end !! test @@ -28149,17 +28548,17 @@ parsoid=html2wt <tbody> <tr><td>a b -</td><td data-parsoid='{"stx_v":"row"}'>c</td></tr> +</td><td data-parsoid='{"stx":"row"}'>c</td></tr> <tr><td><p>x</p> -</td><td data-parsoid='{"stx_v":"row", "startTagSrc": "{{!}}{{!}}"}'>y</td></tr> +</td><td data-parsoid='{"stx":"row", "startTagSrc": "{{!}}{{!}}"}'>y</td></tr> </tbody></table> <table> <tbody> <tr><th>a b -</th><th data-parsoid='{"stx_v":"row"}'>c</th></tr> +</th><th data-parsoid='{"stx":"row"}'>c</th></tr> <tr><th><p>x</h> -</th><th data-parsoid='{"stx_v":"row"}'>y</th></tr> +</th><th data-parsoid='{"stx":"row"}'>y</th></tr> </tbody></table> !! wikitext {| @@ -28321,7 +28720,15 @@ parsoid=wt2html !! wikitext {{echo|hi}}[http://example.com [[ho]]] !! html/parsoid -<p><span about="#mwt1" typeof="mw:Transclusion" data-mw='{"parts":[{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"hi"}},"i":0}}]}'>hi</span><a rel="mw:ExtLink" href="http://example.com"></a><a rel="mw:WikiLink" href="./Ho" title="Ho" data-parsoid='{"misnested":true}'>ho</a></p> +<p><span about="#mwt1" typeof="mw:Transclusion" data-mw='{"parts":[{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"hi"}},"i":0}}]}'>hi</span><a rel="mw:ExtLink" class="external autonumber" href="http://example.com"></a><a rel="mw:WikiLink" href="./Ho" title="Ho" data-parsoid='{"misnested":true}'>ho</a></p> +!! end + +!! test +Catch regression when unpacking with trailing content +!! wikitext +{{echo|Foo <references/> bar}} +!! html/parsoid +<p about="#mwt2" typeof="mw:Transclusion" data-mw='{"parts":[{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"Foo <references/> bar"}},"i":0}}]}'>Foo </p><ol class="mw-references references" typeof="mw:Extension/references" about="#mwt2" data-mw='{"name":"references","attrs":{}}'></ol><p about="#mwt2"> bar</p> !! end !! test @@ -28557,7 +28964,7 @@ parsoid={ !! html/parsoid <h2>foo<br/>bar</h2> !! wikitext -== foo<br> bar == +== foo<br /> bar == !! end !! test @@ -29088,6 +29495,64 @@ y }} !! end +!! test +New list is serialized on newlines +!! options +parsoid=html2wt +!! html/parsoid +<p>The quick brown fox jumps over the lazy dog.</p><ul> +<li>Yesterday</li> +<li>Today</li> +<li>Tomorrow</li> +</ul><p>The quick onyx goblin jumps over the lazy dwarf.</p> +!! wikitext +The quick brown fox jumps over the lazy dog. + +* Yesterday +* Today +* Tomorrow + +The quick onyx goblin jumps over the lazy dwarf. +!! end + +!! test +New lists in formatting elements serialized w/o newlines +!! options +parsoid=html2wt +!! html/parsoid +<small> + +<ul> +<li>123</li> +</ul> + +</small> + +<small><ul><li>hi</li></ul></small> +!! wikitext +<small> +* 123 +</small> + +<small> +* hi +</small> +!! end + +!! test +New list in table doesn't need newlines +!! options +parsoid=html2wt +!! html/parsoid +<table><tr><td><ul><li>test</li><li>123</li></td></tr></table> +!! wikitext +{| +| +* test +* 123 +|} +!! end + # --------------------------------------------------- # End of tests spec'ing wikitext serialization norms | # --------------------------------------------------- @@ -29267,17 +29732,15 @@ parsoid={ !! test Empty LI (T49673) !! wikitext -* a +*a * * -* b -!! html/php+tidy -<ul> -<li>a</li> +*b +!! html+tidy +<ul><li>a</li> <li class="mw-empty-elt"></li> <li class="mw-empty-elt"></li> -<li>b</li> -</ul> +<li>b</li></ul> !! end !! test @@ -29285,13 +29748,9 @@ Thumbnail output !! wikitext [[File:Thumb.png|thumb]] !! html/php+tidy -<div class="thumb tright"> -<div class="thumbinner" style="width:137px;"><a href="/wiki/File:Thumb.png" class="image"><img alt="Thumb.png" src="http://example.com/images/e/ea/Thumb.png" width="135" height="135" class="thumbimage" /></a> -<div class="thumbcaption"> -<div class="magnify"><a href="/wiki/File:Thumb.png" class="internal" title="Enlarge"></a></div> -</div> -</div> -</div> +<div class="thumb tright"><div class="thumbinner" style="width:137px;"><a href="/wiki/File:Thumb.png" class="image"><img alt="Thumb.png" src="http://example.com/images/e/ea/Thumb.png" width="135" height="135" class="thumbimage" /></a> <div class="thumbcaption"><div class="magnify"><a href="/wiki/File:Thumb.png" class="internal" title="Enlarge"></a></div></div></div></div> +!! html/parsoid +<figure class="mw-default-size" typeof="mw:Image/Thumb"><a href="./File:Thumb.png"><img resource="./File:Thumb.png" src="//example.com/images/e/ea/Thumb.png" data-file-width="135" data-file-height="135" data-file-type="bitmap" height="135" width="135"/></a></figure> !! end !! test @@ -29305,16 +29764,14 @@ unclosed internal link XSS (T137264) <p>[[#%3Cscript%3Ealert(1)%3C/script%3E|</p> !! end -# Use $wgRawHtml to inject a <style> tag, since you normally can't in wikitext -# (Parsoid doesn't support $wgRawHtml==true) !! test Validating that <style> isn't eaten by tidy (T167349) !! options -wgRawHtml=1 +styletag=1 !! wikitext <div class="foo"> -<html><style>.foo::before { content: "<foo>"; }</style></html> -<html><style data-mw-foobar="baz">.foo::after { content: "<bar>"; }</style></html> +<style>.foo::before { content: "<foo>"; }</style> +<style data-mw-foobar="baz">.foo::after { content: "<bar>"; }</style> </div> !! html/php+tidy <div class="foo"> @@ -29324,9 +29781,107 @@ wgRawHtml=1 !! end !! test +Validating that <style> isn't wrapped in a paragraph (T186965) +!! options +styletag=1 +!! wikitext +A style tag, by itself or with other style/link tags, shouldn't be wrapped in a paragraph + +<style>.foo::before { content: "<foo>"; }</style> + +<style>.foo::before { content: "<foo>"; }</style> <link rel="foo" href="bar"/><style>.foo::before { content: "<foo>"; }</style> + +But if it's on a line with other content, let it be wrapped. + +<style>.foo::before { content: "<foo>"; }</style> bar + +foo <style>.foo::before { content: "<foo>"; }</style> + +foo <style>.foo::before { content: "<foo>"; }</style> bar + +And the same if we have non-paragraph-breaking whitespace + +foo +<style>.foo::before { content: "<foo>"; }</style> +bar +!! html/php +<p>A style tag, by itself or with other style/link tags, shouldn't be wrapped in a paragraph +</p> +<style>.foo::before { content: "<foo>"; }</style> +<style>.foo::before { content: "<foo>"; }</style> <link rel="foo" href="bar"/><style>.foo::before { content: "<foo>"; }</style> +<p>But if it's on a line with other content, let it be wrapped. +</p><p><style>.foo::before { content: "<foo>"; }</style> bar +</p><p>foo <style>.foo::before { content: "<foo>"; }</style> +</p><p>foo <style>.foo::before { content: "<foo>"; }</style> bar +</p><p>And the same if we have non-paragraph-breaking whitespace +</p><p>foo +<style>.foo::before { content: "<foo>"; }</style> +bar +</p> +!! end + +!! test +Validating that <link> isn't wrapped in a paragraph (T186965) +!! options +styletag=1 +!! wikitext +A link tag, by itself or with other style/link tags, shouldn't be wrapped in a paragraph + +<link rel="foo" href="bar"/> + +<link rel="foo" href="bar"/> <style>.foo::before { content: "<foo>"; }</style><link rel="foo" href="bar"/> + +But if it's on a line with other content, let it be wrapped. + +<link rel="foo" href="bar"/> bar + +foo <link rel="foo" href="bar"/> + +foo <link rel="foo" href="bar"/> bar + +And the same if we have non-paragraph-breaking whitespace + +foo +<link rel="foo" href="bar"/> +bar +!! html/php +<p>A link tag, by itself or with other style/link tags, shouldn't be wrapped in a paragraph +</p> +<link rel="foo" href="bar"/> +<link rel="foo" href="bar"/> <style>.foo::before { content: "<foo>"; }</style><link rel="foo" href="bar"/> +<p>But if it's on a line with other content, let it be wrapped. +</p><p><link rel="foo" href="bar"/> bar +</p><p>foo <link rel="foo" href="bar"/> +</p><p>foo <link rel="foo" href="bar"/> bar +</p><p>And the same if we have non-paragraph-breaking whitespace +</p><p>foo +<link rel="foo" href="bar"/> +bar +</p> +!! end + +!! test Decoding of HTML entities in headings and links for IDs and link fragments (T103714) +!! config +wgFragmentMode=[ 'html5', 'legacy' ] +!! wikitext +==A&B&C&amp;D&amp;amp;E== +[[#A&B&C&amp;D&amp;amp;E]] +!! html/php +<h2><span id="A.26B.26C.26amp.3BD.26amp.3Bamp.3BE"></span><span class="mw-headline" id="A&B&C&amp;D&amp;amp;E">A&B&C&amp;D&amp;amp;E</span><span class="mw-editsection"><span class="mw-editsection-bracket">[</span><a href="/index.php?title=Parser_test&action=edit&section=1" title="Edit section: A&B&C&amp;D&amp;amp;E">edit</a><span class="mw-editsection-bracket">]</span></span></h2> +<p><a href="#A&B&C&amp;D&amp;amp;E">#A&B&C&amp;D&amp;amp;E</a> +</p> +!! html/parsoid +<h2 id="A&B&C&amp;D&amp;amp;E"><span id="A.26B.26C.26amp.3BD.26amp.3Bamp.3BE" typeof="mw:FallbackId" data-parsoid="{}"></span>A&B<span typeof="mw:Entity" data-parsoid='{"src":"&amp;","srcContent":"&"}'>&</span>C<span typeof="mw:Entity" data-parsoid='{"src":"&amp;","srcContent":"&"}'>&</span>amp;D<span typeof="mw:Entity" data-parsoid='{"src":"&amp;","srcContent":"&"}'>&</span>amp;amp;E</h2> +<p><a rel="mw:WikiLink" href="./Main_Page#A&B&C&amp;D&amp;amp;E" data-parsoid='{"stx":"simple","a":{"href":"./Main_Page#A&B&C&amp;D&amp;amp;E"},"sa":{"href":"#A&B&amp;C&amp;amp;D&amp;amp;amp;E"}}'>#A&B&C&amp;D&amp;amp;E</a></p> +!! end + +!! test +Decoding of HTML entities in headings and links for IDs and link fragments (T103714) (legacy) +!! config +wgFragmentMode=[ 'legacy' ] !! wikitext -== A&B&C&amp;D&amp;amp;E == +==A&B&C&amp;D&amp;amp;E== [[#A&B&C&amp;D&amp;amp;E]] !! html/php <h2><span class="mw-headline" id="A.26B.26C.26amp.3BD.26amp.3Bamp.3BE">A&B&C&amp;D&amp;amp;E</span><span class="mw-editsection"><span class="mw-editsection-bracket">[</span><a href="/index.php?title=Parser_test&action=edit&section=1" title="Edit section: A&B&C&amp;D&amp;amp;E">edit</a><span class="mw-editsection-bracket">]</span></span></h2> @@ -29335,32 +29890,61 @@ Decoding of HTML entities in headings and links for IDs and link fragments (T103 !! end !! test +Decoding of HTML entities in embedded HTML tags +!! wikitext +<table class="1&2&3&amp;4&amp;amp;5"><tr><td>x</td></tr></table> +!! html/php +<table class="1&2&3&amp;4&amp;amp;5"><tr><td>x</td></tr></table> + +!! html/parsoid +<table class="1&2&3&amp;4&amp;amp;5" data-parsoid='{"stx":"html","a":{"class":"1&2&3&amp;4&amp;amp;5"},"sa":{"class":"1&2&amp;3&amp;amp;4&amp;amp;amp;5"}}'><tbody><tr data-parsoid='{"stx":"html"}'><td data-parsoid='{"stx":"html"}'>x</td></tr></tbody></table> +!! end + +!! test Decoding of HTML entities in indicator names for IDs (T104196) !! options +parsoid=wt2html,html2html showindicators !! wikitext <indicator name="1&2&3&amp;4&amp;amp;5">Indicator</indicator> !! html/php 1&2&3&4&amp;5=Indicator +!! html/parsoid +<p><span typeof="mw:Extension/indicator" about="#mwt3" data-mw='{"name":"indicator","attrs":{"name":"1&2&3&amp;4&amp;amp;5"},"body":{"extsrc":"Indicator"}}'></span></p> +!! end + +# this version of the test strips out the ambiguity so Parsoid rts cleanly +!! test +Decoding of HTML entities in indicator names for IDs (unambiguous) (T104196) +!! options +showindicators +!! wikitext +<indicator name="1&2&3&amp;4&amp;amp;5">Indicator</indicator> +!! html/php +1&2&3&4&amp;5=Indicator + +!! html/parsoid +<p><span typeof="mw:Extension/indicator" about="#mwt3" data-mw='{"name":"indicator","attrs":{"name":"1&2&3&amp;4&amp;amp;5"},"body":{"extsrc":"Indicator"}}'></span></p> !! end +# This fragment mode is what Parsoid supports. !! test HTML5 ids: fallback to legacy !! config wgFragmentMode=[ 'html5', 'legacy' ] !! wikitext -== Foo bar == +==Foo bar== -== foo Bar == +==foo Bar== -== Тест == +==Тест== -== Тест == +==Тест== -== тест == +==тест== -== Hey < # " > % : ' == +==Hey < # " > % : '== [[#Foo bar]] [[#foo Bar]] [[#Тест]] [[#тест]] [[#Hey < # " > % : ']] {{anchorencode:💩}} <span id="{{anchorencode:💩}}"></span> @@ -29369,7 +29953,7 @@ wgFragmentMode=[ 'html5', 'legacy' ] [[#啤酒]] [[#%E5%95%A4%E9%85%92]] !! html/php -<div id="toc" class="toc"><div class="toctitle"><h2>Contents</h2></div> +<div id="toc" class="toc"><div class="toctitle" lang="en" dir="ltr"><h2>Contents</h2></div> <ul> <li class="toclevel-1 tocsection-1"><a href="#Foo_bar"><span class="tocnumber">1</span> <span class="toctext">Foo bar</span></a></li> <li class="toclevel-1 tocsection-2"><a href="#foo_Bar_2"><span class="tocnumber">2</span> <span class="toctext">foo Bar</span></a></li> @@ -29390,24 +29974,43 @@ wgFragmentMode=[ 'html5', 'legacy' ] </p><p>💩 <span id="💩"></span> </p><p><a href="#啤酒">#啤酒</a> <a href="#啤酒">#啤酒</a> </p> +!! html/parsoid +<h2 id="Foo_bar">Foo bar</h2> + +<h2 id="foo_Bar_2">foo Bar</h2> + +<h2 id="Тест"><span id=".D0.A2.D0.B5.D1.81.D1.82" typeof="mw:FallbackId"></span>Тест</h2> + +<h2 id="Тест_2"><span id=".D0.A2.D0.B5.D1.81.D1.82_2" typeof="mw:FallbackId"></span>Тест</h2> + +<h2 id="тест"><span id=".D1.82.D0.B5.D1.81.D1.82" typeof="mw:FallbackId"></span>тест</h2> + +<h2 id="Hey_<_#_"_>_%_:_'"><span id="Hey_.3C_.23_.22_.3E_.25_:_.27" typeof="mw:FallbackId"></span>Hey < # " > %<span typeof="mw:DisplaySpace mw:Placeholder" data-parsoid='{"src":" ","isDisplayHack":true}'> </span>: '</h2> +<p><a rel="mw:WikiLink" href="./Main_Page#Foo_bar">#Foo bar</a> <a rel="mw:WikiLink" href="./Main_Page#foo_Bar">#foo Bar</a> <a rel="mw:WikiLink" href="./Main_Page#Тест">#Тест</a> <a rel="mw:WikiLink" href="./Main_Page#тест">#тест</a> <a rel="mw:WikiLink" href="./Main_Page#Hey_<_#_"_>_%_:_'" data-parsoid='{"stx":"simple","a":{"href":"./Main_Page#Hey_<_#_\"_>_%_:_'"},"sa":{"href":"#Hey < # \" > % : '"}}'>#Hey < # " > % : '</a></p> + +<p><span about="#mwt1" typeof="mw:Transclusion" data-mw='{"parts":[{"template":{"target":{"wt":"anchorencode:💩","function":"anchorencode"},"params":{},"i":0}}]}'>💩</span> <span id="💩" about="#mwt3" typeof="mw:ExpandedAttrs" data-mw='{"attribs":[[{"txt":"id"},{"html":"<span about=\"#mwt2\" typeof=\"mw:Transclusion\" data-parsoid='{\"pi\":[[]],\"dsr\":[178,197,null,null]}' data-mw='{\"parts\":[{\"template\":{\"target\":{\"wt\":\"anchorencode:💩\",\"function\":\"anchorencode\"},\"params\":{},\"i\":0}}]}'>💩</span>"}]]}'></span></p> + +<!-- These two links should produce identical HTML --> +<p><a rel="mw:WikiLink" href="./Main_Page#啤酒">#啤酒</a> <a rel="mw:WikiLink" href="./Main_Page#啤酒" data-parsoid='{"stx":"simple","a":{"href":"./Main_Page#啤酒"},"sa":{"href":"#%E5%95%A4%E9%85%92"}}'>#啤酒</a></p> !! end +# Parsoid doesn't support this mode !! test HTML5 ids: legacy with a fallback to modern !! config wgFragmentMode=[ 'legacy', 'html5' ] !! wikitext -== Foo bar == +==Foo bar== -== foo Bar == +==foo Bar== -== Тест == +==Тест== -== Тест == +==Тест== -== тест == +==тест== -== Hey < # " > % : ' == +==Hey < # " > % : '== [[#Foo bar]] [[#foo Bar]] [[#Тест]] [[#тест]] [[#Hey < # " > % : ']] {{anchorencode:💩}} <span id="{{anchorencode:💩}}"></span> @@ -29416,7 +30019,7 @@ wgFragmentMode=[ 'legacy', 'html5' ] [[#啤酒]] [[#%E5%95%A4%E9%85%92]] !! html/php -<div id="toc" class="toc"><div class="toctitle"><h2>Contents</h2></div> +<div id="toc" class="toc"><div class="toctitle" lang="en" dir="ltr"><h2>Contents</h2></div> <ul> <li class="toclevel-1 tocsection-1"><a href="#Foo_bar"><span class="tocnumber">1</span> <span class="toctext">Foo bar</span></a></li> <li class="toclevel-1 tocsection-2"><a href="#foo_Bar_2"><span class="tocnumber">2</span> <span class="toctext">foo Bar</span></a></li> @@ -29439,22 +30042,23 @@ wgFragmentMode=[ 'legacy', 'html5' ] </p> !! end +# Parsoid doesn't support this mode. !! test HTML5 ids: no legacy !! config wgFragmentMode=[ 'html5' ] !! wikitext -== Foo bar == +==Foo bar== -== foo Bar == +==foo Bar== -== Тест == +==Тест== -== Тест == +==Тест== -== тест == +==тест== -== Hey < # " > % : ' == +==Hey < # " > % : '== [[#Foo bar]] [[#foo Bar]] [[#Тест]] [[#тест]] [[#Hey < # " > % : ']] {{anchorencode:💩}} <span id="{{anchorencode:💩}}"></span> @@ -29463,7 +30067,7 @@ wgFragmentMode=[ 'html5' ] [[#啤酒]] [[#%E5%95%A4%E9%85%92]] !! html/php -<div id="toc" class="toc"><div class="toctitle"><h2>Contents</h2></div> +<div id="toc" class="toc"><div class="toctitle" lang="en" dir="ltr"><h2>Contents</h2></div> <ul> <li class="toclevel-1 tocsection-1"><a href="#Foo_bar"><span class="tocnumber">1</span> <span class="toctext">Foo bar</span></a></li> <li class="toclevel-1 tocsection-2"><a href="#foo_Bar_2"><span class="tocnumber">2</span> <span class="toctext">foo Bar</span></a></li> @@ -29485,3 +30089,741 @@ wgFragmentMode=[ 'html5' ] </p><p><a href="#啤酒">#啤酒</a> <a href="#啤酒">#啤酒</a> </p> !! end + +!! test +T90902: Normalize weird characters in section IDs +!! config +wgFragmentMode=[ 'html5', 'legacy' ] +!! wikitext +==Foo bar== +[[#Foo bar]] + +!! html/php +<h2><span class="mw-headline" id="Foo_bar">Foo bar</span><span class="mw-editsection"><span class="mw-editsection-bracket">[</span><a href="/index.php?title=Parser_test&action=edit&section=1" title="Edit section: Foo bar">edit</a><span class="mw-editsection-bracket">]</span></span></h2> +<p><a href="#Foo_bar">#Foo bar</a> +</p> +!! html/parsoid +<h2 id="Foo_bar"> Foo<span typeof="mw:Entity" data-parsoid='{"src":"&nbsp;","srcContent":" "}'> </span>bar </h2> +<p><a rel="mw:WikiLink" href="./Main_Page#Foo_bar" data-parsoid='{"stx":"simple","a":{"href":"./Main_Page#Foo_bar"},"sa":{"href":"#Foo&nbsp;bar"}}'>#Foo bar</a></p> +!! end + +!! test +T51672: Test for brackets in attributes of elements in external link texts +!! wikitext +[http://example.com/ link <span title="title with [brackets]">span</span>] +[http://example.com/ link <span title="title with [brackets]">span</span>] + +!! html/php +<p><a rel="nofollow" class="external text" href="http://example.com/">link <span title="title with [brackets]">span</span></a> +<a rel="nofollow" class="external text" href="http://example.com/">link <span title="title with [brackets]">span</span></a> +</p> +!! html/parsoid +<p><a rel="mw:ExtLink" class="external text" href="http://example.com/">link <span title="title with [brackets]">span</span></a> +<a rel="mw:ExtLink" class="external text" href="http://example.com/">link <span title="title with [brackets]" data-parsoid='{"stx":"html","a":{"title":"title with [brackets]"},"sa":{"title":"title with &#91;brackets&#93;"}}'>span</span></a></p> +!! end + +!! test +T72875: Test for brackets in attributes of elements in internal link texts +!! wikitext +[[Foo|link <span title="title with [[double brackets]]">span</span>]] +[[Foo|link <span title="title with [[double brackets]]">span</span>]] + +!! html/php +<p><a href="/wiki/Foo" title="Foo">link <span title="title with [[double brackets]]">span</span></a> +<a href="/wiki/Foo" title="Foo">link <span title="title with [[double brackets]]">span</span></a> +</p> +!! html/parsoid +<p><a rel="mw:WikiLink" href="./Foo" title="Foo">link <span title="title with [[double brackets]]">span</span></a> +<a rel="mw:WikiLink" href="./Foo" title="Foo">link <span title="title with [[double brackets]]" data-parsoid='{"stx":"html","a":{"title":"title with [[double brackets]]"},"sa":{"title":"title with &#91;&#91;double brackets&#93;&#93;"}}'>span</span></a></p> +!! end + +!! test +T179544: {{anchorencode:}} output should be always usable in links +!! config +wgFragmentMode=[ 'html5' ] +!! wikitext +<span id="{{anchorencode:[foo]}}"></span>[[#{{anchorencode:[foo]}}]] +!! html/php +<p><span id="[foo]"></span><a href="#[foo]">#[foo]</a> +</p> +!! html/parsoid +<p><span id="[foo]" about="#mwt3" typeof="mw:ExpandedAttrs" data-parsoid='{"stx":"html","a":{"id":"[foo]"},"sa":{"id":"{{anchorencode:[foo]}}"}}' data-mw='{"attribs":[[{"txt":"id"},{"html":"<span typeof=\"mw:Transclusion mw:Entity\" about=\"#mwt1\" data-parsoid='{\"srcContent\":\"[\",\"dsr\":[10,32,null,null],\"pi\":[[]]}' data-mw='{\"parts\":[{\"template\":{\"target\":{\"wt\":\"anchorencode:[foo]\",\"function\":\"anchorencode\"},\"params\":{},\"i\":0}}]}'>[</span><span about=\"#mwt1\" data-parsoid=\"{}\">foo</span><span typeof=\"mw:Entity\" about=\"#mwt1\" data-parsoid='{\"src\":\"&amp;#x5D;\",\"srcContent\":\"]\"}'>]</span>"}]]}'></span><a typeof="mw:ExpandedAttrs" about="#mwt4" rel="mw:WikiLink" href="./Main_Page#[foo]" data-parsoid='{"stx":"simple","a":{"href":"./Main_Page#[foo]"},"sa":{"href":"#{{anchorencode:[foo]}}"}}' data-mw='{"attribs":[[{"txt":"href"},{"html":"#<span typeof=\"mw:Transclusion mw:Entity\" about=\"#mwt2\" data-parsoid='{\"srcContent\":\"[\",\"dsr\":[44,66,null,null],\"pi\":[[]]}' data-mw='{\"parts\":[{\"template\":{\"target\":{\"wt\":\"anchorencode:[foo]\",\"function\":\"anchorencode\"},\"params\":{},\"i\":0}}]}'>[</span><span about=\"#mwt2\" data-parsoid=\"{}\">foo</span><span typeof=\"mw:Entity\" about=\"#mwt2\" data-parsoid='{\"src\":\"&amp;#x5D;\",\"srcContent\":\"]\"}'>]</span>"}]]}'>#[foo]</a></p> +!! end + +## ------------------------------ +## Parsoid section-wrapping tests +## ------------------------------ +!! test +Section wrapping for well-nested sections (no leading content) +!! options +parsoid={ + "wrapSections": true +} +!! wikitext +=1= +a + +=2= +b + +==2.1== +c + +==2.2== +d + +===2.2.1=== +e + +=3= +f +!! html/parsoid +<section data-mw-section-id="0"></section><section data-mw-section-id="1"><h1 id="1">1</h1> +<p>a</p> + +</section><section data-mw-section-id="2"><h1 id="2">2</h1> +<p>b</p> + +<section data-mw-section-id="3"><h2 id="2.1">2.1</h2> +<p>c</p> + +</section><section data-mw-section-id="4"><h2 id="2.2">2.2</h2> +<p>d</p> + +<section data-mw-section-id="5"><h3 id="2.2.1">2.2.1</h3> +<p>e</p> + +</section></section></section><section data-mw-section-id="6"><h1 id="3">3</h1> +<p>f</p> + +</section> +!! end + +!! test +Section wrapping for well-nested sections (with leading content) +!! options +parsoid={ + "wrapSections": true +} +!! wikitext +Para 1. + +Para 2 with a <div>nested in it</div> + +Para 3. + +=1= +a + +=2= +b + +==2.1== +c +!! html/parsoid +<section data-mw-section-id="0"><p>Para 1.</p> + +<p>Para 2 with a </p><div>nested in it</div> + +<p>Para 3.</p> + +</section><section data-mw-section-id="1"><h1 id="1">1</h1> +<p>a</p> + +</section><section data-mw-section-id="2"><h1 id="2">2</h1> +<p>b</p> + +<section data-mw-section-id="3"><h2 id="2.1">2.1</h2> +<p>c</p> + +</section></section> +!! end + +!! test +Section wrapping with template-generated sections (good nesting 1) +!! options +parsoid={ + "wrapSections": true +} +!! wikitext +=1= +a + +{{echo|1= +==1.1== +b +}} + +==1.2== +c + +=2= +d +!! html/parsoid +<section data-mw-section-id="0"></section><section data-mw-section-id="1"><h1 id="1">1</h1> +<p>a</p> + +<section data-mw-section-id="-1"><h2 about="#mwt1" typeof="mw:Transclusion" id="1.1" data-parsoid='{"dsr":[9,33,null,null],"pi":[[{"k":"1","named":true,"spc":["","","\n","\n"]}]]}' data-mw='{"parts":[{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"==1.1==\nb"}},"i":0}}]}'>1.1</h2><span about="#mwt1"> +</span><p about="#mwt1">b</p> +</section><section data-mw-section-id="3"><h2 id="1.2">1.2</h2> +<p>c</p> + +</section></section><section data-mw-section-id="4"><h1 id="2">2</h1> +<p>d</p></section> +!! end + +# In this example, the template scope is mildly expanded to incorporate the +# trailing newline after the transclusion since that is part of section 1.1.1 +!! test +Section wrapping with template-generated sections (good nesting 2) +!! options +parsoid={ + "wrapSections": true, + "modes": ["wt2html", "wt2wt"] +} +!! wikitext +=1= +a + +{{echo|1= +==1.1== +b +===1.1.1=== +d +}} +=2= +e +!! html/parsoid +<section data-mw-section-id="0"></section><section data-mw-section-id="1"><h1 id="1">1</h1> +<p>a</p> + +<section data-mw-section-id="-1"><h2 about="#mwt1" typeof="mw:Transclusion" id="1.1" data-parsoid='{"dsr":[9,50,null,null],"pi":[[{"k":"1","named":true,"spc":["","","\n","\n"]}]]}' data-mw='{"parts":[{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"==1.1==\nb\n===1.1.1===\nd"}},"i":0}},"\n"]}'>1.1</h2><span about="#mwt1"> +</span><p about="#mwt1">b</p><span about="#mwt1"> +</span><section data-mw-section-id="-1" about="#mwt1"><h3 about="#mwt1" id="1.1.1">1.1.1</h3><span about="#mwt1"> +</span><p about="#mwt1">d</p><span about="#mwt1"> +</span></section></section></section><section data-mw-section-id="4" data-parsoid="{}"><h1 id="2">2</h1> +<p>e</p></section> +!! end + +# In this example, the template scope is mildly expanded to incorporate the +# trailing newline after the transclusion since that is part of section 1.2.1 +!! test +Section wrapping with template-generated sections (good nesting 3) +!! options +parsoid={ + "wrapSections": true, + "modes": ["wt2html", "wt2wt"] +} +!! wikitext +=1= +a + +{{echo|1= +x +==1.1== +b +==1.2== +c +===1.2.1=== +d +}} +=2= +e +!! html/parsoid +<section data-mw-section-id="0"></section><section data-mw-section-id="1" data-parsoid="{}"><h1 id="1"> 1 </h1> +<p>a</p> + +<p about="#mwt1" typeof="mw:Transclusion" data-parsoid='{"dsr":[9,60,0,0],"pi":[[{"k":"1","named":true,"spc":["","","\n","\n"]}]]}' data-mw='{"parts":[{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"x\n==1.1==\nb\n==1.2==\nc\n===1.2.1===\nd"}},"i":0}},"\n"]}'>x</p><span about="#mwt1"> +</span><section data-mw-section-id="-1" about="#mwt1"><h2 about="#mwt1" id="1.1">1.1</h2><span about="#mwt1"> +</span><p about="#mwt1">b</p><span about="#mwt1"> +</span></section><section data-mw-section-id="-1" about="#mwt1"><h2 about="#mwt1" id="1.2">1.2</h2><span about="#mwt1"> +</span><p about="#mwt1">c</p><span about="#mwt1"> +</span><section data-mw-section-id="-1" about="#mwt1"><h3 about="#mwt1" id="1.2.1">1.2.1</h3><span about="#mwt1"> +</span><p about="#mwt1">d</p><span about="#mwt1"> +</span></section></section></section><section data-mw-section-id="5"><h1 id="2">2</h1> +<p>e</p></section> +!! end + +# Because of section-wrapping and template-wrapping interactions, +# the scope of the template is expanded so that the template markup +# is valid in the presence of <section> tags. +# This exercises the s1 is null scenario in the wrapSections code +!! test +Section wrapping with template-generated sections (bad nesting 1) +!! options +parsoid={ + "wrapSections": true +} +!! wikitext +<div> +a + +{{echo| +=1= +b +}} + +c +</div> +!! html/parsoid +<section data-mw-section-id="-1"></section><section data-mw-section-id="-2"><div data-parsoid='{"stx":"html"}'> +<p>a</p> + +<span about="#mwt1" typeof="mw:Transclusion" data-parsoid='{"pi":[[{"k":"1"}]]}' data-mw='{"parts":[{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"\n=1=\nb\n"}},"i":0}},"\n\nc\n"]}'> +</span><section data-mw-section-id="-1" about="#mwt1"><h1 about="#mwt1" id="1">1</h1><span about="#mwt1"> +</span><p about="#mwt1">b +</p><span about="#mwt1"> + +</span><p about="#mwt1">c</p><span about="#mwt1"> +</span></section></div></section> +!! end + +# Because of section-wrapping and template-wrapping interactions, +# the scope of the template is expanded so that the template markup +# is valid in the presence of <section> tags. +# This exercises the s1 is ancestor of s2 scenario in the wrapSections code +!! test +Section wrapping with template-generated sections (bad nesting 2) +!! options +parsoid={ + "wrapSections": true +} +!! wikitext +=1= +a + +{{echo|1= +=2= +b +==2.1== +c +}} + +d + +=3= +e +!! html/parsoid +<section data-mw-section-id="0"></section><section data-mw-section-id="1"><h1 id="1">1</h1> +<p>a</p> + +</section><section data-mw-section-id="-1"><h1 about="#mwt1" typeof="mw:Transclusion" id="2" data-parsoid='{"dsr":[9,45,null,null],"pi":[[{"k":"1","named":true,"spc":["","","\n","\n"]}]]}' data-mw='{"parts":[{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"=2=\nb\n==2.1==\nc"}},"i":0}},"\n\nd\n\n"]}'>2</h1><span about="#mwt1"> +</span><p about="#mwt1">b</p><span about="#mwt1"> +</span><section data-mw-section-id="-1" about="#mwt1"><h2 about="#mwt1" id="2.1">2.1</h2><span about="#mwt1"> +</span><p about="#mwt1">c</p><span about="#mwt1"> + +</span><p about="#mwt1">d</p><span about="#mwt1"> + +</span></section></section><section data-mw-section-id="4"><h1 id="3">3</h1> +<p>e</p></section> +!! end + +# Because of section-wrapping and template-wrapping interactions, +# additional template wrappers are added to <section> tags +# so that template wrapping semantics are valid whether section +# tags are retained or stripped. But, the template scope can expand +# greatly when accounting for section tags. +# This exercises the s1 and s2 are in different subtrees scenario +!! test +Section wrapping with template-generated sections (bad nesting 3) +!! options +parsoid={ + "wrapSections": true, + "modes": ["wt2html", "wt2wt"] +} +!! wikitext +=1= +a + +{{echo|1= +==1.2== +b +=2= +c +}} + +d + +=3= +e +!! html/parsoid +<section data-mw-section-id="0"></section><section data-mw-section-id="1" about="#mwt1" typeof="mw:Transclusion" data-mw='{"parts":["=1=\na\n\n",{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"==1.2==\nb\n=2=\nc"}},"i":0}},"\n\nd\n\n"]}'><h1 id="1">1</h1> +<p>a</p> + +<section data-mw-section-id="-1"><h2 about="#mwt1" typeof="mw:Transclusion" id="1.2" data-mw='{"parts":[{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"==1.2==\nb\n=2=\nc"}},"i":0}}]}'>1.2</h2><span about="#mwt1"> +</span><p about="#mwt1">b</p><span about="#mwt1"> +</span></section></section><section data-mw-section-id="-1" about="#mwt1"><h1 about="#mwt1" id="2">2</h1><span about="#mwt1"> +</span><p about="#mwt1">c</p> + +<p>d</p> +</section><section data-mw-section-id="4" data-parsoid="{}"><h1 id="3">3</h1> +<p>e</p></section> +!! end + +!! test +Section wrapping with uneditable lead section + div wrapping multiple sections +!! options +parsoid={ + "wrapSections": true +} +!! wikitext +foo + +<div style="border:1px solid red;"> +=1= +a + +==1.1== +b + +=2= +c +</div> + +=3= +d + +==3.1== +e +!! html/parsoid +<section data-mw-section-id="-1"><p>foo</p> + +</section><section data-mw-section-id="-2"><div style="border:1px solid red;"> +<section data-mw-section-id="1"><h1 id="1">1</h1> +<p>a</p> + +<section data-mw-section-id="2"><h2 id="1.1">1.1</h2> +<p>b</p> + +</section></section><section data-mw-section-id="-1"><h1 id="2">2</h1> +<p>c</p> +</section></div> + +</section><section data-mw-section-id="4"><h1 id="3">3</h1> +<p>d</p> + +<section data-mw-section-id="5"><h2 id="3.1">3.1</h2> +<p>e</p> +</section></section> +!! end + +!! test +Section wrapping with editable lead section + div overlapping multiple sections +!! options +parsoid={ + "wrapSections": true +} +!! wikitext +foo + +=1= +a +<div style="border:1px solid red;"> +b + +==1.1== +c + +=2= +d +</div> +e + +=3= +f + +==3.1== +g +!! html/parsoid +<section data-mw-section-id="0"><p>foo</p> + +</section><section data-mw-section-id="-1"><h1 id="1">1</h1> +<p>a</p> +</section><section data-mw-section-id="-2"><div style="border:1px solid red;"> +<p>b</p> + +<section data-mw-section-id="2"><h2 id="1.1">1.1</h2> +<p>c</p> + +</section><section data-mw-section-id="-1"><h1 id="2">2</h1> +<p>d</p> +</section></div> +<p>e</p> + +</section><section data-mw-section-id="4"><h1 id="3">3</h1> +<p>f</p> + +<section data-mw-section-id="5"><h2 id="3.1">3.1</h2> +<p>g</p> +</section></section> +!! end + +!! test +HTML header tags should not be wrapped in section tags +!! options +parsoid={ + "wrapSections": true +} +!! wikitext +foo + +<h1>a</h1> + +=b= + +<h1>c</h1> + +=d= +!! html/parsoid +<section data-mw-section-id="0"><p>foo</p> + +<h1 id="a" data-parsoid='{"stx":"html"}'>a</h1> + +</section><section data-mw-section-id="1"><h1 id="b">b</h1> + +<h1 id="c" data-parsoid='{"stx":"html"}'>c</h1> + +</section><section data-mw-section-id="2"><h1 id="d">d</h1></section> +!! end + +!! test +Lead section containing only whitespace and comments. +!! options +parsoid={ + "wrapSections": true +} +!! wikitext + +<!-- this is a comment, presumably significant to editors --> +=1= +a + +=2= +b +!! html/parsoid +<section data-mw-section-id="0" data-parsoid="{}"> +<!-- this is a comment, presumably significant to editors --> +</section><section data-mw-section-id="1"><h1 id="1">1</h1> +<p>a</p> + +</section><section data-mw-section-id="2"><h1 id="2">2</h1> +<p>b</p></section> +!! end + +!! test +Pseudo-sections emitted by templates should have id -2 +!! options +parsoid={ + "wrapSections": true +} +!! wikitext +foo +{{echo|<div> +==a== +==b== +</div> +}} +!! html/parsoid +<section data-mw-section-id="-1"><p>foo</p> +</section><section data-mw-section-id="-2"><div about="#mwt1" typeof="mw:Transclusion" data-parsoid='{"stx":"html","pi":[[{"k":"1"}]]}' data-mw='{"parts":[{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"<div>\n==a==\n==b==\n</div>\n"}},"i":0}}]}'> +<section data-mw-section-id="-1"><h2 id="a">a</h2> +</section><section data-mw-section-id="-1"><h2 id="b">b</h2> +</section></div><span about="#mwt1"> +</span></section> +!! end + +########################################################################## +Tests demonstrating white-space insensitivity in input wikitext +for wikitext headings, wikitext list items, and wikitext table captions, +headings, and cells. HTML versions of the same should preserve whitespace. +########################################################################## +!! test +Trim whitespace in wikitext headings, list items, table captions, headings, and cells +!! wikitext +__NOTOC__ +== <!--c1--> <!--c2--> Spaces <!--c3--> <!--c4--> == +== <!--c2--> <!--c2--> Tabs <!--c3--><!--c4--> == +* <!--c1--> <!--c2--> List item <!--c3--> <!--c4--> +; <!--term to define--> term : <!--term's definition--> definition +{| +|+ <!--c1--> <!--c2--> Table Caption <!--c3--> <!--c4--> +|- +! <!--c1--> <!--c2--> Table Heading 1 <!--c3--> <!--c4--> !! Table Heading 2 <!--c5--> +|- +| <!--c1--> <!--c2--> Table Cell 1 <!--c3--> <!--c4--> || Table Cell 2 <!--c5--> +|- +| class="foo" || <!--c1--> <!--c2--> Table Cell 3 <!--c3--> <!--c4--> +|- +| <!--c1--> testing [[one|two]] <!--c2--> | <!--c3--> some content +|} +: {| + | <!--c1--> <!--c2--> Table Cell 1 <!--c3--> <!--c4--> || Table Cell 2 <!--c5--> + |} foo <!--c1--> +!! html/php+tidy +<h2><span class="mw-headline" id="Spaces">Spaces</span><span class="mw-editsection"><span class="mw-editsection-bracket">[</span><a href="/index.php?title=Parser_test&action=edit&section=1" title="Edit section: Spaces">edit</a><span class="mw-editsection-bracket">]</span></span></h2> +<h2><span class="mw-headline" id="Tabs">Tabs</span><span class="mw-editsection"><span class="mw-editsection-bracket">[</span><a href="/index.php?title=Parser_test&action=edit&section=2" title="Edit section: Tabs">edit</a><span class="mw-editsection-bracket">]</span></span></h2> +<ul><li>List item</li></ul> +<dl><dt>term </dt> +<dd>definition</dd></dl> +<table> +<caption>Table Caption +</caption> +<tbody><tr> +<th>Table Heading 1</th> +<th>Table Heading 2 +</th></tr> +<tr> +<td>Table Cell 1</td> +<td>Table Cell 2 +</td></tr> +<tr> +<td>class="foo"</td> +<td>Table Cell 3 +</td></tr> +<tr> +<td>testing <a href="/index.php?title=One&action=edit&redlink=1" class="new" title="One (page does not exist)">two</a> | some content +</td></tr></tbody></table> +<dl><dd><table> +<tbody><tr> +<td>Table Cell 1</td> +<td>Table Cell 2 +</td></tr></tbody></table> foo</dd></dl> +!! end + +# Looks like <caption> is not accepted in HTML +!! test +Do not trim whitespace in HTML headings, list items, table captions, headings, and cells +!! wikitext +__NOTOC__ +<h2> <!--c1--> <!--c2--> Heading <!--c3--> <!--c4--> </h2> +<ul><li> <!--c1--> <!--c2--> List item <!--c3--> <!--c4--> </li></ul> +<table> +<tr><th> <!--c1--> <!--c2--> Table Heading <!--c3--> <!--c4--> <th></tr> +<tr><td> <!--c1--> <!--c2--> Table Cell <!--c3--> <!--c4--> <th></tr> +</table> +!! html/php+tidy +<h2><span class="mw-headline" id="Heading"> Heading </span></h2> +<ul><li> List item </li></ul> +<table> +<tbody><tr><th> Table Heading </th><th></th></tr> +<tr><td> Table Cell </td><th></th></tr> +</tbody></table> +!! end + +!! test +Do not trim whitespace in links and quotes +!! wikitext +foo '' <!--c1--> italic <!--c2--> '' and ''' <!--c3--> bold <!--c4--> ''' +[[Foo| some text ]] +!! html/php+tidy +<p>foo <i> italic </i> and <b> bold </b> +<a href="/wiki/Foo" title="Foo"> some text </a> +</p> +!! end + +!! test +Remove p tags surrounding a single element in a figcaption +!! options +parsoid=html2wt +!! wikitext +[[File:Foobar.jpg|right|200x200px|Caption]] +!! html/parsoid +<figure class="mw-halign-right" typeof="mw:Image"><a href="./File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/thumb/3/3a/Foobar.jpg/200px-Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="23" width="200"/></a><figcaption><p>Caption</p></figcaption></figure> +!! end + +!! test +Selser preserves lack of newline before list and allows newline after the list +!! options +parsoid={ + "modes": ["selser"], + "scrubWikitext": true, + "changes": [ + [ "ul", "after", "<p>footer</p>" ] + ] +} +!! wikitext +header +*foo +*bar +!! wikitext/edited +header +*foo +*bar + +footer +!! end + + +!! test +Selser does not introduce newlines between unedited paragraph preceding the list +!! options +parsoid={ + "modes": ["selser"], + "changes": [ + [ "table tbody tr td p:last-child", "empty" ] + ] +} +!! wikitext +{| +| +header +*foo +*bar +footer +|} +!! wikitext/edited +{| +| +header +*foo +*bar + +|} +!! end + +!! test +Selser does not introduce newlines between unedited paragraph following the list +!! options +parsoid={ + "modes": ["selser"], + "changes": [ + [ "table tbody tr td p:first-child", "empty" ] + ] +} +!! wikitext +{| +| +header +*foo +*bar +footer +|} +!! wikitext/edited +{| +| + +*foo +*bar +footer +|} +!! end + +!! test +Remove a list item but do not insert newline above list +!! options +parsoid={ + "modes": ["selser"], + "changes": [ + [ "ul li:last-child", "remove" ] + ] +} +!! wikitext +header +*foo +*bar +footer +!! wikitext/edited +header +*foo +footer +!! end diff --git a/www/wiki/tests/parser/parserTestsParserHook.php b/www/wiki/tests/parser/parserTestsParserHook.php deleted file mode 100644 index 5bf50ead..00000000 --- a/www/wiki/tests/parser/parserTestsParserHook.php +++ /dev/null @@ -1,67 +0,0 @@ -<?php -/** - * A basic extension that's used by the parser tests to test whether input and - * arguments are passed to extensions properly. - * - * Copyright © 2005, 2006 Ævar Arnfjörð Bjarmason - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along - * with this program; if not, write to the Free Software Foundation, Inc., - * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - * http://www.gnu.org/copyleft/gpl.html - * - * @file - * @ingroup Testing - * @author Ævar Arnfjörð Bjarmason <avarab@gmail.com> - */ - -class ParserTestParserHook { - - static function setup( &$parser ) { - $parser->setHook( 'tag', [ __CLASS__, 'dumpHook' ] ); - $parser->setHook( 'tåg', [ __CLASS__, 'dumpHook' ] ); - $parser->setHook( 'statictag', [ __CLASS__, 'staticTagHook' ] ); - return true; - } - - static function dumpHook( $in, $argv ) { - return "<pre>\n" . - var_export( $in, true ) . "\n" . - var_export( $argv, true ) . "\n" . - "</pre>"; - } - - static function staticTagHook( $in, $argv, $parser ) { - if ( !count( $argv ) ) { - $parser->static_tag_buf = $in; - return ''; - } elseif ( count( $argv ) === 1 && isset( $argv['action'] ) - && $argv['action'] === 'flush' && $in === null - ) { - // Clear the buffer, we probably don't need to - if ( isset( $parser->static_tag_buf ) ) { - $tmp = $parser->static_tag_buf; - } else { - $tmp = ''; - } - $parser->static_tag_buf = null; - return $tmp; - } else { // wtf? - return - "\nCall this extension as <statictag>string</statictag> or as" . - " <statictag action=flush/>, not in any other way.\n" . - "text: " . var_export( $in, true ) . "\n" . - "argv: " . var_export( $argv, true ) . "\n"; - } - } -} diff --git a/www/wiki/tests/parserTests.php b/www/wiki/tests/parserTests.php deleted file mode 100644 index b3cb89ae..00000000 --- a/www/wiki/tests/parserTests.php +++ /dev/null @@ -1,93 +0,0 @@ -<?php -/** - * MediaWiki parser test suite - * - * Copyright © 2004 Brion Vibber <brion@pobox.com> - * https://www.mediawiki.org/ - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along - * with this program; if not, write to the Free Software Foundation, Inc., - * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - * http://www.gnu.org/copyleft/gpl.html - * - * @file - * @ingroup Testing - */ - -define( 'MW_PARSER_TEST', true ); - -$options = [ 'quick', 'color', 'quiet', 'help', 'show-output', - 'record', 'run-disabled', 'run-parsoid' ]; -$optionsWithArgs = [ 'regex', 'filter', 'seed', 'setversion', 'file' ]; - -require_once __DIR__ . '/../maintenance/commandLine.inc'; -require_once __DIR__ . '/TestsAutoLoader.php'; - -if ( isset( $options['help'] ) ) { - echo <<<ENDS -MediaWiki $wgVersion parser test suite -Usage: php parserTests.php [options...] - -Options: - --quick Suppress diff output of failed tests - --quiet Suppress notification of passed tests (shows only failed tests) - --show-output Show expected and actual output - --color[=yes|no] Override terminal detection and force color output on or off - use wgCommandLineDarkBg = true; if your term is dark - --regex Only run tests whose descriptions which match given regex - --filter Alias for --regex - --file=<testfile> Run test cases from a custom file instead of parserTests.txt - --record Record tests in database - --compare Compare with recorded results, without updating the database. - --setversion When using --record, set the version string to use (useful - with git-svn so that you can get the exact revision) - --keep-uploads Re-use the same upload directory for each test, don't delete it - --fuzz Do a fuzz test instead of a normal test - --seed <n> Start the fuzz test from the specified seed - --help Show this help message - --run-disabled run disabled tests - --run-parsoid run parsoid tests (normally disabled) - -ENDS; - exit( 0 ); -} - -# Cases of weird db corruption were encountered when running tests on earlyish -# versions of SQLite -if ( $wgDBtype == 'sqlite' ) { - $db = wfGetDB( DB_MASTER ); - $version = $db->getServerVersion(); - if ( version_compare( $version, '3.6' ) < 0 ) { - die( "Parser tests require SQLite version 3.6 or later, you have $version\n" ); - } -} - -$tester = new ParserTest( $options ); - -if ( isset( $options['file'] ) ) { - $files = [ $options['file'] ]; -} else { - // Default parser tests and any set from extensions or local config - $files = $wgParserTestFiles; -} - -# Print out software version to assist with locating regressions -$version = SpecialVersion::getVersion( 'nodb' ); -echo "This is MediaWiki version {$version}.\n\n"; - -if ( isset( $options['fuzz'] ) ) { - $tester->fuzzTest( $files ); -} else { - $ok = $tester->runTestsFromFiles( $files ); - exit( $ok ? 0 : 1 ); -} diff --git a/www/wiki/tests/phan/config.php b/www/wiki/tests/phan/config.php index 8a82d74c..71ebd6f4 100644 --- a/www/wiki/tests/phan/config.php +++ b/www/wiki/tests/phan/config.php @@ -1,7 +1,5 @@ <?php -use \Phan\Config; - // If xdebug is enabled, we need to increase the nesting level for phan ini_set( 'xdebug.max_nesting_level', 1000 ); @@ -40,10 +38,13 @@ return [ function_exists( 'tideways_enable' ) ? [] : [ 'tests/phan/stubs/tideways.php' ], class_exists( PEAR::class ) ? [] : [ 'tests/phan/stubs/mail.php' ], class_exists( Memcached::class ) ? [] : [ 'tests/phan/stubs/memcached.php' ], + // Per composer.json, PHPUnit 6 is used for PHP 7.0+, PHPUnit 4 otherwise. + // Load the interface for the version of PHPUnit that isn't installed. + // Phan only supports PHP 7.0+ (and not HHVM), so we only need to stub PHPUnit 4. + class_exists( PHPUnit_TextUI_Command::class ) ? [] : [ 'tests/phan/stubs/phpunit4.php' ], [ 'maintenance/7zip.inc', 'maintenance/backup.inc', - 'maintenance/backupPrefetch.inc', 'maintenance/cleanupTable.inc', 'maintenance/CodeCleanerGlobalsPass.inc', 'maintenance/commandLine.inc', @@ -297,20 +298,30 @@ return [ * to this black-list to inhibit them from being reported. */ 'suppress_issue_types' => [ + // approximate error count: 29 + "PhanCommentParamOnEmptyParamList", + // approximate error count: 33 + "PhanCommentParamWithoutRealParam", // approximate error count: 8 "PhanDeprecatedClass", // approximate error count: 415 "PhanDeprecatedFunction", // approximate error count: 25 "PhanDeprecatedProperty", + // approximate error count: 17 + "PhanNonClassMethodCall", // approximate error count: 11 "PhanParamReqAfterOpt", // approximate error count: 888 "PhanParamSignatureMismatch", // approximate error count: 7 "PhanParamSignatureMismatchInternal", + // approximate error count: 1 + "PhanParamSignatureRealMismatchTooFewParameters", // approximate error count: 125 "PhanParamTooMany", + // approximate error count: 1 + "PhanParamTooManyCallable", // approximate error count: 3 "PhanParamTooManyInternal", // approximate error count: 1 @@ -319,12 +330,28 @@ return [ "PhanTraitParentReference", // approximate error count: 3 "PhanTypeComparisonFromArray", + // approximate error count: 2 + "PhanTypeComparisonToArray", // approximate error count: 3 "PhanTypeInvalidRightOperand", + // approximate error count: 1 + "PhanTypeMagicVoidWithReturn", // approximate error count: 218 "PhanTypeMismatchArgument", // approximate error count: 13 "PhanTypeMismatchArgumentInternal", + // approximate error count: 6 + "PhanTypeMismatchDeclaredParam", + // approximate error count: 111 + "PhanTypeMismatchDeclaredParamNullable", + // approximate error count: 1 + "PhanTypeMismatchDefault", + // approximate error count: 5 + "PhanTypeMismatchDimAssignment", + // approximate error count: 2 + "PhanTypeMismatchDimEmpty", + // approximate error count: 1 + "PhanTypeMismatchDimFetch", // approximate error count: 14 "PhanTypeMismatchForeach", // approximate error count: 56 @@ -335,6 +362,8 @@ return [ "PhanTypeMissingReturn", // approximate error count: 5 "PhanTypeNonVarPassByRef", + // approximate error count: 1 + "PhanUndeclaredClassInCallable", // approximate error count: 32 "PhanUndeclaredConstant", // approximate error count: 233 @@ -343,6 +372,12 @@ return [ "PhanUndeclaredProperty", // approximate error count: 3 "PhanUndeclaredStaticMethod", + // approximate error count: 11 + "PhanUndeclaredTypeReturnType", + // approximate error count: 27 + "PhanUndeclaredVariable", + // approximate error count: 58 + "PhanUndeclaredVariableDim", ], /** diff --git a/www/wiki/tests/phan/stubs/hhvm.php b/www/wiki/tests/phan/stubs/hhvm.php index 79feaa00..364ebdaa 100644 --- a/www/wiki/tests/phan/stubs/hhvm.php +++ b/www/wiki/tests/phan/stubs/hhvm.php @@ -16,7 +16,7 @@ * http://www.gnu.org/copyleft/gpl.html */ -// @codingStandardsIgnoreFile +// phpcs:ignoreFile /** * @param callable $callback diff --git a/www/wiki/tests/phan/stubs/mail.php b/www/wiki/tests/phan/stubs/mail.php index 7cd90167..ba1efb96 100644 --- a/www/wiki/tests/phan/stubs/mail.php +++ b/www/wiki/tests/phan/stubs/mail.php @@ -3,7 +3,7 @@ /** * Minimal set of classes necessary for UserMailer to be happy. Types * taken from documentation at pear.php.net. - * @codingStandardsIgnoreFile + * phpcs:ignoreFile */ class PEAR { diff --git a/www/wiki/tests/phan/stubs/memcached.php b/www/wiki/tests/phan/stubs/memcached.php index ee47937a..0f8859d2 100644 --- a/www/wiki/tests/phan/stubs/memcached.php +++ b/www/wiki/tests/phan/stubs/memcached.php @@ -5,7 +5,7 @@ * that they are optional. Phan can not detect this and thus throws an error for a usage with * no params. So we have this small stub just for the constructor to allow no params. * @see https://secure.php.net/manual/en/memcached.construct.php - * @codingStandardsIgnoreFile + * phpcs:ignoreFile */ class Memcached { diff --git a/www/wiki/tests/phan/stubs/phpunit4.php b/www/wiki/tests/phan/stubs/phpunit4.php new file mode 100644 index 00000000..e5e88e6b --- /dev/null +++ b/www/wiki/tests/phan/stubs/phpunit4.php @@ -0,0 +1,11 @@ +<?php + +/** + * Some old classes from PHPUnit 4 that MediaWiki (conditionally) references. + * + * phpcs:ignoreFile + */ + +class PHPUnit_TextUI_Command { + +} diff --git a/www/wiki/tests/phan/stubs/tideways.php b/www/wiki/tests/phan/stubs/tideways.php index 1372219b..34ac735c 100644 --- a/www/wiki/tests/phan/stubs/tideways.php +++ b/www/wiki/tests/phan/stubs/tideways.php @@ -2,7 +2,7 @@ /** * Minimal set of classes necessary for Xhprof using tideways - * @codingStandardsIgnoreFile + * phpcs:ignoreFile */ function tideways_enable(){ diff --git a/www/wiki/tests/phan/stubs/wikidiff.php b/www/wiki/tests/phan/stubs/wikidiff.php index 1015b0b9..02bcd1fb 100644 --- a/www/wiki/tests/phan/stubs/wikidiff.php +++ b/www/wiki/tests/phan/stubs/wikidiff.php @@ -16,7 +16,7 @@ * http://www.gnu.org/copyleft/gpl.html */ -// @codingStandardsIgnoreFile +// phpcs:ignoreFile /** * @param string $text1 @@ -27,3 +27,13 @@ */ function wikidiff2_do_diff( $text1, $text2, $numContextLines, $movedParagraphDetectionCutoff = 0 ) { } + +/** + * @param string $text1 + * @param string $text2 + * @param int $numContextLines + * @param int $maxMovedLines + * @return string + */ +function wikidiff2_inline_diff( $text1, $text2, $numContextLines, $maxMovedLines = 25 ) { +} diff --git a/www/wiki/tests/phpunit/HamcrestPHPUnitIntegration.php b/www/wiki/tests/phpunit/HamcrestPHPUnitIntegration.php new file mode 100644 index 00000000..def08ff3 --- /dev/null +++ b/www/wiki/tests/phpunit/HamcrestPHPUnitIntegration.php @@ -0,0 +1,34 @@ +<?php +/** + * Copyright (C) 2018 Kunal Mehta <legoktm@member.fsf.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. + * + */ + +/** + * @since 1.31 + */ +trait HamcrestPHPUnitIntegration { + + /** + * Wrapper around Hamcrest's assertThat, which marks the assertion + * for PHPUnit so the test is not marked as risky + */ + public function assertThatHamcrest( /* ... */ ) { + call_user_func_array( 'assertThat', func_get_args() ); + $this->addToAssertionCount( 1 ); + } +} diff --git a/www/wiki/tests/phpunit/MediaWikiCoversValidator.php b/www/wiki/tests/phpunit/MediaWikiCoversValidator.php new file mode 100644 index 00000000..a79a139c --- /dev/null +++ b/www/wiki/tests/phpunit/MediaWikiCoversValidator.php @@ -0,0 +1,50 @@ +<?php +/** + * Copyright (C) 2017 Kunal Mehta <legoktm@member.fsf.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. + * + */ + +/** + * Trait that checks that covers tags are valid, since PHPUnit + * won't do it unless you run it with coverage, which is super + * slow. + * + * @since 1.31 + */ +trait MediaWikiCoversValidator { + + /** + * Test that all methods in this class that begin + * with "test" have valid covers tags. + */ + public function testValidCovers() { + $methods = get_class_methods( $this ); + $class = get_class( $this ); + $bad = ''; + foreach ( $methods as $method ) { + if ( strpos( $method, 'test' ) === 0 ) { + try { + PHPUnit_Util_Test::getLinesToBeCovered( $class, $method ); + } catch ( PHPUnit_Framework_CodeCoverageException $e ) { + $bad .= "$class::$method: {$e->getMessage()}\n"; + } + } + } + + $this->assertEquals( '', $bad ); + } +} diff --git a/www/wiki/tests/phpunit/MediaWikiPHPUnitTestListener.php b/www/wiki/tests/phpunit/MediaWikiPHPUnitTestListener.php index dd606d8a..0a162a28 100644 --- a/www/wiki/tests/phpunit/MediaWikiPHPUnitTestListener.php +++ b/www/wiki/tests/phpunit/MediaWikiPHPUnitTestListener.php @@ -11,7 +11,7 @@ class MediaWikiPHPUnitTestListener protected function getTestName( PHPUnit_Framework_Test $test ) { $name = get_class( $test ); - if ( $test instanceof PHPUnit_Framework_TestCase ) { + if ( $test instanceof PHPUnit\Framework\TestCase ) { $name .= '::' . $test->getName( true ); } diff --git a/www/wiki/tests/phpunit/MediaWikiTestCase.php b/www/wiki/tests/phpunit/MediaWikiTestCase.php index f04eec73..87ca9181 100644 --- a/www/wiki/tests/phpunit/MediaWikiTestCase.php +++ b/www/wiki/tests/phpunit/MediaWikiTestCase.php @@ -5,14 +5,19 @@ use MediaWiki\Logger\LoggerFactory; use MediaWiki\Logger\MonologSpi; use MediaWiki\MediaWikiServices; use Psr\Log\LoggerInterface; +use Wikimedia\Rdbms\IDatabase; use Wikimedia\Rdbms\IMaintainableDatabase; use Wikimedia\Rdbms\Database; +use Wikimedia\Rdbms\LBFactory; use Wikimedia\TestingAccessWrapper; /** * @since 1.18 */ -abstract class MediaWikiTestCase extends PHPUnit_Framework_TestCase { +abstract class MediaWikiTestCase extends PHPUnit\Framework\TestCase { + + use MediaWikiCoversValidator; + use PHPUnit4And6Compat; /** * The service locator created by prepareServices(). This service locator will @@ -256,20 +261,19 @@ abstract class MediaWikiTestCase extends PHPUnit_Framework_TestCase { * 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. */ - $hashCache = [ 'class' => 'HashBagOStuff', 'reportDupes' => false ]; + $hashCache = [ 'class' => HashBagOStuff::class, 'reportDupes' => false ]; $objectCaches = [ CACHE_DB => $hashCache, CACHE_ACCEL => $hashCache, CACHE_MEMCACHED => $hashCache, 'apc' => $hashCache, 'apcu' => $hashCache, - 'xcache' => $hashCache, 'wincache' => $hashCache, ] + $baseConfig->get( 'ObjectCaches' ); $defaultOverrides->set( 'ObjectCaches', $objectCaches ); $defaultOverrides->set( 'MainCacheType', CACHE_NONE ); - $defaultOverrides->set( 'JobTypeConf', [ 'default' => [ 'class' => 'JobQueueMemory' ] ] ); + $defaultOverrides->set( 'JobTypeConf', [ 'default' => [ 'class' => JobQueueMemory::class ] ] ); // Use a fast hash algorithm to hash passwords. $defaultOverrides->set( 'PasswordDefault', 'A' ); @@ -406,6 +410,7 @@ abstract class MediaWikiTestCase extends PHPUnit_Framework_TestCase { // is available in subclass's setUpBeforeClass() and setUp() methods. // This would also remove the need for the HACK that is oncePerClass(). if ( $this->oncePerClass() ) { + $this->setUpSchema( $this->db ); $this->addDBDataOnce(); } @@ -515,8 +520,9 @@ abstract class MediaWikiTestCase extends PHPUnit_Framework_TestCase { // XXX: reset maintenance triggers // Hook into period lag checks which often happen in long-running scripts - $lbFactory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory(); - Maintenance::setLBFactoryTriggers( $lbFactory ); + $services = MediaWikiServices::getInstance(); + $lbFactory = $services->getDBLoadBalancerFactory(); + Maintenance::setLBFactoryTriggers( $lbFactory, $services->getMainConfig() ); ob_start( 'MediaWikiTestCase::wfResetOutputBuffersBarrier' ); } @@ -898,6 +904,36 @@ abstract class MediaWikiTestCase extends PHPUnit_Framework_TestCase { } /** + * Alters $wgGroupPermissions for the duration of the test. Can be called + * with an array, like + * [ '*' => [ 'read' => false ], 'user' => [ 'read' => false ] ] + * or three values to set a single permission, like + * $this->setGroupPermissions( '*', 'read', false ); + * + * @since 1.31 + * @param array|string $newPerms Either an array of permissions to change, + * in which case the next two parameters are ignored; or a single string + * identifying a group, to use with the next two parameters. + * @param string|null $newKey + * @param mixed $newValue + */ + public function setGroupPermissions( $newPerms, $newKey = null, $newValue = null ) { + global $wgGroupPermissions; + + $this->stashMwGlobals( 'wgGroupPermissions' ); + + if ( is_string( $newPerms ) ) { + $newPerms = [ $newPerms => [ $newKey => $newValue ] ]; + } + + foreach ( $newPerms as $group => $permissions ) { + foreach ( $permissions as $key => $value ) { + $wgGroupPermissions[$group][$key] = $value; + } + } + } + + /** * Sets the logger for a specified channel, for the duration of the test. * @since 1.27 * @param string $channel @@ -968,12 +1004,13 @@ abstract class MediaWikiTestCase extends PHPUnit_Framework_TestCase { * @since 1.18 */ public function needsDB() { - # if the test says it uses database tables, it needs the database + // 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 + // If the test class says it belongs to the Database group, it needs the database. + // NOTE: This ONLY checks for the group in the class level doc comment. $rc = new ReflectionClass( $this ); if ( preg_match( '/@group +Database/im', $rc->getDocComment() ) ) { return true; @@ -1007,10 +1044,6 @@ abstract class MediaWikiTestCase extends PHPUnit_Framework_TestCase { $user = static::getTestSysop()->getUser(); $comment = __METHOD__ . ': Sample page for unit test.'; - // Avoid memory leak...? - // LinkCache::singleton()->clear(); - // Maybe. But doing this absolutely breaks $title->isRedirect() when called during unit tests.... - $page = WikiPage::factory( $title ); $page->doEditContent( ContentHandler::makeContent( $text, $title ), $comment, 0, false, $user ); @@ -1077,6 +1110,8 @@ abstract class MediaWikiTestCase extends PHPUnit_Framework_TestCase { } } + SiteStatsInit::doPlaceholderInit(); + User::resetIdByNameCache(); // Make sysop user @@ -1152,6 +1187,8 @@ abstract class MediaWikiTestCase extends PHPUnit_Framework_TestCase { $dbClone = new CloneDatabase( $db, $tablesCloned, $prefix ); $dbClone->useTemporaryTables( self::$useTemporaryTables ); + $db->_originalTablePrefix = $db->tablePrefix(); + if ( ( $db->getType() == 'oracle' || !self::$useTemporaryTables ) && self::$reuseDB ) { CloneDatabase::changePrefix( $prefix ); @@ -1296,6 +1333,205 @@ abstract class MediaWikiTestCase extends PHPUnit_Framework_TestCase { } /** + * @throws LogicException if the given database connection is not a set up to use + * mock tables. + */ + private function ensureMockDatabaseConnection( IDatabase $db ) { + if ( $db->tablePrefix() !== $this->dbPrefix() ) { + throw new LogicException( + 'Trying to delete mock tables, but table prefix does not indicate a mock database.' + ); + } + } + + private static $schemaOverrideDefaults = [ + 'scripts' => [], + 'create' => [], + 'drop' => [], + 'alter' => [], + ]; + + /** + * Stub. If a test suite needs to test against a specific database schema, it should + * override this method and return the appropriate information from it. + * + * @param IMaintainableDatabase $db The DB connection to use for the mock schema. + * May be used to check the current state of the schema, to determine what + * overrides are needed. + * + * @return array An associative array with the following fields: + * - 'scripts': any SQL scripts to run. If empty or not present, schema overrides are skipped. + * - 'create': A list of tables created (may or may not exist in the original schema). + * - 'drop': A list of tables dropped (expected to be present in the original schema). + * - 'alter': A list of tables altered (expected to be present in the original schema). + */ + protected function getSchemaOverrides( IMaintainableDatabase $db ) { + return []; + } + + /** + * Undoes the dpecified schema overrides.. + * Called once per test class, just before addDataOnce(). + * + * @param IMaintainableDatabase $db + * @param array $oldOverrides + */ + private function undoSchemaOverrides( IMaintainableDatabase $db, $oldOverrides ) { + $this->ensureMockDatabaseConnection( $db ); + + $oldOverrides = $oldOverrides + self::$schemaOverrideDefaults; + $originalTables = $this->listOriginalTables( $db ); + + // Drop tables that need to be restored or removed. + $tablesToDrop = array_merge( $oldOverrides['create'], $oldOverrides['alter'] ); + + // Restore tables that have been dropped or created or altered, + // if they exist in the original schema. + $tablesToRestore = array_merge( $tablesToDrop, $oldOverrides['drop'] ); + $tablesToRestore = array_intersect( $originalTables, $tablesToRestore ); + + if ( $tablesToDrop ) { + $this->dropMockTables( $db, $tablesToDrop ); + } + + if ( $tablesToRestore ) { + $this->recloneMockTables( $db, $tablesToRestore ); + } + } + + /** + * Applies the schema overrides returned by getSchemaOverrides(), + * after undoing any previously applied schema overrides. + * Called once per test class, just before addDataOnce(). + */ + private function setUpSchema( IMaintainableDatabase $db ) { + // Undo any active overrides. + $oldOverrides = isset( $db->_schemaOverrides ) ? $db->_schemaOverrides + : self::$schemaOverrideDefaults; + + if ( $oldOverrides['alter'] || $oldOverrides['create'] || $oldOverrides['drop'] ) { + $this->undoSchemaOverrides( $db, $oldOverrides ); + } + + // Determine new overrides. + $overrides = $this->getSchemaOverrides( $db ) + self::$schemaOverrideDefaults; + + $extraKeys = array_diff( + array_keys( $overrides ), + array_keys( self::$schemaOverrideDefaults ) + ); + + if ( $extraKeys ) { + throw new InvalidArgumentException( + 'Schema override contains extra keys: ' . var_export( $extraKeys, true ) + ); + } + + if ( !$overrides['scripts'] ) { + // no scripts to run + return; + } + + if ( !$overrides['create'] && !$overrides['drop'] && !$overrides['alter'] ) { + throw new InvalidArgumentException( + 'Schema override scripts given, but no tables are declared to be ' + . 'created, dropped or altered.' + ); + } + + $this->ensureMockDatabaseConnection( $db ); + + // Drop the tables that will be created by the schema scripts. + $originalTables = $this->listOriginalTables( $db ); + $tablesToDrop = array_intersect( $originalTables, $overrides['create'] ); + + if ( $tablesToDrop ) { + $this->dropMockTables( $db, $tablesToDrop ); + } + + // Run schema override scripts. + foreach ( $overrides['scripts'] as $script ) { + $db->sourceFile( + $script, + null, + null, + __METHOD__, + function ( $cmd ) { + return $this->mungeSchemaUpdateQuery( $cmd ); + } + ); + } + + $db->_schemaOverrides = $overrides; + } + + private function mungeSchemaUpdateQuery( $cmd ) { + return self::$useTemporaryTables + ? preg_replace( '/\bCREATE\s+TABLE\b/i', 'CREATE TEMPORARY TABLE', $cmd ) + : $cmd; + } + + /** + * Drops the given mock tables. + * + * @param IMaintainableDatabase $db + * @param array $tables + */ + private function dropMockTables( IMaintainableDatabase $db, array $tables ) { + $this->ensureMockDatabaseConnection( $db ); + + foreach ( $tables as $tbl ) { + $tbl = $db->tableName( $tbl ); + $db->query( "DROP TABLE IF EXISTS $tbl", __METHOD__ ); + + if ( $tbl === 'page' ) { + // Forget about the pages since they don't + // exist in the DB. + LinkCache::singleton()->clear(); + } + } + } + + /** + * Lists all tables in the live database schema. + * + * @param IMaintainableDatabase $db + * @return array + */ + private function listOriginalTables( IMaintainableDatabase $db ) { + if ( !isset( $db->_originalTablePrefix ) ) { + throw new LogicException( 'No original table prefix know, cannot list tables!' ); + } + + $originalTables = $db->listTables( $db->_originalTablePrefix, __METHOD__ ); + return $originalTables; + } + + /** + * Re-clones the given mock tables to restore them based on the live database schema. + * The tables listed in $tables are expected to currently not exist, so dropMockTables() + * should be called first. + * + * @param IMaintainableDatabase $db + * @param array $tables + */ + private function recloneMockTables( IMaintainableDatabase $db, array $tables ) { + $this->ensureMockDatabaseConnection( $db ); + + if ( !isset( $db->_originalTablePrefix ) ) { + throw new LogicException( 'No original table prefix know, cannot restore tables!' ); + } + + $originalTables = $this->listOriginalTables( $db ); + $tables = array_intersect( $tables, $originalTables ); + + $dbClone = new CloneDatabase( $db, $tables, $db->tablePrefix(), $db->_originalTablePrefix ); + $dbClone->useTemporaryTables( self::$useTemporaryTables ); + + $dbClone->cloneTableStructure(); + } + + /** * Empty all tables so they can be repopulated for tests * * @param Database $db|null Database to reset @@ -1303,8 +1539,9 @@ abstract class MediaWikiTestCase extends PHPUnit_Framework_TestCase { */ private function resetDB( $db, $tablesUsed ) { if ( $db ) { - $userTables = [ 'user', 'user_groups', 'user_properties' ]; - $pageTables = [ 'page', 'revision', 'ip_changes', 'revision_comment_temp', 'comment' ]; + $userTables = [ 'user', 'user_groups', 'user_properties', 'actor' ]; + $pageTables = [ 'page', 'revision', 'ip_changes', 'revision_comment_temp', + 'revision_actor_temp', 'comment' ]; $coreDBDataTables = array_merge( $userTables, $pageTables ); // If any of the user or page tables were marked as used, we should clear all of them. @@ -1323,12 +1560,21 @@ abstract class MediaWikiTestCase extends PHPUnit_Framework_TestCase { continue; } + if ( !$db->tableExists( $tbl ) ) { + continue; + } + if ( $truncate ) { $db->query( 'TRUNCATE TABLE ' . $db->tableName( $tbl ), __METHOD__ ); } else { $db->delete( $tbl, '*', __METHOD__ ); } + if ( in_array( $db->getType(), [ 'postgres', 'sqlite' ], true ) ) { + // Reset the table's sequence too. + $db->resetSequenceForTable( $tbl, __METHOD__ ); + } + if ( $tbl === 'page' ) { // Forget about the pages since they don't // exist in the DB. @@ -1343,50 +1589,12 @@ abstract class MediaWikiTestCase extends PHPUnit_Framework_TestCase { } } - /** - * @since 1.18 - * - * @param string $func - * @param array $args - * - * @return mixed - * @throws MWException - */ - public function __call( $func, $args ) { - static $compatibility = [ - 'createMock' => 'createMock2', - ]; - - if ( isset( $compatibility[$func] ) ) { - return call_user_func_array( [ $this, $compatibility[$func] ], $args ); - } else { - throw new MWException( "Called non-existent $func method on " . static::class ); - } - } - - /** - * Return a test double for the specified class. - * - * @param string $originalClassName - * @return PHPUnit_Framework_MockObject_MockObject - * @throws Exception - */ - private function createMock2( $originalClassName ) { - return $this->getMockBuilder( $originalClassName ) - ->disableOriginalConstructor() - ->disableOriginalClone() - ->disableArgumentCloning() - // New in phpunit-mock-objects 3.2 (phpunit 5.4.0) - // ->disallowMockingUnknownTypes() - ->getMock(); - } - private static function unprefixTable( &$tableName, $ind, $prefix ) { $tableName = substr( $tableName, strlen( $prefix ) ); } private static function isNotUnittest( $table ) { - return strpos( $table, 'unittest_' ) !== 0; + return strpos( $table, self::DB_PREFIX ) !== 0; } /** @@ -1465,9 +1673,9 @@ abstract class MediaWikiTestCase extends PHPUnit_Framework_TestCase { * @param string $function */ public function hideDeprecated( $function ) { - MediaWiki\suppressWarnings(); + Wikimedia\suppressWarnings(); wfDeprecated( $function ); - MediaWiki\restoreWarnings(); + Wikimedia\restoreWarnings(); } /** @@ -1482,13 +1690,17 @@ abstract class MediaWikiTestCase extends PHPUnit_Framework_TestCase { * @param string|array $fields The columns to include in the result (and to sort by) * @param string|array $condition "where" condition(s) * @param array $expectedRows An array of arrays giving the expected rows. + * @param array $options Options for the query + * @param array $join_conds Join conditions for the query * * @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 ) { + protected function assertSelect( + $table, $fields, $condition, array $expectedRows, array $options = [], array $join_conds = [] + ) { 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.' ); @@ -1496,7 +1708,14 @@ abstract class MediaWikiTestCase extends PHPUnit_Framework_TestCase { $db = wfGetDB( DB_REPLICA ); - $res = $db->select( $table, $fields, $condition, wfGetCaller(), [ 'ORDER BY' => $fields ] ); + $res = $db->select( + $table, + $fields, + $condition, + wfGetCaller(), + $options + [ 'ORDER BY' => $fields ], + $join_conds + ); $this->assertNotEmpty( $res, "query failed: " . $db->lastError() ); $i = 0; @@ -1750,9 +1969,9 @@ abstract class MediaWikiTestCase extends PHPUnit_Framework_TestCase { # This check may also protect against code injection in # case of broken installations. - MediaWiki\suppressWarnings(); + Wikimedia\suppressWarnings(); $haveDiff3 = $wgDiff3 && file_exists( $wgDiff3 ); - MediaWiki\restoreWarnings(); + Wikimedia\restoreWarnings(); if ( !$haveDiff3 ) { $this->markTestSkipped( "Skip test, since diff3 is not configured" ); @@ -1777,61 +1996,6 @@ abstract class MediaWikiTestCase extends PHPUnit_Framework_TestCase { } /** - * Asserts that the given string is a valid HTML snippet. - * Wraps the given string in the required top level tags and - * then calls assertValidHtmlDocument(). - * The snippet is expected to be HTML 5. - * - * @since 1.23 - * - * @note Will mark the test as skipped if the "tidy" module is not installed. - * @note This ignores $wgUseTidy, so we can check for valid HTML even (and especially) - * when automatic tidying is disabled. - * - * @param string $html An HTML snippet (treated as the contents of the body tag). - */ - protected function assertValidHtmlSnippet( $html ) { - $html = '<!DOCTYPE html><html><head><title>test</title></head><body>' . $html . '</body></html>'; - $this->assertValidHtmlDocument( $html ); - } - - /** - * Asserts that the given string is valid HTML document. - * - * @since 1.23 - * - * @note Will mark the test as skipped if the "tidy" module is not installed. - * @note This ignores $wgUseTidy, so we can check for valid HTML even (and especially) - * when automatic tidying is disabled. - * - * @param string $html A complete HTML document - */ - protected function assertValidHtmlDocument( $html ) { - // Note: we only validate if the tidy PHP extension is available. - // In case wgTidyInternal is false, MWTidy would fall back to the command line version - // of tidy. In that case however, we can not reliably detect whether a failing validation - // is due to malformed HTML, or caused by tidy not being installed as a command line tool. - // That would cause all HTML assertions to fail on a system that has no tidy installed. - if ( !$GLOBALS['wgTidyInternal'] || !MWTidy::isEnabled() ) { - $this->markTestSkipped( 'Tidy extension not installed' ); - } - - $errorBuffer = ''; - MWTidy::checkErrors( $html, $errorBuffer ); - $allErrors = preg_split( '/[\r\n]+/', $errorBuffer ); - - // Filter Tidy warnings which aren't useful for us. - // Tidy eg. often cries about parameters missing which have actually - // been deprecated since HTML4, thus we should not care about them. - $errors = preg_grep( - '/^(.*Warning: (trimming empty|.* lacks ".*?" attribute).*|\s*)$/m', - $allErrors, PREG_GREP_INVERT - ); - - $this->assertEmpty( $errors, implode( "\n", $errors ) ); - } - - /** * Used as a marker to prevent wfResetOutputBuffers from breaking PHPUnit. * @param string $buffer * @return string diff --git a/www/wiki/tests/phpunit/PHPUnit4And6Compat.php b/www/wiki/tests/phpunit/PHPUnit4And6Compat.php new file mode 100644 index 00000000..672ab4a4 --- /dev/null +++ b/www/wiki/tests/phpunit/PHPUnit4And6Compat.php @@ -0,0 +1,121 @@ +<?php +/** + * Copyright (C) 2018 Kunal Mehta <legoktm@member.fsf.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. + * + */ + +/** + * @since 1.31 + */ +trait PHPUnit4And6Compat { + /** + * @see PHPUnit_Framework_TestCase::setExpectedException + * + * This function was renamed to expectException() in PHPUnit 6, so this + * is a temporary backwards-compatibility layer while we transition. + */ + public function setExpectedException( $name, $message = '', $code = null ) { + if ( is_callable( [ $this, 'expectException' ] ) ) { + if ( $name !== null ) { + $this->expectException( $name ); + } + if ( $message !== '' ) { + $this->expectExceptionMessage( $message ); + } + if ( $code !== null ) { + $this->expectExceptionCode( $code ); + } + } else { + parent::setExpectedException( $name, $message, $code ); + } + } + + /** + * @see PHPUnit_Framework_TestCase::getMock + * + * @return PHPUnit_Framework_MockObject_MockObject + */ + public function getMock( $originalClassName, $methods = [], array $arguments = [], + $mockClassName = '', $callOriginalConstructor = true, $callOriginalClone = true, + $callAutoload = true, $cloneArguments = false, $callOriginalMethods = false, + $proxyTarget = null + ) { + if ( is_callable( 'parent::getMock' ) ) { + return parent::getMock( + $originalClassName, $methods, $arguments, $mockClassName, + $callOriginalConstructor, $callOriginalClone, $callAutoload, + $cloneArguments, $callOriginalMethods, $proxyTarget + ); + } else { + $builder = $this->getMockBuilder( $originalClassName ) + ->setMethods( $methods ) + ->setConstructorArgs( $arguments ) + ->setMockClassName( $mockClassName ) + ->setProxyTarget( $proxyTarget ); + if ( $callOriginalConstructor ) { + $builder->enableOriginalConstructor(); + } else { + $builder->disableOriginalConstructor(); + } + if ( $callOriginalClone ) { + $builder->enableOriginalClone(); + } else { + $builder->disableOriginalClone(); + } + if ( $callAutoload ) { + $builder->enableAutoload(); + } else { + $builder->disableAutoload(); + } + if ( $cloneArguments ) { + $builder->enableArgumentCloning(); + } else { + $builder->disableArgumentCloning(); + } + if ( $callOriginalMethods ) { + $builder->enableProxyingToOriginalMethods(); + } else { + $builder->disableProxyingToOriginalMethods(); + } + + return $builder->getMock(); + } + } + + /** + * Return a test double for the specified class. This + * is a forward port of the createMock function that + * was introduced in PHPUnit 5.4. + * + * @param string $originalClassName + * @return PHPUnit_Framework_MockObject_MockObject + * @throws Exception + */ + public function createMock( $originalClassName ) { + if ( is_callable( 'parent::createMock' ) ) { + return parent::createMock( $originalClassName ); + } + // Compat for PHPUnit <= 5.4 + return $this->getMockBuilder( $originalClassName ) + ->disableOriginalConstructor() + ->disableOriginalClone() + ->disableArgumentCloning() + // New in phpunit-mock-objects 3.2 (phpunit 5.4.0) + // ->disallowMockingUnknownTypes() + ->getMock(); + } +} diff --git a/www/wiki/tests/phpunit/ResourceLoaderTestCase.php b/www/wiki/tests/phpunit/ResourceLoaderTestCase.php index 1024ecd0..d5c14a25 100644 --- a/www/wiki/tests/phpunit/ResourceLoaderTestCase.php +++ b/www/wiki/tests/phpunit/ResourceLoaderTestCase.php @@ -40,7 +40,7 @@ abstract class ResourceLoaderTestCase extends MediaWikiTestCase { 'skin' => $options['skin'], 'target' => 'phpunit', ] ); - $ctx = $this->getMockBuilder( 'ResourceLoaderContext' ) + $ctx = $this->getMockBuilder( ResourceLoaderContext::class ) ->setConstructorArgs( [ $resourceLoader, $request ] ) ->setMethods( [ 'getDirection' ] ) ->getMock(); @@ -61,7 +61,6 @@ abstract class ResourceLoaderTestCase extends MediaWikiTestCase { // For wfScript() 'ScriptPath' => '/w', - 'ScriptExtension' => '.php', 'Script' => '/w/index.php', 'LoadScript' => '/w/load.php', ]; @@ -87,7 +86,6 @@ class ResourceLoaderTestModule extends ResourceLoaderModule { protected $dependencies = []; protected $group = null; protected $source = 'local'; - protected $position = 'bottom'; protected $script = ''; protected $styles = ''; protected $skipFunction = null; @@ -126,9 +124,6 @@ class ResourceLoaderTestModule extends ResourceLoaderModule { public function getSource() { return $this->source; } - public function getPosition() { - return $this->position; - } public function getType() { return $this->type; diff --git a/www/wiki/tests/phpunit/autoload.ide.php b/www/wiki/tests/phpunit/autoload.ide.php index 106ab683..4b0b1873 100644 --- a/www/wiki/tests/phpunit/autoload.ide.php +++ b/www/wiki/tests/phpunit/autoload.ide.php @@ -38,16 +38,13 @@ $maintenance->setup(); // to $maintenance->mSelf. Keep that here for b/c $self = $maintenance->getName(); global $IP; -# Start the autoloader, so that extensions can derive classes from core files -require_once "$IP/includes/AutoLoader.php"; -# Grab profiling functions -require_once "$IP/includes/profiler/ProfilerFunctions.php"; - -# Start the profiler +# Get profiler configuraton $wgProfiler = []; if ( file_exists( "$IP/StartProfiler.php" ) ) { require "$IP/StartProfiler.php"; } +# Start the autoloader, so that extensions can derive classes from core files +require_once "$IP/includes/AutoLoader.php"; $requireOnceGlobalsScope = function ( $file ) use ( $self ) { foreach ( array_keys( $GLOBALS ) as $varName ) { @@ -93,7 +90,7 @@ if ( $maintenance->getDbType() === Maintenance::DB_NONE ) { || ( $wgLocalisationCacheConf['store'] == 'detect' && !$wgCacheDirectory ) ) ) { - $wgLocalisationCacheConf['storeClass'] = 'LCStoreNull'; + $wgLocalisationCacheConf['storeClass'] = LCStoreNull::class; } } diff --git a/www/wiki/tests/phpunit/data/categoriesrdf/categoriesRdf-out.nt b/www/wiki/tests/phpunit/data/categoriesrdf/categoriesRdf-out.nt index d2d7ea81..bbb37870 100644 --- a/www/wiki/tests/phpunit/data/categoriesrdf/categoriesRdf-out.nt +++ b/www/wiki/tests/phpunit/data/categoriesrdf/categoriesRdf-out.nt @@ -1,16 +1,23 @@ -<http://acme.test/categoriesDump> <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://schema.org/Dataset> . -<http://acme.test/categoriesDump> <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://www.w3.org/2002/07/owl#Ontology> . -<http://acme.test/categoriesDump> <http://creativecommons.org/ns#license> <https://creativecommons.org/licenses/by-sa/3.0/> . -<http://acme.test/categoriesDump> <http://schema.org/softwareVersion> "1.0" . -<http://acme.test/categoriesDump> <http://schema.org/dateModified> "{DATE}"^^<http://www.w3.org/2001/XMLSchema#dateTime> . -<http://acme.test/categoriesDump> <http://schema.org/isPartOf> <http://acme.test/> . -<http://acme.test/categoriesDump> <http://www.w3.org/2002/07/owl#imports> <https://www.mediawiki.org/ontology/ontology.owl> . +<http://acme.test/wiki/Special:CategoryDump> <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://schema.org/Dataset> . +<http://acme.test/wiki/Special:CategoryDump> <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://www.w3.org/2002/07/owl#Ontology> . +<http://acme.test/wiki/Special:CategoryDump> <http://creativecommons.org/ns#license> <https://creativecommons.org/licenses/by-sa/3.0/> . +<http://acme.test/wiki/Special:CategoryDump> <http://schema.org/softwareVersion> "1.1" . +<http://acme.test/wiki/Special:CategoryDump> <http://schema.org/dateModified> "{DATE}"^^<http://www.w3.org/2001/XMLSchema#dateTime> . +<http://acme.test/wiki/Special:CategoryDump> <http://schema.org/isPartOf> <http://acme.test/> . +<http://acme.test/wiki/Special:CategoryDump> <http://www.w3.org/2002/07/owl#imports> <https://www.mediawiki.org/ontology/ontology.owl> . <http://acme.test/wiki/Category:Category_One> <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <https://www.mediawiki.org/ontology#Category> . <http://acme.test/wiki/Category:Category_One> <http://www.w3.org/2000/01/rdf-schema#label> "Category One" . +<http://acme.test/wiki/Category:Category_One> <https://www.mediawiki.org/ontology#pages> "7"^^<http://www.w3.org/2001/XMLSchema#integer> . +<http://acme.test/wiki/Category:Category_One> <https://www.mediawiki.org/ontology#subcategories> "10"^^<http://www.w3.org/2001/XMLSchema#integer> . <http://acme.test/wiki/Category:2_Category_Two> <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <https://www.mediawiki.org/ontology#Category> . +<http://acme.test/wiki/Category:2_Category_Two> <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <https://www.mediawiki.org/ontology#HiddenCategory> . <http://acme.test/wiki/Category:2_Category_Two> <http://www.w3.org/2000/01/rdf-schema#label> "2 Category Two" . +<http://acme.test/wiki/Category:2_Category_Two> <https://www.mediawiki.org/ontology#pages> "17"^^<http://www.w3.org/2001/XMLSchema#integer> . +<http://acme.test/wiki/Category:2_Category_Two> <https://www.mediawiki.org/ontology#subcategories> "0"^^<http://www.w3.org/2001/XMLSchema#integer> . <http://acme.test/wiki/Category:Category_One> <https://www.mediawiki.org/ontology#isInCategory> <http://acme.test/wiki/Category:Parent_of_1> . <http://acme.test/wiki/Category:2_Category_Two> <https://www.mediawiki.org/ontology#isInCategory> <http://acme.test/wiki/Category:Parent_of_2> . <http://acme.test/wiki/Category:%D0%A2%D1%80%D0%B5%D1%82%D1%8C%D1%8F_%D0%BA%D0%B0%D1%82%D0%B5%D0%B3%D0%BE%D1%80%D0%B8%D1%8F> <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <https://www.mediawiki.org/ontology#Category> . <http://acme.test/wiki/Category:%D0%A2%D1%80%D0%B5%D1%82%D1%8C%D1%8F_%D0%BA%D0%B0%D1%82%D0%B5%D0%B3%D0%BE%D1%80%D0%B8%D1%8F> <http://www.w3.org/2000/01/rdf-schema#label> "\u0422\u0440\u0435\u0442\u044C\u044F \u043A\u0430\u0442\u0435\u0433\u043E\u0440\u0438\u044F" . +<http://acme.test/wiki/Category:%D0%A2%D1%80%D0%B5%D1%82%D1%8C%D1%8F_%D0%BA%D0%B0%D1%82%D0%B5%D0%B3%D0%BE%D1%80%D0%B8%D1%8F> <https://www.mediawiki.org/ontology#pages> "0"^^<http://www.w3.org/2001/XMLSchema#integer> . +<http://acme.test/wiki/Category:%D0%A2%D1%80%D0%B5%D1%82%D1%8C%D1%8F_%D0%BA%D0%B0%D1%82%D0%B5%D0%B3%D0%BE%D1%80%D0%B8%D1%8F> <https://www.mediawiki.org/ontology#subcategories> "0"^^<http://www.w3.org/2001/XMLSchema#integer> . <http://acme.test/wiki/Category:%D0%A2%D1%80%D0%B5%D1%82%D1%8C%D1%8F_%D0%BA%D0%B0%D1%82%D0%B5%D0%B3%D0%BE%D1%80%D0%B8%D1%8F> <https://www.mediawiki.org/ontology#isInCategory> <http://acme.test/wiki/Category:Parent_of_3> . diff --git a/www/wiki/tests/phpunit/data/composer/composer.json b/www/wiki/tests/phpunit/data/composer/composer.json index bcd196f4..9b902ae8 100644 --- a/www/wiki/tests/phpunit/data/composer/composer.json +++ b/www/wiki/tests/phpunit/data/composer/composer.json @@ -9,7 +9,7 @@ "homepage": "https://www.mediawiki.org/wiki/Special:Version/Credits" } ], - "license": "GPL-2.0", + "license": "GPL-2.0-only", "support": { "issues": "https://bugzilla.wikimedia.org/", "irc": "irc://irc.freenode.net/mediawiki", diff --git a/www/wiki/tests/phpunit/data/composer/composer.lock b/www/wiki/tests/phpunit/data/composer/composer.lock index cae6a478..5c030db8 100644 --- a/www/wiki/tests/phpunit/data/composer/composer.lock +++ b/www/wiki/tests/phpunit/data/composer/composer.lock @@ -162,7 +162,7 @@ "notification-url": "https://packagist.org/downloads/", "license": [ "MIT", - "GPL-3.0" + "GPL-3.0-only" ], "authors": [ { @@ -207,7 +207,7 @@ }, "notification-url": "https://packagist.org/downloads/", "license": [ - "GPL-2.0+" + "GPL-2.0-or-later" ], "authors": [ { @@ -265,7 +265,7 @@ }, "notification-url": "https://packagist.org/downloads/", "license": [ - "GPL-2.0+", + "GPL-2.0-or-later", "MIT" ], "description": "The primary aim is to allow users to select a language and configure its support in an easy way. Main features are language selection, input methods and web fonts.", @@ -374,7 +374,7 @@ }, "notification-url": "https://packagist.org/downloads/", "license": [ - "GPL-2.0" + "GPL-2.0-only" ], "authors": [ { diff --git a/www/wiki/tests/phpunit/data/composer/installed.json b/www/wiki/tests/phpunit/data/composer/installed.json new file mode 100644 index 00000000..88a6bae2 --- /dev/null +++ b/www/wiki/tests/phpunit/data/composer/installed.json @@ -0,0 +1,1682 @@ +[ + { + "name": "leafo/lessphp", + "version": "v0.5.0", + "version_normalized": "0.5.0.0", + "source": { + "type": "git", + "url": "https://github.com/leafo/lessphp.git", + "reference": "0f5a7f5545d2bcf4e9fad9a228c8ad89cc9aa283" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/leafo/lessphp/zipball/0f5a7f5545d2bcf4e9fad9a228c8ad89cc9aa283", + "reference": "0f5a7f5545d2bcf4e9fad9a228c8ad89cc9aa283", + "shasum": "" + }, + "time": "2014-11-24T18:39:20+00:00", + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "0.4.x-dev" + } + }, + "installation-source": "dist", + "autoload": { + "classmap": [ + "lessc.inc.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT", + "GPL-3.0-only" + ], + "authors": [ + { + "name": "Leaf Corcoran", + "email": "leafot@gmail.com", + "homepage": "http://leafo.net" + } + ], + "description": "lessphp is a compiler for LESS written in PHP.", + "homepage": "http://leafo.net/lessphp/" + }, + { + "name": "psr/log", + "version": "1.0.0", + "version_normalized": "1.0.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/log.git", + "reference": "fe0936ee26643249e916849d48e3a51d5f5e278b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/log/zipball/fe0936ee26643249e916849d48e3a51d5f5e278b", + "reference": "fe0936ee26643249e916849d48e3a51d5f5e278b", + "shasum": "" + }, + "time": "2012-12-21T11:40:51+00:00", + "type": "library", + "installation-source": "dist", + "autoload": { + "psr-0": { + "Psr\\Log\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interface for logging libraries", + "keywords": [ + "log", + "psr", + "psr-3" + ] + }, + { + "name": "cssjanus/cssjanus", + "version": "v1.1.1", + "version_normalized": "1.1.1.0", + "source": { + "type": "git", + "url": "https://github.com/cssjanus/php-cssjanus.git", + "reference": "62a9c32e6e140de09082b40a6e99d868ad14d4e0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/cssjanus/php-cssjanus/zipball/62a9c32e6e140de09082b40a6e99d868ad14d4e0", + "reference": "62a9c32e6e140de09082b40a6e99d868ad14d4e0", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "require-dev": { + "jakub-onderka/php-parallel-lint": "0.8.*", + "phpunit/phpunit": "3.7.*", + "squizlabs/php_codesniffer": "1.*" + }, + "time": "2014-11-14T20:00:50+00:00", + "type": "library", + "installation-source": "dist", + "autoload": { + "psr-0": { + "": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "description": "Convert CSS stylesheets between left-to-right and right-to-left." + }, + { + "name": "cdb/cdb", + "version": "1.0.0", + "version_normalized": "1.0.0.0", + "source": { + "type": "git", + "url": "https://github.com/wikimedia/cdb.git", + "reference": "918601ea3d31b8c37312e9c0e54446aa8bfb3425" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/wikimedia/cdb/zipball/918601ea3d31b8c37312e9c0e54446aa8bfb3425", + "reference": "918601ea3d31b8c37312e9c0e54446aa8bfb3425", + "shasum": "" + }, + "require": { + "php": ">=5.3.2" + }, + "require-dev": { + "phpunit/phpunit": "*" + }, + "time": "2014-11-12T19:03:26+00:00", + "type": "library", + "installation-source": "dist", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "GPLv2" + ], + "authors": [ + { + "name": "Tim Starling", + "email": "tstarling@wikimedia.org" + }, + { + "name": "Chad Horohoe", + "email": "chad@wikimedia.org" + } + ], + "description": "Constant Database (CDB) wrapper library for PHP. Provides pure-PHP fallback when dba_* functions are absent.", + "homepage": "https://www.mediawiki.org/wiki/CDB", + "abandoned": "wikimedia/cdb" + }, + { + "name": "sebastian/version", + "version": "2.0.1", + "version_normalized": "2.0.1.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/version.git", + "reference": "99732be0ddb3361e16ad77b68ba41efc8e979019" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/99732be0ddb3361e16ad77b68ba41efc8e979019", + "reference": "99732be0ddb3361e16ad77b68ba41efc8e979019", + "shasum": "" + }, + "require": { + "php": ">=5.6" + }, + "time": "2016-10-03T07:35:21+00:00", + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "installation-source": "dist", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that helps with managing the version number of Git-hosted PHP projects", + "homepage": "https://github.com/sebastianbergmann/version" + }, + { + "name": "sebastian/resource-operations", + "version": "1.0.0", + "version_normalized": "1.0.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/resource-operations.git", + "reference": "ce990bb21759f94aeafd30209e8cfcdfa8bc3f52" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/ce990bb21759f94aeafd30209e8cfcdfa8bc3f52", + "reference": "ce990bb21759f94aeafd30209e8cfcdfa8bc3f52", + "shasum": "" + }, + "require": { + "php": ">=5.6.0" + }, + "time": "2015-07-28T20:34:47+00:00", + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "installation-source": "dist", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides a list of PHP built-in functions that operate on resources", + "homepage": "https://www.github.com/sebastianbergmann/resource-operations" + }, + { + "name": "sebastian/recursion-context", + "version": "3.0.0", + "version_normalized": "3.0.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/recursion-context.git", + "reference": "5b0cd723502bac3b006cbf3dbf7a1e3fcefe4fa8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/5b0cd723502bac3b006cbf3dbf7a1e3fcefe4fa8", + "reference": "5b0cd723502bac3b006cbf3dbf7a1e3fcefe4fa8", + "shasum": "" + }, + "require": { + "php": "^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^6.0" + }, + "time": "2017-03-03T06:23:57+00:00", + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0.x-dev" + } + }, + "installation-source": "dist", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + } + ], + "description": "Provides functionality to recursively process PHP variables", + "homepage": "http://www.github.com/sebastianbergmann/recursion-context" + }, + { + "name": "sebastian/object-reflector", + "version": "1.1.1", + "version_normalized": "1.1.1.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-reflector.git", + "reference": "773f97c67f28de00d397be301821b06708fca0be" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/773f97c67f28de00d397be301821b06708fca0be", + "reference": "773f97c67f28de00d397be301821b06708fca0be", + "shasum": "" + }, + "require": { + "php": "^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^6.0" + }, + "time": "2017-03-29T09:07:27+00:00", + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.1-dev" + } + }, + "installation-source": "dist", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Allows reflection of object attributes, including inherited and non-public ones", + "homepage": "https://github.com/sebastianbergmann/object-reflector/" + }, + { + "name": "sebastian/object-enumerator", + "version": "3.0.3", + "version_normalized": "3.0.3.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-enumerator.git", + "reference": "7cfd9e65d11ffb5af41198476395774d4c8a84c5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/7cfd9e65d11ffb5af41198476395774d4c8a84c5", + "reference": "7cfd9e65d11ffb5af41198476395774d4c8a84c5", + "shasum": "" + }, + "require": { + "php": "^7.0", + "sebastian/object-reflector": "^1.1.1", + "sebastian/recursion-context": "^3.0" + }, + "require-dev": { + "phpunit/phpunit": "^6.0" + }, + "time": "2017-08-03T12:35:26+00:00", + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0.x-dev" + } + }, + "installation-source": "dist", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Traverses array structures and object graphs to enumerate all referenced objects", + "homepage": "https://github.com/sebastianbergmann/object-enumerator/" + }, + { + "name": "sebastian/global-state", + "version": "2.0.0", + "version_normalized": "2.0.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/global-state.git", + "reference": "e8ba02eed7bbbb9e59e43dedd3dddeff4a56b0c4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/e8ba02eed7bbbb9e59e43dedd3dddeff4a56b0c4", + "reference": "e8ba02eed7bbbb9e59e43dedd3dddeff4a56b0c4", + "shasum": "" + }, + "require": { + "php": "^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^6.0" + }, + "suggest": { + "ext-uopz": "*" + }, + "time": "2017-04-27T15:39:26+00:00", + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "installation-source": "dist", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Snapshotting of global state", + "homepage": "http://www.github.com/sebastianbergmann/global-state", + "keywords": [ + "global state" + ] + }, + { + "name": "sebastian/exporter", + "version": "3.1.0", + "version_normalized": "3.1.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/exporter.git", + "reference": "234199f4528de6d12aaa58b612e98f7d36adb937" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/234199f4528de6d12aaa58b612e98f7d36adb937", + "reference": "234199f4528de6d12aaa58b612e98f7d36adb937", + "shasum": "" + }, + "require": { + "php": "^7.0", + "sebastian/recursion-context": "^3.0" + }, + "require-dev": { + "ext-mbstring": "*", + "phpunit/phpunit": "^6.0" + }, + "time": "2017-04-03T13:19:02+00:00", + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.1.x-dev" + } + }, + "installation-source": "dist", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@2bepublished.at" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + } + ], + "description": "Provides the functionality to export PHP variables for visualization", + "homepage": "http://www.github.com/sebastianbergmann/exporter", + "keywords": [ + "export", + "exporter" + ] + }, + { + "name": "sebastian/environment", + "version": "3.1.0", + "version_normalized": "3.1.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/environment.git", + "reference": "cd0871b3975fb7fc44d11314fd1ee20925fce4f5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/cd0871b3975fb7fc44d11314fd1ee20925fce4f5", + "reference": "cd0871b3975fb7fc44d11314fd1ee20925fce4f5", + "shasum": "" + }, + "require": { + "php": "^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^6.1" + }, + "time": "2017-07-01T08:51:00+00:00", + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.1.x-dev" + } + }, + "installation-source": "dist", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides functionality to handle HHVM/PHP environments", + "homepage": "http://www.github.com/sebastianbergmann/environment", + "keywords": [ + "Xdebug", + "environment", + "hhvm" + ] + }, + { + "name": "sebastian/diff", + "version": "2.0.1", + "version_normalized": "2.0.1.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/diff.git", + "reference": "347c1d8b49c5c3ee30c7040ea6fc446790e6bddd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/347c1d8b49c5c3ee30c7040ea6fc446790e6bddd", + "reference": "347c1d8b49c5c3ee30c7040ea6fc446790e6bddd", + "shasum": "" + }, + "require": { + "php": "^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^6.2" + }, + "time": "2017-08-03T08:09:46+00:00", + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "installation-source": "dist", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Kore Nordmann", + "email": "mail@kore-nordmann.de" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Diff implementation", + "homepage": "https://github.com/sebastianbergmann/diff", + "keywords": [ + "diff" + ] + }, + { + "name": "sebastian/comparator", + "version": "2.1.1", + "version_normalized": "2.1.1.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/comparator.git", + "reference": "b11c729f95109b56a0fe9650c6a63a0fcd8c439f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/b11c729f95109b56a0fe9650c6a63a0fcd8c439f", + "reference": "b11c729f95109b56a0fe9650c6a63a0fcd8c439f", + "shasum": "" + }, + "require": { + "php": "^7.0", + "sebastian/diff": "^2.0", + "sebastian/exporter": "^3.1" + }, + "require-dev": { + "phpunit/phpunit": "^6.4" + }, + "time": "2017-12-22T14:50:35+00:00", + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.1.x-dev" + } + }, + "installation-source": "dist", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@2bepublished.at" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides the functionality to compare PHP values for equality", + "homepage": "https://github.com/sebastianbergmann/comparator", + "keywords": [ + "comparator", + "compare", + "equality" + ] + }, + { + "name": "doctrine/instantiator", + "version": "1.1.0", + "version_normalized": "1.1.0.0", + "source": { + "type": "git", + "url": "https://github.com/doctrine/instantiator.git", + "reference": "185b8868aa9bf7159f5f953ed5afb2d7fcdc3bda" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/instantiator/zipball/185b8868aa9bf7159f5f953ed5afb2d7fcdc3bda", + "reference": "185b8868aa9bf7159f5f953ed5afb2d7fcdc3bda", + "shasum": "" + }, + "require": { + "php": "^7.1" + }, + "require-dev": { + "athletic/athletic": "~0.1.8", + "ext-pdo": "*", + "ext-phar": "*", + "phpunit/phpunit": "^6.2.3", + "squizlabs/php_codesniffer": "^3.0.2" + }, + "time": "2017-07-22T11:58:36+00:00", + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.2.x-dev" + } + }, + "installation-source": "dist", + "autoload": { + "psr-4": { + "Doctrine\\Instantiator\\": "src/Doctrine/Instantiator/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Marco Pivetta", + "email": "ocramius@gmail.com", + "homepage": "http://ocramius.github.com/" + } + ], + "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors", + "homepage": "https://github.com/doctrine/instantiator", + "keywords": [ + "constructor", + "instantiate" + ] + }, + { + "name": "phpunit/php-text-template", + "version": "1.2.1", + "version_normalized": "1.2.1.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-text-template.git", + "reference": "31f8b717e51d9a2afca6c9f046f5d69fc27c8686" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/31f8b717e51d9a2afca6c9f046f5d69fc27c8686", + "reference": "31f8b717e51d9a2afca6c9f046f5d69fc27c8686", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "time": "2015-06-21T13:50:34+00:00", + "type": "library", + "installation-source": "dist", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Simple template engine.", + "homepage": "https://github.com/sebastianbergmann/php-text-template/", + "keywords": [ + "template" + ] + }, + { + "name": "phpunit/phpunit-mock-objects", + "version": "5.0.6", + "version_normalized": "5.0.6.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/phpunit-mock-objects.git", + "reference": "33fd41a76e746b8fa96d00b49a23dadfa8334cdf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit-mock-objects/zipball/33fd41a76e746b8fa96d00b49a23dadfa8334cdf", + "reference": "33fd41a76e746b8fa96d00b49a23dadfa8334cdf", + "shasum": "" + }, + "require": { + "doctrine/instantiator": "^1.0.5", + "php": "^7.0", + "phpunit/php-text-template": "^1.2.1", + "sebastian/exporter": "^3.1" + }, + "conflict": { + "phpunit/phpunit": "<6.0" + }, + "require-dev": { + "phpunit/phpunit": "^6.5" + }, + "suggest": { + "ext-soap": "*" + }, + "time": "2018-01-06T05:45:45+00:00", + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.0.x-dev" + } + }, + "installation-source": "dist", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Mock Object library for PHPUnit", + "homepage": "https://github.com/sebastianbergmann/phpunit-mock-objects/", + "keywords": [ + "mock", + "xunit" + ] + }, + { + "name": "phpunit/php-timer", + "version": "1.0.9", + "version_normalized": "1.0.9.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-timer.git", + "reference": "3dcf38ca72b158baf0bc245e9184d3fdffa9c46f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/3dcf38ca72b158baf0bc245e9184d3fdffa9c46f", + "reference": "3dcf38ca72b158baf0bc245e9184d3fdffa9c46f", + "shasum": "" + }, + "require": { + "php": "^5.3.3 || ^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.0" + }, + "time": "2017-02-26T11:10:40+00:00", + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "installation-source": "dist", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sb@sebastian-bergmann.de", + "role": "lead" + } + ], + "description": "Utility class for timing", + "homepage": "https://github.com/sebastianbergmann/php-timer/", + "keywords": [ + "timer" + ] + }, + { + "name": "phpunit/php-file-iterator", + "version": "1.4.5", + "version_normalized": "1.4.5.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-file-iterator.git", + "reference": "730b01bc3e867237eaac355e06a36b85dd93a8b4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/730b01bc3e867237eaac355e06a36b85dd93a8b4", + "reference": "730b01bc3e867237eaac355e06a36b85dd93a8b4", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "time": "2017-11-27T13:52:08+00:00", + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.4.x-dev" + } + }, + "installation-source": "dist", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sb@sebastian-bergmann.de", + "role": "lead" + } + ], + "description": "FilterIterator implementation that filters files based on a list of suffixes.", + "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", + "keywords": [ + "filesystem", + "iterator" + ] + }, + { + "name": "theseer/tokenizer", + "version": "1.1.0", + "version_normalized": "1.1.0.0", + "source": { + "type": "git", + "url": "https://github.com/theseer/tokenizer.git", + "reference": "cb2f008f3f05af2893a87208fe6a6c4985483f8b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/cb2f008f3f05af2893a87208fe6a6c4985483f8b", + "reference": "cb2f008f3f05af2893a87208fe6a6c4985483f8b", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-tokenizer": "*", + "ext-xmlwriter": "*", + "php": "^7.0" + }, + "time": "2017-04-07T12:08:54+00:00", + "type": "library", + "installation-source": "dist", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + } + ], + "description": "A small library for converting tokenized PHP source code into XML and potentially other formats" + }, + { + "name": "sebastian/code-unit-reverse-lookup", + "version": "1.0.1", + "version_normalized": "1.0.1.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", + "reference": "4419fcdb5eabb9caa61a27c7a1db532a6b55dd18" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/4419fcdb5eabb9caa61a27c7a1db532a6b55dd18", + "reference": "4419fcdb5eabb9caa61a27c7a1db532a6b55dd18", + "shasum": "" + }, + "require": { + "php": "^5.6 || ^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^5.7 || ^6.0" + }, + "time": "2017-03-04T06:30:41+00:00", + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "installation-source": "dist", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Looks up which function or method a line of code belongs to", + "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/" + }, + { + "name": "phpunit/php-token-stream", + "version": "2.0.2", + "version_normalized": "2.0.2.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-token-stream.git", + "reference": "791198a2c6254db10131eecfe8c06670700904db" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/791198a2c6254db10131eecfe8c06670700904db", + "reference": "791198a2c6254db10131eecfe8c06670700904db", + "shasum": "" + }, + "require": { + "ext-tokenizer": "*", + "php": "^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^6.2.4" + }, + "time": "2017-11-27T05:48:46+00:00", + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "installation-source": "dist", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Wrapper around PHP's tokenizer extension.", + "homepage": "https://github.com/sebastianbergmann/php-token-stream/", + "keywords": [ + "tokenizer" + ] + }, + { + "name": "phpunit/php-code-coverage", + "version": "5.3.0", + "version_normalized": "5.3.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-code-coverage.git", + "reference": "661f34d0bd3f1a7225ef491a70a020ad23a057a1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/661f34d0bd3f1a7225ef491a70a020ad23a057a1", + "reference": "661f34d0bd3f1a7225ef491a70a020ad23a057a1", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-xmlwriter": "*", + "php": "^7.0", + "phpunit/php-file-iterator": "^1.4.2", + "phpunit/php-text-template": "^1.2.1", + "phpunit/php-token-stream": "^2.0.1", + "sebastian/code-unit-reverse-lookup": "^1.0.1", + "sebastian/environment": "^3.0", + "sebastian/version": "^2.0.1", + "theseer/tokenizer": "^1.1" + }, + "require-dev": { + "phpunit/phpunit": "^6.0" + }, + "suggest": { + "ext-xdebug": "^2.5.5" + }, + "time": "2017-12-06T09:29:45+00:00", + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.3.x-dev" + } + }, + "installation-source": "dist", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", + "homepage": "https://github.com/sebastianbergmann/php-code-coverage", + "keywords": [ + "coverage", + "testing", + "xunit" + ] + }, + { + "name": "webmozart/assert", + "version": "1.2.0", + "version_normalized": "1.2.0.0", + "source": { + "type": "git", + "url": "https://github.com/webmozart/assert.git", + "reference": "2db61e59ff05fe5126d152bd0655c9ea113e550f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/webmozart/assert/zipball/2db61e59ff05fe5126d152bd0655c9ea113e550f", + "reference": "2db61e59ff05fe5126d152bd0655c9ea113e550f", + "shasum": "" + }, + "require": { + "php": "^5.3.3 || ^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.6", + "sebastian/version": "^1.0.1" + }, + "time": "2016-11-23T20:04:58+00:00", + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.3-dev" + } + }, + "installation-source": "dist", + "autoload": { + "psr-4": { + "Webmozart\\Assert\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + } + ], + "description": "Assertions to validate method input/output with nice error messages.", + "keywords": [ + "assert", + "check", + "validate" + ] + }, + { + "name": "phpdocumentor/reflection-common", + "version": "1.0.1", + "version_normalized": "1.0.1.0", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/ReflectionCommon.git", + "reference": "21bdeb5f65d7ebf9f43b1b25d404f87deab5bfb6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/21bdeb5f65d7ebf9f43b1b25d404f87deab5bfb6", + "reference": "21bdeb5f65d7ebf9f43b1b25d404f87deab5bfb6", + "shasum": "" + }, + "require": { + "php": ">=5.5" + }, + "require-dev": { + "phpunit/phpunit": "^4.6" + }, + "time": "2017-09-11T18:02:19+00:00", + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "installation-source": "dist", + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": [ + "src" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jaap van Otterdijk", + "email": "opensource@ijaap.nl" + } + ], + "description": "Common reflection classes used by phpdocumentor to reflect the code structure", + "homepage": "http://www.phpdoc.org", + "keywords": [ + "FQSEN", + "phpDocumentor", + "phpdoc", + "reflection", + "static analysis" + ] + }, + { + "name": "phpdocumentor/type-resolver", + "version": "0.4.0", + "version_normalized": "0.4.0.0", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/TypeResolver.git", + "reference": "9c977708995954784726e25d0cd1dddf4e65b0f7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/9c977708995954784726e25d0cd1dddf4e65b0f7", + "reference": "9c977708995954784726e25d0cd1dddf4e65b0f7", + "shasum": "" + }, + "require": { + "php": "^5.5 || ^7.0", + "phpdocumentor/reflection-common": "^1.0" + }, + "require-dev": { + "mockery/mockery": "^0.9.4", + "phpunit/phpunit": "^5.2||^4.8.24" + }, + "time": "2017-07-14T14:27:02+00:00", + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "installation-source": "dist", + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": [ + "src/" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mike van Riel", + "email": "me@mikevanriel.com" + } + ] + }, + { + "name": "phpdocumentor/reflection-docblock", + "version": "4.2.0", + "version_normalized": "4.2.0.0", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", + "reference": "66465776cfc249844bde6d117abff1d22e06c2da" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/66465776cfc249844bde6d117abff1d22e06c2da", + "reference": "66465776cfc249844bde6d117abff1d22e06c2da", + "shasum": "" + }, + "require": { + "php": "^7.0", + "phpdocumentor/reflection-common": "^1.0.0", + "phpdocumentor/type-resolver": "^0.4.0", + "webmozart/assert": "^1.0" + }, + "require-dev": { + "doctrine/instantiator": "~1.0.5", + "mockery/mockery": "^1.0", + "phpunit/phpunit": "^6.4" + }, + "time": "2017-11-27T17:38:31+00:00", + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.x-dev" + } + }, + "installation-source": "dist", + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": [ + "src/" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mike van Riel", + "email": "me@mikevanriel.com" + } + ], + "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock." + }, + { + "name": "phpspec/prophecy", + "version": "1.7.3", + "version_normalized": "1.7.3.0", + "source": { + "type": "git", + "url": "https://github.com/phpspec/prophecy.git", + "reference": "e4ed002c67da8eceb0eb8ddb8b3847bb53c5c2bf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpspec/prophecy/zipball/e4ed002c67da8eceb0eb8ddb8b3847bb53c5c2bf", + "reference": "e4ed002c67da8eceb0eb8ddb8b3847bb53c5c2bf", + "shasum": "" + }, + "require": { + "doctrine/instantiator": "^1.0.2", + "php": "^5.3|^7.0", + "phpdocumentor/reflection-docblock": "^2.0|^3.0.2|^4.0", + "sebastian/comparator": "^1.1|^2.0", + "sebastian/recursion-context": "^1.0|^2.0|^3.0" + }, + "require-dev": { + "phpspec/phpspec": "^2.5|^3.2", + "phpunit/phpunit": "^4.8.35 || ^5.7" + }, + "time": "2017-11-24T13:59:53+00:00", + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.7.x-dev" + } + }, + "installation-source": "dist", + "autoload": { + "psr-0": { + "Prophecy\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Konstantin Kudryashov", + "email": "ever.zet@gmail.com", + "homepage": "http://everzet.com" + }, + { + "name": "Marcello Duarte", + "email": "marcello.duarte@gmail.com" + } + ], + "description": "Highly opinionated mocking framework for PHP 5.3+", + "homepage": "https://github.com/phpspec/prophecy", + "keywords": [ + "Double", + "Dummy", + "fake", + "mock", + "spy", + "stub" + ] + }, + { + "name": "phar-io/version", + "version": "1.0.1", + "version_normalized": "1.0.1.0", + "source": { + "type": "git", + "url": "https://github.com/phar-io/version.git", + "reference": "a70c0ced4be299a63d32fa96d9281d03e94041df" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/version/zipball/a70c0ced4be299a63d32fa96d9281d03e94041df", + "reference": "a70c0ced4be299a63d32fa96d9281d03e94041df", + "shasum": "" + }, + "require": { + "php": "^5.6 || ^7.0" + }, + "time": "2017-03-05T17:38:23+00:00", + "type": "library", + "installation-source": "dist", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Library for handling version information and constraints" + }, + { + "name": "phar-io/manifest", + "version": "1.0.1", + "version_normalized": "1.0.1.0", + "source": { + "type": "git", + "url": "https://github.com/phar-io/manifest.git", + "reference": "2df402786ab5368a0169091f61a7c1e0eb6852d0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/manifest/zipball/2df402786ab5368a0169091f61a7c1e0eb6852d0", + "reference": "2df402786ab5368a0169091f61a7c1e0eb6852d0", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-phar": "*", + "phar-io/version": "^1.0.1", + "php": "^5.6 || ^7.0" + }, + "time": "2017-03-05T18:14:27+00:00", + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "installation-source": "dist", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)" + }, + { + "name": "myclabs/deep-copy", + "version": "1.7.0", + "version_normalized": "1.7.0.0", + "source": { + "type": "git", + "url": "https://github.com/myclabs/DeepCopy.git", + "reference": "3b8a3a99ba1f6a3952ac2747d989303cbd6b7a3e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/3b8a3a99ba1f6a3952ac2747d989303cbd6b7a3e", + "reference": "3b8a3a99ba1f6a3952ac2747d989303cbd6b7a3e", + "shasum": "" + }, + "require": { + "php": "^5.6 || ^7.0" + }, + "require-dev": { + "doctrine/collections": "^1.0", + "doctrine/common": "^2.6", + "phpunit/phpunit": "^4.1" + }, + "time": "2017-10-19T19:58:43+00:00", + "type": "library", + "installation-source": "dist", + "autoload": { + "psr-4": { + "DeepCopy\\": "src/DeepCopy/" + }, + "files": [ + "src/DeepCopy/deep_copy.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Create deep copies (clones) of your objects", + "keywords": [ + "clone", + "copy", + "duplicate", + "object", + "object graph" + ] + }, + { + "name": "phpunit/phpunit", + "version": "6.5.5", + "version_normalized": "6.5.5.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/phpunit.git", + "reference": "83d27937a310f2984fd575686138597147bdc7df" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/83d27937a310f2984fd575686138597147bdc7df", + "reference": "83d27937a310f2984fd575686138597147bdc7df", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-json": "*", + "ext-libxml": "*", + "ext-mbstring": "*", + "ext-xml": "*", + "myclabs/deep-copy": "^1.6.1", + "phar-io/manifest": "^1.0.1", + "phar-io/version": "^1.0", + "php": "^7.0", + "phpspec/prophecy": "^1.7", + "phpunit/php-code-coverage": "^5.3", + "phpunit/php-file-iterator": "^1.4.3", + "phpunit/php-text-template": "^1.2.1", + "phpunit/php-timer": "^1.0.9", + "phpunit/phpunit-mock-objects": "^5.0.5", + "sebastian/comparator": "^2.1", + "sebastian/diff": "^2.0", + "sebastian/environment": "^3.1", + "sebastian/exporter": "^3.1", + "sebastian/global-state": "^2.0", + "sebastian/object-enumerator": "^3.0.3", + "sebastian/resource-operations": "^1.0", + "sebastian/version": "^2.0.1" + }, + "conflict": { + "phpdocumentor/reflection-docblock": "3.0.2", + "phpunit/dbunit": "<3.0" + }, + "require-dev": { + "ext-pdo": "*" + }, + "suggest": { + "ext-xdebug": "*", + "phpunit/php-invoker": "^1.1" + }, + "time": "2017-12-17T06:31:19+00:00", + "bin": [ + "phpunit" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "6.5.x-dev" + } + }, + "installation-source": "dist", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "The PHP Unit Testing framework.", + "homepage": "https://phpunit.de/", + "keywords": [ + "phpunit", + "testing", + "xunit" + ] + } +] diff --git a/www/wiki/tests/phpunit/data/composer/new-composer.json b/www/wiki/tests/phpunit/data/composer/new-composer.json index 0634c2dd..3a886769 100644 --- a/www/wiki/tests/phpunit/data/composer/new-composer.json +++ b/www/wiki/tests/phpunit/data/composer/new-composer.json @@ -9,7 +9,7 @@ "homepage": "https://www.mediawiki.org/wiki/Special:Version/Credits" } ], - "license": "GPL-2.0", + "license": "GPL-2.0-only", "support": { "issues": "https://bugzilla.wikimedia.org/", "irc": "irc://irc.freenode.net/mediawiki", diff --git a/www/wiki/tests/phpunit/data/cssmin/circle.svg b/www/wiki/tests/phpunit/data/cssmin/circle.svg index 4f7af217..415d9920 100644 --- a/www/wiki/tests/phpunit/data/cssmin/circle.svg +++ b/www/wiki/tests/phpunit/data/cssmin/circle.svg @@ -1,4 +1,5 @@ <?xml version="1.0" encoding="UTF-8"?> -<svg xmlns="http://www.w3.org/2000/svg" width="8" height="8"> +<svg xmlns="http://www.w3.org/2000/svg" width="8" height="8" viewBox="0 0 8 8"> <circle cx="4" cy="4" r="2"/> -</svg> + <a xmlns:xlink="http://www.w3.org/1999/xlink" xlink:title="?>">test</a> +</svg>
\ No newline at end of file diff --git a/www/wiki/tests/phpunit/data/db/sqlite/tables-1.19.sql b/www/wiki/tests/phpunit/data/db/sqlite/tables-1.19.sql new file mode 100644 index 00000000..db853fcb --- /dev/null +++ b/www/wiki/tests/phpunit/data/db/sqlite/tables-1.19.sql @@ -0,0 +1,531 @@ +-- This is a copy of MediaWiki 1.19 schema shared by MySQL and SQLite. +-- It is used for updater testing. Comments are stripped to decrease +-- file size, as we don't need to maintain it. + +CREATE TABLE /*_*/user ( + user_id int unsigned NOT NULL PRIMARY KEY AUTO_INCREMENT, + user_name varchar(255) binary NOT NULL default '', + user_real_name varchar(255) binary NOT NULL default '', + user_password tinyblob NOT NULL, + user_newpassword tinyblob NOT NULL, + user_newpass_time binary(14), + user_email tinytext NOT NULL, + user_touched binary(14) NOT NULL default '', + user_token binary(32) NOT NULL default '', + user_email_authenticated binary(14), + user_email_token binary(32), + user_email_token_expires binary(14), + user_registration binary(14), + user_editcount int +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/user_name ON /*_*/user (user_name); +CREATE INDEX /*i*/user_email_token ON /*_*/user (user_email_token); +CREATE INDEX /*i*/user_email ON /*_*/user (user_email(50)); +CREATE TABLE /*_*/user_groups ( + ug_user int unsigned NOT NULL default 0, + ug_group varbinary(32) NOT NULL default '' +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/ug_user_group ON /*_*/user_groups (ug_user,ug_group); +CREATE INDEX /*i*/ug_group ON /*_*/user_groups (ug_group); +CREATE TABLE /*_*/user_former_groups ( + ufg_user int unsigned NOT NULL default 0, + ufg_group varbinary(32) NOT NULL default '' +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/ufg_user_group ON /*_*/user_former_groups (ufg_user,ufg_group); +CREATE TABLE /*_*/user_newtalk ( + user_id int NOT NULL default 0, + user_ip varbinary(40) NOT NULL default '', + user_last_timestamp varbinary(14) NULL default NULL +) /*$wgDBTableOptions*/; +CREATE INDEX /*i*/un_user_id ON /*_*/user_newtalk (user_id); +CREATE INDEX /*i*/un_user_ip ON /*_*/user_newtalk (user_ip); +CREATE TABLE /*_*/user_properties ( + up_user int NOT NULL, + up_property varbinary(255) NOT NULL, + up_value blob +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/user_properties_user_property ON /*_*/user_properties (up_user,up_property); +CREATE INDEX /*i*/user_properties_property ON /*_*/user_properties (up_property); +CREATE TABLE /*_*/page ( + page_id int unsigned NOT NULL PRIMARY KEY AUTO_INCREMENT, + page_namespace int NOT NULL, + page_title varchar(255) binary NOT NULL, + page_restrictions tinyblob NOT NULL, + page_counter bigint unsigned NOT NULL default 0, + page_is_redirect tinyint unsigned NOT NULL default 0, + page_is_new tinyint unsigned NOT NULL default 0, + page_random real unsigned NOT NULL, + page_touched binary(14) NOT NULL default '', + page_latest int unsigned NOT NULL, + page_len int unsigned NOT NULL +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/name_title ON /*_*/page (page_namespace,page_title); +CREATE INDEX /*i*/page_random ON /*_*/page (page_random); +CREATE INDEX /*i*/page_len ON /*_*/page (page_len); +CREATE INDEX /*i*/page_redirect_namespace_len ON /*_*/page (page_is_redirect, page_namespace, page_len); +CREATE TABLE /*_*/revision ( + rev_id int unsigned NOT NULL PRIMARY KEY AUTO_INCREMENT, + rev_page int unsigned NOT NULL, + rev_text_id int unsigned NOT NULL, + rev_comment tinyblob NOT NULL, + rev_user int unsigned NOT NULL default 0, + rev_user_text varchar(255) binary NOT NULL default '', + rev_timestamp binary(14) NOT NULL default '', + rev_minor_edit tinyint unsigned NOT NULL default 0, + rev_deleted tinyint unsigned NOT NULL default 0, + rev_len int unsigned, + rev_parent_id int unsigned default NULL, + rev_sha1 varbinary(32) NOT NULL default '' +) /*$wgDBTableOptions*/ MAX_ROWS=10000000 AVG_ROW_LENGTH=1024; +CREATE UNIQUE INDEX /*i*/rev_page_id ON /*_*/revision (rev_page, rev_id); +CREATE INDEX /*i*/rev_timestamp ON /*_*/revision (rev_timestamp); +CREATE INDEX /*i*/page_timestamp ON /*_*/revision (rev_page,rev_timestamp); +CREATE INDEX /*i*/user_timestamp ON /*_*/revision (rev_user,rev_timestamp); +CREATE INDEX /*i*/usertext_timestamp ON /*_*/revision (rev_user_text,rev_timestamp); +CREATE TABLE /*_*/text ( + old_id int unsigned NOT NULL PRIMARY KEY AUTO_INCREMENT, + old_text mediumblob NOT NULL, + old_flags tinyblob NOT NULL +) /*$wgDBTableOptions*/ MAX_ROWS=10000000 AVG_ROW_LENGTH=10240; +CREATE TABLE /*_*/archive ( + ar_namespace int NOT NULL default 0, + ar_title varchar(255) binary NOT NULL default '', + ar_text mediumblob NOT NULL, + ar_comment tinyblob NOT NULL, + ar_user int unsigned NOT NULL default 0, + ar_user_text varchar(255) binary NOT NULL, + ar_timestamp binary(14) NOT NULL default '', + ar_minor_edit tinyint NOT NULL default 0, + ar_flags tinyblob NOT NULL, + ar_rev_id int unsigned, + ar_text_id int unsigned, + ar_deleted tinyint unsigned NOT NULL default 0, + ar_len int unsigned, + ar_page_id int unsigned, + ar_parent_id int unsigned default NULL, + ar_sha1 varbinary(32) NOT NULL default '' +) /*$wgDBTableOptions*/; +CREATE INDEX /*i*/name_title_timestamp ON /*_*/archive (ar_namespace,ar_title,ar_timestamp); +CREATE INDEX /*i*/ar_usertext_timestamp ON /*_*/archive (ar_user_text,ar_timestamp); +CREATE INDEX /*i*/ar_revid ON /*_*/archive (ar_rev_id); +CREATE TABLE /*_*/pagelinks ( + pl_from int unsigned NOT NULL default 0, + pl_namespace int NOT NULL default 0, + pl_title varchar(255) binary NOT NULL default '' +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/pl_from ON /*_*/pagelinks (pl_from,pl_namespace,pl_title); +CREATE UNIQUE INDEX /*i*/pl_namespace ON /*_*/pagelinks (pl_namespace,pl_title,pl_from); +CREATE TABLE /*_*/templatelinks ( + tl_from int unsigned NOT NULL default 0, + tl_namespace int NOT NULL default 0, + tl_title varchar(255) binary NOT NULL default '' +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/tl_from ON /*_*/templatelinks (tl_from,tl_namespace,tl_title); +CREATE UNIQUE INDEX /*i*/tl_namespace ON /*_*/templatelinks (tl_namespace,tl_title,tl_from); +CREATE TABLE /*_*/imagelinks ( + il_from int unsigned NOT NULL default 0, + il_to varchar(255) binary NOT NULL default '' +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/il_from ON /*_*/imagelinks (il_from,il_to); +CREATE UNIQUE INDEX /*i*/il_to ON /*_*/imagelinks (il_to,il_from); +CREATE TABLE /*_*/categorylinks ( + cl_from int unsigned NOT NULL default 0, + cl_to varchar(255) binary NOT NULL default '', + cl_sortkey varbinary(230) NOT NULL default '', + cl_sortkey_prefix varchar(255) binary NOT NULL default '', + cl_timestamp timestamp NOT NULL, + cl_collation varbinary(32) NOT NULL default '', + cl_type ENUM('page', 'subcat', 'file') NOT NULL default 'page' +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/cl_from ON /*_*/categorylinks (cl_from,cl_to); +CREATE INDEX /*i*/cl_sortkey ON /*_*/categorylinks (cl_to,cl_type,cl_sortkey,cl_from); +CREATE INDEX /*i*/cl_timestamp ON /*_*/categorylinks (cl_to,cl_timestamp); +CREATE INDEX /*i*/cl_collation ON /*_*/categorylinks (cl_collation); +CREATE TABLE /*_*/category ( + cat_id int unsigned NOT NULL PRIMARY KEY AUTO_INCREMENT, + cat_title varchar(255) binary NOT NULL, + cat_pages int signed NOT NULL default 0, + cat_subcats int signed NOT NULL default 0, + cat_files int signed NOT NULL default 0, + cat_hidden tinyint unsigned NOT NULL default 0 +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/cat_title ON /*_*/category (cat_title); +CREATE INDEX /*i*/cat_pages ON /*_*/category (cat_pages); +CREATE TABLE /*_*/externallinks ( + el_from int unsigned NOT NULL default 0, + el_to blob NOT NULL, + el_index blob NOT NULL +) /*$wgDBTableOptions*/; +CREATE INDEX /*i*/el_from ON /*_*/externallinks (el_from, el_to(40)); +CREATE INDEX /*i*/el_to ON /*_*/externallinks (el_to(60), el_from); +CREATE INDEX /*i*/el_index ON /*_*/externallinks (el_index(60)); +CREATE TABLE /*_*/external_user ( + eu_local_id int unsigned NOT NULL PRIMARY KEY, + eu_external_id varchar(255) binary NOT NULL +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/eu_external_id ON /*_*/external_user (eu_external_id); +CREATE TABLE /*_*/langlinks ( + ll_from int unsigned NOT NULL default 0, + ll_lang varbinary(20) NOT NULL default '', + ll_title varchar(255) binary NOT NULL default '' +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/ll_from ON /*_*/langlinks (ll_from, ll_lang); +CREATE INDEX /*i*/ll_lang ON /*_*/langlinks (ll_lang, ll_title); +CREATE TABLE /*_*/iwlinks ( + iwl_from int unsigned NOT NULL default 0, + iwl_prefix varbinary(20) NOT NULL default '', + iwl_title varchar(255) binary NOT NULL default '' +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/iwl_from ON /*_*/iwlinks (iwl_from, iwl_prefix, iwl_title); +CREATE UNIQUE INDEX /*i*/iwl_prefix_title_from ON /*_*/iwlinks (iwl_prefix, iwl_title, iwl_from); +CREATE TABLE /*_*/site_stats ( + ss_row_id int unsigned NOT NULL, + ss_total_views bigint unsigned default 0, + ss_total_edits bigint unsigned default 0, + ss_good_articles bigint unsigned default 0, + ss_total_pages bigint default '-1', + ss_users bigint default '-1', + ss_active_users bigint default '-1', + ss_admins int default '-1', + ss_images int default 0 +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/ss_row_id ON /*_*/site_stats (ss_row_id); +CREATE TABLE /*_*/hitcounter ( + hc_id int unsigned NOT NULL +) ENGINE=HEAP MAX_ROWS=25000; +CREATE TABLE /*_*/ipblocks ( + ipb_id int NOT NULL PRIMARY KEY AUTO_INCREMENT, + ipb_address tinyblob NOT NULL, + ipb_user int unsigned NOT NULL default 0, + ipb_by int unsigned NOT NULL default 0, + ipb_by_text varchar(255) binary NOT NULL default '', + ipb_reason tinyblob NOT NULL, + ipb_timestamp binary(14) NOT NULL default '', + ipb_auto bool NOT NULL default 0, + ipb_anon_only bool NOT NULL default 0, + ipb_create_account bool NOT NULL default 1, + ipb_enable_autoblock bool NOT NULL default '1', + ipb_expiry varbinary(14) NOT NULL default '', + ipb_range_start tinyblob NOT NULL, + ipb_range_end tinyblob NOT NULL, + ipb_deleted bool NOT NULL default 0, + ipb_block_email bool NOT NULL default 0, + ipb_allow_usertalk bool NOT NULL default 0 +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/ipb_address ON /*_*/ipblocks (ipb_address(255), ipb_user, ipb_auto, ipb_anon_only); +CREATE INDEX /*i*/ipb_user ON /*_*/ipblocks (ipb_user); +CREATE INDEX /*i*/ipb_range ON /*_*/ipblocks (ipb_range_start(8), ipb_range_end(8)); +CREATE INDEX /*i*/ipb_timestamp ON /*_*/ipblocks (ipb_timestamp); +CREATE INDEX /*i*/ipb_expiry ON /*_*/ipblocks (ipb_expiry); +CREATE TABLE /*_*/image ( + img_name varchar(255) binary NOT NULL default '' PRIMARY KEY, + img_size int unsigned NOT NULL default 0, + img_width int NOT NULL default 0, + img_height int NOT NULL default 0, + img_metadata mediumblob NOT NULL, + img_bits int NOT NULL default 0, + img_media_type ENUM("UNKNOWN", "BITMAP", "DRAWING", "AUDIO", "VIDEO", "MULTIMEDIA", "OFFICE", "TEXT", "EXECUTABLE", "ARCHIVE") default NULL, + img_major_mime ENUM("unknown", "application", "audio", "image", "text", "video", "message", "model", "multipart") NOT NULL default "unknown", + img_minor_mime varbinary(100) NOT NULL default "unknown", + img_description tinyblob NOT NULL, + img_user int unsigned NOT NULL default 0, + img_user_text varchar(255) binary NOT NULL, + img_timestamp varbinary(14) NOT NULL default '', + img_sha1 varbinary(32) NOT NULL default '' +) /*$wgDBTableOptions*/; +CREATE INDEX /*i*/img_usertext_timestamp ON /*_*/image (img_user_text,img_timestamp); +CREATE INDEX /*i*/img_size ON /*_*/image (img_size); +CREATE INDEX /*i*/img_timestamp ON /*_*/image (img_timestamp); +CREATE INDEX /*i*/img_sha1 ON /*_*/image (img_sha1); +CREATE TABLE /*_*/oldimage ( + oi_name varchar(255) binary NOT NULL default '', + oi_archive_name varchar(255) binary NOT NULL default '', + oi_size int unsigned NOT NULL default 0, + oi_width int NOT NULL default 0, + oi_height int NOT NULL default 0, + oi_bits int NOT NULL default 0, + oi_description tinyblob NOT NULL, + oi_user int unsigned NOT NULL default 0, + oi_user_text varchar(255) binary NOT NULL, + oi_timestamp binary(14) NOT NULL default '', + oi_metadata mediumblob NOT NULL, + oi_media_type ENUM("UNKNOWN", "BITMAP", "DRAWING", "AUDIO", "VIDEO", "MULTIMEDIA", "OFFICE", "TEXT", "EXECUTABLE", "ARCHIVE") default NULL, + oi_major_mime ENUM("unknown", "application", "audio", "image", "text", "video", "message", "model", "multipart") NOT NULL default "unknown", + oi_minor_mime varbinary(100) NOT NULL default "unknown", + oi_deleted tinyint unsigned NOT NULL default 0, + oi_sha1 varbinary(32) NOT NULL default '' +) /*$wgDBTableOptions*/; +CREATE INDEX /*i*/oi_usertext_timestamp ON /*_*/oldimage (oi_user_text,oi_timestamp); +CREATE INDEX /*i*/oi_name_timestamp ON /*_*/oldimage (oi_name,oi_timestamp); +CREATE INDEX /*i*/oi_name_archive_name ON /*_*/oldimage (oi_name,oi_archive_name(14)); +CREATE INDEX /*i*/oi_sha1 ON /*_*/oldimage (oi_sha1); +CREATE TABLE /*_*/filearchive ( + fa_id int NOT NULL PRIMARY KEY AUTO_INCREMENT, + fa_name varchar(255) binary NOT NULL default '', + fa_archive_name varchar(255) binary default '', + fa_storage_group varbinary(16), + fa_storage_key varbinary(64) default '', + fa_deleted_user int, + fa_deleted_timestamp binary(14) default '', + fa_deleted_reason text, + fa_size int unsigned default 0, + fa_width int default 0, + fa_height int default 0, + fa_metadata mediumblob, + fa_bits int default 0, + fa_media_type ENUM("UNKNOWN", "BITMAP", "DRAWING", "AUDIO", "VIDEO", "MULTIMEDIA", "OFFICE", "TEXT", "EXECUTABLE", "ARCHIVE") default NULL, + fa_major_mime ENUM("unknown", "application", "audio", "image", "text", "video", "message", "model", "multipart") default "unknown", + fa_minor_mime varbinary(100) default "unknown", + fa_description tinyblob, + fa_user int unsigned default 0, + fa_user_text varchar(255) binary, + fa_timestamp binary(14) default '', + fa_deleted tinyint unsigned NOT NULL default 0 +) /*$wgDBTableOptions*/; +CREATE INDEX /*i*/fa_name ON /*_*/filearchive (fa_name, fa_timestamp); +CREATE INDEX /*i*/fa_storage_group ON /*_*/filearchive (fa_storage_group, fa_storage_key); +CREATE INDEX /*i*/fa_deleted_timestamp ON /*_*/filearchive (fa_deleted_timestamp); +CREATE INDEX /*i*/fa_user_timestamp ON /*_*/filearchive (fa_user_text,fa_timestamp); +CREATE TABLE /*_*/uploadstash ( + us_id int unsigned NOT NULL PRIMARY KEY auto_increment, + us_user int unsigned NOT NULL, + us_key varchar(255) NOT NULL, + us_orig_path varchar(255) NOT NULL, + us_path varchar(255) NOT NULL, + us_source_type varchar(50), + us_timestamp varbinary(14) not null, + us_status varchar(50) not null, + us_chunk_inx int unsigned NULL, + us_size int unsigned NOT NULL, + us_sha1 varchar(31) NOT NULL, + us_mime varchar(255), + us_media_type ENUM("UNKNOWN", "BITMAP", "DRAWING", "AUDIO", "VIDEO", "MULTIMEDIA", "OFFICE", "TEXT", "EXECUTABLE", "ARCHIVE") default NULL, + us_image_width int unsigned, + us_image_height int unsigned, + us_image_bits smallint unsigned +) /*$wgDBTableOptions*/; +CREATE INDEX /*i*/us_user ON /*_*/uploadstash (us_user); +CREATE UNIQUE INDEX /*i*/us_key ON /*_*/uploadstash (us_key); +CREATE INDEX /*i*/us_timestamp ON /*_*/uploadstash (us_timestamp); +CREATE TABLE /*_*/recentchanges ( + rc_id int NOT NULL PRIMARY KEY AUTO_INCREMENT, + rc_timestamp varbinary(14) NOT NULL default '', + rc_cur_time varbinary(14) NOT NULL default '', + rc_user int unsigned NOT NULL default 0, + rc_user_text varchar(255) binary NOT NULL, + rc_namespace int NOT NULL default 0, + rc_title varchar(255) binary NOT NULL default '', + rc_comment varchar(255) binary NOT NULL default '', + rc_minor tinyint unsigned NOT NULL default 0, + rc_bot tinyint unsigned NOT NULL default 0, + rc_new tinyint unsigned NOT NULL default 0, + rc_cur_id int unsigned NOT NULL default 0, + rc_this_oldid int unsigned NOT NULL default 0, + rc_last_oldid int unsigned NOT NULL default 0, + rc_type tinyint unsigned NOT NULL default 0, + rc_moved_to_ns tinyint unsigned NOT NULL default 0, + rc_moved_to_title varchar(255) binary NOT NULL default '', + rc_patrolled tinyint unsigned NOT NULL default 0, + rc_ip varbinary(40) NOT NULL default '', + rc_old_len int, + rc_new_len int, + rc_deleted tinyint unsigned NOT NULL default 0, + rc_logid int unsigned NOT NULL default 0, + rc_log_type varbinary(255) NULL default NULL, + rc_log_action varbinary(255) NULL default NULL, + rc_params blob NULL +) /*$wgDBTableOptions*/; +CREATE INDEX /*i*/rc_timestamp ON /*_*/recentchanges (rc_timestamp); +CREATE INDEX /*i*/rc_namespace_title ON /*_*/recentchanges (rc_namespace, rc_title); +CREATE INDEX /*i*/rc_cur_id ON /*_*/recentchanges (rc_cur_id); +CREATE INDEX /*i*/new_name_timestamp ON /*_*/recentchanges (rc_new,rc_namespace,rc_timestamp); +CREATE INDEX /*i*/rc_ip ON /*_*/recentchanges (rc_ip); +CREATE INDEX /*i*/rc_ns_usertext ON /*_*/recentchanges (rc_namespace, rc_user_text); +CREATE INDEX /*i*/rc_user_text ON /*_*/recentchanges (rc_user_text, rc_timestamp); +CREATE TABLE /*_*/watchlist ( + wl_user int unsigned NOT NULL, + wl_namespace int NOT NULL default 0, + wl_title varchar(255) binary NOT NULL default '', + wl_notificationtimestamp varbinary(14) +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/wl_user ON /*_*/watchlist (wl_user, wl_namespace, wl_title); +CREATE INDEX /*i*/namespace_title ON /*_*/watchlist (wl_namespace, wl_title); +CREATE TABLE /*_*/searchindex ( + si_page int unsigned NOT NULL, + si_title varchar(255) NOT NULL default '', + si_text mediumtext NOT NULL +) ENGINE=MyISAM; +CREATE UNIQUE INDEX /*i*/si_page ON /*_*/searchindex (si_page); +CREATE FULLTEXT INDEX /*i*/si_title ON /*_*/searchindex (si_title); +CREATE FULLTEXT INDEX /*i*/si_text ON /*_*/searchindex (si_text); +CREATE TABLE /*_*/interwiki ( + iw_prefix varchar(32) NOT NULL, + iw_url blob NOT NULL, + iw_api blob NOT NULL, + iw_wikiid varchar(64) NOT NULL, + iw_local bool NOT NULL, + iw_trans tinyint NOT NULL default 0 +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/iw_prefix ON /*_*/interwiki (iw_prefix); +CREATE TABLE /*_*/querycache ( + qc_type varbinary(32) NOT NULL, + qc_value int unsigned NOT NULL default 0, + qc_namespace int NOT NULL default 0, + qc_title varchar(255) binary NOT NULL default '' +) /*$wgDBTableOptions*/; +CREATE INDEX /*i*/qc_type ON /*_*/querycache (qc_type,qc_value); +CREATE TABLE /*_*/objectcache ( + keyname varbinary(255) NOT NULL default '' PRIMARY KEY, + value mediumblob, + exptime datetime +) /*$wgDBTableOptions*/; +CREATE INDEX /*i*/exptime ON /*_*/objectcache (exptime); +CREATE TABLE /*_*/transcache ( + tc_url varbinary(255) NOT NULL, + tc_contents text, + tc_time binary(14) NOT NULL +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/tc_url_idx ON /*_*/transcache (tc_url); +CREATE TABLE /*_*/logging ( + log_id int unsigned NOT NULL PRIMARY KEY AUTO_INCREMENT, + log_type varbinary(32) NOT NULL default '', + log_action varbinary(32) NOT NULL default '', + log_timestamp binary(14) NOT NULL default '19700101000000', + log_user int unsigned NOT NULL default 0, + log_user_text varchar(255) binary NOT NULL default '', + log_namespace int NOT NULL default 0, + log_title varchar(255) binary NOT NULL default '', + log_page int unsigned NULL, + log_comment varchar(255) NOT NULL default '', + log_params blob NOT NULL, + log_deleted tinyint unsigned NOT NULL default 0 +) /*$wgDBTableOptions*/; +CREATE INDEX /*i*/type_time ON /*_*/logging (log_type, log_timestamp); +CREATE INDEX /*i*/user_time ON /*_*/logging (log_user, log_timestamp); +CREATE INDEX /*i*/page_time ON /*_*/logging (log_namespace, log_title, log_timestamp); +CREATE INDEX /*i*/times ON /*_*/logging (log_timestamp); +CREATE INDEX /*i*/log_user_type_time ON /*_*/logging (log_user, log_type, log_timestamp); +CREATE INDEX /*i*/log_page_id_time ON /*_*/logging (log_page,log_timestamp); +CREATE INDEX /*i*/type_action ON /*_*/logging(log_type, log_action, log_timestamp); +CREATE TABLE /*_*/log_search ( + ls_field varbinary(32) NOT NULL, + ls_value varchar(255) NOT NULL, + ls_log_id int unsigned NOT NULL default 0 +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/ls_field_val ON /*_*/log_search (ls_field,ls_value,ls_log_id); +CREATE INDEX /*i*/ls_log_id ON /*_*/log_search (ls_log_id); +CREATE TABLE /*_*/job ( + job_id int unsigned NOT NULL PRIMARY KEY AUTO_INCREMENT, + job_cmd varbinary(60) NOT NULL default '', + job_namespace int NOT NULL, + job_title varchar(255) binary NOT NULL, + job_timestamp varbinary(14) NULL default NULL, + job_params blob NOT NULL +) /*$wgDBTableOptions*/; +CREATE INDEX /*i*/job_cmd ON /*_*/job (job_cmd, job_namespace, job_title, job_params(128)); +CREATE INDEX /*i*/job_timestamp ON /*_*/job(job_timestamp); +CREATE TABLE /*_*/querycache_info ( + qci_type varbinary(32) NOT NULL default '', + qci_timestamp binary(14) NOT NULL default '19700101000000' +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/qci_type ON /*_*/querycache_info (qci_type); +CREATE TABLE /*_*/redirect ( + rd_from int unsigned NOT NULL default 0 PRIMARY KEY, + rd_namespace int NOT NULL default 0, + rd_title varchar(255) binary NOT NULL default '', + rd_interwiki varchar(32) default NULL, + rd_fragment varchar(255) binary default NULL +) /*$wgDBTableOptions*/; +CREATE INDEX /*i*/rd_ns_title ON /*_*/redirect (rd_namespace,rd_title,rd_from); +CREATE TABLE /*_*/querycachetwo ( + qcc_type varbinary(32) NOT NULL, + qcc_value int unsigned NOT NULL default 0, + qcc_namespace int NOT NULL default 0, + qcc_title varchar(255) binary NOT NULL default '', + qcc_namespacetwo int NOT NULL default 0, + qcc_titletwo varchar(255) binary NOT NULL default '' +) /*$wgDBTableOptions*/; +CREATE INDEX /*i*/qcc_type ON /*_*/querycachetwo (qcc_type,qcc_value); +CREATE INDEX /*i*/qcc_title ON /*_*/querycachetwo (qcc_type,qcc_namespace,qcc_title); +CREATE INDEX /*i*/qcc_titletwo ON /*_*/querycachetwo (qcc_type,qcc_namespacetwo,qcc_titletwo); +CREATE TABLE /*_*/page_restrictions ( + pr_page int NOT NULL, + pr_type varbinary(60) NOT NULL, + pr_level varbinary(60) NOT NULL, + pr_cascade tinyint NOT NULL, + pr_user int NULL, + pr_expiry varbinary(14) NULL, + pr_id int unsigned NOT NULL PRIMARY KEY AUTO_INCREMENT +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/pr_pagetype ON /*_*/page_restrictions (pr_page,pr_type); +CREATE INDEX /*i*/pr_typelevel ON /*_*/page_restrictions (pr_type,pr_level); +CREATE INDEX /*i*/pr_level ON /*_*/page_restrictions (pr_level); +CREATE INDEX /*i*/pr_cascade ON /*_*/page_restrictions (pr_cascade); +CREATE TABLE /*_*/protected_titles ( + pt_namespace int NOT NULL, + pt_title varchar(255) binary NOT NULL, + pt_user int unsigned NOT NULL, + pt_reason tinyblob, + pt_timestamp binary(14) NOT NULL, + pt_expiry varbinary(14) NOT NULL default '', + pt_create_perm varbinary(60) NOT NULL +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/pt_namespace_title ON /*_*/protected_titles (pt_namespace,pt_title); +CREATE INDEX /*i*/pt_timestamp ON /*_*/protected_titles (pt_timestamp); +CREATE TABLE /*_*/page_props ( + pp_page int NOT NULL, + pp_propname varbinary(60) NOT NULL, + pp_value blob NOT NULL +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/pp_page_propname ON /*_*/page_props (pp_page,pp_propname); +CREATE TABLE /*_*/updatelog ( + ul_key varchar(255) NOT NULL PRIMARY KEY, + ul_value blob +) /*$wgDBTableOptions*/; +CREATE TABLE /*_*/change_tag ( + ct_rc_id int NULL, + ct_log_id int NULL, + ct_rev_id int NULL, + ct_tag varchar(255) NOT NULL, + ct_params blob NULL +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/change_tag_rc_tag ON /*_*/change_tag (ct_rc_id,ct_tag); +CREATE UNIQUE INDEX /*i*/change_tag_log_tag ON /*_*/change_tag (ct_log_id,ct_tag); +CREATE UNIQUE INDEX /*i*/change_tag_rev_tag ON /*_*/change_tag (ct_rev_id,ct_tag); +CREATE INDEX /*i*/change_tag_tag_id ON /*_*/change_tag (ct_tag,ct_rc_id,ct_rev_id,ct_log_id); +CREATE TABLE /*_*/tag_summary ( + ts_rc_id int NULL, + ts_log_id int NULL, + ts_rev_id int NULL, + ts_tags blob NOT NULL +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/tag_summary_rc_id ON /*_*/tag_summary (ts_rc_id); +CREATE UNIQUE INDEX /*i*/tag_summary_log_id ON /*_*/tag_summary (ts_log_id); +CREATE UNIQUE INDEX /*i*/tag_summary_rev_id ON /*_*/tag_summary (ts_rev_id); +CREATE TABLE /*_*/valid_tag ( + vt_tag varchar(255) NOT NULL PRIMARY KEY +) /*$wgDBTableOptions*/; +CREATE TABLE /*_*/l10n_cache ( + lc_lang varbinary(32) NOT NULL, + lc_key varchar(255) NOT NULL, + lc_value mediumblob NOT NULL +) /*$wgDBTableOptions*/; +CREATE INDEX /*i*/lc_lang_key ON /*_*/l10n_cache (lc_lang, lc_key); +CREATE TABLE /*_*/msg_resource ( + mr_resource varbinary(255) NOT NULL, + mr_lang varbinary(32) NOT NULL, + mr_blob mediumblob NOT NULL, + mr_timestamp binary(14) NOT NULL +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/mr_resource_lang ON /*_*/msg_resource (mr_resource, mr_lang); +CREATE TABLE /*_*/msg_resource_links ( + mrl_resource varbinary(255) NOT NULL, + mrl_message varbinary(255) NOT NULL +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/mrl_message_resource ON /*_*/msg_resource_links (mrl_message, mrl_resource); +CREATE TABLE /*_*/module_deps ( + md_module varbinary(255) NOT NULL, + md_skin varbinary(32) NOT NULL, + md_deps mediumblob NOT NULL +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/md_module_skin ON /*_*/module_deps (md_module, md_skin); diff --git a/www/wiki/tests/phpunit/data/db/sqlite/tables-1.20.sql b/www/wiki/tests/phpunit/data/db/sqlite/tables-1.20.sql new file mode 100644 index 00000000..d6c4f5bc --- /dev/null +++ b/www/wiki/tests/phpunit/data/db/sqlite/tables-1.20.sql @@ -0,0 +1,534 @@ +-- This is a copy of MediaWiki 1.20 schema shared by MySQL and SQLite. +-- It is used for updater testing. Comments are stripped to decrease +-- file size, as we don't need to maintain it. + +CREATE TABLE /*_*/user ( + user_id int unsigned NOT NULL PRIMARY KEY AUTO_INCREMENT, + user_name varchar(255) binary NOT NULL default '', + user_real_name varchar(255) binary NOT NULL default '', + user_password tinyblob NOT NULL, + user_newpassword tinyblob NOT NULL, + user_newpass_time binary(14), + user_email tinytext NOT NULL, + user_touched binary(14) NOT NULL default '', + user_token binary(32) NOT NULL default '', + user_email_authenticated binary(14), + user_email_token binary(32), + user_email_token_expires binary(14), + user_registration binary(14), + user_editcount int +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/user_name ON /*_*/user (user_name); +CREATE INDEX /*i*/user_email_token ON /*_*/user (user_email_token); +CREATE INDEX /*i*/user_email ON /*_*/user (user_email(50)); +CREATE TABLE /*_*/user_groups ( + ug_user int unsigned NOT NULL default 0, + ug_group varbinary(32) NOT NULL default '' +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/ug_user_group ON /*_*/user_groups (ug_user,ug_group); +CREATE INDEX /*i*/ug_group ON /*_*/user_groups (ug_group); +CREATE TABLE /*_*/user_former_groups ( + ufg_user int unsigned NOT NULL default 0, + ufg_group varbinary(32) NOT NULL default '' +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/ufg_user_group ON /*_*/user_former_groups (ufg_user,ufg_group); +CREATE TABLE /*_*/user_newtalk ( + user_id int NOT NULL default 0, + user_ip varbinary(40) NOT NULL default '', + user_last_timestamp varbinary(14) NULL default NULL +) /*$wgDBTableOptions*/; +CREATE INDEX /*i*/un_user_id ON /*_*/user_newtalk (user_id); +CREATE INDEX /*i*/un_user_ip ON /*_*/user_newtalk (user_ip); +CREATE TABLE /*_*/user_properties ( + up_user int NOT NULL, + up_property varbinary(255) NOT NULL, + up_value blob +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/user_properties_user_property ON /*_*/user_properties (up_user,up_property); +CREATE INDEX /*i*/user_properties_property ON /*_*/user_properties (up_property); +CREATE TABLE /*_*/page ( + page_id int unsigned NOT NULL PRIMARY KEY AUTO_INCREMENT, + page_namespace int NOT NULL, + page_title varchar(255) binary NOT NULL, + page_restrictions tinyblob NOT NULL, + page_counter bigint unsigned NOT NULL default 0, + page_is_redirect tinyint unsigned NOT NULL default 0, + page_is_new tinyint unsigned NOT NULL default 0, + page_random real unsigned NOT NULL, + page_touched binary(14) NOT NULL default '', + page_latest int unsigned NOT NULL, + page_len int unsigned NOT NULL +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/name_title ON /*_*/page (page_namespace,page_title); +CREATE INDEX /*i*/page_random ON /*_*/page (page_random); +CREATE INDEX /*i*/page_len ON /*_*/page (page_len); +CREATE INDEX /*i*/page_redirect_namespace_len ON /*_*/page (page_is_redirect, page_namespace, page_len); +CREATE TABLE /*_*/revision ( + rev_id int unsigned NOT NULL PRIMARY KEY AUTO_INCREMENT, + rev_page int unsigned NOT NULL, + rev_text_id int unsigned NOT NULL, + rev_comment tinyblob NOT NULL, + rev_user int unsigned NOT NULL default 0, + rev_user_text varchar(255) binary NOT NULL default '', + rev_timestamp binary(14) NOT NULL default '', + rev_minor_edit tinyint unsigned NOT NULL default 0, + rev_deleted tinyint unsigned NOT NULL default 0, + rev_len int unsigned, + rev_parent_id int unsigned default NULL, + rev_sha1 varbinary(32) NOT NULL default '' +) /*$wgDBTableOptions*/ MAX_ROWS=10000000 AVG_ROW_LENGTH=1024; +CREATE UNIQUE INDEX /*i*/rev_page_id ON /*_*/revision (rev_page, rev_id); +CREATE INDEX /*i*/rev_timestamp ON /*_*/revision (rev_timestamp); +CREATE INDEX /*i*/page_timestamp ON /*_*/revision (rev_page,rev_timestamp); +CREATE INDEX /*i*/user_timestamp ON /*_*/revision (rev_user,rev_timestamp); +CREATE INDEX /*i*/usertext_timestamp ON /*_*/revision (rev_user_text,rev_timestamp); +CREATE INDEX /*i*/page_user_timestamp ON /*_*/revision (rev_page,rev_user,rev_timestamp); +CREATE TABLE /*_*/text ( + old_id int unsigned NOT NULL PRIMARY KEY AUTO_INCREMENT, + old_text mediumblob NOT NULL, + old_flags tinyblob NOT NULL +) /*$wgDBTableOptions*/ MAX_ROWS=10000000 AVG_ROW_LENGTH=10240; +CREATE TABLE /*_*/archive ( + ar_namespace int NOT NULL default 0, + ar_title varchar(255) binary NOT NULL default '', + ar_text mediumblob NOT NULL, + ar_comment tinyblob NOT NULL, + ar_user int unsigned NOT NULL default 0, + ar_user_text varchar(255) binary NOT NULL, + ar_timestamp binary(14) NOT NULL default '', + ar_minor_edit tinyint NOT NULL default 0, + ar_flags tinyblob NOT NULL, + ar_rev_id int unsigned, + ar_text_id int unsigned, + ar_deleted tinyint unsigned NOT NULL default 0, + ar_len int unsigned, + ar_page_id int unsigned, + ar_parent_id int unsigned default NULL, + ar_sha1 varbinary(32) NOT NULL default '' +) /*$wgDBTableOptions*/; +CREATE INDEX /*i*/name_title_timestamp ON /*_*/archive (ar_namespace,ar_title,ar_timestamp); +CREATE INDEX /*i*/ar_usertext_timestamp ON /*_*/archive (ar_user_text,ar_timestamp); +CREATE INDEX /*i*/ar_revid ON /*_*/archive (ar_rev_id); +CREATE TABLE /*_*/pagelinks ( + pl_from int unsigned NOT NULL default 0, + pl_namespace int NOT NULL default 0, + pl_title varchar(255) binary NOT NULL default '' +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/pl_from ON /*_*/pagelinks (pl_from,pl_namespace,pl_title); +CREATE UNIQUE INDEX /*i*/pl_namespace ON /*_*/pagelinks (pl_namespace,pl_title,pl_from); +CREATE TABLE /*_*/templatelinks ( + tl_from int unsigned NOT NULL default 0, + tl_namespace int NOT NULL default 0, + tl_title varchar(255) binary NOT NULL default '' +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/tl_from ON /*_*/templatelinks (tl_from,tl_namespace,tl_title); +CREATE UNIQUE INDEX /*i*/tl_namespace ON /*_*/templatelinks (tl_namespace,tl_title,tl_from); +CREATE TABLE /*_*/imagelinks ( + il_from int unsigned NOT NULL default 0, + il_to varchar(255) binary NOT NULL default '' +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/il_from ON /*_*/imagelinks (il_from,il_to); +CREATE UNIQUE INDEX /*i*/il_to ON /*_*/imagelinks (il_to,il_from); +CREATE TABLE /*_*/categorylinks ( + cl_from int unsigned NOT NULL default 0, + cl_to varchar(255) binary NOT NULL default '', + cl_sortkey varbinary(230) NOT NULL default '', + cl_sortkey_prefix varchar(255) binary NOT NULL default '', + cl_timestamp timestamp NOT NULL, + cl_collation varbinary(32) NOT NULL default '', + cl_type ENUM('page', 'subcat', 'file') NOT NULL default 'page' +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/cl_from ON /*_*/categorylinks (cl_from,cl_to); +CREATE INDEX /*i*/cl_sortkey ON /*_*/categorylinks (cl_to,cl_type,cl_sortkey,cl_from); +CREATE INDEX /*i*/cl_timestamp ON /*_*/categorylinks (cl_to,cl_timestamp); +CREATE INDEX /*i*/cl_collation ON /*_*/categorylinks (cl_collation); +CREATE TABLE /*_*/category ( + cat_id int unsigned NOT NULL PRIMARY KEY AUTO_INCREMENT, + cat_title varchar(255) binary NOT NULL, + cat_pages int signed NOT NULL default 0, + cat_subcats int signed NOT NULL default 0, + cat_files int signed NOT NULL default 0 +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/cat_title ON /*_*/category (cat_title); +CREATE INDEX /*i*/cat_pages ON /*_*/category (cat_pages); +CREATE TABLE /*_*/externallinks ( + el_from int unsigned NOT NULL default 0, + el_to blob NOT NULL, + el_index blob NOT NULL +) /*$wgDBTableOptions*/; +CREATE INDEX /*i*/el_from ON /*_*/externallinks (el_from, el_to(40)); +CREATE INDEX /*i*/el_to ON /*_*/externallinks (el_to(60), el_from); +CREATE INDEX /*i*/el_index ON /*_*/externallinks (el_index(60)); +CREATE TABLE /*_*/external_user ( + eu_local_id int unsigned NOT NULL PRIMARY KEY, + eu_external_id varchar(255) binary NOT NULL +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/eu_external_id ON /*_*/external_user (eu_external_id); +CREATE TABLE /*_*/langlinks ( + ll_from int unsigned NOT NULL default 0, + ll_lang varbinary(20) NOT NULL default '', + ll_title varchar(255) binary NOT NULL default '' +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/ll_from ON /*_*/langlinks (ll_from, ll_lang); +CREATE INDEX /*i*/ll_lang ON /*_*/langlinks (ll_lang, ll_title); +CREATE TABLE /*_*/iwlinks ( + iwl_from int unsigned NOT NULL default 0, + iwl_prefix varbinary(20) NOT NULL default '', + iwl_title varchar(255) binary NOT NULL default '' +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/iwl_from ON /*_*/iwlinks (iwl_from, iwl_prefix, iwl_title); +CREATE UNIQUE INDEX /*i*/iwl_prefix_title_from ON /*_*/iwlinks (iwl_prefix, iwl_title, iwl_from); +CREATE TABLE /*_*/site_stats ( + ss_row_id int unsigned NOT NULL, + ss_total_views bigint unsigned default 0, + ss_total_edits bigint unsigned default 0, + ss_good_articles bigint unsigned default 0, + ss_total_pages bigint default '-1', + ss_users bigint default '-1', + ss_active_users bigint default '-1', + ss_admins int default '-1', + ss_images int default 0 +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/ss_row_id ON /*_*/site_stats (ss_row_id); +CREATE TABLE /*_*/hitcounter ( + hc_id int unsigned NOT NULL +) ENGINE=HEAP MAX_ROWS=25000; +CREATE TABLE /*_*/ipblocks ( + ipb_id int NOT NULL PRIMARY KEY AUTO_INCREMENT, + ipb_address tinyblob NOT NULL, + ipb_user int unsigned NOT NULL default 0, + ipb_by int unsigned NOT NULL default 0, + ipb_by_text varchar(255) binary NOT NULL default '', + ipb_reason tinyblob NOT NULL, + ipb_timestamp binary(14) NOT NULL default '', + ipb_auto bool NOT NULL default 0, + ipb_anon_only bool NOT NULL default 0, + ipb_create_account bool NOT NULL default 1, + ipb_enable_autoblock bool NOT NULL default '1', + ipb_expiry varbinary(14) NOT NULL default '', + ipb_range_start tinyblob NOT NULL, + ipb_range_end tinyblob NOT NULL, + ipb_deleted bool NOT NULL default 0, + ipb_block_email bool NOT NULL default 0, + ipb_allow_usertalk bool NOT NULL default 0, + ipb_parent_block_id int default NULL +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/ipb_address ON /*_*/ipblocks (ipb_address(255), ipb_user, ipb_auto, ipb_anon_only); +CREATE INDEX /*i*/ipb_user ON /*_*/ipblocks (ipb_user); +CREATE INDEX /*i*/ipb_range ON /*_*/ipblocks (ipb_range_start(8), ipb_range_end(8)); +CREATE INDEX /*i*/ipb_timestamp ON /*_*/ipblocks (ipb_timestamp); +CREATE INDEX /*i*/ipb_expiry ON /*_*/ipblocks (ipb_expiry); +CREATE INDEX /*i*/ipb_parent_block_id ON /*_*/ipblocks (ipb_parent_block_id); +CREATE TABLE /*_*/image ( + img_name varchar(255) binary NOT NULL default '' PRIMARY KEY, + img_size int unsigned NOT NULL default 0, + img_width int NOT NULL default 0, + img_height int NOT NULL default 0, + img_metadata mediumblob NOT NULL, + img_bits int NOT NULL default 0, + img_media_type ENUM("UNKNOWN", "BITMAP", "DRAWING", "AUDIO", "VIDEO", "MULTIMEDIA", "OFFICE", "TEXT", "EXECUTABLE", "ARCHIVE") default NULL, + img_major_mime ENUM("unknown", "application", "audio", "image", "text", "video", "message", "model", "multipart") NOT NULL default "unknown", + img_minor_mime varbinary(100) NOT NULL default "unknown", + img_description tinyblob NOT NULL, + img_user int unsigned NOT NULL default 0, + img_user_text varchar(255) binary NOT NULL, + img_timestamp varbinary(14) NOT NULL default '', + img_sha1 varbinary(32) NOT NULL default '' +) /*$wgDBTableOptions*/; +CREATE INDEX /*i*/img_usertext_timestamp ON /*_*/image (img_user_text,img_timestamp); +CREATE INDEX /*i*/img_size ON /*_*/image (img_size); +CREATE INDEX /*i*/img_timestamp ON /*_*/image (img_timestamp); +CREATE INDEX /*i*/img_sha1 ON /*_*/image (img_sha1); +CREATE TABLE /*_*/oldimage ( + oi_name varchar(255) binary NOT NULL default '', + oi_archive_name varchar(255) binary NOT NULL default '', + oi_size int unsigned NOT NULL default 0, + oi_width int NOT NULL default 0, + oi_height int NOT NULL default 0, + oi_bits int NOT NULL default 0, + oi_description tinyblob NOT NULL, + oi_user int unsigned NOT NULL default 0, + oi_user_text varchar(255) binary NOT NULL, + oi_timestamp binary(14) NOT NULL default '', + oi_metadata mediumblob NOT NULL, + oi_media_type ENUM("UNKNOWN", "BITMAP", "DRAWING", "AUDIO", "VIDEO", "MULTIMEDIA", "OFFICE", "TEXT", "EXECUTABLE", "ARCHIVE") default NULL, + oi_major_mime ENUM("unknown", "application", "audio", "image", "text", "video", "message", "model", "multipart") NOT NULL default "unknown", + oi_minor_mime varbinary(100) NOT NULL default "unknown", + oi_deleted tinyint unsigned NOT NULL default 0, + oi_sha1 varbinary(32) NOT NULL default '' +) /*$wgDBTableOptions*/; +CREATE INDEX /*i*/oi_usertext_timestamp ON /*_*/oldimage (oi_user_text,oi_timestamp); +CREATE INDEX /*i*/oi_name_timestamp ON /*_*/oldimage (oi_name,oi_timestamp); +CREATE INDEX /*i*/oi_name_archive_name ON /*_*/oldimage (oi_name,oi_archive_name(14)); +CREATE INDEX /*i*/oi_sha1 ON /*_*/oldimage (oi_sha1); +CREATE TABLE /*_*/filearchive ( + fa_id int NOT NULL PRIMARY KEY AUTO_INCREMENT, + fa_name varchar(255) binary NOT NULL default '', + fa_archive_name varchar(255) binary default '', + fa_storage_group varbinary(16), + fa_storage_key varbinary(64) default '', + fa_deleted_user int, + fa_deleted_timestamp binary(14) default '', + fa_deleted_reason text, + fa_size int unsigned default 0, + fa_width int default 0, + fa_height int default 0, + fa_metadata mediumblob, + fa_bits int default 0, + fa_media_type ENUM("UNKNOWN", "BITMAP", "DRAWING", "AUDIO", "VIDEO", "MULTIMEDIA", "OFFICE", "TEXT", "EXECUTABLE", "ARCHIVE") default NULL, + fa_major_mime ENUM("unknown", "application", "audio", "image", "text", "video", "message", "model", "multipart") default "unknown", + fa_minor_mime varbinary(100) default "unknown", + fa_description tinyblob, + fa_user int unsigned default 0, + fa_user_text varchar(255) binary, + fa_timestamp binary(14) default '', + fa_deleted tinyint unsigned NOT NULL default 0 +) /*$wgDBTableOptions*/; +CREATE INDEX /*i*/fa_name ON /*_*/filearchive (fa_name, fa_timestamp); +CREATE INDEX /*i*/fa_storage_group ON /*_*/filearchive (fa_storage_group, fa_storage_key); +CREATE INDEX /*i*/fa_deleted_timestamp ON /*_*/filearchive (fa_deleted_timestamp); +CREATE INDEX /*i*/fa_user_timestamp ON /*_*/filearchive (fa_user_text,fa_timestamp); +CREATE TABLE /*_*/uploadstash ( + us_id int unsigned NOT NULL PRIMARY KEY AUTO_INCREMENT, + us_user int unsigned NOT NULL, + us_key varchar(255) NOT NULL, + us_orig_path varchar(255) NOT NULL, + us_path varchar(255) NOT NULL, + us_source_type varchar(50), + us_timestamp varbinary(14) NOT NULL, + us_status varchar(50) NOT NULL, + us_chunk_inx int unsigned NULL, + us_size int unsigned NOT NULL, + us_sha1 varchar(31) NOT NULL, + us_mime varchar(255), + us_media_type ENUM("UNKNOWN", "BITMAP", "DRAWING", "AUDIO", "VIDEO", "MULTIMEDIA", "OFFICE", "TEXT", "EXECUTABLE", "ARCHIVE") default NULL, + us_image_width int unsigned, + us_image_height int unsigned, + us_image_bits smallint unsigned +) /*$wgDBTableOptions*/; +CREATE INDEX /*i*/us_user ON /*_*/uploadstash (us_user); +CREATE UNIQUE INDEX /*i*/us_key ON /*_*/uploadstash (us_key); +CREATE INDEX /*i*/us_timestamp ON /*_*/uploadstash (us_timestamp); +CREATE TABLE /*_*/recentchanges ( + rc_id int NOT NULL PRIMARY KEY AUTO_INCREMENT, + rc_timestamp varbinary(14) NOT NULL default '', + rc_cur_time varbinary(14) NOT NULL default '', + rc_user int unsigned NOT NULL default 0, + rc_user_text varchar(255) binary NOT NULL, + rc_namespace int NOT NULL default 0, + rc_title varchar(255) binary NOT NULL default '', + rc_comment varchar(255) binary NOT NULL default '', + rc_minor tinyint unsigned NOT NULL default 0, + rc_bot tinyint unsigned NOT NULL default 0, + rc_new tinyint unsigned NOT NULL default 0, + rc_cur_id int unsigned NOT NULL default 0, + rc_this_oldid int unsigned NOT NULL default 0, + rc_last_oldid int unsigned NOT NULL default 0, + rc_type tinyint unsigned NOT NULL default 0, + rc_moved_to_ns tinyint unsigned NOT NULL default 0, + rc_moved_to_title varchar(255) binary NOT NULL default '', + rc_patrolled tinyint unsigned NOT NULL default 0, + rc_ip varbinary(40) NOT NULL default '', + rc_old_len int, + rc_new_len int, + rc_deleted tinyint unsigned NOT NULL default 0, + rc_logid int unsigned NOT NULL default 0, + rc_log_type varbinary(255) NULL default NULL, + rc_log_action varbinary(255) NULL default NULL, + rc_params blob NULL +) /*$wgDBTableOptions*/; +CREATE INDEX /*i*/rc_timestamp ON /*_*/recentchanges (rc_timestamp); +CREATE INDEX /*i*/rc_namespace_title ON /*_*/recentchanges (rc_namespace, rc_title); +CREATE INDEX /*i*/rc_cur_id ON /*_*/recentchanges (rc_cur_id); +CREATE INDEX /*i*/new_name_timestamp ON /*_*/recentchanges (rc_new,rc_namespace,rc_timestamp); +CREATE INDEX /*i*/rc_ip ON /*_*/recentchanges (rc_ip); +CREATE INDEX /*i*/rc_ns_usertext ON /*_*/recentchanges (rc_namespace, rc_user_text); +CREATE INDEX /*i*/rc_user_text ON /*_*/recentchanges (rc_user_text, rc_timestamp); +CREATE TABLE /*_*/watchlist ( + wl_user int unsigned NOT NULL, + wl_namespace int NOT NULL default 0, + wl_title varchar(255) binary NOT NULL default '', + wl_notificationtimestamp varbinary(14) +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/wl_user ON /*_*/watchlist (wl_user, wl_namespace, wl_title); +CREATE INDEX /*i*/namespace_title ON /*_*/watchlist (wl_namespace, wl_title); +CREATE TABLE /*_*/searchindex ( + si_page int unsigned NOT NULL, + si_title varchar(255) NOT NULL default '', + si_text mediumtext NOT NULL +) ENGINE=MyISAM; +CREATE UNIQUE INDEX /*i*/si_page ON /*_*/searchindex (si_page); +CREATE FULLTEXT INDEX /*i*/si_title ON /*_*/searchindex (si_title); +CREATE FULLTEXT INDEX /*i*/si_text ON /*_*/searchindex (si_text); +CREATE TABLE /*_*/interwiki ( + iw_prefix varchar(32) NOT NULL, + iw_url blob NOT NULL, + iw_api blob NOT NULL, + iw_wikiid varchar(64) NOT NULL, + iw_local bool NOT NULL, + iw_trans tinyint NOT NULL default 0 +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/iw_prefix ON /*_*/interwiki (iw_prefix); +CREATE TABLE /*_*/querycache ( + qc_type varbinary(32) NOT NULL, + qc_value int unsigned NOT NULL default 0, + qc_namespace int NOT NULL default 0, + qc_title varchar(255) binary NOT NULL default '' +) /*$wgDBTableOptions*/; +CREATE INDEX /*i*/qc_type ON /*_*/querycache (qc_type,qc_value); +CREATE TABLE /*_*/objectcache ( + keyname varbinary(255) NOT NULL default '' PRIMARY KEY, + value mediumblob, + exptime datetime +) /*$wgDBTableOptions*/; +CREATE INDEX /*i*/exptime ON /*_*/objectcache (exptime); +CREATE TABLE /*_*/transcache ( + tc_url varbinary(255) NOT NULL, + tc_contents text, + tc_time binary(14) NOT NULL +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/tc_url_idx ON /*_*/transcache (tc_url); +CREATE TABLE /*_*/logging ( + log_id int unsigned NOT NULL PRIMARY KEY AUTO_INCREMENT, + log_type varbinary(32) NOT NULL default '', + log_action varbinary(32) NOT NULL default '', + log_timestamp binary(14) NOT NULL default '19700101000000', + log_user int unsigned NOT NULL default 0, + log_user_text varchar(255) binary NOT NULL default '', + log_namespace int NOT NULL default 0, + log_title varchar(255) binary NOT NULL default '', + log_page int unsigned NULL, + log_comment varchar(255) NOT NULL default '', + log_params blob NOT NULL, + log_deleted tinyint unsigned NOT NULL default 0 +) /*$wgDBTableOptions*/; +CREATE INDEX /*i*/type_time ON /*_*/logging (log_type, log_timestamp); +CREATE INDEX /*i*/user_time ON /*_*/logging (log_user, log_timestamp); +CREATE INDEX /*i*/page_time ON /*_*/logging (log_namespace, log_title, log_timestamp); +CREATE INDEX /*i*/times ON /*_*/logging (log_timestamp); +CREATE INDEX /*i*/log_user_type_time ON /*_*/logging (log_user, log_type, log_timestamp); +CREATE INDEX /*i*/log_page_id_time ON /*_*/logging (log_page,log_timestamp); +CREATE INDEX /*i*/type_action ON /*_*/logging (log_type, log_action, log_timestamp); +CREATE TABLE /*_*/log_search ( + ls_field varbinary(32) NOT NULL, + ls_value varchar(255) NOT NULL, + ls_log_id int unsigned NOT NULL default 0 +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/ls_field_val ON /*_*/log_search (ls_field,ls_value,ls_log_id); +CREATE INDEX /*i*/ls_log_id ON /*_*/log_search (ls_log_id); +CREATE TABLE /*_*/job ( + job_id int unsigned NOT NULL PRIMARY KEY AUTO_INCREMENT, + job_cmd varbinary(60) NOT NULL default '', + job_namespace int NOT NULL, + job_title varchar(255) binary NOT NULL, + job_timestamp varbinary(14) NULL default NULL, + job_params blob NOT NULL +) /*$wgDBTableOptions*/; +CREATE INDEX /*i*/job_cmd ON /*_*/job (job_cmd, job_namespace, job_title, job_params(128)); +CREATE INDEX /*i*/job_timestamp ON /*_*/job (job_timestamp); +CREATE TABLE /*_*/querycache_info ( + qci_type varbinary(32) NOT NULL default '', + qci_timestamp binary(14) NOT NULL default '19700101000000' +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/qci_type ON /*_*/querycache_info (qci_type); +CREATE TABLE /*_*/redirect ( + rd_from int unsigned NOT NULL default 0 PRIMARY KEY, + rd_namespace int NOT NULL default 0, + rd_title varchar(255) binary NOT NULL default '', + rd_interwiki varchar(32) default NULL, + rd_fragment varchar(255) binary default NULL +) /*$wgDBTableOptions*/; +CREATE INDEX /*i*/rd_ns_title ON /*_*/redirect (rd_namespace,rd_title,rd_from); +CREATE TABLE /*_*/querycachetwo ( + qcc_type varbinary(32) NOT NULL, + qcc_value int unsigned NOT NULL default 0, + qcc_namespace int NOT NULL default 0, + qcc_title varchar(255) binary NOT NULL default '', + qcc_namespacetwo int NOT NULL default 0, + qcc_titletwo varchar(255) binary NOT NULL default '' +) /*$wgDBTableOptions*/; +CREATE INDEX /*i*/qcc_type ON /*_*/querycachetwo (qcc_type,qcc_value); +CREATE INDEX /*i*/qcc_title ON /*_*/querycachetwo (qcc_type,qcc_namespace,qcc_title); +CREATE INDEX /*i*/qcc_titletwo ON /*_*/querycachetwo (qcc_type,qcc_namespacetwo,qcc_titletwo); +CREATE TABLE /*_*/page_restrictions ( + pr_page int NOT NULL, + pr_type varbinary(60) NOT NULL, + pr_level varbinary(60) NOT NULL, + pr_cascade tinyint NOT NULL, + pr_user int NULL, + pr_expiry varbinary(14) NULL, + pr_id int unsigned NOT NULL PRIMARY KEY AUTO_INCREMENT +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/pr_pagetype ON /*_*/page_restrictions (pr_page,pr_type); +CREATE INDEX /*i*/pr_typelevel ON /*_*/page_restrictions (pr_type,pr_level); +CREATE INDEX /*i*/pr_level ON /*_*/page_restrictions (pr_level); +CREATE INDEX /*i*/pr_cascade ON /*_*/page_restrictions (pr_cascade); +CREATE TABLE /*_*/protected_titles ( + pt_namespace int NOT NULL, + pt_title varchar(255) binary NOT NULL, + pt_user int unsigned NOT NULL, + pt_reason tinyblob, + pt_timestamp binary(14) NOT NULL, + pt_expiry varbinary(14) NOT NULL default '', + pt_create_perm varbinary(60) NOT NULL +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/pt_namespace_title ON /*_*/protected_titles (pt_namespace,pt_title); +CREATE INDEX /*i*/pt_timestamp ON /*_*/protected_titles (pt_timestamp); +CREATE TABLE /*_*/page_props ( + pp_page int NOT NULL, + pp_propname varbinary(60) NOT NULL, + pp_value blob NOT NULL +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/pp_page_propname ON /*_*/page_props (pp_page,pp_propname); +CREATE TABLE /*_*/updatelog ( + ul_key varchar(255) NOT NULL PRIMARY KEY, + ul_value blob +) /*$wgDBTableOptions*/; +CREATE TABLE /*_*/change_tag ( + ct_rc_id int NULL, + ct_log_id int NULL, + ct_rev_id int NULL, + ct_tag varchar(255) NOT NULL, + ct_params blob NULL +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/change_tag_rc_tag ON /*_*/change_tag (ct_rc_id,ct_tag); +CREATE UNIQUE INDEX /*i*/change_tag_log_tag ON /*_*/change_tag (ct_log_id,ct_tag); +CREATE UNIQUE INDEX /*i*/change_tag_rev_tag ON /*_*/change_tag (ct_rev_id,ct_tag); +CREATE INDEX /*i*/change_tag_tag_id ON /*_*/change_tag (ct_tag,ct_rc_id,ct_rev_id,ct_log_id); +CREATE TABLE /*_*/tag_summary ( + ts_rc_id int NULL, + ts_log_id int NULL, + ts_rev_id int NULL, + ts_tags blob NOT NULL +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/tag_summary_rc_id ON /*_*/tag_summary (ts_rc_id); +CREATE UNIQUE INDEX /*i*/tag_summary_log_id ON /*_*/tag_summary (ts_log_id); +CREATE UNIQUE INDEX /*i*/tag_summary_rev_id ON /*_*/tag_summary (ts_rev_id); +CREATE TABLE /*_*/valid_tag ( + vt_tag varchar(255) NOT NULL PRIMARY KEY +) /*$wgDBTableOptions*/; +CREATE TABLE /*_*/l10n_cache ( + lc_lang varbinary(32) NOT NULL, + lc_key varchar(255) NOT NULL, + lc_value mediumblob NOT NULL +) /*$wgDBTableOptions*/; +CREATE INDEX /*i*/lc_lang_key ON /*_*/l10n_cache (lc_lang, lc_key); +CREATE TABLE /*_*/msg_resource ( + mr_resource varbinary(255) NOT NULL, + mr_lang varbinary(32) NOT NULL, + mr_blob mediumblob NOT NULL, + mr_timestamp binary(14) NOT NULL +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/mr_resource_lang ON /*_*/msg_resource (mr_resource, mr_lang); +CREATE TABLE /*_*/msg_resource_links ( + mrl_resource varbinary(255) NOT NULL, + mrl_message varbinary(255) NOT NULL +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/mrl_message_resource ON /*_*/msg_resource_links (mrl_message, mrl_resource); +CREATE TABLE /*_*/module_deps ( + md_module varbinary(255) NOT NULL, + md_skin varbinary(32) NOT NULL, + md_deps mediumblob NOT NULL +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/md_module_skin ON /*_*/module_deps (md_module, md_skin); +-- vim: sw=2 sts=2 et
\ No newline at end of file diff --git a/www/wiki/tests/phpunit/data/db/sqlite/tables-1.21.sql b/www/wiki/tests/phpunit/data/db/sqlite/tables-1.21.sql new file mode 100644 index 00000000..dbc84a60 --- /dev/null +++ b/www/wiki/tests/phpunit/data/db/sqlite/tables-1.21.sql @@ -0,0 +1,577 @@ +-- This is a copy of MediaWiki 1.21 schema shared by MySQL and SQLite. +-- It is used for updater testing. Comments are stripped to decrease +-- file size, as we don't need to maintain it. + +CREATE TABLE /*_*/user ( + user_id int unsigned NOT NULL PRIMARY KEY AUTO_INCREMENT, + user_name varchar(255) binary NOT NULL default '', + user_real_name varchar(255) binary NOT NULL default '', + user_password tinyblob NOT NULL, + user_newpassword tinyblob NOT NULL, + user_newpass_time binary(14), + user_email tinytext NOT NULL, + user_touched binary(14) NOT NULL default '', + user_token binary(32) NOT NULL default '', + user_email_authenticated binary(14), + user_email_token binary(32), + user_email_token_expires binary(14), + user_registration binary(14), + user_editcount int +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/user_name ON /*_*/user (user_name); +CREATE INDEX /*i*/user_email_token ON /*_*/user (user_email_token); +CREATE INDEX /*i*/user_email ON /*_*/user (user_email(50)); +CREATE TABLE /*_*/user_groups ( + ug_user int unsigned NOT NULL default 0, + ug_group varbinary(255) NOT NULL default '' +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/ug_user_group ON /*_*/user_groups (ug_user,ug_group); +CREATE INDEX /*i*/ug_group ON /*_*/user_groups (ug_group); +CREATE TABLE /*_*/user_former_groups ( + ufg_user int unsigned NOT NULL default 0, + ufg_group varbinary(255) NOT NULL default '' +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/ufg_user_group ON /*_*/user_former_groups (ufg_user,ufg_group); +CREATE TABLE /*_*/user_newtalk ( + user_id int NOT NULL default 0, + user_ip varbinary(40) NOT NULL default '', + user_last_timestamp varbinary(14) NULL default NULL +) /*$wgDBTableOptions*/; +CREATE INDEX /*i*/un_user_id ON /*_*/user_newtalk (user_id); +CREATE INDEX /*i*/un_user_ip ON /*_*/user_newtalk (user_ip); +CREATE TABLE /*_*/user_properties ( + up_user int NOT NULL, + up_property varbinary(255) NOT NULL, + up_value blob +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/user_properties_user_property ON /*_*/user_properties (up_user,up_property); +CREATE INDEX /*i*/user_properties_property ON /*_*/user_properties (up_property); +CREATE TABLE /*_*/page ( + page_id int unsigned NOT NULL PRIMARY KEY AUTO_INCREMENT, + page_namespace int NOT NULL, + page_title varchar(255) binary NOT NULL, + page_restrictions tinyblob NOT NULL, + page_counter bigint unsigned NOT NULL default 0, + page_is_redirect tinyint unsigned NOT NULL default 0, + page_is_new tinyint unsigned NOT NULL default 0, + page_random real unsigned NOT NULL, + page_touched binary(14) NOT NULL default '', + page_latest int unsigned NOT NULL, + page_len int unsigned NOT NULL, + page_content_model varbinary(32) DEFAULT NULL +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/name_title ON /*_*/page (page_namespace,page_title); +CREATE INDEX /*i*/page_random ON /*_*/page (page_random); +CREATE INDEX /*i*/page_len ON /*_*/page (page_len); +CREATE INDEX /*i*/page_redirect_namespace_len ON /*_*/page (page_is_redirect, page_namespace, page_len); +CREATE TABLE /*_*/revision ( + rev_id int unsigned NOT NULL PRIMARY KEY AUTO_INCREMENT, + rev_page int unsigned NOT NULL, + rev_text_id int unsigned NOT NULL, + rev_comment tinyblob NOT NULL, + rev_user int unsigned NOT NULL default 0, + rev_user_text varchar(255) binary NOT NULL default '', + rev_timestamp binary(14) NOT NULL default '', + rev_minor_edit tinyint unsigned NOT NULL default 0, + rev_deleted tinyint unsigned NOT NULL default 0, + rev_len int unsigned, + rev_parent_id int unsigned default NULL, + rev_sha1 varbinary(32) NOT NULL default '', + rev_content_model varbinary(32) DEFAULT NULL, + rev_content_format varbinary(64) DEFAULT NULL +) /*$wgDBTableOptions*/ MAX_ROWS=10000000 AVG_ROW_LENGTH=1024; +CREATE UNIQUE INDEX /*i*/rev_page_id ON /*_*/revision (rev_page, rev_id); +CREATE INDEX /*i*/rev_timestamp ON /*_*/revision (rev_timestamp); +CREATE INDEX /*i*/page_timestamp ON /*_*/revision (rev_page,rev_timestamp); +CREATE INDEX /*i*/user_timestamp ON /*_*/revision (rev_user,rev_timestamp); +CREATE INDEX /*i*/usertext_timestamp ON /*_*/revision (rev_user_text,rev_timestamp); +CREATE INDEX /*i*/page_user_timestamp ON /*_*/revision (rev_page,rev_user,rev_timestamp); +CREATE TABLE /*_*/text ( + old_id int unsigned NOT NULL PRIMARY KEY AUTO_INCREMENT, + old_text mediumblob NOT NULL, + old_flags tinyblob NOT NULL +) /*$wgDBTableOptions*/ MAX_ROWS=10000000 AVG_ROW_LENGTH=10240; +CREATE TABLE /*_*/archive ( + ar_namespace int NOT NULL default 0, + ar_title varchar(255) binary NOT NULL default '', + ar_text mediumblob NOT NULL, + ar_comment tinyblob NOT NULL, + ar_user int unsigned NOT NULL default 0, + ar_user_text varchar(255) binary NOT NULL, + ar_timestamp binary(14) NOT NULL default '', + ar_minor_edit tinyint NOT NULL default 0, + ar_flags tinyblob NOT NULL, + ar_rev_id int unsigned, + ar_text_id int unsigned, + ar_deleted tinyint unsigned NOT NULL default 0, + ar_len int unsigned, + ar_page_id int unsigned, + ar_parent_id int unsigned default NULL, + ar_sha1 varbinary(32) NOT NULL default '', + ar_content_model varbinary(32) DEFAULT NULL, + ar_content_format varbinary(64) DEFAULT NULL +) /*$wgDBTableOptions*/; +CREATE INDEX /*i*/name_title_timestamp ON /*_*/archive (ar_namespace,ar_title,ar_timestamp); +CREATE INDEX /*i*/ar_usertext_timestamp ON /*_*/archive (ar_user_text,ar_timestamp); +CREATE INDEX /*i*/ar_revid ON /*_*/archive (ar_rev_id); +CREATE TABLE /*_*/pagelinks ( + pl_from int unsigned NOT NULL default 0, + pl_namespace int NOT NULL default 0, + pl_title varchar(255) binary NOT NULL default '' +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/pl_from ON /*_*/pagelinks (pl_from,pl_namespace,pl_title); +CREATE UNIQUE INDEX /*i*/pl_namespace ON /*_*/pagelinks (pl_namespace,pl_title,pl_from); +CREATE TABLE /*_*/templatelinks ( + tl_from int unsigned NOT NULL default 0, + tl_namespace int NOT NULL default 0, + tl_title varchar(255) binary NOT NULL default '' +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/tl_from ON /*_*/templatelinks (tl_from,tl_namespace,tl_title); +CREATE UNIQUE INDEX /*i*/tl_namespace ON /*_*/templatelinks (tl_namespace,tl_title,tl_from); +CREATE TABLE /*_*/imagelinks ( + il_from int unsigned NOT NULL default 0, + il_to varchar(255) binary NOT NULL default '' +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/il_from ON /*_*/imagelinks (il_from,il_to); +CREATE UNIQUE INDEX /*i*/il_to ON /*_*/imagelinks (il_to,il_from); +CREATE TABLE /*_*/categorylinks ( + cl_from int unsigned NOT NULL default 0, + cl_to varchar(255) binary NOT NULL default '', + cl_sortkey varbinary(230) NOT NULL default '', + cl_sortkey_prefix varchar(255) binary NOT NULL default '', + cl_timestamp timestamp NOT NULL, + cl_collation varbinary(32) NOT NULL default '', + cl_type ENUM('page', 'subcat', 'file') NOT NULL default 'page' +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/cl_from ON /*_*/categorylinks (cl_from,cl_to); +CREATE INDEX /*i*/cl_sortkey ON /*_*/categorylinks (cl_to,cl_type,cl_sortkey,cl_from); +CREATE INDEX /*i*/cl_timestamp ON /*_*/categorylinks (cl_to,cl_timestamp); +CREATE INDEX /*i*/cl_collation ON /*_*/categorylinks (cl_collation); +CREATE TABLE /*_*/category ( + cat_id int unsigned NOT NULL PRIMARY KEY AUTO_INCREMENT, + cat_title varchar(255) binary NOT NULL, + cat_pages int signed NOT NULL default 0, + cat_subcats int signed NOT NULL default 0, + cat_files int signed NOT NULL default 0 +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/cat_title ON /*_*/category (cat_title); +CREATE INDEX /*i*/cat_pages ON /*_*/category (cat_pages); +CREATE TABLE /*_*/externallinks ( + el_from int unsigned NOT NULL default 0, + el_to blob NOT NULL, + el_index blob NOT NULL +) /*$wgDBTableOptions*/; +CREATE INDEX /*i*/el_from ON /*_*/externallinks (el_from, el_to(40)); +CREATE INDEX /*i*/el_to ON /*_*/externallinks (el_to(60), el_from); +CREATE INDEX /*i*/el_index ON /*_*/externallinks (el_index(60)); +CREATE TABLE /*_*/external_user ( + eu_local_id int unsigned NOT NULL PRIMARY KEY, + eu_external_id varchar(255) binary NOT NULL +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/eu_external_id ON /*_*/external_user (eu_external_id); +CREATE TABLE /*_*/langlinks ( + ll_from int unsigned NOT NULL default 0, + ll_lang varbinary(20) NOT NULL default '', + ll_title varchar(255) binary NOT NULL default '' +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/ll_from ON /*_*/langlinks (ll_from, ll_lang); +CREATE INDEX /*i*/ll_lang ON /*_*/langlinks (ll_lang, ll_title); +CREATE TABLE /*_*/iwlinks ( + iwl_from int unsigned NOT NULL default 0, + iwl_prefix varbinary(20) NOT NULL default '', + iwl_title varchar(255) binary NOT NULL default '' +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/iwl_from ON /*_*/iwlinks (iwl_from, iwl_prefix, iwl_title); +CREATE UNIQUE INDEX /*i*/iwl_prefix_title_from ON /*_*/iwlinks (iwl_prefix, iwl_title, iwl_from); +CREATE TABLE /*_*/site_stats ( + ss_row_id int unsigned NOT NULL, + ss_total_views bigint unsigned default 0, + ss_total_edits bigint unsigned default 0, + ss_good_articles bigint unsigned default 0, + ss_total_pages bigint default '-1', + ss_users bigint default '-1', + ss_active_users bigint default '-1', + ss_images int default 0 +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/ss_row_id ON /*_*/site_stats (ss_row_id); +CREATE TABLE /*_*/hitcounter ( + hc_id int unsigned NOT NULL +) ENGINE=HEAP MAX_ROWS=25000; +CREATE TABLE /*_*/ipblocks ( + ipb_id int NOT NULL PRIMARY KEY AUTO_INCREMENT, + ipb_address tinyblob NOT NULL, + ipb_user int unsigned NOT NULL default 0, + ipb_by int unsigned NOT NULL default 0, + ipb_by_text varchar(255) binary NOT NULL default '', + ipb_reason tinyblob NOT NULL, + ipb_timestamp binary(14) NOT NULL default '', + ipb_auto bool NOT NULL default 0, + ipb_anon_only bool NOT NULL default 0, + ipb_create_account bool NOT NULL default 1, + ipb_enable_autoblock bool NOT NULL default '1', + ipb_expiry varbinary(14) NOT NULL default '', + ipb_range_start tinyblob NOT NULL, + ipb_range_end tinyblob NOT NULL, + ipb_deleted bool NOT NULL default 0, + ipb_block_email bool NOT NULL default 0, + ipb_allow_usertalk bool NOT NULL default 0, + ipb_parent_block_id int default NULL +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/ipb_address ON /*_*/ipblocks (ipb_address(255), ipb_user, ipb_auto, ipb_anon_only); +CREATE INDEX /*i*/ipb_user ON /*_*/ipblocks (ipb_user); +CREATE INDEX /*i*/ipb_range ON /*_*/ipblocks (ipb_range_start(8), ipb_range_end(8)); +CREATE INDEX /*i*/ipb_timestamp ON /*_*/ipblocks (ipb_timestamp); +CREATE INDEX /*i*/ipb_expiry ON /*_*/ipblocks (ipb_expiry); +CREATE INDEX /*i*/ipb_parent_block_id ON /*_*/ipblocks (ipb_parent_block_id); +CREATE TABLE /*_*/image ( + img_name varchar(255) binary NOT NULL default '' PRIMARY KEY, + img_size int unsigned NOT NULL default 0, + img_width int NOT NULL default 0, + img_height int NOT NULL default 0, + img_metadata mediumblob NOT NULL, + img_bits int NOT NULL default 0, + img_media_type ENUM("UNKNOWN", "BITMAP", "DRAWING", "AUDIO", "VIDEO", "MULTIMEDIA", "OFFICE", "TEXT", "EXECUTABLE", "ARCHIVE") default NULL, + img_major_mime ENUM("unknown", "application", "audio", "image", "text", "video", "message", "model", "multipart") NOT NULL default "unknown", + img_minor_mime varbinary(100) NOT NULL default "unknown", + img_description tinyblob NOT NULL, + img_user int unsigned NOT NULL default 0, + img_user_text varchar(255) binary NOT NULL, + img_timestamp varbinary(14) NOT NULL default '', + img_sha1 varbinary(32) NOT NULL default '' +) /*$wgDBTableOptions*/; +CREATE INDEX /*i*/img_usertext_timestamp ON /*_*/image (img_user_text,img_timestamp); +CREATE INDEX /*i*/img_size ON /*_*/image (img_size); +CREATE INDEX /*i*/img_timestamp ON /*_*/image (img_timestamp); +CREATE INDEX /*i*/img_sha1 ON /*_*/image (img_sha1(10)); +CREATE INDEX /*i*/img_media_mime ON /*_*/image (img_media_type,img_major_mime,img_minor_mime); +CREATE TABLE /*_*/oldimage ( + oi_name varchar(255) binary NOT NULL default '', + oi_archive_name varchar(255) binary NOT NULL default '', + oi_size int unsigned NOT NULL default 0, + oi_width int NOT NULL default 0, + oi_height int NOT NULL default 0, + oi_bits int NOT NULL default 0, + oi_description tinyblob NOT NULL, + oi_user int unsigned NOT NULL default 0, + oi_user_text varchar(255) binary NOT NULL, + oi_timestamp binary(14) NOT NULL default '', + oi_metadata mediumblob NOT NULL, + oi_media_type ENUM("UNKNOWN", "BITMAP", "DRAWING", "AUDIO", "VIDEO", "MULTIMEDIA", "OFFICE", "TEXT", "EXECUTABLE", "ARCHIVE") default NULL, + oi_major_mime ENUM("unknown", "application", "audio", "image", "text", "video", "message", "model", "multipart") NOT NULL default "unknown", + oi_minor_mime varbinary(100) NOT NULL default "unknown", + oi_deleted tinyint unsigned NOT NULL default 0, + oi_sha1 varbinary(32) NOT NULL default '' +) /*$wgDBTableOptions*/; +CREATE INDEX /*i*/oi_usertext_timestamp ON /*_*/oldimage (oi_user_text,oi_timestamp); +CREATE INDEX /*i*/oi_name_timestamp ON /*_*/oldimage (oi_name,oi_timestamp); +CREATE INDEX /*i*/oi_name_archive_name ON /*_*/oldimage (oi_name,oi_archive_name(14)); +CREATE INDEX /*i*/oi_sha1 ON /*_*/oldimage (oi_sha1(10)); +CREATE TABLE /*_*/filearchive ( + fa_id int NOT NULL PRIMARY KEY AUTO_INCREMENT, + fa_name varchar(255) binary NOT NULL default '', + fa_archive_name varchar(255) binary default '', + fa_storage_group varbinary(16), + fa_storage_key varbinary(64) default '', + fa_deleted_user int, + fa_deleted_timestamp binary(14) default '', + fa_deleted_reason text, + fa_size int unsigned default 0, + fa_width int default 0, + fa_height int default 0, + fa_metadata mediumblob, + fa_bits int default 0, + fa_media_type ENUM("UNKNOWN", "BITMAP", "DRAWING", "AUDIO", "VIDEO", "MULTIMEDIA", "OFFICE", "TEXT", "EXECUTABLE", "ARCHIVE") default NULL, + fa_major_mime ENUM("unknown", "application", "audio", "image", "text", "video", "message", "model", "multipart") default "unknown", + fa_minor_mime varbinary(100) default "unknown", + fa_description tinyblob, + fa_user int unsigned default 0, + fa_user_text varchar(255) binary, + fa_timestamp binary(14) default '', + fa_deleted tinyint unsigned NOT NULL default 0, + fa_sha1 varbinary(32) NOT NULL default '' +) /*$wgDBTableOptions*/; +CREATE INDEX /*i*/fa_name ON /*_*/filearchive (fa_name, fa_timestamp); +CREATE INDEX /*i*/fa_storage_group ON /*_*/filearchive (fa_storage_group, fa_storage_key); +CREATE INDEX /*i*/fa_deleted_timestamp ON /*_*/filearchive (fa_deleted_timestamp); +CREATE INDEX /*i*/fa_user_timestamp ON /*_*/filearchive (fa_user_text,fa_timestamp); +CREATE INDEX /*i*/fa_sha1 ON /*_*/filearchive (fa_sha1(10)); +CREATE TABLE /*_*/uploadstash ( + us_id int unsigned NOT NULL PRIMARY KEY AUTO_INCREMENT, + us_user int unsigned NOT NULL, + us_key varchar(255) NOT NULL, + us_orig_path varchar(255) NOT NULL, + us_path varchar(255) NOT NULL, + us_source_type varchar(50), + us_timestamp varbinary(14) NOT NULL, + us_status varchar(50) NOT NULL, + us_chunk_inx int unsigned NULL, + us_props blob, + us_size int unsigned NOT NULL, + us_sha1 varchar(31) NOT NULL, + us_mime varchar(255), + us_media_type ENUM("UNKNOWN", "BITMAP", "DRAWING", "AUDIO", "VIDEO", "MULTIMEDIA", "OFFICE", "TEXT", "EXECUTABLE", "ARCHIVE") default NULL, + us_image_width int unsigned, + us_image_height int unsigned, + us_image_bits smallint unsigned +) /*$wgDBTableOptions*/; +CREATE INDEX /*i*/us_user ON /*_*/uploadstash (us_user); +CREATE UNIQUE INDEX /*i*/us_key ON /*_*/uploadstash (us_key); +CREATE INDEX /*i*/us_timestamp ON /*_*/uploadstash (us_timestamp); +CREATE TABLE /*_*/recentchanges ( + rc_id int NOT NULL PRIMARY KEY AUTO_INCREMENT, + rc_timestamp varbinary(14) NOT NULL default '', + rc_cur_time varbinary(14) NOT NULL default '', + rc_user int unsigned NOT NULL default 0, + rc_user_text varchar(255) binary NOT NULL, + rc_namespace int NOT NULL default 0, + rc_title varchar(255) binary NOT NULL default '', + rc_comment varchar(255) binary NOT NULL default '', + rc_minor tinyint unsigned NOT NULL default 0, + rc_bot tinyint unsigned NOT NULL default 0, + rc_new tinyint unsigned NOT NULL default 0, + rc_cur_id int unsigned NOT NULL default 0, + rc_this_oldid int unsigned NOT NULL default 0, + rc_last_oldid int unsigned NOT NULL default 0, + rc_type tinyint unsigned NOT NULL default 0, + rc_patrolled tinyint unsigned NOT NULL default 0, + rc_ip varbinary(40) NOT NULL default '', + rc_old_len int, + rc_new_len int, + rc_deleted tinyint unsigned NOT NULL default 0, + rc_logid int unsigned NOT NULL default 0, + rc_log_type varbinary(255) NULL default NULL, + rc_log_action varbinary(255) NULL default NULL, + rc_params blob NULL +) /*$wgDBTableOptions*/; +CREATE INDEX /*i*/rc_timestamp ON /*_*/recentchanges (rc_timestamp); +CREATE INDEX /*i*/rc_namespace_title ON /*_*/recentchanges (rc_namespace, rc_title); +CREATE INDEX /*i*/rc_cur_id ON /*_*/recentchanges (rc_cur_id); +CREATE INDEX /*i*/new_name_timestamp ON /*_*/recentchanges (rc_new,rc_namespace,rc_timestamp); +CREATE INDEX /*i*/rc_ip ON /*_*/recentchanges (rc_ip); +CREATE INDEX /*i*/rc_ns_usertext ON /*_*/recentchanges (rc_namespace, rc_user_text); +CREATE INDEX /*i*/rc_user_text ON /*_*/recentchanges (rc_user_text, rc_timestamp); +CREATE TABLE /*_*/watchlist ( + wl_user int unsigned NOT NULL, + wl_namespace int NOT NULL default 0, + wl_title varchar(255) binary NOT NULL default '', + wl_notificationtimestamp varbinary(14) +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/wl_user ON /*_*/watchlist (wl_user, wl_namespace, wl_title); +CREATE INDEX /*i*/namespace_title ON /*_*/watchlist (wl_namespace, wl_title); +CREATE TABLE /*_*/searchindex ( + si_page int unsigned NOT NULL, + si_title varchar(255) NOT NULL default '', + si_text mediumtext NOT NULL +) ENGINE=MyISAM; +CREATE UNIQUE INDEX /*i*/si_page ON /*_*/searchindex (si_page); +CREATE FULLTEXT INDEX /*i*/si_title ON /*_*/searchindex (si_title); +CREATE FULLTEXT INDEX /*i*/si_text ON /*_*/searchindex (si_text); +CREATE TABLE /*_*/interwiki ( + iw_prefix varchar(32) NOT NULL, + iw_url blob NOT NULL, + iw_api blob NOT NULL, + iw_wikiid varchar(64) NOT NULL, + iw_local bool NOT NULL, + iw_trans tinyint NOT NULL default 0 +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/iw_prefix ON /*_*/interwiki (iw_prefix); +CREATE TABLE /*_*/querycache ( + qc_type varbinary(32) NOT NULL, + qc_value int unsigned NOT NULL default 0, + qc_namespace int NOT NULL default 0, + qc_title varchar(255) binary NOT NULL default '' +) /*$wgDBTableOptions*/; +CREATE INDEX /*i*/qc_type ON /*_*/querycache (qc_type,qc_value); +CREATE TABLE /*_*/objectcache ( + keyname varbinary(255) NOT NULL default '' PRIMARY KEY, + value mediumblob, + exptime datetime +) /*$wgDBTableOptions*/; +CREATE INDEX /*i*/exptime ON /*_*/objectcache (exptime); +CREATE TABLE /*_*/transcache ( + tc_url varbinary(255) NOT NULL, + tc_contents text, + tc_time binary(14) NOT NULL +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/tc_url_idx ON /*_*/transcache (tc_url); +CREATE TABLE /*_*/logging ( + log_id int unsigned NOT NULL PRIMARY KEY AUTO_INCREMENT, + log_type varbinary(32) NOT NULL default '', + log_action varbinary(32) NOT NULL default '', + log_timestamp binary(14) NOT NULL default '19700101000000', + log_user int unsigned NOT NULL default 0, + log_user_text varchar(255) binary NOT NULL default '', + log_namespace int NOT NULL default 0, + log_title varchar(255) binary NOT NULL default '', + log_page int unsigned NULL, + log_comment varchar(255) NOT NULL default '', + log_params blob NOT NULL, + log_deleted tinyint unsigned NOT NULL default 0 +) /*$wgDBTableOptions*/; +CREATE INDEX /*i*/type_time ON /*_*/logging (log_type, log_timestamp); +CREATE INDEX /*i*/user_time ON /*_*/logging (log_user, log_timestamp); +CREATE INDEX /*i*/page_time ON /*_*/logging (log_namespace, log_title, log_timestamp); +CREATE INDEX /*i*/times ON /*_*/logging (log_timestamp); +CREATE INDEX /*i*/log_user_type_time ON /*_*/logging (log_user, log_type, log_timestamp); +CREATE INDEX /*i*/log_page_id_time ON /*_*/logging (log_page,log_timestamp); +CREATE INDEX /*i*/type_action ON /*_*/logging (log_type, log_action, log_timestamp); +CREATE TABLE /*_*/log_search ( + ls_field varbinary(32) NOT NULL, + ls_value varchar(255) NOT NULL, + ls_log_id int unsigned NOT NULL default 0 +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/ls_field_val ON /*_*/log_search (ls_field,ls_value,ls_log_id); +CREATE INDEX /*i*/ls_log_id ON /*_*/log_search (ls_log_id); +CREATE TABLE /*_*/job ( + job_id int unsigned NOT NULL PRIMARY KEY AUTO_INCREMENT, + job_cmd varbinary(60) NOT NULL default '', + job_namespace int NOT NULL, + job_title varchar(255) binary NOT NULL, + job_timestamp varbinary(14) NULL default NULL, + job_params blob NOT NULL, + job_random integer unsigned NOT NULL default 0, + job_attempts integer unsigned NOT NULL default 0, + job_token varbinary(32) NOT NULL default '', + job_token_timestamp varbinary(14) NULL default NULL, + job_sha1 varbinary(32) NOT NULL default '' +) /*$wgDBTableOptions*/; +CREATE INDEX /*i*/job_sha1 ON /*_*/job (job_sha1); +CREATE INDEX /*i*/job_cmd_token ON /*_*/job (job_cmd,job_token,job_random); +CREATE INDEX /*i*/job_cmd_token_id ON /*_*/job (job_cmd,job_token,job_id); +CREATE INDEX /*i*/job_cmd ON /*_*/job (job_cmd, job_namespace, job_title, job_params(128)); +CREATE INDEX /*i*/job_timestamp ON /*_*/job (job_timestamp); +CREATE TABLE /*_*/querycache_info ( + qci_type varbinary(32) NOT NULL default '', + qci_timestamp binary(14) NOT NULL default '19700101000000' +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/qci_type ON /*_*/querycache_info (qci_type); +CREATE TABLE /*_*/redirect ( + rd_from int unsigned NOT NULL default 0 PRIMARY KEY, + rd_namespace int NOT NULL default 0, + rd_title varchar(255) binary NOT NULL default '', + rd_interwiki varchar(32) default NULL, + rd_fragment varchar(255) binary default NULL +) /*$wgDBTableOptions*/; +CREATE INDEX /*i*/rd_ns_title ON /*_*/redirect (rd_namespace,rd_title,rd_from); +CREATE TABLE /*_*/querycachetwo ( + qcc_type varbinary(32) NOT NULL, + qcc_value int unsigned NOT NULL default 0, + qcc_namespace int NOT NULL default 0, + qcc_title varchar(255) binary NOT NULL default '', + qcc_namespacetwo int NOT NULL default 0, + qcc_titletwo varchar(255) binary NOT NULL default '' +) /*$wgDBTableOptions*/; +CREATE INDEX /*i*/qcc_type ON /*_*/querycachetwo (qcc_type,qcc_value); +CREATE INDEX /*i*/qcc_title ON /*_*/querycachetwo (qcc_type,qcc_namespace,qcc_title); +CREATE INDEX /*i*/qcc_titletwo ON /*_*/querycachetwo (qcc_type,qcc_namespacetwo,qcc_titletwo); +CREATE TABLE /*_*/page_restrictions ( + pr_page int NOT NULL, + pr_type varbinary(60) NOT NULL, + pr_level varbinary(60) NOT NULL, + pr_cascade tinyint NOT NULL, + pr_user int NULL, + pr_expiry varbinary(14) NULL, + pr_id int unsigned NOT NULL PRIMARY KEY AUTO_INCREMENT +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/pr_pagetype ON /*_*/page_restrictions (pr_page,pr_type); +CREATE INDEX /*i*/pr_typelevel ON /*_*/page_restrictions (pr_type,pr_level); +CREATE INDEX /*i*/pr_level ON /*_*/page_restrictions (pr_level); +CREATE INDEX /*i*/pr_cascade ON /*_*/page_restrictions (pr_cascade); +CREATE TABLE /*_*/protected_titles ( + pt_namespace int NOT NULL, + pt_title varchar(255) binary NOT NULL, + pt_user int unsigned NOT NULL, + pt_reason tinyblob, + pt_timestamp binary(14) NOT NULL, + pt_expiry varbinary(14) NOT NULL default '', + pt_create_perm varbinary(60) NOT NULL +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/pt_namespace_title ON /*_*/protected_titles (pt_namespace,pt_title); +CREATE INDEX /*i*/pt_timestamp ON /*_*/protected_titles (pt_timestamp); +CREATE TABLE /*_*/page_props ( + pp_page int NOT NULL, + pp_propname varbinary(60) NOT NULL, + pp_value blob NOT NULL +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/pp_page_propname ON /*_*/page_props (pp_page,pp_propname); +CREATE UNIQUE INDEX /*i*/pp_propname_page ON /*_*/page_props (pp_propname,pp_page); +CREATE TABLE /*_*/updatelog ( + ul_key varchar(255) NOT NULL PRIMARY KEY, + ul_value blob +) /*$wgDBTableOptions*/; +CREATE TABLE /*_*/change_tag ( + ct_rc_id int NULL, + ct_log_id int NULL, + ct_rev_id int NULL, + ct_tag varchar(255) NOT NULL, + ct_params blob NULL +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/change_tag_rc_tag ON /*_*/change_tag (ct_rc_id,ct_tag); +CREATE UNIQUE INDEX /*i*/change_tag_log_tag ON /*_*/change_tag (ct_log_id,ct_tag); +CREATE UNIQUE INDEX /*i*/change_tag_rev_tag ON /*_*/change_tag (ct_rev_id,ct_tag); +CREATE INDEX /*i*/change_tag_tag_id ON /*_*/change_tag (ct_tag,ct_rc_id,ct_rev_id,ct_log_id); +CREATE TABLE /*_*/tag_summary ( + ts_rc_id int NULL, + ts_log_id int NULL, + ts_rev_id int NULL, + ts_tags blob NOT NULL +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/tag_summary_rc_id ON /*_*/tag_summary (ts_rc_id); +CREATE UNIQUE INDEX /*i*/tag_summary_log_id ON /*_*/tag_summary (ts_log_id); +CREATE UNIQUE INDEX /*i*/tag_summary_rev_id ON /*_*/tag_summary (ts_rev_id); +CREATE TABLE /*_*/valid_tag ( + vt_tag varchar(255) NOT NULL PRIMARY KEY +) /*$wgDBTableOptions*/; +CREATE TABLE /*_*/l10n_cache ( + lc_lang varbinary(32) NOT NULL, + lc_key varchar(255) NOT NULL, + lc_value mediumblob NOT NULL +) /*$wgDBTableOptions*/; +CREATE INDEX /*i*/lc_lang_key ON /*_*/l10n_cache (lc_lang, lc_key); +CREATE TABLE /*_*/msg_resource ( + mr_resource varbinary(255) NOT NULL, + mr_lang varbinary(32) NOT NULL, + mr_blob mediumblob NOT NULL, + mr_timestamp binary(14) NOT NULL +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/mr_resource_lang ON /*_*/msg_resource (mr_resource, mr_lang); +CREATE TABLE /*_*/msg_resource_links ( + mrl_resource varbinary(255) NOT NULL, + mrl_message varbinary(255) NOT NULL +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/mrl_message_resource ON /*_*/msg_resource_links (mrl_message, mrl_resource); +CREATE TABLE /*_*/module_deps ( + md_module varbinary(255) NOT NULL, + md_skin varbinary(32) NOT NULL, + md_deps mediumblob NOT NULL +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/md_module_skin ON /*_*/module_deps (md_module, md_skin); +CREATE TABLE /*_*/sites ( + site_id INT UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT, + site_global_key varbinary(32) NOT NULL, + site_type varbinary(32) NOT NULL, + site_group varbinary(32) NOT NULL, + site_source varbinary(32) NOT NULL, + site_language varbinary(32) NOT NULL, + site_protocol varbinary(32) NOT NULL, + site_domain VARCHAR(255) NOT NULL, + site_data BLOB NOT NULL, + site_forward bool NOT NULL, + site_config BLOB NOT NULL +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/sites_global_key ON /*_*/sites (site_global_key); +CREATE INDEX /*i*/sites_type ON /*_*/sites (site_type); +CREATE INDEX /*i*/sites_group ON /*_*/sites (site_group); +CREATE INDEX /*i*/sites_source ON /*_*/sites (site_source); +CREATE INDEX /*i*/sites_language ON /*_*/sites (site_language); +CREATE INDEX /*i*/sites_protocol ON /*_*/sites (site_protocol); +CREATE INDEX /*i*/sites_domain ON /*_*/sites (site_domain); +CREATE INDEX /*i*/sites_forward ON /*_*/sites (site_forward); +CREATE TABLE /*_*/site_identifiers ( + si_site INT UNSIGNED NOT NULL, + si_type varbinary(32) NOT NULL, + si_key varbinary(32) NOT NULL +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/site_ids_type ON /*_*/site_identifiers (si_type, si_key); +CREATE INDEX /*i*/site_ids_site ON /*_*/site_identifiers (si_site); +CREATE INDEX /*i*/site_ids_key ON /*_*/site_identifiers (si_key); diff --git a/www/wiki/tests/phpunit/data/db/sqlite/tables-1.22.sql b/www/wiki/tests/phpunit/data/db/sqlite/tables-1.22.sql new file mode 100644 index 00000000..74c5bd5d --- /dev/null +++ b/www/wiki/tests/phpunit/data/db/sqlite/tables-1.22.sql @@ -0,0 +1,575 @@ +-- This is a copy of MediaWiki 1.22 schema shared by MySQL and SQLite. +-- It is used for updater testing. Comments are stripped to decrease +-- file size, as we don't need to maintain it. + +CREATE TABLE /*_*/user ( + user_id int unsigned NOT NULL PRIMARY KEY AUTO_INCREMENT, + user_name varchar(255) binary NOT NULL default '', + user_real_name varchar(255) binary NOT NULL default '', + user_password tinyblob NOT NULL, + user_newpassword tinyblob NOT NULL, + user_newpass_time binary(14), + user_email tinytext NOT NULL, + user_touched binary(14) NOT NULL default '', + user_token binary(32) NOT NULL default '', + user_email_authenticated binary(14), + user_email_token binary(32), + user_email_token_expires binary(14), + user_registration binary(14), + user_editcount int +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/user_name ON /*_*/user (user_name); +CREATE INDEX /*i*/user_email_token ON /*_*/user (user_email_token); +CREATE INDEX /*i*/user_email ON /*_*/user (user_email(50)); +CREATE TABLE /*_*/user_groups ( + ug_user int unsigned NOT NULL default 0, + ug_group varbinary(255) NOT NULL default '' +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/ug_user_group ON /*_*/user_groups (ug_user,ug_group); +CREATE INDEX /*i*/ug_group ON /*_*/user_groups (ug_group); +CREATE TABLE /*_*/user_former_groups ( + ufg_user int unsigned NOT NULL default 0, + ufg_group varbinary(255) NOT NULL default '' +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/ufg_user_group ON /*_*/user_former_groups (ufg_user,ufg_group); +CREATE TABLE /*_*/user_newtalk ( + user_id int NOT NULL default 0, + user_ip varbinary(40) NOT NULL default '', + user_last_timestamp varbinary(14) NULL default NULL +) /*$wgDBTableOptions*/; +CREATE INDEX /*i*/un_user_id ON /*_*/user_newtalk (user_id); +CREATE INDEX /*i*/un_user_ip ON /*_*/user_newtalk (user_ip); +CREATE TABLE /*_*/user_properties ( + up_user int NOT NULL, + up_property varbinary(255) NOT NULL, + up_value blob +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/user_properties_user_property ON /*_*/user_properties (up_user,up_property); +CREATE INDEX /*i*/user_properties_property ON /*_*/user_properties (up_property); +CREATE TABLE /*_*/page ( + page_id int unsigned NOT NULL PRIMARY KEY AUTO_INCREMENT, + page_namespace int NOT NULL, + page_title varchar(255) binary NOT NULL, + page_restrictions tinyblob NOT NULL, + page_counter bigint unsigned NOT NULL default 0, + page_is_redirect tinyint unsigned NOT NULL default 0, + page_is_new tinyint unsigned NOT NULL default 0, + page_random real unsigned NOT NULL, + page_touched binary(14) NOT NULL default '', + page_latest int unsigned NOT NULL, + page_len int unsigned NOT NULL, + page_content_model varbinary(32) DEFAULT NULL +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/name_title ON /*_*/page (page_namespace,page_title); +CREATE INDEX /*i*/page_random ON /*_*/page (page_random); +CREATE INDEX /*i*/page_len ON /*_*/page (page_len); +CREATE INDEX /*i*/page_redirect_namespace_len ON /*_*/page (page_is_redirect, page_namespace, page_len); +CREATE TABLE /*_*/revision ( + rev_id int unsigned NOT NULL PRIMARY KEY AUTO_INCREMENT, + rev_page int unsigned NOT NULL, + rev_text_id int unsigned NOT NULL, + rev_comment tinyblob NOT NULL, + rev_user int unsigned NOT NULL default 0, + rev_user_text varchar(255) binary NOT NULL default '', + rev_timestamp binary(14) NOT NULL default '', + rev_minor_edit tinyint unsigned NOT NULL default 0, + rev_deleted tinyint unsigned NOT NULL default 0, + rev_len int unsigned, + rev_parent_id int unsigned default NULL, + rev_sha1 varbinary(32) NOT NULL default '', + rev_content_model varbinary(32) DEFAULT NULL, + rev_content_format varbinary(64) DEFAULT NULL +) /*$wgDBTableOptions*/ MAX_ROWS=10000000 AVG_ROW_LENGTH=1024; +CREATE UNIQUE INDEX /*i*/rev_page_id ON /*_*/revision (rev_page, rev_id); +CREATE INDEX /*i*/rev_timestamp ON /*_*/revision (rev_timestamp); +CREATE INDEX /*i*/page_timestamp ON /*_*/revision (rev_page,rev_timestamp); +CREATE INDEX /*i*/user_timestamp ON /*_*/revision (rev_user,rev_timestamp); +CREATE INDEX /*i*/usertext_timestamp ON /*_*/revision (rev_user_text,rev_timestamp); +CREATE INDEX /*i*/page_user_timestamp ON /*_*/revision (rev_page,rev_user,rev_timestamp); +CREATE TABLE /*_*/text ( + old_id int unsigned NOT NULL PRIMARY KEY AUTO_INCREMENT, + old_text mediumblob NOT NULL, + old_flags tinyblob NOT NULL +) /*$wgDBTableOptions*/ MAX_ROWS=10000000 AVG_ROW_LENGTH=10240; +CREATE TABLE /*_*/archive ( + ar_id int unsigned NOT NULL PRIMARY KEY AUTO_INCREMENT, + ar_namespace int NOT NULL default 0, + ar_title varchar(255) binary NOT NULL default '', + ar_text mediumblob NOT NULL, + ar_comment tinyblob NOT NULL, + ar_user int unsigned NOT NULL default 0, + ar_user_text varchar(255) binary NOT NULL, + ar_timestamp binary(14) NOT NULL default '', + ar_minor_edit tinyint NOT NULL default 0, + ar_flags tinyblob NOT NULL, + ar_rev_id int unsigned, + ar_text_id int unsigned, + ar_deleted tinyint unsigned NOT NULL default 0, + ar_len int unsigned, + ar_page_id int unsigned, + ar_parent_id int unsigned default NULL, + ar_sha1 varbinary(32) NOT NULL default '', + ar_content_model varbinary(32) DEFAULT NULL, + ar_content_format varbinary(64) DEFAULT NULL +) /*$wgDBTableOptions*/; +CREATE INDEX /*i*/name_title_timestamp ON /*_*/archive (ar_namespace,ar_title,ar_timestamp); +CREATE INDEX /*i*/ar_usertext_timestamp ON /*_*/archive (ar_user_text,ar_timestamp); +CREATE INDEX /*i*/ar_revid ON /*_*/archive (ar_rev_id); +CREATE TABLE /*_*/pagelinks ( + pl_from int unsigned NOT NULL default 0, + pl_namespace int NOT NULL default 0, + pl_title varchar(255) binary NOT NULL default '' +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/pl_from ON /*_*/pagelinks (pl_from,pl_namespace,pl_title); +CREATE UNIQUE INDEX /*i*/pl_namespace ON /*_*/pagelinks (pl_namespace,pl_title,pl_from); +CREATE TABLE /*_*/templatelinks ( + tl_from int unsigned NOT NULL default 0, + tl_namespace int NOT NULL default 0, + tl_title varchar(255) binary NOT NULL default '' +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/tl_from ON /*_*/templatelinks (tl_from,tl_namespace,tl_title); +CREATE UNIQUE INDEX /*i*/tl_namespace ON /*_*/templatelinks (tl_namespace,tl_title,tl_from); +CREATE TABLE /*_*/imagelinks ( + il_from int unsigned NOT NULL default 0, + il_to varchar(255) binary NOT NULL default '' +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/il_from ON /*_*/imagelinks (il_from,il_to); +CREATE UNIQUE INDEX /*i*/il_to ON /*_*/imagelinks (il_to,il_from); +CREATE TABLE /*_*/categorylinks ( + cl_from int unsigned NOT NULL default 0, + cl_to varchar(255) binary NOT NULL default '', + cl_sortkey varbinary(230) NOT NULL default '', + cl_sortkey_prefix varchar(255) binary NOT NULL default '', + cl_timestamp timestamp NOT NULL, + cl_collation varbinary(32) NOT NULL default '', + cl_type ENUM('page', 'subcat', 'file') NOT NULL default 'page' +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/cl_from ON /*_*/categorylinks (cl_from,cl_to); +CREATE INDEX /*i*/cl_sortkey ON /*_*/categorylinks (cl_to,cl_type,cl_sortkey,cl_from); +CREATE INDEX /*i*/cl_timestamp ON /*_*/categorylinks (cl_to,cl_timestamp); +CREATE INDEX /*i*/cl_collation ON /*_*/categorylinks (cl_collation); +CREATE TABLE /*_*/category ( + cat_id int unsigned NOT NULL PRIMARY KEY AUTO_INCREMENT, + cat_title varchar(255) binary NOT NULL, + cat_pages int signed NOT NULL default 0, + cat_subcats int signed NOT NULL default 0, + cat_files int signed NOT NULL default 0 +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/cat_title ON /*_*/category (cat_title); +CREATE INDEX /*i*/cat_pages ON /*_*/category (cat_pages); +CREATE TABLE /*_*/externallinks ( + el_id int unsigned NOT NULL PRIMARY KEY AUTO_INCREMENT, + el_from int unsigned NOT NULL default 0, + el_to blob NOT NULL, + el_index blob NOT NULL +) /*$wgDBTableOptions*/; +CREATE INDEX /*i*/el_from ON /*_*/externallinks (el_from, el_to(40)); +CREATE INDEX /*i*/el_to ON /*_*/externallinks (el_to(60), el_from); +CREATE INDEX /*i*/el_index ON /*_*/externallinks (el_index(60)); +CREATE TABLE /*_*/langlinks ( + ll_from int unsigned NOT NULL default 0, + ll_lang varbinary(20) NOT NULL default '', + ll_title varchar(255) binary NOT NULL default '' +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/ll_from ON /*_*/langlinks (ll_from, ll_lang); +CREATE INDEX /*i*/ll_lang ON /*_*/langlinks (ll_lang, ll_title); +CREATE TABLE /*_*/iwlinks ( + iwl_from int unsigned NOT NULL default 0, + iwl_prefix varbinary(20) NOT NULL default '', + iwl_title varchar(255) binary NOT NULL default '' +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/iwl_from ON /*_*/iwlinks (iwl_from, iwl_prefix, iwl_title); +CREATE INDEX /*i*/iwl_prefix_title_from ON /*_*/iwlinks (iwl_prefix, iwl_title, iwl_from); +CREATE INDEX /*i*/iwl_prefix_from_title ON /*_*/iwlinks (iwl_prefix, iwl_from, iwl_title); +CREATE TABLE /*_*/site_stats ( + ss_row_id int unsigned NOT NULL, + ss_total_views bigint unsigned default 0, + ss_total_edits bigint unsigned default 0, + ss_good_articles bigint unsigned default 0, + ss_total_pages bigint default '-1', + ss_users bigint default '-1', + ss_active_users bigint default '-1', + ss_images int default 0 +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/ss_row_id ON /*_*/site_stats (ss_row_id); +CREATE TABLE /*_*/hitcounter ( + hc_id int unsigned NOT NULL +) ENGINE=HEAP MAX_ROWS=25000; +CREATE TABLE /*_*/ipblocks ( + ipb_id int NOT NULL PRIMARY KEY AUTO_INCREMENT, + ipb_address tinyblob NOT NULL, + ipb_user int unsigned NOT NULL default 0, + ipb_by int unsigned NOT NULL default 0, + ipb_by_text varchar(255) binary NOT NULL default '', + ipb_reason tinyblob NOT NULL, + ipb_timestamp binary(14) NOT NULL default '', + ipb_auto bool NOT NULL default 0, + ipb_anon_only bool NOT NULL default 0, + ipb_create_account bool NOT NULL default 1, + ipb_enable_autoblock bool NOT NULL default '1', + ipb_expiry varbinary(14) NOT NULL default '', + ipb_range_start tinyblob NOT NULL, + ipb_range_end tinyblob NOT NULL, + ipb_deleted bool NOT NULL default 0, + ipb_block_email bool NOT NULL default 0, + ipb_allow_usertalk bool NOT NULL default 0, + ipb_parent_block_id int default NULL +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/ipb_address ON /*_*/ipblocks (ipb_address(255), ipb_user, ipb_auto, ipb_anon_only); +CREATE INDEX /*i*/ipb_user ON /*_*/ipblocks (ipb_user); +CREATE INDEX /*i*/ipb_range ON /*_*/ipblocks (ipb_range_start(8), ipb_range_end(8)); +CREATE INDEX /*i*/ipb_timestamp ON /*_*/ipblocks (ipb_timestamp); +CREATE INDEX /*i*/ipb_expiry ON /*_*/ipblocks (ipb_expiry); +CREATE INDEX /*i*/ipb_parent_block_id ON /*_*/ipblocks (ipb_parent_block_id); +CREATE TABLE /*_*/image ( + img_name varchar(255) binary NOT NULL default '' PRIMARY KEY, + img_size int unsigned NOT NULL default 0, + img_width int NOT NULL default 0, + img_height int NOT NULL default 0, + img_metadata mediumblob NOT NULL, + img_bits int NOT NULL default 0, + img_media_type ENUM("UNKNOWN", "BITMAP", "DRAWING", "AUDIO", "VIDEO", "MULTIMEDIA", "OFFICE", "TEXT", "EXECUTABLE", "ARCHIVE") default NULL, + img_major_mime ENUM("unknown", "application", "audio", "image", "text", "video", "message", "model", "multipart") NOT NULL default "unknown", + img_minor_mime varbinary(100) NOT NULL default "unknown", + img_description tinyblob NOT NULL, + img_user int unsigned NOT NULL default 0, + img_user_text varchar(255) binary NOT NULL, + img_timestamp varbinary(14) NOT NULL default '', + img_sha1 varbinary(32) NOT NULL default '' +) /*$wgDBTableOptions*/; +CREATE INDEX /*i*/img_usertext_timestamp ON /*_*/image (img_user_text,img_timestamp); +CREATE INDEX /*i*/img_size ON /*_*/image (img_size); +CREATE INDEX /*i*/img_timestamp ON /*_*/image (img_timestamp); +CREATE INDEX /*i*/img_sha1 ON /*_*/image (img_sha1(10)); +CREATE INDEX /*i*/img_media_mime ON /*_*/image (img_media_type,img_major_mime,img_minor_mime); +CREATE TABLE /*_*/oldimage ( + oi_name varchar(255) binary NOT NULL default '', + oi_archive_name varchar(255) binary NOT NULL default '', + oi_size int unsigned NOT NULL default 0, + oi_width int NOT NULL default 0, + oi_height int NOT NULL default 0, + oi_bits int NOT NULL default 0, + oi_description tinyblob NOT NULL, + oi_user int unsigned NOT NULL default 0, + oi_user_text varchar(255) binary NOT NULL, + oi_timestamp binary(14) NOT NULL default '', + oi_metadata mediumblob NOT NULL, + oi_media_type ENUM("UNKNOWN", "BITMAP", "DRAWING", "AUDIO", "VIDEO", "MULTIMEDIA", "OFFICE", "TEXT", "EXECUTABLE", "ARCHIVE") default NULL, + oi_major_mime ENUM("unknown", "application", "audio", "image", "text", "video", "message", "model", "multipart") NOT NULL default "unknown", + oi_minor_mime varbinary(100) NOT NULL default "unknown", + oi_deleted tinyint unsigned NOT NULL default 0, + oi_sha1 varbinary(32) NOT NULL default '' +) /*$wgDBTableOptions*/; +CREATE INDEX /*i*/oi_usertext_timestamp ON /*_*/oldimage (oi_user_text,oi_timestamp); +CREATE INDEX /*i*/oi_name_timestamp ON /*_*/oldimage (oi_name,oi_timestamp); +CREATE INDEX /*i*/oi_name_archive_name ON /*_*/oldimage (oi_name,oi_archive_name(14)); +CREATE INDEX /*i*/oi_sha1 ON /*_*/oldimage (oi_sha1(10)); +CREATE TABLE /*_*/filearchive ( + fa_id int NOT NULL PRIMARY KEY AUTO_INCREMENT, + fa_name varchar(255) binary NOT NULL default '', + fa_archive_name varchar(255) binary default '', + fa_storage_group varbinary(16), + fa_storage_key varbinary(64) default '', + fa_deleted_user int, + fa_deleted_timestamp binary(14) default '', + fa_deleted_reason text, + fa_size int unsigned default 0, + fa_width int default 0, + fa_height int default 0, + fa_metadata mediumblob, + fa_bits int default 0, + fa_media_type ENUM("UNKNOWN", "BITMAP", "DRAWING", "AUDIO", "VIDEO", "MULTIMEDIA", "OFFICE", "TEXT", "EXECUTABLE", "ARCHIVE") default NULL, + fa_major_mime ENUM("unknown", "application", "audio", "image", "text", "video", "message", "model", "multipart") default "unknown", + fa_minor_mime varbinary(100) default "unknown", + fa_description tinyblob, + fa_user int unsigned default 0, + fa_user_text varchar(255) binary, + fa_timestamp binary(14) default '', + fa_deleted tinyint unsigned NOT NULL default 0, + fa_sha1 varbinary(32) NOT NULL default '' +) /*$wgDBTableOptions*/; +CREATE INDEX /*i*/fa_name ON /*_*/filearchive (fa_name, fa_timestamp); +CREATE INDEX /*i*/fa_storage_group ON /*_*/filearchive (fa_storage_group, fa_storage_key); +CREATE INDEX /*i*/fa_deleted_timestamp ON /*_*/filearchive (fa_deleted_timestamp); +CREATE INDEX /*i*/fa_user_timestamp ON /*_*/filearchive (fa_user_text,fa_timestamp); +CREATE INDEX /*i*/fa_sha1 ON /*_*/filearchive (fa_sha1(10)); +CREATE TABLE /*_*/uploadstash ( + us_id int unsigned NOT NULL PRIMARY KEY AUTO_INCREMENT, + us_user int unsigned NOT NULL, + us_key varchar(255) NOT NULL, + us_orig_path varchar(255) NOT NULL, + us_path varchar(255) NOT NULL, + us_source_type varchar(50), + us_timestamp varbinary(14) NOT NULL, + us_status varchar(50) NOT NULL, + us_chunk_inx int unsigned NULL, + us_props blob, + us_size int unsigned NOT NULL, + us_sha1 varchar(31) NOT NULL, + us_mime varchar(255), + us_media_type ENUM("UNKNOWN", "BITMAP", "DRAWING", "AUDIO", "VIDEO", "MULTIMEDIA", "OFFICE", "TEXT", "EXECUTABLE", "ARCHIVE") default NULL, + us_image_width int unsigned, + us_image_height int unsigned, + us_image_bits smallint unsigned +) /*$wgDBTableOptions*/; +CREATE INDEX /*i*/us_user ON /*_*/uploadstash (us_user); +CREATE UNIQUE INDEX /*i*/us_key ON /*_*/uploadstash (us_key); +CREATE INDEX /*i*/us_timestamp ON /*_*/uploadstash (us_timestamp); +CREATE TABLE /*_*/recentchanges ( + rc_id int NOT NULL PRIMARY KEY AUTO_INCREMENT, + rc_timestamp varbinary(14) NOT NULL default '', + rc_cur_time varbinary(14) NOT NULL default '', + rc_user int unsigned NOT NULL default 0, + rc_user_text varchar(255) binary NOT NULL, + rc_namespace int NOT NULL default 0, + rc_title varchar(255) binary NOT NULL default '', + rc_comment varchar(255) binary NOT NULL default '', + rc_minor tinyint unsigned NOT NULL default 0, + rc_bot tinyint unsigned NOT NULL default 0, + rc_new tinyint unsigned NOT NULL default 0, + rc_cur_id int unsigned NOT NULL default 0, + rc_this_oldid int unsigned NOT NULL default 0, + rc_last_oldid int unsigned NOT NULL default 0, + rc_type tinyint unsigned NOT NULL default 0, + rc_patrolled tinyint unsigned NOT NULL default 0, + rc_ip varbinary(40) NOT NULL default '', + rc_old_len int, + rc_new_len int, + rc_deleted tinyint unsigned NOT NULL default 0, + rc_logid int unsigned NOT NULL default 0, + rc_log_type varbinary(255) NULL default NULL, + rc_log_action varbinary(255) NULL default NULL, + rc_params blob NULL +) /*$wgDBTableOptions*/; +CREATE INDEX /*i*/rc_timestamp ON /*_*/recentchanges (rc_timestamp); +CREATE INDEX /*i*/rc_namespace_title ON /*_*/recentchanges (rc_namespace, rc_title); +CREATE INDEX /*i*/rc_cur_id ON /*_*/recentchanges (rc_cur_id); +CREATE INDEX /*i*/new_name_timestamp ON /*_*/recentchanges (rc_new,rc_namespace,rc_timestamp); +CREATE INDEX /*i*/rc_ip ON /*_*/recentchanges (rc_ip); +CREATE INDEX /*i*/rc_ns_usertext ON /*_*/recentchanges (rc_namespace, rc_user_text); +CREATE INDEX /*i*/rc_user_text ON /*_*/recentchanges (rc_user_text, rc_timestamp); +CREATE TABLE /*_*/watchlist ( + wl_user int unsigned NOT NULL, + wl_namespace int NOT NULL default 0, + wl_title varchar(255) binary NOT NULL default '', + wl_notificationtimestamp varbinary(14) +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/wl_user ON /*_*/watchlist (wl_user, wl_namespace, wl_title); +CREATE INDEX /*i*/namespace_title ON /*_*/watchlist (wl_namespace, wl_title); +CREATE TABLE /*_*/searchindex ( + si_page int unsigned NOT NULL, + si_title varchar(255) NOT NULL default '', + si_text mediumtext NOT NULL +) ENGINE=MyISAM DEFAULT CHARSET=utf8; +CREATE UNIQUE INDEX /*i*/si_page ON /*_*/searchindex (si_page); +CREATE FULLTEXT INDEX /*i*/si_title ON /*_*/searchindex (si_title); +CREATE FULLTEXT INDEX /*i*/si_text ON /*_*/searchindex (si_text); +CREATE TABLE /*_*/interwiki ( + iw_prefix varchar(32) NOT NULL, + iw_url blob NOT NULL, + iw_api blob NOT NULL, + iw_wikiid varchar(64) NOT NULL, + iw_local bool NOT NULL, + iw_trans tinyint NOT NULL default 0 +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/iw_prefix ON /*_*/interwiki (iw_prefix); +CREATE TABLE /*_*/querycache ( + qc_type varbinary(32) NOT NULL, + qc_value int unsigned NOT NULL default 0, + qc_namespace int NOT NULL default 0, + qc_title varchar(255) binary NOT NULL default '' +) /*$wgDBTableOptions*/; +CREATE INDEX /*i*/qc_type ON /*_*/querycache (qc_type,qc_value); +CREATE TABLE /*_*/objectcache ( + keyname varbinary(255) NOT NULL default '' PRIMARY KEY, + value mediumblob, + exptime datetime +) /*$wgDBTableOptions*/; +CREATE INDEX /*i*/exptime ON /*_*/objectcache (exptime); +CREATE TABLE /*_*/transcache ( + tc_url varbinary(255) NOT NULL, + tc_contents text, + tc_time binary(14) NOT NULL +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/tc_url_idx ON /*_*/transcache (tc_url); +CREATE TABLE /*_*/logging ( + log_id int unsigned NOT NULL PRIMARY KEY AUTO_INCREMENT, + log_type varbinary(32) NOT NULL default '', + log_action varbinary(32) NOT NULL default '', + log_timestamp binary(14) NOT NULL default '19700101000000', + log_user int unsigned NOT NULL default 0, + log_user_text varchar(255) binary NOT NULL default '', + log_namespace int NOT NULL default 0, + log_title varchar(255) binary NOT NULL default '', + log_page int unsigned NULL, + log_comment varchar(255) NOT NULL default '', + log_params blob NOT NULL, + log_deleted tinyint unsigned NOT NULL default 0 +) /*$wgDBTableOptions*/; +CREATE INDEX /*i*/type_time ON /*_*/logging (log_type, log_timestamp); +CREATE INDEX /*i*/user_time ON /*_*/logging (log_user, log_timestamp); +CREATE INDEX /*i*/page_time ON /*_*/logging (log_namespace, log_title, log_timestamp); +CREATE INDEX /*i*/times ON /*_*/logging (log_timestamp); +CREATE INDEX /*i*/log_user_type_time ON /*_*/logging (log_user, log_type, log_timestamp); +CREATE INDEX /*i*/log_page_id_time ON /*_*/logging (log_page,log_timestamp); +CREATE INDEX /*i*/type_action ON /*_*/logging (log_type, log_action, log_timestamp); +CREATE TABLE /*_*/log_search ( + ls_field varbinary(32) NOT NULL, + ls_value varchar(255) NOT NULL, + ls_log_id int unsigned NOT NULL default 0 +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/ls_field_val ON /*_*/log_search (ls_field,ls_value,ls_log_id); +CREATE INDEX /*i*/ls_log_id ON /*_*/log_search (ls_log_id); +CREATE TABLE /*_*/job ( + job_id int unsigned NOT NULL PRIMARY KEY AUTO_INCREMENT, + job_cmd varbinary(60) NOT NULL default '', + job_namespace int NOT NULL, + job_title varchar(255) binary NOT NULL, + job_timestamp varbinary(14) NULL default NULL, + job_params blob NOT NULL, + job_random integer unsigned NOT NULL default 0, + job_attempts integer unsigned NOT NULL default 0, + job_token varbinary(32) NOT NULL default '', + job_token_timestamp varbinary(14) NULL default NULL, + job_sha1 varbinary(32) NOT NULL default '' +) /*$wgDBTableOptions*/; +CREATE INDEX /*i*/job_sha1 ON /*_*/job (job_sha1); +CREATE INDEX /*i*/job_cmd_token ON /*_*/job (job_cmd,job_token,job_random); +CREATE INDEX /*i*/job_cmd_token_id ON /*_*/job (job_cmd,job_token,job_id); +CREATE INDEX /*i*/job_cmd ON /*_*/job (job_cmd, job_namespace, job_title, job_params(128)); +CREATE INDEX /*i*/job_timestamp ON /*_*/job (job_timestamp); +CREATE TABLE /*_*/querycache_info ( + qci_type varbinary(32) NOT NULL default '', + qci_timestamp binary(14) NOT NULL default '19700101000000' +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/qci_type ON /*_*/querycache_info (qci_type); +CREATE TABLE /*_*/redirect ( + rd_from int unsigned NOT NULL default 0 PRIMARY KEY, + rd_namespace int NOT NULL default 0, + rd_title varchar(255) binary NOT NULL default '', + rd_interwiki varchar(32) default NULL, + rd_fragment varchar(255) binary default NULL +) /*$wgDBTableOptions*/; +CREATE INDEX /*i*/rd_ns_title ON /*_*/redirect (rd_namespace,rd_title,rd_from); +CREATE TABLE /*_*/querycachetwo ( + qcc_type varbinary(32) NOT NULL, + qcc_value int unsigned NOT NULL default 0, + qcc_namespace int NOT NULL default 0, + qcc_title varchar(255) binary NOT NULL default '', + qcc_namespacetwo int NOT NULL default 0, + qcc_titletwo varchar(255) binary NOT NULL default '' +) /*$wgDBTableOptions*/; +CREATE INDEX /*i*/qcc_type ON /*_*/querycachetwo (qcc_type,qcc_value); +CREATE INDEX /*i*/qcc_title ON /*_*/querycachetwo (qcc_type,qcc_namespace,qcc_title); +CREATE INDEX /*i*/qcc_titletwo ON /*_*/querycachetwo (qcc_type,qcc_namespacetwo,qcc_titletwo); +CREATE TABLE /*_*/page_restrictions ( + pr_page int NOT NULL, + pr_type varbinary(60) NOT NULL, + pr_level varbinary(60) NOT NULL, + pr_cascade tinyint NOT NULL, + pr_user int NULL, + pr_expiry varbinary(14) NULL, + pr_id int unsigned NOT NULL PRIMARY KEY AUTO_INCREMENT +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/pr_pagetype ON /*_*/page_restrictions (pr_page,pr_type); +CREATE INDEX /*i*/pr_typelevel ON /*_*/page_restrictions (pr_type,pr_level); +CREATE INDEX /*i*/pr_level ON /*_*/page_restrictions (pr_level); +CREATE INDEX /*i*/pr_cascade ON /*_*/page_restrictions (pr_cascade); +CREATE TABLE /*_*/protected_titles ( + pt_namespace int NOT NULL, + pt_title varchar(255) binary NOT NULL, + pt_user int unsigned NOT NULL, + pt_reason tinyblob, + pt_timestamp binary(14) NOT NULL, + pt_expiry varbinary(14) NOT NULL default '', + pt_create_perm varbinary(60) NOT NULL +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/pt_namespace_title ON /*_*/protected_titles (pt_namespace,pt_title); +CREATE INDEX /*i*/pt_timestamp ON /*_*/protected_titles (pt_timestamp); +CREATE TABLE /*_*/page_props ( + pp_page int NOT NULL, + pp_propname varbinary(60) NOT NULL, + pp_value blob NOT NULL +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/pp_page_propname ON /*_*/page_props (pp_page,pp_propname); +CREATE UNIQUE INDEX /*i*/pp_propname_page ON /*_*/page_props (pp_propname,pp_page); +CREATE TABLE /*_*/updatelog ( + ul_key varchar(255) NOT NULL PRIMARY KEY, + ul_value blob +) /*$wgDBTableOptions*/; +CREATE TABLE /*_*/change_tag ( + ct_rc_id int NULL, + ct_log_id int NULL, + ct_rev_id int NULL, + ct_tag varchar(255) NOT NULL, + ct_params blob NULL +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/change_tag_rc_tag ON /*_*/change_tag (ct_rc_id,ct_tag); +CREATE UNIQUE INDEX /*i*/change_tag_log_tag ON /*_*/change_tag (ct_log_id,ct_tag); +CREATE UNIQUE INDEX /*i*/change_tag_rev_tag ON /*_*/change_tag (ct_rev_id,ct_tag); +CREATE INDEX /*i*/change_tag_tag_id ON /*_*/change_tag (ct_tag,ct_rc_id,ct_rev_id,ct_log_id); +CREATE TABLE /*_*/tag_summary ( + ts_rc_id int NULL, + ts_log_id int NULL, + ts_rev_id int NULL, + ts_tags blob NOT NULL +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/tag_summary_rc_id ON /*_*/tag_summary (ts_rc_id); +CREATE UNIQUE INDEX /*i*/tag_summary_log_id ON /*_*/tag_summary (ts_log_id); +CREATE UNIQUE INDEX /*i*/tag_summary_rev_id ON /*_*/tag_summary (ts_rev_id); +CREATE TABLE /*_*/valid_tag ( + vt_tag varchar(255) NOT NULL PRIMARY KEY +) /*$wgDBTableOptions*/; +CREATE TABLE /*_*/l10n_cache ( + lc_lang varbinary(32) NOT NULL, + lc_key varchar(255) NOT NULL, + lc_value mediumblob NOT NULL +) /*$wgDBTableOptions*/; +CREATE INDEX /*i*/lc_lang_key ON /*_*/l10n_cache (lc_lang, lc_key); +CREATE TABLE /*_*/msg_resource ( + mr_resource varbinary(255) NOT NULL, + mr_lang varbinary(32) NOT NULL, + mr_blob mediumblob NOT NULL, + mr_timestamp binary(14) NOT NULL +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/mr_resource_lang ON /*_*/msg_resource (mr_resource, mr_lang); +CREATE TABLE /*_*/msg_resource_links ( + mrl_resource varbinary(255) NOT NULL, + mrl_message varbinary(255) NOT NULL +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/mrl_message_resource ON /*_*/msg_resource_links (mrl_message, mrl_resource); +CREATE TABLE /*_*/module_deps ( + md_module varbinary(255) NOT NULL, + md_skin varbinary(32) NOT NULL, + md_deps mediumblob NOT NULL +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/md_module_skin ON /*_*/module_deps (md_module, md_skin); +CREATE TABLE /*_*/sites ( + site_id INT UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT, + site_global_key varbinary(32) NOT NULL, + site_type varbinary(32) NOT NULL, + site_group varbinary(32) NOT NULL, + site_source varbinary(32) NOT NULL, + site_language varbinary(32) NOT NULL, + site_protocol varbinary(32) NOT NULL, + site_domain VARCHAR(255) NOT NULL, + site_data BLOB NOT NULL, + site_forward bool NOT NULL, + site_config BLOB NOT NULL +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/sites_global_key ON /*_*/sites (site_global_key); +CREATE INDEX /*i*/sites_type ON /*_*/sites (site_type); +CREATE INDEX /*i*/sites_group ON /*_*/sites (site_group); +CREATE INDEX /*i*/sites_source ON /*_*/sites (site_source); +CREATE INDEX /*i*/sites_language ON /*_*/sites (site_language); +CREATE INDEX /*i*/sites_protocol ON /*_*/sites (site_protocol); +CREATE INDEX /*i*/sites_domain ON /*_*/sites (site_domain); +CREATE INDEX /*i*/sites_forward ON /*_*/sites (site_forward); +CREATE TABLE /*_*/site_identifiers ( + si_site INT UNSIGNED NOT NULL, + si_type varbinary(32) NOT NULL, + si_key varbinary(32) NOT NULL +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/site_ids_type ON /*_*/site_identifiers (si_type, si_key); +CREATE INDEX /*i*/site_ids_site ON /*_*/site_identifiers (si_site); +CREATE INDEX /*i*/site_ids_key ON /*_*/site_identifiers (si_key); diff --git a/www/wiki/tests/phpunit/data/db/sqlite/tables-1.23.sql b/www/wiki/tests/phpunit/data/db/sqlite/tables-1.23.sql new file mode 100644 index 00000000..1c3a8ae4 --- /dev/null +++ b/www/wiki/tests/phpunit/data/db/sqlite/tables-1.23.sql @@ -0,0 +1,580 @@ +-- This is a copy of MediaWiki 1.23 schema shared by MySQL and SQLite. +-- It is used for updater testing. Comments are stripped to decrease +-- file size, as we don't need to maintain it. + +CREATE TABLE /*_*/user ( + user_id int unsigned NOT NULL PRIMARY KEY AUTO_INCREMENT, + user_name varchar(255) binary NOT NULL default '', + user_real_name varchar(255) binary NOT NULL default '', + user_password tinyblob NOT NULL, + user_newpassword tinyblob NOT NULL, + user_newpass_time binary(14), + user_email tinytext NOT NULL, + user_touched binary(14) NOT NULL default '', + user_token binary(32) NOT NULL default '', + user_email_authenticated binary(14), + user_email_token binary(32), + user_email_token_expires binary(14), + user_registration binary(14), + user_editcount int, + user_password_expires varbinary(14) DEFAULT NULL +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/user_name ON /*_*/user (user_name); +CREATE INDEX /*i*/user_email_token ON /*_*/user (user_email_token); +CREATE INDEX /*i*/user_email ON /*_*/user (user_email(50)); +CREATE TABLE /*_*/user_groups ( + ug_user int unsigned NOT NULL default 0, + ug_group varbinary(255) NOT NULL default '' +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/ug_user_group ON /*_*/user_groups (ug_user,ug_group); +CREATE INDEX /*i*/ug_group ON /*_*/user_groups (ug_group); +CREATE TABLE /*_*/user_former_groups ( + ufg_user int unsigned NOT NULL default 0, + ufg_group varbinary(255) NOT NULL default '' +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/ufg_user_group ON /*_*/user_former_groups (ufg_user,ufg_group); +CREATE TABLE /*_*/user_newtalk ( + user_id int NOT NULL default 0, + user_ip varbinary(40) NOT NULL default '', + user_last_timestamp varbinary(14) NULL default NULL +) /*$wgDBTableOptions*/; +CREATE INDEX /*i*/un_user_id ON /*_*/user_newtalk (user_id); +CREATE INDEX /*i*/un_user_ip ON /*_*/user_newtalk (user_ip); +CREATE TABLE /*_*/user_properties ( + up_user int NOT NULL, + up_property varbinary(255) NOT NULL, + up_value blob +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/user_properties_user_property ON /*_*/user_properties (up_user,up_property); +CREATE INDEX /*i*/user_properties_property ON /*_*/user_properties (up_property); +CREATE TABLE /*_*/page ( + page_id int unsigned NOT NULL PRIMARY KEY AUTO_INCREMENT, + page_namespace int NOT NULL, + page_title varchar(255) binary NOT NULL, + page_restrictions tinyblob NOT NULL, + page_counter bigint unsigned NOT NULL default 0, + page_is_redirect tinyint unsigned NOT NULL default 0, + page_is_new tinyint unsigned NOT NULL default 0, + page_random real unsigned NOT NULL, + page_touched binary(14) NOT NULL default '', + page_links_updated varbinary(14) NULL default NULL, + page_latest int unsigned NOT NULL, + page_len int unsigned NOT NULL, + page_content_model varbinary(32) DEFAULT NULL +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/name_title ON /*_*/page (page_namespace,page_title); +CREATE INDEX /*i*/page_random ON /*_*/page (page_random); +CREATE INDEX /*i*/page_len ON /*_*/page (page_len); +CREATE INDEX /*i*/page_redirect_namespace_len ON /*_*/page (page_is_redirect, page_namespace, page_len); +CREATE TABLE /*_*/revision ( + rev_id int unsigned NOT NULL PRIMARY KEY AUTO_INCREMENT, + rev_page int unsigned NOT NULL, + rev_text_id int unsigned NOT NULL, + rev_comment tinyblob NOT NULL, + rev_user int unsigned NOT NULL default 0, + rev_user_text varchar(255) binary NOT NULL default '', + rev_timestamp binary(14) NOT NULL default '', + rev_minor_edit tinyint unsigned NOT NULL default 0, + rev_deleted tinyint unsigned NOT NULL default 0, + rev_len int unsigned, + rev_parent_id int unsigned default NULL, + rev_sha1 varbinary(32) NOT NULL default '', + rev_content_model varbinary(32) DEFAULT NULL, + rev_content_format varbinary(64) DEFAULT NULL +) /*$wgDBTableOptions*/ MAX_ROWS=10000000 AVG_ROW_LENGTH=1024; +CREATE UNIQUE INDEX /*i*/rev_page_id ON /*_*/revision (rev_page, rev_id); +CREATE INDEX /*i*/rev_timestamp ON /*_*/revision (rev_timestamp); +CREATE INDEX /*i*/page_timestamp ON /*_*/revision (rev_page,rev_timestamp); +CREATE INDEX /*i*/user_timestamp ON /*_*/revision (rev_user,rev_timestamp); +CREATE INDEX /*i*/usertext_timestamp ON /*_*/revision (rev_user_text,rev_timestamp); +CREATE INDEX /*i*/page_user_timestamp ON /*_*/revision (rev_page,rev_user,rev_timestamp); +CREATE TABLE /*_*/text ( + old_id int unsigned NOT NULL PRIMARY KEY AUTO_INCREMENT, + old_text mediumblob NOT NULL, + old_flags tinyblob NOT NULL +) /*$wgDBTableOptions*/ MAX_ROWS=10000000 AVG_ROW_LENGTH=10240; +CREATE TABLE /*_*/archive ( + ar_id int unsigned NOT NULL PRIMARY KEY AUTO_INCREMENT, + ar_namespace int NOT NULL default 0, + ar_title varchar(255) binary NOT NULL default '', + ar_text mediumblob NOT NULL, + ar_comment tinyblob NOT NULL, + ar_user int unsigned NOT NULL default 0, + ar_user_text varchar(255) binary NOT NULL, + ar_timestamp binary(14) NOT NULL default '', + ar_minor_edit tinyint NOT NULL default 0, + ar_flags tinyblob NOT NULL, + ar_rev_id int unsigned, + ar_text_id int unsigned, + ar_deleted tinyint unsigned NOT NULL default 0, + ar_len int unsigned, + ar_page_id int unsigned, + ar_parent_id int unsigned default NULL, + ar_sha1 varbinary(32) NOT NULL default '', + ar_content_model varbinary(32) DEFAULT NULL, + ar_content_format varbinary(64) DEFAULT NULL +) /*$wgDBTableOptions*/; +CREATE INDEX /*i*/name_title_timestamp ON /*_*/archive (ar_namespace,ar_title,ar_timestamp); +CREATE INDEX /*i*/ar_usertext_timestamp ON /*_*/archive (ar_user_text,ar_timestamp); +CREATE INDEX /*i*/ar_revid ON /*_*/archive (ar_rev_id); +CREATE TABLE /*_*/pagelinks ( + pl_from int unsigned NOT NULL default 0, + pl_namespace int NOT NULL default 0, + pl_title varchar(255) binary NOT NULL default '' +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/pl_from ON /*_*/pagelinks (pl_from,pl_namespace,pl_title); +CREATE UNIQUE INDEX /*i*/pl_namespace ON /*_*/pagelinks (pl_namespace,pl_title,pl_from); +CREATE TABLE /*_*/templatelinks ( + tl_from int unsigned NOT NULL default 0, + tl_namespace int NOT NULL default 0, + tl_title varchar(255) binary NOT NULL default '' +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/tl_from ON /*_*/templatelinks (tl_from,tl_namespace,tl_title); +CREATE UNIQUE INDEX /*i*/tl_namespace ON /*_*/templatelinks (tl_namespace,tl_title,tl_from); +CREATE TABLE /*_*/imagelinks ( + il_from int unsigned NOT NULL default 0, + il_to varchar(255) binary NOT NULL default '' +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/il_from ON /*_*/imagelinks (il_from,il_to); +CREATE UNIQUE INDEX /*i*/il_to ON /*_*/imagelinks (il_to,il_from); +CREATE TABLE /*_*/categorylinks ( + cl_from int unsigned NOT NULL default 0, + cl_to varchar(255) binary NOT NULL default '', + cl_sortkey varbinary(230) NOT NULL default '', + cl_sortkey_prefix varchar(255) binary NOT NULL default '', + cl_timestamp timestamp NOT NULL, + cl_collation varbinary(32) NOT NULL default '', + cl_type ENUM('page', 'subcat', 'file') NOT NULL default 'page' +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/cl_from ON /*_*/categorylinks (cl_from,cl_to); +CREATE INDEX /*i*/cl_sortkey ON /*_*/categorylinks (cl_to,cl_type,cl_sortkey,cl_from); +CREATE INDEX /*i*/cl_timestamp ON /*_*/categorylinks (cl_to,cl_timestamp); +CREATE INDEX /*i*/cl_collation ON /*_*/categorylinks (cl_collation); +CREATE TABLE /*_*/category ( + cat_id int unsigned NOT NULL PRIMARY KEY AUTO_INCREMENT, + cat_title varchar(255) binary NOT NULL, + cat_pages int signed NOT NULL default 0, + cat_subcats int signed NOT NULL default 0, + cat_files int signed NOT NULL default 0 +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/cat_title ON /*_*/category (cat_title); +CREATE INDEX /*i*/cat_pages ON /*_*/category (cat_pages); +CREATE TABLE /*_*/externallinks ( + el_id int unsigned NOT NULL PRIMARY KEY AUTO_INCREMENT, + el_from int unsigned NOT NULL default 0, + el_to blob NOT NULL, + el_index blob NOT NULL +) /*$wgDBTableOptions*/; +CREATE INDEX /*i*/el_from ON /*_*/externallinks (el_from, el_to(40)); +CREATE INDEX /*i*/el_to ON /*_*/externallinks (el_to(60), el_from); +CREATE INDEX /*i*/el_index ON /*_*/externallinks (el_index(60)); +CREATE TABLE /*_*/langlinks ( + ll_from int unsigned NOT NULL default 0, + ll_lang varbinary(20) NOT NULL default '', + ll_title varchar(255) binary NOT NULL default '' +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/ll_from ON /*_*/langlinks (ll_from, ll_lang); +CREATE INDEX /*i*/ll_lang ON /*_*/langlinks (ll_lang, ll_title); +CREATE TABLE /*_*/iwlinks ( + iwl_from int unsigned NOT NULL default 0, + iwl_prefix varbinary(20) NOT NULL default '', + iwl_title varchar(255) binary NOT NULL default '' +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/iwl_from ON /*_*/iwlinks (iwl_from, iwl_prefix, iwl_title); +CREATE INDEX /*i*/iwl_prefix_title_from ON /*_*/iwlinks (iwl_prefix, iwl_title, iwl_from); +CREATE INDEX /*i*/iwl_prefix_from_title ON /*_*/iwlinks (iwl_prefix, iwl_from, iwl_title); +CREATE TABLE /*_*/site_stats ( + ss_row_id int unsigned NOT NULL, + ss_total_views bigint unsigned default 0, + ss_total_edits bigint unsigned default 0, + ss_good_articles bigint unsigned default 0, + ss_total_pages bigint default '-1', + ss_users bigint default '-1', + ss_active_users bigint default '-1', + ss_images int default 0 +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/ss_row_id ON /*_*/site_stats (ss_row_id); +CREATE TABLE /*_*/hitcounter ( + hc_id int unsigned NOT NULL +) ENGINE=HEAP MAX_ROWS=25000; +CREATE TABLE /*_*/ipblocks ( + ipb_id int NOT NULL PRIMARY KEY AUTO_INCREMENT, + ipb_address tinyblob NOT NULL, + ipb_user int unsigned NOT NULL default 0, + ipb_by int unsigned NOT NULL default 0, + ipb_by_text varchar(255) binary NOT NULL default '', + ipb_reason tinyblob NOT NULL, + ipb_timestamp binary(14) NOT NULL default '', + ipb_auto bool NOT NULL default 0, + ipb_anon_only bool NOT NULL default 0, + ipb_create_account bool NOT NULL default 1, + ipb_enable_autoblock bool NOT NULL default '1', + ipb_expiry varbinary(14) NOT NULL default '', + ipb_range_start tinyblob NOT NULL, + ipb_range_end tinyblob NOT NULL, + ipb_deleted bool NOT NULL default 0, + ipb_block_email bool NOT NULL default 0, + ipb_allow_usertalk bool NOT NULL default 0, + ipb_parent_block_id int default NULL +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/ipb_address ON /*_*/ipblocks (ipb_address(255), ipb_user, ipb_auto, ipb_anon_only); +CREATE INDEX /*i*/ipb_user ON /*_*/ipblocks (ipb_user); +CREATE INDEX /*i*/ipb_range ON /*_*/ipblocks (ipb_range_start(8), ipb_range_end(8)); +CREATE INDEX /*i*/ipb_timestamp ON /*_*/ipblocks (ipb_timestamp); +CREATE INDEX /*i*/ipb_expiry ON /*_*/ipblocks (ipb_expiry); +CREATE INDEX /*i*/ipb_parent_block_id ON /*_*/ipblocks (ipb_parent_block_id); +CREATE TABLE /*_*/image ( + img_name varchar(255) binary NOT NULL default '' PRIMARY KEY, + img_size int unsigned NOT NULL default 0, + img_width int NOT NULL default 0, + img_height int NOT NULL default 0, + img_metadata mediumblob NOT NULL, + img_bits int NOT NULL default 0, + img_media_type ENUM("UNKNOWN", "BITMAP", "DRAWING", "AUDIO", "VIDEO", "MULTIMEDIA", "OFFICE", "TEXT", "EXECUTABLE", "ARCHIVE") default NULL, + img_major_mime ENUM("unknown", "application", "audio", "image", "text", "video", "message", "model", "multipart") NOT NULL default "unknown", + img_minor_mime varbinary(100) NOT NULL default "unknown", + img_description tinyblob NOT NULL, + img_user int unsigned NOT NULL default 0, + img_user_text varchar(255) binary NOT NULL, + img_timestamp varbinary(14) NOT NULL default '', + img_sha1 varbinary(32) NOT NULL default '' +) /*$wgDBTableOptions*/; +CREATE INDEX /*i*/img_usertext_timestamp ON /*_*/image (img_user_text,img_timestamp); +CREATE INDEX /*i*/img_size ON /*_*/image (img_size); +CREATE INDEX /*i*/img_timestamp ON /*_*/image (img_timestamp); +CREATE INDEX /*i*/img_sha1 ON /*_*/image (img_sha1(10)); +CREATE INDEX /*i*/img_media_mime ON /*_*/image (img_media_type,img_major_mime,img_minor_mime); +CREATE TABLE /*_*/oldimage ( + oi_name varchar(255) binary NOT NULL default '', + oi_archive_name varchar(255) binary NOT NULL default '', + oi_size int unsigned NOT NULL default 0, + oi_width int NOT NULL default 0, + oi_height int NOT NULL default 0, + oi_bits int NOT NULL default 0, + oi_description tinyblob NOT NULL, + oi_user int unsigned NOT NULL default 0, + oi_user_text varchar(255) binary NOT NULL, + oi_timestamp binary(14) NOT NULL default '', + oi_metadata mediumblob NOT NULL, + oi_media_type ENUM("UNKNOWN", "BITMAP", "DRAWING", "AUDIO", "VIDEO", "MULTIMEDIA", "OFFICE", "TEXT", "EXECUTABLE", "ARCHIVE") default NULL, + oi_major_mime ENUM("unknown", "application", "audio", "image", "text", "video", "message", "model", "multipart") NOT NULL default "unknown", + oi_minor_mime varbinary(100) NOT NULL default "unknown", + oi_deleted tinyint unsigned NOT NULL default 0, + oi_sha1 varbinary(32) NOT NULL default '' +) /*$wgDBTableOptions*/; +CREATE INDEX /*i*/oi_usertext_timestamp ON /*_*/oldimage (oi_user_text,oi_timestamp); +CREATE INDEX /*i*/oi_name_timestamp ON /*_*/oldimage (oi_name,oi_timestamp); +CREATE INDEX /*i*/oi_name_archive_name ON /*_*/oldimage (oi_name,oi_archive_name(14)); +CREATE INDEX /*i*/oi_sha1 ON /*_*/oldimage (oi_sha1(10)); +CREATE TABLE /*_*/filearchive ( + fa_id int NOT NULL PRIMARY KEY AUTO_INCREMENT, + fa_name varchar(255) binary NOT NULL default '', + fa_archive_name varchar(255) binary default '', + fa_storage_group varbinary(16), + fa_storage_key varbinary(64) default '', + fa_deleted_user int, + fa_deleted_timestamp binary(14) default '', + fa_deleted_reason text, + fa_size int unsigned default 0, + fa_width int default 0, + fa_height int default 0, + fa_metadata mediumblob, + fa_bits int default 0, + fa_media_type ENUM("UNKNOWN", "BITMAP", "DRAWING", "AUDIO", "VIDEO", "MULTIMEDIA", "OFFICE", "TEXT", "EXECUTABLE", "ARCHIVE") default NULL, + fa_major_mime ENUM("unknown", "application", "audio", "image", "text", "video", "message", "model", "multipart") default "unknown", + fa_minor_mime varbinary(100) default "unknown", + fa_description tinyblob, + fa_user int unsigned default 0, + fa_user_text varchar(255) binary, + fa_timestamp binary(14) default '', + fa_deleted tinyint unsigned NOT NULL default 0, + fa_sha1 varbinary(32) NOT NULL default '' +) /*$wgDBTableOptions*/; +CREATE INDEX /*i*/fa_name ON /*_*/filearchive (fa_name, fa_timestamp); +CREATE INDEX /*i*/fa_storage_group ON /*_*/filearchive (fa_storage_group, fa_storage_key); +CREATE INDEX /*i*/fa_deleted_timestamp ON /*_*/filearchive (fa_deleted_timestamp); +CREATE INDEX /*i*/fa_user_timestamp ON /*_*/filearchive (fa_user_text,fa_timestamp); +CREATE INDEX /*i*/fa_sha1 ON /*_*/filearchive (fa_sha1(10)); +CREATE TABLE /*_*/uploadstash ( + us_id int unsigned NOT NULL PRIMARY KEY AUTO_INCREMENT, + us_user int unsigned NOT NULL, + us_key varchar(255) NOT NULL, + us_orig_path varchar(255) NOT NULL, + us_path varchar(255) NOT NULL, + us_source_type varchar(50), + us_timestamp varbinary(14) NOT NULL, + us_status varchar(50) NOT NULL, + us_chunk_inx int unsigned NULL, + us_props blob, + us_size int unsigned NOT NULL, + us_sha1 varchar(31) NOT NULL, + us_mime varchar(255), + us_media_type ENUM("UNKNOWN", "BITMAP", "DRAWING", "AUDIO", "VIDEO", "MULTIMEDIA", "OFFICE", "TEXT", "EXECUTABLE", "ARCHIVE") default NULL, + us_image_width int unsigned, + us_image_height int unsigned, + us_image_bits smallint unsigned +) /*$wgDBTableOptions*/; +CREATE INDEX /*i*/us_user ON /*_*/uploadstash (us_user); +CREATE UNIQUE INDEX /*i*/us_key ON /*_*/uploadstash (us_key); +CREATE INDEX /*i*/us_timestamp ON /*_*/uploadstash (us_timestamp); +CREATE TABLE /*_*/recentchanges ( + rc_id int NOT NULL PRIMARY KEY AUTO_INCREMENT, + rc_timestamp varbinary(14) NOT NULL default '', + rc_cur_time varbinary(14) NOT NULL default '', + rc_user int unsigned NOT NULL default 0, + rc_user_text varchar(255) binary NOT NULL, + rc_namespace int NOT NULL default 0, + rc_title varchar(255) binary NOT NULL default '', + rc_comment varchar(255) binary NOT NULL default '', + rc_minor tinyint unsigned NOT NULL default 0, + rc_bot tinyint unsigned NOT NULL default 0, + rc_new tinyint unsigned NOT NULL default 0, + rc_cur_id int unsigned NOT NULL default 0, + rc_this_oldid int unsigned NOT NULL default 0, + rc_last_oldid int unsigned NOT NULL default 0, + rc_type tinyint unsigned NOT NULL default 0, + rc_source varchar(16) binary not null default '', + rc_patrolled tinyint unsigned NOT NULL default 0, + rc_ip varbinary(40) NOT NULL default '', + rc_old_len int, + rc_new_len int, + rc_deleted tinyint unsigned NOT NULL default 0, + rc_logid int unsigned NOT NULL default 0, + rc_log_type varbinary(255) NULL default NULL, + rc_log_action varbinary(255) NULL default NULL, + rc_params blob NULL +) /*$wgDBTableOptions*/; +CREATE INDEX /*i*/rc_timestamp ON /*_*/recentchanges (rc_timestamp); +CREATE INDEX /*i*/rc_namespace_title ON /*_*/recentchanges (rc_namespace, rc_title); +CREATE INDEX /*i*/rc_cur_id ON /*_*/recentchanges (rc_cur_id); +CREATE INDEX /*i*/new_name_timestamp ON /*_*/recentchanges (rc_new,rc_namespace,rc_timestamp); +CREATE INDEX /*i*/rc_ip ON /*_*/recentchanges (rc_ip); +CREATE INDEX /*i*/rc_ns_usertext ON /*_*/recentchanges (rc_namespace, rc_user_text); +CREATE INDEX /*i*/rc_user_text ON /*_*/recentchanges (rc_user_text, rc_timestamp); +CREATE TABLE /*_*/watchlist ( + wl_user int unsigned NOT NULL, + wl_namespace int NOT NULL default 0, + wl_title varchar(255) binary NOT NULL default '', + wl_notificationtimestamp varbinary(14) +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/wl_user ON /*_*/watchlist (wl_user, wl_namespace, wl_title); +CREATE INDEX /*i*/namespace_title ON /*_*/watchlist (wl_namespace, wl_title); +CREATE TABLE /*_*/searchindex ( + si_page int unsigned NOT NULL, + si_title varchar(255) NOT NULL default '', + si_text mediumtext NOT NULL +) ENGINE=MyISAM DEFAULT CHARSET=utf8; +CREATE UNIQUE INDEX /*i*/si_page ON /*_*/searchindex (si_page); +CREATE FULLTEXT INDEX /*i*/si_title ON /*_*/searchindex (si_title); +CREATE FULLTEXT INDEX /*i*/si_text ON /*_*/searchindex (si_text); +CREATE TABLE /*_*/interwiki ( + iw_prefix varchar(32) NOT NULL, + iw_url blob NOT NULL, + iw_api blob NOT NULL, + iw_wikiid varchar(64) NOT NULL, + iw_local bool NOT NULL, + iw_trans tinyint NOT NULL default 0 +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/iw_prefix ON /*_*/interwiki (iw_prefix); +CREATE TABLE /*_*/querycache ( + qc_type varbinary(32) NOT NULL, + qc_value int unsigned NOT NULL default 0, + qc_namespace int NOT NULL default 0, + qc_title varchar(255) binary NOT NULL default '' +) /*$wgDBTableOptions*/; +CREATE INDEX /*i*/qc_type ON /*_*/querycache (qc_type,qc_value); +CREATE TABLE /*_*/objectcache ( + keyname varbinary(255) NOT NULL default '' PRIMARY KEY, + value mediumblob, + exptime datetime +) /*$wgDBTableOptions*/; +CREATE INDEX /*i*/exptime ON /*_*/objectcache (exptime); +CREATE TABLE /*_*/transcache ( + tc_url varbinary(255) NOT NULL, + tc_contents text, + tc_time binary(14) NOT NULL +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/tc_url_idx ON /*_*/transcache (tc_url); +CREATE TABLE /*_*/logging ( + log_id int unsigned NOT NULL PRIMARY KEY AUTO_INCREMENT, + log_type varbinary(32) NOT NULL default '', + log_action varbinary(32) NOT NULL default '', + log_timestamp binary(14) NOT NULL default '19700101000000', + log_user int unsigned NOT NULL default 0, + log_user_text varchar(255) binary NOT NULL default '', + log_namespace int NOT NULL default 0, + log_title varchar(255) binary NOT NULL default '', + log_page int unsigned NULL, + log_comment varchar(255) NOT NULL default '', + log_params blob NOT NULL, + log_deleted tinyint unsigned NOT NULL default 0 +) /*$wgDBTableOptions*/; +CREATE INDEX /*i*/type_time ON /*_*/logging (log_type, log_timestamp); +CREATE INDEX /*i*/user_time ON /*_*/logging (log_user, log_timestamp); +CREATE INDEX /*i*/page_time ON /*_*/logging (log_namespace, log_title, log_timestamp); +CREATE INDEX /*i*/times ON /*_*/logging (log_timestamp); +CREATE INDEX /*i*/log_user_type_time ON /*_*/logging (log_user, log_type, log_timestamp); +CREATE INDEX /*i*/log_page_id_time ON /*_*/logging (log_page,log_timestamp); +CREATE INDEX /*i*/type_action ON /*_*/logging (log_type, log_action, log_timestamp); +CREATE INDEX /*i*/log_user_text_type_time ON /*_*/logging (log_user_text, log_type, log_timestamp); +CREATE INDEX /*i*/log_user_text_time ON /*_*/logging (log_user_text, log_timestamp); +CREATE TABLE /*_*/log_search ( + ls_field varbinary(32) NOT NULL, + ls_value varchar(255) NOT NULL, + ls_log_id int unsigned NOT NULL default 0 +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/ls_field_val ON /*_*/log_search (ls_field,ls_value,ls_log_id); +CREATE INDEX /*i*/ls_log_id ON /*_*/log_search (ls_log_id); +CREATE TABLE /*_*/job ( + job_id int unsigned NOT NULL PRIMARY KEY AUTO_INCREMENT, + job_cmd varbinary(60) NOT NULL default '', + job_namespace int NOT NULL, + job_title varchar(255) binary NOT NULL, + job_timestamp varbinary(14) NULL default NULL, + job_params blob NOT NULL, + job_random integer unsigned NOT NULL default 0, + job_attempts integer unsigned NOT NULL default 0, + job_token varbinary(32) NOT NULL default '', + job_token_timestamp varbinary(14) NULL default NULL, + job_sha1 varbinary(32) NOT NULL default '' +) /*$wgDBTableOptions*/; +CREATE INDEX /*i*/job_sha1 ON /*_*/job (job_sha1); +CREATE INDEX /*i*/job_cmd_token ON /*_*/job (job_cmd,job_token,job_random); +CREATE INDEX /*i*/job_cmd_token_id ON /*_*/job (job_cmd,job_token,job_id); +CREATE INDEX /*i*/job_cmd ON /*_*/job (job_cmd, job_namespace, job_title, job_params(128)); +CREATE INDEX /*i*/job_timestamp ON /*_*/job (job_timestamp); +CREATE TABLE /*_*/querycache_info ( + qci_type varbinary(32) NOT NULL default '', + qci_timestamp binary(14) NOT NULL default '19700101000000' +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/qci_type ON /*_*/querycache_info (qci_type); +CREATE TABLE /*_*/redirect ( + rd_from int unsigned NOT NULL default 0 PRIMARY KEY, + rd_namespace int NOT NULL default 0, + rd_title varchar(255) binary NOT NULL default '', + rd_interwiki varchar(32) default NULL, + rd_fragment varchar(255) binary default NULL +) /*$wgDBTableOptions*/; +CREATE INDEX /*i*/rd_ns_title ON /*_*/redirect (rd_namespace,rd_title,rd_from); +CREATE TABLE /*_*/querycachetwo ( + qcc_type varbinary(32) NOT NULL, + qcc_value int unsigned NOT NULL default 0, + qcc_namespace int NOT NULL default 0, + qcc_title varchar(255) binary NOT NULL default '', + qcc_namespacetwo int NOT NULL default 0, + qcc_titletwo varchar(255) binary NOT NULL default '' +) /*$wgDBTableOptions*/; +CREATE INDEX /*i*/qcc_type ON /*_*/querycachetwo (qcc_type,qcc_value); +CREATE INDEX /*i*/qcc_title ON /*_*/querycachetwo (qcc_type,qcc_namespace,qcc_title); +CREATE INDEX /*i*/qcc_titletwo ON /*_*/querycachetwo (qcc_type,qcc_namespacetwo,qcc_titletwo); +CREATE TABLE /*_*/page_restrictions ( + pr_id int unsigned NOT NULL PRIMARY KEY AUTO_INCREMENT, + pr_page int NOT NULL, + pr_type varbinary(60) NOT NULL, + pr_level varbinary(60) NOT NULL, + pr_cascade tinyint NOT NULL, + pr_user int NULL, + pr_expiry varbinary(14) NULL +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/pr_pagetype ON /*_*/page_restrictions (pr_page,pr_type); +CREATE INDEX /*i*/pr_typelevel ON /*_*/page_restrictions (pr_type,pr_level); +CREATE INDEX /*i*/pr_level ON /*_*/page_restrictions (pr_level); +CREATE INDEX /*i*/pr_cascade ON /*_*/page_restrictions (pr_cascade); +CREATE TABLE /*_*/protected_titles ( + pt_namespace int NOT NULL, + pt_title varchar(255) binary NOT NULL, + pt_user int unsigned NOT NULL, + pt_reason tinyblob, + pt_timestamp binary(14) NOT NULL, + pt_expiry varbinary(14) NOT NULL default '', + pt_create_perm varbinary(60) NOT NULL +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/pt_namespace_title ON /*_*/protected_titles (pt_namespace,pt_title); +CREATE INDEX /*i*/pt_timestamp ON /*_*/protected_titles (pt_timestamp); +CREATE TABLE /*_*/page_props ( + pp_page int NOT NULL, + pp_propname varbinary(60) NOT NULL, + pp_value blob NOT NULL +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/pp_page_propname ON /*_*/page_props (pp_page,pp_propname); +CREATE UNIQUE INDEX /*i*/pp_propname_page ON /*_*/page_props (pp_propname,pp_page); +CREATE TABLE /*_*/updatelog ( + ul_key varchar(255) NOT NULL PRIMARY KEY, + ul_value blob +) /*$wgDBTableOptions*/; +CREATE TABLE /*_*/change_tag ( + ct_rc_id int NULL, + ct_log_id int NULL, + ct_rev_id int NULL, + ct_tag varchar(255) NOT NULL, + ct_params blob NULL +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/change_tag_rc_tag ON /*_*/change_tag (ct_rc_id,ct_tag); +CREATE UNIQUE INDEX /*i*/change_tag_log_tag ON /*_*/change_tag (ct_log_id,ct_tag); +CREATE UNIQUE INDEX /*i*/change_tag_rev_tag ON /*_*/change_tag (ct_rev_id,ct_tag); +CREATE INDEX /*i*/change_tag_tag_id ON /*_*/change_tag (ct_tag,ct_rc_id,ct_rev_id,ct_log_id); +CREATE TABLE /*_*/tag_summary ( + ts_rc_id int NULL, + ts_log_id int NULL, + ts_rev_id int NULL, + ts_tags blob NOT NULL +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/tag_summary_rc_id ON /*_*/tag_summary (ts_rc_id); +CREATE UNIQUE INDEX /*i*/tag_summary_log_id ON /*_*/tag_summary (ts_log_id); +CREATE UNIQUE INDEX /*i*/tag_summary_rev_id ON /*_*/tag_summary (ts_rev_id); +CREATE TABLE /*_*/valid_tag ( + vt_tag varchar(255) NOT NULL PRIMARY KEY +) /*$wgDBTableOptions*/; +CREATE TABLE /*_*/l10n_cache ( + lc_lang varbinary(32) NOT NULL, + lc_key varchar(255) NOT NULL, + lc_value mediumblob NOT NULL +) /*$wgDBTableOptions*/; +CREATE INDEX /*i*/lc_lang_key ON /*_*/l10n_cache (lc_lang, lc_key); +CREATE TABLE /*_*/msg_resource ( + mr_resource varbinary(255) NOT NULL, + mr_lang varbinary(32) NOT NULL, + mr_blob mediumblob NOT NULL, + mr_timestamp binary(14) NOT NULL +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/mr_resource_lang ON /*_*/msg_resource (mr_resource, mr_lang); +CREATE TABLE /*_*/msg_resource_links ( + mrl_resource varbinary(255) NOT NULL, + mrl_message varbinary(255) NOT NULL +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/mrl_message_resource ON /*_*/msg_resource_links (mrl_message, mrl_resource); +CREATE TABLE /*_*/module_deps ( + md_module varbinary(255) NOT NULL, + md_skin varbinary(32) NOT NULL, + md_deps mediumblob NOT NULL +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/md_module_skin ON /*_*/module_deps (md_module, md_skin); +CREATE TABLE /*_*/sites ( + site_id INT UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT, + site_global_key varbinary(32) NOT NULL, + site_type varbinary(32) NOT NULL, + site_group varbinary(32) NOT NULL, + site_source varbinary(32) NOT NULL, + site_language varbinary(32) NOT NULL, + site_protocol varbinary(32) NOT NULL, + site_domain VARCHAR(255) NOT NULL, + site_data BLOB NOT NULL, + site_forward bool NOT NULL, + site_config BLOB NOT NULL +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/sites_global_key ON /*_*/sites (site_global_key); +CREATE INDEX /*i*/sites_type ON /*_*/sites (site_type); +CREATE INDEX /*i*/sites_group ON /*_*/sites (site_group); +CREATE INDEX /*i*/sites_source ON /*_*/sites (site_source); +CREATE INDEX /*i*/sites_language ON /*_*/sites (site_language); +CREATE INDEX /*i*/sites_protocol ON /*_*/sites (site_protocol); +CREATE INDEX /*i*/sites_domain ON /*_*/sites (site_domain); +CREATE INDEX /*i*/sites_forward ON /*_*/sites (site_forward); +CREATE TABLE /*_*/site_identifiers ( + si_site INT UNSIGNED NOT NULL, + si_type varbinary(32) NOT NULL, + si_key varbinary(32) NOT NULL +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/site_ids_type ON /*_*/site_identifiers (si_type, si_key); +CREATE INDEX /*i*/site_ids_site ON /*_*/site_identifiers (si_site); +CREATE INDEX /*i*/site_ids_key ON /*_*/site_identifiers (si_key); diff --git a/www/wiki/tests/phpunit/data/helpers/WellProtectedClass.php b/www/wiki/tests/phpunit/data/helpers/WellProtectedClass.php deleted file mode 100644 index f2b5a149..00000000 --- a/www/wiki/tests/phpunit/data/helpers/WellProtectedClass.php +++ /dev/null @@ -1,59 +0,0 @@ -<?php - -class WellProtectedParentClass { - private $privateParentProperty; - - public function __construct() { - $this->privateParentProperty = 9000; - } - - private function incrementPrivateParentPropertyValue() { - $this->privateParentProperty++; - } - - public function getPrivateParentProperty() { - return $this->privateParentProperty; - } -} - -class WellProtectedClass extends WellProtectedParentClass { - protected static $staticProperty = 'sp'; - private static $staticPrivateProperty = 'spp'; - - protected $property; - private $privateProperty; - - protected static function staticMethod() { - return 'sm'; - } - - private static function staticPrivateMethod() { - return 'spm'; - } - - public function __construct() { - parent::__construct(); - $this->property = 1; - $this->privateProperty = 42; - } - - protected function incrementPropertyValue() { - $this->property++; - } - - private function incrementPrivatePropertyValue() { - $this->privateProperty++; - } - - public function getProperty() { - return $this->property; - } - - public function getPrivateProperty() { - return $this->privateProperty; - } - - protected function whatSecondArg( $a, $b = false ) { - return $b; - } -} diff --git a/www/wiki/tests/phpunit/data/localisationcache/uk.json b/www/wiki/tests/phpunit/data/localisationcache/uk.json deleted file mode 100644 index f63ce5d3..00000000 --- a/www/wiki/tests/phpunit/data/localisationcache/uk.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "present-uk": "uk" -} diff --git a/www/wiki/tests/phpunit/data/media/README b/www/wiki/tests/phpunit/data/media/README index 9913f685..52f19128 100644 --- a/www/wiki/tests/phpunit/data/media/README +++ b/www/wiki/tests/phpunit/data/media/README @@ -4,18 +4,18 @@ tests in includes/media directory. Image credits: QA_icon.svg: -http://es.wikipedia.org/wiki/Archivo:QA_icon.svg +https://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 +https://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 +https://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 +https://commons.wikimedia.org/wiki/File:US_states_by_total_state_tax_revenue.svg CC BY 3.0 TastyCakes on English Wikipedia @@ -32,7 +32,7 @@ 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 ) +https://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/www/wiki/tests/phpunit/data/media/jpeg-segment-loop1.jpg b/www/wiki/tests/phpunit/data/media/jpeg-segment-loop1.jpg Binary files differnew file mode 100644 index 00000000..962f3fe0 --- /dev/null +++ b/www/wiki/tests/phpunit/data/media/jpeg-segment-loop1.jpg diff --git a/www/wiki/tests/phpunit/data/media/jpeg-segment-loop2.jpg b/www/wiki/tests/phpunit/data/media/jpeg-segment-loop2.jpg Binary files differnew file mode 100644 index 00000000..e3a7505c --- /dev/null +++ b/www/wiki/tests/phpunit/data/media/jpeg-segment-loop2.jpg diff --git a/www/wiki/tests/phpunit/data/registration/bad_spdx.json b/www/wiki/tests/phpunit/data/registration/bad_spdx.json new file mode 100644 index 00000000..383ab047 --- /dev/null +++ b/www/wiki/tests/phpunit/data/registration/bad_spdx.json @@ -0,0 +1,6 @@ +{ + "name": "FooBar", + "license-name": "not a license identifier", + "manifest_version": 1 +} + diff --git a/www/wiki/tests/phpunit/data/registration/good.json b/www/wiki/tests/phpunit/data/registration/good.json new file mode 100644 index 00000000..ad16c5e4 --- /dev/null +++ b/www/wiki/tests/phpunit/data/registration/good.json @@ -0,0 +1,4 @@ +{ + "name": "FooBar", + "manifest_version": 1 +} diff --git a/www/wiki/tests/phpunit/data/registration/invalid.json b/www/wiki/tests/phpunit/data/registration/invalid.json new file mode 100644 index 00000000..4d1fa589 --- /dev/null +++ b/www/wiki/tests/phpunit/data/registration/invalid.json @@ -0,0 +1,5 @@ +{ + "name": "FooBar", + "license-name": [ "array" ], + "manifest_version": 1 +} diff --git a/www/wiki/tests/phpunit/data/registration/newer_manifest_version.json b/www/wiki/tests/phpunit/data/registration/newer_manifest_version.json new file mode 100644 index 00000000..29c668ee --- /dev/null +++ b/www/wiki/tests/phpunit/data/registration/newer_manifest_version.json @@ -0,0 +1,4 @@ +{ + "name": "FooBar", + "manifest_version": 999999 +} diff --git a/www/wiki/tests/phpunit/data/registration/no_manifest_version.json b/www/wiki/tests/phpunit/data/registration/no_manifest_version.json new file mode 100644 index 00000000..1a6119f7 --- /dev/null +++ b/www/wiki/tests/phpunit/data/registration/no_manifest_version.json @@ -0,0 +1,3 @@ +{ + "name": "FooBar" +} diff --git a/www/wiki/tests/phpunit/data/registration/notjson.txt b/www/wiki/tests/phpunit/data/registration/notjson.txt new file mode 100644 index 00000000..d47b4607 --- /dev/null +++ b/www/wiki/tests/phpunit/data/registration/notjson.txt @@ -0,0 +1 @@ +This is definitely not JSON. diff --git a/www/wiki/tests/phpunit/data/registration/old_manifest_version.json b/www/wiki/tests/phpunit/data/registration/old_manifest_version.json new file mode 100644 index 00000000..f50faa1b --- /dev/null +++ b/www/wiki/tests/phpunit/data/registration/old_manifest_version.json @@ -0,0 +1,4 @@ +{ + "name": "FooBar", + "manifest_version": -2 +} diff --git a/www/wiki/tests/phpunit/data/resourceloader/add.gif b/www/wiki/tests/phpunit/data/resourceloader/add.gif Binary files differdeleted file mode 100644 index 5f454ca1..00000000 --- a/www/wiki/tests/phpunit/data/resourceloader/add.gif +++ /dev/null diff --git a/www/wiki/tests/phpunit/data/resourceloader/bold-a.svg b/www/wiki/tests/phpunit/data/resourceloader/bold-a.svg deleted file mode 100644 index 4b828779..00000000 --- a/www/wiki/tests/phpunit/data/resourceloader/bold-a.svg +++ /dev/null @@ -1,6 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"> - <g id="bold-a"> - <path d="M16 18h3l-5-12h-3l-5 12h3l1.25-3h4.5l1.25 3zm-4.917-5l1.417-3.4 1.417 3.4h-2.834z"/> - </g> -</svg> diff --git a/www/wiki/tests/phpunit/data/resourceloader/bold-b.svg b/www/wiki/tests/phpunit/data/resourceloader/bold-b.svg deleted file mode 100644 index 4f648203..00000000 --- a/www/wiki/tests/phpunit/data/resourceloader/bold-b.svg +++ /dev/null @@ -1,6 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"> - <g id="bold-b"> - <path id="b" d="M7 18h6c2 0 4-1 4-3 0-1.064.011-1.975-1.989-3 2-.975 1.989-1.935 1.989-3 0-2-2-3-4-3h-6v12zm7-8c0 1.001 0 1-2 1h-2v-3h2c2 0 2 0 2 1v1zm-2 6h-2v-3h2c2 0 2 0 2 1v1s0 1-2 1z"/> - </g> -</svg> diff --git a/www/wiki/tests/phpunit/data/resourceloader/bold-f.svg b/www/wiki/tests/phpunit/data/resourceloader/bold-f.svg deleted file mode 100644 index 357d2e5d..00000000 --- a/www/wiki/tests/phpunit/data/resourceloader/bold-f.svg +++ /dev/null @@ -1,6 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"> - <g id="bold-f"> - <path id="f" d="M16 8v-2h-8v12h3v-5h4v-2h-4v-3z"/> - </g> -</svg> diff --git a/www/wiki/tests/phpunit/data/resourceloader/help-ltr.svg b/www/wiki/tests/phpunit/data/resourceloader/help-ltr.svg deleted file mode 100644 index bb2545c5..00000000 --- a/www/wiki/tests/phpunit/data/resourceloader/help-ltr.svg +++ /dev/null @@ -1,10 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"> - <g id="help"> - <path id="circle" d="M12.001 2.085c-5.478 0-9.916 4.438-9.916 9.916 0 5.476 4.438 9.914 9.916 9.914 5.476 0 9.914-4.438 9.914-9.914 0-5.478-4.438-9.916-9.914-9.916zm.001 18c-4.465 0-8.084-3.619-8.084-8.083 0-4.465 3.619-8.084 8.084-8.084 4.464 0 8.083 3.619 8.083 8.084 0 4.464-3.619 8.083-8.083 8.083z"/> - <g id="question-mark"> - <path id="top" d="M11.766 6.688c-2.5 0-3.219 2.188-3.219 2.188l1.411.854s.298-.791.901-1.229c.516-.375 1.625-.625 2.219.125.701.885-.17 1.587-1.078 2.719-.953 1.186-1 3.655-1 3.655h1.969s.135-2.318 1.041-3.381c.603-.707 1.443-1.338 1.443-2.494s-1.187-2.437-3.687-2.437z"/> - <path id="bottom" d="M11 16h2v2h-2z"/> - </g> - </g> -</svg> diff --git a/www/wiki/tests/phpunit/data/resourceloader/help-rtl.svg b/www/wiki/tests/phpunit/data/resourceloader/help-rtl.svg deleted file mode 100644 index 255ae95b..00000000 --- a/www/wiki/tests/phpunit/data/resourceloader/help-rtl.svg +++ /dev/null @@ -1,10 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"> - <g id="help"> - <path id="circle" d="M12.001 2.085c-5.478 0-9.916 4.438-9.916 9.916 0 5.476 4.438 9.914 9.916 9.914 5.476 0 9.914-4.438 9.914-9.914 0-5.478-4.438-9.916-9.914-9.916zm.001 18c-4.465 0-8.084-3.619-8.084-8.083 0-4.465 3.619-8.084 8.084-8.084 4.464 0 8.083 3.619 8.083 8.084 0 4.464-3.619 8.083-8.083 8.083z"/> - <g id="question-mark" transform="translate(24, 0) scale(-1, 1)"> - <path id="top" d="M11.766 6.688c-2.5 0-3.219 2.188-3.219 2.188l1.411.854s.298-.791.901-1.229c.516-.375 1.625-.625 2.219.125.701.885-.17 1.587-1.078 2.719-.953 1.186-1 3.655-1 3.655h1.969s.135-2.318 1.041-3.381c.603-.707 1.443-1.338 1.443-2.494s-1.187-2.437-3.687-2.437z"/> - <path id="bottom" d="M11 16h2v2h-2z"/> - </g> - </g> -</svg> diff --git a/www/wiki/tests/phpunit/data/resourceloader/next.svg b/www/wiki/tests/phpunit/data/resourceloader/next.svg deleted file mode 100644 index 02b4e387..00000000 --- a/www/wiki/tests/phpunit/data/resourceloader/next.svg +++ /dev/null @@ -1,4 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"> - <path d="M16.5 13.1l-8.9 8.9c-.8-.8-.8-2 0-2.8l6.1-6.1-6-6.1c-.8-.8-.8-2 0-2.8l8.8 8.9z" id="path108"/> -</svg> diff --git a/www/wiki/tests/phpunit/data/resourceloader/next_massage.svg b/www/wiki/tests/phpunit/data/resourceloader/next_massage.svg deleted file mode 100644 index bbd1a8d6..00000000 --- a/www/wiki/tests/phpunit/data/resourceloader/next_massage.svg +++ /dev/null @@ -1,4 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"> - <path d="M 16.5 13.1 l -8.9 8.9 c -0.8 -0.8 -0.8 -2 0 -2.8 l 6.1 -6.1 -6 -6.1 c -0.8 -0.8 -0.8 -2 0 -2.8 l 8.8 8.9 z" id="path108"/> -</svg> diff --git a/www/wiki/tests/phpunit/data/resourceloader/prev.svg b/www/wiki/tests/phpunit/data/resourceloader/prev.svg deleted file mode 100644 index f31ec095..00000000 --- a/www/wiki/tests/phpunit/data/resourceloader/prev.svg +++ /dev/null @@ -1,4 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"> - <path d="M7 13.1l8.9 8.9c.8-.8.8-2 0-2.8l-6.1-6.1 6-6.1c.8-.8.8-2 0-2.8l-8.8 8.9z"/> -</svg> diff --git a/www/wiki/tests/phpunit/data/resourceloader/remove.svg b/www/wiki/tests/phpunit/data/resourceloader/remove.svg deleted file mode 100644 index 6ad79174..00000000 --- a/www/wiki/tests/phpunit/data/resourceloader/remove.svg +++ /dev/null @@ -1,6 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"> - <g id="remove"> - <path id="trash-can" d="M12 10h-1v6h1v-6zm-2 0h-1v6h1v-6zm4 0h-1v6h1v-6zm0-4v-1h-5v1h-3v3h1v7.966l1 1.031v-.074.077h6.984l.016-.018v.015l1-1.031v-7.966h1v-3h-3zm1 11h-7v-8h7v8zm1-9h-9v-1h9v1z"/> - </g> -</svg> diff --git a/www/wiki/tests/phpunit/data/resourceloader/remove_variantize.svg b/www/wiki/tests/phpunit/data/resourceloader/remove_variantize.svg deleted file mode 100644 index bcbe8712..00000000 --- a/www/wiki/tests/phpunit/data/resourceloader/remove_variantize.svg +++ /dev/null @@ -1,6 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><g fill="red"> - <g xmlns:default="http://www.w3.org/2000/svg" id="remove"> - <path id="trash-can" d="M12 10h-1v6h1v-6zm-2 0h-1v6h1v-6zm4 0h-1v6h1v-6zm0-4v-1h-5v1h-3v3h1v7.966l1 1.031v-.074.077h6.984l.016-.018v.015l1-1.031v-7.966h1v-3h-3zm1 11h-7v-8h7v8zm1-9h-9v-1h9v1z"/> - </g> -</g></svg> diff --git a/www/wiki/tests/phpunit/data/templates/conds.mustache b/www/wiki/tests/phpunit/data/templates/conds.mustache deleted file mode 100644 index 5ebd2ea3..00000000 --- a/www/wiki/tests/phpunit/data/templates/conds.mustache +++ /dev/null @@ -1 +0,0 @@ -{{#list}}oh no{{/list}}{{#foo}}none of this should render{{/foo}}
\ No newline at end of file diff --git a/www/wiki/tests/phpunit/includes/ActorMigrationTest.php b/www/wiki/tests/phpunit/includes/ActorMigrationTest.php new file mode 100644 index 00000000..1b0c848b --- /dev/null +++ b/www/wiki/tests/phpunit/includes/ActorMigrationTest.php @@ -0,0 +1,695 @@ +<?php + +use MediaWiki\User\UserIdentity; +use Wikimedia\TestingAccessWrapper; + +/** + * @group Database + * @covers ActorMigration + */ +class ActorMigrationTest extends MediaWikiLangTestCase { + + protected $tablesUsed = [ + 'revision', + 'revision_actor_temp', + 'ipblocks', + 'recentchanges', + 'actor', + ]; + + /** + * Create an ActorMigration for a particular stage + * @param int $stage + * @return ActorMigration + */ + protected function makeMigration( $stage ) { + return new ActorMigration( $stage ); + } + + /** + * @dataProvider provideGetJoin + * @param int $stage + * @param string $key + * @param array $expect + */ + public function testGetJoin( $stage, $key, $expect ) { + $m = $this->makeMigration( $stage ); + $result = $m->getJoin( $key ); + $this->assertEquals( $expect, $result ); + } + + public static function provideGetJoin() { + return [ + 'Simple table, old' => [ + MIGRATION_OLD, 'rc_user', [ + 'tables' => [], + 'fields' => [ + 'rc_user' => 'rc_user', + 'rc_user_text' => 'rc_user_text', + 'rc_actor' => 'NULL', + ], + 'joins' => [], + ], + ], + 'Simple table, write-both' => [ + MIGRATION_WRITE_BOTH, 'rc_user', [ + 'tables' => [ 'actor_rc_user' => 'actor' ], + 'fields' => [ + 'rc_user' => 'COALESCE( actor_rc_user.actor_user, rc_user )', + 'rc_user_text' => 'COALESCE( actor_rc_user.actor_name, rc_user_text )', + 'rc_actor' => 'rc_actor', + ], + 'joins' => [ + 'actor_rc_user' => [ 'LEFT JOIN', 'actor_rc_user.actor_id = rc_actor' ], + ], + ], + ], + 'Simple table, write-new' => [ + MIGRATION_WRITE_NEW, 'rc_user', [ + 'tables' => [ 'actor_rc_user' => 'actor' ], + 'fields' => [ + 'rc_user' => 'COALESCE( actor_rc_user.actor_user, rc_user )', + 'rc_user_text' => 'COALESCE( actor_rc_user.actor_name, rc_user_text )', + 'rc_actor' => 'rc_actor', + ], + 'joins' => [ + 'actor_rc_user' => [ 'LEFT JOIN', 'actor_rc_user.actor_id = rc_actor' ], + ], + ], + ], + 'Simple table, new' => [ + MIGRATION_NEW, 'rc_user', [ + 'tables' => [ 'actor_rc_user' => 'actor' ], + 'fields' => [ + 'rc_user' => 'actor_rc_user.actor_user', + 'rc_user_text' => 'actor_rc_user.actor_name', + 'rc_actor' => 'rc_actor', + ], + 'joins' => [ + 'actor_rc_user' => [ 'JOIN', 'actor_rc_user.actor_id = rc_actor' ], + ], + ], + ], + + 'ipblocks, old' => [ + MIGRATION_OLD, 'ipb_by', [ + 'tables' => [], + 'fields' => [ + 'ipb_by' => 'ipb_by', + 'ipb_by_text' => 'ipb_by_text', + 'ipb_by_actor' => 'NULL', + ], + 'joins' => [], + ], + ], + 'ipblocks, write-both' => [ + MIGRATION_WRITE_BOTH, 'ipb_by', [ + 'tables' => [ 'actor_ipb_by' => 'actor' ], + 'fields' => [ + 'ipb_by' => 'COALESCE( actor_ipb_by.actor_user, ipb_by )', + 'ipb_by_text' => 'COALESCE( actor_ipb_by.actor_name, ipb_by_text )', + 'ipb_by_actor' => 'ipb_by_actor', + ], + 'joins' => [ + 'actor_ipb_by' => [ 'LEFT JOIN', 'actor_ipb_by.actor_id = ipb_by_actor' ], + ], + ], + ], + 'ipblocks, write-new' => [ + MIGRATION_WRITE_NEW, 'ipb_by', [ + 'tables' => [ 'actor_ipb_by' => 'actor' ], + 'fields' => [ + 'ipb_by' => 'COALESCE( actor_ipb_by.actor_user, ipb_by )', + 'ipb_by_text' => 'COALESCE( actor_ipb_by.actor_name, ipb_by_text )', + 'ipb_by_actor' => 'ipb_by_actor', + ], + 'joins' => [ + 'actor_ipb_by' => [ 'LEFT JOIN', 'actor_ipb_by.actor_id = ipb_by_actor' ], + ], + ], + ], + 'ipblocks, new' => [ + MIGRATION_NEW, 'ipb_by', [ + 'tables' => [ 'actor_ipb_by' => 'actor' ], + 'fields' => [ + 'ipb_by' => 'actor_ipb_by.actor_user', + 'ipb_by_text' => 'actor_ipb_by.actor_name', + 'ipb_by_actor' => 'ipb_by_actor', + ], + 'joins' => [ + 'actor_ipb_by' => [ 'JOIN', 'actor_ipb_by.actor_id = ipb_by_actor' ], + ], + ], + ], + + 'Revision, old' => [ + MIGRATION_OLD, 'rev_user', [ + 'tables' => [], + 'fields' => [ + 'rev_user' => 'rev_user', + 'rev_user_text' => 'rev_user_text', + 'rev_actor' => 'NULL', + ], + 'joins' => [], + ], + ], + 'Revision, write-both' => [ + MIGRATION_WRITE_BOTH, 'rev_user', [ + 'tables' => [ + 'temp_rev_user' => 'revision_actor_temp', + 'actor_rev_user' => 'actor', + ], + 'fields' => [ + 'rev_user' => 'COALESCE( actor_rev_user.actor_user, rev_user )', + 'rev_user_text' => 'COALESCE( actor_rev_user.actor_name, rev_user_text )', + 'rev_actor' => 'temp_rev_user.revactor_actor', + ], + 'joins' => [ + 'temp_rev_user' => [ 'LEFT JOIN', 'temp_rev_user.revactor_rev = rev_id' ], + 'actor_rev_user' => [ 'LEFT JOIN', 'actor_rev_user.actor_id = temp_rev_user.revactor_actor' ], + ], + ], + ], + 'Revision, write-new' => [ + MIGRATION_WRITE_NEW, 'rev_user', [ + 'tables' => [ + 'temp_rev_user' => 'revision_actor_temp', + 'actor_rev_user' => 'actor', + ], + 'fields' => [ + 'rev_user' => 'COALESCE( actor_rev_user.actor_user, rev_user )', + 'rev_user_text' => 'COALESCE( actor_rev_user.actor_name, rev_user_text )', + 'rev_actor' => 'temp_rev_user.revactor_actor', + ], + 'joins' => [ + 'temp_rev_user' => [ 'LEFT JOIN', 'temp_rev_user.revactor_rev = rev_id' ], + 'actor_rev_user' => [ 'LEFT JOIN', 'actor_rev_user.actor_id = temp_rev_user.revactor_actor' ], + ], + ], + ], + 'Revision, new' => [ + MIGRATION_NEW, 'rev_user', [ + 'tables' => [ + 'temp_rev_user' => 'revision_actor_temp', + 'actor_rev_user' => 'actor', + ], + 'fields' => [ + 'rev_user' => 'actor_rev_user.actor_user', + 'rev_user_text' => 'actor_rev_user.actor_name', + 'rev_actor' => 'temp_rev_user.revactor_actor', + ], + 'joins' => [ + 'temp_rev_user' => [ 'JOIN', 'temp_rev_user.revactor_rev = rev_id' ], + 'actor_rev_user' => [ 'JOIN', 'actor_rev_user.actor_id = temp_rev_user.revactor_actor' ], + ], + ], + ], + ]; + } + + /** + * @dataProvider provideGetWhere + * @param int $stage + * @param string $key + * @param UserIdentity[] $users + * @param bool $useId + * @param array $expect + */ + public function testGetWhere( $stage, $key, $users, $useId, $expect ) { + $expect['conds'] = '(' . implode( ') OR (', $expect['orconds'] ) . ')'; + + if ( count( $users ) === 1 ) { + $users = reset( $users ); + } + + $m = $this->makeMigration( $stage ); + $result = $m->getWhere( $this->db, $key, $users, $useId ); + $this->assertEquals( $expect, $result ); + } + + public function provideGetWhere() { + $makeUserIdentity = function ( $id, $name, $actor ) { + $u = $this->getMock( UserIdentity::class ); + $u->method( 'getId' )->willReturn( $id ); + $u->method( 'getName' )->willReturn( $name ); + $u->method( 'getActorId' )->willReturn( $actor ); + return $u; + }; + + $genericUser = [ $makeUserIdentity( 1, 'User1', 11 ) ]; + $complicatedUsers = [ + $makeUserIdentity( 1, 'User1', 11 ), + $makeUserIdentity( 2, 'User2', 12 ), + $makeUserIdentity( 3, 'User3', 0 ), + $makeUserIdentity( 0, '192.168.12.34', 34 ), + $makeUserIdentity( 0, '192.168.12.35', 0 ), + ]; + + return [ + 'Simple table, old' => [ + MIGRATION_OLD, 'rc_user', $genericUser, true, [ + 'tables' => [], + 'orconds' => [ 'userid' => "rc_user = '1'" ], + 'joins' => [], + ], + ], + 'Simple table, write-both' => [ + MIGRATION_WRITE_BOTH, 'rc_user', $genericUser, true, [ + 'tables' => [], + 'orconds' => [ + 'actor' => "rc_actor = '11'", + 'userid' => "rc_actor = '0' AND rc_user = '1'" + ], + 'joins' => [], + ], + ], + 'Simple table, write-new' => [ + MIGRATION_WRITE_NEW, 'rc_user', $genericUser, true, [ + 'tables' => [], + 'orconds' => [ + 'actor' => "rc_actor = '11'", + 'userid' => "rc_actor = '0' AND rc_user = '1'" + ], + 'joins' => [], + ], + ], + 'Simple table, new' => [ + MIGRATION_NEW, 'rc_user', $genericUser, true, [ + 'tables' => [], + 'orconds' => [ 'actor' => "rc_actor = '11'" ], + 'joins' => [], + ], + ], + + 'ipblocks, old' => [ + MIGRATION_OLD, 'ipb_by', $genericUser, true, [ + 'tables' => [], + 'orconds' => [ 'userid' => "ipb_by = '1'" ], + 'joins' => [], + ], + ], + 'ipblocks, write-both' => [ + MIGRATION_WRITE_BOTH, 'ipb_by', $genericUser, true, [ + 'tables' => [], + 'orconds' => [ + 'actor' => "ipb_by_actor = '11'", + 'userid' => "ipb_by_actor = '0' AND ipb_by = '1'" + ], + 'joins' => [], + ], + ], + 'ipblocks, write-new' => [ + MIGRATION_WRITE_NEW, 'ipb_by', $genericUser, true, [ + 'tables' => [], + 'orconds' => [ + 'actor' => "ipb_by_actor = '11'", + 'userid' => "ipb_by_actor = '0' AND ipb_by = '1'" + ], + 'joins' => [], + ], + ], + 'ipblocks, new' => [ + MIGRATION_NEW, 'ipb_by', $genericUser, true, [ + 'tables' => [], + 'orconds' => [ 'actor' => "ipb_by_actor = '11'" ], + 'joins' => [], + ], + ], + + 'Revision, old' => [ + MIGRATION_OLD, 'rev_user', $genericUser, true, [ + 'tables' => [], + 'orconds' => [ 'userid' => "rev_user = '1'" ], + 'joins' => [], + ], + ], + 'Revision, write-both' => [ + MIGRATION_WRITE_BOTH, 'rev_user', $genericUser, true, [ + 'tables' => [ + 'temp_rev_user' => 'revision_actor_temp', + ], + 'orconds' => [ + 'actor' => + "(temp_rev_user.revactor_actor IS NOT NULL) AND temp_rev_user.revactor_actor = '11'", + 'userid' => "temp_rev_user.revactor_actor IS NULL AND rev_user = '1'" + ], + 'joins' => [ + 'temp_rev_user' => [ 'LEFT JOIN', 'temp_rev_user.revactor_rev = rev_id' ], + ], + ], + ], + 'Revision, write-new' => [ + MIGRATION_WRITE_NEW, 'rev_user', $genericUser, true, [ + 'tables' => [ + 'temp_rev_user' => 'revision_actor_temp', + ], + 'orconds' => [ + 'actor' => + "(temp_rev_user.revactor_actor IS NOT NULL) AND temp_rev_user.revactor_actor = '11'", + 'userid' => "temp_rev_user.revactor_actor IS NULL AND rev_user = '1'" + ], + 'joins' => [ + 'temp_rev_user' => [ 'LEFT JOIN', 'temp_rev_user.revactor_rev = rev_id' ], + ], + ], + ], + 'Revision, new' => [ + MIGRATION_NEW, 'rev_user', $genericUser, true, [ + 'tables' => [ + 'temp_rev_user' => 'revision_actor_temp', + ], + 'orconds' => [ 'actor' => "temp_rev_user.revactor_actor = '11'" ], + 'joins' => [ + 'temp_rev_user' => [ 'JOIN', 'temp_rev_user.revactor_rev = rev_id' ], + ], + ], + ], + + 'Multiple users, old' => [ + MIGRATION_OLD, 'rc_user', $complicatedUsers, true, [ + 'tables' => [], + 'orconds' => [ + 'userid' => "rc_user IN ('1','2','3') ", + 'username' => "rc_user_text IN ('192.168.12.34','192.168.12.35') " + ], + 'joins' => [], + ], + ], + 'Multiple users, write-both' => [ + MIGRATION_WRITE_BOTH, 'rc_user', $complicatedUsers, true, [ + 'tables' => [], + 'orconds' => [ + 'actor' => "rc_actor IN ('11','12','34') ", + 'userid' => "rc_actor = '0' AND rc_user IN ('1','2','3') ", + 'username' => "rc_actor = '0' AND rc_user_text IN ('192.168.12.34','192.168.12.35') " + ], + 'joins' => [], + ], + ], + 'Multiple users, write-new' => [ + MIGRATION_WRITE_NEW, 'rc_user', $complicatedUsers, true, [ + 'tables' => [], + 'orconds' => [ + 'actor' => "rc_actor IN ('11','12','34') ", + 'userid' => "rc_actor = '0' AND rc_user IN ('1','2','3') ", + 'username' => "rc_actor = '0' AND rc_user_text IN ('192.168.12.34','192.168.12.35') " + ], + 'joins' => [], + ], + ], + 'Multiple users, new' => [ + MIGRATION_NEW, 'rc_user', $complicatedUsers, true, [ + 'tables' => [], + 'orconds' => [ 'actor' => "rc_actor IN ('11','12','34') " ], + 'joins' => [], + ], + ], + + 'Multiple users, no use ID, old' => [ + MIGRATION_OLD, 'rc_user', $complicatedUsers, false, [ + 'tables' => [], + 'orconds' => [ + 'username' => "rc_user_text IN ('User1','User2','User3','192.168.12.34','192.168.12.35') " + ], + 'joins' => [], + ], + ], + 'Multiple users, write-both' => [ + MIGRATION_WRITE_BOTH, 'rc_user', $complicatedUsers, false, [ + 'tables' => [], + 'orconds' => [ + 'actor' => "rc_actor IN ('11','12','34') ", + 'username' => "rc_actor = '0' AND " + . "rc_user_text IN ('User1','User2','User3','192.168.12.34','192.168.12.35') " + ], + 'joins' => [], + ], + ], + 'Multiple users, write-new' => [ + MIGRATION_WRITE_NEW, 'rc_user', $complicatedUsers, false, [ + 'tables' => [], + 'orconds' => [ + 'actor' => "rc_actor IN ('11','12','34') ", + 'username' => "rc_actor = '0' AND " + . "rc_user_text IN ('User1','User2','User3','192.168.12.34','192.168.12.35') " + ], + 'joins' => [], + ], + ], + 'Multiple users, new' => [ + MIGRATION_NEW, 'rc_user', $complicatedUsers, false, [ + 'tables' => [], + 'orconds' => [ 'actor' => "rc_actor IN ('11','12','34') " ], + 'joins' => [], + ], + ], + ]; + } + + /** + * @dataProvider provideInsertRoundTrip + * @param string $table + * @param string $key + * @param string $pk + * @param array $extraFields + */ + public function testInsertRoundTrip( $table, $key, $pk, $extraFields ) { + $u = $this->getTestUser()->getUser(); + $user = $this->getMock( UserIdentity::class ); + $user->method( 'getId' )->willReturn( $u->getId() ); + $user->method( 'getName' )->willReturn( $u->getName() ); + if ( $u->getActorId( $this->db ) ) { + $user->method( 'getActorId' )->willReturn( $u->getActorId() ); + } else { + $this->db->insert( + 'actor', + [ 'actor_user' => $u->getId(), 'actor_name' => $u->getName() ], + __METHOD__ + ); + $user->method( 'getActorId' )->willReturn( $this->db->insertId() ); + } + + $stages = [ + MIGRATION_OLD => [ MIGRATION_OLD, MIGRATION_WRITE_NEW ], + MIGRATION_WRITE_BOTH => [ MIGRATION_OLD, MIGRATION_NEW ], + MIGRATION_WRITE_NEW => [ MIGRATION_WRITE_BOTH, MIGRATION_NEW ], + MIGRATION_NEW => [ MIGRATION_WRITE_BOTH, MIGRATION_NEW ], + ]; + + $nameKey = $key . '_text'; + $actorKey = $key === 'ipb_by' ? 'ipb_by_actor' : substr( $key, 0, -5 ) . '_actor'; + + foreach ( $stages as $writeStage => $readRange ) { + if ( $key === 'ipb_by' ) { + $extraFields['ipb_address'] = __CLASS__ . "#$writeStage"; + } + + $w = $this->makeMigration( $writeStage ); + $usesTemp = $key === 'rev_user'; + + if ( $usesTemp ) { + list( $fields, $callback ) = $w->getInsertValuesWithTempTable( $this->db, $key, $user ); + } else { + $fields = $w->getInsertValues( $this->db, $key, $user ); + } + + if ( $writeStage <= MIGRATION_WRITE_BOTH ) { + $this->assertSame( $user->getId(), $fields[$key], "old field, stage=$writeStage" ); + $this->assertSame( $user->getName(), $fields[$nameKey], "old field, stage=$writeStage" ); + } else { + $this->assertArrayNotHasKey( $key, $fields, "old field, stage=$writeStage" ); + $this->assertArrayNotHasKey( $nameKey, $fields, "old field, stage=$writeStage" ); + } + if ( $writeStage >= MIGRATION_WRITE_BOTH && !$usesTemp ) { + $this->assertSame( $user->getActorId(), $fields[$actorKey], "new field, stage=$writeStage" ); + } else { + $this->assertArrayNotHasKey( $actorKey, $fields, "new field, stage=$writeStage" ); + } + + $this->db->insert( $table, $extraFields + $fields, __METHOD__ ); + $id = $this->db->insertId(); + if ( $usesTemp ) { + $callback( $id, $extraFields ); + } + + for ( $readStage = $readRange[0]; $readStage <= $readRange[1]; $readStage++ ) { + $r = $this->makeMigration( $readStage ); + + $queryInfo = $r->getJoin( $key ); + $row = $this->db->selectRow( + [ $table ] + $queryInfo['tables'], + $queryInfo['fields'], + [ $pk => $id ], + __METHOD__, + [], + $queryInfo['joins'] + ); + + $this->assertSame( $user->getId(), (int)$row->$key, "w=$writeStage, r=$readStage, id" ); + $this->assertSame( $user->getName(), $row->$nameKey, "w=$writeStage, r=$readStage, name" ); + $this->assertSame( + $readStage === MIGRATION_OLD || $writeStage === MIGRATION_OLD ? 0 : $user->getActorId(), + (int)$row->$actorKey, + "w=$writeStage, r=$readStage, actor" + ); + } + } + } + + public static function provideInsertRoundTrip() { + $db = wfGetDB( DB_REPLICA ); // for timestamps + + $ipbfields = [ + ]; + $revfields = [ + ]; + + return [ + 'recentchanges' => [ 'recentchanges', 'rc_user', 'rc_id', [ + 'rc_timestamp' => $db->timestamp(), + 'rc_namespace' => 0, + 'rc_title' => 'Test', + 'rc_this_oldid' => 42, + 'rc_last_oldid' => 41, + 'rc_source' => 'test', + ] ], + 'ipblocks' => [ 'ipblocks', 'ipb_by', 'ipb_id', [ + 'ipb_range_start' => '', + 'ipb_range_end' => '', + 'ipb_timestamp' => $db->timestamp(), + 'ipb_expiry' => $db->getInfinity(), + ] ], + 'revision' => [ 'revision', 'rev_user', 'rev_id', [ + 'rev_page' => 42, + 'rev_text_id' => 42, + 'rev_len' => 0, + 'rev_timestamp' => $db->timestamp(), + ] ], + ]; + } + + public static function provideStages() { + return [ + 'MIGRATION_OLD' => [ MIGRATION_OLD ], + 'MIGRATION_WRITE_BOTH' => [ MIGRATION_WRITE_BOTH ], + 'MIGRATION_WRITE_NEW' => [ MIGRATION_WRITE_NEW ], + 'MIGRATION_NEW' => [ MIGRATION_NEW ], + ]; + } + + /** + * @dataProvider provideStages + * @param int $stage + * @expectedException InvalidArgumentException + * @expectedExceptionMessage Must use getInsertValuesWithTempTable() for rev_user + */ + public function testInsertWrong( $stage ) { + $m = $this->makeMigration( $stage ); + $m->getInsertValues( $this->db, 'rev_user', $this->getTestUser()->getUser() ); + } + + /** + * @dataProvider provideStages + * @param int $stage + * @expectedException InvalidArgumentException + * @expectedExceptionMessage Must use getInsertValues() for rc_user + */ + public function testInsertWithTempTableWrong( $stage ) { + $m = $this->makeMigration( $stage ); + $m->getInsertValuesWithTempTable( $this->db, 'rc_user', $this->getTestUser()->getUser() ); + } + + /** + * @dataProvider provideStages + * @param int $stage + */ + public function testInsertWithTempTableDeprecated( $stage ) { + $wrap = TestingAccessWrapper::newFromClass( ActorMigration::class ); + $wrap->formerTempTables += [ 'rc_user' => '1.30' ]; + + $this->hideDeprecated( 'ActorMigration::getInsertValuesWithTempTable for rc_user' ); + $m = $this->makeMigration( $stage ); + list( $fields, $callback ) + = $m->getInsertValuesWithTempTable( $this->db, 'rc_user', $this->getTestUser()->getUser() ); + $this->assertTrue( is_callable( $callback ) ); + } + + /** + * @dataProvider provideStages + * @param int $stage + * @expectedException InvalidArgumentException + * @expectedExceptionMessage $extra[rev_timestamp] is not provided + */ + public function testInsertWithTempTableCallbackMissingFields( $stage ) { + $m = $this->makeMigration( $stage ); + list( $fields, $callback ) + = $m->getInsertValuesWithTempTable( $this->db, 'rev_user', $this->getTestUser()->getUser() ); + $callback( 1, [] ); + } + + public function testInsertUserIdentity() { + $user = $this->getTestUser()->getUser(); + $userIdentity = $this->getMock( UserIdentity::class ); + $userIdentity->method( 'getId' )->willReturn( $user->getId() ); + $userIdentity->method( 'getName' )->willReturn( $user->getName() ); + $userIdentity->method( 'getActorId' )->willReturn( 0 ); + + list( $cFields, $cCallback ) = CommentStore::newKey( 'rev_comment' ) + ->insertWithTempTable( $this->db, '' ); + $m = $this->makeMigration( MIGRATION_WRITE_BOTH ); + list( $fields, $callback ) = + $m->getInsertValuesWithTempTable( $this->db, 'rev_user', $userIdentity ); + $extraFields = [ + 'rev_page' => 42, + 'rev_text_id' => 42, + 'rev_len' => 0, + 'rev_timestamp' => $this->db->timestamp(), + ] + $cFields; + $this->db->insert( 'revision', $extraFields + $fields, __METHOD__ ); + $id = $this->db->insertId(); + $callback( $id, $extraFields ); + $cCallback( $id ); + + $qi = Revision::getQueryInfo(); + $row = $this->db->selectRow( + $qi['tables'], $qi['fields'], [ 'rev_id' => $id ], __METHOD__, [], $qi['joins'] + ); + $this->assertSame( $user->getId(), (int)$row->rev_user ); + $this->assertSame( $user->getName(), $row->rev_user_text ); + $this->assertSame( $user->getActorId(), (int)$row->rev_actor ); + + $m = $this->makeMigration( MIGRATION_WRITE_BOTH ); + $fields = $m->getInsertValues( $this->db, 'dummy_user', $userIdentity ); + $this->assertSame( $user->getId(), $fields['dummy_user'] ); + $this->assertSame( $user->getName(), $fields['dummy_user_text'] ); + $this->assertSame( $user->getActorId(), $fields['dummy_actor'] ); + } + + public function testConstructor() { + $m = ActorMigration::newMigration(); + $this->assertInstanceOf( ActorMigration::class, $m ); + $this->assertSame( $m, ActorMigration::newMigration() ); + } + + /** + * @dataProvider provideIsAnon + * @param int $stage + * @param string $isAnon + * @param string $isNotAnon + */ + public function testIsAnon( $stage, $isAnon, $isNotAnon ) { + $m = $this->makeMigration( $stage ); + $this->assertSame( $isAnon, $m->isAnon( 'foo' ) ); + $this->assertSame( $isNotAnon, $m->isNotAnon( 'foo' ) ); + } + + public static function provideIsAnon() { + return [ + 'MIGRATION_OLD' => [ MIGRATION_OLD, 'foo = 0', 'foo != 0' ], + 'MIGRATION_WRITE_BOTH' => [ MIGRATION_WRITE_BOTH, 'foo = 0', 'foo != 0' ], + 'MIGRATION_WRITE_NEW' => [ MIGRATION_WRITE_NEW, 'foo = 0', 'foo != 0' ], + 'MIGRATION_NEW' => [ MIGRATION_NEW, 'foo IS NULL', 'foo IS NOT NULL' ], + ]; + } + +} diff --git a/www/wiki/tests/phpunit/includes/AutopromoteTest.php b/www/wiki/tests/phpunit/includes/AutopromoteTest.php index 785aa4e3..8c4a488e 100644 --- a/www/wiki/tests/phpunit/includes/AutopromoteTest.php +++ b/www/wiki/tests/phpunit/includes/AutopromoteTest.php @@ -1,5 +1,8 @@ <?php +/** + * @covers Autopromote + */ class AutopromoteTest extends MediaWikiTestCase { /** * T157718: Verify Autopromote does not perform edit count lookup if requirement is 0 or invalid @@ -17,7 +20,7 @@ class AutopromoteTest extends MediaWikiTestCase { ] ); /** @var PHPUnit_Framework_MockObject_MockObject|User $userMock */ - $userMock = $this->getMock( 'User', [ 'getEditCount' ] ); + $userMock = $this->getMock( User::class, [ 'getEditCount' ] ); if ( $requirement > 0 ) { $userMock->expects( $this->once() ) ->method( 'getEditCount' ) diff --git a/www/wiki/tests/phpunit/includes/BlockTest.php b/www/wiki/tests/phpunit/includes/BlockTest.php index 63d05a06..19780a68 100644 --- a/www/wiki/tests/phpunit/includes/BlockTest.php +++ b/www/wiki/tests/phpunit/includes/BlockTest.php @@ -6,69 +6,63 @@ */ class BlockTest extends MediaWikiLangTestCase { - /** @var Block */ - private $block; - private $madeAt; - - /* variable used to save up the blockID we insert in this test suite */ - private $blockId; - - function addDBData() { - $user = User::newFromName( 'UTBlockee' ); - if ( $user->getId() == 0 ) { - $user->addToDatabase(); - TestUser::setPasswordForUser( $user, 'UTBlockeePassword' ); - - $user->saveSettings(); - } + /** + * @return User + */ + private function getUserForBlocking() { + $testUser = $this->getMutableTestUser(); + $user = $testUser->getUser(); + $user->addToDatabase(); + TestUser::setPasswordForUser( $user, 'UTBlockeePassword' ); + $user->saveSettings(); + return $user; + } + /** + * @param User $user + * + * @return Block + * @throws MWException + */ + private function addBlockForUser( User $user ) { // Delete the last round's block if it's still there - $oldBlock = Block::newFromTarget( 'UTBlockee' ); + $oldBlock = Block::newFromTarget( $user->getName() ); if ( $oldBlock ) { // An old block will prevent our new one from saving. $oldBlock->delete(); } $blockOptions = [ - 'address' => 'UTBlockee', + 'address' => $user->getName(), 'user' => $user->getId(), + 'by' => $this->getTestSysop()->getUser()->getId(), 'reason' => 'Parce que', 'expiry' => time() + 100500, ]; - $this->block = new Block( $blockOptions ); - $this->madeAt = wfTimestamp( TS_MW ); + $block = new Block( $blockOptions ); - $this->block->insert(); + $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 { + if ( !$block->getId() ) { 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 ); - } + return $block; } /** * @covers Block::newFromTarget */ public function testINewFromTargetReturnsCorrectBlock() { + $user = $this->getUserForBlocking(); + $block = $this->addBlockForUser( $user ); + $this->assertTrue( - $this->block->equals( Block::newFromTarget( 'UTBlockee' ) ), + $block->equals( Block::newFromTarget( $user->getName() ) ), "newFromTarget() returns the same block as the one that was made" ); } @@ -77,18 +71,26 @@ class BlockTest extends MediaWikiLangTestCase { * @covers Block::newFromID */ public function testINewFromIDReturnsCorrectBlock() { + $user = $this->getUserForBlocking(); + $block = $this->addBlockForUser( $user ); + $this->assertTrue( - $this->block->equals( Block::newFromID( $this->blockId ) ), + $block->equals( Block::newFromID( $block->getId() ) ), "newFromID() returns the same block as the one that was made" ); } /** * per T28425 + * @covers Block::__construct */ public function testBug26425BlockTimestampDefaultsToTime() { + $user = $this->getUserForBlocking(); + $block = $this->addBlockForUser( $user ); + $madeAt = wfTimestamp( TS_MW ); + // delta to stop one-off errors when things happen to go over a second mark. - $delta = abs( $this->madeAt - $this->block->mTimestamp ); + $delta = abs( $madeAt - $block->mTimestamp ); $this->assertLessThan( 2, $delta, @@ -105,9 +107,12 @@ class BlockTest extends MediaWikiLangTestCase { * @covers Block::newFromTarget */ public function testBug29116NewFromTargetWithEmptyIp( $vagueTarget ) { - $block = Block::newFromTarget( 'UTBlockee', $vagueTarget ); + $user = $this->getUserForBlocking(); + $initialBlock = $this->addBlockForUser( $user ); + $block = Block::newFromTarget( $user->getName(), $vagueTarget ); + $this->assertTrue( - $this->block->equals( $block ), + $initialBlock->equals( $block ), "newFromTarget() returns the same block as the one that was made when " . "given empty vagueTarget param " . var_export( $vagueTarget, true ) ); @@ -157,7 +162,7 @@ class BlockTest extends MediaWikiLangTestCase { 'enableAutoblock' => true, 'hideName' => true, 'blockEmail' => true, - 'byText' => 'MetaWikiUser', + 'byText' => 'm>MetaWikiUser', ]; $block = new Block( $blockOptions ); $block->insert(); @@ -170,7 +175,7 @@ class BlockTest extends MediaWikiLangTestCase { ); $this->assertInstanceOf( - 'Block', + Block::class, $userBlock, "'$username' block block object should be existent" ); @@ -211,7 +216,7 @@ class BlockTest extends MediaWikiLangTestCase { 'enableAutoblock' => true, 'hideName' => true, 'blockEmail' => true, - 'byText' => 'MetaWikiUser', + 'byText' => 'Meta>MetaWikiUser', ]; $block = new Block( $blockOptions ); @@ -227,8 +232,9 @@ class BlockTest extends MediaWikiLangTestCase { 'Correct blockee name' ); $this->assertEquals( $userId, $block->getTarget()->getId(), 'Correct blockee id' ); - $this->assertEquals( 'MetaWikiUser', $block->getBlocker(), 'Correct blocker name' ); - $this->assertEquals( 'MetaWikiUser', $block->getByName(), 'Correct blocker name' ); + $this->assertEquals( 'Meta>MetaWikiUser', $block->getBlocker()->getName(), + 'Correct blocker name' ); + $this->assertEquals( 'Meta>MetaWikiUser', $block->getByName(), 'Correct blocker name' ); $this->assertEquals( 0, $block->getBy(), 'Correct blocker id' ); } @@ -279,6 +285,7 @@ class BlockTest extends MediaWikiLangTestCase { ], ]; + $blocker = $this->getTestUser()->getUser(); foreach ( $blockList as $insBlock ) { $target = $insBlock['target']; @@ -290,7 +297,7 @@ class BlockTest extends MediaWikiLangTestCase { $block = new Block(); $block->setTarget( $target ); - $block->setBlocker( 'testblocker@global' ); + $block->setBlocker( $blocker ); $block->mReason = $insBlock['desc']; $block->mExpiry = 'infinity'; $block->prevents( 'createaccount', $insBlock['ACDisable'] ); @@ -351,6 +358,9 @@ class BlockTest extends MediaWikiLangTestCase { * @covers Block::chooseBlock */ public function testBlocksOnXff( $xff, $exCount, $exResult ) { + $user = $this->getUserForBlocking(); + $this->addBlockForUser( $user ); + $list = array_map( 'trim', explode( ',', $xff ) ); $xffblocks = Block::getBlocksForIPList( $list, true ); $this->assertEquals( $exCount, count( $xffblocks ), 'Number of blocks for ' . $xff ); @@ -358,6 +368,9 @@ class BlockTest extends MediaWikiLangTestCase { $this->assertEquals( $exResult, $block->mReason, 'Correct block type for XFF header ' . $xff ); } + /** + * @covers Block::__construct + */ public function testDeprecatedConstructor() { $this->hideDeprecated( 'Block::__construct with multiple arguments' ); $username = 'UnthinkablySecretRandomUsername'; @@ -381,7 +394,7 @@ class BlockTest extends MediaWikiLangTestCase { $block = new Block( /* address */ $username, /* user */ 0, - /* by */ 0, + /* by */ $this->getTestSysop()->getUser()->getId(), /* reason */ $reason, /* timestamp */ 0, /* auto */ false, @@ -410,13 +423,21 @@ class BlockTest extends MediaWikiLangTestCase { ); } + /** + * @covers Block::getSystemBlockType + * @covers Block::insert + * @covers Block::doAutoblock + */ public function testSystemBlocks() { + $user = $this->getUserForBlocking(); + $this->addBlockForUser( $user ); + $blockOptions = [ - 'address' => 'UTBlockee', + 'address' => $user->getName(), 'reason' => 'test system block', 'timestamp' => wfTimestampNow(), 'expiry' => $this->db->getInfinity(), - 'byText' => 'MetaWikiUser', + 'byText' => 'MediaWiki default', 'systemBlock' => 'test', 'enableAutoblock' => true, ]; @@ -438,4 +459,5 @@ class BlockTest extends MediaWikiLangTestCase { $this->assertSame( 'Cannot autoblock from a system block', $ex->getMessage() ); } } + } diff --git a/www/wiki/tests/phpunit/includes/CollationTest.php b/www/wiki/tests/phpunit/includes/CollationTest.php deleted file mode 100644 index bf283aae..00000000 --- a/www/wiki/tests/phpunit/includes/CollationTest.php +++ /dev/null @@ -1,117 +0,0 @@ -<?php - -/** - * Class CollationTest - * @covers Collation - * @covers IcuCollation - * @covers IdentityCollation - * @covers UppercaseCollation - */ -class CollationTest extends MediaWikiLangTestCase { - protected function setUp() { - parent::setUp(); - $this->checkPHPExtension( 'intl' ); - } - - /** - * 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 string $lang Language code for collator - * @param string $base Base string - * @param string $extended 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" ); - } - - public static function prefixDataProvider() { - return [ - [ 'en', 'A', 'AA' ], - [ 'en', 'A', 'AAA' ], - [ 'en', 'Д', 'ДЂ' ], - [ 'en', 'Д', 'ДA' ], - // 'Ʒ' should expand to 'Z ' (note space). - [ 'fi', 'Z', 'Ʒ' ], - // 'Þ' should expand to 'th' - [ 'sv', 't', 'Þ' ], - // Javanese is a limited use alphabet, so should have 3 bytes - // per character, so do some tests with it. - [ 'en', 'ꦲ', 'ꦲꦤ' ], - [ 'en', 'ꦲ', 'ꦲД' ], - [ '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" ); - } - - public static function notPrefixDataProvider() { - return [ - [ 'en', 'A', 'B' ], - [ 'en', 'AC', 'ABC' ], - [ 'en', 'Z', 'Ʒ' ], - [ 'en', 'A', 'ꦲ' ], - ]; - } - - /** - * Test correct first letter is fetched. - * - * @param string $collation Collation name (aka uca-en) - * @param string $string String to get first letter of - * @param string $firstLetter Expected first letter. - * - * @dataProvider firstLetterProvider - */ - public function testGetFirstLetter( $collation, $string, $firstLetter ) { - $col = Collation::factory( $collation ); - $this->assertEquals( $firstLetter, $col->getFirstLetter( $string ) ); - } - - function firstLetterProvider() { - return [ - [ 'uppercase', 'Abc', 'A' ], - [ 'uppercase', 'abc', 'A' ], - [ 'identity', 'abc', 'a' ], - [ 'uca-en', 'abc', 'A' ], - [ 'uca-en', ' ', ' ' ], - [ 'uca-en', 'Êveryone', 'E' ], - [ 'uca-vi', 'Êveryone', 'Ê' ], - // Make sure thorn is not a first letter. - [ 'uca-sv', 'The', 'T' ], - [ 'uca-sv', 'Å', 'Å' ], - [ 'uca-hu', 'dzsdo', 'Dzs' ], - [ 'uca-hu', 'dzdso', 'Dz' ], - [ 'uca-hu', 'CSD', 'Cs' ], - [ 'uca-root', 'CSD', 'C' ], - [ 'uca-fi', 'Ǥ', 'G' ], - [ 'uca-fi', 'Ŧ', 'T' ], - [ 'uca-fi', 'Ʒ', 'Z' ], - [ 'uca-fi', 'Ŋ', 'N' ], - ]; - } -} diff --git a/www/wiki/tests/phpunit/includes/CommentStoreTest.php b/www/wiki/tests/phpunit/includes/CommentStoreTest.php index 9369f306..a5108976 100644 --- a/www/wiki/tests/phpunit/includes/CommentStoreTest.php +++ b/www/wiki/tests/phpunit/includes/CommentStoreTest.php @@ -20,11 +20,22 @@ class CommentStoreTest extends MediaWikiLangTestCase { /** * Create a store for a particular stage * @param int $stage + * @return CommentStore + */ + protected function makeStore( $stage ) { + global $wgContLang; + $store = new CommentStore( $wgContLang, $stage ); + return $store; + } + + /** + * Create a store for a particular stage and key (for testing deprecated behaviour) + * @param int $stage * @param string $key * @return CommentStore */ - protected function makeStore( $stage, $key ) { - $store = new CommentStore( $key ); + protected function makeStoreWithKey( $stage, $key ) { + $store = CommentStore::newKey( $key ); TestingAccessWrapper::newFromObject( $store )->stage = $stage; return $store; } @@ -35,12 +46,24 @@ class CommentStoreTest extends MediaWikiLangTestCase { * @param string $key * @param array $expect */ - public function testGetFields( $stage, $key, $expect ) { - $store = $this->makeStore( $stage, $key ); + public function testGetFields_withKeyConstruction( $stage, $key, $expect ) { + $store = $this->makeStoreWithKey( $stage, $key ); $result = $store->getFields(); $this->assertEquals( $expect, $result ); } + /** + * @dataProvider provideGetFields + * @param int $stage + * @param string $key + * @param array $expect + */ + public function testGetFields( $stage, $key, $expect ) { + $store = $this->makeStore( $stage ); + $result = $store->getFields( $key ); + $this->assertEquals( $expect, $result ); + } + public static function provideGetFields() { return [ 'Simple table, old' => [ @@ -110,12 +133,24 @@ class CommentStoreTest extends MediaWikiLangTestCase { * @param string $key * @param array $expect */ - public function testGetJoin( $stage, $key, $expect ) { - $store = $this->makeStore( $stage, $key ); + public function testGetJoin_withKeyConstruction( $stage, $key, $expect ) { + $store = $this->makeStoreWithKey( $stage, $key ); $result = $store->getJoin(); $this->assertEquals( $expect, $result ); } + /** + * @dataProvider provideGetJoin + * @param int $stage + * @param string $key + * @param array $expect + */ + public function testGetJoin( $stage, $key, $expect ) { + $store = $this->makeStore( $stage ); + $result = $store->getJoin( $key ); + $this->assertEquals( $expect, $result ); + } + public static function provideGetJoin() { return [ 'Simple table, old' => [ @@ -343,11 +378,106 @@ class CommentStoreTest extends MediaWikiLangTestCase { $extraFields['ipb_address'] = __CLASS__ . "#$writeStage"; } - $wstore = $this->makeStore( $writeStage, $key ); + $wstore = $this->makeStore( $writeStage ); $usesTemp = $key === 'rev_comment'; if ( $usesTemp ) { - list( $fields, $callback ) = $wstore->insertWithTempTable( $this->db, $comment, $data ); + list( $fields, $callback ) = $wstore->insertWithTempTable( + $this->db, $key, $comment, $data + ); + } else { + $fields = $wstore->insert( $this->db, $key, $comment, $data ); + } + + if ( $writeStage <= MIGRATION_WRITE_BOTH ) { + $this->assertSame( $expect['text'], $fields[$key], "old field, stage=$writeStage" ); + } else { + $this->assertArrayNotHasKey( $key, $fields, "old field, stage=$writeStage" ); + } + if ( $writeStage >= MIGRATION_WRITE_BOTH && !$usesTemp ) { + $this->assertArrayHasKey( "{$key}_id", $fields, "new field, stage=$writeStage" ); + } else { + $this->assertArrayNotHasKey( "{$key}_id", $fields, "new field, stage=$writeStage" ); + } + + $this->db->insert( $table, $extraFields + $fields, __METHOD__ ); + $id = $this->db->insertId(); + if ( $usesTemp ) { + $callback( $id ); + } + + for ( $readStage = $readRange[0]; $readStage <= $readRange[1]; $readStage++ ) { + $rstore = $this->makeStore( $readStage ); + + $fieldRow = $this->db->selectRow( + $table, + $rstore->getFields( $key ), + [ $pk => $id ], + __METHOD__ + ); + + $queryInfo = $rstore->getJoin( $key ); + $joinRow = $this->db->selectRow( + [ $table ] + $queryInfo['tables'], + $queryInfo['fields'], + [ $pk => $id ], + __METHOD__, + [], + $queryInfo['joins'] + ); + + $this->assertComment( + $writeStage === MIGRATION_OLD || $readStage === MIGRATION_OLD ? $expectOld : $expect, + $rstore->getCommentLegacy( $this->db, $key, $fieldRow ), + "w=$writeStage, r=$readStage, from getFields()" + ); + $this->assertComment( + $writeStage === MIGRATION_OLD || $readStage === MIGRATION_OLD ? $expectOld : $expect, + $rstore->getComment( $key, $joinRow ), + "w=$writeStage, r=$readStage, from getJoin()" + ); + } + } + } + + /** + * @dataProvider provideInsertRoundTrip + * @param string $table + * @param string $key + * @param string $pk + * @param string $extraFields + * @param string|Message $comment + * @param array|null $data + * @param array $expect + */ + public function testInsertRoundTrip_withKeyConstruction( + $table, $key, $pk, $extraFields, $comment, $data, $expect + ) { + $expectOld = [ + 'text' => $expect['text'], + 'message' => new RawMessage( '$1', [ $expect['text'] ] ), + 'data' => null, + ]; + + $stages = [ + MIGRATION_OLD => [ MIGRATION_OLD, MIGRATION_WRITE_NEW ], + MIGRATION_WRITE_BOTH => [ MIGRATION_OLD, MIGRATION_NEW ], + MIGRATION_WRITE_NEW => [ MIGRATION_WRITE_BOTH, MIGRATION_NEW ], + MIGRATION_NEW => [ MIGRATION_WRITE_BOTH, MIGRATION_NEW ], + ]; + + foreach ( $stages as $writeStage => $readRange ) { + if ( $key === 'ipb_reason' ) { + $extraFields['ipb_address'] = __CLASS__ . "#$writeStage"; + } + + $wstore = $this->makeStoreWithKey( $writeStage, $key ); + $usesTemp = $key === 'rev_comment'; + + if ( $usesTemp ) { + list( $fields, $callback ) = $wstore->insertWithTempTable( + $this->db, $comment, $data + ); } else { $fields = $wstore->insert( $this->db, $comment, $data ); } @@ -370,7 +500,7 @@ class CommentStoreTest extends MediaWikiLangTestCase { } for ( $readStage = $readRange[0]; $readStage <= $readRange[1]; $readStage++ ) { - $rstore = $this->makeStore( $readStage, $key ); + $rstore = $this->makeStoreWithKey( $readStage, $key ); $fieldRow = $this->db->selectRow( $table, @@ -412,7 +542,6 @@ class CommentStoreTest extends MediaWikiLangTestCase { $ipbfields = [ 'ipb_range_start' => '', 'ipb_range_end' => '', - 'ipb_by' => 0, 'ipb_timestamp' => $db->timestamp(), 'ipb_expiry' => $db->getInfinity(), ]; @@ -420,8 +549,6 @@ class CommentStoreTest extends MediaWikiLangTestCase { 'rev_page' => 42, 'rev_text_id' => 42, 'rev_len' => 0, - 'rev_user' => 0, - 'rev_user_text' => '', 'rev_timestamp' => $db->timestamp(), ]; $comStoreComment = new CommentStoreComment( @@ -518,26 +645,26 @@ class CommentStoreTest extends MediaWikiLangTestCase { } public function testGetCommentErrors() { - MediaWiki\suppressWarnings(); - $reset = new ScopedCallback( 'MediaWiki\restoreWarnings' ); + Wikimedia\suppressWarnings(); + $reset = new ScopedCallback( 'Wikimedia\restoreWarnings' ); - $store = $this->makeStore( MIGRATION_OLD, 'dummy' ); - $res = $store->getComment( [ 'dummy' => 'comment' ] ); + $store = $this->makeStore( MIGRATION_OLD ); + $res = $store->getComment( 'dummy', [ 'dummy' => 'comment' ] ); $this->assertSame( '', $res->text ); - $res = $store->getComment( [ 'dummy' => 'comment' ], true ); + $res = $store->getComment( 'dummy', [ 'dummy' => 'comment' ], true ); $this->assertSame( 'comment', $res->text ); - $store = $this->makeStore( MIGRATION_NEW, 'dummy' ); + $store = $this->makeStore( MIGRATION_NEW ); try { - $store->getComment( [ 'dummy' => 'comment' ] ); + $store->getComment( 'dummy', [ 'dummy' => 'comment' ] ); $this->fail( 'Expected exception not thrown' ); } catch ( InvalidArgumentException $ex ) { $this->assertSame( '$row does not contain fields needed for comment dummy', $ex->getMessage() ); } - $res = $store->getComment( [ 'dummy' => 'comment' ], true ); + $res = $store->getComment( 'dummy', [ 'dummy' => 'comment' ], true ); $this->assertSame( 'comment', $res->text ); try { - $store->getComment( [ 'dummy_id' => 1 ] ); + $store->getComment( 'dummy', [ 'dummy_id' => 1 ] ); $this->fail( 'Expected exception not thrown' ); } catch ( InvalidArgumentException $ex ) { $this->assertSame( @@ -547,19 +674,19 @@ class CommentStoreTest extends MediaWikiLangTestCase { ); } - $store = $this->makeStore( MIGRATION_NEW, 'rev_comment' ); + $store = $this->makeStore( MIGRATION_NEW ); try { - $store->getComment( [ 'rev_comment' => 'comment' ] ); + $store->getComment( 'rev_comment', [ 'rev_comment' => 'comment' ] ); $this->fail( 'Expected exception not thrown' ); } catch ( InvalidArgumentException $ex ) { $this->assertSame( '$row does not contain fields needed for comment rev_comment', $ex->getMessage() ); } - $res = $store->getComment( [ 'rev_comment' => 'comment' ], true ); + $res = $store->getComment( 'rev_comment', [ 'rev_comment' => 'comment' ], true ); $this->assertSame( 'comment', $res->text ); try { - $store->getComment( [ 'rev_comment_pk' => 1 ] ); + $store->getComment( 'rev_comment', [ 'rev_comment_pk' => 1 ] ); $this->fail( 'Expected exception not thrown' ); } catch ( InvalidArgumentException $ex ) { $this->assertSame( @@ -586,8 +713,8 @@ class CommentStoreTest extends MediaWikiLangTestCase { * @expectedExceptionMessage Must use insertWithTempTable() for rev_comment */ public function testInsertWrong( $stage ) { - $store = $this->makeStore( $stage, 'rev_comment' ); - $store->insert( $this->db, 'foo' ); + $store = $this->makeStore( $stage ); + $store->insert( $this->db, 'rev_comment', 'foo' ); } /** @@ -597,8 +724,8 @@ class CommentStoreTest extends MediaWikiLangTestCase { * @expectedExceptionMessage Must use insert() for ipb_reason */ public function testInsertWithTempTableWrong( $stage ) { - $store = $this->makeStore( $stage, 'ipb_reason' ); - $store->insertWithTempTable( $this->db, 'foo' ); + $store = $this->makeStore( $stage ); + $store->insertWithTempTable( $this->db, 'ipb_reason', 'foo' ); } /** @@ -610,8 +737,8 @@ class CommentStoreTest extends MediaWikiLangTestCase { $wrap->formerTempTables += [ 'ipb_reason' => '1.30' ]; $this->hideDeprecated( 'CommentStore::insertWithTempTable for ipb_reason' ); - $store = $this->makeStore( $stage, 'ipb_reason' ); - list( $fields, $callback ) = $store->insertWithTempTable( $this->db, 'foo' ); + $store = $this->makeStore( $stage ); + list( $fields, $callback ) = $store->insertWithTempTable( $this->db, 'ipb_reason', 'foo' ); $this->assertTrue( is_callable( $callback ) ); } @@ -620,8 +747,8 @@ class CommentStoreTest extends MediaWikiLangTestCase { $truncated1 = str_repeat( '💣', 63 ) . '...'; $truncated2 = str_repeat( '💣', CommentStore::COMMENT_CHARACTER_LIMIT - 3 ) . '...'; - $store = $this->makeStore( MIGRATION_WRITE_BOTH, 'ipb_reason' ); - $fields = $store->insert( $this->db, $comment ); + $store = $this->makeStore( MIGRATION_WRITE_BOTH ); + $fields = $store->insert( $this->db, 'ipb_reason', $comment ); $this->assertSame( $truncated1, $fields['ipb_reason'] ); $stored = $this->db->selectField( 'comment', 'comment_text', [ 'comment_id' => $fields['ipb_reason_id'] ], __METHOD__ @@ -634,13 +761,17 @@ class CommentStoreTest extends MediaWikiLangTestCase { * @expectedExceptionMessage Comment data is too long (65611 bytes, maximum is 65535) */ public function testInsertTooMuchData() { - $store = $this->makeStore( MIGRATION_WRITE_BOTH, 'ipb_reason' ); - $store->insert( $this->db, 'foo', [ + $store = $this->makeStore( MIGRATION_WRITE_BOTH ); + $store->insert( $this->db, 'ipb_reason', 'foo', [ 'long' => str_repeat( '💣', 16400 ) ] ); } - public function testConstructor() { + public function testGetStore() { + $this->assertInstanceOf( CommentStore::class, CommentStore::getStore() ); + } + + public function testNewKey() { $this->assertInstanceOf( CommentStore::class, CommentStore::newKey( 'dummy' ) ); } diff --git a/www/wiki/tests/phpunit/includes/EditPageTest.php b/www/wiki/tests/phpunit/includes/EditPageTest.php index ad0d07a4..8f0826b5 100644 --- a/www/wiki/tests/phpunit/includes/EditPageTest.php +++ b/www/wiki/tests/phpunit/includes/EditPageTest.php @@ -29,10 +29,18 @@ class EditPageTest extends MediaWikiLangTestCase { $wgNamespaceContentModels[12312] = "testing"; $wgContentHandlers["testing"] = 'DummyContentHandlerForTesting'; - MWNamespace::getCanonicalNamespaces( true ); # reset namespace cache + MWNamespace::clearCaches(); $wgContLang->resetNamespaces(); # reset namespace cache } + protected function tearDown() { + global $wgContLang; + + MWNamespace::clearCaches(); + $wgContLang->resetNamespaces(); # reset namespace cache + parent::tearDown(); + } + /** * @dataProvider provideExtractSectionTitle * @covers EditPage::extractSectionTitle @@ -709,7 +717,7 @@ hello $ep->importFormData( $req ); $this->setExpectedException( - 'MWException', + MWException::class, 'This content model is not supported: testing' ); diff --git a/www/wiki/tests/phpunit/includes/ExtraParserTest.php b/www/wiki/tests/phpunit/includes/ExtraParserTest.php index a4e3bb94..75ebd31a 100644 --- a/www/wiki/tests/phpunit/includes/ExtraParserTest.php +++ b/www/wiki/tests/phpunit/includes/ExtraParserTest.php @@ -26,7 +26,6 @@ class ExtraParserTest extends MediaWikiTestCase { // FIXME: This test should pass without setting global content language $this->options = ParserOptions::newFromUserAndLang( new User, $contLang ); $this->options->setTemplateCallback( [ __CLASS__, 'statelessFetchTemplate' ] ); - $this->options->setWrapOutputClass( false ); $this->parser = new Parser; MagicWord::clearCache(); @@ -41,9 +40,8 @@ class ExtraParserTest extends MediaWikiTestCase { $title = Title::newFromText( 'Unit test' ); $options = ParserOptions::newFromUser( new User() ); - $options->setWrapOutputClass( false ); $this->assertEquals( "<p>$longLine</p>", - $this->parser->parse( $longLine, $title, $options )->getText() ); + $this->parser->parse( $longLine, $title, $options )->getText( [ 'unwrap' => true ] ) ); } /** @@ -55,7 +53,7 @@ class ExtraParserTest extends MediaWikiTestCase { $parserOutput = $this->parser->parse( "Test\n{{Foo}}\n{{Bar}}", $title, $this->options ); $this->assertEquals( "<p>Test\nContent of <i>Template:Foo</i>\nContent of <i>Template:Bar</i>\n</p>", - $parserOutput->getText() + $parserOutput->getText( [ 'unwrap' => true ] ) ); } @@ -193,7 +191,6 @@ class ExtraParserTest extends MediaWikiTestCase { } /** - * @group Database * @covers Parser::parse */ public function testTrackingCategory() { @@ -207,7 +204,6 @@ class ExtraParserTest extends MediaWikiTestCase { } /** - * @group Database * @covers Parser::parse */ public function testTrackingCategorySpecial() { diff --git a/www/wiki/tests/phpunit/includes/FauxRequestTest.php b/www/wiki/tests/phpunit/includes/FauxRequestTest.php index 9fe694da..9e7d6802 100644 --- a/www/wiki/tests/phpunit/includes/FauxRequestTest.php +++ b/www/wiki/tests/phpunit/includes/FauxRequestTest.php @@ -2,7 +2,11 @@ use MediaWiki\Session\SessionManager; -class FauxRequestTest extends PHPUnit_Framework_TestCase { +class FauxRequestTest extends PHPUnit\Framework\TestCase { + + use MediaWikiCoversValidator; + use PHPUnit4And6Compat; + /** * @covers FauxRequest::__construct */ @@ -39,13 +43,19 @@ class FauxRequestTest extends PHPUnit_Framework_TestCase { $this->assertEquals( '', $req->getText( 'z' ) ); } - // Integration test for parent method. + /** + * Integration test for parent method + * @covers FauxRequest::getVal + */ public function testGetVal() { $req = new FauxRequest( [ 'crlf' => "A\r\nb" ] ); $this->assertSame( "A\r\nb", $req->getVal( 'crlf' ), 'CRLF' ); } - // Integration test for parent method. + /** + * Integration test for parent method + * @covers FauxRequest::getRawVal + */ public function testGetRawVal() { $req = new FauxRequest( [ 'x' => 'Value', diff --git a/www/wiki/tests/phpunit/includes/GitInfoTest.php b/www/wiki/tests/phpunit/includes/GitInfoTest.php index ae858f5d..1037b370 100644 --- a/www/wiki/tests/phpunit/includes/GitInfoTest.php +++ b/www/wiki/tests/phpunit/includes/GitInfoTest.php @@ -4,6 +4,30 @@ */ class GitInfoTest extends MediaWikiTestCase { + public static function setUpBeforeClass() { + mkdir( __DIR__ . '/../data/gitrepo' ); + mkdir( __DIR__ . '/../data/gitrepo/1' ); + mkdir( __DIR__ . '/../data/gitrepo/2' ); + mkdir( __DIR__ . '/../data/gitrepo/3' ); + mkdir( __DIR__ . '/../data/gitrepo/1/.git' ); + mkdir( __DIR__ . '/../data/gitrepo/1/.git/refs' ); + mkdir( __DIR__ . '/../data/gitrepo/1/.git/refs/heads' ); + file_put_contents( __DIR__ . '/../data/gitrepo/1/.git/HEAD', + "ref: refs/heads/master\n" ); + file_put_contents( __DIR__ . '/../data/gitrepo/1/.git/refs/heads/master', + "0123456789012345678901234567890123abcdef\n" ); + file_put_contents( __DIR__ . '/../data/gitrepo/1/.git/packed-refs', + "abcdef6789012345678901234567890123456789 refs/heads/master\n" ); + file_put_contents( __DIR__ . '/../data/gitrepo/2/.git', + "gitdir: ../1/.git\n" ); + file_put_contents( __DIR__ . '/../data/gitrepo/3/.git', + 'gitdir: ' . __DIR__ . "/../data/gitrepo/1/.git\n" ); + } + + public static function tearDownAfterClass() { + wfRecursiveRemoveDir( __DIR__ . '/../data/gitrepo' ); + } + protected function setUp() { parent::setUp(); $this->setMwGlobals( 'wgGitInfoCacheDirectory', __DIR__ . '/../data/gitinfo' ); @@ -43,4 +67,36 @@ class GitInfoTest extends MediaWikiTestCase { $this->assertTrue( $fixture->cacheIsComplete() ); } + public function testReadingHead() { + $dir = __DIR__ . '/../data/gitrepo/1'; + $fixture = new GitInfo( $dir ); + + $this->assertEquals( 'refs/heads/master', $fixture->getHead() ); + $this->assertEquals( '0123456789012345678901234567890123abcdef', $fixture->getHeadSHA1() ); + } + + public function testIndirection() { + $dir = __DIR__ . '/../data/gitrepo/2'; + $fixture = new GitInfo( $dir ); + + $this->assertEquals( 'refs/heads/master', $fixture->getHead() ); + $this->assertEquals( '0123456789012345678901234567890123abcdef', $fixture->getHeadSHA1() ); + } + + public function testIndirection2() { + $dir = __DIR__ . '/../data/gitrepo/3'; + $fixture = new GitInfo( $dir ); + + $this->assertEquals( 'refs/heads/master', $fixture->getHead() ); + $this->assertEquals( '0123456789012345678901234567890123abcdef', $fixture->getHeadSHA1() ); + } + + public function testReadingPackedRefs() { + $dir = __DIR__ . '/../data/gitrepo/1'; + unlink( __DIR__ . '/../data/gitrepo/1/.git/refs/heads/master' ); + $fixture = new GitInfo( $dir ); + + $this->assertEquals( 'refs/heads/master', $fixture->getHead() ); + $this->assertEquals( 'abcdef6789012345678901234567890123456789', $fixture->getHeadSHA1() ); + } } diff --git a/www/wiki/tests/phpunit/includes/GlobalFunctions/GlobalTest.php b/www/wiki/tests/phpunit/includes/GlobalFunctions/GlobalTest.php index 5e54b8d4..ee4819fa 100644 --- a/www/wiki/tests/phpunit/includes/GlobalFunctions/GlobalTest.php +++ b/www/wiki/tests/phpunit/includes/GlobalFunctions/GlobalTest.php @@ -474,25 +474,45 @@ class GlobalTest extends MediaWikiTestCase { } /** + * @covers ::wfMerge + */ + public function testMerge_worksWithLessParameters() { + $this->markTestSkippedIfNoDiff3(); + + $mergedText = null; + $successfulMerge = wfMerge( "old1\n\nold2", "old1\n\nnew2", "new1\n\nold2", $mergedText ); + + $mergedText = null; + $conflictingMerge = wfMerge( 'old', 'old and mine', 'old and yours', $mergedText ); + + $this->assertEquals( true, $successfulMerge ); + $this->assertEquals( false, $conflictingMerge ); + } + + /** * @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 bool $expectedMergeResult Whether the merge should be a success * @param string $expectedText Text after merge has been completed + * @param string $expectedMergeAttemptResult Diff3 output if conflicts occur * * @dataProvider provideMerge() * @group medium * @covers ::wfMerge */ - public function testMerge( $old, $mine, $yours, $expectedMergeResult, $expectedText ) { + public function testMerge( $old, $mine, $yours, $expectedMergeResult, $expectedText, + $expectedMergeAttemptResult ) { $this->markTestSkippedIfNoDiff3(); $mergedText = null; - $isMerged = wfMerge( $old, $mine, $yours, $mergedText ); + $attemptMergeResult = null; + $isMerged = wfMerge( $old, $mine, $yours, $mergedText, $mergeAttemptResult ); $msg = 'Merge should be a '; $msg .= $expectedMergeResult ? 'success' : 'failure'; $this->assertEquals( $expectedMergeResult, $isMerged, $msg ); + $this->assertEquals( $expectedMergeAttemptResult, $mergeAttemptResult ); if ( $isMerged ) { // Verify the merged text @@ -530,6 +550,9 @@ class GlobalTest extends MediaWikiTestCase { "one one one ONE ONE\n" . "\n" . "two two TWO TWO\n", // note: will always end in a newline + + // mergeAttemptResult: + "", ], // #1: conflict, fail @@ -552,6 +575,13 @@ class GlobalTest extends MediaWikiTestCase { // result: null, + + // mergeAttemptResult: + "1,3c\n" . + "one one one\n" . + "\n" . + "two two\n" . + ".\n", ], ]; } @@ -681,9 +711,9 @@ class GlobalTest extends MediaWikiTestCase { public function testWfMkdirParents() { // Should not return true if file exists instead of directory $fname = $this->getNewTempFile(); - MediaWiki\suppressWarnings(); + Wikimedia\suppressWarnings(); $ok = wfMkdirParents( $fname ); - MediaWiki\restoreWarnings(); + Wikimedia\restoreWarnings(); $this->assertFalse( $ok ); } @@ -722,6 +752,9 @@ class GlobalTest extends MediaWikiTestCase { ); } + /** + * @covers ::wfMemcKey + */ public function testWfMemcKey() { $cache = ObjectCache::getLocalClusterInstance(); $this->assertEquals( @@ -730,6 +763,9 @@ class GlobalTest extends MediaWikiTestCase { ); } + /** + * @covers ::wfForeignMemcKey + */ public function testWfForeignMemcKey() { $cache = ObjectCache::getLocalClusterInstance(); $keyspace = $this->readAttribute( $cache, 'keyspace' ); @@ -739,6 +775,9 @@ class GlobalTest extends MediaWikiTestCase { ); } + /** + * @covers ::wfGlobalCacheKey + */ public function testWfGlobalCacheKey() { $cache = ObjectCache::getLocalClusterInstance(); $this->assertEquals( diff --git a/www/wiki/tests/phpunit/includes/GlobalFunctions/wfArrayFilterTest.php b/www/wiki/tests/phpunit/includes/GlobalFunctions/wfArrayFilterTest.php index 388aee79..1011a37c 100644 --- a/www/wiki/tests/phpunit/includes/GlobalFunctions/wfArrayFilterTest.php +++ b/www/wiki/tests/phpunit/includes/GlobalFunctions/wfArrayFilterTest.php @@ -1,6 +1,11 @@ <?php -class WfArrayFilterTest extends \PHPUnit_Framework_TestCase { +/** + * @group GlobalFunctions + * @covers ::wfArrayFilter + * @covers ::wfArrayFilterByKey + */ +class WfArrayFilterTest extends \PHPUnit\Framework\TestCase { public function testWfArrayFilter() { $arr = [ 'a' => 1, 'b' => 2, 'c' => 3 ]; $filtered = wfArrayFilter( $arr, function ( $val, $key ) { diff --git a/www/wiki/tests/phpunit/includes/GlobalFunctions/wfBCP47Test.php b/www/wiki/tests/phpunit/includes/GlobalFunctions/wfBCP47Test.php deleted file mode 100644 index 8fbca6cf..00000000 --- a/www/wiki/tests/phpunit/includes/GlobalFunctions/wfBCP47Test.php +++ /dev/null @@ -1,121 +0,0 @@ -<?php -/** - * @group GlobalFunctions - * @covers ::wfBCP47 - */ -class WfBCP47Test extends MediaWikiTestCase { - /** - * test @see wfBCP47(). - * Please note the BCP 47 explicitly state that language codes are case - * insensitive, there are some exceptions to the rule :) - * This test is used to verify our formatting against all lower and - * all upper cases language code. - * - * @see https://tools.ietf.org/html/bcp47 - * @dataProvider provideLanguageCodes() - */ - public function testBCP47( $code, $expected ) { - $code = strtolower( $code ); - $this->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 [ - // Extracted from BCP 47 (list not exhaustive) - # 2.1.1 - [ 'en-ca-x-ca', 'en-CA-x-ca' ], - [ 'sgn-be-fr', 'sgn-BE-FR' ], - [ 'az-latn-x-latn', 'az-Latn-x-latn' ], - # 2.2 - [ 'sr-Latn-RS', 'sr-Latn-RS' ], - [ 'az-arab-ir', 'az-Arab-IR' ], - - # 2.2.5 - [ 'sl-nedis', 'sl-nedis' ], - [ 'de-ch-1996', 'de-CH-1996' ], - - # 2.2.6 - [ - 'en-latn-gb-boont-r-extended-sequence-x-private', - 'en-Latn-GB-boont-r-extended-sequence-x-private' - ], - - // Examples from BCP 47 Appendix A - # Simple language subtag: - [ 'DE', 'de' ], - [ 'fR', 'fr' ], - [ 'ja', 'ja' ], - - # Language subtag plus script subtag: - [ 'zh-hans', 'zh-Hans' ], - [ 'sr-cyrl', 'sr-Cyrl' ], - [ 'sr-latn', 'sr-Latn' ], - - # Extended language subtags and their primary language subtag - # counterparts: - [ 'zh-cmn-hans-cn', 'zh-cmn-Hans-CN' ], - [ 'cmn-hans-cn', 'cmn-Hans-CN' ], - [ 'zh-yue-hk', 'zh-yue-HK' ], - [ 'yue-hk', 'yue-HK' ], - - # Language-Script-Region: - [ 'zh-hans-cn', 'zh-Hans-CN' ], - [ 'sr-latn-RS', 'sr-Latn-RS' ], - - # Language-Variant: - [ 'sl-rozaj', 'sl-rozaj' ], - [ 'sl-rozaj-biske', 'sl-rozaj-biske' ], - [ 'sl-nedis', 'sl-nedis' ], - - # Language-Region-Variant: - [ 'de-ch-1901', 'de-CH-1901' ], - [ 'sl-it-nedis', 'sl-IT-nedis' ], - - # Language-Script-Region-Variant: - [ 'hy-latn-it-arevela', 'hy-Latn-IT-arevela' ], - - # Language-Region: - [ 'de-de', 'de-DE' ], - [ 'en-us', 'en-US' ], - [ 'es-419', 'es-419' ], - - # Private use subtags: - [ 'de-ch-x-phonebk', 'de-CH-x-phonebk' ], - [ 'az-arab-x-aze-derbend', 'az-Arab-x-aze-derbend' ], - /** - * Previous test does not reflect the BCP 47 which states: - * az-Arab-x-AZE-derbend - * AZE being private, it should be lower case, hence the test above - * should probably be: - * [ 'az-arab-x-aze-derbend', 'az-Arab-x-AZE-derbend' ], - */ - - # Private use registry values: - [ 'x-whatever', 'x-whatever' ], - [ 'qaa-qaaa-qm-x-southern', 'qaa-Qaaa-QM-x-southern' ], - [ 'de-qaaa', 'de-Qaaa' ], - [ 'sr-latn-qm', 'sr-Latn-QM' ], - [ 'sr-qaaa-rs', 'sr-Qaaa-RS' ], - - # Tags that use extensions - [ 'en-us-u-islamcal', 'en-US-u-islamcal' ], - [ 'zh-cn-a-myext-x-private', 'zh-CN-a-myext-x-private' ], - [ '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/www/wiki/tests/phpunit/includes/GlobalFunctions/wfStringToBoolTest.php b/www/wiki/tests/phpunit/includes/GlobalFunctions/wfStringToBoolTest.php new file mode 100644 index 00000000..7f56b605 --- /dev/null +++ b/www/wiki/tests/phpunit/includes/GlobalFunctions/wfStringToBoolTest.php @@ -0,0 +1,51 @@ +<?php + +/** + * @group GlobalFunctions + * @covers ::wfStringToBool + */ +class WfStringToBoolTest extends MediaWikiTestCase { + + public function getTestCases() { + return [ + [ 'true', true ], + [ 'on', true ], + [ 'yes', true ], + [ 'TRUE', true ], + [ 'YeS', true ], + [ 'On', true ], + [ '1', true ], + [ '+1', true ], + [ '01', true ], + [ '-001', true ], + [ ' 1', true ], + [ '-1 ', true ], + [ '', false ], + [ '0', false ], + [ 'false', false ], + [ 'NO', false ], + [ 'NOT', false ], + [ 'never', false ], + [ '!&', false ], + [ '-0', false ], + [ '+0', false ], + [ 'forget about it', false ], + [ ' on', false ], + [ 'true ', false ], + ]; + } + + /** + * @dataProvider getTestCases + * @param string $str + * @param bool $bool + */ + public function testStr2Bool( $str, $bool ) { + if ( $bool ) { + $this->assertTrue( wfStringToBool( $str ) ); + } else { + $this->assertFalse( wfStringToBool( $str ) ); + } + } + +} diff --git a/www/wiki/tests/phpunit/includes/HooksTest.php b/www/wiki/tests/phpunit/includes/HooksTest.php index 87acb52e..efe92ec4 100644 --- a/www/wiki/tests/phpunit/includes/HooksTest.php +++ b/www/wiki/tests/phpunit/includes/HooksTest.php @@ -54,6 +54,8 @@ class HooksTest extends MediaWikiTestCase { */ public function testOldStyleHooks( $msg, array $hook, $expectedFoo, $expectedBar ) { global $wgHooks; + + $this->hideDeprecated( 'wfRunHooks' ); $foo = $bar = 'original'; $wgHooks['MediaWikiHooksTest001'][] = $hook; @@ -67,6 +69,7 @@ class HooksTest extends MediaWikiTestCase { * @dataProvider provideHooks * @covers Hooks::register * @covers Hooks::run + * @covers Hooks::callHook */ public function testNewStyleHooks( $msg, $hook, $expectedFoo, $expectedBar ) { $foo = $bar = 'original'; @@ -83,6 +86,7 @@ class HooksTest extends MediaWikiTestCase { * @covers Hooks::register * @covers Hooks::getHandlers * @covers Hooks::run + * @covers Hooks::callHook */ public function testNewStyleHookInteraction() { global $wgHooks; @@ -122,6 +126,7 @@ class HooksTest extends MediaWikiTestCase { /** * @expectedException MWException * @covers Hooks::run + * @covers Hooks::callHook */ public function testUncallableFunction() { Hooks::register( 'MediaWikiHooksTest001', 'ThisFunctionDoesntExist' ); @@ -130,6 +135,7 @@ class HooksTest extends MediaWikiTestCase { /** * @covers Hooks::run + * @covers Hooks::callHook */ public function testFalseReturn() { Hooks::register( 'MediaWikiHooksTest001', function ( &$foo ) { @@ -147,6 +153,7 @@ class HooksTest extends MediaWikiTestCase { /** * @covers Hooks::runWithoutAbort + * @covers Hooks::callHook */ public function testRunWithoutAbort() { $list = []; @@ -169,6 +176,7 @@ class HooksTest extends MediaWikiTestCase { /** * @covers Hooks::runWithoutAbort + * @covers Hooks::callHook */ public function testRunWithoutAbortWarning() { Hooks::register( 'MediaWikiHooksTest001', function ( &$foo ) { diff --git a/www/wiki/tests/phpunit/includes/HtmlTest.php b/www/wiki/tests/phpunit/includes/HtmlTest.php index f3d49161..6695fce3 100644 --- a/www/wiki/tests/phpunit/includes/HtmlTest.php +++ b/www/wiki/tests/phpunit/includes/HtmlTest.php @@ -1,5 +1,4 @@ <?php -/** tests for includes/Html.php */ class HtmlTest extends MediaWikiTestCase { @@ -386,6 +385,9 @@ class HtmlTest extends MediaWikiTestCase { ); } + /** + * @covers Html::namespaceSelector + */ public function testCanFilterOutNamespaces() { $this->assertEquals( '<select id="namespace" name="namespace">' . "\n" . @@ -408,6 +410,9 @@ class HtmlTest extends MediaWikiTestCase { ); } + /** + * @covers Html::namespaceSelector + */ public function testCanDisableANamespaces() { $this->assertEquals( '<select id="namespace" name="namespace">' . "\n" . @@ -448,6 +453,47 @@ class HtmlTest extends MediaWikiTestCase { } /** + * @covers Html::warningBox + * @covers Html::messageBox + */ + public function testWarningBox() { + $this->assertEquals( + Html::warningBox( 'warn' ), + '<div class="warningbox">warn</div>' + ); + } + + /** + * @covers Html::errorBox + * @covers Html::messageBox + */ + public function testErrorBox() { + $this->assertEquals( + Html::errorBox( 'err' ), + '<div class="errorbox">err</div>' + ); + $this->assertEquals( + Html::errorBox( 'err', 'heading' ), + '<div class="errorbox"><h2>heading</h2>err</div>' + ); + } + + /** + * @covers Html::successBox + * @covers Html::messageBox + */ + public function testSuccessBox() { + $this->assertEquals( + Html::successBox( 'great' ), + '<div class="successbox">great</div>' + ); + $this->assertEquals( + Html::successBox( '<script>beware no escaping!</script>' ), + '<div class="successbox"><script>beware no escaping!</script></div>' + ); + } + + /** * List of input element types values introduced by HTML5 * Full list at https://www.w3.org/TR/html-markup/input.html */ @@ -637,6 +683,9 @@ class HtmlTest extends MediaWikiTestCase { return $ret; } + /** + * @covers Html::input + */ public function testWrapperInput() { $this->assertEquals( '<input type="radio" value="testval" name="testname"/>', @@ -650,6 +699,9 @@ class HtmlTest extends MediaWikiTestCase { ); } + /** + * @covers Html::check + */ public function testWrapperCheck() { $this->assertEquals( '<input type="checkbox" value="1" name="testname"/>', @@ -668,6 +720,9 @@ class HtmlTest extends MediaWikiTestCase { ); } + /** + * @covers Html::radio + */ public function testWrapperRadio() { $this->assertEquals( '<input type="radio" value="1" name="testname"/>', @@ -686,6 +741,9 @@ class HtmlTest extends MediaWikiTestCase { ); } + /** + * @covers Html::label + */ public function testWrapperLabel() { $this->assertEquals( '<label for="testid">testlabel</label>', diff --git a/www/wiki/tests/phpunit/includes/HttpTest.php b/www/wiki/tests/phpunit/includes/HttpTest.php deleted file mode 100644 index 4c2e02be..00000000 --- a/www/wiki/tests/phpunit/includes/HttpTest.php +++ /dev/null @@ -1,534 +0,0 @@ -<?php - -/** - * @group Http - */ -class HttpTest extends MediaWikiTestCase { - /** - * @dataProvider cookieDomains - * @covers Cookie::validateCookieDomain - */ - public function testValidateCookieDomain( $expected, $domain, $origin = null ) { - if ( $origin ) { - $ok = Cookie::validateCookieDomain( $domain, $origin ); - $msg = "$domain against origin $origin"; - } else { - $ok = Cookie::validateCookieDomain( $domain ); - $msg = "$domain"; - } - $this->assertEquals( $expected, $ok, $msg ); - } - - public static function cookieDomains() { - return [ - [ false, "org" ], - [ false, ".org" ], - [ true, "wikipedia.org" ], - [ true, ".wikipedia.org" ], - [ false, "co.uk" ], - [ false, ".co.uk" ], - [ false, "gov.uk" ], - [ false, ".gov.uk" ], - [ true, "supermarket.uk" ], - [ false, "uk" ], - [ false, ".uk" ], - [ false, "127.0.0." ], - [ false, "127." ], - [ false, "127.0.0.1." ], - [ true, "127.0.0.1" ], - [ false, "333.0.0.1" ], - [ true, "example.com" ], - [ false, "example.com." ], - [ true, ".example.com" ], - - [ true, ".example.com", "www.example.com" ], - [ false, "example.com", "www.example.com" ], - [ true, "127.0.0.1", "127.0.0.1" ], - [ 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 - ); - } - - /** - * @covers Http::getProxy - */ - public function testGetProxy() { - $this->setMwGlobals( 'wgHTTPProxy', 'proxy.domain.tld' ); - $this->assertEquals( - 'proxy.domain.tld', - Http::getProxy() - ); - } - - /** - * Feeds URI to test a long regular expression in Http::isValidURI - */ - public static function provideURI() { - /** Format: 'boolean expectation', 'URI to test', 'Optional message' */ - return [ - [ false, '¿non sens before!! http://a', 'Allow anything before URI' ], - - # (http|https) - only two schemes allowed - [ true, 'http://www.example.org/' ], - [ true, 'https://www.example.org/' ], - [ true, 'http://www.example.org', 'URI without directory' ], - [ true, 'http://a', 'Short name' ], - [ true, 'http://étoile', 'Allow UTF-8 in hostname' ], # 'étoile' is french for 'star' - [ false, '\\host\directory', 'CIFS share' ], - [ false, 'gopher://host/dir', 'Reject gopher scheme' ], - [ false, 'telnet://host', 'Reject telnet scheme' ], - - # :\/\/ - double slashes - [ false, 'http//example.org', 'Reject missing colon in protocol' ], - [ false, 'http:/example.org', 'Reject missing slash in protocol' ], - [ false, 'http:example.org', 'Must have two slashes' ], - # Following fail since hostname can be made of anything - [ false, 'http:///example.org', 'Must have exactly two slashes, not three' ], - - # (\w+:{0,1}\w*@)? - optional user:pass - [ true, 'http://user@host', 'Username provided' ], - [ true, 'http://user:@host', 'Username provided, no password' ], - [ true, 'http://user:pass@host', 'Username and password provided' ], - - # (\S+) - host part is made of anything not whitespaces - // commented these out in order to remove @group Broken - // @todo are these valid tests? if so, fix Http::isValidURI so it can handle them - // 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 - [ true, 'http://example.org:80/' ], - [ true, 'https://example.org:80/' ], - [ true, 'http://example.org:443/' ], - [ true, 'https://example.org:443/' ], - - # Part after the hostname is / or / with something else - [ true, 'http://example/#' ], - [ true, 'http://example/!' ], - [ true, 'http://example/:' ], - [ true, 'http://example/.' ], - [ true, 'http://example/?' ], - [ true, 'http://example/+' ], - [ true, 'http://example/=' ], - [ true, 'http://example/&' ], - [ true, 'http://example/%' ], - [ true, 'http://example/@' ], - [ true, 'http://example/-' ], - [ true, 'http://example//' ], - [ true, 'http://example/&' ], - - # Fragment - [ true, 'http://exam#ple.org', ], # This one is valid, really! - [ true, 'http://example.org:80#anchor' ], - [ true, 'http://example.org/?id#anchor' ], - [ true, 'http://example.org/?#anchor' ], - - [ 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', [], __METHOD__ ); - - # Forge a Location header - $h->setRespHeaders( 'location', [ - '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', [ - 'https://oldsite/file.ext' - ] - ); - $this->assertEquals( - 'https://oldsite/file.ext', - $h->getFinalUrl(), - "Location to the HTTPS version of the site" - ); - - $h->setRespHeaders( 'location', [ - '/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!" ) - ); - } - - /** - * Constant values are from PHP 5.3.28 using cURL 7.24.0 - * @see http://php.net/manual/en/curl.constants.php - * - * All constant values are present so that developers don’t need to remember - * to add them if added at a later date. The commented out constants were - * not found anywhere in the MediaWiki core code. - * - * Commented out constants that were not available in: - * HipHop VM 3.3.0 (rel) - * Compiler: heads/master-0-g08810d920dfff59e0774cf2d651f92f13a637175 - * Repo schema: 3214fc2c684a4520485f715ee45f33f2182324b1 - * Extension API: 20140829 - * - * Commented out constants that were removed in PHP 5.6.0 - * - * @covers CurlHttpRequest::execute - */ - public function provideCurlConstants() { - return [ - [ 'CURLAUTH_ANY' ], - [ 'CURLAUTH_ANYSAFE' ], - [ 'CURLAUTH_BASIC' ], - [ 'CURLAUTH_DIGEST' ], - [ 'CURLAUTH_GSSNEGOTIATE' ], - [ 'CURLAUTH_NTLM' ], - // array( 'CURLCLOSEPOLICY_CALLBACK' ), // removed in PHP 5.6.0 - // array( 'CURLCLOSEPOLICY_LEAST_RECENTLY_USED' ), // removed in PHP 5.6.0 - // array( 'CURLCLOSEPOLICY_LEAST_TRAFFIC' ), // removed in PHP 5.6.0 - // array( 'CURLCLOSEPOLICY_OLDEST' ), // removed in PHP 5.6.0 - // array( 'CURLCLOSEPOLICY_SLOWEST' ), // removed in PHP 5.6.0 - [ 'CURLE_ABORTED_BY_CALLBACK' ], - [ 'CURLE_BAD_CALLING_ORDER' ], - [ 'CURLE_BAD_CONTENT_ENCODING' ], - [ 'CURLE_BAD_FUNCTION_ARGUMENT' ], - [ 'CURLE_BAD_PASSWORD_ENTERED' ], - [ 'CURLE_COULDNT_CONNECT' ], - [ 'CURLE_COULDNT_RESOLVE_HOST' ], - [ 'CURLE_COULDNT_RESOLVE_PROXY' ], - [ 'CURLE_FAILED_INIT' ], - [ 'CURLE_FILESIZE_EXCEEDED' ], - [ 'CURLE_FILE_COULDNT_READ_FILE' ], - [ 'CURLE_FTP_ACCESS_DENIED' ], - [ 'CURLE_FTP_BAD_DOWNLOAD_RESUME' ], - [ 'CURLE_FTP_CANT_GET_HOST' ], - [ 'CURLE_FTP_CANT_RECONNECT' ], - [ 'CURLE_FTP_COULDNT_GET_SIZE' ], - [ 'CURLE_FTP_COULDNT_RETR_FILE' ], - [ 'CURLE_FTP_COULDNT_SET_ASCII' ], - [ 'CURLE_FTP_COULDNT_SET_BINARY' ], - [ 'CURLE_FTP_COULDNT_STOR_FILE' ], - [ 'CURLE_FTP_COULDNT_USE_REST' ], - [ 'CURLE_FTP_PORT_FAILED' ], - [ 'CURLE_FTP_QUOTE_ERROR' ], - [ 'CURLE_FTP_SSL_FAILED' ], - [ 'CURLE_FTP_USER_PASSWORD_INCORRECT' ], - [ 'CURLE_FTP_WEIRD_227_FORMAT' ], - [ 'CURLE_FTP_WEIRD_PASS_REPLY' ], - [ 'CURLE_FTP_WEIRD_PASV_REPLY' ], - [ 'CURLE_FTP_WEIRD_SERVER_REPLY' ], - [ 'CURLE_FTP_WEIRD_USER_REPLY' ], - [ 'CURLE_FTP_WRITE_ERROR' ], - [ 'CURLE_FUNCTION_NOT_FOUND' ], - [ 'CURLE_GOT_NOTHING' ], - [ 'CURLE_HTTP_NOT_FOUND' ], - [ 'CURLE_HTTP_PORT_FAILED' ], - [ 'CURLE_HTTP_POST_ERROR' ], - [ 'CURLE_HTTP_RANGE_ERROR' ], - [ 'CURLE_LDAP_CANNOT_BIND' ], - [ 'CURLE_LDAP_INVALID_URL' ], - [ 'CURLE_LDAP_SEARCH_FAILED' ], - [ 'CURLE_LIBRARY_NOT_FOUND' ], - [ 'CURLE_MALFORMAT_USER' ], - [ 'CURLE_OBSOLETE' ], - [ 'CURLE_OK' ], - [ 'CURLE_OPERATION_TIMEOUTED' ], - [ 'CURLE_OUT_OF_MEMORY' ], - [ 'CURLE_PARTIAL_FILE' ], - [ 'CURLE_READ_ERROR' ], - [ 'CURLE_RECV_ERROR' ], - [ 'CURLE_SEND_ERROR' ], - [ 'CURLE_SHARE_IN_USE' ], - // array( 'CURLE_SSH' ), // not present in HHVM 3.3.0-dev - [ 'CURLE_SSL_CACERT' ], - [ 'CURLE_SSL_CERTPROBLEM' ], - [ 'CURLE_SSL_CIPHER' ], - [ 'CURLE_SSL_CONNECT_ERROR' ], - [ 'CURLE_SSL_ENGINE_NOTFOUND' ], - [ 'CURLE_SSL_ENGINE_SETFAILED' ], - [ 'CURLE_SSL_PEER_CERTIFICATE' ], - [ 'CURLE_TELNET_OPTION_SYNTAX' ], - [ 'CURLE_TOO_MANY_REDIRECTS' ], - [ 'CURLE_UNKNOWN_TELNET_OPTION' ], - [ 'CURLE_UNSUPPORTED_PROTOCOL' ], - [ 'CURLE_URL_MALFORMAT' ], - [ 'CURLE_URL_MALFORMAT_USER' ], - [ 'CURLE_WRITE_ERROR' ], - [ 'CURLFTPAUTH_DEFAULT' ], - [ 'CURLFTPAUTH_SSL' ], - [ 'CURLFTPAUTH_TLS' ], - // array( 'CURLFTPMETHOD_MULTICWD' ), // not present in HHVM 3.3.0-dev - // array( 'CURLFTPMETHOD_NOCWD' ), // not present in HHVM 3.3.0-dev - // array( 'CURLFTPMETHOD_SINGLECWD' ), // not present in HHVM 3.3.0-dev - [ 'CURLFTPSSL_ALL' ], - [ 'CURLFTPSSL_CONTROL' ], - [ 'CURLFTPSSL_NONE' ], - [ 'CURLFTPSSL_TRY' ], - // array( 'CURLINFO_CERTINFO' ), // not present in HHVM 3.3.0-dev - [ 'CURLINFO_CONNECT_TIME' ], - [ 'CURLINFO_CONTENT_LENGTH_DOWNLOAD' ], - [ 'CURLINFO_CONTENT_LENGTH_UPLOAD' ], - [ 'CURLINFO_CONTENT_TYPE' ], - [ 'CURLINFO_EFFECTIVE_URL' ], - [ 'CURLINFO_FILETIME' ], - [ 'CURLINFO_HEADER_OUT' ], - [ 'CURLINFO_HEADER_SIZE' ], - [ 'CURLINFO_HTTP_CODE' ], - [ 'CURLINFO_NAMELOOKUP_TIME' ], - [ 'CURLINFO_PRETRANSFER_TIME' ], - [ 'CURLINFO_PRIVATE' ], - [ 'CURLINFO_REDIRECT_COUNT' ], - [ 'CURLINFO_REDIRECT_TIME' ], - // array( 'CURLINFO_REDIRECT_URL' ), // not present in HHVM 3.3.0-dev - [ 'CURLINFO_REQUEST_SIZE' ], - [ 'CURLINFO_SIZE_DOWNLOAD' ], - [ 'CURLINFO_SIZE_UPLOAD' ], - [ 'CURLINFO_SPEED_DOWNLOAD' ], - [ 'CURLINFO_SPEED_UPLOAD' ], - [ 'CURLINFO_SSL_VERIFYRESULT' ], - [ 'CURLINFO_STARTTRANSFER_TIME' ], - [ 'CURLINFO_TOTAL_TIME' ], - [ 'CURLMSG_DONE' ], - [ 'CURLM_BAD_EASY_HANDLE' ], - [ 'CURLM_BAD_HANDLE' ], - [ 'CURLM_CALL_MULTI_PERFORM' ], - [ 'CURLM_INTERNAL_ERROR' ], - [ 'CURLM_OK' ], - [ 'CURLM_OUT_OF_MEMORY' ], - [ 'CURLOPT_AUTOREFERER' ], - [ 'CURLOPT_BINARYTRANSFER' ], - [ 'CURLOPT_BUFFERSIZE' ], - [ 'CURLOPT_CAINFO' ], - [ 'CURLOPT_CAPATH' ], - // array( 'CURLOPT_CERTINFO' ), // not present in HHVM 3.3.0-dev - // array( 'CURLOPT_CLOSEPOLICY' ), // removed in PHP 5.6.0 - [ 'CURLOPT_CONNECTTIMEOUT' ], - [ 'CURLOPT_CONNECTTIMEOUT_MS' ], - [ 'CURLOPT_COOKIE' ], - [ 'CURLOPT_COOKIEFILE' ], - [ 'CURLOPT_COOKIEJAR' ], - [ 'CURLOPT_COOKIESESSION' ], - [ 'CURLOPT_CRLF' ], - [ 'CURLOPT_CUSTOMREQUEST' ], - [ 'CURLOPT_DNS_CACHE_TIMEOUT' ], - [ 'CURLOPT_DNS_USE_GLOBAL_CACHE' ], - [ 'CURLOPT_EGDSOCKET' ], - [ 'CURLOPT_ENCODING' ], - [ 'CURLOPT_FAILONERROR' ], - [ 'CURLOPT_FILE' ], - [ 'CURLOPT_FILETIME' ], - [ 'CURLOPT_FOLLOWLOCATION' ], - [ 'CURLOPT_FORBID_REUSE' ], - [ 'CURLOPT_FRESH_CONNECT' ], - [ 'CURLOPT_FTPAPPEND' ], - [ 'CURLOPT_FTPLISTONLY' ], - [ 'CURLOPT_FTPPORT' ], - [ 'CURLOPT_FTPSSLAUTH' ], - [ 'CURLOPT_FTP_CREATE_MISSING_DIRS' ], - // array( 'CURLOPT_FTP_FILEMETHOD' ), // not present in HHVM 3.3.0-dev - // array( 'CURLOPT_FTP_SKIP_PASV_IP' ), // not present in HHVM 3.3.0-dev - [ 'CURLOPT_FTP_SSL' ], - [ 'CURLOPT_FTP_USE_EPRT' ], - [ 'CURLOPT_FTP_USE_EPSV' ], - [ 'CURLOPT_HEADER' ], - [ 'CURLOPT_HEADERFUNCTION' ], - [ 'CURLOPT_HTTP200ALIASES' ], - [ 'CURLOPT_HTTPAUTH' ], - [ 'CURLOPT_HTTPGET' ], - [ 'CURLOPT_HTTPHEADER' ], - [ 'CURLOPT_HTTPPROXYTUNNEL' ], - [ 'CURLOPT_HTTP_VERSION' ], - [ 'CURLOPT_INFILE' ], - [ 'CURLOPT_INFILESIZE' ], - [ 'CURLOPT_INTERFACE' ], - [ 'CURLOPT_IPRESOLVE' ], - // array( 'CURLOPT_KEYPASSWD' ), // not present in HHVM 3.3.0-dev - [ 'CURLOPT_KRB4LEVEL' ], - [ 'CURLOPT_LOW_SPEED_LIMIT' ], - [ 'CURLOPT_LOW_SPEED_TIME' ], - [ 'CURLOPT_MAXCONNECTS' ], - [ 'CURLOPT_MAXREDIRS' ], - // array( 'CURLOPT_MAX_RECV_SPEED_LARGE' ), // not present in HHVM 3.3.0-dev - // array( 'CURLOPT_MAX_SEND_SPEED_LARGE' ), // not present in HHVM 3.3.0-dev - [ 'CURLOPT_NETRC' ], - [ 'CURLOPT_NOBODY' ], - [ 'CURLOPT_NOPROGRESS' ], - [ 'CURLOPT_NOSIGNAL' ], - [ 'CURLOPT_PORT' ], - [ 'CURLOPT_POST' ], - [ 'CURLOPT_POSTFIELDS' ], - [ 'CURLOPT_POSTQUOTE' ], - [ 'CURLOPT_POSTREDIR' ], - [ 'CURLOPT_PRIVATE' ], - [ 'CURLOPT_PROGRESSFUNCTION' ], - // array( 'CURLOPT_PROTOCOLS' ), // not present in HHVM 3.3.0-dev - [ 'CURLOPT_PROXY' ], - [ 'CURLOPT_PROXYAUTH' ], - [ 'CURLOPT_PROXYPORT' ], - [ 'CURLOPT_PROXYTYPE' ], - [ 'CURLOPT_PROXYUSERPWD' ], - [ 'CURLOPT_PUT' ], - [ 'CURLOPT_QUOTE' ], - [ 'CURLOPT_RANDOM_FILE' ], - [ 'CURLOPT_RANGE' ], - [ 'CURLOPT_READDATA' ], - [ 'CURLOPT_READFUNCTION' ], - // array( 'CURLOPT_REDIR_PROTOCOLS' ), // not present in HHVM 3.3.0-dev - [ 'CURLOPT_REFERER' ], - [ 'CURLOPT_RESUME_FROM' ], - [ 'CURLOPT_RETURNTRANSFER' ], - // array( 'CURLOPT_SSH_AUTH_TYPES' ), // not present in HHVM 3.3.0-dev - // array( 'CURLOPT_SSH_HOST_PUBLIC_KEY_MD5' ), // not present in HHVM 3.3.0-dev - // array( 'CURLOPT_SSH_PRIVATE_KEYFILE' ), // not present in HHVM 3.3.0-dev - // array( 'CURLOPT_SSH_PUBLIC_KEYFILE' ), // not present in HHVM 3.3.0-dev - [ 'CURLOPT_SSLCERT' ], - [ 'CURLOPT_SSLCERTPASSWD' ], - [ 'CURLOPT_SSLCERTTYPE' ], - [ 'CURLOPT_SSLENGINE' ], - [ 'CURLOPT_SSLENGINE_DEFAULT' ], - [ 'CURLOPT_SSLKEY' ], - [ 'CURLOPT_SSLKEYPASSWD' ], - [ 'CURLOPT_SSLKEYTYPE' ], - [ 'CURLOPT_SSLVERSION' ], - [ 'CURLOPT_SSL_CIPHER_LIST' ], - [ 'CURLOPT_SSL_VERIFYHOST' ], - [ 'CURLOPT_SSL_VERIFYPEER' ], - [ 'CURLOPT_STDERR' ], - [ 'CURLOPT_TCP_NODELAY' ], - [ 'CURLOPT_TIMECONDITION' ], - [ 'CURLOPT_TIMEOUT' ], - [ 'CURLOPT_TIMEOUT_MS' ], - [ 'CURLOPT_TIMEVALUE' ], - [ 'CURLOPT_TRANSFERTEXT' ], - [ 'CURLOPT_UNRESTRICTED_AUTH' ], - [ 'CURLOPT_UPLOAD' ], - [ 'CURLOPT_URL' ], - [ 'CURLOPT_USERAGENT' ], - [ 'CURLOPT_USERPWD' ], - [ 'CURLOPT_VERBOSE' ], - [ 'CURLOPT_WRITEFUNCTION' ], - [ 'CURLOPT_WRITEHEADER' ], - // array( 'CURLPROTO_ALL' ), // not present in HHVM 3.3.0-dev - // array( 'CURLPROTO_DICT' ), // not present in HHVM 3.3.0-dev - // array( 'CURLPROTO_FILE' ), // not present in HHVM 3.3.0-dev - // array( 'CURLPROTO_FTP' ), // not present in HHVM 3.3.0-dev - // array( 'CURLPROTO_FTPS' ), // not present in HHVM 3.3.0-dev - // array( 'CURLPROTO_HTTP' ), // not present in HHVM 3.3.0-dev - // array( 'CURLPROTO_HTTPS' ), // not present in HHVM 3.3.0-dev - // array( 'CURLPROTO_LDAP' ), // not present in HHVM 3.3.0-dev - // array( 'CURLPROTO_LDAPS' ), // not present in HHVM 3.3.0-dev - // array( 'CURLPROTO_SCP' ), // not present in HHVM 3.3.0-dev - // array( 'CURLPROTO_SFTP' ), // not present in HHVM 3.3.0-dev - // array( 'CURLPROTO_TELNET' ), // not present in HHVM 3.3.0-dev - // array( 'CURLPROTO_TFTP' ), // not present in HHVM 3.3.0-dev - [ 'CURLPROXY_HTTP' ], - // array( 'CURLPROXY_SOCKS4' ), // not present in HHVM 3.3.0-dev - [ 'CURLPROXY_SOCKS5' ], - // array( 'CURLSSH_AUTH_DEFAULT' ), // not present in HHVM 3.3.0-dev - // array( 'CURLSSH_AUTH_HOST' ), // not present in HHVM 3.3.0-dev - // array( 'CURLSSH_AUTH_KEYBOARD' ), // not present in HHVM 3.3.0-dev - // array( 'CURLSSH_AUTH_NONE' ), // not present in HHVM 3.3.0-dev - // array( 'CURLSSH_AUTH_PASSWORD' ), // not present in HHVM 3.3.0-dev - // array( 'CURLSSH_AUTH_PUBLICKEY' ), // not present in HHVM 3.3.0-dev - [ 'CURLVERSION_NOW' ], - [ 'CURL_HTTP_VERSION_1_0' ], - [ 'CURL_HTTP_VERSION_1_1' ], - [ 'CURL_HTTP_VERSION_NONE' ], - [ 'CURL_IPRESOLVE_V4' ], - [ 'CURL_IPRESOLVE_V6' ], - [ 'CURL_IPRESOLVE_WHATEVER' ], - [ 'CURL_NETRC_IGNORED' ], - [ 'CURL_NETRC_OPTIONAL' ], - [ 'CURL_NETRC_REQUIRED' ], - [ 'CURL_TIMECOND_IFMODSINCE' ], - [ 'CURL_TIMECOND_IFUNMODSINCE' ], - [ 'CURL_TIMECOND_LASTMOD' ], - [ 'CURL_VERSION_IPV6' ], - [ 'CURL_VERSION_KERBEROS4' ], - [ 'CURL_VERSION_LIBZ' ], - [ 'CURL_VERSION_SSL' ], - ]; - } - - /** - * Added this test based on an issue experienced with HHVM 3.3.0-dev - * where it did not define a cURL constant. - * - * @bug 70570 - * @dataProvider provideCurlConstants - */ - public function testCurlConstants( $value ) { - $this->assertTrue( defined( $value ), $value . ' not defined' ); - } -} - -/** - * 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, $caller = __METHOD__ ) { - 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, $caller ); - 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, $caller ); - 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/www/wiki/tests/phpunit/includes/LicensesTest.php b/www/wiki/tests/phpunit/includes/LicensesTest.php index c7b3ce89..0e96bf44 100644 --- a/www/wiki/tests/phpunit/includes/LicensesTest.php +++ b/www/wiki/tests/phpunit/includes/LicensesTest.php @@ -20,6 +20,6 @@ class LicensesTest extends MediaWikiTestCase { 'name' => 'AnotherName', 'licenses' => $str, ] ); - $this->assertThat( $lc, $this->isInstanceOf( 'Licenses' ) ); + $this->assertThat( $lc, $this->isInstanceOf( Licenses::class ) ); } } diff --git a/www/wiki/tests/phpunit/includes/LinkFilterTest.php b/www/wiki/tests/phpunit/includes/LinkFilterTest.php index ed4958f2..51b54d2c 100644 --- a/www/wiki/tests/phpunit/includes/LinkFilterTest.php +++ b/www/wiki/tests/phpunit/includes/LinkFilterTest.php @@ -3,6 +3,7 @@ use Wikimedia\Rdbms\LikeMatch; /** + * @covers LinkFilter * @group Database */ class LinkFilterTest extends MediaWikiLangTestCase { diff --git a/www/wiki/tests/phpunit/includes/LinkerTest.php b/www/wiki/tests/phpunit/includes/LinkerTest.php index f4844f89..f9e2cc17 100644 --- a/www/wiki/tests/phpunit/includes/LinkerTest.php +++ b/www/wiki/tests/phpunit/includes/LinkerTest.php @@ -5,7 +5,6 @@ use MediaWiki\MediaWikiServices; /** * @group Database */ - class LinkerTest extends MediaWikiLangTestCase { /** @@ -133,7 +132,7 @@ class LinkerTest extends MediaWikiLangTestCase { public function provideCasesForFormatComment() { $wikiId = 'enwiki'; // $wgConf has a fake entry for this - // @codingStandardsIgnoreStart Generic.Files.LineLength + // phpcs:disable Generic.Files.LineLength return [ // Linker::formatComment [ @@ -257,7 +256,7 @@ class LinkerTest extends MediaWikiLangTestCase { false, false, $wikiId ], ]; - // @codingStandardsIgnoreEnd + // phpcs:enable } /** @@ -289,7 +288,7 @@ class LinkerTest extends MediaWikiLangTestCase { } public static function provideCasesForFormatLinksInComment() { - // @codingStandardsIgnoreStart Generic.Files.LineLength + // phpcs:disable Generic.Files.LineLength return [ [ 'foo bar <a href="/wiki/Special:BlankPage" title="Special:BlankPage">Special:BlankPage</a>', @@ -312,43 +311,43 @@ class LinkerTest extends MediaWikiLangTestCase { 'enwiki', ], ]; - // @codingStandardsIgnoreEnd + // phpcs:enable } public static function provideLinkBeginHook() { - // @codingStandardsIgnoreStart Generic.Files.LineLength + // phpcs:disable Generic.Files.LineLength return [ // Modify $html [ - function( $dummy, $title, &$html, &$attribs, &$query, &$options, &$ret ) { + function ( $dummy, $title, &$html, &$attribs, &$query, &$options, &$ret ) { $html = 'foobar'; }, '<a href="/wiki/Special:BlankPage" title="Special:BlankPage">foobar</a>' ], // Modify $attribs [ - function( $dummy, $title, &$html, &$attribs, &$query, &$options, &$ret ) { + function ( $dummy, $title, &$html, &$attribs, &$query, &$options, &$ret ) { $attribs['bar'] = 'baz'; }, '<a href="/wiki/Special:BlankPage" title="Special:BlankPage" bar="baz">Special:BlankPage</a>' ], // Modify $query [ - function( $dummy, $title, &$html, &$attribs, &$query, &$options, &$ret ) { + function ( $dummy, $title, &$html, &$attribs, &$query, &$options, &$ret ) { $query['bar'] = 'baz'; }, '<a href="/w/index.php?title=Special:BlankPage&bar=baz" title="Special:BlankPage">Special:BlankPage</a>' ], // Force HTTP $options [ - function( $dummy, $title, &$html, &$attribs, &$query, &$options, &$ret ) { + function ( $dummy, $title, &$html, &$attribs, &$query, &$options, &$ret ) { $options = [ 'http' ]; }, '<a href="http://example.org/wiki/Special:BlankPage" title="Special:BlankPage">Special:BlankPage</a>' ], // Force 'forcearticlepath' in $options [ - function( $dummy, $title, &$html, &$attribs, &$query, &$options, &$ret ) { + function ( $dummy, $title, &$html, &$attribs, &$query, &$options, &$ret ) { $options = [ 'forcearticlepath' ]; $query['foo'] = 'bar'; }, @@ -356,14 +355,14 @@ class LinkerTest extends MediaWikiLangTestCase { ], // Abort early [ - function( $dummy, $title, &$html, &$attribs, &$query, &$options, &$ret ) { + function ( $dummy, $title, &$html, &$attribs, &$query, &$options, &$ret ) { $ret = 'foobar'; return false; }, 'foobar' ], ]; - // @codingStandardsIgnoreEnd + // phpcs:enable } /** diff --git a/www/wiki/tests/phpunit/includes/ListToggleTest.php b/www/wiki/tests/phpunit/includes/ListToggleTest.php new file mode 100644 index 00000000..3574545e --- /dev/null +++ b/www/wiki/tests/phpunit/includes/ListToggleTest.php @@ -0,0 +1,49 @@ +<?php + +/** + * @covers ListToggle + */ +class ListToggleTest extends MediaWikiTestCase { + + /** + * @covers ListToggle::__construct + */ + public function testConstruct() { + $output = $this->getMockBuilder( OutputPage::class ) + ->setMethods( null ) + ->disableOriginalConstructor() + ->getMock(); + + $listToggle = new ListToggle( $output ); + + $this->assertInstanceOf( ListToggle::class, $listToggle ); + $this->assertContains( 'mediawiki.checkboxtoggle', $output->getModules() ); + $this->assertContains( 'mediawiki.checkboxtoggle.styles', $output->getModuleStyles() ); + } + + /** + * @covers ListToggle::getHTML + */ + public function testGetHTML() { + $output = $this->createMock( OutputPage::class ); + $output->expects( $this->any() ) + ->method( 'msg' ) + ->will( $this->returnCallback( function ( $key ) { + return wfMessage( $key )->inLanguage( 'qqx' ); + } ) ); + $output->expects( $this->once() ) + ->method( 'getLanguage' ) + ->will( $this->returnValue( Language::factory( 'qqx' ) ) ); + + $listToggle = new ListToggle( $output ); + + $html = $listToggle->getHTML(); + $this->assertEquals( '<div class="mw-checkbox-toggle-controls">' . + '(checkbox-select: <a class="mw-checkbox-all" role="button"' . + ' tabindex="0">(checkbox-all)</a>(comma-separator)' . + '<a class="mw-checkbox-none" role="button" tabindex="0">' . + '(checkbox-none)</a>(comma-separator)<a class="mw-checkbox-invert" ' . + 'role="button" tabindex="0">(checkbox-invert)</a>)</div>', + $html ); + } +} diff --git a/www/wiki/tests/phpunit/includes/MWNamespaceTest.php b/www/wiki/tests/phpunit/includes/MWNamespaceTest.php index 498532f7..15e2defc 100644 --- a/www/wiki/tests/phpunit/includes/MWNamespaceTest.php +++ b/www/wiki/tests/phpunit/includes/MWNamespaceTest.php @@ -5,12 +5,8 @@ * @file */ -/** - * Test class for MWNamespace. - * @todo covers tags - * @todo FIXME: this test file is a mess - */ class MWNamespaceTest extends MediaWikiTestCase { + protected function setUp() { parent::setUp(); @@ -27,15 +23,20 @@ class MWNamespaceTest extends MediaWikiTestCase { ] ); } -# ### START OF TESTS ######################################################### - /** * @todo Write more texts, handle $wgAllowImageMoving setting * @covers MWNamespace::isMovable */ public function testIsMovable() { $this->assertFalse( MWNamespace::isMovable( NS_SPECIAL ) ); - # @todo FIXME: Write more tests!! + } + + private function assertIsSubject( $ns ) { + $this->assertTrue( MWNamespace::isSubject( $ns ) ); + } + + private function assertIsNotSubject( $ns ) { + $this->assertFalse( MWNamespace::isSubject( $ns ) ); } /** @@ -58,6 +59,14 @@ class MWNamespaceTest extends MediaWikiTestCase { $this->assertIsNotSubject( 101 ); # user defined } + private function assertIsTalk( $ns ) { + $this->assertTrue( MWNamespace::isTalk( $ns ) ); + } + + private function assertIsNotTalk( $ns ) { + $this->assertFalse( MWNamespace::isTalk( $ns ) ); + } + /** * Reverse of testIsSubject(). * Please update testIsSubject() if you change assertions below @@ -155,18 +164,6 @@ class MWNamespaceTest extends MediaWikiTestCase { } /** - * @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 @@ -216,52 +213,6 @@ class MWNamespaceTest extends MediaWikiTestCase { ); } - /** - * @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 provideHasTalkNamespace() { return [ [ NS_MEDIA, false ], @@ -301,6 +252,14 @@ class MWNamespaceTest extends MediaWikiTestCase { $this->assertSame( $actual, $expected, "NS $index" ); } + private function assertIsContent( $ns ) { + $this->assertTrue( MWNamespace::isContent( $ns ) ); + } + + private function assertIsNotContent( $ns ) { + $this->assertFalse( MWNamespace::isContent( $ns ) ); + } + /** * @covers MWNamespace::isContent */ @@ -340,6 +299,14 @@ class MWNamespaceTest extends MediaWikiTestCase { $this->assertIsContent( NS_MAIN ); } + private function assertIsWatchable( $ns ) { + $this->assertTrue( MWNamespace::isWatchable( $ns ) ); + } + + private function assertIsNotWatchable( $ns ) { + $this->assertFalse( MWNamespace::isWatchable( $ns ) ); + } + /** * @covers MWNamespace::isWatchable */ @@ -357,6 +324,14 @@ class MWNamespaceTest extends MediaWikiTestCase { $this->assertIsWatchable( 101 ); } + private function assertHasSubpages( $ns ) { + $this->assertTrue( MWNamespace::hasSubpages( $ns ) ); + } + + private function assertHasNotSubpages( $ns ) { + $this->assertFalse( MWNamespace::hasSubpages( $ns ) ); + } + /** * @covers MWNamespace::hasSubpages */ @@ -465,6 +440,14 @@ class MWNamespaceTest extends MediaWikiTestCase { "Subject namespaces should not have NS_SPECIAL" ); } + private function assertIsCapitalized( $ns ) { + $this->assertTrue( MWNamespace::isCapitalized( $ns ) ); + } + + private function assertIsNotCapitalized( $ns ) { + $this->assertFalse( MWNamespace::isCapitalized( $ns ) ); + } + /** * Some namespaces are always capitalized per code definition * in MWNamespace::$alwaysCapitalizedNamespaces @@ -585,48 +568,11 @@ class MWNamespaceTest extends MediaWikiTestCase { $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 ) ); + private function assertSameSubject( $ns1, $ns2, $msg = '' ) { + $this->assertTrue( MWNamespace::subjectEquals( $ns1, $ns2 ), $msg ); } - function assertDifferentSubject( $ns1, $ns2, $msg = '' ) { - $this->assertFalse( MWNamespace::subjectEquals( $ns1, $ns2, $msg ) ); + private function assertDifferentSubject( $ns1, $ns2, $msg = '' ) { + $this->assertFalse( MWNamespace::subjectEquals( $ns1, $ns2 ), $msg ); } } diff --git a/www/wiki/tests/phpunit/includes/MWTimestampTest.php b/www/wiki/tests/phpunit/includes/MWTimestampTest.php index c1a46fed..9735eebd 100644 --- a/www/wiki/tests/phpunit/includes/MWTimestampTest.php +++ b/www/wiki/tests/phpunit/includes/MWTimestampTest.php @@ -23,7 +23,7 @@ class MWTimestampTest extends MediaWikiLangTestCase { $expectedOutput, // The expected output $desc // Description ) { - $user = $this->createMock( 'User' ); + $user = $this->createMock( User::class ); $user->expects( $this->any() ) ->method( 'getOption' ) ->with( 'timecorrection' ) @@ -156,7 +156,7 @@ class MWTimestampTest extends MediaWikiLangTestCase { $expectedOutput, // The expected output $desc // Description ) { - $user = $this->createMock( 'User' ); + $user = $this->createMock( User::class ); $user->expects( $this->any() ) ->method( 'getOption' ) ->with( 'timecorrection' ) diff --git a/www/wiki/tests/phpunit/includes/MediaWikiServicesTest.php b/www/wiki/tests/phpunit/includes/MediaWikiServicesTest.php index b301b8b7..03588aec 100644 --- a/www/wiki/tests/phpunit/includes/MediaWikiServicesTest.php +++ b/www/wiki/tests/phpunit/includes/MediaWikiServicesTest.php @@ -1,5 +1,6 @@ <?php -use Liuggio\StatsdClient\Factory\StatsdDataFactory; + +use Mediawiki\Http\HttpRequestFactory; use MediaWiki\Interwiki\InterwikiLookup; use MediaWiki\Linker\LinkRenderer; use MediaWiki\Linker\LinkRendererFactory; @@ -7,8 +8,12 @@ use MediaWiki\MediaWikiServices; use MediaWiki\Services\DestructibleService; use MediaWiki\Services\SalvageableService; use MediaWiki\Services\ServiceDisabledException; -use Wikimedia\Rdbms\LBFactory; use MediaWiki\Shell\CommandFactory; +use MediaWiki\Storage\BlobStore; +use MediaWiki\Storage\BlobStoreFactory; +use MediaWiki\Storage\RevisionLookup; +use MediaWiki\Storage\RevisionStore; +use MediaWiki\Storage\SqlBlobStore; /** * @covers MediaWiki\MediaWikiServices @@ -49,14 +54,14 @@ class MediaWikiServicesTest extends MediaWikiTestCase { public function testGetInstance() { $services = MediaWikiServices::getInstance(); - $this->assertInstanceOf( 'MediaWiki\\MediaWikiServices', $services ); + $this->assertInstanceOf( MediaWikiServices::class, $services ); } public function testForceGlobalInstance() { $newServices = $this->newMediaWikiServices(); $oldServices = MediaWikiServices::forceGlobalInstance( $newServices ); - $this->assertInstanceOf( 'MediaWiki\\MediaWikiServices', $oldServices ); + $this->assertInstanceOf( MediaWikiServices::class, $oldServices ); $this->assertNotSame( $oldServices, $newServices ); $theServices = MediaWikiServices::getInstance(); @@ -145,7 +150,7 @@ class MediaWikiServicesTest extends MediaWikiTestCase { $newServices = $this->newMediaWikiServices(); $oldServices = MediaWikiServices::forceGlobalInstance( $newServices ); - $lbFactory = $this->getMockBuilder( 'LBFactorySimple' ) + $lbFactory = $this->getMockBuilder( \Wikimedia\Rdbms\LBFactorySimple::class ) ->disableOriginalConstructor() ->getMock(); @@ -173,6 +178,9 @@ class MediaWikiServicesTest extends MediaWikiTestCase { MediaWikiServices::forceGlobalInstance( $oldServices ); $newServices->destroy(); + + // No exception was thrown, avoid being risky + $this->assertTrue( true ); } public function testResetChildProcessServices() { @@ -220,7 +228,7 @@ class MediaWikiServicesTest extends MediaWikiTestCase { 'Test', function () use ( &$serviceCounter ) { $serviceCounter++; - $service = $this->createMock( 'MediaWiki\Services\DestructibleService' ); + $service = $this->createMock( MediaWiki\Services\DestructibleService::class ); $service->expects( $this->once() )->method( 'destroy' ); return $service; } @@ -249,7 +257,7 @@ class MediaWikiServicesTest extends MediaWikiTestCase { $services->defineService( 'Test', function () { - $service = $this->createMock( 'MediaWiki\Services\DestructibleService' ); + $service = $this->createMock( MediaWiki\Services\DestructibleService::class ); $service->expects( $this->never() )->method( 'destroy' ); return $service; } @@ -311,7 +319,7 @@ class MediaWikiServicesTest extends MediaWikiTestCase { 'SearchEngineConfig' => [ 'SearchEngineConfig', SearchEngineConfig::class ], 'SkinFactory' => [ 'SkinFactory', SkinFactory::class ], 'DBLoadBalancerFactory' => [ 'DBLoadBalancerFactory', Wikimedia\Rdbms\LBFactory::class ], - 'DBLoadBalancer' => [ 'DBLoadBalancer', 'LoadBalancer' ], + 'DBLoadBalancer' => [ 'DBLoadBalancer', Wikimedia\Rdbms\LoadBalancer::class ], 'WatchedItemStore' => [ 'WatchedItemStore', WatchedItemStore::class ], 'WatchedItemQueryService' => [ 'WatchedItemQueryService', WatchedItemQueryService::class ], 'CryptRand' => [ 'CryptRand', CryptRand::class ], @@ -333,6 +341,13 @@ class MediaWikiServicesTest extends MediaWikiTestCase { 'LocalServerObjectCache' => [ 'LocalServerObjectCache', BagOStuff::class ], 'VirtualRESTServiceClient' => [ 'VirtualRESTServiceClient', VirtualRESTServiceClient::class ], 'ShellCommandFactory' => [ 'ShellCommandFactory', CommandFactory::class ], + 'BlobStoreFactory' => [ 'BlobStoreFactory', BlobStoreFactory::class ], + 'BlobStore' => [ 'BlobStore', BlobStore::class ], + '_SqlBlobStore' => [ '_SqlBlobStore', SqlBlobStore::class ], + 'RevisionStore' => [ 'RevisionStore', RevisionStore::class ], + 'RevisionLookup' => [ 'RevisionLookup', RevisionLookup::class ], + 'HttpRequestFactory' => [ 'HttpRequestFactory', HttpRequestFactory::class ], + 'CommentStore' => [ 'CommentStore', CommentStore::class ], ]; } diff --git a/www/wiki/tests/phpunit/includes/MediaWikiVersionFetcherTest.php b/www/wiki/tests/phpunit/includes/MediaWikiVersionFetcherTest.php index fa59ef29..87a7dffd 100644 --- a/www/wiki/tests/phpunit/includes/MediaWikiVersionFetcherTest.php +++ b/www/wiki/tests/phpunit/includes/MediaWikiVersionFetcherTest.php @@ -10,7 +10,9 @@ * * @author Jeroen De Dauw < jeroendedauw@gmail.com > */ -class MediaWikiVersionFetcherTest extends PHPUnit_Framework_TestCase { +class MediaWikiVersionFetcherTest extends PHPUnit\Framework\TestCase { + + use MediaWikiCoversValidator; public function testReturnsResult() { $versionFetcher = new MediaWikiVersionFetcher(); diff --git a/www/wiki/tests/phpunit/includes/MergeHistoryTest.php b/www/wiki/tests/phpunit/includes/MergeHistoryTest.php index f44ae322..54db581c 100644 --- a/www/wiki/tests/phpunit/includes/MergeHistoryTest.php +++ b/www/wiki/tests/phpunit/includes/MergeHistoryTest.php @@ -68,7 +68,7 @@ class MergeHistoryTest extends MediaWikiTestCase { public function testIsValidMergeRevisionLimit() { $limit = MergeHistory::REVISION_LIMIT; - $mh = $this->getMockBuilder( 'MergeHistory' ) + $mh = $this->getMockBuilder( MergeHistory::class ) ->setMethods( [ 'getRevisionCount' ] ) ->setConstructorArgs( [ Title::newFromText( 'Test' ), diff --git a/www/wiki/tests/phpunit/includes/MessageTest.php b/www/wiki/tests/phpunit/includes/MessageTest.php index 912bffe6..70f4af9e 100644 --- a/www/wiki/tests/phpunit/includes/MessageTest.php +++ b/www/wiki/tests/phpunit/includes/MessageTest.php @@ -1,7 +1,11 @@ <?php +use Wikimedia\ObjectFactory; use Wikimedia\TestingAccessWrapper; +/** + * @group Database + */ class MessageTest extends MediaWikiLangTestCase { protected function setUp() { @@ -24,7 +28,7 @@ class MessageTest extends MediaWikiLangTestCase { $this->assertSame( $params, $message->getParams() ); $this->assertEquals( $expectedLang, $message->getLanguage() ); - $messageSpecifier = $this->getMockForAbstractClass( 'MessageSpecifier' ); + $messageSpecifier = $this->getMockForAbstractClass( MessageSpecifier::class ); $messageSpecifier->expects( $this->any() ) ->method( 'getKey' )->will( $this->returnValue( $key ) ); $messageSpecifier->expects( $this->any() ) @@ -197,16 +201,16 @@ class MessageTest extends MediaWikiLangTestCase { * @covers ::wfMessage */ public function testWfMessage() { - $this->assertInstanceOf( 'Message', wfMessage( 'mainpage' ) ); - $this->assertInstanceOf( 'Message', wfMessage( 'i-dont-exist-evar' ) ); + $this->assertInstanceOf( Message::class, wfMessage( 'mainpage' ) ); + $this->assertInstanceOf( Message::class, wfMessage( 'i-dont-exist-evar' ) ); } /** * @covers Message::newFromKey */ public function testNewFromKey() { - $this->assertInstanceOf( 'Message', Message::newFromKey( 'mainpage' ) ); - $this->assertInstanceOf( 'Message', Message::newFromKey( 'i-dont-exist-evar' ) ); + $this->assertInstanceOf( Message::class, Message::newFromKey( 'mainpage' ) ); + $this->assertInstanceOf( Message::class, Message::newFromKey( 'i-dont-exist-evar' ) ); } /** @@ -467,7 +471,6 @@ class MessageTest extends MediaWikiLangTestCase { /** * FIXME: This should not need database, but Language#formatExpiry does (T57912) - * @group Database * @covers Message::expiryParam * @covers Message::expiryParams */ @@ -810,7 +813,7 @@ class MessageTest extends MediaWikiLangTestCase { $msg = unserialize( serialize( $msg ) ); $this->assertSame( '(<a>foo</a>)', $msg->parse() ); $title = TestingAccessWrapper::newFromObject( $msg )->title; - $this->assertInstanceOf( 'Title', $title ); + $this->assertInstanceOf( Title::class, $title ); $this->assertSame( 'Testing', $title->getFullText() ); $msg = new Message( 'mainpage' ); diff --git a/www/wiki/tests/phpunit/includes/MimeMagicTest.php b/www/wiki/tests/phpunit/includes/MimeMagicTest.php deleted file mode 100644 index e00cf0ce..00000000 --- a/www/wiki/tests/phpunit/includes/MimeMagicTest.php +++ /dev/null @@ -1,51 +0,0 @@ -<?php -class MimeMagicTest extends PHPUnit_Framework_TestCase { - - /** @var MimeMagic */ - private $mimeMagic; - - function setUp() { - $this->mimeMagic = MimeMagic::singleton(); - parent::setUp(); - } - - /** - * @dataProvider providerImproveTypeFromExtension - * @param string $ext File extension (no leading dot) - * @param string $oldMime Initially detected MIME - * @param string $expectedMime MIME type after taking extension into account - */ - function testImproveTypeFromExtension( $ext, $oldMime, $expectedMime ) { - $actualMime = $this->mimeMagic->improveTypeFromExtension( $oldMime, $ext ); - $this->assertEquals( $expectedMime, $actualMime ); - } - - function providerImproveTypeFromExtension() { - return [ - [ 'gif', 'image/gif', 'image/gif' ], - [ 'gif', 'unknown/unknown', 'unknown/unknown' ], - [ 'wrl', 'unknown/unknown', 'model/vrml' ], - [ 'txt', 'text/plain', 'text/plain' ], - [ 'csv', 'text/plain', 'text/csv' ], - [ 'tsv', 'text/plain', 'text/tab-separated-values' ], - [ 'js', 'text/javascript', 'application/javascript' ], - [ 'js', 'application/x-javascript', 'application/javascript' ], - [ 'json', 'text/plain', 'application/json' ], - [ 'foo', 'application/x-opc+zip', 'application/zip' ], - [ 'docx', 'application/x-opc+zip', - 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' ], - [ 'djvu', 'image/x-djvu', 'image/vnd.djvu' ], - [ 'wav', 'audio/wav', 'audio/wav' ], - ]; - } - - /** - * Test to make sure that encoder=ffmpeg2theora doesn't trigger - * MEDIATYPE_VIDEO (bug 63584) - */ - function testOggRecognize() { - $oggFile = __DIR__ . '/../data/media/say-test.ogg'; - $actualType = $this->mimeMagic->getMediaType( $oggFile, 'application/ogg' ); - $this->assertEquals( $actualType, MEDIATYPE_AUDIO ); - } -} diff --git a/www/wiki/tests/phpunit/includes/OutputPageTest.php b/www/wiki/tests/phpunit/includes/OutputPageTest.php index 52103f97..88c585fe 100644 --- a/www/wiki/tests/phpunit/includes/OutputPageTest.php +++ b/www/wiki/tests/phpunit/includes/OutputPageTest.php @@ -266,15 +266,15 @@ class OutputPageTest extends MediaWikiTestCase { 'UploadPath' => $uploadPath, ] ); - MediaWiki\suppressWarnings(); + Wikimedia\suppressWarnings(); $actual = OutputPage::transformResourcePath( $conf, $path ); - MediaWiki\restoreWarnings(); + Wikimedia\restoreWarnings(); $this->assertEquals( $expected ?: $path, $actual ); } public static function provideMakeResourceLoaderLink() { - // @codingStandardsIgnoreStart Generic.Files.LineLength + // phpcs:disable Generic.Files.LineLength return [ // Single only=scripts load [ @@ -297,7 +297,7 @@ class OutputPageTest extends MediaWikiTestCase { . "});</script>" ], ]; - // @codingStandardsIgnoreEnd + // phpcs:enable } /** @@ -311,7 +311,7 @@ class OutputPageTest extends MediaWikiTestCase { 'wgResourceLoaderDebug' => false, 'wgLoadScript' => 'http://127.0.0.1:8080/w/load.php', ] ); - $class = new ReflectionClass( 'OutputPage' ); + $class = new ReflectionClass( OutputPage::class ); $method = $class->getMethod( 'makeResourceLoaderLink' ); $method->setAccessible( true ); $ctx = new RequestContext(); @@ -345,6 +345,7 @@ class OutputPageTest extends MediaWikiTestCase { } public static function provideBuildExemptModules() { + // phpcs:disable Generic.Files.LineLength return [ 'empty' => [ 'exemptStyleModules' => [], @@ -354,7 +355,6 @@ class OutputPageTest extends MediaWikiTestCase { 'exemptStyleModules' => [ 'site' => [], 'noscript' => [], 'private' => [], 'user' => [] ], '<meta name="ResourceLoaderDynamicStyles" content=""/>', ], - // @codingStandardsIgnoreStart Generic.Files.LineLength 'default logged-out' => [ 'exemptStyleModules' => [ 'site' => [ 'site.styles' ] ], '<meta name="ResourceLoaderDynamicStyles" content=""/>' . "\n" . @@ -377,8 +377,8 @@ class OutputPageTest extends MediaWikiTestCase { '<link rel="stylesheet" href="/w/load.php?debug=false&lang=en&modules=example.user&only=styles&skin=fallback&version=0a56zyi"/>' . "\n" . '<link rel="stylesheet" href="/w/load.php?debug=false&lang=en&modules=user.styles&only=styles&skin=fallback&version=1e9z0ox"/>', ], - // @codingStandardsIgnoreEnd Generic.Files.LineLength ]; + // phpcs:enable } /** @@ -398,7 +398,7 @@ class OutputPageTest extends MediaWikiTestCase { $ctx = new RequestContext(); $ctx->setSkin( SkinFactory::getDefaultInstance()->makeSkin( 'fallback' ) ); $ctx->setLanguage( 'en' ); - $outputPage = $this->getMockBuilder( 'OutputPage' ) + $outputPage = $this->getMockBuilder( OutputPage::class ) ->setConstructorArgs( [ $ctx ] ) ->setMethods( [ 'isUserCssPreview', 'buildCssLinksArray' ] ) ->getMock(); @@ -434,7 +434,7 @@ class OutputPageTest extends MediaWikiTestCase { */ public function testVaryHeaders( $calls, $vary, $key ) { // get rid of default Vary fields - $outputPage = $this->getMockBuilder( 'OutputPage' ) + $outputPage = $this->getMockBuilder( OutputPage::class ) ->setConstructorArgs( [ new RequestContext() ] ) ->setMethods( [ 'getCacheVaryCookies' ] ) ->getMock(); @@ -525,7 +525,7 @@ class OutputPageTest extends MediaWikiTestCase { $this->assertTrue( $outputPage->haveCacheVaryCookies() ); } - /* + /** * @covers OutputPage::addCategoryLinks * @covers OutputPage::getCategories */ @@ -539,7 +539,7 @@ class OutputPageTest extends MediaWikiTestCase { 'page_title' => 'Test2' ] ] ); - $outputPage = $this->getMockBuilder( 'OutputPage' ) + $outputPage = $this->getMockBuilder( OutputPage::class ) ->setConstructorArgs( [ new RequestContext() ] ) ->setMethods( [ 'addCategoryLinksToLBAndGetResult' ] ) ->getMock(); @@ -643,6 +643,17 @@ class OutputPageTest extends MediaWikiTestCase { [ [ 'ResourceBasePath' => '/w', + 'Logo' => '/img/default.png', + 'LogoHD' => [ + 'svg' => '/img/vector.svg', + ], + ], + 'Link: </img/vector.svg>;rel=preload;as=image' + + ], + [ + [ + 'ResourceBasePath' => '/w', 'Logo' => '/w/test.jpg', 'LogoHD' => false, 'UploadPath' => '/w/images', @@ -685,10 +696,6 @@ class NullMessageBlobStore extends MessageBlobStore { return []; } - public function insertMessageBlob( $name, ResourceLoaderModule $module, $lang ) { - return false; - } - public function updateModule( $name, ResourceLoaderModule $module, $lang ) { } diff --git a/www/wiki/tests/phpunit/includes/PageArchiveTest.php b/www/wiki/tests/phpunit/includes/PageArchiveTest.php index 6420c395..623d4a65 100644 --- a/www/wiki/tests/phpunit/includes/PageArchiveTest.php +++ b/www/wiki/tests/phpunit/includes/PageArchiveTest.php @@ -11,8 +11,9 @@ * ^--- important, causes tests not to fail with timeout */ class PageArchiveTest extends MediaWikiTestCase { + /** - * @var WikiPage $archivedPage + * @var PageArchive $archivedPage */ private $archivedPage; @@ -49,6 +50,10 @@ class PageArchiveTest extends MediaWikiTestCase { protected function setUp() { parent::setUp(); + $this->setMwGlobals( 'wgCommentTableSchemaMigrationStage', MIGRATION_OLD ); + $this->setMwGlobals( 'wgActorTableSchemaMigrationStage', MIGRATION_OLD ); + $this->overrideMwServices(); + // First create our dummy page $page = Title::newFromText( 'PageArchiveTest_thePage' ); $page = new WikiPage( $page ); @@ -78,33 +83,183 @@ class PageArchiveTest extends MediaWikiTestCase { /** * @covers PageArchive::undelete + * @covers PageArchive::undeleteRevisions */ public function testUndeleteRevisions() { // First make sure old revisions are archived $dbr = wfGetDB( DB_REPLICA ); - $res = $dbr->select( 'archive', '*', [ 'ar_rev_id' => $this->ipRevId ] ); + $arQuery = Revision::getArchiveQueryInfo(); + $res = $dbr->select( + $arQuery['tables'], + $arQuery['fields'], + [ 'ar_rev_id' => $this->ipRevId ], + __METHOD__, + [], + $arQuery['joins'] + ); $row = $res->fetchObject(); $this->assertEquals( $this->ipEditor, $row->ar_user_text ); // Should not be in revision - $res = $dbr->select( 'revision', '*', [ 'rev_id' => $this->ipRevId ] ); + $res = $dbr->select( 'revision', '1', [ 'rev_id' => $this->ipRevId ] ); $this->assertFalse( $res->fetchObject() ); // Should not be in ip_changes - $res = $dbr->select( 'ip_changes', '*', [ 'ipc_rev_id' => $this->ipRevId ] ); + $res = $dbr->select( 'ip_changes', '1', [ 'ipc_rev_id' => $this->ipRevId ] ); $this->assertFalse( $res->fetchObject() ); // Restore the page $this->archivedPage->undelete( [] ); // Should be back in revision - $res = $dbr->select( 'revision', '*', [ 'rev_id' => $this->ipRevId ] ); + $revQuery = Revision::getQueryInfo(); + $res = $dbr->select( + $revQuery['tables'], + $revQuery['fields'], + [ 'rev_id' => $this->ipRevId ], + __METHOD__, + [], + $revQuery['joins'] + ); $row = $res->fetchObject(); $this->assertEquals( $this->ipEditor, $row->rev_user_text ); // Should be back in ip_changes - $res = $dbr->select( 'ip_changes', '*', [ 'ipc_rev_id' => $this->ipRevId ] ); + $res = $dbr->select( 'ip_changes', [ 'ipc_hex' ], [ 'ipc_rev_id' => $this->ipRevId ] ); $row = $res->fetchObject(); $this->assertEquals( IP::toHex( $this->ipEditor ), $row->ipc_hex ); } + + /** + * @covers PageArchive::listRevisions + */ + public function testListRevisions() { + $this->setMwGlobals( 'wgCommentTableSchemaMigrationStage', MIGRATION_OLD ); + $this->overrideMwServices(); + + $revisions = $this->archivedPage->listRevisions(); + $this->assertEquals( 2, $revisions->numRows() ); + + // Get the rows as arrays + $row1 = (array)$revisions->current(); + $row2 = (array)$revisions->next(); + // Unset the timestamps (we assume they will be right... + $this->assertInternalType( 'string', $row1['ar_timestamp'] ); + $this->assertInternalType( 'string', $row2['ar_timestamp'] ); + unset( $row1['ar_timestamp'] ); + unset( $row2['ar_timestamp'] ); + + $this->assertEquals( + [ + 'ar_minor_edit' => '0', + 'ar_user' => '0', + 'ar_user_text' => '2600:387:ed7:947e:8c16:a1ad:dd34:1dd7', + 'ar_actor' => null, + 'ar_len' => '11', + 'ar_deleted' => '0', + 'ar_rev_id' => '3', + 'ar_sha1' => '0qdrpxl537ivfnx4gcpnzz0285yxryy', + 'ar_page_id' => '2', + 'ar_comment_text' => 'just a test', + 'ar_comment_data' => null, + 'ar_comment_cid' => null, + 'ar_content_format' => null, + 'ar_content_model' => null, + 'ts_tags' => null, + 'ar_id' => '2', + 'ar_namespace' => '0', + 'ar_title' => 'PageArchiveTest_thePage', + 'ar_text_id' => '3', + 'ar_parent_id' => '2', + ], + $row1 + ); + $this->assertEquals( + [ + 'ar_minor_edit' => '0', + 'ar_user' => '0', + 'ar_user_text' => '127.0.0.1', + 'ar_actor' => null, + 'ar_len' => '7', + 'ar_deleted' => '0', + 'ar_rev_id' => '2', + 'ar_sha1' => 'pr0s8e18148pxhgjfa0gjrvpy8fiyxc', + 'ar_page_id' => '2', + 'ar_comment_text' => 'testing', + 'ar_comment_data' => null, + 'ar_comment_cid' => null, + 'ar_content_format' => null, + 'ar_content_model' => null, + 'ts_tags' => null, + 'ar_id' => '1', + 'ar_namespace' => '0', + 'ar_title' => 'PageArchiveTest_thePage', + 'ar_text_id' => '2', + 'ar_parent_id' => '0', + ], + $row2 + ); + } + + /** + * @covers PageArchive::listPagesBySearch + */ + public function testListPagesBySearch() { + $pages = PageArchive::listPagesBySearch( 'PageArchiveTest_thePage' ); + $this->assertSame( 1, $pages->numRows() ); + + $page = (array)$pages->current(); + + $this->assertSame( + [ + 'ar_namespace' => '0', + 'ar_title' => 'PageArchiveTest_thePage', + 'count' => '2', + ], + $page + ); + } + + /** + * @covers PageArchive::listPagesBySearch + */ + public function testListPagesByPrefix() { + $pages = PageArchive::listPagesByPrefix( 'PageArchiveTest' ); + $this->assertSame( 1, $pages->numRows() ); + + $page = (array)$pages->current(); + + $this->assertSame( + [ + 'ar_namespace' => '0', + 'ar_title' => 'PageArchiveTest_thePage', + 'count' => '2', + ], + $page + ); + } + + /** + * @covers PageArchive::getTextFromRow + */ + public function testGetTextFromRow() { + $row = (object)[ 'ar_text_id' => 2 ]; + $text = $this->archivedPage->getTextFromRow( $row ); + $this->assertSame( 'testing', $text ); + } + + /** + * @covers PageArchive::getLastRevisionText + */ + public function testGetLastRevisionText() { + $text = $this->archivedPage->getLastRevisionText(); + $this->assertSame( 'Lorem Ipsum', $text ); + } + + /** + * @covers PageArchive::isDeleted + */ + public function testIsDeleted() { + $this->assertTrue( $this->archivedPage->isDeleted() ); + } } diff --git a/www/wiki/tests/phpunit/includes/PagePropsTest.php b/www/wiki/tests/phpunit/includes/PagePropsTest.php index 29c9e228..f602cdab 100644 --- a/www/wiki/tests/phpunit/includes/PagePropsTest.php +++ b/www/wiki/tests/phpunit/includes/PagePropsTest.php @@ -1,13 +1,15 @@ <?php /** + * @covers PageProps + * * @group Database * ^--- tell jenkins this test needs the database * * @group medium * ^--- tell phpunit that these test cases may take longer than 2 seconds. */ -class TestPageProps extends MediaWikiLangTestCase { +class PagePropsTest extends MediaWikiLangTestCase { /** * @var Title $title1 @@ -35,7 +37,7 @@ class TestPageProps extends MediaWikiLangTestCase { $wgNamespaceContentModels[12312] = 'DUMMY'; $wgContentHandlers['DUMMY'] = 'DummyContentHandlerForTesting'; - MWNamespace::getCanonicalNamespaces( true ); # reset namespace cache + MWNamespace::clearCaches(); $wgContLang->resetNamespaces(); # reset namespace cache if ( !$this->the_properties ) { @@ -81,7 +83,7 @@ class TestPageProps extends MediaWikiLangTestCase { unset( $wgNamespaceContentModels[12312] ); unset( $wgContentHandlers['DUMMY'] ); - MWNamespace::getCanonicalNamespaces( true ); # reset namespace cache + MWNamespace::clearCaches(); $wgContLang->resetNamespaces(); # reset namespace cache } diff --git a/www/wiki/tests/phpunit/includes/PathRouterTest.php b/www/wiki/tests/phpunit/includes/PathRouterTest.php index 67364cb3..fc6a70b1 100644 --- a/www/wiki/tests/phpunit/includes/PathRouterTest.php +++ b/www/wiki/tests/phpunit/includes/PathRouterTest.php @@ -236,7 +236,7 @@ class PathRouterTest extends MediaWikiTestCase { * Ensure the router doesn't choke on long paths. */ public function testLength() { - // @codingStandardsIgnoreStart Ignore long line warnings + // phpcs:disable Generic.Files.LineLength $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." ); @@ -244,7 +244,7 @@ class PathRouterTest extends MediaWikiTestCase { $matches, [ '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." ] ); - // @codingStandardsIgnoreEnd + // phpcs:enable } /** diff --git a/www/wiki/tests/phpunit/includes/PreferencesTest.php b/www/wiki/tests/phpunit/includes/PreferencesTest.php index d78c1e7d..4d3b195c 100644 --- a/www/wiki/tests/phpunit/includes/PreferencesTest.php +++ b/www/wiki/tests/phpunit/includes/PreferencesTest.php @@ -44,6 +44,7 @@ class PreferencesTest extends MediaWikiTestCase { /** * Placeholder to verify T36302 * @covers Preferences::profilePreferences + * @deprecated replaced by DefaultPreferencesFactoryTest::testEmailAuthentication() */ public function testEmailAuthenticationFieldWhenUserHasNoEmail() { $prefs = $this->prefsFor( 'noemail' ); @@ -56,6 +57,7 @@ class PreferencesTest extends MediaWikiTestCase { /** * Placeholder to verify T36302 * @covers Preferences::profilePreferences + * @deprecated replaced by DefaultPreferencesFactoryTest::testEmailAuthentication() */ public function testEmailAuthenticationFieldWhenUserEmailNotAuthenticated() { $prefs = $this->prefsFor( 'notauth' ); @@ -68,6 +70,7 @@ class PreferencesTest extends MediaWikiTestCase { /** * Placeholder to verify T36302 * @covers Preferences::profilePreferences + * @deprecated replaced by DefaultPreferencesFactoryTest::testEmailAuthentication() */ public function testEmailAuthenticationFieldWhenUserEmailIsAuthenticated() { $prefs = $this->prefsFor( 'auth' ); @@ -77,86 +80,11 @@ class PreferencesTest extends MediaWikiTestCase { $this->assertEquals( 'mw-email-authenticated', $prefs['emailauthentication']['cssclass'] ); } - /** - * Test that PreferencesFormPreSave hook has correct data: - * - user Object is passed - * - oldUserOptions contains previous user options (before save) - * - formData and User object have set up new properties - * - * @see https://phabricator.wikimedia.org/T169365 - * @covers Preferences::tryFormSubmit - */ - public function testPreferencesFormPreSaveHookHasCorrectData() { - $oldOptions = [ - 'test' => 'abc', - 'option' => 'old' - ]; - $newOptions = [ - 'test' => 'abc', - 'option' => 'new' - ]; - $configMock = new HashConfig( [ - 'HiddenPrefs' => [] - ] ); - $form = $this->getMockBuilder( PreferencesForm::class ) - ->disableOriginalConstructor() - ->getMock(); - - $userMock = $this->getMockBuilder( User::class ) - ->disableOriginalConstructor() - ->getMock(); - $userMock->method( 'getOptions' ) - ->willReturn( $oldOptions ); - $userMock->method( 'isAllowedAny' ) - ->willReturn( true ); - $userMock->method( 'isAllowed' ) - ->willReturn( true ); - - $userMock->expects( $this->exactly( 2 ) ) - ->method( 'setOption' ) - ->withConsecutive( - [ $this->equalTo( 'test' ), $this->equalTo( $newOptions[ 'test' ] ) ], - [ $this->equalTo( 'option' ), $this->equalTo( $newOptions[ 'option' ] ) ] - ); - - $form->expects( $this->any() ) - ->method( 'getModifiedUser' ) - ->willReturn( $userMock ); - - $form->expects( $this->any() ) - ->method( 'getContext' ) - ->willReturn( $this->context ); - - $form->expects( $this->any() ) - ->method( 'getConfig' ) - ->willReturn( $configMock ); - - $this->setTemporaryHook( 'PreferencesFormPreSave', function ( - $formData, $form, $user, &$result, $oldUserOptions ) - use ( $newOptions, $oldOptions, $userMock ) { - $this->assertSame( $userMock, $user ); - foreach ( $newOptions as $option => $value ) { - $this->assertSame( $value, $formData[ $option ] ); - } - foreach ( $oldOptions as $option => $value ) { - $this->assertSame( $value, $oldUserOptions[ $option ] ); - } - $this->assertEquals( true, $result ); - } - ); - - Preferences::tryFormSubmit( $newOptions, $form ); - } - /** Helper */ protected function prefsFor( $user_key ) { - $preferences = []; - Preferences::profilePreferences( + return Preferences::getPreferences( $this->prefUsers[$user_key], - $this->context, - $preferences + $this->context ); - - return $preferences; } } diff --git a/www/wiki/tests/phpunit/includes/PrefixSearchTest.php b/www/wiki/tests/phpunit/includes/PrefixSearchTest.php index a6cf14a3..ed34a8ab 100644 --- a/www/wiki/tests/phpunit/includes/PrefixSearchTest.php +++ b/www/wiki/tests/phpunit/includes/PrefixSearchTest.php @@ -58,20 +58,23 @@ class PrefixSearchTest extends MediaWikiLangTestCase { 'wgCapitalLinkOverrides' => [ self::NS_NONCAP => false ], ] ); - $this->originalHandlers = TestingAccessWrapper::newFromClass( 'Hooks' )->handlers; - TestingAccessWrapper::newFromClass( 'Hooks' )->handlers = []; + $this->originalHandlers = TestingAccessWrapper::newFromClass( Hooks::class )->handlers; + TestingAccessWrapper::newFromClass( Hooks::class )->handlers = []; // Clear caches so that our new namespace appears - MWNamespace::getCanonicalNamespaces( true ); + MWNamespace::clearCaches(); Language::factory( 'en' )->resetNamespaces(); SpecialPageFactory::resetList(); } public function tearDown() { + MWNamespace::clearCaches(); + Language::factory( 'en' )->resetNamespaces(); + parent::tearDown(); - TestingAccessWrapper::newFromClass( 'Hooks' )->handlers = $this->originalHandlers; + TestingAccessWrapper::newFromClass( Hooks::class )->handlers = $this->originalHandlers; SpecialPageFactory::resetList(); } diff --git a/www/wiki/tests/phpunit/includes/RevisionContentHandlerDbTest.php b/www/wiki/tests/phpunit/includes/RevisionContentHandlerDbTest.php new file mode 100644 index 00000000..fa0153d3 --- /dev/null +++ b/www/wiki/tests/phpunit/includes/RevisionContentHandlerDbTest.php @@ -0,0 +1,14 @@ +<?php + +/** + * @group Database + * @group medium + * @group ContentHandler + */ +class RevisionContentHandlerDbTest extends RevisionDbTestBase { + + protected function getContentHandlerUseDB() { + return true; + } + +} diff --git a/www/wiki/tests/phpunit/includes/RevisionDbTestBase.php b/www/wiki/tests/phpunit/includes/RevisionDbTestBase.php new file mode 100644 index 00000000..5de34d1b --- /dev/null +++ b/www/wiki/tests/phpunit/includes/RevisionDbTestBase.php @@ -0,0 +1,1505 @@ +<?php +use MediaWiki\MediaWikiServices; +use MediaWiki\Storage\RevisionStore; +use MediaWiki\Storage\IncompleteRevisionException; +use MediaWiki\Storage\RevisionRecord; + +/** + * RevisionDbTestBase contains test cases for the Revision class that have Database interactions. + * + * @group Database + * @group medium + */ +abstract class RevisionDbTestBase extends MediaWikiTestCase { + + /** + * @var WikiPage $testPage + */ + private $testPage; + + public function __construct( $name = null, array $data = [], $dataName = '' ) { + parent::__construct( $name, $data, $dataName ); + + $this->tablesUsed = array_merge( $this->tablesUsed, + [ + 'page', + 'revision', + 'ip_changes', + 'text', + 'archive', + + 'recentchanges', + 'logging', + + 'page_props', + 'pagelinks', + 'categorylinks', + 'langlinks', + 'externallinks', + 'imagelinks', + 'templatelinks', + 'iwlinks' + ] + ); + } + + protected function setUp() { + global $wgContLang; + + parent::setUp(); + + $this->mergeMwGlobalArrayValue( + 'wgExtraNamespaces', + [ + 12312 => 'Dummy', + 12313 => 'Dummy_talk', + ] + ); + + $this->mergeMwGlobalArrayValue( + 'wgNamespaceContentModels', + [ + 12312 => DummyContentForTesting::MODEL_ID, + ] + ); + + $this->mergeMwGlobalArrayValue( + 'wgContentHandlers', + [ + DummyContentForTesting::MODEL_ID => 'DummyContentHandlerForTesting', + RevisionTestModifyableContent::MODEL_ID => 'RevisionTestModifyableContentHandler', + ] + ); + + $this->setMwGlobals( 'wgContentHandlerUseDB', $this->getContentHandlerUseDB() ); + + MWNamespace::clearCaches(); + // Reset namespace cache + $wgContLang->resetNamespaces(); + + if ( !$this->testPage ) { + /** + * We have to create a new page for each subclass as the page creation may result + * in different DB fields being filled based on configuration. + */ + $this->testPage = $this->createPage( __CLASS__, __CLASS__ ); + } + } + + protected function tearDown() { + global $wgContLang; + + parent::tearDown(); + + MWNamespace::clearCaches(); + // Reset namespace cache + $wgContLang->resetNamespaces(); + } + + abstract protected function getContentHandlerUseDB(); + + private function makeRevisionWithProps( $props = null ) { + if ( $props === null ) { + $props = []; + } + + if ( !isset( $props['content'] ) && !isset( $props['text'] ) ) { + $props['text'] = 'Lorem Ipsum'; + } + + if ( !isset( $props['user_text'] ) ) { + $user = $this->getTestUser()->getUser(); + $props['user_text'] = $user->getName(); + $props['user'] = $user->getId(); + } + + if ( !isset( $props['user'] ) ) { + $props['user'] = 0; + } + + if ( !isset( $props['comment'] ) ) { + $props['comment'] = 'just a test'; + } + + if ( !isset( $props['page'] ) ) { + $props['page'] = $this->testPage->getId(); + } + + if ( !isset( $props['content_model'] ) ) { + $props['content_model'] = CONTENT_MODEL_WIKITEXT; + } + + $rev = new Revision( $props ); + + $dbw = wfGetDB( DB_MASTER ); + $rev->insertOn( $dbw ); + + return $rev; + } + + /** + * @param string $titleString + * @param string $text + * @param string|null $model + * + * @return WikiPage + */ + private function createPage( $titleString, $text, $model = null ) { + if ( !preg_match( '/:/', $titleString ) && + ( $model === null || $model === CONTENT_MODEL_WIKITEXT ) + ) { + $ns = $this->getDefaultWikitextNS(); + $titleString = MWNamespace::getCanonicalName( $ns ) . ':' . $titleString; + } + + $title = Title::newFromText( $titleString ); + $wikipage = new WikiPage( $title ); + + // Delete the article if it already exists + if ( $wikipage->exists() ) { + $wikipage->doDeleteArticle( "done" ); + } + + $content = ContentHandler::makeContent( $text, $title, $model ); + $wikipage->doEditContent( $content, __METHOD__, EDIT_NEW ); + + return $wikipage; + } + + private 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::getRecentChange + */ + public function testGetRecentChange() { + $rev = $this->testPage->getRevision(); + $recentChange = $rev->getRecentChange(); + + // Make sure various attributes look right / the correct entry has been retrieved. + $this->assertEquals( $rev->getTimestamp(), $recentChange->getAttribute( 'rc_timestamp' ) ); + $this->assertEquals( + $rev->getTitle()->getNamespace(), + $recentChange->getAttribute( 'rc_namespace' ) + ); + $this->assertEquals( + $rev->getTitle()->getDBkey(), + $recentChange->getAttribute( 'rc_title' ) + ); + $this->assertEquals( $rev->getUser(), $recentChange->getAttribute( 'rc_user' ) ); + $this->assertEquals( $rev->getUserText(), $recentChange->getAttribute( 'rc_user_text' ) ); + $this->assertEquals( $rev->getComment(), $recentChange->getAttribute( 'rc_comment' ) ); + $this->assertEquals( $rev->getPage(), $recentChange->getAttribute( 'rc_cur_id' ) ); + $this->assertEquals( $rev->getId(), $recentChange->getAttribute( 'rc_this_oldid' ) ); + } + + /** + * @covers Revision::insertOn + */ + public function testInsertOn_success() { + $parentId = $this->testPage->getLatest(); + + // If an ExternalStore is set don't use it. + $this->setMwGlobals( 'wgDefaultExternalStore', false ); + + $rev = new Revision( [ + 'page' => $this->testPage->getId(), + 'title' => $this->testPage->getTitle(), + 'text' => 'Revision Text', + 'comment' => 'Revision comment', + ] ); + + $revId = $rev->insertOn( wfGetDB( DB_MASTER ) ); + + $this->assertInternalType( 'integer', $revId ); + $this->assertSame( $revId, $rev->getId() ); + + // getTextId() must be an int! + $this->assertInternalType( 'integer', $rev->getTextId() ); + + $mainSlot = $rev->getRevisionRecord()->getSlot( 'main', RevisionRecord::RAW ); + + // we currently only support storage in the text table + $textId = MediaWikiServices::getInstance() + ->getBlobStore() + ->getTextIdFromAddress( $mainSlot->getAddress() ); + + $this->assertSelect( + 'text', + [ 'old_id', 'old_text' ], + "old_id = $textId", + [ [ strval( $textId ), 'Revision Text' ] ] + ); + $this->assertSelect( + 'revision', + [ + 'rev_id', + 'rev_page', + 'rev_text_id', + 'rev_minor_edit', + 'rev_deleted', + 'rev_len', + 'rev_parent_id', + 'rev_sha1', + ], + "rev_id = {$rev->getId()}", + [ [ + strval( $rev->getId() ), + strval( $this->testPage->getId() ), + strval( $textId ), + '0', + '0', + '13', + strval( $parentId ), + 's0ngbdoxagreuf2vjtuxzwdz64n29xm', + ] ] + ); + } + + /** + * @covers Revision::insertOn + */ + public function testInsertOn_exceptionOnNoPage() { + // If an ExternalStore is set don't use it. + $this->setMwGlobals( 'wgDefaultExternalStore', false ); + $this->setExpectedException( + IncompleteRevisionException::class, + "rev_page field must not be 0!" + ); + + $title = Title::newFromText( 'Nonexistant-' . __METHOD__ ); + $rev = new Revision( [], 0, $title ); + + $rev->insertOn( wfGetDB( DB_MASTER ) ); + } + + /** + * @covers Revision::newFromTitle + */ + public function testNewFromTitle_withoutId() { + $latestRevId = $this->testPage->getLatest(); + + $rev = Revision::newFromTitle( $this->testPage->getTitle() ); + + $this->assertTrue( $this->testPage->getTitle()->equals( $rev->getTitle() ) ); + $this->assertEquals( $latestRevId, $rev->getId() ); + } + + /** + * @covers Revision::newFromTitle + */ + public function testNewFromTitle_withId() { + $latestRevId = $this->testPage->getLatest(); + + $rev = Revision::newFromTitle( $this->testPage->getTitle(), $latestRevId ); + + $this->assertTrue( $this->testPage->getTitle()->equals( $rev->getTitle() ) ); + $this->assertEquals( $latestRevId, $rev->getId() ); + } + + /** + * @covers Revision::newFromTitle + */ + public function testNewFromTitle_withBadId() { + $latestRevId = $this->testPage->getLatest(); + + $rev = Revision::newFromTitle( $this->testPage->getTitle(), $latestRevId + 1 ); + + $this->assertNull( $rev ); + } + + /** + * @covers Revision::newFromRow + */ + public function testNewFromRow() { + $orig = $this->makeRevisionWithProps(); + + $dbr = wfGetDB( DB_REPLICA ); + $revQuery = Revision::getQueryInfo(); + $res = $dbr->select( $revQuery['tables'], $revQuery['fields'], [ 'rev_id' => $orig->getId() ], + __METHOD__, [], $revQuery['joins'] ); + $this->assertTrue( is_object( $res ), 'query failed' ); + + $row = $res->fetchObject(); + $res->free(); + + $rev = Revision::newFromRow( $row ); + + $this->assertRevEquals( $orig, $rev ); + } + + public function provideNewFromArchiveRow() { + yield [ + function ( $f ) { + return $f; + }, + ]; + yield [ + function ( $f ) { + return $f + [ 'ar_namespace', 'ar_title' ]; + }, + ]; + yield [ + function ( $f ) { + unset( $f['ar_text_id'] ); + return $f; + }, + ]; + yield [ + function ( $f ) { + unset( $f['ar_page_id'] ); + return $f; + }, + ]; + yield [ + function ( $f ) { + unset( $f['ar_parent_id'] ); + return $f; + }, + ]; + yield [ + function ( $f ) { + unset( $f['ar_rev_id'] ); + return $f; + }, + ]; + yield [ + function ( $f ) { + unset( $f['ar_sha1'] ); + return $f; + }, + ]; + } + + /** + * @dataProvider provideNewFromArchiveRow + * @covers Revision::newFromArchiveRow + */ + public function testNewFromArchiveRow( $selectModifier ) { + $services = MediaWikiServices::getInstance(); + + $store = new RevisionStore( + $services->getDBLoadBalancer(), + $services->getService( '_SqlBlobStore' ), + $services->getMainWANObjectCache(), + $services->getCommentStore(), + $services->getActorMigration() + ); + + $store->setContentHandlerUseDB( $this->getContentHandlerUseDB() ); + $this->setService( 'RevisionStore', $store ); + + $page = $this->createPage( + 'RevisionStorageTest_testNewFromArchiveRow', + 'Lorem Ipsum', + CONTENT_MODEL_WIKITEXT + ); + $orig = $page->getRevision(); + $page->doDeleteArticle( 'test Revision::newFromArchiveRow' ); + + $dbr = wfGetDB( DB_REPLICA ); + $arQuery = Revision::getArchiveQueryInfo(); + $arQuery['fields'] = $selectModifier( $arQuery['fields'] ); + $res = $dbr->select( + $arQuery['tables'], $arQuery['fields'], [ 'ar_rev_id' => $orig->getId() ], + __METHOD__, [], $arQuery['joins'] + ); + $this->assertTrue( is_object( $res ), 'query failed' ); + + $row = $res->fetchObject(); + $res->free(); + + // MCR migration note: $row is now required to contain ar_title and ar_namespace. + // Alternatively, a Title object can be passed to RevisionStore::newRevisionFromArchiveRow + $rev = Revision::newFromArchiveRow( $row ); + + $this->assertRevEquals( $orig, $rev ); + } + + /** + * @covers Revision::newFromArchiveRow + */ + public function testNewFromArchiveRowOverrides() { + $page = $this->createPage( + 'RevisionStorageTest_testNewFromArchiveRow', + 'Lorem Ipsum', + CONTENT_MODEL_WIKITEXT + ); + $orig = $page->getRevision(); + $page->doDeleteArticle( 'test Revision::newFromArchiveRow' ); + + $dbr = wfGetDB( DB_REPLICA ); + $arQuery = Revision::getArchiveQueryInfo(); + $res = $dbr->select( + $arQuery['tables'], $arQuery['fields'], [ 'ar_rev_id' => $orig->getId() ], + __METHOD__, [], $arQuery['joins'] + ); + $this->assertTrue( is_object( $res ), 'query failed' ); + + $row = $res->fetchObject(); + $res->free(); + + $rev = Revision::newFromArchiveRow( $row, [ 'comment_text' => 'SOMEOVERRIDE' ] ); + + $this->assertNotEquals( $orig->getComment(), $rev->getComment() ); + $this->assertEquals( 'SOMEOVERRIDE', $rev->getComment() ); + } + + /** + * @covers Revision::newFromId + */ + public function testNewFromId() { + $orig = $this->testPage->getRevision(); + $rev = Revision::newFromId( $orig->getId() ); + $this->assertRevEquals( $orig, $rev ); + } + + /** + * @covers Revision::newFromPageId + */ + public function testNewFromPageId() { + $rev = Revision::newFromPageId( $this->testPage->getId() ); + $this->assertRevEquals( + $this->testPage->getRevision(), + $rev + ); + } + + /** + * @covers Revision::newFromPageId + */ + public function testNewFromPageIdWithLatestId() { + $rev = Revision::newFromPageId( + $this->testPage->getId(), + $this->testPage->getLatest() + ); + $this->assertRevEquals( + $this->testPage->getRevision(), + $rev + ); + } + + /** + * @covers Revision::newFromPageId + */ + public function testNewFromPageIdWithNotLatestId() { + $content = new WikitextContent( __METHOD__ ); + $this->testPage->doEditContent( $content, __METHOD__ ); + $rev = Revision::newFromPageId( + $this->testPage->getId(), + $this->testPage->getRevision()->getPrevious()->getId() + ); + $this->assertRevEquals( + $this->testPage->getRevision()->getPrevious(), + $rev + ); + } + + /** + * @covers Revision::fetchRevision + */ + public function testFetchRevision() { + // Hidden process cache assertion below + $this->testPage->getRevision()->getId(); + + $this->testPage->doEditContent( new WikitextContent( __METHOD__ ), __METHOD__ ); + $id = $this->testPage->getRevision()->getId(); + + $this->hideDeprecated( 'Revision::fetchRevision' ); + $res = Revision::fetchRevision( $this->testPage->getTitle() ); + + # note: order is unspecified + $rows = []; + while ( ( $row = $res->fetchObject() ) ) { + $rows[$row->rev_id] = $row; + } + + $this->assertEmpty( $rows, 'expected empty set' ); + } + + /** + * @covers Revision::getPage + */ + public function testGetPage() { + $page = $this->testPage; + + $orig = $this->makeRevisionWithProps( [ 'page' => $page->getId() ] ); + $rev = Revision::newFromId( $orig->getId() ); + + $this->assertEquals( $page->getId(), $rev->getPage() ); + } + + /** + * @covers Revision::isCurrent + */ + public function testIsCurrent() { + $rev1 = $this->testPage->getRevision(); + + # @todo find out if this should be true + # $this->assertTrue( $rev1->isCurrent() ); + + $rev1x = Revision::newFromId( $rev1->getId() ); + $this->assertTrue( $rev1x->isCurrent() ); + + $this->testPage->doEditContent( new WikitextContent( __METHOD__ ), __METHOD__ ); + $rev2 = $this->testPage->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() { + $oldestRevision = $this->testPage->getOldestRevision(); + $latestRevision = $this->testPage->getLatest(); + + $this->assertNull( $oldestRevision->getPrevious() ); + + $this->testPage->doEditContent( new WikitextContent( __METHOD__ ), __METHOD__ ); + $newRevision = $this->testPage->getRevision(); + + $this->assertNotNull( $newRevision->getPrevious() ); + $this->assertEquals( $latestRevision, $newRevision->getPrevious()->getId() ); + } + + /** + * @covers Revision::getNext + */ + public function testGetNext() { + $rev1 = $this->testPage->getRevision(); + + $this->assertNull( $rev1->getNext() ); + + $this->testPage->doEditContent( new WikitextContent( __METHOD__ ), __METHOD__ ); + $rev2 = $this->testPage->getRevision(); + + $this->assertNotNull( $rev1->getNext() ); + $this->assertEquals( $rev2->getId(), $rev1->getNext()->getId() ); + } + + /** + * @covers Revision::newNullRevision + */ + public function testNewNullRevision() { + $this->testPage->doEditContent( new WikitextContent( __METHOD__ ), __METHOD__ ); + $orig = $this->testPage->getRevision(); + + $dbw = wfGetDB( DB_MASTER ); + $rev = Revision::newNullRevision( $dbw, $this->testPage->getId(), 'a null revision', false ); + + $this->assertNotEquals( $orig->getId(), $rev->getId(), + 'new null revision should have a different id from the original revision' ); + $this->assertEquals( $orig->getTextId(), $rev->getTextId(), + 'new null revision should have the same text id as the original revision' ); + $this->assertEquals( $orig->getSha1(), $rev->getSha1(), + 'new null revision should have the same SHA1 as the original revision' ); + $this->assertTrue( $orig->getRevisionRecord()->hasSameContent( $rev->getRevisionRecord() ), + 'new null revision should have the same content as the original revision' ); + $this->assertEquals( __METHOD__, $rev->getContent()->getNativeData() ); + } + + /** + * @covers Revision::newNullRevision + */ + public function testNewNullRevision_badPage() { + $dbw = wfGetDB( DB_MASTER ); + $rev = Revision::newNullRevision( $dbw, -1, 'a null revision', false ); + + $this->assertNull( $rev ); + } + + /** + * @covers Revision::insertOn + */ + public function testInsertOn() { + $ip = '2600:387:ed7:947e:8c16:a1ad:dd34:1dd7'; + + $orig = $this->makeRevisionWithProps( [ + 'user_text' => $ip + ] ); + + // Make sure the revision was copied to ip_changes + $dbr = wfGetDB( DB_REPLICA ); + $res = $dbr->select( 'ip_changes', '*', [ 'ipc_rev_id' => $orig->getId() ] ); + $row = $res->fetchObject(); + + $this->assertEquals( IP::toHex( $ip ), $row->ipc_hex ); + $this->assertEquals( + $orig->getTimestamp(), + wfTimestamp( TS_MW, $row->ipc_rev_timestamp ) + ); + } + + public static function provideUserWasLastToEdit() { + yield 'actually the last edit' => [ 3, true ]; + yield 'not the current edit, but still by this user' => [ 2, true ]; + yield 'edit by another user' => [ 1, false ]; + yield 'first edit, by this user, but another user edited in the mean time' => [ 0, false ]; + } + + /** + * @covers Revision::userWasLastToEdit + * @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 = []; + + // create revisions ----------------------------- + $page = WikiPage::factory( Title::newFromText( + 'RevisionStorageTest_testUserWasLastToEdit', $ns ) ); + $page->insertOn( $dbw ); + + $revisions[0] = new Revision( [ + 'page' => $page->getId(), + // we need the title to determine the page's default content model + 'title' => $page->getTitle(), + 'timestamp' => '20120101000000', + 'user' => $userA->getId(), + 'text' => 'zero', + 'content_model' => CONTENT_MODEL_WIKITEXT, + 'comment' => 'edit zero' + ] ); + $revisions[0]->insertOn( $dbw ); + + $revisions[1] = new Revision( [ + 'page' => $page->getId(), + // still need the title, because $page->getId() is 0 (there's no entry in the page table) + 'title' => $page->getTitle(), + 'timestamp' => '20120101000100', + 'user' => $userA->getId(), + 'text' => 'one', + 'content_model' => CONTENT_MODEL_WIKITEXT, + 'comment' => 'edit one' + ] ); + $revisions[1]->insertOn( $dbw ); + + $revisions[2] = new Revision( [ + 'page' => $page->getId(), + 'title' => $page->getTitle(), + 'timestamp' => '20120101000200', + 'user' => $userB->getId(), + 'text' => 'two', + 'content_model' => CONTENT_MODEL_WIKITEXT, + 'comment' => 'edit two' + ] ); + $revisions[2]->insertOn( $dbw ); + + $revisions[3] = new Revision( [ + 'page' => $page->getId(), + 'title' => $page->getTitle(), + 'timestamp' => '20120101000300', + 'user' => $userA->getId(), + 'text' => 'three', + 'content_model' => CONTENT_MODEL_WIKITEXT, + 'comment' => 'edit three' + ] ); + $revisions[3]->insertOn( $dbw ); + + $revisions[4] = new Revision( [ + 'page' => $page->getId(), + 'title' => $page->getTitle(), + 'timestamp' => '20120101000200', + 'user' => $userA->getId(), + 'text' => 'zero', + 'content_model' => CONTENT_MODEL_WIKITEXT, + 'comment' => 'edit four' + ] ); + $revisions[4]->insertOn( $dbw ); + + // test it --------------------------------- + $since = $revisions[$sinceIdx]->getTimestamp(); + + $revQuery = Revision::getQueryInfo(); + $allRows = iterator_to_array( $dbw->select( + $revQuery['tables'], + [ 'rev_id', 'rev_timestamp', 'rev_user' => $revQuery['fields']['rev_user'] ], + [ + 'rev_page' => $page->getId(), + //'rev_timestamp > ' . $dbw->addQuotes( $dbw->timestamp( $since ) ) + ], + __METHOD__, + [ 'ORDER BY' => 'rev_timestamp ASC', 'LIMIT' => 50 ], + $revQuery['joins'] + ) ); + + $wasLast = Revision::userWasLastToEdit( $dbw, $page->getId(), $userA->getId(), $since ); + + $this->assertEquals( $expectedLast, $wasLast ); + } + + /** + * @param string $text + * @param string $title + * @param string $model + * @param string $format + * + * @return Revision + */ + private 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( + [ + 'id' => 42, + 'page' => 23, + 'title' => $title, + + 'content' => $content, + 'length' => $content->getSize(), + 'comment' => "testing", + 'minor_edit' => false, + + 'content_format' => $format, + ] + ); + + return $rev; + } + + public function provideGetContentModel() { + // NOTE: we expect the help namespace to always contain wikitext + return [ + [ 'hello world', 'Help:Hello', null, null, CONTENT_MODEL_WIKITEXT ], + [ 'hello world', 'User:hello/there.css', null, null, CONTENT_MODEL_CSS ], + [ serialize( 'hello world' ), 'Dummy:Hello', null, null, DummyContentForTesting::MODEL_ID ], + ]; + } + + /** + * @dataProvider provideGetContentModel + * @covers Revision::getContentModel + */ + public function testGetContentModel( $text, $title, $model, $format, $expectedModel ) { + $rev = $this->newTestRevision( $text, $title, $model, $format ); + + $this->assertEquals( $expectedModel, $rev->getContentModel() ); + } + + public function provideGetContentFormat() { + // NOTE: we expect the help namespace to always contain wikitext + return [ + [ 'hello world', 'Help:Hello', null, null, CONTENT_FORMAT_WIKITEXT ], + [ 'hello world', 'Help:Hello', CONTENT_MODEL_CSS, null, CONTENT_FORMAT_CSS ], + [ 'hello world', 'User:hello/there.css', null, null, CONTENT_FORMAT_CSS ], + [ serialize( 'hello world' ), 'Dummy:Hello', null, null, DummyContentForTesting::MODEL_ID ], + ]; + } + + /** + * @dataProvider provideGetContentFormat + * @covers Revision::getContentFormat + */ + public function testGetContentFormat( $text, $title, $model, $format, $expectedFormat ) { + $rev = $this->newTestRevision( $text, $title, $model, $format ); + + $this->assertEquals( $expectedFormat, $rev->getContentFormat() ); + } + + public function provideGetContentHandler() { + // NOTE: we expect the help namespace to always contain wikitext + return [ + [ 'hello world', 'Help:Hello', null, null, WikitextContentHandler::class ], + [ 'hello world', 'User:hello/there.css', null, null, CssContentHandler::class ], + [ serialize( 'hello world' ), 'Dummy:Hello', null, null, DummyContentHandlerForTesting::class ], + ]; + } + + /** + * @dataProvider provideGetContentHandler + * @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() ) ); + } + + public function provideGetContent() { + // NOTE: we expect the help namespace to always contain wikitext + return [ + [ 'hello world', 'Help:Hello', null, null, Revision::FOR_PUBLIC, 'hello world' ], + [ + serialize( 'hello world' ), + 'Hello', + DummyContentForTesting::MODEL_ID, + null, + Revision::FOR_PUBLIC, + serialize( 'hello world' ) + ], + [ + serialize( 'hello world' ), + 'Dummy:Hello', + null, + null, + Revision::FOR_PUBLIC, + serialize( 'hello world' ) + ], + ]; + } + + /** + * @dataProvider provideGetContent + * @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 ) + ); + } + + /** + * @covers Revision::getContent + */ + public function testGetContent_failure() { + $rev = new Revision( [ + 'page' => $this->testPage->getId(), + 'content_model' => $this->testPage->getContentModel(), + 'text_id' => 123456789, // not in the test DB + ] ); + + Wikimedia\suppressWarnings(); // bad text_id will trigger a warning. + + $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." ); + + Wikimedia\restoreWarnings(); + } + + public function provideGetSize() { + return [ + [ "hello world.", CONTENT_MODEL_WIKITEXT, 12 ], + [ serialize( "hello world." ), DummyContentForTesting::MODEL_ID, 12 ], + ]; + } + + /** + * @covers Revision::getSize + * @dataProvider provideGetSize + */ + public function testGetSize( $text, $model, $expected_size ) { + $rev = $this->newTestRevision( $text, 'RevisionTest_testGetSize', $model ); + $this->assertEquals( $expected_size, $rev->getSize() ); + } + + public function provideGetSha1() { + return [ + [ "hello world.", CONTENT_MODEL_WIKITEXT, Revision::base36Sha1( "hello world." ) ], + [ + serialize( "hello world." ), + DummyContentForTesting::MODEL_ID, + Revision::base36Sha1( serialize( "hello world." ) ) + ], + ]; + } + + /** + * @covers Revision::getSha1 + * @dataProvider provideGetSha1 + */ + public function testGetSha1( $text, $model, $expected_hash ) { + $rev = $this->newTestRevision( $text, 'RevisionTest_testGetSha1', $model ); + $this->assertEquals( $expected_hash, $rev->getSha1() ); + } + + /** + * Tests whether $rev->getContent() returns a clone when needed. + * + * @covers Revision::getContent + */ + public function testGetContentClone() { + $content = new RevisionTestModifyableContent( "foo" ); + + $rev = new Revision( + [ + 'id' => 42, + 'page' => 23, + 'title' => Title::newFromText( "testGetContentClone_dummy" ), + + 'content' => $content, + 'length' => $content->getSize(), + 'comment' => "testing", + 'minor_edit' => false, + ] + ); + + /** @var RevisionTestModifyableContent $content */ + $content = $rev->getContent( Revision::RAW ); + $content->setText( "bar" ); + + /** @var RevisionTestModifyableContent $content2 */ + $content2 = $rev->getContent( Revision::RAW ); + // content is mutable, expect clone + $this->assertNotSame( $content, $content2, "expected a clone" ); + // clone should contain the original text + $this->assertEquals( "foo", $content2->getText() ); + + $content2->setText( "bla bla" ); + // clones should be independent + $this->assertEquals( "bar", $content->getText() ); + } + + /** + * Tests whether $rev->getContent() returns the same object repeatedly if appropriate. + * @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 ); + } + + /** + * @covers Revision::loadFromId + */ + public function testLoadFromId() { + $rev = $this->testPage->getRevision(); + $this->hideDeprecated( 'Revision::loadFromId' ); + $this->assertRevEquals( + $rev, + Revision::loadFromId( wfGetDB( DB_MASTER ), $rev->getId() ) + ); + } + + /** + * @covers Revision::loadFromPageId + */ + public function testLoadFromPageId() { + $this->assertRevEquals( + $this->testPage->getRevision(), + Revision::loadFromPageId( wfGetDB( DB_MASTER ), $this->testPage->getId() ) + ); + } + + /** + * @covers Revision::loadFromPageId + */ + public function testLoadFromPageIdWithLatestRevId() { + $this->assertRevEquals( + $this->testPage->getRevision(), + Revision::loadFromPageId( + wfGetDB( DB_MASTER ), + $this->testPage->getId(), + $this->testPage->getLatest() + ) + ); + } + + /** + * @covers Revision::loadFromPageId + */ + public function testLoadFromPageIdWithNotLatestRevId() { + $this->testPage->doEditContent( new WikitextContent( __METHOD__ ), __METHOD__ ); + $this->assertRevEquals( + $this->testPage->getRevision()->getPrevious(), + Revision::loadFromPageId( + wfGetDB( DB_MASTER ), + $this->testPage->getId(), + $this->testPage->getRevision()->getPrevious()->getId() + ) + ); + } + + /** + * @covers Revision::loadFromTitle + */ + public function testLoadFromTitle() { + $this->assertRevEquals( + $this->testPage->getRevision(), + Revision::loadFromTitle( wfGetDB( DB_MASTER ), $this->testPage->getTitle() ) + ); + } + + /** + * @covers Revision::loadFromTitle + */ + public function testLoadFromTitleWithLatestRevId() { + $this->assertRevEquals( + $this->testPage->getRevision(), + Revision::loadFromTitle( + wfGetDB( DB_MASTER ), + $this->testPage->getTitle(), + $this->testPage->getLatest() + ) + ); + } + + /** + * @covers Revision::loadFromTitle + */ + public function testLoadFromTitleWithNotLatestRevId() { + $this->testPage->doEditContent( new WikitextContent( __METHOD__ ), __METHOD__ ); + $this->assertRevEquals( + $this->testPage->getRevision()->getPrevious(), + Revision::loadFromTitle( + wfGetDB( DB_MASTER ), + $this->testPage->getTitle(), + $this->testPage->getRevision()->getPrevious()->getId() + ) + ); + } + + /** + * @covers Revision::loadFromTimestamp() + */ + public function testLoadFromTimestamp() { + $this->assertRevEquals( + $this->testPage->getRevision(), + Revision::loadFromTimestamp( + wfGetDB( DB_MASTER ), + $this->testPage->getTitle(), + $this->testPage->getRevision()->getTimestamp() + ) + ); + } + + /** + * @covers Revision::getParentLengths + */ + public function testGetParentLengths_noRevIds() { + $this->assertSame( + [], + Revision::getParentLengths( + wfGetDB( DB_MASTER ), + [] + ) + ); + } + + /** + * @covers Revision::getParentLengths + */ + public function testGetParentLengths_oneRevId() { + $text = '831jr091jr0921kr21kr0921kjr0921j09rj1'; + $textLength = strlen( $text ); + + $this->testPage->doEditContent( new WikitextContent( $text ), __METHOD__ ); + $rev[1] = $this->testPage->getLatest(); + + $this->assertSame( + [ $rev[1] => $textLength ], + Revision::getParentLengths( + wfGetDB( DB_MASTER ), + [ $rev[1] ] + ) + ); + } + + /** + * @covers Revision::getParentLengths + */ + public function testGetParentLengths_multipleRevIds() { + $textOne = '831jr091jr0921kr21kr0921kjr0921j09rj1'; + $textOneLength = strlen( $textOne ); + $textTwo = '831jr091jr092121j09rj1'; + $textTwoLength = strlen( $textTwo ); + + $this->testPage->doEditContent( new WikitextContent( $textOne ), __METHOD__ ); + $rev[1] = $this->testPage->getLatest(); + $this->testPage->doEditContent( new WikitextContent( $textTwo ), __METHOD__ ); + $rev[2] = $this->testPage->getLatest(); + + $this->assertSame( + [ $rev[1] => $textOneLength, $rev[2] => $textTwoLength ], + Revision::getParentLengths( + wfGetDB( DB_MASTER ), + [ $rev[1], $rev[2] ] + ) + ); + } + + /** + * @covers Revision::getTitle + */ + public function testGetTitle_fromExistingRevision() { + $this->assertTrue( + $this->testPage->getTitle()->equals( + $this->testPage->getRevision()->getTitle() + ) + ); + } + + /** + * @covers Revision::getTitle + */ + public function testGetTitle_fromRevisionWhichWillLoadTheTitle() { + $rev = new Revision( [ 'id' => $this->testPage->getLatest() ] ); + $this->assertTrue( + $this->testPage->getTitle()->equals( + $rev->getTitle() + ) + ); + } + + /** + * @covers Revision::isMinor + */ + public function testIsMinor_true() { + // Use a sysop to ensure we can mark edits as minor + $sysop = $this->getTestSysop()->getUser(); + + $this->testPage->doEditContent( + new WikitextContent( __METHOD__ ), + __METHOD__, + EDIT_MINOR, + false, + $sysop + ); + $rev = $this->testPage->getRevision(); + + $this->assertSame( true, $rev->isMinor() ); + } + + /** + * @covers Revision::isMinor + */ + public function testIsMinor_false() { + $this->testPage->doEditContent( + new WikitextContent( __METHOD__ ), + __METHOD__, + 0 + ); + $rev = $this->testPage->getRevision(); + + $this->assertSame( false, $rev->isMinor() ); + } + + /** + * @covers Revision::getTimestamp + */ + public function testGetTimestamp() { + $testTimestamp = wfTimestampNow(); + + $this->testPage->doEditContent( + new WikitextContent( __METHOD__ ), + __METHOD__ + ); + $rev = $this->testPage->getRevision(); + + $this->assertInternalType( 'string', $rev->getTimestamp() ); + $this->assertTrue( strlen( $rev->getTimestamp() ) == strlen( 'YYYYMMDDHHMMSS' ) ); + $this->assertContains( substr( $testTimestamp, 0, 10 ), $rev->getTimestamp() ); + } + + /** + * @covers Revision::getUser + * @covers Revision::getUserText + */ + public function testGetUserAndText() { + $sysop = $this->getTestSysop()->getUser(); + + $this->testPage->doEditContent( + new WikitextContent( __METHOD__ ), + __METHOD__, + 0, + false, + $sysop + ); + $rev = $this->testPage->getRevision(); + + $this->assertSame( $sysop->getId(), $rev->getUser() ); + $this->assertSame( $sysop->getName(), $rev->getUserText() ); + } + + /** + * @covers Revision::isDeleted + */ + public function testIsDeleted_nothingDeleted() { + $rev = $this->testPage->getRevision(); + + $this->assertSame( false, $rev->isDeleted( Revision::DELETED_TEXT ) ); + $this->assertSame( false, $rev->isDeleted( Revision::DELETED_COMMENT ) ); + $this->assertSame( false, $rev->isDeleted( Revision::DELETED_RESTRICTED ) ); + $this->assertSame( false, $rev->isDeleted( Revision::DELETED_USER ) ); + } + + /** + * @covers Revision::getVisibility + */ + public function testGetVisibility_nothingDeleted() { + $rev = $this->testPage->getRevision(); + + $this->assertSame( 0, $rev->getVisibility() ); + } + + /** + * @covers Revision::getComment + */ + public function testGetComment_notDeleted() { + $expectedSummary = 'goatlicious summary'; + + $this->testPage->doEditContent( + new WikitextContent( __METHOD__ ), + $expectedSummary + ); + $rev = $this->testPage->getRevision(); + + $this->assertSame( $expectedSummary, $rev->getComment() ); + } + + /** + * @covers Revision::isUnpatrolled + */ + public function testIsUnpatrolled_returnsRecentChangesId() { + $this->testPage->doEditContent( new WikitextContent( __METHOD__ ), __METHOD__ ); + $rev = $this->testPage->getRevision(); + + $this->assertGreaterThan( 0, $rev->isUnpatrolled() ); + $this->assertSame( $rev->getRecentChange()->getAttribute( 'rc_id' ), $rev->isUnpatrolled() ); + } + + /** + * @covers Revision::isUnpatrolled + */ + public function testIsUnpatrolled_returnsZeroIfPatrolled() { + // This assumes that sysops are auto patrolled + $sysop = $this->getTestSysop()->getUser(); + $this->testPage->doEditContent( + new WikitextContent( __METHOD__ ), + __METHOD__, + 0, + false, + $sysop + ); + $rev = $this->testPage->getRevision(); + + $this->assertSame( 0, $rev->isUnpatrolled() ); + } + + /** + * This is a simple blanket test for all simple content getters and is methods to provide some + * coverage before the split of Revision into multiple classes for MCR work. + * @covers Revision::getContent + * @covers Revision::getSerializedData + * @covers Revision::getContentModel + * @covers Revision::getContentFormat + * @covers Revision::getContentHandler + */ + public function testSimpleContentGetters() { + $expectedText = 'testSimpleContentGetters in Revision. Goats love MCR...'; + $expectedSummary = 'goatlicious testSimpleContentGetters summary'; + + $this->testPage->doEditContent( + new WikitextContent( $expectedText ), + $expectedSummary + ); + $rev = $this->testPage->getRevision(); + + $this->assertSame( $expectedText, $rev->getContent()->getNativeData() ); + $this->assertSame( $expectedText, $rev->getSerializedData() ); + $this->assertSame( $this->testPage->getContentModel(), $rev->getContentModel() ); + $this->assertSame( $this->testPage->getContent()->getDefaultFormat(), $rev->getContentFormat() ); + $this->assertSame( $this->testPage->getContentHandler(), $rev->getContentHandler() ); + } + + /** + * @covers Revision::newKnownCurrent + */ + public function testNewKnownCurrent() { + // Setup the services + $cache = new WANObjectCache( [ 'cache' => new HashBagOStuff() ] ); + $this->setService( 'MainWANObjectCache', $cache ); + $db = wfGetDB( DB_MASTER ); + + // Get a fresh revision to use during testing + $this->testPage->doEditContent( new WikitextContent( __METHOD__ ), __METHOD__ ); + $rev = $this->testPage->getRevision(); + + // Clear any previous cache for the revision during creation + $key = $cache->makeGlobalKey( 'revision-row-1.29', + $db->getDomainID(), + $rev->getPage(), + $rev->getId() + ); + $cache->delete( $key, WANObjectCache::HOLDOFF_NONE ); + $this->assertFalse( $cache->get( $key ) ); + + // Get the new revision and make sure it is in the cache and correct + $newRev = Revision::newKnownCurrent( $db, $rev->getPage(), $rev->getId() ); + $this->assertRevEquals( $rev, $newRev ); + + $cachedRow = $cache->get( $key ); + $this->assertNotFalse( $cachedRow ); + $this->assertEquals( $rev->getId(), $cachedRow->rev_id ); + } + + public function testNewKnownCurrent_withPageId() { + $db = wfGetDB( DB_MASTER ); + + $this->testPage->doEditContent( new WikitextContent( __METHOD__ ), __METHOD__ ); + $rev = $this->testPage->getRevision(); + + $pageId = $this->testPage->getId(); + + $newRev = Revision::newKnownCurrent( $db, $pageId, $rev->getId() ); + $this->assertRevEquals( $rev, $newRev ); + } + + public function testNewKnownCurrent_returnsFalseWhenTitleDoesntExist() { + $db = wfGetDB( DB_MASTER ); + + $this->assertFalse( Revision::newKnownCurrent( $db, 0 ) ); + } + + public function provideUserCanBitfield() { + yield [ 0, 0, [], null, true ]; + // Bitfields match, user has no permissions + yield [ Revision::DELETED_TEXT, Revision::DELETED_TEXT, [], null, false ]; + yield [ Revision::DELETED_COMMENT, Revision::DELETED_COMMENT, [], null, false ]; + yield [ Revision::DELETED_USER, Revision::DELETED_USER, [], null, false ]; + yield [ Revision::DELETED_RESTRICTED, Revision::DELETED_RESTRICTED, [], null, false ]; + // Bitfields match, user (admin) does have permissions + yield [ Revision::DELETED_TEXT, Revision::DELETED_TEXT, [ 'sysop' ], null, true ]; + yield [ Revision::DELETED_COMMENT, Revision::DELETED_COMMENT, [ 'sysop' ], null, true ]; + yield [ Revision::DELETED_USER, Revision::DELETED_USER, [ 'sysop' ], null, true ]; + // Bitfields match, user (admin) does not have permissions + yield [ Revision::DELETED_RESTRICTED, Revision::DELETED_RESTRICTED, [ 'sysop' ], null, false ]; + // Bitfields match, user (oversight) does have permissions + yield [ Revision::DELETED_RESTRICTED, Revision::DELETED_RESTRICTED, [ 'oversight' ], null, true ]; + // Check permissions using the title + yield [ + Revision::DELETED_TEXT, + Revision::DELETED_TEXT, + [ 'sysop' ], + Title::newFromText( __METHOD__ ), + true, + ]; + yield [ + Revision::DELETED_TEXT, + Revision::DELETED_TEXT, + [], + Title::newFromText( __METHOD__ ), + false, + ]; + } + + /** + * @dataProvider provideUserCanBitfield + * @covers Revision::userCanBitfield + */ + public function testUserCanBitfield( $bitField, $field, $userGroups, $title, $expected ) { + $this->setMwGlobals( + 'wgGroupPermissions', + [ + 'sysop' => [ + 'deletedtext' => true, + 'deletedhistory' => true, + ], + 'oversight' => [ + 'viewsuppressed' => true, + 'suppressrevision' => true, + ], + ] + ); + $user = $this->getTestUser( $userGroups )->getUser(); + + $this->assertSame( + $expected, + Revision::userCanBitfield( $bitField, $field, $user, $title ) + ); + + // Fallback to $wgUser + $this->setMwGlobals( + 'wgUser', + $user + ); + $this->assertSame( + $expected, + Revision::userCanBitfield( $bitField, $field, null, $title ) + ); + } + + public function provideUserCan() { + yield [ 0, 0, [], true ]; + // Bitfields match, user has no permissions + yield [ Revision::DELETED_TEXT, Revision::DELETED_TEXT, [], false ]; + yield [ Revision::DELETED_COMMENT, Revision::DELETED_COMMENT, [], false ]; + yield [ Revision::DELETED_USER, Revision::DELETED_USER, [], false ]; + yield [ Revision::DELETED_RESTRICTED, Revision::DELETED_RESTRICTED, [], false ]; + // Bitfields match, user (admin) does have permissions + yield [ Revision::DELETED_TEXT, Revision::DELETED_TEXT, [ 'sysop' ], true ]; + yield [ Revision::DELETED_COMMENT, Revision::DELETED_COMMENT, [ 'sysop' ], true ]; + yield [ Revision::DELETED_USER, Revision::DELETED_USER, [ 'sysop' ], true ]; + // Bitfields match, user (admin) does not have permissions + yield [ Revision::DELETED_RESTRICTED, Revision::DELETED_RESTRICTED, [ 'sysop' ], false ]; + // Bitfields match, user (oversight) does have permissions + yield [ Revision::DELETED_RESTRICTED, Revision::DELETED_RESTRICTED, [ 'oversight' ], true ]; + } + + /** + * @dataProvider provideUserCan + * @covers Revision::userCan + */ + public function testUserCan( $bitField, $field, $userGroups, $expected ) { + $this->setMwGlobals( + 'wgGroupPermissions', + [ + 'sysop' => [ + 'deletedtext' => true, + 'deletedhistory' => true, + ], + 'oversight' => [ + 'viewsuppressed' => true, + 'suppressrevision' => true, + ], + ] + ); + $user = $this->getTestUser( $userGroups )->getUser(); + $revision = new Revision( [ 'deleted' => $bitField ], 0, $this->testPage->getTitle() ); + + $this->assertSame( + $expected, + $revision->userCan( $field, $user ) + ); + } + +} diff --git a/www/wiki/tests/phpunit/includes/RevisionNoContentHandlerDbTest.php b/www/wiki/tests/phpunit/includes/RevisionNoContentHandlerDbTest.php new file mode 100644 index 00000000..c980a487 --- /dev/null +++ b/www/wiki/tests/phpunit/includes/RevisionNoContentHandlerDbTest.php @@ -0,0 +1,14 @@ +<?php + +/** + * @group Database + * @group medium + * @group ContentHandler + */ +class RevisionNoContentHandlerDbTest extends RevisionDbTestBase { + + protected function getContentHandlerUseDB() { + return false; + } + +} diff --git a/www/wiki/tests/phpunit/includes/RevisionStorageTest.php b/www/wiki/tests/phpunit/includes/RevisionStorageTest.php deleted file mode 100644 index e9f16dbd..00000000 --- a/www/wiki/tests/phpunit/includes/RevisionStorageTest.php +++ /dev/null @@ -1,574 +0,0 @@ -<?php - -/** - * Test class for Revision storage. - * - * @group ContentHandler - * @group Database - * ^--- important, causes temporary tables to be used instead of the real database - * - * @group medium - * ^--- important, causes tests not to fail with timeout - */ -class RevisionStorageTest extends MediaWikiTestCase { - /** - * @var WikiPage $the_page - */ - private $the_page; - - function __construct( $name = null, array $data = [], $dataName = '' ) { - parent::__construct( $name, $data, $dataName ); - - $this->tablesUsed = array_merge( $this->tablesUsed, - [ 'page', - 'revision', - 'ip_changes', - '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 - ); - } - - $this->tablesUsed[] = 'archive'; - } - - protected 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 = []; - } - - 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_REPLICA ); - $res = $dbr->select( 'revision', Revision::selectFields(), [ '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_REPLICA ); - $res = $dbr->select( 'revision', Revision::selectFields(), [ '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_REPLICA ); - $res = $dbr->select( - 'archive', Revision::selectArchiveFields(), [ '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 - ); - - // Hidden process cache assertion below - $page->getRevision()->getId(); - - $page->doEditContent( new WikitextContent( 'two' ), 'second rev' ); - $id = $page->getRevision()->getId(); - - $res = Revision::fetchRevision( $page->getTitle() ); - - # note: order is unspecified - $rows = []; - while ( ( $row = $res->fetchObject() ) ) { - $rows[$row->rev_id] = $row; - } - - $this->assertEquals( 1, count( $rows ), 'expected exactly one revision' ); - $this->assertArrayHasKey( $id, $rows, 'missing revision with id ' . $id ); - } - - /** - * @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( [ 'page' => $page->getId() ] ); - $rev = Revision::newFromId( $orig->getId() ); - - $this->assertEquals( $page->getId(), $rev->getPage() ); - } - - /** - * @covers Revision::getContent - */ - public function testGetContent_failure() { - $rev = new Revision( [ - '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( [ 'text' => 'hello hello.' ] ); - $rev = Revision::newFromId( $orig->getId() ); - - $this->assertEquals( 'hello hello.', $rev->getContent()->getNativeData() ); - } - - /** - * @covers Revision::getContentModel - */ - public function testGetContentModel() { - global $wgContentHandlerUseDB; - - if ( !$wgContentHandlerUseDB ) { - $this->markTestSkipped( '$wgContentHandlerUseDB is disabled' ); - } - - $orig = $this->makeRevision( [ '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( [ - '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() ); - } - - /** - * @covers Revision::insertOn - */ - public function testInsertOn() { - $ip = '2600:387:ed7:947e:8c16:a1ad:dd34:1dd7'; - - $orig = $this->makeRevision( [ - 'user_text' => $ip - ] ); - - // Make sure the revision was copied to ip_changes - $dbr = wfGetDB( DB_REPLICA ); - $res = $dbr->select( 'ip_changes', '*', [ 'ipc_rev_id' => $orig->getId() ] ); - $row = $res->fetchObject(); - - $this->assertEquals( IP::toHex( $ip ), $row->ipc_hex ); - $this->assertEquals( $orig->getTimestamp(), $row->ipc_rev_timestamp ); - } - - public static function provideUserWasLastToEdit() { - return [ - [ # 0 - 3, true, # actually the last edit - ], - [ # 1 - 2, true, # not the current edit, but still by this user - ], - [ # 2 - 1, false, # edit by another user - ], - [ # 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 = []; - - // create revisions ----------------------------- - $page = WikiPage::factory( Title::newFromText( - 'RevisionStorageTest_testUserWasLastToEdit', $ns ) ); - $page->insertOn( $dbw ); - - # zero - $revisions[0] = new Revision( [ - 'page' => $page->getId(), - // we need the title to determine the page's default content model - 'title' => $page->getTitle(), - 'timestamp' => '20120101000000', - 'user' => $userA->getId(), - 'text' => 'zero', - 'content_model' => CONTENT_MODEL_WIKITEXT, - 'summary' => 'edit zero' - ] ); - $revisions[0]->insertOn( $dbw ); - - # one - $revisions[1] = new Revision( [ - 'page' => $page->getId(), - // still need the title, because $page->getId() is 0 (there's no entry in the page table) - 'title' => $page->getTitle(), - 'timestamp' => '20120101000100', - 'user' => $userA->getId(), - 'text' => 'one', - 'content_model' => CONTENT_MODEL_WIKITEXT, - 'summary' => 'edit one' - ] ); - $revisions[1]->insertOn( $dbw ); - - # two - $revisions[2] = new Revision( [ - '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( [ - '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( [ - '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/www/wiki/tests/phpunit/includes/RevisionStorageTestContentHandlerUseDB.php b/www/wiki/tests/phpunit/includes/RevisionStorageTestContentHandlerUseDB.php deleted file mode 100644 index 9e667f21..00000000 --- a/www/wiki/tests/phpunit/includes/RevisionStorageTestContentHandlerUseDB.php +++ /dev/null @@ -1,89 +0,0 @@ -<?php - -/** - * @group ContentHandler - * @group Database - * ^--- important, causes temporary tables to be used instead of the real database - */ -class RevisionTestContentHandlerUseDB extends RevisionStorageTest { - - protected function setUp() { - $this->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( [ '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( [ '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/www/wiki/tests/phpunit/includes/RevisionTest.php b/www/wiki/tests/phpunit/includes/RevisionTest.php index c971a40c..ab067a47 100644 --- a/www/wiki/tests/phpunit/includes/RevisionTest.php +++ b/www/wiki/tests/phpunit/includes/RevisionTest.php @@ -1,139 +1,569 @@ <?php +use MediaWiki\MediaWikiServices; +use MediaWiki\Storage\BlobStoreFactory; +use MediaWiki\Storage\MutableRevisionRecord; +use MediaWiki\Storage\RevisionAccessException; +use MediaWiki\Storage\RevisionRecord; +use MediaWiki\Storage\RevisionStore; +use MediaWiki\Storage\SlotRecord; +use MediaWiki\Storage\SqlBlobStore; +use Wikimedia\Rdbms\IDatabase; +use Wikimedia\Rdbms\LoadBalancer; + /** - * @group ContentHandler + * Test cases in RevisionTest should not interact with the Database. + * For test cases that need Database interaction see RevisionDbTestBase. */ class RevisionTest extends MediaWikiTestCase { - protected function setUp() { - global $wgContLang; - parent::setUp(); + public function provideConstructFromArray() { + yield 'with text' => [ + [ + 'text' => 'hello world.', + 'content_model' => CONTENT_MODEL_JAVASCRIPT + ], + ]; + yield 'with content' => [ + [ + 'content' => new JavaScriptContent( 'hellow world.' ) + ], + ]; + // FIXME: test with and without user ID, and with a user object. + // We can't prepare that here though, since we don't yet have a dummy DB + } + + /** + * @param string $model + * @return Title + */ + public function getMockTitle( $model = CONTENT_MODEL_WIKITEXT ) { + $mock = $this->getMockBuilder( Title::class ) + ->disableOriginalConstructor() + ->getMock(); + $mock->expects( $this->any() ) + ->method( 'getNamespace' ) + ->will( $this->returnValue( $this->getDefaultWikitextNS() ) ); + $mock->expects( $this->any() ) + ->method( 'getPrefixedText' ) + ->will( $this->returnValue( 'RevisionTest' ) ); + $mock->expects( $this->any() ) + ->method( 'getDBkey' ) + ->will( $this->returnValue( 'RevisionTest' ) ); + $mock->expects( $this->any() ) + ->method( 'getArticleID' ) + ->will( $this->returnValue( 23 ) ); + $mock->expects( $this->any() ) + ->method( 'getContentModel' ) + ->will( $this->returnValue( $model ) ); + + return $mock; + } + + /** + * @dataProvider provideConstructFromArray + * @covers Revision::__construct + * @covers \MediaWiki\Storage\RevisionStore::newMutableRevisionFromArray + */ + public function testConstructFromArray( $rowArray ) { + $rev = new Revision( $rowArray, 0, $this->getMockTitle() ); + $this->assertNotNull( $rev->getContent(), 'no content object available' ); + $this->assertEquals( CONTENT_MODEL_JAVASCRIPT, $rev->getContent()->getModel() ); + $this->assertEquals( CONTENT_MODEL_JAVASCRIPT, $rev->getContentModel() ); + } - $this->setMwGlobals( [ - 'wgContLang' => Language::factory( 'en' ), - 'wgLanguageCode' => 'en', - 'wgLegacyEncoding' => false, - 'wgCompressRevisions' => false, + /** + * @covers Revision::__construct + * @covers \MediaWiki\Storage\RevisionStore::newMutableRevisionFromArray + */ + public function testConstructFromEmptyArray() { + $rev = new Revision( [], 0, $this->getMockTitle() ); + $this->assertNull( $rev->getContent(), 'no content object should be available' ); + } - 'wgContentHandlerTextFallback' => 'ignore', - ] ); + /** + * @covers Revision::__construct + * @covers \MediaWiki\Storage\RevisionStore::newMutableRevisionFromArray + */ + public function testConstructFromArrayWithBadPageId() { + Wikimedia\suppressWarnings(); + $rev = new Revision( [ 'page' => 77777777 ] ); + $this->assertSame( 77777777, $rev->getPage() ); + Wikimedia\restoreWarnings(); + } - $this->mergeMwGlobalArrayValue( - 'wgExtraNamespaces', + public function provideConstructFromArray_userSetAsExpected() { + yield 'no user defaults to wgUser' => [ [ - 12312 => 'Dummy', - 12313 => 'Dummy_talk', - ] - ); + 'content' => new JavaScriptContent( 'hello world.' ), + ], + null, + null, + ]; + yield 'user text and id' => [ + [ + 'content' => new JavaScriptContent( 'hello world.' ), + 'user_text' => 'SomeTextUserName', + 'user' => 99, - $this->mergeMwGlobalArrayValue( - 'wgNamespaceContentModels', + ], + 99, + 'SomeTextUserName', + ]; + yield 'user text only' => [ [ - 12312 => 'testing', - ] - ); + 'content' => new JavaScriptContent( 'hello world.' ), + 'user_text' => '111.111.111.111', + ], + 0, + '111.111.111.111', + ]; + } - $this->mergeMwGlobalArrayValue( - 'wgContentHandlers', + /** + * @dataProvider provideConstructFromArray_userSetAsExpected + * @covers Revision::__construct + * @covers \MediaWiki\Storage\RevisionStore::newMutableRevisionFromArray + * + * @param array $rowArray + * @param mixed $expectedUserId null to expect the current wgUser ID + * @param mixed $expectedUserName null to expect the current wgUser name + */ + public function testConstructFromArray_userSetAsExpected( + array $rowArray, + $expectedUserId, + $expectedUserName + ) { + $testUser = $this->getTestUser()->getUser(); + $this->setMwGlobals( 'wgUser', $testUser ); + if ( $expectedUserId === null ) { + $expectedUserId = $testUser->getId(); + } + if ( $expectedUserName === null ) { + $expectedUserName = $testUser->getName(); + } + + $rev = new Revision( $rowArray, 0, $this->getMockTitle() ); + $this->assertEquals( $expectedUserId, $rev->getUser() ); + $this->assertEquals( $expectedUserName, $rev->getUserText() ); + } + + public function provideConstructFromArrayThrowsExceptions() { + yield 'content and text_id both not empty' => [ [ - 'testing' => 'DummyContentHandlerForTesting', - 'RevisionTestModifyableContent' => 'RevisionTestModifyableContentHandler', - ] + 'content' => new WikitextContent( 'GOAT' ), + 'text_id' => 'someid', + ], + new MWException( "Text already stored in external store (id someid), " . + "can't serialize content object" ) + ]; + yield 'with bad content object (class)' => [ + [ 'content' => new stdClass() ], + new MWException( 'content field must contain a Content object.' ) + ]; + yield 'with bad content object (string)' => [ + [ 'content' => 'ImAGoat' ], + new MWException( 'content field must contain a Content object.' ) + ]; + yield 'bad row format' => [ + 'imastring, not a row', + new InvalidArgumentException( + '$row must be a row object, an associative array, or a RevisionRecord' + ) + ]; + } + + /** + * @dataProvider provideConstructFromArrayThrowsExceptions + * @covers Revision::__construct + * @covers \MediaWiki\Storage\RevisionStore::newMutableRevisionFromArray + */ + public function testConstructFromArrayThrowsExceptions( $rowArray, Exception $expectedException ) { + $this->setExpectedException( + get_class( $expectedException ), + $expectedException->getMessage(), + $expectedException->getCode() ); + new Revision( $rowArray, 0, $this->getMockTitle() ); + } + + /** + * @covers Revision::__construct + * @covers \MediaWiki\Storage\RevisionStore::newMutableRevisionFromArray + */ + public function testConstructFromNothing() { + $this->setExpectedException( + InvalidArgumentException::class + ); + new Revision( [] ); + } + + public function provideConstructFromRow() { + yield 'Full construction' => [ + [ + 'rev_id' => '42', + 'rev_page' => '23', + 'rev_text_id' => '2', + 'rev_timestamp' => '20171017114835', + 'rev_user_text' => '127.0.0.1', + 'rev_user' => '0', + 'rev_minor_edit' => '0', + 'rev_deleted' => '0', + 'rev_len' => '46', + 'rev_parent_id' => '1', + 'rev_sha1' => 'rdqbbzs3pkhihgbs8qf2q9jsvheag5z', + 'rev_comment_text' => 'Goat Comment!', + 'rev_comment_data' => null, + 'rev_comment_cid' => null, + 'rev_content_format' => 'GOATFORMAT', + 'rev_content_model' => 'GOATMODEL', + ], + function ( RevisionTest $testCase, Revision $rev ) { + $testCase->assertSame( 42, $rev->getId() ); + $testCase->assertSame( 23, $rev->getPage() ); + $testCase->assertSame( 2, $rev->getTextId() ); + $testCase->assertSame( '20171017114835', $rev->getTimestamp() ); + $testCase->assertSame( '127.0.0.1', $rev->getUserText() ); + $testCase->assertSame( 0, $rev->getUser() ); + $testCase->assertSame( false, $rev->isMinor() ); + $testCase->assertSame( false, $rev->isDeleted( Revision::DELETED_TEXT ) ); + $testCase->assertSame( 46, $rev->getSize() ); + $testCase->assertSame( 1, $rev->getParentId() ); + $testCase->assertSame( 'rdqbbzs3pkhihgbs8qf2q9jsvheag5z', $rev->getSha1() ); + $testCase->assertSame( 'Goat Comment!', $rev->getComment() ); + $testCase->assertSame( 'GOATFORMAT', $rev->getContentFormat() ); + $testCase->assertSame( 'GOATMODEL', $rev->getContentModel() ); + } + ]; + yield 'default field values' => [ + [ + 'rev_id' => '42', + 'rev_page' => '23', + 'rev_text_id' => '2', + 'rev_timestamp' => '20171017114835', + 'rev_user_text' => '127.0.0.1', + 'rev_user' => '0', + 'rev_minor_edit' => '0', + 'rev_deleted' => '0', + 'rev_comment_text' => 'Goat Comment!', + 'rev_comment_data' => null, + 'rev_comment_cid' => null, + ], + function ( RevisionTest $testCase, Revision $rev ) { + // parent ID may be null + $testCase->assertSame( null, $rev->getParentId(), 'revision id' ); + + // given fields + $testCase->assertSame( $rev->getTimestamp(), '20171017114835', 'timestamp' ); + $testCase->assertSame( $rev->getUserText(), '127.0.0.1', 'user name' ); + $testCase->assertSame( $rev->getUser(), 0, 'user id' ); + $testCase->assertSame( $rev->getComment(), 'Goat Comment!' ); + $testCase->assertSame( false, $rev->isMinor(), 'minor edit' ); + $testCase->assertSame( 0, $rev->getVisibility(), 'visibility flags' ); + + // computed fields + $testCase->assertNotNull( $rev->getSize(), 'size' ); + $testCase->assertNotNull( $rev->getSha1(), 'hash' ); + + // NOTE: model and format will be detected based on the namespace of the (mock) title + $testCase->assertSame( 'text/x-wiki', $rev->getContentFormat(), 'format' ); + $testCase->assertSame( 'wikitext', $rev->getContentModel(), 'model' ); + } + ]; + } + + /** + * @dataProvider provideConstructFromRow + * @covers Revision::__construct + * @covers \MediaWiki\Storage\RevisionStore::newMutableRevisionFromArray + */ + public function testConstructFromRow( array $arrayData, $assertions ) { + $data = 'Hello goat.'; // needs to match model and format + + $blobStore = $this->getMockBuilder( SqlBlobStore::class ) + ->disableOriginalConstructor() + ->getMock(); + + $blobStore->method( 'getBlob' ) + ->will( $this->returnValue( $data ) ); + + $blobStore->method( 'getTextIdFromAddress' ) + ->will( $this->returnCallback( + function ( $address ) { + // Turn "tt:1234" into 12345. + // Note that this must be functional so we can test getTextId(). + // Ideally, we'd un-mock getTextIdFromAddress and use its actual implementation. + $parts = explode( ':', $address ); + return (int)array_pop( $parts ); + } + ) ); + + // Note override internal service, so RevisionStore uses it as well. + $this->setService( 'BlobStoreFactory', $this->mockBlobStoreFactory( $blobStore ) ); + + $row = (object)$arrayData; + $rev = new Revision( $row, 0, $this->getMockTitle() ); + $assertions( $this, $rev ); + } + + /** + * @covers Revision::__construct + * @covers \MediaWiki\Storage\RevisionStore::newMutableRevisionFromArray + */ + public function testConstructFromRowWithBadPageId() { + $this->setMwGlobals( 'wgCommentTableSchemaMigrationStage', MIGRATION_OLD ); + $this->overrideMwServices(); + Wikimedia\suppressWarnings(); + $rev = new Revision( (object)[ 'rev_page' => 77777777 ] ); + $this->assertSame( 77777777, $rev->getPage() ); + Wikimedia\restoreWarnings(); + } - MWNamespace::getCanonicalNamespaces( true ); # reset namespace cache - $wgContLang->resetNamespaces(); # reset namespace cache + public function provideGetRevisionText() { + yield 'Generic test' => [ + 'This is a goat of revision text.', + [ + 'old_flags' => '', + 'old_text' => 'This is a goat of revision text.', + ], + ]; + } + + public function provideGetId() { + yield [ + [], + null + ]; + yield [ + [ 'id' => 998 ], + 998 + ]; + } + + /** + * @dataProvider provideGetId + * @covers Revision::getId + */ + public function testGetId( $rowArray, $expectedId ) { + $rev = new Revision( $rowArray, 0, $this->getMockTitle() ); + $this->assertEquals( $expectedId, $rev->getId() ); + } + + public function provideSetId() { + yield [ '123', 123 ]; + yield [ 456, 456 ]; + } + + /** + * @dataProvider provideSetId + * @covers Revision::setId + */ + public function testSetId( $input, $expected ) { + $rev = new Revision( [], 0, $this->getMockTitle() ); + $rev->setId( $input ); + $this->assertSame( $expected, $rev->getId() ); + } + + public function provideSetUserIdAndName() { + yield [ '123', 123, 'GOaT' ]; + yield [ 456, 456, 'GOaT' ]; + } + + /** + * @dataProvider provideSetUserIdAndName + * @covers Revision::setUserIdAndName + */ + public function testSetUserIdAndName( $inputId, $expectedId, $name ) { + $rev = new Revision( [], 0, $this->getMockTitle() ); + $rev->setUserIdAndName( $inputId, $name ); + $this->assertSame( $expectedId, $rev->getUser( Revision::RAW ) ); + $this->assertEquals( $name, $rev->getUserText( Revision::RAW ) ); } - function tearDown() { - global $wgContLang; + public function provideGetTextId() { + yield [ [], null ]; + yield [ [ 'text_id' => '123' ], 123 ]; + yield [ [ 'text_id' => 456 ], 456 ]; + } - MWNamespace::getCanonicalNamespaces( true ); # reset namespace cache - $wgContLang->resetNamespaces(); # reset namespace cache + /** + * @dataProvider provideGetTextId + * @covers Revision::getTextId() + */ + public function testGetTextId( $rowArray, $expected ) { + $rev = new Revision( $rowArray, 0, $this->getMockTitle() ); + $this->assertSame( $expected, $rev->getTextId() ); + } - parent::tearDown(); + public function provideGetParentId() { + yield [ [], null ]; + yield [ [ 'parent_id' => '123' ], 123 ]; + yield [ [ 'parent_id' => 456 ], 456 ]; + } + + /** + * @dataProvider provideGetParentId + * @covers Revision::getParentId() + */ + public function testGetParentId( $rowArray, $expected ) { + $rev = new Revision( $rowArray, 0, $this->getMockTitle() ); + $this->assertSame( $expected, $rev->getParentId() ); } /** * @covers Revision::getRevisionText + * @dataProvider provideGetRevisionText */ - public function testGetRevisionText() { - $row = new stdClass; - $row->old_flags = ''; - $row->old_text = 'This is a bunch of revision text.'; + public function testGetRevisionText( $expected, $rowData, $prefix = 'old_', $wiki = false ) { $this->assertEquals( - 'This is a bunch of revision text.', - Revision::getRevisionText( $row ) ); + $expected, + Revision::getRevisionText( (object)$rowData, $prefix, $wiki ) ); + } + + public function provideGetRevisionTextWithZlibExtension() { + yield 'Generic gzip test' => [ + 'This is a small goat of revision text.', + [ + 'old_flags' => 'gzip', + 'old_text' => gzdeflate( 'This is a small goat of revision text.' ), + ], + ]; } /** * @covers Revision::getRevisionText + * @dataProvider provideGetRevisionTextWithZlibExtension */ - public function testGetRevisionTextGzip() { + public function testGetRevisionWithZlibExtension( $expected, $rowData ) { $this->checkPHPExtension( 'zlib' ); + $this->testGetRevisionText( $expected, $rowData ); + } - $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 ) ); + private function getWANObjectCache() { + return new WANObjectCache( [ 'cache' => new HashBagOStuff() ] ); } /** - * @covers Revision::getRevisionText + * @return SqlBlobStore */ - 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 ) ); + private function getBlobStore() { + /** @var LoadBalancer $lb */ + $lb = $this->getMockBuilder( LoadBalancer::class ) + ->disableOriginalConstructor() + ->getMock(); + + $cache = $this->getWANObjectCache(); + + $blobStore = new SqlBlobStore( $lb, $cache ); + return $blobStore; + } + + private function mockBlobStoreFactory( $blobStore ) { + /** @var LoadBalancer $lb */ + $factory = $this->getMockBuilder( BlobStoreFactory::class ) + ->disableOriginalConstructor() + ->getMock(); + $factory->expects( $this->any() ) + ->method( 'newBlobStore' ) + ->willReturn( $blobStore ); + $factory->expects( $this->any() ) + ->method( 'newSqlBlobStore' ) + ->willReturn( $blobStore ); + return $factory; } /** - * @covers Revision::getRevisionText + * @return RevisionStore */ - public function testGetRevisionTextUtf8Legacy() { - $row = new stdClass; - $row->old_flags = ''; - $row->old_text = "Wiki est l'\xe9cole superieur !"; - $GLOBALS['wgLegacyEncoding'] = 'iso-8859-1'; - $this->assertEquals( + private function getRevisionStore() { + /** @var LoadBalancer $lb */ + $lb = $this->getMockBuilder( LoadBalancer::class ) + ->disableOriginalConstructor() + ->getMock(); + + $cache = $this->getWANObjectCache(); + + $blobStore = new RevisionStore( + $lb, + $this->getBlobStore(), + $cache, + MediaWikiServices::getInstance()->getCommentStore(), + MediaWikiServices::getInstance()->getActorMigration() + ); + return $blobStore; + } + + public function provideGetRevisionTextWithLegacyEncoding() { + yield 'Utf8Native' => [ "Wiki est l'\xc3\xa9cole superieur !", - Revision::getRevisionText( $row ) ); + 'fr', + 'iso-8859-1', + [ + 'old_flags' => 'utf-8', + 'old_text' => "Wiki est l'\xc3\xa9cole superieur !", + ] + ]; + yield 'Utf8Legacy' => [ + "Wiki est l'\xc3\xa9cole superieur !", + 'fr', + 'iso-8859-1', + [ + 'old_flags' => '', + 'old_text' => "Wiki est l'\xe9cole superieur !", + ] + ]; } /** * @covers Revision::getRevisionText + * @dataProvider provideGetRevisionTextWithLegacyEncoding */ - public function testGetRevisionTextUtf8NativeGzip() { - $this->checkPHPExtension( 'zlib' ); + public function testGetRevisionWithLegacyEncoding( $expected, $lang, $encoding, $rowData ) { + $blobStore = $this->getBlobStore(); + $blobStore->setLegacyEncoding( $encoding, Language::factory( $lang ) ); + $this->setService( 'BlobStoreFactory', $this->mockBlobStoreFactory( $blobStore ) ); - $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( + $this->testGetRevisionText( $expected, $rowData ); + } + + public function provideGetRevisionTextWithGzipAndLegacyEncoding() { + /** + * WARNING! + * Do not set the external flag! + * Otherwise, getRevisionText will hit the live database (if ExternalStore is enabled)! + */ + yield 'Utf8NativeGzip' => [ + "Wiki est l'\xc3\xa9cole superieur !", + 'fr', + 'iso-8859-1', + [ + 'old_flags' => 'gzip,utf-8', + 'old_text' => gzdeflate( "Wiki est l'\xc3\xa9cole superieur !" ), + ] + ]; + yield 'Utf8LegacyGzip' => [ "Wiki est l'\xc3\xa9cole superieur !", - Revision::getRevisionText( $row ) ); + 'fr', + 'iso-8859-1', + [ + 'old_flags' => 'gzip', + 'old_text' => gzdeflate( "Wiki est l'\xe9cole superieur !" ), + ] + ]; } /** * @covers Revision::getRevisionText + * @dataProvider provideGetRevisionTextWithGzipAndLegacyEncoding */ - public function testGetRevisionTextUtf8LegacyGzip() { + public function testGetRevisionWithGzipAndLegacyEncoding( $expected, $lang, $encoding, $rowData ) { $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 ) ); + $blobStore = $this->getBlobStore(); + $blobStore->setLegacyEncoding( $encoding, Language::factory( $lang ) ); + $this->setService( 'BlobStoreFactory', $this->mockBlobStoreFactory( $blobStore ) ); + + $this->testGetRevisionText( $expected, $rowData ); } /** @@ -158,7 +588,10 @@ class RevisionTest extends MediaWikiTestCase { */ public function testCompressRevisionTextUtf8Gzip() { $this->checkPHPExtension( 'zlib' ); - $this->setMwGlobals( 'wgCompressRevisions', true ); + + $blobStore = $this->getBlobStore(); + $blobStore->setCompressBlobs( true ); + $this->setService( 'BlobStoreFactory', $this->mockBlobStoreFactory( $blobStore ) ); $row = new stdClass; $row->old_text = "Wiki est l'\xc3\xa9cole superieur !"; @@ -173,293 +606,893 @@ class RevisionTest extends MediaWikiTestCase { Revision::getRevisionText( $row ), "getRevisionText" ); } - # ========================================================================= + /** + * @covers Revision::loadFromTitle + */ + public function testLoadFromTitle() { + $this->setMwGlobals( 'wgCommentTableSchemaMigrationStage', MIGRATION_OLD ); + $this->setMwGlobals( 'wgActorTableSchemaMigrationStage', MIGRATION_OLD ); + $this->overrideMwServices(); + $title = $this->getMockTitle(); + + $conditions = [ + 'rev_id=page_latest', + 'page_namespace' => $title->getNamespace(), + 'page_title' => $title->getDBkey() + ]; + + $row = (object)[ + 'rev_id' => '42', + 'rev_page' => $title->getArticleID(), + 'rev_text_id' => '2', + 'rev_timestamp' => '20171017114835', + 'rev_user_text' => '127.0.0.1', + 'rev_user' => '0', + 'rev_minor_edit' => '0', + 'rev_deleted' => '0', + 'rev_len' => '46', + 'rev_parent_id' => '1', + 'rev_sha1' => 'rdqbbzs3pkhihgbs8qf2q9jsvheag5z', + 'rev_comment_text' => 'Goat Comment!', + 'rev_comment_data' => null, + 'rev_comment_cid' => null, + 'rev_content_format' => 'GOATFORMAT', + 'rev_content_model' => 'GOATMODEL', + ]; + + $db = $this->getMock( IDatabase::class ); + $db->expects( $this->any() ) + ->method( 'getDomainId' ) + ->will( $this->returnValue( wfWikiID() ) ); + $db->expects( $this->once() ) + ->method( 'selectRow' ) + ->with( + $this->equalTo( [ 'revision', 'page', 'user' ] ), + // We don't really care about the fields are they come from the selectField methods + $this->isType( 'array' ), + $this->equalTo( $conditions ), + // Method name + $this->stringContains( 'fetchRevisionRowFromConds' ), + // We don't really care about the options here + $this->isType( 'array' ), + // We don't really care about the join conds are they come from the joinCond methods + $this->isType( 'array' ) + ) + ->willReturn( $row ); + + $revision = Revision::loadFromTitle( $db, $title ); + + $this->assertEquals( $title->getArticleID(), $revision->getTitle()->getArticleID() ); + $this->assertEquals( $row->rev_id, $revision->getId() ); + $this->assertEquals( $row->rev_len, $revision->getSize() ); + $this->assertEquals( $row->rev_sha1, $revision->getSha1() ); + $this->assertEquals( $row->rev_parent_id, $revision->getParentId() ); + $this->assertEquals( $row->rev_timestamp, $revision->getTimestamp() ); + $this->assertEquals( $row->rev_comment_text, $revision->getComment() ); + $this->assertEquals( $row->rev_user_text, $revision->getUserText() ); + } + + public function provideDecompressRevisionText() { + yield '(no legacy encoding), false in false out' => [ false, false, [], false ]; + yield '(no legacy encoding), empty in empty out' => [ false, '', [], '' ]; + yield '(no legacy encoding), empty in empty out' => [ false, 'A', [], 'A' ]; + yield '(no legacy encoding), string in with gzip flag returns string' => [ + // gzip string below generated with gzdeflate( 'AAAABBAAA' ) + false, "sttttr\002\022\000", [ 'gzip' ], 'AAAABBAAA', + ]; + yield '(no legacy encoding), string in with object flag returns false' => [ + // gzip string below generated with serialize( 'JOJO' ) + false, "s:4:\"JOJO\";", [ 'object' ], false, + ]; + yield '(no legacy encoding), serialized object in with object flag returns string' => [ + false, + // Using a TitleValue object as it has a getText method (which is needed) + serialize( new TitleValue( 0, 'HHJJDDFF' ) ), + [ 'object' ], + 'HHJJDDFF', + ]; + yield '(no legacy encoding), serialized object in with object & gzip flag returns string' => [ + false, + // Using a TitleValue object as it has a getText method (which is needed) + gzdeflate( serialize( new TitleValue( 0, '8219JJJ840' ) ) ), + [ 'object', 'gzip' ], + '8219JJJ840', + ]; + yield '(ISO-8859-1 encoding), string in string out' => [ + 'ISO-8859-1', + iconv( 'utf-8', 'ISO-8859-1', "1®Àþ1" ), + [], + '1®Àþ1', + ]; + yield '(ISO-8859-1 encoding), serialized object in with gzip flags returns string' => [ + 'ISO-8859-1', + gzdeflate( iconv( 'utf-8', 'ISO-8859-1', "4®Àþ4" ) ), + [ 'gzip' ], + '4®Àþ4', + ]; + yield '(ISO-8859-1 encoding), serialized object in with object flags returns string' => [ + 'ISO-8859-1', + serialize( new TitleValue( 0, iconv( 'utf-8', 'ISO-8859-1', "3®Àþ3" ) ) ), + [ 'object' ], + '3®Àþ3', + ]; + yield '(ISO-8859-1 encoding), serialized object in with object & gzip flags returns string' => [ + 'ISO-8859-1', + gzdeflate( serialize( new TitleValue( 0, iconv( 'utf-8', 'ISO-8859-1', "2®Àþ2" ) ) ) ), + [ 'gzip', 'object' ], + '2®Àþ2', + ]; + } /** - * @param string $text - * @param string $title - * @param string $model - * @param string $format + * @dataProvider provideDecompressRevisionText + * @covers Revision::decompressRevisionText * - * @return Revision + * @param bool $legacyEncoding + * @param mixed $text + * @param array $flags + * @param mixed $expected */ - function newTestRevision( $text, $title = "Test", - $model = CONTENT_MODEL_WIKITEXT, $format = null - ) { - if ( is_string( $title ) ) { - $title = Title::newFromText( $title ); + public function testDecompressRevisionText( $legacyEncoding, $text, $flags, $expected ) { + $blobStore = $this->getBlobStore(); + if ( $legacyEncoding ) { + $blobStore->setLegacyEncoding( $legacyEncoding, Language::factory( 'en' ) ); } - $content = ContentHandler::makeContent( $text, $title, $model, $format ); + $this->setService( 'BlobStoreFactory', $this->mockBlobStoreFactory( $blobStore ) ); + $this->assertSame( + $expected, + Revision::decompressRevisionText( $text, $flags ) + ); + } - $rev = new Revision( - [ - 'id' => 42, - 'page' => 23, - 'title' => $title, + /** + * @covers Revision::getRevisionText + */ + public function testGetRevisionText_returnsFalseWhenNoTextField() { + $this->assertFalse( Revision::getRevisionText( new stdClass() ) ); + } - 'content' => $content, - 'length' => $content->getSize(), - 'comment' => "testing", - 'minor_edit' => false, + public function provideTestGetRevisionText_returnsDecompressedTextFieldWhenNotExternal() { + yield 'Just text' => [ + (object)[ 'old_text' => 'SomeText' ], + 'old_', + 'SomeText' + ]; + // gzip string below generated with gzdeflate( 'AAAABBAAA' ) + yield 'gzip text' => [ + (object)[ + 'old_text' => "sttttr\002\022\000", + 'old_flags' => 'gzip' + ], + 'old_', + 'AAAABBAAA' + ]; + yield 'gzip text and different prefix' => [ + (object)[ + 'jojo_text' => "sttttr\002\022\000", + 'jojo_flags' => 'gzip' + ], + 'jojo_', + 'AAAABBAAA' + ]; + } - 'content_format' => $format, - ] - ); + /** + * @dataProvider provideTestGetRevisionText_returnsDecompressedTextFieldWhenNotExternal + * @covers Revision::getRevisionText + */ + public function testGetRevisionText_returnsDecompressedTextFieldWhenNotExternal( + $row, + $prefix, + $expected + ) { + $this->assertSame( $expected, Revision::getRevisionText( $row, $prefix ) ); + } - return $rev; + public function provideTestGetRevisionText_external_returnsFalseWhenNotEnoughUrlParts() { + yield 'Just some text' => [ 'someNonUrlText' ]; + yield 'No second URL part' => [ 'someProtocol://' ]; } - function dataGetContentModel() { - // NOTE: we expect the help namespace to always contain wikitext - return [ - [ 'hello world', 'Help:Hello', null, null, CONTENT_MODEL_WIKITEXT ], - [ 'hello world', 'User:hello/there.css', null, null, CONTENT_MODEL_CSS ], - [ serialize( 'hello world' ), 'Dummy:Hello', null, null, "testing" ], - ]; + /** + * @dataProvider provideTestGetRevisionText_external_returnsFalseWhenNotEnoughUrlParts + * @covers Revision::getRevisionText + */ + public function testGetRevisionText_external_returnsFalseWhenNotEnoughUrlParts( + $text + ) { + $this->assertFalse( + Revision::getRevisionText( + (object)[ + 'old_text' => $text, + 'old_flags' => 'external', + ] + ) + ); } /** - * @group Database - * @dataProvider dataGetContentModel - * @covers Revision::getContentModel + * @covers Revision::getRevisionText */ - public function testGetContentModel( $text, $title, $model, $format, $expectedModel ) { - $rev = $this->newTestRevision( $text, $title, $model, $format ); + public function testGetRevisionText_external_noOldId() { + $this->setService( + 'ExternalStoreFactory', + new ExternalStoreFactory( [ 'ForTesting' ] ) + ); + $this->assertSame( + 'AAAABBAAA', + Revision::getRevisionText( + (object)[ + 'old_text' => 'ForTesting://cluster1/12345', + 'old_flags' => 'external,gzip', + ] + ) + ); + } - $this->assertEquals( $expectedModel, $rev->getContentModel() ); + /** + * @covers Revision::getRevisionText + */ + public function testGetRevisionText_external_oldId() { + $cache = $this->getWANObjectCache(); + $this->setService( 'MainWANObjectCache', $cache ); + + $this->setService( + 'ExternalStoreFactory', + new ExternalStoreFactory( [ 'ForTesting' ] ) + ); + + $lb = $this->getMockBuilder( LoadBalancer::class ) + ->disableOriginalConstructor() + ->getMock(); + + $blobStore = new SqlBlobStore( $lb, $cache ); + $this->setService( 'BlobStoreFactory', $this->mockBlobStoreFactory( $blobStore ) ); + + $this->assertSame( + 'AAAABBAAA', + Revision::getRevisionText( + (object)[ + 'old_text' => 'ForTesting://cluster1/12345', + 'old_flags' => 'external,gzip', + 'old_id' => '7777', + ] + ) + ); + + $cacheKey = $cache->makeKey( 'revisiontext', 'textid', 'tt:7777' ); + $this->assertSame( 'AAAABBAAA', $cache->get( $cacheKey ) ); } - function dataGetContentFormat() { - // NOTE: we expect the help namespace to always contain wikitext - return [ - [ 'hello world', 'Help:Hello', null, null, CONTENT_FORMAT_WIKITEXT ], - [ 'hello world', 'Help:Hello', CONTENT_MODEL_CSS, null, CONTENT_FORMAT_CSS ], - [ 'hello world', 'User:hello/there.css', null, null, CONTENT_FORMAT_CSS ], - [ serialize( 'hello world' ), 'Dummy:Hello', null, null, "testing" ], - ]; + /** + * @covers Revision::userJoinCond + */ + public function testUserJoinCond() { + $this->hideDeprecated( 'Revision::userJoinCond' ); + $this->setMwGlobals( 'wgActorTableSchemaMigrationStage', MIGRATION_OLD ); + $this->overrideMwServices(); + $this->assertEquals( + [ 'LEFT JOIN', [ 'rev_user != 0', 'user_id = rev_user' ] ], + Revision::userJoinCond() + ); } /** - * @group Database - * @dataProvider dataGetContentFormat - * @covers Revision::getContentFormat + * @covers Revision::pageJoinCond */ - public function testGetContentFormat( $text, $title, $model, $format, $expectedFormat ) { - $rev = $this->newTestRevision( $text, $title, $model, $format ); + public function testPageJoinCond() { + $this->hideDeprecated( 'Revision::pageJoinCond' ); + $this->assertEquals( + [ 'INNER JOIN', [ 'page_id = rev_page' ] ], + Revision::pageJoinCond() + ); + } - $this->assertEquals( $expectedFormat, $rev->getContentFormat() ); + private function overrideCommentStoreAndActorMigration() { + $mockStore = $this->getMockBuilder( CommentStore::class ) + ->disableOriginalConstructor() + ->getMock(); + $mockStore->expects( $this->any() ) + ->method( 'getFields' ) + ->willReturn( [ 'commentstore' => 'fields' ] ); + $mockStore->expects( $this->any() ) + ->method( 'getJoin' ) + ->willReturn( [ + 'tables' => [ 'commentstore' => 'table' ], + 'fields' => [ 'commentstore' => 'field' ], + 'joins' => [ 'commentstore' => 'join' ], + ] ); + $this->setService( 'CommentStore', $mockStore ); + + $mockStore = $this->getMockBuilder( ActorMigration::class ) + ->disableOriginalConstructor() + ->getMock(); + $mockStore->expects( $this->any() ) + ->method( 'getJoin' ) + ->willReturnCallback( function ( $key ) { + $p = strtok( $key, '_' ); + return [ + 'tables' => [ 'actormigration' => 'table' ], + 'fields' => [ + $p . '_user' => 'actormigration_user', + $p . '_user_text' => 'actormigration_user_text', + $p . '_actor' => 'actormigration_actor', + ], + 'joins' => [ 'actormigration' => 'join' ], + ]; + } ); + $this->setService( 'ActorMigration', $mockStore ); } - function dataGetContentHandler() { - // NOTE: we expect the help namespace to always contain wikitext - return [ - [ 'hello world', 'Help:Hello', null, null, 'WikitextContentHandler' ], - [ 'hello world', 'User:hello/there.css', null, null, 'CssContentHandler' ], - [ serialize( 'hello world' ), 'Dummy:Hello', null, null, 'DummyContentHandlerForTesting' ], + public function provideSelectFields() { + yield [ + true, + [ + 'rev_id', + 'rev_page', + 'rev_text_id', + 'rev_timestamp', + 'rev_user_text', + 'rev_user', + 'rev_actor' => 'NULL', + 'rev_minor_edit', + 'rev_deleted', + 'rev_len', + 'rev_parent_id', + 'rev_sha1', + 'commentstore' => 'fields', + 'rev_content_format', + 'rev_content_model', + ] + ]; + yield [ + false, + [ + 'rev_id', + 'rev_page', + 'rev_text_id', + 'rev_timestamp', + 'rev_user_text', + 'rev_user', + 'rev_actor' => 'NULL', + 'rev_minor_edit', + 'rev_deleted', + 'rev_len', + 'rev_parent_id', + 'rev_sha1', + 'commentstore' => 'fields', + ] ]; } /** - * @group Database - * @dataProvider dataGetContentHandler - * @covers Revision::getContentHandler + * @dataProvider provideSelectFields + * @covers Revision::selectFields */ - public function testGetContentHandler( $text, $title, $model, $format, $expectedClass ) { - $rev = $this->newTestRevision( $text, $title, $model, $format ); - - $this->assertEquals( $expectedClass, get_class( $rev->getContentHandler() ) ); + public function testSelectFields( $contentHandlerUseDB, $expected ) { + $this->hideDeprecated( 'Revision::selectFields' ); + $this->setMwGlobals( 'wgContentHandlerUseDB', $contentHandlerUseDB ); + $this->setMwGlobals( 'wgActorTableSchemaMigrationStage', MIGRATION_OLD ); + $this->overrideCommentStoreAndActorMigration(); + $this->assertEquals( $expected, Revision::selectFields() ); } - function dataGetContent() { - // NOTE: we expect the help namespace to always contain wikitext - return [ - [ 'hello world', 'Help:Hello', null, null, Revision::FOR_PUBLIC, 'hello world' ], + public function provideSelectArchiveFields() { + yield [ + true, [ - serialize( 'hello world' ), - 'Hello', - "testing", - null, - Revision::FOR_PUBLIC, - serialize( 'hello world' ) - ], + 'ar_id', + 'ar_page_id', + 'ar_rev_id', + 'ar_text_id', + 'ar_timestamp', + 'ar_user_text', + 'ar_user', + 'ar_actor' => 'NULL', + 'ar_minor_edit', + 'ar_deleted', + 'ar_len', + 'ar_parent_id', + 'ar_sha1', + 'commentstore' => 'fields', + 'ar_content_format', + 'ar_content_model', + ] + ]; + yield [ + false, [ - serialize( 'hello world' ), - 'Dummy:Hello', - null, - null, - Revision::FOR_PUBLIC, - serialize( 'hello world' ) - ], + 'ar_id', + 'ar_page_id', + 'ar_rev_id', + 'ar_text_id', + 'ar_timestamp', + 'ar_user_text', + 'ar_user', + 'ar_actor' => 'NULL', + 'ar_minor_edit', + 'ar_deleted', + 'ar_len', + 'ar_parent_id', + 'ar_sha1', + 'commentstore' => 'fields', + ] ]; } /** - * @group Database - * @dataProvider dataGetContent - * @covers Revision::getContent + * @dataProvider provideSelectArchiveFields + * @covers Revision::selectArchiveFields */ - public function testGetContent( $text, $title, $model, $format, - $audience, $expectedSerialization - ) { - $rev = $this->newTestRevision( $text, $title, $model, $format ); - $content = $rev->getContent( $audience ); + public function testSelectArchiveFields( $contentHandlerUseDB, $expected ) { + $this->hideDeprecated( 'Revision::selectArchiveFields' ); + $this->setMwGlobals( 'wgContentHandlerUseDB', $contentHandlerUseDB ); + $this->setMwGlobals( 'wgActorTableSchemaMigrationStage', MIGRATION_OLD ); + $this->overrideCommentStoreAndActorMigration(); + $this->assertEquals( $expected, Revision::selectArchiveFields() ); + } + /** + * @covers Revision::selectTextFields + */ + public function testSelectTextFields() { + $this->hideDeprecated( 'Revision::selectTextFields' ); $this->assertEquals( - $expectedSerialization, - is_null( $content ) ? null : $content->serialize( $format ) + [ + 'old_text', + 'old_flags', + ], + Revision::selectTextFields() ); } - public function dataGetSize() { - return [ - [ "hello world.", CONTENT_MODEL_WIKITEXT, 12 ], - [ serialize( "hello world." ), "testing", 12 ], - ]; + /** + * @covers Revision::selectPageFields + */ + public function testSelectPageFields() { + $this->hideDeprecated( 'Revision::selectPageFields' ); + $this->assertEquals( + [ + 'page_namespace', + 'page_title', + 'page_id', + 'page_latest', + 'page_is_redirect', + 'page_len', + ], + Revision::selectPageFields() + ); } /** - * @covers Revision::getSize - * @group Database - * @dataProvider dataGetSize + * @covers Revision::selectUserFields */ - public function testGetSize( $text, $model, $expected_size ) { - $rev = $this->newTestRevision( $text, 'RevisionTest_testGetSize', $model ); - $this->assertEquals( $expected_size, $rev->getSize() ); + public function testSelectUserFields() { + $this->hideDeprecated( 'Revision::selectUserFields' ); + $this->assertEquals( + [ + 'user_name', + ], + Revision::selectUserFields() + ); } - public function dataGetSha1() { - return [ - [ "hello world.", CONTENT_MODEL_WIKITEXT, Revision::base36Sha1( "hello world." ) ], + public function provideGetArchiveQueryInfo() { + yield 'wgContentHandlerUseDB false' => [ [ - serialize( "hello world." ), - "testing", - Revision::base36Sha1( serialize( "hello world." ) ) + 'wgContentHandlerUseDB' => false, ], + [ + 'tables' => [ + 'archive', + 'commentstore' => 'table', + 'actormigration' => 'table', + ], + 'fields' => [ + 'ar_id', + 'ar_page_id', + 'ar_namespace', + 'ar_title', + 'ar_rev_id', + 'ar_text_id', + 'ar_timestamp', + 'ar_minor_edit', + 'ar_deleted', + 'ar_len', + 'ar_parent_id', + 'ar_sha1', + 'commentstore' => 'field', + 'ar_user' => 'actormigration_user', + 'ar_user_text' => 'actormigration_user_text', + 'ar_actor' => 'actormigration_actor', + ], + 'joins' => [ 'commentstore' => 'join', 'actormigration' => 'join' ], + ] + ]; + yield 'wgContentHandlerUseDB true' => [ + [ + 'wgContentHandlerUseDB' => true, + ], + [ + 'tables' => [ + 'archive', + 'commentstore' => 'table', + 'actormigration' => 'table', + ], + 'fields' => [ + 'ar_id', + 'ar_page_id', + 'ar_namespace', + 'ar_title', + 'ar_rev_id', + 'ar_text_id', + 'ar_timestamp', + 'ar_minor_edit', + 'ar_deleted', + 'ar_len', + 'ar_parent_id', + 'ar_sha1', + 'commentstore' => 'field', + 'ar_user' => 'actormigration_user', + 'ar_user_text' => 'actormigration_user_text', + 'ar_actor' => 'actormigration_actor', + 'ar_content_format', + 'ar_content_model', + ], + 'joins' => [ 'commentstore' => 'join', 'actormigration' => 'join' ], + ] ]; } /** - * @covers Revision::getSha1 - * @group Database - * @dataProvider dataGetSha1 + * @covers Revision::getArchiveQueryInfo + * @dataProvider provideGetArchiveQueryInfo */ - public function testGetSha1( $text, $model, $expected_hash ) { - $rev = $this->newTestRevision( $text, 'RevisionTest_testGetSha1', $model ); - $this->assertEquals( $expected_hash, $rev->getSha1() ); + public function testGetArchiveQueryInfo( $globals, $expected ) { + $this->setMwGlobals( $globals ); + $this->overrideCommentStoreAndActorMigration(); + + $revisionStore = $this->getRevisionStore(); + $revisionStore->setContentHandlerUseDB( $globals['wgContentHandlerUseDB'] ); + $this->setService( 'RevisionStore', $revisionStore ); + $this->assertEquals( + $expected, + Revision::getArchiveQueryInfo() + ); + } + + public function provideGetQueryInfo() { + yield 'wgContentHandlerUseDB false, opts none' => [ + [ + 'wgContentHandlerUseDB' => false, + ], + [], + [ + 'tables' => [ 'revision', 'commentstore' => 'table', 'actormigration' => 'table' ], + 'fields' => [ + 'rev_id', + 'rev_page', + 'rev_text_id', + 'rev_timestamp', + 'rev_minor_edit', + 'rev_deleted', + 'rev_len', + 'rev_parent_id', + 'rev_sha1', + 'commentstore' => 'field', + 'rev_user' => 'actormigration_user', + 'rev_user_text' => 'actormigration_user_text', + 'rev_actor' => 'actormigration_actor', + ], + 'joins' => [ 'commentstore' => 'join', 'actormigration' => 'join' ], + ], + ]; + yield 'wgContentHandlerUseDB false, opts page' => [ + [ + 'wgContentHandlerUseDB' => false, + ], + [ 'page' ], + [ + 'tables' => [ 'revision', 'commentstore' => 'table', 'actormigration' => 'table', 'page' ], + 'fields' => [ + 'rev_id', + 'rev_page', + 'rev_text_id', + 'rev_timestamp', + 'rev_minor_edit', + 'rev_deleted', + 'rev_len', + 'rev_parent_id', + 'rev_sha1', + 'commentstore' => 'field', + 'rev_user' => 'actormigration_user', + 'rev_user_text' => 'actormigration_user_text', + 'rev_actor' => 'actormigration_actor', + 'page_namespace', + 'page_title', + 'page_id', + 'page_latest', + 'page_is_redirect', + 'page_len', + ], + 'joins' => [ + 'page' => [ + 'INNER JOIN', + [ 'page_id = rev_page' ], + ], + 'commentstore' => 'join', + 'actormigration' => 'join', + ], + ], + ]; + yield 'wgContentHandlerUseDB false, opts user' => [ + [ + 'wgContentHandlerUseDB' => false, + ], + [ 'user' ], + [ + 'tables' => [ 'revision', 'commentstore' => 'table', 'actormigration' => 'table', 'user' ], + 'fields' => [ + 'rev_id', + 'rev_page', + 'rev_text_id', + 'rev_timestamp', + 'rev_minor_edit', + 'rev_deleted', + 'rev_len', + 'rev_parent_id', + 'rev_sha1', + 'commentstore' => 'field', + 'rev_user' => 'actormigration_user', + 'rev_user_text' => 'actormigration_user_text', + 'rev_actor' => 'actormigration_actor', + 'user_name', + ], + 'joins' => [ + 'user' => [ + 'LEFT JOIN', + [ + 'actormigration_user != 0', + 'user_id = actormigration_user', + ], + ], + 'commentstore' => 'join', + 'actormigration' => 'join', + ], + ], + ]; + yield 'wgContentHandlerUseDB false, opts text' => [ + [ + 'wgContentHandlerUseDB' => false, + ], + [ 'text' ], + [ + 'tables' => [ 'revision', 'commentstore' => 'table', 'actormigration' => 'table', 'text' ], + 'fields' => [ + 'rev_id', + 'rev_page', + 'rev_text_id', + 'rev_timestamp', + 'rev_minor_edit', + 'rev_deleted', + 'rev_len', + 'rev_parent_id', + 'rev_sha1', + 'commentstore' => 'field', + 'rev_user' => 'actormigration_user', + 'rev_user_text' => 'actormigration_user_text', + 'rev_actor' => 'actormigration_actor', + 'old_text', + 'old_flags', + ], + 'joins' => [ + 'text' => [ + 'INNER JOIN', + [ 'rev_text_id=old_id' ], + ], + 'commentstore' => 'join', + 'actormigration' => 'join', + ], + ], + ]; + yield 'wgContentHandlerUseDB false, opts 3' => [ + [ + 'wgContentHandlerUseDB' => false, + ], + [ 'text', 'page', 'user' ], + [ + 'tables' => [ + 'revision', 'commentstore' => 'table', 'actormigration' => 'table', 'page', 'user', 'text' + ], + 'fields' => [ + 'rev_id', + 'rev_page', + 'rev_text_id', + 'rev_timestamp', + 'rev_minor_edit', + 'rev_deleted', + 'rev_len', + 'rev_parent_id', + 'rev_sha1', + 'commentstore' => 'field', + 'rev_user' => 'actormigration_user', + 'rev_user_text' => 'actormigration_user_text', + 'rev_actor' => 'actormigration_actor', + 'page_namespace', + 'page_title', + 'page_id', + 'page_latest', + 'page_is_redirect', + 'page_len', + 'user_name', + 'old_text', + 'old_flags', + ], + 'joins' => [ + 'page' => [ + 'INNER JOIN', + [ 'page_id = rev_page' ], + ], + 'user' => [ + 'LEFT JOIN', + [ + 'actormigration_user != 0', + 'user_id = actormigration_user', + ], + ], + 'text' => [ + 'INNER JOIN', + [ 'rev_text_id=old_id' ], + ], + 'commentstore' => 'join', + 'actormigration' => 'join', + ], + ], + ]; + yield 'wgContentHandlerUseDB true, opts none' => [ + [ + 'wgContentHandlerUseDB' => true, + ], + [], + [ + 'tables' => [ 'revision', 'commentstore' => 'table', 'actormigration' => 'table' ], + 'fields' => [ + 'rev_id', + 'rev_page', + 'rev_text_id', + 'rev_timestamp', + 'rev_minor_edit', + 'rev_deleted', + 'rev_len', + 'rev_parent_id', + 'rev_sha1', + 'commentstore' => 'field', + 'rev_user' => 'actormigration_user', + 'rev_user_text' => 'actormigration_user_text', + 'rev_actor' => 'actormigration_actor', + 'rev_content_format', + 'rev_content_model', + ], + 'joins' => [ 'commentstore' => 'join', 'actormigration' => 'join' ], + ], + ]; } /** - * @covers Revision::__construct + * @covers Revision::getQueryInfo + * @dataProvider provideGetQueryInfo */ - public function testConstructWithText() { - $rev = new Revision( [ - 'text' => 'hello world.', - 'content_model' => CONTENT_MODEL_JAVASCRIPT - ] ); + public function testGetQueryInfo( $globals, $options, $expected ) { + $this->setMwGlobals( $globals ); + $this->overrideCommentStoreAndActorMigration(); - $this->assertNotNull( $rev->getContent(), 'no content object available' ); - $this->assertEquals( CONTENT_MODEL_JAVASCRIPT, $rev->getContent()->getModel() ); - $this->assertEquals( CONTENT_MODEL_JAVASCRIPT, $rev->getContentModel() ); + $revisionStore = $this->getRevisionStore(); + $revisionStore->setContentHandlerUseDB( $globals['wgContentHandlerUseDB'] ); + $this->setService( 'RevisionStore', $revisionStore ); + + $this->assertEquals( + $expected, + Revision::getQueryInfo( $options ) + ); } /** - * @covers Revision::__construct + * @covers Revision::getSize */ - public function testConstructWithContent() { - $title = Title::newFromText( 'RevisionTest_testConstructWithContent' ); + public function testGetSize() { + $title = $this->getMockTitle(); - $rev = new Revision( [ - 'content' => ContentHandler::makeContent( 'hello world.', $title, CONTENT_MODEL_JAVASCRIPT ), - ] ); + $rec = new MutableRevisionRecord( $title ); + $rev = new Revision( $rec, 0, $title ); - $this->assertNotNull( $rev->getContent(), 'no content object available' ); - $this->assertEquals( CONTENT_MODEL_JAVASCRIPT, $rev->getContent()->getModel() ); - $this->assertEquals( CONTENT_MODEL_JAVASCRIPT, $rev->getContentModel() ); + $this->assertSame( 0, $rev->getSize(), 'Size of no slots is 0' ); + + $rec->setSize( 13 ); + $this->assertSame( 13, $rev->getSize() ); } /** - * Tests whether $rev->getContent() returns a clone when needed. - * - * @group Database - * @covers Revision::getContent + * @covers Revision::getSize */ - public function testGetContentClone() { - $content = new RevisionTestModifyableContent( "foo" ); + public function testGetSize_failure() { + $title = $this->getMockTitle(); - $rev = new Revision( - [ - 'id' => 42, - 'page' => 23, - 'title' => Title::newFromText( "testGetContentClone_dummy" ), + $rec = $this->getMockBuilder( RevisionRecord::class ) + ->disableOriginalConstructor() + ->getMock(); - 'content' => $content, - 'length' => $content->getSize(), - 'comment' => "testing", - 'minor_edit' => false, - ] - ); + $rec->method( 'getSize' ) + ->willThrowException( new RevisionAccessException( 'Oops!' ) ); - $content = $rev->getContent( Revision::RAW ); - $content->setText( "bar" ); + $rev = new Revision( $rec, 0, $title ); + $this->assertNull( $rev->getSize() ); + } - $content2 = $rev->getContent( Revision::RAW ); - // content is mutable, expect clone - $this->assertNotSame( $content, $content2, "expected a clone" ); - // clone should contain the original text - $this->assertEquals( "foo", $content2->getText() ); + /** + * @covers Revision::getSha1 + */ + public function testGetSha1() { + $title = $this->getMockTitle(); + + $rec = new MutableRevisionRecord( $title ); + $rev = new Revision( $rec, 0, $title ); + + $emptyHash = SlotRecord::base36Sha1( '' ); + $this->assertSame( $emptyHash, $rev->getSha1(), 'Sha1 of no slots is hash of empty string' ); - $content2->setText( "bla bla" ); - $this->assertEquals( "bar", $content->getText() ); // clones should be independent + $rec->setSha1( 'deadbeef' ); + $this->assertSame( 'deadbeef', $rev->getSha1() ); } /** - * Tests whether $rev->getContent() returns the same object repeatedly if appropriate. - * - * @group Database - * @covers Revision::getContent + * @covers Revision::getSha1 */ - public function testGetContentUncloned() { - $rev = $this->newTestRevision( "hello", "testGetContentUncloned_dummy", CONTENT_MODEL_WIKITEXT ); - $content = $rev->getContent( Revision::RAW ); - $content2 = $rev->getContent( Revision::RAW ); + public function testGetSha1_failure() { + $title = $this->getMockTitle(); - // for immutable content like wikitext, this should be the same object - $this->assertSame( $content, $content2 ); - } -} + $rec = $this->getMockBuilder( RevisionRecord::class ) + ->disableOriginalConstructor() + ->getMock(); -class RevisionTestModifyableContent extends TextContent { - public function __construct( $text ) { - parent::__construct( $text, "RevisionTestModifyableContent" ); - } + $rec->method( 'getSha1' ) + ->willThrowException( new RevisionAccessException( 'Oops!' ) ); - public function copy() { - return new RevisionTestModifyableContent( $this->mText ); + $rev = new Revision( $rec, 0, $title ); + $this->assertNull( $rev->getSha1() ); } - public function getText() { - return $this->mText; - } + /** + * @covers Revision::getContent + */ + public function testGetContent() { + $title = $this->getMockTitle(); - public function setText( $text ) { - $this->mText = $text; - } -} + $rec = new MutableRevisionRecord( $title ); + $rev = new Revision( $rec, 0, $title ); -class RevisionTestModifyableContentHandler extends TextContentHandler { + $this->assertNull( $rev->getContent(), 'Content of no slots is null' ); - public function __construct() { - parent::__construct( "RevisionTestModifyableContent", [ CONTENT_FORMAT_TEXT ] ); + $content = new TextContent( 'Hello Kittens!' ); + $rec->setContent( 'main', $content ); + $this->assertSame( $content, $rev->getContent() ); } - public function unserializeContent( $text, $format = null ) { - $this->checkFormat( $format ); + /** + * @covers Revision::getContent + */ + public function testGetContent_failure() { + $title = $this->getMockTitle(); + + $rec = $this->getMockBuilder( RevisionRecord::class ) + ->disableOriginalConstructor() + ->getMock(); - return new RevisionTestModifyableContent( $text ); - } + $rec->method( 'getContent' ) + ->willThrowException( new RevisionAccessException( 'Oops!' ) ); - public function makeEmptyContent() { - return new RevisionTestModifyableContent( '' ); + $rev = new Revision( $rec, 0, $title ); + $this->assertNull( $rev->getContent() ); } + } diff --git a/www/wiki/tests/phpunit/includes/RevisionTestModifyableContent.php b/www/wiki/tests/phpunit/includes/RevisionTestModifyableContent.php new file mode 100644 index 00000000..6dcba53c --- /dev/null +++ b/www/wiki/tests/phpunit/includes/RevisionTestModifyableContent.php @@ -0,0 +1,23 @@ +<?php + +class RevisionTestModifyableContent extends TextContent { + + const MODEL_ID = "RevisionTestModifyableContent"; + + public function __construct( $text ) { + parent::__construct( $text, self::MODEL_ID ); + } + + public function copy() { + return new RevisionTestModifyableContent( $this->mText ); + } + + public function getText() { + return $this->mText; + } + + public function setText( $text ) { + $this->mText = $text; + } + +} diff --git a/www/wiki/tests/phpunit/includes/RevisionTestModifyableContentHandler.php b/www/wiki/tests/phpunit/includes/RevisionTestModifyableContentHandler.php new file mode 100644 index 00000000..bc4e40a4 --- /dev/null +++ b/www/wiki/tests/phpunit/includes/RevisionTestModifyableContentHandler.php @@ -0,0 +1,19 @@ +<?php + +class RevisionTestModifyableContentHandler extends TextContentHandler { + + public function __construct() { + parent::__construct( RevisionTestModifyableContent::MODEL_ID, [ 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/www/wiki/tests/phpunit/includes/SampleTest.php b/www/wiki/tests/phpunit/includes/SampleTest.php index 02935a53..3d74ae3e 100644 --- a/www/wiki/tests/phpunit/includes/SampleTest.php +++ b/www/wiki/tests/phpunit/includes/SampleTest.php @@ -1,6 +1,6 @@ <?php -class TestSample extends MediaWikiLangTestCase { +class SampleTest extends MediaWikiLangTestCase { /** * Anything that needs to happen before your tests should go here. @@ -36,7 +36,7 @@ class TestSample extends MediaWikiLangTestCase { */ public function testTitleObjectStringConversion() { $title = Title::newFromText( "text" ); - $this->assertInstanceOf( 'Title', $title, "Title creation" ); + $this->assertInstanceOf( Title::class, $title, "Title creation" ); $this->assertEquals( "Text", $title, "Automatic string conversion" ); $title = Title::newFromText( "text", NS_MEDIA ); @@ -57,12 +57,12 @@ class TestSample extends MediaWikiLangTestCase { ]; } - // @codingStandardsIgnoreStart Generic.Files.LineLength /** + * phpcs:disable Generic.Files.LineLength * @dataProvider provideTitles * See https://phpunit.de/manual/3.7/en/appendixes.annotations.html#appendixes.annotations.dataProvider + * phpcs:enable */ - // @codingStandardsIgnoreEnd public function testCreateBasicListOfTitles( $titleName, $ns, $text ) { $title = Title::newFromText( $titleName, $ns ); $this->assertEquals( $text, "$title", "see if '$titleName' matches '$text'" ); @@ -95,12 +95,10 @@ class TestSample extends MediaWikiLangTestCase { $this->assertTrue( $title->isLocal() ); } - // @codingStandardsIgnoreStart Generic.Files.LineLength /** * @expectedException InvalidArgumentException * See https://phpunit.de/manual/3.7/en/appendixes.annotations.html#appendixes.annotations.expectedException */ - // @codingStandardsIgnoreEnd public function testTitleObjectFromObject() { $title = Title::newFromText( Title::newFromText( "test" ) ); $this->assertEquals( "Test", $title->isLocal() ); diff --git a/www/wiki/tests/phpunit/includes/SanitizerValidateEmailTest.php b/www/wiki/tests/phpunit/includes/SanitizerValidateEmailTest.php index 24485133..c4e43084 100644 --- a/www/wiki/tests/phpunit/includes/SanitizerValidateEmailTest.php +++ b/www/wiki/tests/phpunit/includes/SanitizerValidateEmailTest.php @@ -5,7 +5,9 @@ * @todo all test methods in this class should be refactored and... * use a single test method and a single data provider... */ -class SanitizerValidateEmailTest extends PHPUnit_Framework_TestCase { +class SanitizerValidateEmailTest extends PHPUnit\Framework\TestCase { + + use MediaWikiCoversValidator; private function checkEmail( $addr, $expected = true, $msg = '' ) { if ( $msg == '' ) { diff --git a/www/wiki/tests/phpunit/includes/SiteStatsTest.php b/www/wiki/tests/phpunit/includes/SiteStatsTest.php index cdbf9fd9..56bde5da 100644 --- a/www/wiki/tests/phpunit/includes/SiteStatsTest.php +++ b/www/wiki/tests/phpunit/includes/SiteStatsTest.php @@ -11,9 +11,10 @@ class SiteStatsTest extends MediaWikiTestCase { $cache = \MediaWiki\MediaWikiServices::getInstance()->getMainWANObjectCache(); $jobq = JobQueueGroup::singleton(); - // Delete EditPage jobs that might have been left behind by other tests + // Delete jobs that might have been left behind by other tests $jobq->get( 'htmlCacheUpdate' )->delete(); $jobq->get( 'recentChangesUpdate' )->delete(); + $jobq->get( 'userGroupExpiry' )->delete(); $cache->delete( $cache->makeKey( 'SiteStats', 'jobscount' ) ); $jobq->push( new NullJob( Title::newMainPage(), [] ) ); diff --git a/www/wiki/tests/phpunit/includes/StatusTest.php b/www/wiki/tests/phpunit/includes/StatusTest.php index 7e56ebf2..6e62afdd 100644 --- a/www/wiki/tests/phpunit/includes/StatusTest.php +++ b/www/wiki/tests/phpunit/includes/StatusTest.php @@ -5,11 +5,6 @@ */ class StatusTest extends MediaWikiLangTestCase { - public function testCanConstruct() { - new Status(); - $this->assertTrue( true ); - } - /** * @dataProvider provideValues * @covers Status::newGood @@ -35,7 +30,7 @@ class StatusTest extends MediaWikiLangTestCase { * @covers Status::newFatal */ public function testNewFatalWithMessage() { - $message = $this->getMockBuilder( 'Message' ) + $message = $this->getMockBuilder( Message::class ) ->disableOriginalConstructor() ->getMock(); @@ -229,7 +224,7 @@ class StatusTest extends MediaWikiLangTestCase { } protected function getMockMessage( $key = 'key', $params = [] ) { - $message = $this->getMockBuilder( 'Message' ) + $message = $this->getMockBuilder( Message::class ) ->disableOriginalConstructor() ->getMock(); $message->expects( $this->atLeastOnce() ) @@ -316,7 +311,7 @@ class StatusTest extends MediaWikiLangTestCase { * @covers Status::cleanParams */ public function testCleanParams( $cleanCallback, $params, $expected ) { - $method = new ReflectionMethod( 'Status', 'cleanParams' ); + $method = new ReflectionMethod( Status::class, 'cleanParams' ); $method->setAccessible( true ); $status = new Status(); $status->cleanCallback = $cleanCallback; @@ -406,8 +401,8 @@ class StatusTest extends MediaWikiLangTestCase { $status, "* ⧼fooBar!⧽\n* ⧼fooBar2!⧽\n", "(wrap-long: * (fooBar!)\n* (fooBar2!)\n)", - "<ul><li> ⧼fooBar!⧽</li>\n<li> ⧼fooBar2!⧽</li></ul>\n", - "<p>(wrap-long: * (fooBar!)\n</p>\n<ul><li> (fooBar2!)</li></ul>\n<p>)\n</p>", + "<ul><li>⧼fooBar!⧽</li>\n<li>⧼fooBar2!⧽</li></ul>\n", + "<p>(wrap-long: * (fooBar!)\n</p>\n<ul><li>(fooBar2!)</li></ul>\n<p>)\n</p>", ]; $status = new Status(); @@ -427,8 +422,8 @@ class StatusTest extends MediaWikiLangTestCase { $status, "* ⧼fooBar!⧽\n* ⧼fooBar2!⧽\n", "(wrap-long: * (fooBar!: foo, bar)\n* (fooBar2!)\n)", - "<ul><li> ⧼fooBar!⧽</li>\n<li> ⧼fooBar2!⧽</li></ul>\n", - "<p>(wrap-long: * (fooBar!: foo, bar)\n</p>\n<ul><li> (fooBar2!)</li></ul>\n<p>)\n</p>", + "<ul><li>⧼fooBar!⧽</li>\n<li>⧼fooBar2!⧽</li></ul>\n", + "<p>(wrap-long: * (fooBar!: foo, bar)\n</p>\n<ul><li>(fooBar2!)</li></ul>\n<p>)\n</p>", ]; return $testCases; @@ -454,23 +449,23 @@ class StatusTest extends MediaWikiLangTestCase { Status $status, $expectedParams = [], $expectedKey, $expectedWrapper ) { $message = $status->getMessage( null, null, 'qqx' ); - $this->assertInstanceOf( 'Message', $message ); + $this->assertInstanceOf( Message::class, $message ); $this->assertEquals( $expectedParams, self::sanitizedMessageParams( $message ), 'Message::getParams' ); $this->assertEquals( $expectedKey, $message->getKey(), 'Message::getKey' ); $message = $status->getMessage( 'wrapper-short', 'wrapper-long' ); - $this->assertInstanceOf( 'Message', $message ); + $this->assertInstanceOf( Message::class, $message ); $this->assertEquals( $expectedWrapper, $message->getKey(), 'Message::getKey with wrappers' ); $this->assertCount( 1, $message->getParams(), 'Message::getParams with wrappers' ); $message = $status->getMessage( 'wrapper' ); - $this->assertInstanceOf( 'Message', $message ); + $this->assertInstanceOf( Message::class, $message ); $this->assertEquals( 'wrapper', $message->getKey(), 'Message::getKey with wrappers' ); $this->assertCount( 1, $message->getParams(), 'Message::getParams with wrappers' ); $message = $status->getMessage( false, 'wrapper' ); - $this->assertInstanceOf( 'Message', $message ); + $this->assertInstanceOf( Message::class, $message ); $this->assertEquals( 'wrapper', $message->getKey(), 'Message::getKey with wrappers' ); $this->assertCount( 1, $message->getParams(), 'Message::getParams with wrappers' ); } @@ -565,7 +560,7 @@ class StatusTest extends MediaWikiLangTestCase { * @covers Status::getErrorMessage */ public function testGetErrorMessage() { - $method = new ReflectionMethod( 'Status', 'getErrorMessage' ); + $method = new ReflectionMethod( Status::class, 'getErrorMessage' ); $method->setAccessible( true ); $status = new Status(); $key = 'foo'; @@ -573,7 +568,7 @@ class StatusTest extends MediaWikiLangTestCase { /** @var Message $message */ $message = $method->invoke( $status, array_merge( [ $key ], $params ) ); - $this->assertInstanceOf( 'Message', $message ); + $this->assertInstanceOf( Message::class, $message ); $this->assertEquals( $key, $message->getKey() ); $this->assertEquals( $params, $message->getParams() ); } @@ -582,7 +577,7 @@ class StatusTest extends MediaWikiLangTestCase { * @covers Status::getErrorMessageArray */ public function testGetErrorMessageArray() { - $method = new ReflectionMethod( 'Status', 'getErrorMessageArray' ); + $method = new ReflectionMethod( Status::class, 'getErrorMessageArray' ); $method->setAccessible( true ); $status = new Status(); $key = 'foo'; @@ -600,7 +595,7 @@ class StatusTest extends MediaWikiLangTestCase { $this->assertInternalType( 'array', $messageArray ); $this->assertCount( 2, $messageArray ); foreach ( $messageArray as $message ) { - $this->assertInstanceOf( 'Message', $message ); + $this->assertInstanceOf( Message::class, $message ); $this->assertEquals( $key, $message->getKey() ); $this->assertEquals( $params, $message->getParams() ); } diff --git a/www/wiki/tests/phpunit/includes/Storage/BlobStoreFactoryTest.php b/www/wiki/tests/phpunit/includes/Storage/BlobStoreFactoryTest.php new file mode 100644 index 00000000..252c6578 --- /dev/null +++ b/www/wiki/tests/phpunit/includes/Storage/BlobStoreFactoryTest.php @@ -0,0 +1,46 @@ +<?php + +namespace MediaWiki\Tests\Storage; + +use MediaWiki\MediaWikiServices; +use MediaWiki\Storage\BlobStore; +use MediaWiki\Storage\SqlBlobStore; +use MediaWikiTestCase; +use Wikimedia\TestingAccessWrapper; + +/** + * @covers \MediaWiki\Storage\BlobStoreFactory + */ +class BlobStoreFactoryTest extends MediaWikiTestCase { + + public function provideWikiIds() { + yield [ false ]; + yield [ 'someWiki' ]; + } + + /** + * @dataProvider provideWikiIds + */ + public function testNewBlobStore( $wikiId ) { + $factory = MediaWikiServices::getInstance()->getBlobStoreFactory(); + $store = $factory->newBlobStore( $wikiId ); + $this->assertInstanceOf( BlobStore::class, $store ); + + // This only works as we currently know this is a SqlBlobStore object + $wrapper = TestingAccessWrapper::newFromObject( $store ); + $this->assertEquals( $wikiId, $wrapper->wikiId ); + } + + /** + * @dataProvider provideWikiIds + */ + public function testNewSqlBlobStore( $wikiId ) { + $factory = MediaWikiServices::getInstance()->getBlobStoreFactory(); + $store = $factory->newSqlBlobStore( $wikiId ); + $this->assertInstanceOf( SqlBlobStore::class, $store ); + + $wrapper = TestingAccessWrapper::newFromObject( $store ); + $this->assertEquals( $wikiId, $wrapper->wikiId ); + } + +} diff --git a/www/wiki/tests/phpunit/includes/Storage/MutableRevisionRecordTest.php b/www/wiki/tests/phpunit/includes/Storage/MutableRevisionRecordTest.php new file mode 100644 index 00000000..dd2c4b68 --- /dev/null +++ b/www/wiki/tests/phpunit/includes/Storage/MutableRevisionRecordTest.php @@ -0,0 +1,212 @@ +<?php + +namespace MediaWiki\Tests\Storage; + +use CommentStoreComment; +use InvalidArgumentException; +use MediaWiki\Storage\MutableRevisionRecord; +use MediaWiki\Storage\RevisionAccessException; +use MediaWiki\Storage\RevisionRecord; +use MediaWiki\Storage\SlotRecord; +use MediaWiki\User\UserIdentityValue; +use MediaWikiTestCase; +use TextContent; +use Title; +use WikitextContent; + +/** + * @covers \MediaWiki\Storage\MutableRevisionRecord + * @covers \MediaWiki\Storage\RevisionRecord + */ +class MutableRevisionRecordTest extends MediaWikiTestCase { + + use RevisionRecordTests; + + /** + * @param array $rowOverrides + * + * @return MutableRevisionRecord + */ + protected function newRevision( array $rowOverrides = [] ) { + $title = Title::newFromText( 'Dummy' ); + $title->resetArticleID( 17 ); + + $user = new UserIdentityValue( 11, 'Tester', 0 ); + $comment = CommentStoreComment::newUnsavedComment( 'Hello World' ); + + $record = new MutableRevisionRecord( $title ); + + if ( isset( $rowOverrides['rev_deleted'] ) ) { + $record->setVisibility( $rowOverrides['rev_deleted'] ); + } + + if ( isset( $rowOverrides['rev_id'] ) ) { + $record->setId( $rowOverrides['rev_id'] ); + } + + if ( isset( $rowOverrides['rev_page'] ) ) { + $record->setPageId( $rowOverrides['rev_page'] ); + } + + $record->setContent( 'main', new TextContent( 'Lorem Ipsum' ) ); + $record->setComment( $comment ); + $record->setUser( $user ); + + return $record; + } + + public function provideConstructor() { + $title = Title::newFromText( 'Dummy' ); + $title->resetArticleID( 17 ); + + yield [ + $title, + 'acmewiki' + ]; + } + + /** + * @dataProvider provideConstructor + * + * @param Title $title + * @param bool $wikiId + */ + public function testConstructorAndGetters( + Title $title, + $wikiId = false + ) { + $rec = new MutableRevisionRecord( $title, $wikiId ); + + $this->assertSame( $title, $rec->getPageAsLinkTarget(), 'getPageAsLinkTarget' ); + $this->assertSame( $wikiId, $rec->getWikiId(), 'getWikiId' ); + } + + public function provideConstructorFailure() { + $title = Title::newFromText( 'Dummy' ); + $title->resetArticleID( 17 ); + + yield 'not a wiki id' => [ + $title, + null + ]; + } + + /** + * @dataProvider provideConstructorFailure + * + * @param Title $title + * @param bool $wikiId + */ + public function testConstructorFailure( + Title $title, + $wikiId = false + ) { + $this->setExpectedException( InvalidArgumentException::class ); + new MutableRevisionRecord( $title, $wikiId ); + } + + public function testSetGetId() { + $record = new MutableRevisionRecord( Title::newFromText( 'Foo' ) ); + $this->assertNull( $record->getId() ); + $record->setId( 888 ); + $this->assertSame( 888, $record->getId() ); + } + + public function testSetGetUser() { + $record = new MutableRevisionRecord( Title::newFromText( 'Foo' ) ); + $user = $this->getTestSysop()->getUser(); + $this->assertNull( $record->getUser() ); + $record->setUser( $user ); + $this->assertSame( $user, $record->getUser() ); + } + + public function testSetGetPageId() { + $record = new MutableRevisionRecord( Title::newFromText( 'Foo' ) ); + $this->assertSame( 0, $record->getPageId() ); + $record->setPageId( 999 ); + $this->assertSame( 999, $record->getPageId() ); + } + + public function testSetGetParentId() { + $record = new MutableRevisionRecord( Title::newFromText( 'Foo' ) ); + $this->assertNull( $record->getParentId() ); + $record->setParentId( 100 ); + $this->assertSame( 100, $record->getParentId() ); + } + + public function testGetMainContentWhenEmpty() { + $record = new MutableRevisionRecord( Title::newFromText( 'Foo' ) ); + $this->setExpectedException( RevisionAccessException::class ); + $this->assertNull( $record->getContent( 'main' ) ); + } + + public function testSetGetMainContent() { + $record = new MutableRevisionRecord( Title::newFromText( 'Foo' ) ); + $content = new WikitextContent( 'Badger' ); + $record->setContent( 'main', $content ); + $this->assertSame( $content, $record->getContent( 'main' ) ); + } + + public function testGetSlotWhenEmpty() { + $record = new MutableRevisionRecord( Title::newFromText( 'Foo' ) ); + $this->assertFalse( $record->hasSlot( 'main' ) ); + + $this->setExpectedException( RevisionAccessException::class ); + $record->getSlot( 'main' ); + } + + public function testSetGetSlot() { + $record = new MutableRevisionRecord( Title::newFromText( 'Foo' ) ); + $slot = SlotRecord::newUnsaved( + 'main', + new WikitextContent( 'x' ) + ); + $record->setSlot( $slot ); + $this->assertTrue( $record->hasSlot( 'main' ) ); + $this->assertSame( $slot, $record->getSlot( 'main' ) ); + } + + public function testSetGetMinor() { + $record = new MutableRevisionRecord( Title::newFromText( 'Foo' ) ); + $this->assertFalse( $record->isMinor() ); + $record->setMinorEdit( true ); + $this->assertSame( true, $record->isMinor() ); + } + + public function testSetGetTimestamp() { + $record = new MutableRevisionRecord( Title::newFromText( 'Foo' ) ); + $this->assertNull( $record->getTimestamp() ); + $record->setTimestamp( '20180101010101' ); + $this->assertSame( '20180101010101', $record->getTimestamp() ); + } + + public function testSetGetVisibility() { + $record = new MutableRevisionRecord( Title::newFromText( 'Foo' ) ); + $this->assertSame( 0, $record->getVisibility() ); + $record->setVisibility( RevisionRecord::DELETED_USER ); + $this->assertSame( RevisionRecord::DELETED_USER, $record->getVisibility() ); + } + + public function testSetGetSha1() { + $record = new MutableRevisionRecord( Title::newFromText( 'Foo' ) ); + $this->assertSame( 'phoiac9h4m842xq45sp7s6u21eteeq1', $record->getSha1() ); + $record->setSha1( 'someHash' ); + $this->assertSame( 'someHash', $record->getSha1() ); + } + + public function testSetGetSize() { + $record = new MutableRevisionRecord( Title::newFromText( 'Foo' ) ); + $this->assertSame( 0, $record->getSize() ); + $record->setSize( 775 ); + $this->assertSame( 775, $record->getSize() ); + } + + public function testSetGetComment() { + $record = new MutableRevisionRecord( Title::newFromText( 'Foo' ) ); + $comment = new CommentStoreComment( 1, 'foo' ); + $this->assertNull( $record->getComment() ); + $record->setComment( $comment ); + $this->assertSame( $comment, $record->getComment() ); + } + +} diff --git a/www/wiki/tests/phpunit/includes/Storage/MutableRevisionSlotsTest.php b/www/wiki/tests/phpunit/includes/Storage/MutableRevisionSlotsTest.php new file mode 100644 index 00000000..0416bcfa --- /dev/null +++ b/www/wiki/tests/phpunit/includes/Storage/MutableRevisionSlotsTest.php @@ -0,0 +1,76 @@ +<?php + +namespace MediaWiki\Tests\Storage; + +use MediaWiki\Storage\MutableRevisionSlots; +use MediaWiki\Storage\RevisionAccessException; +use MediaWiki\Storage\SlotRecord; +use WikitextContent; + +/** + * @covers \MediaWiki\Storage\MutableRevisionSlots + */ +class MutableRevisionSlotsTest extends RevisionSlotsTest { + + public function testSetMultipleSlots() { + $slots = new MutableRevisionSlots(); + + $this->assertSame( [], $slots->getSlots() ); + + $slotA = SlotRecord::newUnsaved( 'some', new WikitextContent( 'A' ) ); + $slots->setSlot( $slotA ); + $this->assertTrue( $slots->hasSlot( 'some' ) ); + $this->assertSame( $slotA, $slots->getSlot( 'some' ) ); + $this->assertSame( [ 'some' => $slotA ], $slots->getSlots() ); + + $slotB = SlotRecord::newUnsaved( 'other', new WikitextContent( 'B' ) ); + $slots->setSlot( $slotB ); + $this->assertTrue( $slots->hasSlot( 'other' ) ); + $this->assertSame( $slotB, $slots->getSlot( 'other' ) ); + $this->assertSame( [ 'some' => $slotA, 'other' => $slotB ], $slots->getSlots() ); + } + + public function testSetExistingSlotOverwritesSlot() { + $slots = new MutableRevisionSlots(); + + $this->assertSame( [], $slots->getSlots() ); + + $slotA = SlotRecord::newUnsaved( 'main', new WikitextContent( 'A' ) ); + $slots->setSlot( $slotA ); + $this->assertSame( $slotA, $slots->getSlot( 'main' ) ); + $this->assertSame( [ 'main' => $slotA ], $slots->getSlots() ); + + $slotB = SlotRecord::newUnsaved( 'main', new WikitextContent( 'B' ) ); + $slots->setSlot( $slotB ); + $this->assertSame( $slotB, $slots->getSlot( 'main' ) ); + $this->assertSame( [ 'main' => $slotB ], $slots->getSlots() ); + } + + public function testSetContentOfExistingSlotOverwritesContent() { + $slots = new MutableRevisionSlots(); + + $this->assertSame( [], $slots->getSlots() ); + + $slotA = SlotRecord::newUnsaved( 'main', new WikitextContent( 'A' ) ); + $slots->setSlot( $slotA ); + $this->assertSame( $slotA, $slots->getSlot( 'main' ) ); + $this->assertSame( [ 'main' => $slotA ], $slots->getSlots() ); + + $newContent = new WikitextContent( 'B' ); + $slots->setContent( 'main', $newContent ); + $this->assertSame( $newContent, $slots->getContent( 'main' ) ); + } + + public function testRemoveExistingSlot() { + $slotA = SlotRecord::newUnsaved( 'main', new WikitextContent( 'A' ) ); + $slots = new MutableRevisionSlots( [ $slotA ] ); + + $this->assertSame( [ 'main' => $slotA ], $slots->getSlots() ); + + $slots->removeSlot( 'main' ); + $this->assertSame( [], $slots->getSlots() ); + $this->setExpectedException( RevisionAccessException::class ); + $slots->getSlot( 'main' ); + } + +} diff --git a/www/wiki/tests/phpunit/includes/Storage/NameTableStoreTest.php b/www/wiki/tests/phpunit/includes/Storage/NameTableStoreTest.php new file mode 100644 index 00000000..0cd164b7 --- /dev/null +++ b/www/wiki/tests/phpunit/includes/Storage/NameTableStoreTest.php @@ -0,0 +1,298 @@ +<?php + +namespace MediaWiki\Tests\Storage; + +use BagOStuff; +use EmptyBagOStuff; +use HashBagOStuff; +use MediaWiki\Storage\NameTableAccessException; +use MediaWiki\Storage\NameTableStore; +use MediaWikiTestCase; +use Psr\Log\NullLogger; +use WANObjectCache; +use Wikimedia\Rdbms\Database; +use Wikimedia\Rdbms\LoadBalancer; +use Wikimedia\TestingAccessWrapper; + +/** + * @author Addshore + * @group Database + * @covers \MediaWiki\Storage\NameTableStore + */ +class NameTableStoreTest extends MediaWikiTestCase { + + public function setUp() { + $this->tablesUsed[] = 'slot_roles'; + parent::setUp(); + } + + private function populateTable( $values ) { + $insertValues = []; + foreach ( $values as $name ) { + $insertValues[] = [ 'role_name' => $name ]; + } + $this->db->insert( 'slot_roles', $insertValues ); + } + + private function getHashWANObjectCache( $cacheBag ) { + return new WANObjectCache( [ 'cache' => $cacheBag ] ); + } + + /** + * @param $db + * @return \PHPUnit_Framework_MockObject_MockObject|LoadBalancer + */ + private function getMockLoadBalancer( $db ) { + $mock = $this->getMockBuilder( LoadBalancer::class ) + ->disableOriginalConstructor() + ->getMock(); + $mock->expects( $this->any() ) + ->method( 'getConnection' ) + ->willReturn( $db ); + return $mock; + } + + private function getCallCheckingDb( $insertCalls, $selectCalls ) { + $mock = $this->getMockBuilder( Database::class ) + ->disableOriginalConstructor() + ->getMock(); + $mock->expects( $this->exactly( $insertCalls ) ) + ->method( 'insert' ) + ->willReturnCallback( function () { + return call_user_func_array( [ $this->db, 'insert' ], func_get_args() ); + } ); + $mock->expects( $this->exactly( $selectCalls ) ) + ->method( 'select' ) + ->willReturnCallback( function () { + return call_user_func_array( [ $this->db, 'select' ], func_get_args() ); + } ); + $mock->expects( $this->exactly( $insertCalls ) ) + ->method( 'affectedRows' ) + ->willReturnCallback( function () { + return call_user_func_array( [ $this->db, 'affectedRows' ], func_get_args() ); + } ); + $mock->expects( $this->any() ) + ->method( 'insertId' ) + ->willReturnCallback( function () { + return call_user_func_array( [ $this->db, 'insertId' ], func_get_args() ); + } ); + return $mock; + } + + private function getNameTableSqlStore( + BagOStuff $cacheBag, + $insertCalls, + $selectCalls, + $normalizationCallback = null + ) { + return new NameTableStore( + $this->getMockLoadBalancer( $this->getCallCheckingDb( $insertCalls, $selectCalls ) ), + $this->getHashWANObjectCache( $cacheBag ), + new NullLogger(), + 'slot_roles', 'role_id', 'role_name', + $normalizationCallback + ); + } + + public function provideGetAndAcquireId() { + return [ + 'no wancache, empty table' => + [ new EmptyBagOStuff(), true, 1, [], 'foo', 1 ], + 'no wancache, one matching value' => + [ new EmptyBagOStuff(), false, 1, [ 'foo' ], 'foo', 1 ], + 'no wancache, one not matching value' => + [ new EmptyBagOStuff(), true, 1, [ 'bar' ], 'foo', 2 ], + 'no wancache, multiple, one matching value' => + [ new EmptyBagOStuff(), false, 1, [ 'foo', 'bar' ], 'bar', 2 ], + 'no wancache, multiple, no matching value' => + [ new EmptyBagOStuff(), true, 1, [ 'foo', 'bar' ], 'baz', 3 ], + 'wancache, empty table' => + [ new HashBagOStuff(), true, 1, [], 'foo', 1 ], + 'wancache, one matching value' => + [ new HashBagOStuff(), false, 1, [ 'foo' ], 'foo', 1 ], + 'wancache, one not matching value' => + [ new HashBagOStuff(), true, 1, [ 'bar' ], 'foo', 2 ], + 'wancache, multiple, one matching value' => + [ new HashBagOStuff(), false, 1, [ 'foo', 'bar' ], 'bar', 2 ], + 'wancache, multiple, no matching value' => + [ new HashBagOStuff(), true, 1, [ 'foo', 'bar' ], 'baz', 3 ], + ]; + } + + /** + * @dataProvider provideGetAndAcquireId + * @param BagOStuff $cacheBag to use in the WANObjectCache service + * @param bool $needsInsert Does the value we are testing need to be inserted? + * @param int $selectCalls Number of times the select DB method will be called + * @param string[] $existingValues to be added to the db table + * @param string $name name to acquire + * @param int $expectedId the id we expect the name to have + */ + public function testGetAndAcquireId( + $cacheBag, + $needsInsert, + $selectCalls, + $existingValues, + $name, + $expectedId + ) { + $this->populateTable( $existingValues ); + $store = $this->getNameTableSqlStore( $cacheBag, (int)$needsInsert, $selectCalls ); + + // Some names will not initially exist + try { + $result = $store->getId( $name ); + $this->assertSame( $expectedId, $result ); + } catch ( NameTableAccessException $e ) { + if ( $needsInsert ) { + $this->assertTrue( true ); // Expected exception + } else { + $this->fail( 'Did not expect an exception, but got one: ' . $e->getMessage() ); + } + } + + // All names should return their id here + $this->assertSame( $expectedId, $store->acquireId( $name ) ); + + // acquireId inserted these names, so now everything should exist with getId + $this->assertSame( $expectedId, $store->getId( $name ) ); + + // calling getId again will also still work, and not result in more selects + $this->assertSame( $expectedId, $store->getId( $name ) ); + } + + public function provideTestGetAndAcquireIdNameNormalization() { + yield [ 'A', 'a', 'strtolower' ]; + yield [ 'b', 'B', 'strtoupper' ]; + yield [ + 'X', + 'X', + function ( $name ) { + return $name; + } + ]; + yield [ 'ZZ', 'ZZ-a', __CLASS__ . '::appendDashAToString' ]; + } + + public static function appendDashAToString( $string ) { + return $string . '-a'; + } + + /** + * @dataProvider provideTestGetAndAcquireIdNameNormalization + */ + public function testGetAndAcquireIdNameNormalization( + $nameIn, + $nameOut, + $normalizationCallback + ) { + $store = $this->getNameTableSqlStore( + new EmptyBagOStuff(), + 1, + 1, + $normalizationCallback + ); + $acquiredId = $store->acquireId( $nameIn ); + $this->assertSame( $nameOut, $store->getName( $acquiredId ) ); + } + + public function provideGetName() { + return [ + [ new HashBagOStuff(), 3, 3 ], + [ new EmptyBagOStuff(), 3, 3 ], + ]; + } + + /** + * @dataProvider provideGetName + */ + public function testGetName( $cacheBag, $insertCalls, $selectCalls ) { + $store = $this->getNameTableSqlStore( $cacheBag, $insertCalls, $selectCalls ); + + // Get 1 ID and make sure getName returns correctly + $fooId = $store->acquireId( 'foo' ); + $this->assertSame( 'foo', $store->getName( $fooId ) ); + + // Get another ID and make sure getName returns correctly + $barId = $store->acquireId( 'bar' ); + $this->assertSame( 'bar', $store->getName( $barId ) ); + + // Blitz the cache and make sure it still returns + TestingAccessWrapper::newFromObject( $store )->tableCache = null; + $this->assertSame( 'foo', $store->getName( $fooId ) ); + $this->assertSame( 'bar', $store->getName( $barId ) ); + + // Blitz the cache again and get another ID and make sure getName returns correctly + TestingAccessWrapper::newFromObject( $store )->tableCache = null; + $bazId = $store->acquireId( 'baz' ); + $this->assertSame( 'baz', $store->getName( $bazId ) ); + $this->assertSame( 'baz', $store->getName( $bazId ) ); + } + + public function testGetName_masterFallback() { + $store = $this->getNameTableSqlStore( new EmptyBagOStuff(), 1, 2 ); + + // Insert a new name + $fooId = $store->acquireId( 'foo' ); + + // Empty the process cache, getCachedTable() will now return this empty array + TestingAccessWrapper::newFromObject( $store )->tableCache = []; + + // getName should fallback to master, which is why we assert 2 selectCalls above + $this->assertSame( 'foo', $store->getName( $fooId ) ); + } + + public function testGetMap_empty() { + $this->populateTable( [] ); + $store = $this->getNameTableSqlStore( new HashBagOStuff(), 0, 1 ); + $table = $store->getMap(); + $this->assertSame( [], $table ); + } + + public function testGetMap_twoValues() { + $this->populateTable( [ 'foo', 'bar' ] ); + $store = $this->getNameTableSqlStore( new HashBagOStuff(), 0, 1 ); + + // We are using a cache, so 2 calls should only result in 1 select on the db + $store->getMap(); + $table = $store->getMap(); + + $expected = [ 1 => 'foo', 2 => 'bar' ]; + $this->assertSame( $expected, $table ); + // Make sure the table returned is the same as the cached table + $this->assertSame( $expected, TestingAccessWrapper::newFromObject( $store )->tableCache ); + } + + public function testCacheRaceCondition() { + $wanHashBag = new HashBagOStuff(); + $store1 = $this->getNameTableSqlStore( $wanHashBag, 1, 1 ); + $store2 = $this->getNameTableSqlStore( $wanHashBag, 1, 0 ); + $store3 = $this->getNameTableSqlStore( $wanHashBag, 1, 1 ); + + // Cache the current table in the instances we will use + // This simulates multiple requests running simultaneously + $store1->getMap(); + $store2->getMap(); + $store3->getMap(); + + // Store 2 separate names using different instances + $fooId = $store1->acquireId( 'foo' ); + $barId = $store2->acquireId( 'bar' ); + + // Each of these instances should be aware of what they have inserted + $this->assertSame( $fooId, $store1->acquireId( 'foo' ) ); + $this->assertSame( $barId, $store2->acquireId( 'bar' ) ); + + // A new store should be able to get both of these new Ids + // Note: before there was a race condition here where acquireId( 'bar' ) would update the + // cache with data missing the 'foo' key that it was not aware of + $store4 = $this->getNameTableSqlStore( $wanHashBag, 0, 1 ); + $this->assertSame( $fooId, $store4->getId( 'foo' ) ); + $this->assertSame( $barId, $store4->getId( 'bar' ) ); + + // If a store with old cached data tries to acquire these we will get the same ids. + $this->assertSame( $fooId, $store3->acquireId( 'foo' ) ); + $this->assertSame( $barId, $store3->acquireId( 'bar' ) ); + } + +} diff --git a/www/wiki/tests/phpunit/includes/Storage/RevisionArchiveRecordTest.php b/www/wiki/tests/phpunit/includes/Storage/RevisionArchiveRecordTest.php new file mode 100644 index 00000000..f959d680 --- /dev/null +++ b/www/wiki/tests/phpunit/includes/Storage/RevisionArchiveRecordTest.php @@ -0,0 +1,272 @@ +<?php + +namespace MediaWiki\Tests\Storage; + +use CommentStoreComment; +use InvalidArgumentException; +use MediaWiki\Storage\RevisionRecord; +use MediaWiki\Storage\RevisionSlots; +use MediaWiki\Storage\RevisionArchiveRecord; +use MediaWiki\Storage\SlotRecord; +use MediaWiki\User\UserIdentity; +use MediaWiki\User\UserIdentityValue; +use MediaWikiTestCase; +use TextContent; +use Title; + +/** + * @covers \MediaWiki\Storage\RevisionArchiveRecord + * @covers \MediaWiki\Storage\RevisionRecord + */ +class RevisionArchiveRecordTest extends MediaWikiTestCase { + + use RevisionRecordTests; + + /** + * @param array $rowOverrides + * + * @return RevisionArchiveRecord + */ + protected function newRevision( array $rowOverrides = [] ) { + $title = Title::newFromText( 'Dummy' ); + $title->resetArticleID( 17 ); + + $user = new UserIdentityValue( 11, 'Tester', 0 ); + $comment = CommentStoreComment::newUnsavedComment( 'Hello World' ); + + $main = SlotRecord::newUnsaved( 'main', new TextContent( 'Lorem Ipsum' ) ); + $aux = SlotRecord::newUnsaved( 'aux', new TextContent( 'Frumious Bandersnatch' ) ); + $slots = new RevisionSlots( [ $main, $aux ] ); + + $row = [ + 'ar_id' => '5', + 'ar_rev_id' => '7', + 'ar_page_id' => strval( $title->getArticleID() ), + 'ar_timestamp' => '20200101000000', + 'ar_deleted' => 0, + 'ar_minor_edit' => 0, + 'ar_parent_id' => '5', + 'ar_len' => $slots->computeSize(), + 'ar_sha1' => $slots->computeSha1(), + ]; + + foreach ( $rowOverrides as $field => $value ) { + $field = preg_replace( '/^rev_/', 'ar_', $field ); + $row[$field] = $value; + } + + return new RevisionArchiveRecord( $title, $user, $comment, (object)$row, $slots ); + } + + public function provideConstructor() { + $title = Title::newFromText( 'Dummy' ); + $title->resetArticleID( 17 ); + + $user = new UserIdentityValue( 11, 'Tester', 0 ); + $comment = CommentStoreComment::newUnsavedComment( 'Hello World' ); + + $main = SlotRecord::newUnsaved( 'main', new TextContent( 'Lorem Ipsum' ) ); + $aux = SlotRecord::newUnsaved( 'aux', new TextContent( 'Frumious Bandersnatch' ) ); + $slots = new RevisionSlots( [ $main, $aux ] ); + + $protoRow = [ + 'ar_id' => '5', + 'ar_rev_id' => '7', + 'ar_page_id' => strval( $title->getArticleID() ), + 'ar_timestamp' => '20200101000000', + 'ar_deleted' => 0, + 'ar_minor_edit' => 0, + 'ar_parent_id' => '5', + 'ar_len' => $slots->computeSize(), + 'ar_sha1' => $slots->computeSha1(), + ]; + + $row = $protoRow; + yield 'all info' => [ + $title, + $user, + $comment, + (object)$row, + $slots, + 'acmewiki' + ]; + + $row = $protoRow; + $row['ar_minor_edit'] = '1'; + $row['ar_deleted'] = strval( RevisionRecord::DELETED_USER ); + + yield 'minor deleted' => [ + $title, + $user, + $comment, + (object)$row, + $slots + ]; + + $row = $protoRow; + unset( $row['ar_parent'] ); + + yield 'no parent' => [ + $title, + $user, + $comment, + (object)$row, + $slots + ]; + + $row = $protoRow; + $row['ar_len'] = null; + $row['ar_sha1'] = ''; + + yield 'ar_len is null, ar_sha1 is ""' => [ + $title, + $user, + $comment, + (object)$row, + $slots + ]; + + $row = $protoRow; + yield 'no length, no hash' => [ + Title::newFromText( 'DummyDoesNotExist' ), + $user, + $comment, + (object)$row, + $slots + ]; + } + + /** + * @dataProvider provideConstructor + * + * @param Title $title + * @param UserIdentity $user + * @param CommentStoreComment $comment + * @param object $row + * @param RevisionSlots $slots + * @param bool $wikiId + */ + public function testConstructorAndGetters( + Title $title, + UserIdentity $user, + CommentStoreComment $comment, + $row, + RevisionSlots $slots, + $wikiId = false + ) { + $rec = new RevisionArchiveRecord( $title, $user, $comment, $row, $slots, $wikiId ); + + $this->assertSame( $title, $rec->getPageAsLinkTarget(), 'getPageAsLinkTarget' ); + $this->assertSame( $user, $rec->getUser( RevisionRecord::RAW ), 'getUser' ); + $this->assertSame( $comment, $rec->getComment(), 'getComment' ); + + $this->assertSame( $slots->getSlotRoles(), $rec->getSlotRoles(), 'getSlotRoles' ); + $this->assertSame( $wikiId, $rec->getWikiId(), 'getWikiId' ); + + $this->assertSame( (int)$row->ar_id, $rec->getArchiveId(), 'getArchiveId' ); + $this->assertSame( (int)$row->ar_rev_id, $rec->getId(), 'getId' ); + $this->assertSame( (int)$row->ar_page_id, $rec->getPageId(), 'getId' ); + $this->assertSame( $row->ar_timestamp, $rec->getTimestamp(), 'getTimestamp' ); + $this->assertSame( (int)$row->ar_deleted, $rec->getVisibility(), 'getVisibility' ); + $this->assertSame( (bool)$row->ar_minor_edit, $rec->isMinor(), 'getIsMinor' ); + + if ( isset( $row->ar_parent_id ) ) { + $this->assertSame( (int)$row->ar_parent_id, $rec->getParentId(), 'getParentId' ); + } else { + $this->assertSame( 0, $rec->getParentId(), 'getParentId' ); + } + + if ( isset( $row->ar_len ) ) { + $this->assertSame( (int)$row->ar_len, $rec->getSize(), 'getSize' ); + } else { + $this->assertSame( $slots->computeSize(), $rec->getSize(), 'getSize' ); + } + + if ( !empty( $row->ar_sha1 ) ) { + $this->assertSame( $row->ar_sha1, $rec->getSha1(), 'getSha1' ); + } else { + $this->assertSame( $slots->computeSha1(), $rec->getSha1(), 'getSha1' ); + } + } + + public function provideConstructorFailure() { + $title = Title::newFromText( 'Dummy' ); + $title->resetArticleID( 17 ); + + $user = new UserIdentityValue( 11, 'Tester', 0 ); + + $comment = CommentStoreComment::newUnsavedComment( 'Hello World' ); + + $main = SlotRecord::newUnsaved( 'main', new TextContent( 'Lorem Ipsum' ) ); + $aux = SlotRecord::newUnsaved( 'aux', new TextContent( 'Frumious Bandersnatch' ) ); + $slots = new RevisionSlots( [ $main, $aux ] ); + + $protoRow = [ + 'ar_id' => '5', + 'ar_rev_id' => '7', + 'ar_page_id' => strval( $title->getArticleID() ), + 'ar_timestamp' => '20200101000000', + 'ar_deleted' => 0, + 'ar_minor_edit' => 0, + 'ar_parent_id' => '5', + 'ar_len' => $slots->computeSize(), + 'ar_sha1' => $slots->computeSha1(), + ]; + + yield 'not a row' => [ + $title, + $user, + $comment, + 'not a row', + $slots, + 'acmewiki' + ]; + + $row = $protoRow; + $row['ar_timestamp'] = 'kittens'; + + yield 'bad timestamp' => [ + $title, + $user, + $comment, + (object)$row, + $slots + ]; + + $row = $protoRow; + + yield 'bad wiki' => [ + $title, + $user, + $comment, + (object)$row, + $slots, + 12345 + ]; + + // NOTE: $title->getArticleID does *not* have to match ar_page_id in all cases! + } + + /** + * @dataProvider provideConstructorFailure + * + * @param Title $title + * @param UserIdentity $user + * @param CommentStoreComment $comment + * @param object $row + * @param RevisionSlots $slots + * @param bool $wikiId + */ + public function testConstructorFailure( + Title $title, + UserIdentity $user, + CommentStoreComment $comment, + $row, + RevisionSlots $slots, + $wikiId = false + ) { + $this->setExpectedException( InvalidArgumentException::class ); + new RevisionArchiveRecord( $title, $user, $comment, $row, $slots, $wikiId ); + } + +} diff --git a/www/wiki/tests/phpunit/includes/Storage/RevisionRecordTests.php b/www/wiki/tests/phpunit/includes/Storage/RevisionRecordTests.php new file mode 100644 index 00000000..607f7829 --- /dev/null +++ b/www/wiki/tests/phpunit/includes/Storage/RevisionRecordTests.php @@ -0,0 +1,512 @@ +<?php + +namespace MediaWiki\Tests\Storage; + +use CommentStoreComment; +use LogicException; +use MediaWiki\Storage\RevisionRecord; +use MediaWiki\Storage\RevisionSlots; +use MediaWiki\Storage\RevisionStoreRecord; +use MediaWiki\Storage\SlotRecord; +use MediaWiki\Storage\SuppressedDataException; +use MediaWiki\User\UserIdentityValue; +use TextContent; +use Title; + +// PHPCS should not complain about @covers and @dataProvider being used in traits, see T192384 +// phpcs:disable MediaWiki.Commenting.PhpunitAnnotations.NotTestClass + +/** + * @covers \MediaWiki\Storage\RevisionRecord + * + * @note Expects to be used in classes that extend MediaWikiTestCase. + */ +trait RevisionRecordTests { + + /** + * @param array $rowOverrides + * + * @return RevisionRecord + */ + protected abstract function newRevision( array $rowOverrides = [] ); + + private function provideAudienceCheckData( $field ) { + yield 'field accessible for oversighter (ALL)' => [ + RevisionRecord::SUPPRESSED_ALL, + [ 'oversight' ], + true, + false + ]; + + yield 'field accessible for oversighter' => [ + RevisionRecord::DELETED_RESTRICTED | $field, + [ 'oversight' ], + true, + false + ]; + + yield 'field not accessible for sysops (ALL)' => [ + RevisionRecord::SUPPRESSED_ALL, + [ 'sysop' ], + false, + false + ]; + + yield 'field not accessible for sysops' => [ + RevisionRecord::DELETED_RESTRICTED | $field, + [ 'sysop' ], + false, + false + ]; + + yield 'field accessible for sysops' => [ + $field, + [ 'sysop' ], + true, + false + ]; + + yield 'field suppressed for logged in users' => [ + $field, + [ 'user' ], + false, + false + ]; + + yield 'unrelated field suppressed' => [ + $field === RevisionRecord::DELETED_COMMENT + ? RevisionRecord::DELETED_USER + : RevisionRecord::DELETED_COMMENT, + [ 'user' ], + true, + true + ]; + + yield 'nothing suppressed' => [ + 0, + [ 'user' ], + true, + true + ]; + } + + public function testSerialization_fails() { + $this->setExpectedException( LogicException::class ); + $rev = $this->newRevision(); + serialize( $rev ); + } + + public function provideGetComment_audience() { + return $this->provideAudienceCheckData( RevisionRecord::DELETED_COMMENT ); + } + + private function forceStandardPermissions() { + $this->setMwGlobals( + 'wgGroupPermissions', + [ + 'user' => [ + 'viewsuppressed' => false, + 'suppressrevision' => false, + 'deletedtext' => false, + 'deletedhistory' => false, + ], + 'sysop' => [ + 'viewsuppressed' => false, + 'suppressrevision' => false, + 'deletedtext' => true, + 'deletedhistory' => true, + ], + 'oversight' => [ + 'deletedtext' => true, + 'deletedhistory' => true, + 'viewsuppressed' => true, + 'suppressrevision' => true, + ], + ] + ); + } + + /** + * @dataProvider provideGetComment_audience + */ + public function testGetComment_audience( $visibility, $groups, $userCan, $publicCan ) { + $this->forceStandardPermissions(); + + $user = $this->getTestUser( $groups )->getUser(); + $rev = $this->newRevision( [ 'rev_deleted' => $visibility ] ); + + $this->assertNotNull( $rev->getComment( RevisionRecord::RAW ), 'raw can' ); + + $this->assertSame( + $publicCan, + $rev->getComment( RevisionRecord::FOR_PUBLIC ) !== null, + 'public can' + ); + $this->assertSame( + $userCan, + $rev->getComment( RevisionRecord::FOR_THIS_USER, $user ) !== null, + 'user can' + ); + } + + public function provideGetUser_audience() { + return $this->provideAudienceCheckData( RevisionRecord::DELETED_USER ); + } + + /** + * @dataProvider provideGetUser_audience + */ + public function testGetUser_audience( $visibility, $groups, $userCan, $publicCan ) { + $this->forceStandardPermissions(); + + $user = $this->getTestUser( $groups )->getUser(); + $rev = $this->newRevision( [ 'rev_deleted' => $visibility ] ); + + $this->assertNotNull( $rev->getUser( RevisionRecord::RAW ), 'raw can' ); + + $this->assertSame( + $publicCan, + $rev->getUser( RevisionRecord::FOR_PUBLIC ) !== null, + 'public can' + ); + $this->assertSame( + $userCan, + $rev->getUser( RevisionRecord::FOR_THIS_USER, $user ) !== null, + 'user can' + ); + } + + public function provideGetSlot_audience() { + return $this->provideAudienceCheckData( RevisionRecord::DELETED_TEXT ); + } + + /** + * @dataProvider provideGetSlot_audience + */ + public function testGetSlot_audience( $visibility, $groups, $userCan, $publicCan ) { + $this->forceStandardPermissions(); + + $user = $this->getTestUser( $groups )->getUser(); + $rev = $this->newRevision( [ 'rev_deleted' => $visibility ] ); + + // NOTE: slot meta-data is never suppressed, just the content is! + $this->assertTrue( $rev->hasSlot( 'main' ), 'hasSlot is never suppressed' ); + $this->assertNotNull( $rev->getSlot( 'main', RevisionRecord::RAW ), 'raw meta' ); + $this->assertNotNull( $rev->getSlot( 'main', RevisionRecord::FOR_PUBLIC ), 'public meta' ); + + $this->assertNotNull( + $rev->getSlot( 'main', RevisionRecord::FOR_THIS_USER, $user ), + 'user can' + ); + + try { + $rev->getSlot( 'main', RevisionRecord::FOR_PUBLIC )->getContent(); + $exception = null; + } catch ( SuppressedDataException $ex ) { + $exception = $ex; + } + + $this->assertSame( + $publicCan, + $exception === null, + 'public can' + ); + + try { + $rev->getSlot( 'main', RevisionRecord::FOR_THIS_USER, $user )->getContent(); + $exception = null; + } catch ( SuppressedDataException $ex ) { + $exception = $ex; + } + + $this->assertSame( + $userCan, + $exception === null, + 'user can' + ); + } + + /** + * @dataProvider provideGetSlot_audience + */ + public function testGetContent_audience( $visibility, $groups, $userCan, $publicCan ) { + $this->forceStandardPermissions(); + + $user = $this->getTestUser( $groups )->getUser(); + $rev = $this->newRevision( [ 'rev_deleted' => $visibility ] ); + + $this->assertNotNull( $rev->getContent( 'main', RevisionRecord::RAW ), 'raw can' ); + + $this->assertSame( + $publicCan, + $rev->getContent( 'main', RevisionRecord::FOR_PUBLIC ) !== null, + 'public can' + ); + $this->assertSame( + $userCan, + $rev->getContent( 'main', RevisionRecord::FOR_THIS_USER, $user ) !== null, + 'user can' + ); + } + + public function testGetSlot() { + $rev = $this->newRevision(); + + $slot = $rev->getSlot( 'main' ); + $this->assertNotNull( $slot, 'getSlot()' ); + $this->assertSame( 'main', $slot->getRole(), 'getRole()' ); + } + + public function testHasSlot() { + $rev = $this->newRevision(); + + $this->assertTrue( $rev->hasSlot( 'main' ) ); + $this->assertFalse( $rev->hasSlot( 'xyz' ) ); + } + + public function testGetContent() { + $rev = $this->newRevision(); + + $content = $rev->getSlot( 'main' ); + $this->assertNotNull( $content, 'getContent()' ); + $this->assertSame( CONTENT_MODEL_TEXT, $content->getModel(), 'getModel()' ); + } + + public function provideUserCanBitfield() { + yield [ 0, 0, [], null, true ]; + // Bitfields match, user has no permissions + yield [ + RevisionRecord::DELETED_TEXT, + RevisionRecord::DELETED_TEXT, + [], + null, + false + ]; + yield [ + RevisionRecord::DELETED_COMMENT, + RevisionRecord::DELETED_COMMENT, + [], + null, + false, + ]; + yield [ + RevisionRecord::DELETED_USER, + RevisionRecord::DELETED_USER, + [], + null, + false + ]; + yield [ + RevisionRecord::DELETED_RESTRICTED, + RevisionRecord::DELETED_RESTRICTED, + [], + null, + false, + ]; + // Bitfields match, user (admin) does have permissions + yield [ + RevisionRecord::DELETED_TEXT, + RevisionRecord::DELETED_TEXT, + [ 'sysop' ], + null, + true, + ]; + yield [ + RevisionRecord::DELETED_COMMENT, + RevisionRecord::DELETED_COMMENT, + [ 'sysop' ], + null, + true, + ]; + yield [ + RevisionRecord::DELETED_USER, + RevisionRecord::DELETED_USER, + [ 'sysop' ], + null, + true, + ]; + // Bitfields match, user (admin) does not have permissions + yield [ + RevisionRecord::DELETED_RESTRICTED, + RevisionRecord::DELETED_RESTRICTED, + [ 'sysop' ], + null, + false, + ]; + // Bitfields match, user (oversight) does have permissions + yield [ + RevisionRecord::DELETED_RESTRICTED, + RevisionRecord::DELETED_RESTRICTED, + [ 'oversight' ], + null, + true, + ]; + // Check permissions using the title + yield [ + RevisionRecord::DELETED_TEXT, + RevisionRecord::DELETED_TEXT, + [ 'sysop' ], + Title::newFromText( __METHOD__ ), + true, + ]; + yield [ + RevisionRecord::DELETED_TEXT, + RevisionRecord::DELETED_TEXT, + [], + Title::newFromText( __METHOD__ ), + false, + ]; + } + + /** + * @dataProvider provideUserCanBitfield + * @covers \MediaWiki\Storage\RevisionRecord::userCanBitfield + */ + public function testUserCanBitfield( $bitField, $field, $userGroups, $title, $expected ) { + $this->forceStandardPermissions(); + + $user = $this->getTestUser( $userGroups )->getUser(); + + $this->assertSame( + $expected, + RevisionRecord::userCanBitfield( $bitField, $field, $user, $title ) + ); + } + + public function provideHasSameContent() { + /** + * @param SlotRecord[] $slots + * @param int $revId + * @return RevisionStoreRecord + */ + $recordCreator = function ( array $slots, $revId ) { + $title = Title::newFromText( 'provideHasSameContent' ); + $title->resetArticleID( 19 ); + $slots = new RevisionSlots( $slots ); + + return new RevisionStoreRecord( + $title, + new UserIdentityValue( 11, __METHOD__, 0 ), + CommentStoreComment::newUnsavedComment( __METHOD__ ), + (object)[ + 'rev_id' => strval( $revId ), + 'rev_page' => strval( $title->getArticleID() ), + 'rev_timestamp' => '20200101000000', + 'rev_deleted' => 0, + 'rev_minor_edit' => 0, + 'rev_parent_id' => '5', + 'rev_len' => $slots->computeSize(), + 'rev_sha1' => $slots->computeSha1(), + 'page_latest' => '18', + ], + $slots + ); + }; + + // Create some slots with content + $mainA = SlotRecord::newUnsaved( 'main', new TextContent( 'A' ) ); + $mainB = SlotRecord::newUnsaved( 'main', new TextContent( 'B' ) ); + $auxA = SlotRecord::newUnsaved( 'aux', new TextContent( 'A' ) ); + $auxB = SlotRecord::newUnsaved( 'aux', new TextContent( 'A' ) ); + + $initialRecord = $recordCreator( [ $mainA ], 12 ); + + return [ + 'same record object' => [ + true, + $initialRecord, + $initialRecord, + ], + 'same record content, different object' => [ + true, + $recordCreator( [ $mainA ], 12 ), + $recordCreator( [ $mainA ], 13 ), + ], + 'same record content, aux slot, different object' => [ + true, + $recordCreator( [ $auxA ], 12 ), + $recordCreator( [ $auxB ], 13 ), + ], + 'different content' => [ + false, + $recordCreator( [ $mainA ], 12 ), + $recordCreator( [ $mainB ], 13 ), + ], + 'different content and number of slots' => [ + false, + $recordCreator( [ $mainA ], 12 ), + $recordCreator( [ $mainA, $mainB ], 13 ), + ], + ]; + } + + /** + * @dataProvider provideHasSameContent + * @covers \MediaWiki\Storage\RevisionRecord::hasSameContent + * @group Database + */ + public function testHasSameContent( + $expected, + RevisionRecord $record1, + RevisionRecord $record2 + ) { + $this->assertSame( + $expected, + $record1->hasSameContent( $record2 ) + ); + } + + public function provideIsDeleted() { + yield 'no deletion' => [ + 0, + [ + RevisionRecord::DELETED_TEXT => false, + RevisionRecord::DELETED_COMMENT => false, + RevisionRecord::DELETED_USER => false, + RevisionRecord::DELETED_RESTRICTED => false, + ] + ]; + yield 'text deleted' => [ + RevisionRecord::DELETED_TEXT, + [ + RevisionRecord::DELETED_TEXT => true, + RevisionRecord::DELETED_COMMENT => false, + RevisionRecord::DELETED_USER => false, + RevisionRecord::DELETED_RESTRICTED => false, + ] + ]; + yield 'text and comment deleted' => [ + RevisionRecord::DELETED_TEXT + RevisionRecord::DELETED_COMMENT, + [ + RevisionRecord::DELETED_TEXT => true, + RevisionRecord::DELETED_COMMENT => true, + RevisionRecord::DELETED_USER => false, + RevisionRecord::DELETED_RESTRICTED => false, + ] + ]; + yield 'all 4 deleted' => [ + RevisionRecord::DELETED_TEXT + + RevisionRecord::DELETED_COMMENT + + RevisionRecord::DELETED_RESTRICTED + + RevisionRecord::DELETED_USER, + [ + RevisionRecord::DELETED_TEXT => true, + RevisionRecord::DELETED_COMMENT => true, + RevisionRecord::DELETED_USER => true, + RevisionRecord::DELETED_RESTRICTED => true, + ] + ]; + } + + /** + * @dataProvider provideIsDeleted + * @covers \MediaWiki\Storage\RevisionRecord::isDeleted + */ + public function testIsDeleted( $revDeleted, $assertionMap ) { + $rev = $this->newRevision( [ 'rev_deleted' => $revDeleted ] ); + foreach ( $assertionMap as $deletionLevel => $expected ) { + $this->assertSame( $expected, $rev->isDeleted( $deletionLevel ) ); + } + } + +} diff --git a/www/wiki/tests/phpunit/includes/Storage/RevisionSlotsTest.php b/www/wiki/tests/phpunit/includes/Storage/RevisionSlotsTest.php new file mode 100644 index 00000000..b9f833ca --- /dev/null +++ b/www/wiki/tests/phpunit/includes/Storage/RevisionSlotsTest.php @@ -0,0 +1,139 @@ +<?php + +namespace MediaWiki\Tests\Storage; + +use MediaWiki\Storage\RevisionAccessException; +use MediaWiki\Storage\RevisionSlots; +use MediaWiki\Storage\SlotRecord; +use MediaWikiTestCase; +use WikitextContent; + +class RevisionSlotsTest extends MediaWikiTestCase { + + /** + * @param SlotRecord[] $slots + * @return RevisionSlots + */ + protected function newRevisionSlots( $slots = [] ) { + return new RevisionSlots( $slots ); + } + + /** + * @covers \MediaWiki\Storage\RevisionSlots::getSlot + */ + public function testGetSlot() { + $mainSlot = SlotRecord::newUnsaved( 'main', new WikitextContent( 'A' ) ); + $auxSlot = SlotRecord::newUnsaved( 'aux', new WikitextContent( 'B' ) ); + $slots = $this->newRevisionSlots( [ $mainSlot, $auxSlot ] ); + + $this->assertSame( $mainSlot, $slots->getSlot( 'main' ) ); + $this->assertSame( $auxSlot, $slots->getSlot( 'aux' ) ); + $this->setExpectedException( RevisionAccessException::class ); + $slots->getSlot( 'nothere' ); + } + + /** + * @covers \MediaWiki\Storage\RevisionSlots::hasSlot + */ + public function testHasSlot() { + $mainSlot = SlotRecord::newUnsaved( 'main', new WikitextContent( 'A' ) ); + $auxSlot = SlotRecord::newUnsaved( 'aux', new WikitextContent( 'B' ) ); + $slots = $this->newRevisionSlots( [ $mainSlot, $auxSlot ] ); + + $this->assertTrue( $slots->hasSlot( 'main' ) ); + $this->assertTrue( $slots->hasSlot( 'aux' ) ); + $this->assertFalse( $slots->hasSlot( 'AUX' ) ); + $this->assertFalse( $slots->hasSlot( 'xyz' ) ); + } + + /** + * @covers \MediaWiki\Storage\RevisionSlots::getContent + */ + public function testGetContent() { + $mainContent = new WikitextContent( 'A' ); + $auxContent = new WikitextContent( 'B' ); + $mainSlot = SlotRecord::newUnsaved( 'main', $mainContent ); + $auxSlot = SlotRecord::newUnsaved( 'aux', $auxContent ); + $slots = $this->newRevisionSlots( [ $mainSlot, $auxSlot ] ); + + $this->assertSame( $mainContent, $slots->getContent( 'main' ) ); + $this->assertSame( $auxContent, $slots->getContent( 'aux' ) ); + $this->setExpectedException( RevisionAccessException::class ); + $slots->getContent( 'nothere' ); + } + + /** + * @covers \MediaWiki\Storage\RevisionSlots::getSlotRoles + */ + public function testGetSlotRoles_someSlots() { + $mainSlot = SlotRecord::newUnsaved( 'main', new WikitextContent( 'A' ) ); + $auxSlot = SlotRecord::newUnsaved( 'aux', new WikitextContent( 'B' ) ); + $slots = $this->newRevisionSlots( [ $mainSlot, $auxSlot ] ); + + $this->assertSame( [ 'main', 'aux' ], $slots->getSlotRoles() ); + } + + /** + * @covers \MediaWiki\Storage\RevisionSlots::getSlotRoles + */ + public function testGetSlotRoles_noSlots() { + $slots = $this->newRevisionSlots( [] ); + + $this->assertSame( [], $slots->getSlotRoles() ); + } + + /** + * @covers \MediaWiki\Storage\RevisionSlots::getSlots + */ + public function testGetSlots() { + $mainSlot = SlotRecord::newUnsaved( 'main', new WikitextContent( 'A' ) ); + $auxSlot = SlotRecord::newUnsaved( 'aux', new WikitextContent( 'B' ) ); + $slotsArray = [ $mainSlot, $auxSlot ]; + $slots = $this->newRevisionSlots( $slotsArray ); + + $this->assertEquals( [ 'main' => $mainSlot, 'aux' => $auxSlot ], $slots->getSlots() ); + } + + public function provideComputeSize() { + yield [ 1, [ 'A' ] ]; + yield [ 2, [ 'AA' ] ]; + yield [ 4, [ 'AA', 'X', 'H' ] ]; + } + + /** + * @dataProvider provideComputeSize + * @covers \MediaWiki\Storage\RevisionSlots::computeSize + */ + public function testComputeSize( $expected, $contentStrings ) { + $slotsArray = []; + foreach ( $contentStrings as $key => $contentString ) { + $slotsArray[] = SlotRecord::newUnsaved( strval( $key ), new WikitextContent( $contentString ) ); + } + $slots = $this->newRevisionSlots( $slotsArray ); + + $this->assertSame( $expected, $slots->computeSize() ); + } + + public function provideComputeSha1() { + yield [ 'ctqm7794fr2dp1taki8a88ovwnvmnmj', [ 'A' ] ]; + yield [ 'eyq8wiwlcofnaiy4eid97gyfy60uw51', [ 'AA' ] ]; + yield [ 'lavctqfpxartyjr31f853drgfl4kj1g', [ 'AA', 'X', 'H' ] ]; + } + + /** + * @dataProvider provideComputeSha1 + * @covers \MediaWiki\Storage\RevisionSlots::computeSha1 + * @note this test is a bit brittle as the hashes are hardcoded, perhaps just check that strings + * are returned and different Slots objects return different strings? + */ + public function testComputeSha1( $expected, $contentStrings ) { + $slotsArray = []; + foreach ( $contentStrings as $key => $contentString ) { + $slotsArray[] = SlotRecord::newUnsaved( strval( $key ), new WikitextContent( $contentString ) ); + } + $slots = $this->newRevisionSlots( $slotsArray ); + + $this->assertSame( $expected, $slots->computeSha1() ); + } + +} diff --git a/www/wiki/tests/phpunit/includes/Storage/RevisionStoreDbTest.php b/www/wiki/tests/phpunit/includes/Storage/RevisionStoreDbTest.php new file mode 100644 index 00000000..7d6906c1 --- /dev/null +++ b/www/wiki/tests/phpunit/includes/Storage/RevisionStoreDbTest.php @@ -0,0 +1,1281 @@ +<?php + +namespace MediaWiki\Tests\Storage; + +use CommentStoreComment; +use Exception; +use HashBagOStuff; +use InvalidArgumentException; +use Language; +use MediaWiki\Linker\LinkTarget; +use MediaWiki\MediaWikiServices; +use MediaWiki\Storage\BlobStoreFactory; +use MediaWiki\Storage\IncompleteRevisionException; +use MediaWiki\Storage\MutableRevisionRecord; +use MediaWiki\Storage\RevisionRecord; +use MediaWiki\Storage\RevisionStore; +use MediaWiki\Storage\SlotRecord; +use MediaWiki\Storage\SqlBlobStore; +use MediaWikiTestCase; +use Revision; +use TestUserRegistry; +use Title; +use WANObjectCache; +use Wikimedia\Rdbms\Database; +use Wikimedia\Rdbms\DatabaseSqlite; +use Wikimedia\Rdbms\FakeResultWrapper; +use Wikimedia\Rdbms\LoadBalancer; +use Wikimedia\Rdbms\TransactionProfiler; +use WikiPage; +use WikitextContent; + +/** + * @group Database + */ +class RevisionStoreDbTest extends MediaWikiTestCase { + + public function setUp() { + parent::setUp(); + $this->tablesUsed[] = 'archive'; + $this->tablesUsed[] = 'page'; + $this->tablesUsed[] = 'revision'; + $this->tablesUsed[] = 'comment'; + } + + /** + * @return LoadBalancer + */ + private function getLoadBalancerMock( array $server ) { + $lb = $this->getMockBuilder( LoadBalancer::class ) + ->setMethods( [ 'reallyOpenConnection' ] ) + ->setConstructorArgs( [ [ 'servers' => [ $server ] ] ] ) + ->getMock(); + + $lb->method( 'reallyOpenConnection' )->willReturnCallback( + function ( array $server, $dbNameOverride ) { + return $this->getDatabaseMock( $server ); + } + ); + + return $lb; + } + + /** + * @return Database + */ + private function getDatabaseMock( array $params ) { + $db = $this->getMockBuilder( DatabaseSqlite::class ) + ->setMethods( [ 'select', 'doQuery', 'open', 'closeConnection', 'isOpen' ] ) + ->setConstructorArgs( [ $params ] ) + ->getMock(); + + $db->method( 'select' )->willReturn( new FakeResultWrapper( [] ) ); + $db->method( 'isOpen' )->willReturn( true ); + + return $db; + } + + public function provideDomainCheck() { + yield [ false, 'test', '' ]; + yield [ 'test', 'test', '' ]; + + yield [ false, 'test', 'foo_' ]; + yield [ 'test-foo_', 'test', 'foo_' ]; + + yield [ false, 'dash-test', '' ]; + yield [ 'dash-test', 'dash-test', '' ]; + + yield [ false, 'underscore_test', 'foo_' ]; + yield [ 'underscore_test-foo_', 'underscore_test', 'foo_' ]; + } + + /** + * @dataProvider provideDomainCheck + * @covers \MediaWiki\Storage\RevisionStore::checkDatabaseWikiId + */ + public function testDomainCheck( $wikiId, $dbName, $dbPrefix ) { + $this->setMwGlobals( + [ + 'wgDBname' => $dbName, + 'wgDBprefix' => $dbPrefix, + ] + ); + + $loadBalancer = $this->getLoadBalancerMock( + [ + 'host' => '*dummy*', + 'dbDirectory' => '*dummy*', + 'user' => 'test', + 'password' => 'test', + 'flags' => 0, + 'variables' => [], + 'schema' => '', + 'cliMode' => true, + 'agent' => '', + 'load' => 100, + 'profiler' => null, + 'trxProfiler' => new TransactionProfiler(), + 'connLogger' => new \Psr\Log\NullLogger(), + 'queryLogger' => new \Psr\Log\NullLogger(), + 'errorLogger' => function () { + }, + 'deprecationLogger' => function () { + }, + 'type' => 'test', + 'dbname' => $dbName, + 'tablePrefix' => $dbPrefix, + ] + ); + $db = $loadBalancer->getConnection( DB_REPLICA ); + + $blobStore = $this->getMockBuilder( SqlBlobStore::class ) + ->disableOriginalConstructor() + ->getMock(); + + $store = new RevisionStore( + $loadBalancer, + $blobStore, + new WANObjectCache( [ 'cache' => new HashBagOStuff() ] ), + MediaWikiServices::getInstance()->getCommentStore(), + MediaWikiServices::getInstance()->getActorMigration(), + $wikiId + ); + + $count = $store->countRevisionsByPageId( $db, 0 ); + + // Dummy check to make PhpUnit happy. We are really only interested in + // countRevisionsByPageId not failing due to the DB domain check. + $this->assertSame( 0, $count ); + } + + private function assertLinkTargetsEqual( LinkTarget $l1, LinkTarget $l2 ) { + $this->assertEquals( $l1->getDBkey(), $l2->getDBkey() ); + $this->assertEquals( $l1->getNamespace(), $l2->getNamespace() ); + $this->assertEquals( $l1->getFragment(), $l2->getFragment() ); + $this->assertEquals( $l1->getInterwiki(), $l2->getInterwiki() ); + } + + private function assertRevisionRecordsEqual( RevisionRecord $r1, RevisionRecord $r2 ) { + $this->assertEquals( $r1->getUser()->getName(), $r2->getUser()->getName() ); + $this->assertEquals( $r1->getUser()->getId(), $r2->getUser()->getId() ); + $this->assertEquals( $r1->getComment(), $r2->getComment() ); + $this->assertEquals( $r1->getPageAsLinkTarget(), $r2->getPageAsLinkTarget() ); + $this->assertEquals( $r1->getTimestamp(), $r2->getTimestamp() ); + $this->assertEquals( $r1->getVisibility(), $r2->getVisibility() ); + $this->assertEquals( $r1->getSha1(), $r2->getSha1() ); + $this->assertEquals( $r1->getParentId(), $r2->getParentId() ); + $this->assertEquals( $r1->getSize(), $r2->getSize() ); + $this->assertEquals( $r1->getPageId(), $r2->getPageId() ); + $this->assertEquals( $r1->getSlotRoles(), $r2->getSlotRoles() ); + $this->assertEquals( $r1->getWikiId(), $r2->getWikiId() ); + $this->assertEquals( $r1->isMinor(), $r2->isMinor() ); + foreach ( $r1->getSlotRoles() as $role ) { + $this->assertSlotRecordsEqual( $r1->getSlot( $role ), $r2->getSlot( $role ) ); + $this->assertTrue( $r1->getContent( $role )->equals( $r2->getContent( $role ) ) ); + } + foreach ( [ + RevisionRecord::DELETED_TEXT, + RevisionRecord::DELETED_COMMENT, + RevisionRecord::DELETED_USER, + RevisionRecord::DELETED_RESTRICTED, + ] as $field ) { + $this->assertEquals( $r1->isDeleted( $field ), $r2->isDeleted( $field ) ); + } + } + + private function assertSlotRecordsEqual( SlotRecord $s1, SlotRecord $s2 ) { + $this->assertSame( $s1->getRole(), $s2->getRole() ); + $this->assertSame( $s1->getModel(), $s2->getModel() ); + $this->assertSame( $s1->getFormat(), $s2->getFormat() ); + $this->assertSame( $s1->getSha1(), $s2->getSha1() ); + $this->assertSame( $s1->getSize(), $s2->getSize() ); + $this->assertTrue( $s1->getContent()->equals( $s2->getContent() ) ); + + $s1->hasRevision() ? $this->assertSame( $s1->getRevision(), $s2->getRevision() ) : null; + $s1->hasAddress() ? $this->assertSame( $s1->hasAddress(), $s2->hasAddress() ) : null; + } + + private function assertRevisionCompleteness( RevisionRecord $r ) { + foreach ( $r->getSlotRoles() as $role ) { + $this->assertSlotCompleteness( $r, $r->getSlot( $role ) ); + } + } + + private function assertSlotCompleteness( RevisionRecord $r, SlotRecord $slot ) { + $this->assertTrue( $slot->hasAddress() ); + $this->assertSame( $r->getId(), $slot->getRevision() ); + } + + /** + * @param mixed[] $details + * + * @return RevisionRecord + */ + private function getRevisionRecordFromDetailsArray( $title, $details = [] ) { + // Convert some values that can't be provided by dataProviders + $page = WikiPage::factory( $title ); + if ( isset( $details['user'] ) && $details['user'] === true ) { + $details['user'] = $this->getTestUser()->getUser(); + } + if ( isset( $details['page'] ) && $details['page'] === true ) { + $details['page'] = $page->getId(); + } + if ( isset( $details['parent'] ) && $details['parent'] === true ) { + $details['parent'] = $page->getLatest(); + } + + // Create the RevisionRecord with any available data + $rev = new MutableRevisionRecord( $title ); + isset( $details['slot'] ) ? $rev->setSlot( $details['slot'] ) : null; + isset( $details['parent'] ) ? $rev->setParentId( $details['parent'] ) : null; + isset( $details['page'] ) ? $rev->setPageId( $details['page'] ) : null; + isset( $details['size'] ) ? $rev->setSize( $details['size'] ) : null; + isset( $details['sha1'] ) ? $rev->setSha1( $details['sha1'] ) : null; + isset( $details['comment'] ) ? $rev->setComment( $details['comment'] ) : null; + isset( $details['timestamp'] ) ? $rev->setTimestamp( $details['timestamp'] ) : null; + isset( $details['minor'] ) ? $rev->setMinorEdit( $details['minor'] ) : null; + isset( $details['user'] ) ? $rev->setUser( $details['user'] ) : null; + isset( $details['visibility'] ) ? $rev->setVisibility( $details['visibility'] ) : null; + isset( $details['id'] ) ? $rev->setId( $details['id'] ) : null; + + return $rev; + } + + private function getRandomCommentStoreComment() { + return CommentStoreComment::newUnsavedComment( __METHOD__ . '.' . rand( 0, 1000 ) ); + } + + public function provideInsertRevisionOn_successes() { + yield 'Bare minimum revision insertion' => [ + Title::newFromText( 'UTPage' ), + [ + 'slot' => SlotRecord::newUnsaved( 'main', new WikitextContent( 'Chicken' ) ), + 'parent' => true, + 'comment' => $this->getRandomCommentStoreComment(), + 'timestamp' => '20171117010101', + 'user' => true, + ], + ]; + yield 'Detailed revision insertion' => [ + Title::newFromText( 'UTPage' ), + [ + 'slot' => SlotRecord::newUnsaved( 'main', new WikitextContent( 'Chicken' ) ), + 'parent' => true, + 'page' => true, + 'comment' => $this->getRandomCommentStoreComment(), + 'timestamp' => '20171117010101', + 'user' => true, + 'minor' => true, + 'visibility' => RevisionRecord::DELETED_RESTRICTED, + ], + ]; + } + + /** + * @dataProvider provideInsertRevisionOn_successes + * @covers \MediaWiki\Storage\RevisionStore::insertRevisionOn + */ + public function testInsertRevisionOn_successes( Title $title, array $revDetails = [] ) { + $rev = $this->getRevisionRecordFromDetailsArray( $title, $revDetails ); + + $store = MediaWikiServices::getInstance()->getRevisionStore(); + $return = $store->insertRevisionOn( $rev, wfGetDB( DB_MASTER ) ); + + $this->assertLinkTargetsEqual( $title, $return->getPageAsLinkTarget() ); + $this->assertRevisionRecordsEqual( $rev, $return ); + $this->assertRevisionCompleteness( $return ); + } + + /** + * @covers \MediaWiki\Storage\RevisionStore::insertRevisionOn + */ + public function testInsertRevisionOn_blobAddressExists() { + $title = Title::newFromText( 'UTPage' ); + $revDetails = [ + 'slot' => SlotRecord::newUnsaved( 'main', new WikitextContent( 'Chicken' ) ), + 'parent' => true, + 'comment' => $this->getRandomCommentStoreComment(), + 'timestamp' => '20171117010101', + 'user' => true, + ]; + + $store = MediaWikiServices::getInstance()->getRevisionStore(); + + // Insert the first revision + $revOne = $this->getRevisionRecordFromDetailsArray( $title, $revDetails ); + $firstReturn = $store->insertRevisionOn( $revOne, wfGetDB( DB_MASTER ) ); + $this->assertLinkTargetsEqual( $title, $firstReturn->getPageAsLinkTarget() ); + $this->assertRevisionRecordsEqual( $revOne, $firstReturn ); + + // Insert a second revision inheriting the same blob address + $revDetails['slot'] = SlotRecord::newInherited( $firstReturn->getSlot( 'main' ) ); + $revTwo = $this->getRevisionRecordFromDetailsArray( $title, $revDetails ); + $secondReturn = $store->insertRevisionOn( $revTwo, wfGetDB( DB_MASTER ) ); + $this->assertLinkTargetsEqual( $title, $secondReturn->getPageAsLinkTarget() ); + $this->assertRevisionRecordsEqual( $revTwo, $secondReturn ); + + // Assert that the same blob address has been used. + $this->assertEquals( + $firstReturn->getSlot( 'main' )->getAddress(), + $secondReturn->getSlot( 'main' )->getAddress() + ); + // And that different revisions have been created. + $this->assertNotSame( + $firstReturn->getId(), + $secondReturn->getId() + ); + } + + public function provideInsertRevisionOn_failures() { + yield 'no slot' => [ + Title::newFromText( 'UTPage' ), + [ + 'comment' => $this->getRandomCommentStoreComment(), + 'timestamp' => '20171117010101', + 'user' => true, + ], + new InvalidArgumentException( 'At least one slot needs to be defined!' ) + ]; + yield 'slot that is not main slot' => [ + Title::newFromText( 'UTPage' ), + [ + 'slot' => SlotRecord::newUnsaved( 'lalala', new WikitextContent( 'Chicken' ) ), + 'comment' => $this->getRandomCommentStoreComment(), + 'timestamp' => '20171117010101', + 'user' => true, + ], + new InvalidArgumentException( 'Only the main slot is supported for now!' ) + ]; + yield 'no timestamp' => [ + Title::newFromText( 'UTPage' ), + [ + 'slot' => SlotRecord::newUnsaved( 'main', new WikitextContent( 'Chicken' ) ), + 'comment' => $this->getRandomCommentStoreComment(), + 'user' => true, + ], + new IncompleteRevisionException( 'timestamp field must not be NULL!' ) + ]; + yield 'no comment' => [ + Title::newFromText( 'UTPage' ), + [ + 'slot' => SlotRecord::newUnsaved( 'main', new WikitextContent( 'Chicken' ) ), + 'timestamp' => '20171117010101', + 'user' => true, + ], + new IncompleteRevisionException( 'comment must not be NULL!' ) + ]; + yield 'no user' => [ + Title::newFromText( 'UTPage' ), + [ + 'slot' => SlotRecord::newUnsaved( 'main', new WikitextContent( 'Chicken' ) ), + 'comment' => $this->getRandomCommentStoreComment(), + 'timestamp' => '20171117010101', + ], + new IncompleteRevisionException( 'user must not be NULL!' ) + ]; + } + + /** + * @dataProvider provideInsertRevisionOn_failures + * @covers \MediaWiki\Storage\RevisionStore::insertRevisionOn + */ + public function testInsertRevisionOn_failures( + Title $title, + array $revDetails = [], + Exception $exception ) { + $rev = $this->getRevisionRecordFromDetailsArray( $title, $revDetails ); + + $store = MediaWikiServices::getInstance()->getRevisionStore(); + + $this->setExpectedException( + get_class( $exception ), + $exception->getMessage(), + $exception->getCode() + ); + $store->insertRevisionOn( $rev, wfGetDB( DB_MASTER ) ); + } + + public function provideNewNullRevision() { + yield [ + Title::newFromText( 'UTPage' ), + CommentStoreComment::newUnsavedComment( __METHOD__ . ' comment1' ), + true, + ]; + yield [ + Title::newFromText( 'UTPage' ), + CommentStoreComment::newUnsavedComment( __METHOD__ . ' comment2', [ 'a' => 1 ] ), + false, + ]; + } + + /** + * @dataProvider provideNewNullRevision + * @covers \MediaWiki\Storage\RevisionStore::newNullRevision + */ + public function testNewNullRevision( Title $title, $comment, $minor ) { + $store = MediaWikiServices::getInstance()->getRevisionStore(); + $user = TestUserRegistry::getMutableTestUser( __METHOD__ )->getUser(); + + $parent = $store->getRevisionByTitle( $title ); + $record = $store->newNullRevision( + wfGetDB( DB_MASTER ), + $title, + $comment, + $minor, + $user + ); + + $this->assertEquals( $title->getNamespace(), $record->getPageAsLinkTarget()->getNamespace() ); + $this->assertEquals( $title->getDBkey(), $record->getPageAsLinkTarget()->getDBkey() ); + $this->assertEquals( $comment, $record->getComment() ); + $this->assertEquals( $minor, $record->isMinor() ); + $this->assertEquals( $user->getName(), $record->getUser()->getName() ); + $this->assertEquals( $parent->getId(), $record->getParentId() ); + + $parentSlot = $parent->getSlot( 'main' ); + $slot = $record->getSlot( 'main' ); + + $this->assertTrue( $slot->isInherited(), 'isInherited' ); + $this->assertSame( $parentSlot->getOrigin(), $slot->getOrigin(), 'getOrigin' ); + $this->assertSame( $parentSlot->getAddress(), $slot->getAddress(), 'getAddress' ); + } + + /** + * @covers \MediaWiki\Storage\RevisionStore::newNullRevision + */ + public function testNewNullRevision_nonExistingTitle() { + $store = MediaWikiServices::getInstance()->getRevisionStore(); + $record = $store->newNullRevision( + wfGetDB( DB_MASTER ), + Title::newFromText( __METHOD__ . '.iDontExist!' ), + CommentStoreComment::newUnsavedComment( __METHOD__ . ' comment' ), + false, + TestUserRegistry::getMutableTestUser( __METHOD__ )->getUser() + ); + $this->assertNull( $record ); + } + + /** + * @covers \MediaWiki\Storage\RevisionStore::getRcIdIfUnpatrolled + */ + public function testGetRcIdIfUnpatrolled_returnsRecentChangesId() { + $page = WikiPage::factory( Title::newFromText( 'UTPage' ) ); + $status = $page->doEditContent( new WikitextContent( __METHOD__ ), __METHOD__ ); + /** @var Revision $rev */ + $rev = $status->value['revision']; + + $store = MediaWikiServices::getInstance()->getRevisionStore(); + $revisionRecord = $store->getRevisionById( $rev->getId() ); + $result = $store->getRcIdIfUnpatrolled( $revisionRecord ); + + $this->assertGreaterThan( 0, $result ); + $this->assertSame( + $page->getRevision()->getRecentChange()->getAttribute( 'rc_id' ), + $result + ); + } + + /** + * @covers \MediaWiki\Storage\RevisionStore::getRcIdIfUnpatrolled + */ + public function testGetRcIdIfUnpatrolled_returnsZeroIfPatrolled() { + // This assumes that sysops are auto patrolled + $sysop = $this->getTestSysop()->getUser(); + $page = WikiPage::factory( Title::newFromText( 'UTPage' ) ); + $status = $page->doEditContent( + new WikitextContent( __METHOD__ ), + __METHOD__, + 0, + false, + $sysop + ); + /** @var Revision $rev */ + $rev = $status->value['revision']; + + $store = MediaWikiServices::getInstance()->getRevisionStore(); + $revisionRecord = $store->getRevisionById( $rev->getId() ); + $result = $store->getRcIdIfUnpatrolled( $revisionRecord ); + + $this->assertSame( 0, $result ); + } + + /** + * @covers \MediaWiki\Storage\RevisionStore::getRecentChange + */ + public function testGetRecentChange() { + $page = WikiPage::factory( Title::newFromText( 'UTPage' ) ); + $content = new WikitextContent( __METHOD__ ); + $status = $page->doEditContent( $content, __METHOD__ ); + /** @var Revision $rev */ + $rev = $status->value['revision']; + + $store = MediaWikiServices::getInstance()->getRevisionStore(); + $revRecord = $store->getRevisionById( $rev->getId() ); + $recentChange = $store->getRecentChange( $revRecord ); + + $this->assertEquals( $rev->getId(), $recentChange->getAttribute( 'rc_this_oldid' ) ); + $this->assertEquals( $rev->getRecentChange(), $recentChange ); + } + + /** + * @covers \MediaWiki\Storage\RevisionStore::getRevisionById + */ + public function testGetRevisionById() { + $page = WikiPage::factory( Title::newFromText( 'UTPage' ) ); + $content = new WikitextContent( __METHOD__ ); + $status = $page->doEditContent( $content, __METHOD__ ); + /** @var Revision $rev */ + $rev = $status->value['revision']; + + $store = MediaWikiServices::getInstance()->getRevisionStore(); + $revRecord = $store->getRevisionById( $rev->getId() ); + + $this->assertSame( $rev->getId(), $revRecord->getId() ); + $this->assertTrue( $revRecord->getSlot( 'main' )->getContent()->equals( $content ) ); + $this->assertSame( __METHOD__, $revRecord->getComment()->text ); + } + + /** + * @covers \MediaWiki\Storage\RevisionStore::getRevisionByTitle + */ + public function testGetRevisionByTitle() { + $page = WikiPage::factory( Title::newFromText( 'UTPage' ) ); + $content = new WikitextContent( __METHOD__ ); + $status = $page->doEditContent( $content, __METHOD__ ); + /** @var Revision $rev */ + $rev = $status->value['revision']; + + $store = MediaWikiServices::getInstance()->getRevisionStore(); + $revRecord = $store->getRevisionByTitle( $page->getTitle() ); + + $this->assertSame( $rev->getId(), $revRecord->getId() ); + $this->assertTrue( $revRecord->getSlot( 'main' )->getContent()->equals( $content ) ); + $this->assertSame( __METHOD__, $revRecord->getComment()->text ); + } + + /** + * @covers \MediaWiki\Storage\RevisionStore::getRevisionByPageId + */ + public function testGetRevisionByPageId() { + $page = WikiPage::factory( Title::newFromText( 'UTPage' ) ); + $content = new WikitextContent( __METHOD__ ); + $status = $page->doEditContent( $content, __METHOD__ ); + /** @var Revision $rev */ + $rev = $status->value['revision']; + + $store = MediaWikiServices::getInstance()->getRevisionStore(); + $revRecord = $store->getRevisionByPageId( $page->getId() ); + + $this->assertSame( $rev->getId(), $revRecord->getId() ); + $this->assertTrue( $revRecord->getSlot( 'main' )->getContent()->equals( $content ) ); + $this->assertSame( __METHOD__, $revRecord->getComment()->text ); + } + + /** + * @covers \MediaWiki\Storage\RevisionStore::getRevisionByTimestamp + */ + public function testGetRevisionByTimestamp() { + // Make sure there is 1 second between the last revision and the rev we create... + // Otherwise we might not get the correct revision and the test may fail... + // :( + sleep( 1 ); + $page = WikiPage::factory( Title::newFromText( 'UTPage' ) ); + $content = new WikitextContent( __METHOD__ ); + $status = $page->doEditContent( $content, __METHOD__ ); + /** @var Revision $rev */ + $rev = $status->value['revision']; + + $store = MediaWikiServices::getInstance()->getRevisionStore(); + $revRecord = $store->getRevisionByTimestamp( + $page->getTitle(), + $rev->getTimestamp() + ); + + $this->assertSame( $rev->getId(), $revRecord->getId() ); + $this->assertTrue( $revRecord->getSlot( 'main' )->getContent()->equals( $content ) ); + $this->assertSame( __METHOD__, $revRecord->getComment()->text ); + } + + private function revisionToRow( Revision $rev ) { + $page = WikiPage::factory( $rev->getTitle() ); + + return (object)[ + 'rev_id' => (string)$rev->getId(), + 'rev_page' => (string)$rev->getPage(), + 'rev_text_id' => (string)$rev->getTextId(), + 'rev_timestamp' => (string)$rev->getTimestamp(), + 'rev_user_text' => (string)$rev->getUserText(), + 'rev_user' => (string)$rev->getUser(), + 'rev_minor_edit' => $rev->isMinor() ? '1' : '0', + 'rev_deleted' => (string)$rev->getVisibility(), + 'rev_len' => (string)$rev->getSize(), + 'rev_parent_id' => (string)$rev->getParentId(), + 'rev_sha1' => (string)$rev->getSha1(), + 'rev_comment_text' => $rev->getComment(), + 'rev_comment_data' => null, + 'rev_comment_cid' => null, + 'rev_content_format' => $rev->getContentFormat(), + 'rev_content_model' => $rev->getContentModel(), + 'page_namespace' => (string)$page->getTitle()->getNamespace(), + 'page_title' => $page->getTitle()->getDBkey(), + 'page_id' => (string)$page->getId(), + 'page_latest' => (string)$page->getLatest(), + 'page_is_redirect' => $page->isRedirect() ? '1' : '0', + 'page_len' => (string)$page->getContent()->getSize(), + 'user_name' => (string)$rev->getUserText(), + ]; + } + + private function assertRevisionRecordMatchesRevision( + Revision $rev, + RevisionRecord $record + ) { + $this->assertSame( $rev->getId(), $record->getId() ); + $this->assertSame( $rev->getPage(), $record->getPageId() ); + $this->assertSame( $rev->getTimestamp(), $record->getTimestamp() ); + $this->assertSame( $rev->getUserText(), $record->getUser()->getName() ); + $this->assertSame( $rev->getUser(), $record->getUser()->getId() ); + $this->assertSame( $rev->isMinor(), $record->isMinor() ); + $this->assertSame( $rev->getVisibility(), $record->getVisibility() ); + $this->assertSame( $rev->getSize(), $record->getSize() ); + /** + * @note As of MW 1.31, the database schema allows the parent ID to be + * NULL to indicate that it is unknown. + */ + $expectedParent = $rev->getParentId(); + if ( $expectedParent === null ) { + $expectedParent = 0; + } + $this->assertSame( $expectedParent, $record->getParentId() ); + $this->assertSame( $rev->getSha1(), $record->getSha1() ); + $this->assertSame( $rev->getComment(), $record->getComment()->text ); + $this->assertSame( $rev->getContentFormat(), $record->getContent( 'main' )->getDefaultFormat() ); + $this->assertSame( $rev->getContentModel(), $record->getContent( 'main' )->getModel() ); + $this->assertLinkTargetsEqual( $rev->getTitle(), $record->getPageAsLinkTarget() ); + } + + /** + * @covers \MediaWiki\Storage\RevisionStore::newRevisionFromRow + * @covers \MediaWiki\Storage\RevisionStore::newRevisionFromRow_1_29 + */ + public function testNewRevisionFromRow_anonEdit() { + $this->setMwGlobals( 'wgActorTableSchemaMigrationStage', MIGRATION_WRITE_BOTH ); + $this->overrideMwServices(); + + $page = WikiPage::factory( Title::newFromText( 'UTPage' ) ); + $text = __METHOD__ . 'a-ä'; + /** @var Revision $rev */ + $rev = $page->doEditContent( + new WikitextContent( $text ), + __METHOD__ . 'a' + )->value['revision']; + + $store = MediaWikiServices::getInstance()->getRevisionStore(); + $record = $store->newRevisionFromRow( + $this->revisionToRow( $rev ), + [], + $page->getTitle() + ); + $this->assertRevisionRecordMatchesRevision( $rev, $record ); + $this->assertSame( $text, $rev->getContent()->serialize() ); + } + + /** + * @covers \MediaWiki\Storage\RevisionStore::newRevisionFromRow + * @covers \MediaWiki\Storage\RevisionStore::newRevisionFromRow_1_29 + */ + public function testNewRevisionFromRow_anonEdit_legacyEncoding() { + $this->setMwGlobals( 'wgLegacyEncoding', 'windows-1252' ); + $this->overrideMwServices(); + $page = WikiPage::factory( Title::newFromText( 'UTPage' ) ); + $text = __METHOD__ . 'a-ä'; + /** @var Revision $rev */ + $rev = $page->doEditContent( + new WikitextContent( $text ), + __METHOD__. 'a' + )->value['revision']; + + $store = MediaWikiServices::getInstance()->getRevisionStore(); + $record = $store->newRevisionFromRow( + $this->revisionToRow( $rev ), + [], + $page->getTitle() + ); + $this->assertRevisionRecordMatchesRevision( $rev, $record ); + $this->assertSame( $text, $rev->getContent()->serialize() ); + } + + /** + * @covers \MediaWiki\Storage\RevisionStore::newRevisionFromRow + * @covers \MediaWiki\Storage\RevisionStore::newRevisionFromRow_1_29 + */ + public function testNewRevisionFromRow_userEdit() { + $this->setMwGlobals( 'wgActorTableSchemaMigrationStage', MIGRATION_WRITE_BOTH ); + $this->overrideMwServices(); + + $page = WikiPage::factory( Title::newFromText( 'UTPage' ) ); + $text = __METHOD__ . 'b-ä'; + /** @var Revision $rev */ + $rev = $page->doEditContent( + new WikitextContent( $text ), + __METHOD__ . 'b', + 0, + false, + $this->getTestUser()->getUser() + )->value['revision']; + + $store = MediaWikiServices::getInstance()->getRevisionStore(); + $record = $store->newRevisionFromRow( + $this->revisionToRow( $rev ), + [], + $page->getTitle() + ); + $this->assertRevisionRecordMatchesRevision( $rev, $record ); + $this->assertSame( $text, $rev->getContent()->serialize() ); + } + + /** + * @covers \MediaWiki\Storage\RevisionStore::newRevisionFromArchiveRow + */ + public function testNewRevisionFromArchiveRow() { + $store = MediaWikiServices::getInstance()->getRevisionStore(); + $title = Title::newFromText( __METHOD__ ); + $text = __METHOD__ . '-bä'; + $page = WikiPage::factory( $title ); + /** @var Revision $orig */ + $orig = $page->doEditContent( new WikitextContent( $text ), __METHOD__ ) + ->value['revision']; + $page->doDeleteArticle( __METHOD__ ); + + $db = wfGetDB( DB_MASTER ); + $arQuery = $store->getArchiveQueryInfo(); + $res = $db->select( + $arQuery['tables'], $arQuery['fields'], [ 'ar_rev_id' => $orig->getId() ], + __METHOD__, [], $arQuery['joins'] + ); + $this->assertTrue( is_object( $res ), 'query failed' ); + + $row = $res->fetchObject(); + $res->free(); + $record = $store->newRevisionFromArchiveRow( $row ); + + $this->assertRevisionRecordMatchesRevision( $orig, $record ); + $this->assertSame( $text, $record->getContent( 'main' )->serialize() ); + } + + /** + * @covers \MediaWiki\Storage\RevisionStore::newRevisionFromArchiveRow + */ + public function testNewRevisionFromArchiveRow_legacyEncoding() { + $this->setMwGlobals( 'wgLegacyEncoding', 'windows-1252' ); + $this->overrideMwServices(); + $store = MediaWikiServices::getInstance()->getRevisionStore(); + $title = Title::newFromText( __METHOD__ ); + $text = __METHOD__ . '-bä'; + $page = WikiPage::factory( $title ); + /** @var Revision $orig */ + $orig = $page->doEditContent( new WikitextContent( $text ), __METHOD__ ) + ->value['revision']; + $page->doDeleteArticle( __METHOD__ ); + + $db = wfGetDB( DB_MASTER ); + $arQuery = $store->getArchiveQueryInfo(); + $res = $db->select( + $arQuery['tables'], $arQuery['fields'], [ 'ar_rev_id' => $orig->getId() ], + __METHOD__, [], $arQuery['joins'] + ); + $this->assertTrue( is_object( $res ), 'query failed' ); + + $row = $res->fetchObject(); + $res->free(); + $record = $store->newRevisionFromArchiveRow( $row ); + + $this->assertRevisionRecordMatchesRevision( $orig, $record ); + $this->assertSame( $text, $record->getContent( 'main' )->serialize() ); + } + + /** + * @covers \MediaWiki\Storage\RevisionStore::loadRevisionFromId + */ + public function testLoadRevisionFromId() { + $title = Title::newFromText( __METHOD__ ); + $page = WikiPage::factory( $title ); + /** @var Revision $rev */ + $rev = $page->doEditContent( new WikitextContent( __METHOD__ ), __METHOD__ ) + ->value['revision']; + + $store = MediaWikiServices::getInstance()->getRevisionStore(); + $result = $store->loadRevisionFromId( wfGetDB( DB_MASTER ), $rev->getId() ); + $this->assertRevisionRecordMatchesRevision( $rev, $result ); + } + + /** + * @covers \MediaWiki\Storage\RevisionStore::loadRevisionFromPageId + */ + public function testLoadRevisionFromPageId() { + $title = Title::newFromText( __METHOD__ ); + $page = WikiPage::factory( $title ); + /** @var Revision $rev */ + $rev = $page->doEditContent( new WikitextContent( __METHOD__ ), __METHOD__ ) + ->value['revision']; + + $store = MediaWikiServices::getInstance()->getRevisionStore(); + $result = $store->loadRevisionFromPageId( wfGetDB( DB_MASTER ), $page->getId() ); + $this->assertRevisionRecordMatchesRevision( $rev, $result ); + } + + /** + * @covers \MediaWiki\Storage\RevisionStore::loadRevisionFromTitle + */ + public function testLoadRevisionFromTitle() { + $title = Title::newFromText( __METHOD__ ); + $page = WikiPage::factory( $title ); + /** @var Revision $rev */ + $rev = $page->doEditContent( new WikitextContent( __METHOD__ ), __METHOD__ ) + ->value['revision']; + + $store = MediaWikiServices::getInstance()->getRevisionStore(); + $result = $store->loadRevisionFromTitle( wfGetDB( DB_MASTER ), $title ); + $this->assertRevisionRecordMatchesRevision( $rev, $result ); + } + + /** + * @covers \MediaWiki\Storage\RevisionStore::loadRevisionFromTimestamp + */ + public function testLoadRevisionFromTimestamp() { + $title = Title::newFromText( __METHOD__ ); + $page = WikiPage::factory( $title ); + /** @var Revision $revOne */ + $revOne = $page->doEditContent( new WikitextContent( __METHOD__ ), __METHOD__ ) + ->value['revision']; + // Sleep to ensure different timestamps... )(evil) + sleep( 1 ); + /** @var Revision $revTwo */ + $revTwo = $page->doEditContent( new WikitextContent( __METHOD__ . 'a' ), '' ) + ->value['revision']; + + $store = MediaWikiServices::getInstance()->getRevisionStore(); + $this->assertNull( + $store->loadRevisionFromTimestamp( wfGetDB( DB_MASTER ), $title, '20150101010101' ) + ); + $this->assertSame( + $revOne->getId(), + $store->loadRevisionFromTimestamp( + wfGetDB( DB_MASTER ), + $title, + $revOne->getTimestamp() + )->getId() + ); + $this->assertSame( + $revTwo->getId(), + $store->loadRevisionFromTimestamp( + wfGetDB( DB_MASTER ), + $title, + $revTwo->getTimestamp() + )->getId() + ); + } + + /** + * @covers \MediaWiki\Storage\RevisionStore::listRevisionSizes + */ + public function testGetParentLengths() { + $page = WikiPage::factory( Title::newFromText( __METHOD__ ) ); + /** @var Revision $revOne */ + $revOne = $page->doEditContent( + new WikitextContent( __METHOD__ ), __METHOD__ + )->value['revision']; + /** @var Revision $revTwo */ + $revTwo = $page->doEditContent( + new WikitextContent( __METHOD__ . '2' ), __METHOD__ + )->value['revision']; + + $store = MediaWikiServices::getInstance()->getRevisionStore(); + $this->assertSame( + [ + $revOne->getId() => strlen( __METHOD__ ), + ], + $store->listRevisionSizes( + wfGetDB( DB_MASTER ), + [ $revOne->getId() ] + ) + ); + $this->assertSame( + [ + $revOne->getId() => strlen( __METHOD__ ), + $revTwo->getId() => strlen( __METHOD__ ) + 1, + ], + $store->listRevisionSizes( + wfGetDB( DB_MASTER ), + [ $revOne->getId(), $revTwo->getId() ] + ) + ); + } + + /** + * @covers \MediaWiki\Storage\RevisionStore::getPreviousRevision + */ + public function testGetPreviousRevision() { + $page = WikiPage::factory( Title::newFromText( __METHOD__ ) ); + /** @var Revision $revOne */ + $revOne = $page->doEditContent( + new WikitextContent( __METHOD__ ), __METHOD__ + )->value['revision']; + /** @var Revision $revTwo */ + $revTwo = $page->doEditContent( + new WikitextContent( __METHOD__ . '2' ), __METHOD__ + )->value['revision']; + + $store = MediaWikiServices::getInstance()->getRevisionStore(); + $this->assertNull( + $store->getPreviousRevision( $store->getRevisionById( $revOne->getId() ) ) + ); + $this->assertSame( + $revOne->getId(), + $store->getPreviousRevision( $store->getRevisionById( $revTwo->getId() ) )->getId() + ); + } + + /** + * @covers \MediaWiki\Storage\RevisionStore::getNextRevision + */ + public function testGetNextRevision() { + $page = WikiPage::factory( Title::newFromText( __METHOD__ ) ); + /** @var Revision $revOne */ + $revOne = $page->doEditContent( + new WikitextContent( __METHOD__ ), __METHOD__ + )->value['revision']; + /** @var Revision $revTwo */ + $revTwo = $page->doEditContent( + new WikitextContent( __METHOD__ . '2' ), __METHOD__ + )->value['revision']; + + $store = MediaWikiServices::getInstance()->getRevisionStore(); + $this->assertSame( + $revTwo->getId(), + $store->getNextRevision( $store->getRevisionById( $revOne->getId() ) )->getId() + ); + $this->assertNull( + $store->getNextRevision( $store->getRevisionById( $revTwo->getId() ) ) + ); + } + + /** + * @covers \MediaWiki\Storage\RevisionStore::getTimestampFromId + */ + public function testGetTimestampFromId_found() { + $page = WikiPage::factory( Title::newFromText( 'UTPage' ) ); + /** @var Revision $rev */ + $rev = $page->doEditContent( new WikitextContent( __METHOD__ ), __METHOD__ ) + ->value['revision']; + + $store = MediaWikiServices::getInstance()->getRevisionStore(); + $result = $store->getTimestampFromId( + $page->getTitle(), + $rev->getId() + ); + + $this->assertSame( $rev->getTimestamp(), $result ); + } + + /** + * @covers \MediaWiki\Storage\RevisionStore::getTimestampFromId + */ + public function testGetTimestampFromId_notFound() { + $page = WikiPage::factory( Title::newFromText( 'UTPage' ) ); + /** @var Revision $rev */ + $rev = $page->doEditContent( new WikitextContent( __METHOD__ ), __METHOD__ ) + ->value['revision']; + + $store = MediaWikiServices::getInstance()->getRevisionStore(); + $result = $store->getTimestampFromId( + $page->getTitle(), + $rev->getId() + 1 + ); + + $this->assertFalse( $result ); + } + + /** + * @covers \MediaWiki\Storage\RevisionStore::countRevisionsByPageId + */ + public function testCountRevisionsByPageId() { + $store = MediaWikiServices::getInstance()->getRevisionStore(); + $page = WikiPage::factory( Title::newFromText( __METHOD__ ) ); + + $this->assertSame( + 0, + $store->countRevisionsByPageId( wfGetDB( DB_MASTER ), $page->getId() ) + ); + $page->doEditContent( new WikitextContent( 'a' ), 'a' ); + $this->assertSame( + 1, + $store->countRevisionsByPageId( wfGetDB( DB_MASTER ), $page->getId() ) + ); + $page->doEditContent( new WikitextContent( 'b' ), 'b' ); + $this->assertSame( + 2, + $store->countRevisionsByPageId( wfGetDB( DB_MASTER ), $page->getId() ) + ); + } + + /** + * @covers \MediaWiki\Storage\RevisionStore::countRevisionsByTitle + */ + public function testCountRevisionsByTitle() { + $store = MediaWikiServices::getInstance()->getRevisionStore(); + $page = WikiPage::factory( Title::newFromText( __METHOD__ ) ); + + $this->assertSame( + 0, + $store->countRevisionsByTitle( wfGetDB( DB_MASTER ), $page->getTitle() ) + ); + $page->doEditContent( new WikitextContent( 'a' ), 'a' ); + $this->assertSame( + 1, + $store->countRevisionsByTitle( wfGetDB( DB_MASTER ), $page->getTitle() ) + ); + $page->doEditContent( new WikitextContent( 'b' ), 'b' ); + $this->assertSame( + 2, + $store->countRevisionsByTitle( wfGetDB( DB_MASTER ), $page->getTitle() ) + ); + } + + /** + * @covers \MediaWiki\Storage\RevisionStore::userWasLastToEdit + */ + public function testUserWasLastToEdit_false() { + $sysop = $this->getTestSysop()->getUser(); + $page = WikiPage::factory( Title::newFromText( 'UTPage' ) ); + $page->doEditContent( new WikitextContent( __METHOD__ ), __METHOD__ ); + + $store = MediaWikiServices::getInstance()->getRevisionStore(); + $result = $store->userWasLastToEdit( + wfGetDB( DB_MASTER ), + $page->getId(), + $sysop->getId(), + '20160101010101' + ); + $this->assertFalse( $result ); + } + + /** + * @covers \MediaWiki\Storage\RevisionStore::userWasLastToEdit + */ + public function testUserWasLastToEdit_true() { + $startTime = wfTimestampNow(); + $sysop = $this->getTestSysop()->getUser(); + $page = WikiPage::factory( Title::newFromText( 'UTPage' ) ); + $page->doEditContent( + new WikitextContent( __METHOD__ ), + __METHOD__, + 0, + false, + $sysop + ); + + $store = MediaWikiServices::getInstance()->getRevisionStore(); + $result = $store->userWasLastToEdit( + wfGetDB( DB_MASTER ), + $page->getId(), + $sysop->getId(), + $startTime + ); + $this->assertTrue( $result ); + } + + /** + * @covers \MediaWiki\Storage\RevisionStore::getKnownCurrentRevision + */ + public function testGetKnownCurrentRevision() { + $page = WikiPage::factory( Title::newFromText( 'UTPage' ) ); + /** @var Revision $rev */ + $rev = $page->doEditContent( + new WikitextContent( __METHOD__ . 'b' ), + __METHOD__ . 'b', + 0, + false, + $this->getTestUser()->getUser() + )->value['revision']; + + $store = MediaWikiServices::getInstance()->getRevisionStore(); + $record = $store->getKnownCurrentRevision( + $page->getTitle(), + $rev->getId() + ); + + $this->assertRevisionRecordMatchesRevision( $rev, $record ); + } + + public function provideNewMutableRevisionFromArray() { + yield 'Basic array, with page & id' => [ + [ + 'id' => 2, + 'page' => 1, + 'text_id' => 2, + 'timestamp' => '20171017114835', + 'user_text' => '111.0.1.2', + 'user' => 0, + 'minor_edit' => false, + 'deleted' => 0, + 'len' => 46, + 'parent_id' => 1, + 'sha1' => 'rdqbbzs3pkhihgbs8qf2q9jsvheag5z', + 'comment' => 'Goat Comment!', + 'content_format' => 'text/x-wiki', + 'content_model' => 'wikitext', + ] + ]; + yield 'Basic array, content object' => [ + [ + 'id' => 2, + 'page' => 1, + 'timestamp' => '20171017114835', + 'user_text' => '111.0.1.2', + 'user' => 0, + 'minor_edit' => false, + 'deleted' => 0, + 'len' => 46, + 'parent_id' => 1, + 'sha1' => 'rdqbbzs3pkhihgbs8qf2q9jsvheag5z', + 'comment' => 'Goat Comment!', + 'content' => new WikitextContent( 'Some Content' ), + ] + ]; + yield 'Basic array, serialized text' => [ + [ + 'id' => 2, + 'page' => 1, + 'timestamp' => '20171017114835', + 'user_text' => '111.0.1.2', + 'user' => 0, + 'minor_edit' => false, + 'deleted' => 0, + 'len' => 46, + 'parent_id' => 1, + 'sha1' => 'rdqbbzs3pkhihgbs8qf2q9jsvheag5z', + 'comment' => 'Goat Comment!', + 'text' => ( new WikitextContent( 'Söme Content' ) )->serialize(), + ] + ]; + yield 'Basic array, serialized text, utf-8 flags' => [ + [ + 'id' => 2, + 'page' => 1, + 'timestamp' => '20171017114835', + 'user_text' => '111.0.1.2', + 'user' => 0, + 'minor_edit' => false, + 'deleted' => 0, + 'len' => 46, + 'parent_id' => 1, + 'sha1' => 'rdqbbzs3pkhihgbs8qf2q9jsvheag5z', + 'comment' => 'Goat Comment!', + 'text' => ( new WikitextContent( 'Söme Content' ) )->serialize(), + 'flags' => 'utf-8', + ] + ]; + yield 'Basic array, with title' => [ + [ + 'title' => Title::newFromText( 'SomeText' ), + 'text_id' => 2, + 'timestamp' => '20171017114835', + 'user_text' => '111.0.1.2', + 'user' => 0, + 'minor_edit' => false, + 'deleted' => 0, + 'len' => 46, + 'parent_id' => 1, + 'sha1' => 'rdqbbzs3pkhihgbs8qf2q9jsvheag5z', + 'comment' => 'Goat Comment!', + 'content_format' => 'text/x-wiki', + 'content_model' => 'wikitext', + ] + ]; + yield 'Basic array, no user field' => [ + [ + 'id' => 2, + 'page' => 1, + 'text_id' => 2, + 'timestamp' => '20171017114835', + 'user_text' => '111.0.1.3', + 'minor_edit' => false, + 'deleted' => 0, + 'len' => 46, + 'parent_id' => 1, + 'sha1' => 'rdqbbzs3pkhihgbs8qf2q9jsvheag5z', + 'comment' => 'Goat Comment!', + 'content_format' => 'text/x-wiki', + 'content_model' => 'wikitext', + ] + ]; + } + + /** + * @dataProvider provideNewMutableRevisionFromArray + * @covers \MediaWiki\Storage\RevisionStore::newMutableRevisionFromArray + */ + public function testNewMutableRevisionFromArray( array $array ) { + $store = MediaWikiServices::getInstance()->getRevisionStore(); + + $result = $store->newMutableRevisionFromArray( $array ); + + if ( isset( $array['id'] ) ) { + $this->assertSame( $array['id'], $result->getId() ); + } + if ( isset( $array['page'] ) ) { + $this->assertSame( $array['page'], $result->getPageId() ); + } + $this->assertSame( $array['timestamp'], $result->getTimestamp() ); + $this->assertSame( $array['user_text'], $result->getUser()->getName() ); + if ( isset( $array['user'] ) ) { + $this->assertSame( $array['user'], $result->getUser()->getId() ); + } + $this->assertSame( (bool)$array['minor_edit'], $result->isMinor() ); + $this->assertSame( $array['deleted'], $result->getVisibility() ); + $this->assertSame( $array['len'], $result->getSize() ); + $this->assertSame( $array['parent_id'], $result->getParentId() ); + $this->assertSame( $array['sha1'], $result->getSha1() ); + $this->assertSame( $array['comment'], $result->getComment()->text ); + if ( isset( $array['content'] ) ) { + $this->assertTrue( + $result->getSlot( 'main' )->getContent()->equals( $array['content'] ) + ); + } elseif ( isset( $array['text'] ) ) { + $this->assertSame( $array['text'], $result->getSlot( 'main' )->getContent()->serialize() ); + } else { + $this->assertSame( + $array['content_format'], + $result->getSlot( 'main' )->getContent()->getDefaultFormat() + ); + $this->assertSame( $array['content_model'], $result->getSlot( 'main' )->getModel() ); + } + } + + /** + * @dataProvider provideNewMutableRevisionFromArray + * @covers \MediaWiki\Storage\RevisionStore::newMutableRevisionFromArray + */ + public function testNewMutableRevisionFromArray_legacyEncoding( array $array ) { + $cache = new WANObjectCache( [ 'cache' => new HashBagOStuff() ] ); + $blobStore = new SqlBlobStore( wfGetLB(), $cache ); + $blobStore->setLegacyEncoding( 'windows-1252', Language::factory( 'en' ) ); + + $factory = $this->getMockBuilder( BlobStoreFactory::class ) + ->setMethods( [ 'newBlobStore', 'newSqlBlobStore' ] ) + ->disableOriginalConstructor() + ->getMock(); + $factory->expects( $this->any() ) + ->method( 'newBlobStore' ) + ->willReturn( $blobStore ); + $factory->expects( $this->any() ) + ->method( 'newSqlBlobStore' ) + ->willReturn( $blobStore ); + + $this->setService( 'BlobStoreFactory', $factory ); + + $this->testNewMutableRevisionFromArray( $array ); + } + +} diff --git a/www/wiki/tests/phpunit/includes/Storage/RevisionStoreRecordTest.php b/www/wiki/tests/phpunit/includes/Storage/RevisionStoreRecordTest.php new file mode 100644 index 00000000..0295e900 --- /dev/null +++ b/www/wiki/tests/phpunit/includes/Storage/RevisionStoreRecordTest.php @@ -0,0 +1,363 @@ +<?php + +namespace MediaWiki\Tests\Storage; + +use CommentStoreComment; +use InvalidArgumentException; +use MediaWiki\Storage\RevisionRecord; +use MediaWiki\Storage\RevisionSlots; +use MediaWiki\Storage\RevisionStoreRecord; +use MediaWiki\Storage\SlotRecord; +use MediaWiki\User\UserIdentity; +use MediaWiki\User\UserIdentityValue; +use MediaWikiTestCase; +use TextContent; +use Title; + +/** + * @covers \MediaWiki\Storage\RevisionStoreRecord + * @covers \MediaWiki\Storage\RevisionRecord + */ +class RevisionStoreRecordTest extends MediaWikiTestCase { + + use RevisionRecordTests; + + /** + * @param array $rowOverrides + * + * @return RevisionStoreRecord + */ + protected function newRevision( array $rowOverrides = [] ) { + $title = Title::newFromText( 'Dummy' ); + $title->resetArticleID( 17 ); + + $user = new UserIdentityValue( 11, 'Tester', 0 ); + $comment = CommentStoreComment::newUnsavedComment( 'Hello World' ); + + $main = SlotRecord::newUnsaved( 'main', new TextContent( 'Lorem Ipsum' ) ); + $aux = SlotRecord::newUnsaved( 'aux', new TextContent( 'Frumious Bandersnatch' ) ); + $slots = new RevisionSlots( [ $main, $aux ] ); + + $row = [ + 'rev_id' => '7', + 'rev_page' => strval( $title->getArticleID() ), + 'rev_timestamp' => '20200101000000', + 'rev_deleted' => 0, + 'rev_minor_edit' => 0, + 'rev_parent_id' => '5', + 'rev_len' => $slots->computeSize(), + 'rev_sha1' => $slots->computeSha1(), + 'page_latest' => '18', + ]; + + $row = array_merge( $row, $rowOverrides ); + + return new RevisionStoreRecord( $title, $user, $comment, (object)$row, $slots ); + } + + public function provideConstructor() { + $title = Title::newFromText( 'Dummy' ); + $title->resetArticleID( 17 ); + + $user = new UserIdentityValue( 11, 'Tester', 0 ); + $comment = CommentStoreComment::newUnsavedComment( 'Hello World' ); + + $main = SlotRecord::newUnsaved( 'main', new TextContent( 'Lorem Ipsum' ) ); + $aux = SlotRecord::newUnsaved( 'aux', new TextContent( 'Frumious Bandersnatch' ) ); + $slots = new RevisionSlots( [ $main, $aux ] ); + + $protoRow = [ + 'rev_id' => '7', + 'rev_page' => strval( $title->getArticleID() ), + 'rev_timestamp' => '20200101000000', + 'rev_deleted' => 0, + 'rev_minor_edit' => 0, + 'rev_parent_id' => '5', + 'rev_len' => $slots->computeSize(), + 'rev_sha1' => $slots->computeSha1(), + 'page_latest' => '18', + ]; + + $row = $protoRow; + yield 'all info' => [ + $title, + $user, + $comment, + (object)$row, + $slots, + 'acmewiki' + ]; + + $row = $protoRow; + $row['rev_minor_edit'] = '1'; + $row['rev_deleted'] = strval( RevisionRecord::DELETED_USER ); + + yield 'minor deleted' => [ + $title, + $user, + $comment, + (object)$row, + $slots + ]; + + $row = $protoRow; + $row['page_latest'] = $row['rev_id']; + + yield 'latest' => [ + $title, + $user, + $comment, + (object)$row, + $slots + ]; + + $row = $protoRow; + unset( $row['rev_parent'] ); + + yield 'no parent' => [ + $title, + $user, + $comment, + (object)$row, + $slots + ]; + + $row = $protoRow; + $row['rev_len'] = null; + $row['rev_sha1'] = ''; + + yield 'rev_len is null, rev_sha1 is ""' => [ + $title, + $user, + $comment, + (object)$row, + $slots + ]; + + $row = $protoRow; + yield 'no length, no hash' => [ + Title::newFromText( 'DummyDoesNotExist' ), + $user, + $comment, + (object)$row, + $slots + ]; + } + + /** + * @dataProvider provideConstructor + * + * @param Title $title + * @param UserIdentity $user + * @param CommentStoreComment $comment + * @param object $row + * @param RevisionSlots $slots + * @param bool $wikiId + */ + public function testConstructorAndGetters( + Title $title, + UserIdentity $user, + CommentStoreComment $comment, + $row, + RevisionSlots $slots, + $wikiId = false + ) { + $rec = new RevisionStoreRecord( $title, $user, $comment, $row, $slots, $wikiId ); + + $this->assertSame( $title, $rec->getPageAsLinkTarget(), 'getPageAsLinkTarget' ); + $this->assertSame( $user, $rec->getUser( RevisionRecord::RAW ), 'getUser' ); + $this->assertSame( $comment, $rec->getComment(), 'getComment' ); + + $this->assertSame( $slots->getSlotRoles(), $rec->getSlotRoles(), 'getSlotRoles' ); + $this->assertSame( $wikiId, $rec->getWikiId(), 'getWikiId' ); + + $this->assertSame( (int)$row->rev_id, $rec->getId(), 'getId' ); + $this->assertSame( (int)$row->rev_page, $rec->getPageId(), 'getId' ); + $this->assertSame( $row->rev_timestamp, $rec->getTimestamp(), 'getTimestamp' ); + $this->assertSame( (int)$row->rev_deleted, $rec->getVisibility(), 'getVisibility' ); + $this->assertSame( (bool)$row->rev_minor_edit, $rec->isMinor(), 'getIsMinor' ); + + if ( isset( $row->rev_parent_id ) ) { + $this->assertSame( (int)$row->rev_parent_id, $rec->getParentId(), 'getParentId' ); + } else { + $this->assertSame( 0, $rec->getParentId(), 'getParentId' ); + } + + if ( isset( $row->rev_len ) ) { + $this->assertSame( (int)$row->rev_len, $rec->getSize(), 'getSize' ); + } else { + $this->assertSame( $slots->computeSize(), $rec->getSize(), 'getSize' ); + } + + if ( !empty( $row->rev_sha1 ) ) { + $this->assertSame( $row->rev_sha1, $rec->getSha1(), 'getSha1' ); + } else { + $this->assertSame( $slots->computeSha1(), $rec->getSha1(), 'getSha1' ); + } + + if ( isset( $row->page_latest ) ) { + $this->assertSame( + (int)$row->rev_id === (int)$row->page_latest, + $rec->isCurrent(), + 'isCurrent' + ); + } else { + $this->assertSame( + false, + $rec->isCurrent(), + 'isCurrent' + ); + } + } + + public function provideConstructorFailure() { + $title = Title::newFromText( 'Dummy' ); + $title->resetArticleID( 17 ); + + $user = new UserIdentityValue( 11, 'Tester', 0 ); + + $comment = CommentStoreComment::newUnsavedComment( 'Hello World' ); + + $main = SlotRecord::newUnsaved( 'main', new TextContent( 'Lorem Ipsum' ) ); + $aux = SlotRecord::newUnsaved( 'aux', new TextContent( 'Frumious Bandersnatch' ) ); + $slots = new RevisionSlots( [ $main, $aux ] ); + + $protoRow = [ + 'rev_id' => '7', + 'rev_page' => strval( $title->getArticleID() ), + 'rev_timestamp' => '20200101000000', + 'rev_deleted' => 0, + 'rev_minor_edit' => 0, + 'rev_parent_id' => '5', + 'rev_len' => $slots->computeSize(), + 'rev_sha1' => $slots->computeSha1(), + 'page_latest' => '18', + ]; + + yield 'not a row' => [ + $title, + $user, + $comment, + 'not a row', + $slots, + 'acmewiki' + ]; + + $row = $protoRow; + $row['rev_timestamp'] = 'kittens'; + + yield 'bad timestamp' => [ + $title, + $user, + $comment, + (object)$row, + $slots + ]; + + $row = $protoRow; + $row['rev_page'] = 99; + + yield 'page ID mismatch' => [ + $title, + $user, + $comment, + (object)$row, + $slots + ]; + + $row = $protoRow; + + yield 'bad wiki' => [ + $title, + $user, + $comment, + (object)$row, + $slots, + 12345 + ]; + } + + /** + * @dataProvider provideConstructorFailure + * + * @param Title $title + * @param UserIdentity $user + * @param CommentStoreComment $comment + * @param object $row + * @param RevisionSlots $slots + * @param bool $wikiId + */ + public function testConstructorFailure( + Title $title, + UserIdentity $user, + CommentStoreComment $comment, + $row, + RevisionSlots $slots, + $wikiId = false + ) { + $this->setExpectedException( InvalidArgumentException::class ); + new RevisionStoreRecord( $title, $user, $comment, $row, $slots, $wikiId ); + } + + public function provideIsCurrent() { + yield [ + [ + 'rev_id' => 11, + 'page_latest' => 11, + ], + true, + ]; + yield [ + [ + 'rev_id' => 11, + 'page_latest' => 10, + ], + false, + ]; + } + + /** + * @dataProvider provideIsCurrent + */ + public function testIsCurrent( $row, $current ) { + $rev = $this->newRevision( $row ); + + $this->assertSame( $current, $rev->isCurrent(), 'isCurrent()' ); + } + + public function provideGetSlot_audience_latest() { + return $this->provideAudienceCheckData( RevisionRecord::DELETED_TEXT ); + } + + /** + * @dataProvider provideGetSlot_audience_latest + */ + public function testGetSlot_audience_latest( $visibility, $groups, $userCan, $publicCan ) { + $this->forceStandardPermissions(); + + $user = $this->getTestUser( $groups )->getUser(); + $rev = $this->newRevision( + [ + 'rev_deleted' => $visibility, + 'rev_id' => 11, + 'page_latest' => 11, // revision is current + ] + ); + + // NOTE: slot meta-data is never suppressed, just the content is! + $this->assertNotNull( $rev->getSlot( 'main', RevisionRecord::RAW ), 'raw can' ); + $this->assertNotNull( $rev->getSlot( 'main', RevisionRecord::FOR_PUBLIC ), 'public can' ); + + $this->assertNotNull( + $rev->getSlot( 'main', RevisionRecord::FOR_THIS_USER, $user ), + 'user can' + ); + + $rev->getSlot( 'main', RevisionRecord::RAW )->getContent(); + // NOTE: the content of the current revision is never suppressed! + // Check that getContent() doesn't throw SuppressedDataException + $rev->getSlot( 'main', RevisionRecord::FOR_PUBLIC )->getContent(); + $rev->getSlot( 'main', RevisionRecord::FOR_THIS_USER, $user )->getContent(); + } + +} diff --git a/www/wiki/tests/phpunit/includes/Storage/RevisionStoreTest.php b/www/wiki/tests/phpunit/includes/Storage/RevisionStoreTest.php new file mode 100644 index 00000000..0bce572d --- /dev/null +++ b/www/wiki/tests/phpunit/includes/Storage/RevisionStoreTest.php @@ -0,0 +1,690 @@ +<?php + +namespace MediaWiki\Tests\Storage; + +use HashBagOStuff; +use Language; +use MediaWiki\MediaWikiServices; +use MediaWiki\Storage\RevisionAccessException; +use MediaWiki\Storage\RevisionStore; +use MediaWiki\Storage\SqlBlobStore; +use MediaWikiTestCase; +use Title; +use WANObjectCache; +use Wikimedia\Rdbms\Database; +use Wikimedia\Rdbms\LoadBalancer; + +class RevisionStoreTest extends MediaWikiTestCase { + + /** + * @param LoadBalancer $loadBalancer + * @param SqlBlobStore $blobStore + * @param WANObjectCache $WANObjectCache + * + * @return RevisionStore + */ + private function getRevisionStore( + $loadBalancer = null, + $blobStore = null, + $WANObjectCache = null + ) { + return new RevisionStore( + $loadBalancer ? $loadBalancer : $this->getMockLoadBalancer(), + $blobStore ? $blobStore : $this->getMockSqlBlobStore(), + $WANObjectCache ? $WANObjectCache : $this->getHashWANObjectCache(), + MediaWikiServices::getInstance()->getCommentStore(), + MediaWikiServices::getInstance()->getActorMigration() + ); + } + + /** + * @return \PHPUnit_Framework_MockObject_MockObject|LoadBalancer + */ + private function getMockLoadBalancer() { + return $this->getMockBuilder( LoadBalancer::class ) + ->disableOriginalConstructor()->getMock(); + } + + /** + * @return \PHPUnit_Framework_MockObject_MockObject|Database + */ + private function getMockDatabase() { + return $this->getMockBuilder( Database::class ) + ->disableOriginalConstructor()->getMock(); + } + + /** + * @return \PHPUnit_Framework_MockObject_MockObject|SqlBlobStore + */ + private function getMockSqlBlobStore() { + return $this->getMockBuilder( SqlBlobStore::class ) + ->disableOriginalConstructor()->getMock(); + } + + private function getHashWANObjectCache() { + return new WANObjectCache( [ 'cache' => new \HashBagOStuff() ] ); + } + + /** + * @covers \MediaWiki\Storage\RevisionStore::getContentHandlerUseDB + * @covers \MediaWiki\Storage\RevisionStore::setContentHandlerUseDB + */ + public function testGetSetContentHandlerDb() { + $store = $this->getRevisionStore(); + $this->assertTrue( $store->getContentHandlerUseDB() ); + $store->setContentHandlerUseDB( false ); + $this->assertFalse( $store->getContentHandlerUseDB() ); + $store->setContentHandlerUseDB( true ); + $this->assertTrue( $store->getContentHandlerUseDB() ); + } + + private function getDefaultQueryFields() { + return [ + 'rev_id', + 'rev_page', + 'rev_text_id', + 'rev_timestamp', + 'rev_minor_edit', + 'rev_deleted', + 'rev_len', + 'rev_parent_id', + 'rev_sha1', + ]; + } + + private function getCommentQueryFields() { + return [ + 'rev_comment_text' => 'rev_comment', + 'rev_comment_data' => 'NULL', + 'rev_comment_cid' => 'NULL', + ]; + } + + private function getActorQueryFields() { + return [ + 'rev_user' => 'rev_user', + 'rev_user_text' => 'rev_user_text', + 'rev_actor' => 'NULL', + ]; + } + + private function getContentHandlerQueryFields() { + return [ + 'rev_content_format', + 'rev_content_model', + ]; + } + + public function provideGetQueryInfo() { + yield [ + true, + [], + [ + 'tables' => [ 'revision' ], + 'fields' => array_merge( + $this->getDefaultQueryFields(), + $this->getCommentQueryFields(), + $this->getActorQueryFields(), + $this->getContentHandlerQueryFields() + ), + 'joins' => [], + ] + ]; + yield [ + false, + [], + [ + 'tables' => [ 'revision' ], + 'fields' => array_merge( + $this->getDefaultQueryFields(), + $this->getCommentQueryFields(), + $this->getActorQueryFields() + ), + 'joins' => [], + ] + ]; + yield [ + false, + [ 'page' ], + [ + 'tables' => [ 'revision', 'page' ], + 'fields' => array_merge( + $this->getDefaultQueryFields(), + $this->getCommentQueryFields(), + $this->getActorQueryFields(), + [ + 'page_namespace', + 'page_title', + 'page_id', + 'page_latest', + 'page_is_redirect', + 'page_len', + ] + ), + 'joins' => [ + 'page' => [ 'INNER JOIN', [ 'page_id = rev_page' ] ], + ], + ] + ]; + yield [ + false, + [ 'user' ], + [ + 'tables' => [ 'revision', 'user' ], + 'fields' => array_merge( + $this->getDefaultQueryFields(), + $this->getCommentQueryFields(), + $this->getActorQueryFields(), + [ + 'user_name', + ] + ), + 'joins' => [ + 'user' => [ 'LEFT JOIN', [ 'rev_user != 0', 'user_id = rev_user' ] ], + ], + ] + ]; + yield [ + false, + [ 'text' ], + [ + 'tables' => [ 'revision', 'text' ], + 'fields' => array_merge( + $this->getDefaultQueryFields(), + $this->getCommentQueryFields(), + $this->getActorQueryFields(), + [ + 'old_text', + 'old_flags', + ] + ), + 'joins' => [ + 'text' => [ 'INNER JOIN', [ 'rev_text_id=old_id' ] ], + ], + ] + ]; + yield [ + true, + [ 'page', 'user', 'text' ], + [ + 'tables' => [ 'revision', 'page', 'user', 'text' ], + 'fields' => array_merge( + $this->getDefaultQueryFields(), + $this->getCommentQueryFields(), + $this->getActorQueryFields(), + $this->getContentHandlerQueryFields(), + [ + 'page_namespace', + 'page_title', + 'page_id', + 'page_latest', + 'page_is_redirect', + 'page_len', + 'user_name', + 'old_text', + 'old_flags', + ] + ), + 'joins' => [ + 'page' => [ 'INNER JOIN', [ 'page_id = rev_page' ] ], + 'user' => [ 'LEFT JOIN', [ 'rev_user != 0', 'user_id = rev_user' ] ], + 'text' => [ 'INNER JOIN', [ 'rev_text_id=old_id' ] ], + ], + ] + ]; + } + + /** + * @dataProvider provideGetQueryInfo + * @covers \MediaWiki\Storage\RevisionStore::getQueryInfo + */ + public function testGetQueryInfo( $contentHandlerUseDb, $options, $expected ) { + $this->setMwGlobals( 'wgCommentTableSchemaMigrationStage', MIGRATION_OLD ); + $this->setMwGlobals( 'wgActorTableSchemaMigrationStage', MIGRATION_OLD ); + $this->overrideMwServices(); + $store = $this->getRevisionStore(); + $store->setContentHandlerUseDB( $contentHandlerUseDb ); + $this->assertEquals( $expected, $store->getQueryInfo( $options ) ); + } + + private function getDefaultArchiveFields() { + return [ + 'ar_id', + 'ar_page_id', + 'ar_namespace', + 'ar_title', + 'ar_rev_id', + 'ar_text_id', + 'ar_timestamp', + 'ar_minor_edit', + 'ar_deleted', + 'ar_len', + 'ar_parent_id', + 'ar_sha1', + ]; + } + + /** + * @covers \MediaWiki\Storage\RevisionStore::getArchiveQueryInfo + */ + public function testGetArchiveQueryInfo_contentHandlerDb() { + $this->setMwGlobals( 'wgCommentTableSchemaMigrationStage', MIGRATION_OLD ); + $this->setMwGlobals( 'wgActorTableSchemaMigrationStage', MIGRATION_OLD ); + $this->overrideMwServices(); + $store = $this->getRevisionStore(); + $store->setContentHandlerUseDB( true ); + $this->assertEquals( + [ + 'tables' => [ + 'archive' + ], + 'fields' => array_merge( + $this->getDefaultArchiveFields(), + [ + 'ar_comment_text' => 'ar_comment', + 'ar_comment_data' => 'NULL', + 'ar_comment_cid' => 'NULL', + 'ar_user_text' => 'ar_user_text', + 'ar_user' => 'ar_user', + 'ar_actor' => 'NULL', + 'ar_content_format', + 'ar_content_model', + ] + ), + 'joins' => [], + ], + $store->getArchiveQueryInfo() + ); + } + + /** + * @covers \MediaWiki\Storage\RevisionStore::getArchiveQueryInfo + */ + public function testGetArchiveQueryInfo_noContentHandlerDb() { + $this->setMwGlobals( 'wgCommentTableSchemaMigrationStage', MIGRATION_OLD ); + $this->setMwGlobals( 'wgActorTableSchemaMigrationStage', MIGRATION_OLD ); + $this->overrideMwServices(); + $store = $this->getRevisionStore(); + $store->setContentHandlerUseDB( false ); + $this->assertEquals( + [ + 'tables' => [ + 'archive' + ], + 'fields' => array_merge( + $this->getDefaultArchiveFields(), + [ + 'ar_comment_text' => 'ar_comment', + 'ar_comment_data' => 'NULL', + 'ar_comment_cid' => 'NULL', + 'ar_user_text' => 'ar_user_text', + 'ar_user' => 'ar_user', + 'ar_actor' => 'NULL', + ] + ), + 'joins' => [], + ], + $store->getArchiveQueryInfo() + ); + } + + public function testGetTitle_successFromPageId() { + $mockLoadBalancer = $this->getMockLoadBalancer(); + // Title calls wfGetDB() so we have to set the main service + $this->setService( 'DBLoadBalancer', $mockLoadBalancer ); + + $db = $this->getMockDatabase(); + // Title calls wfGetDB() which uses a regular Connection + $mockLoadBalancer->expects( $this->atLeastOnce() ) + ->method( 'getConnection' ) + ->willReturn( $db ); + + // First call to Title::newFromID, faking no result (db lag?) + $db->expects( $this->at( 0 ) ) + ->method( 'selectRow' ) + ->with( + 'page', + $this->anything(), + [ 'page_id' => 1 ] + ) + ->willReturn( (object)[ + 'page_namespace' => '1', + 'page_title' => 'Food', + ] ); + + $store = $this->getRevisionStore( $mockLoadBalancer ); + $title = $store->getTitle( 1, 2, RevisionStore::READ_NORMAL ); + + $this->assertSame( 1, $title->getNamespace() ); + $this->assertSame( 'Food', $title->getDBkey() ); + } + + public function testGetTitle_successFromPageIdOnFallback() { + $mockLoadBalancer = $this->getMockLoadBalancer(); + // Title calls wfGetDB() so we have to set the main service + $this->setService( 'DBLoadBalancer', $mockLoadBalancer ); + + $db = $this->getMockDatabase(); + // Title calls wfGetDB() which uses a regular Connection + // Assert that the first call uses a REPLICA and the second falls back to master + $mockLoadBalancer->expects( $this->exactly( 2 ) ) + ->method( 'getConnection' ) + ->willReturn( $db ); + // RevisionStore getTitle uses a ConnectionRef + $mockLoadBalancer->expects( $this->atLeastOnce() ) + ->method( 'getConnectionRef' ) + ->willReturn( $db ); + + // First call to Title::newFromID, faking no result (db lag?) + $db->expects( $this->at( 0 ) ) + ->method( 'selectRow' ) + ->with( + 'page', + $this->anything(), + [ 'page_id' => 1 ] + ) + ->willReturn( false ); + + // First select using rev_id, faking no result (db lag?) + $db->expects( $this->at( 1 ) ) + ->method( 'selectRow' ) + ->with( + [ 'revision', 'page' ], + $this->anything(), + [ 'rev_id' => 2 ] + ) + ->willReturn( false ); + + // Second call to Title::newFromID, no result + $db->expects( $this->at( 2 ) ) + ->method( 'selectRow' ) + ->with( + 'page', + $this->anything(), + [ 'page_id' => 1 ] + ) + ->willReturn( (object)[ + 'page_namespace' => '2', + 'page_title' => 'Foodey', + ] ); + + $store = $this->getRevisionStore( $mockLoadBalancer ); + $title = $store->getTitle( 1, 2, RevisionStore::READ_NORMAL ); + + $this->assertSame( 2, $title->getNamespace() ); + $this->assertSame( 'Foodey', $title->getDBkey() ); + } + + public function testGetTitle_successFromRevId() { + $mockLoadBalancer = $this->getMockLoadBalancer(); + // Title calls wfGetDB() so we have to set the main service + $this->setService( 'DBLoadBalancer', $mockLoadBalancer ); + + $db = $this->getMockDatabase(); + // Title calls wfGetDB() which uses a regular Connection + $mockLoadBalancer->expects( $this->atLeastOnce() ) + ->method( 'getConnection' ) + ->willReturn( $db ); + // RevisionStore getTitle uses a ConnectionRef + $mockLoadBalancer->expects( $this->atLeastOnce() ) + ->method( 'getConnectionRef' ) + ->willReturn( $db ); + + // First call to Title::newFromID, faking no result (db lag?) + $db->expects( $this->at( 0 ) ) + ->method( 'selectRow' ) + ->with( + 'page', + $this->anything(), + [ 'page_id' => 1 ] + ) + ->willReturn( false ); + + // First select using rev_id, faking no result (db lag?) + $db->expects( $this->at( 1 ) ) + ->method( 'selectRow' ) + ->with( + [ 'revision', 'page' ], + $this->anything(), + [ 'rev_id' => 2 ] + ) + ->willReturn( (object)[ + 'page_namespace' => '1', + 'page_title' => 'Food2', + ] ); + + $store = $this->getRevisionStore( $mockLoadBalancer ); + $title = $store->getTitle( 1, 2, RevisionStore::READ_NORMAL ); + + $this->assertSame( 1, $title->getNamespace() ); + $this->assertSame( 'Food2', $title->getDBkey() ); + } + + public function testGetTitle_successFromRevIdOnFallback() { + $mockLoadBalancer = $this->getMockLoadBalancer(); + // Title calls wfGetDB() so we have to set the main service + $this->setService( 'DBLoadBalancer', $mockLoadBalancer ); + + $db = $this->getMockDatabase(); + // Title calls wfGetDB() which uses a regular Connection + // Assert that the first call uses a REPLICA and the second falls back to master + $mockLoadBalancer->expects( $this->exactly( 2 ) ) + ->method( 'getConnection' ) + ->willReturn( $db ); + // RevisionStore getTitle uses a ConnectionRef + $mockLoadBalancer->expects( $this->atLeastOnce() ) + ->method( 'getConnectionRef' ) + ->willReturn( $db ); + + // First call to Title::newFromID, faking no result (db lag?) + $db->expects( $this->at( 0 ) ) + ->method( 'selectRow' ) + ->with( + 'page', + $this->anything(), + [ 'page_id' => 1 ] + ) + ->willReturn( false ); + + // First select using rev_id, faking no result (db lag?) + $db->expects( $this->at( 1 ) ) + ->method( 'selectRow' ) + ->with( + [ 'revision', 'page' ], + $this->anything(), + [ 'rev_id' => 2 ] + ) + ->willReturn( false ); + + // Second call to Title::newFromID, no result + $db->expects( $this->at( 2 ) ) + ->method( 'selectRow' ) + ->with( + 'page', + $this->anything(), + [ 'page_id' => 1 ] + ) + ->willReturn( false ); + + // Second select using rev_id, result + $db->expects( $this->at( 3 ) ) + ->method( 'selectRow' ) + ->with( + [ 'revision', 'page' ], + $this->anything(), + [ 'rev_id' => 2 ] + ) + ->willReturn( (object)[ + 'page_namespace' => '2', + 'page_title' => 'Foodey', + ] ); + + $store = $this->getRevisionStore( $mockLoadBalancer ); + $title = $store->getTitle( 1, 2, RevisionStore::READ_NORMAL ); + + $this->assertSame( 2, $title->getNamespace() ); + $this->assertSame( 'Foodey', $title->getDBkey() ); + } + + /** + * @covers \MediaWiki\Storage\RevisionStore::getTitle + */ + public function testGetTitle_correctFallbackAndthrowsExceptionAfterFallbacks() { + $mockLoadBalancer = $this->getMockLoadBalancer(); + // Title calls wfGetDB() so we have to set the main service + $this->setService( 'DBLoadBalancer', $mockLoadBalancer ); + + $db = $this->getMockDatabase(); + // Title calls wfGetDB() which uses a regular Connection + // Assert that the first call uses a REPLICA and the second falls back to master + + // RevisionStore getTitle uses getConnectionRef + // Title::newFromID uses getConnection + foreach ( [ 'getConnection', 'getConnectionRef' ] as $method ) { + $mockLoadBalancer->expects( $this->exactly( 2 ) ) + ->method( $method ) + ->willReturnCallback( function ( $masterOrReplica ) use ( $db ) { + static $callCounter = 0; + $callCounter++; + // The first call should be to a REPLICA, and the second a MASTER. + if ( $callCounter === 1 ) { + $this->assertSame( DB_REPLICA, $masterOrReplica ); + } elseif ( $callCounter === 2 ) { + $this->assertSame( DB_MASTER, $masterOrReplica ); + } + return $db; + } ); + } + // First and third call to Title::newFromID, faking no result + foreach ( [ 0, 2 ] as $counter ) { + $db->expects( $this->at( $counter ) ) + ->method( 'selectRow' ) + ->with( + 'page', + $this->anything(), + [ 'page_id' => 1 ] + ) + ->willReturn( false ); + } + + foreach ( [ 1, 3 ] as $counter ) { + $db->expects( $this->at( $counter ) ) + ->method( 'selectRow' ) + ->with( + [ 'revision', 'page' ], + $this->anything(), + [ 'rev_id' => 2 ] + ) + ->willReturn( false ); + } + + $store = $this->getRevisionStore( $mockLoadBalancer ); + + $this->setExpectedException( RevisionAccessException::class ); + $store->getTitle( 1, 2, RevisionStore::READ_NORMAL ); + } + + public function provideNewRevisionFromRow_legacyEncoding_applied() { + yield 'windows-1252, old_flags is empty' => [ + 'windows-1252', + 'en', + [ + 'old_flags' => '', + 'old_text' => "S\xF6me Content", + ], + 'Söme Content' + ]; + + yield 'windows-1252, old_flags is null' => [ + 'windows-1252', + 'en', + [ + 'old_flags' => null, + 'old_text' => "S\xF6me Content", + ], + 'Söme Content' + ]; + } + + /** + * @dataProvider provideNewRevisionFromRow_legacyEncoding_applied + * + * @covers \MediaWiki\Storage\RevisionStore::newRevisionFromRow + * @covers \MediaWiki\Storage\RevisionStore::newRevisionFromRow_1_29 + */ + public function testNewRevisionFromRow_legacyEncoding_applied( $encoding, $locale, $row, $text ) { + $cache = new WANObjectCache( [ 'cache' => new HashBagOStuff() ] ); + + $blobStore = new SqlBlobStore( wfGetLB(), $cache ); + $blobStore->setLegacyEncoding( $encoding, Language::factory( $locale ) ); + + $store = $this->getRevisionStore( wfGetLB(), $blobStore, $cache ); + + $record = $store->newRevisionFromRow( + $this->makeRow( $row ), + 0, + Title::newFromText( __METHOD__ . '-UTPage' ) + ); + + $this->assertSame( $text, $record->getContent( 'main' )->serialize() ); + } + + /** + * @covers \MediaWiki\Storage\RevisionStore::newRevisionFromRow + * @covers \MediaWiki\Storage\RevisionStore::newRevisionFromRow_1_29 + */ + public function testNewRevisionFromRow_legacyEncoding_ignored() { + $row = [ + 'old_flags' => 'utf-8', + 'old_text' => 'Söme Content', + ]; + + $cache = new WANObjectCache( [ 'cache' => new HashBagOStuff() ] ); + + $blobStore = new SqlBlobStore( wfGetLB(), $cache ); + $blobStore->setLegacyEncoding( 'windows-1252', Language::factory( 'en' ) ); + + $store = $this->getRevisionStore( wfGetLB(), $blobStore, $cache ); + + $record = $store->newRevisionFromRow( + $this->makeRow( $row ), + 0, + Title::newFromText( __METHOD__ . '-UTPage' ) + ); + $this->assertSame( 'Söme Content', $record->getContent( 'main' )->serialize() ); + } + + private function makeRow( array $array ) { + $row = $array + [ + 'rev_id' => 7, + 'rev_page' => 5, + 'rev_text_id' => 11, + 'rev_timestamp' => '20110101000000', + 'rev_user_text' => 'Tester', + 'rev_user' => 17, + 'rev_minor_edit' => 0, + 'rev_deleted' => 0, + 'rev_len' => 100, + 'rev_parent_id' => 0, + 'rev_sha1' => 'deadbeef', + 'rev_comment_text' => 'Testing', + 'rev_comment_data' => '{}', + 'rev_comment_cid' => 111, + 'rev_content_format' => CONTENT_FORMAT_TEXT, + 'rev_content_model' => CONTENT_MODEL_TEXT, + 'page_namespace' => 0, + 'page_title' => 'TEST', + 'page_id' => 5, + 'page_latest' => 7, + 'page_is_redirect' => 0, + 'page_len' => 100, + 'user_name' => 'Tester', + 'old_is' => 13, + 'old_text' => 'Hello World', + 'old_flags' => 'utf-8', + ]; + + return (object)$row; + } + +} diff --git a/www/wiki/tests/phpunit/includes/Storage/SlotRecordTest.php b/www/wiki/tests/phpunit/includes/Storage/SlotRecordTest.php new file mode 100644 index 00000000..8f26494d --- /dev/null +++ b/www/wiki/tests/phpunit/includes/Storage/SlotRecordTest.php @@ -0,0 +1,298 @@ +<?php + +namespace MediaWiki\Tests\Storage; + +use InvalidArgumentException; +use LogicException; +use MediaWiki\Storage\IncompleteRevisionException; +use MediaWiki\Storage\SlotRecord; +use MediaWiki\Storage\SuppressedDataException; +use MediaWikiTestCase; +use WikitextContent; + +/** + * @covers \MediaWiki\Storage\SlotRecord + */ +class SlotRecordTest extends MediaWikiTestCase { + + private function makeRow( $data = [] ) { + $data = $data + [ + 'slot_id' => 1234, + 'slot_content_id' => 33, + 'content_size' => '5', + 'content_sha1' => 'someHash', + 'content_address' => 'tt:456', + 'model_name' => CONTENT_MODEL_WIKITEXT, + 'format_name' => CONTENT_FORMAT_WIKITEXT, + 'slot_revision_id' => '2', + 'slot_origin' => '1', + 'role_name' => 'myRole', + ]; + return (object)$data; + } + + public function testCompleteConstruction() { + $row = $this->makeRow(); + $record = new SlotRecord( $row, new WikitextContent( 'A' ) ); + + $this->assertTrue( $record->hasAddress() ); + $this->assertTrue( $record->hasRevision() ); + $this->assertTrue( $record->isInherited() ); + $this->assertSame( 'A', $record->getContent()->getNativeData() ); + $this->assertSame( 5, $record->getSize() ); + $this->assertSame( 'someHash', $record->getSha1() ); + $this->assertSame( CONTENT_MODEL_WIKITEXT, $record->getModel() ); + $this->assertSame( 2, $record->getRevision() ); + $this->assertSame( 1, $record->getOrigin() ); + $this->assertSame( 'tt:456', $record->getAddress() ); + $this->assertSame( 33, $record->getContentId() ); + $this->assertSame( CONTENT_FORMAT_WIKITEXT, $record->getFormat() ); + $this->assertSame( 'myRole', $record->getRole() ); + } + + public function testConstructionDeferred() { + $row = $this->makeRow( [ + 'content_size' => null, // to be computed + 'content_sha1' => null, // to be computed + 'format_name' => function () { + return CONTENT_FORMAT_WIKITEXT; + }, + 'slot_revision_id' => '2', + 'slot_origin' => '2', + ] ); + + $content = function () { + return new WikitextContent( 'A' ); + }; + + $record = new SlotRecord( $row, $content ); + + $this->assertTrue( $record->hasAddress() ); + $this->assertTrue( $record->hasRevision() ); + $this->assertFalse( $record->isInherited() ); + $this->assertSame( 'A', $record->getContent()->getNativeData() ); + $this->assertSame( 1, $record->getSize() ); + $this->assertNotNull( $record->getSha1() ); + $this->assertSame( CONTENT_MODEL_WIKITEXT, $record->getModel() ); + $this->assertSame( 2, $record->getRevision() ); + $this->assertSame( 2, $record->getRevision() ); + $this->assertSame( 'tt:456', $record->getAddress() ); + $this->assertSame( 33, $record->getContentId() ); + $this->assertSame( CONTENT_FORMAT_WIKITEXT, $record->getFormat() ); + $this->assertSame( 'myRole', $record->getRole() ); + } + + public function testNewUnsaved() { + $record = SlotRecord::newUnsaved( 'myRole', new WikitextContent( 'A' ) ); + + $this->assertFalse( $record->hasAddress() ); + $this->assertFalse( $record->hasRevision() ); + $this->assertFalse( $record->isInherited() ); + $this->assertSame( 'A', $record->getContent()->getNativeData() ); + $this->assertSame( 1, $record->getSize() ); + $this->assertNotNull( $record->getSha1() ); + $this->assertSame( CONTENT_MODEL_WIKITEXT, $record->getModel() ); + $this->assertSame( 'myRole', $record->getRole() ); + } + + public function provideInvalidConstruction() { + yield 'both null' => [ null, null ]; + yield 'null row' => [ null, new WikitextContent( 'A' ) ]; + yield 'array row' => [ [], new WikitextContent( 'A' ) ]; + yield 'empty row' => [ (object)[], new WikitextContent( 'A' ) ]; + yield 'null content' => [ (object)[], null ]; + } + + /** + * @dataProvider provideInvalidConstruction + */ + public function testInvalidConstruction( $row, $content ) { + $this->setExpectedException( InvalidArgumentException::class ); + new SlotRecord( $row, $content ); + } + + public function testGetContentId_fails() { + $record = SlotRecord::newUnsaved( 'main', new WikitextContent( 'A' ) ); + $this->setExpectedException( IncompleteRevisionException::class ); + + $record->getContentId(); + } + + public function testGetAddress_fails() { + $record = SlotRecord::newUnsaved( 'main', new WikitextContent( 'A' ) ); + $this->setExpectedException( IncompleteRevisionException::class ); + + $record->getAddress(); + } + + public function provideIncomplete() { + $unsaved = SlotRecord::newUnsaved( 'main', new WikitextContent( 'A' ) ); + yield 'unsaved' => [ $unsaved ]; + + $parent = new SlotRecord( $this->makeRow(), new WikitextContent( 'A' ) ); + $inherited = SlotRecord::newInherited( $parent ); + yield 'inherited' => [ $inherited ]; + } + + /** + * @dataProvider provideIncomplete + */ + public function testGetRevision_fails( SlotRecord $record ) { + $record = SlotRecord::newUnsaved( 'main', new WikitextContent( 'A' ) ); + $this->setExpectedException( IncompleteRevisionException::class ); + + $record->getRevision(); + } + + /** + * @dataProvider provideIncomplete + */ + public function testGetOrigin_fails( SlotRecord $record ) { + $record = SlotRecord::newUnsaved( 'main', new WikitextContent( 'A' ) ); + $this->setExpectedException( IncompleteRevisionException::class ); + + $record->getOrigin(); + } + + public function provideHashStability() { + yield [ '', 'phoiac9h4m842xq45sp7s6u21eteeq1' ]; + yield [ 'Lorem ipsum', 'hcr5u40uxr81d3nx89nvwzclfz6r9c5' ]; + } + + /** + * @dataProvider provideHashStability + */ + public function testHashStability( $text, $hash ) { + // Changing the output of the hash function will break things horribly! + + $this->assertSame( $hash, SlotRecord::base36Sha1( $text ) ); + + $record = SlotRecord::newUnsaved( 'main', new WikitextContent( $text ) ); + $this->assertSame( $hash, $record->getSha1() ); + } + + public function testNewWithSuppressedContent() { + $input = new SlotRecord( $this->makeRow(), new WikitextContent( 'A' ) ); + $output = SlotRecord::newWithSuppressedContent( $input ); + + $this->setExpectedException( SuppressedDataException::class ); + $output->getContent(); + } + + public function testNewInherited() { + $row = $this->makeRow( [ 'slot_revision_id' => 7, 'slot_origin' => 7 ] ); + $parent = new SlotRecord( $row, new WikitextContent( 'A' ) ); + + // This would happen while doing an edit, before saving revision meta-data. + $inherited = SlotRecord::newInherited( $parent ); + + $this->assertSame( $parent->getContentId(), $inherited->getContentId() ); + $this->assertSame( $parent->getAddress(), $inherited->getAddress() ); + $this->assertSame( $parent->getContent(), $inherited->getContent() ); + $this->assertTrue( $inherited->isInherited() ); + $this->assertFalse( $inherited->hasRevision() ); + + // make sure we didn't mess with the internal state of $parent + $this->assertFalse( $parent->isInherited() ); + $this->assertSame( 7, $parent->getRevision() ); + + // This would happen while doing an edit, after saving the revision meta-data + // and content meta-data. + $saved = SlotRecord::newSaved( + 10, + $inherited->getContentId(), + $inherited->getAddress(), + $inherited + ); + $this->assertSame( $parent->getContentId(), $saved->getContentId() ); + $this->assertSame( $parent->getAddress(), $saved->getAddress() ); + $this->assertSame( $parent->getContent(), $saved->getContent() ); + $this->assertTrue( $saved->isInherited() ); + $this->assertTrue( $saved->hasRevision() ); + $this->assertSame( 10, $saved->getRevision() ); + + // make sure we didn't mess with the internal state of $parent or $inherited + $this->assertSame( 7, $parent->getRevision() ); + $this->assertFalse( $inherited->hasRevision() ); + } + + public function testNewSaved() { + // This would happen while doing an edit, before saving revision meta-data. + $unsaved = SlotRecord::newUnsaved( 'main', new WikitextContent( 'A' ) ); + + // This would happen while doing an edit, after saving the revision meta-data + // and content meta-data. + $saved = SlotRecord::newSaved( 10, 20, 'theNewAddress', $unsaved ); + $this->assertFalse( $saved->isInherited() ); + $this->assertTrue( $saved->hasRevision() ); + $this->assertTrue( $saved->hasAddress() ); + $this->assertSame( 'theNewAddress', $saved->getAddress() ); + $this->assertSame( 20, $saved->getContentId() ); + $this->assertSame( 'A', $saved->getContent()->getNativeData() ); + $this->assertSame( 10, $saved->getRevision() ); + $this->assertSame( 10, $saved->getOrigin() ); + + // make sure we didn't mess with the internal state of $unsaved + $this->assertFalse( $unsaved->hasAddress() ); + $this->assertFalse( $unsaved->hasRevision() ); + } + + public function provideNewSaved_LogicException() { + $freshRow = $this->makeRow( [ + 'content_id' => 10, + 'content_address' => 'address:1', + 'slot_origin' => 1, + 'slot_revision_id' => 1, + ] ); + + $freshSlot = new SlotRecord( $freshRow, new WikitextContent( 'A' ) ); + yield 'mismatching address' => [ 1, 10, 'address:BAD', $freshSlot ]; + yield 'mismatching revision' => [ 5, 10, 'address:1', $freshSlot ]; + yield 'mismatching content ID' => [ 1, 17, 'address:1', $freshSlot ]; + + $inheritedRow = $this->makeRow( [ + 'content_id' => null, + 'content_address' => null, + 'slot_origin' => 0, + 'slot_revision_id' => 1, + ] ); + + $inheritedSlot = new SlotRecord( $inheritedRow, new WikitextContent( 'A' ) ); + yield 'inherited, but no address' => [ 1, 10, 'address:2', $inheritedSlot ]; + } + + /** + * @dataProvider provideNewSaved_LogicException + */ + public function testNewSaved_LogicException( + $revisionId, + $contentId, + $contentAddress, + SlotRecord $protoSlot + ) { + $this->setExpectedException( LogicException::class ); + SlotRecord::newSaved( $revisionId, $contentId, $contentAddress, $protoSlot ); + } + + public function provideNewSaved_InvalidArgumentException() { + $unsaved = SlotRecord::newUnsaved( 'main', new WikitextContent( 'A' ) ); + + yield 'bad revision id' => [ 'xyzzy', 5, 'address', $unsaved ]; + yield 'bad content id' => [ 7, 'xyzzy', 'address', $unsaved ]; + yield 'bad content address' => [ 7, 5, 77, $unsaved ]; + } + + /** + * @dataProvider provideNewSaved_InvalidArgumentException + */ + public function testNewSaved_InvalidArgumentException( + $revisionId, + $contentId, + $contentAddress, + SlotRecord $protoSlot + ) { + $this->setExpectedException( InvalidArgumentException::class ); + SlotRecord::newSaved( $revisionId, $contentId, $contentAddress, $protoSlot ); + } + +} diff --git a/www/wiki/tests/phpunit/includes/Storage/SqlBlobStoreTest.php b/www/wiki/tests/phpunit/includes/Storage/SqlBlobStoreTest.php new file mode 100644 index 00000000..dbbef11e --- /dev/null +++ b/www/wiki/tests/phpunit/includes/Storage/SqlBlobStoreTest.php @@ -0,0 +1,241 @@ +<?php + +namespace MediaWiki\Tests\Storage; + +use Language; +use MediaWiki\MediaWikiServices; +use MediaWiki\Storage\SqlBlobStore; +use MediaWikiTestCase; +use stdClass; +use TitleValue; + +/** + * @covers \MediaWiki\Storage\SqlBlobStore + * @group Database + */ +class SqlBlobStoreTest extends MediaWikiTestCase { + + /** + * @return SqlBlobStore + */ + public function getBlobStore( $legacyEncoding = false, $compressRevisions = false ) { + $services = MediaWikiServices::getInstance(); + + $store = new SqlBlobStore( + $services->getDBLoadBalancer(), + $services->getMainWANObjectCache() + ); + + if ( $compressRevisions ) { + $store->setCompressBlobs( $compressRevisions ); + } + if ( $legacyEncoding ) { + $store->setLegacyEncoding( $legacyEncoding, Language::factory( 'en' ) ); + } + + return $store; + } + + /** + * @covers \MediaWiki\Storage\SqlBlobStore::getCompressBlobs() + * @covers \MediaWiki\Storage\SqlBlobStore::setCompressBlobs() + */ + public function testGetSetCompressRevisions() { + $store = $this->getBlobStore(); + $this->assertFalse( $store->getCompressBlobs() ); + $store->setCompressBlobs( true ); + $this->assertTrue( $store->getCompressBlobs() ); + } + + /** + * @covers \MediaWiki\Storage\SqlBlobStore::getLegacyEncoding() + * @covers \MediaWiki\Storage\SqlBlobStore::getLegacyEncodingConversionLang() + * @covers \MediaWiki\Storage\SqlBlobStore::setLegacyEncoding() + */ + public function testGetSetLegacyEncoding() { + $store = $this->getBlobStore(); + $this->assertFalse( $store->getLegacyEncoding() ); + $this->assertNull( $store->getLegacyEncodingConversionLang() ); + $en = Language::factory( 'en' ); + $store->setLegacyEncoding( 'foo', $en ); + $this->assertSame( 'foo', $store->getLegacyEncoding() ); + $this->assertSame( $en, $store->getLegacyEncodingConversionLang() ); + } + + /** + * @covers \MediaWiki\Storage\SqlBlobStore::getCacheExpiry() + * @covers \MediaWiki\Storage\SqlBlobStore::setCacheExpiry() + */ + public function testGetSetCacheExpiry() { + $store = $this->getBlobStore(); + $this->assertSame( 604800, $store->getCacheExpiry() ); + $store->setCacheExpiry( 12 ); + $this->assertSame( 12, $store->getCacheExpiry() ); + } + + /** + * @covers \MediaWiki\Storage\SqlBlobStore::getUseExternalStore() + * @covers \MediaWiki\Storage\SqlBlobStore::setUseExternalStore() + */ + public function testGetSetUseExternalStore() { + $store = $this->getBlobStore(); + $this->assertFalse( $store->getUseExternalStore() ); + $store->setUseExternalStore( true ); + $this->assertTrue( $store->getUseExternalStore() ); + } + + public function provideDecompress() { + yield '(no legacy encoding), false in false out' => [ false, false, [], false ]; + yield '(no legacy encoding), empty in empty out' => [ false, '', [], '' ]; + yield '(no legacy encoding), empty in empty out' => [ false, 'A', [], 'A' ]; + yield '(no legacy encoding), string in with gzip flag returns string' => [ + // gzip string below generated with gzdeflate( 'AAAABBAAA' ) + false, "sttttr\002\022\000", [ 'gzip' ], 'AAAABBAAA', + ]; + yield '(no legacy encoding), string in with object flag returns false' => [ + // gzip string below generated with serialize( 'JOJO' ) + false, "s:4:\"JOJO\";", [ 'object' ], false, + ]; + yield '(no legacy encoding), serialized object in with object flag returns string' => [ + false, + // Using a TitleValue object as it has a getText method (which is needed) + serialize( new TitleValue( 0, 'HHJJDDFF' ) ), + [ 'object' ], + 'HHJJDDFF', + ]; + yield '(no legacy encoding), serialized object in with object & gzip flag returns string' => [ + false, + // Using a TitleValue object as it has a getText method (which is needed) + gzdeflate( serialize( new TitleValue( 0, '8219JJJ840' ) ) ), + [ 'object', 'gzip' ], + '8219JJJ840', + ]; + yield '(ISO-8859-1 encoding), string in string out' => [ + 'ISO-8859-1', + iconv( 'utf-8', 'ISO-8859-1', "1®Àþ1" ), + [], + '1®Àþ1', + ]; + yield '(ISO-8859-1 encoding), serialized object in with gzip flags returns string' => [ + 'ISO-8859-1', + gzdeflate( iconv( 'utf-8', 'ISO-8859-1', "4®Àþ4" ) ), + [ 'gzip' ], + '4®Àþ4', + ]; + yield '(ISO-8859-1 encoding), serialized object in with object flags returns string' => [ + 'ISO-8859-1', + serialize( new TitleValue( 0, iconv( 'utf-8', 'ISO-8859-1', "3®Àþ3" ) ) ), + [ 'object' ], + '3®Àþ3', + ]; + yield '(ISO-8859-1 encoding), serialized object in with object & gzip flags returns string' => [ + 'ISO-8859-1', + gzdeflate( serialize( new TitleValue( 0, iconv( 'utf-8', 'ISO-8859-1', "2®Àþ2" ) ) ) ), + [ 'gzip', 'object' ], + '2®Àþ2', + ]; + yield 'T184749 (windows-1252 encoding), string in string out' => [ + 'windows-1252', + iconv( 'utf-8', 'windows-1252', "sammansättningar" ), + [], + 'sammansättningar', + ]; + yield 'T184749 (windows-1252 encoding), string in string out with gzip' => [ + 'windows-1252', + gzdeflate( iconv( 'utf-8', 'windows-1252', "sammansättningar" ) ), + [ 'gzip' ], + 'sammansättningar', + ]; + } + + /** + * @dataProvider provideDecompress + * @covers \MediaWiki\Storage\SqlBlobStore::decompressData + * + * @param string|bool $legacyEncoding + * @param mixed $data + * @param array $flags + * @param mixed $expected + */ + public function testDecompressData( $legacyEncoding, $data, $flags, $expected ) { + $store = $this->getBlobStore( $legacyEncoding ); + $this->assertSame( + $expected, + $store->decompressData( $data, $flags ) + ); + } + + /** + * @covers \MediaWiki\Storage\SqlBlobStore::compressData + */ + public function testCompressRevisionTextUtf8() { + $store = $this->getBlobStore(); + $row = new stdClass; + $row->old_text = "Wiki est l'\xc3\xa9cole superieur !"; + $row->old_flags = $store->compressData( $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" ); + } + + /** + * @covers \MediaWiki\Storage\SqlBlobStore::compressData + */ + public function testCompressRevisionTextUtf8Gzip() { + $store = $this->getBlobStore( false, true ); + $this->checkPHPExtension( 'zlib' ); + + $row = new stdClass; + $row->old_text = "Wiki est l'\xc3\xa9cole superieur !"; + $row->old_flags = $store->compressData( $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" ); + } + + public function provideBlobs() { + yield [ '' ]; + yield [ 'someText' ]; + yield [ "sammansättningar" ]; + } + + /** + * @dataProvider provideBlobs + * @covers \MediaWiki\Storage\SqlBlobStore::storeBlob + * @covers \MediaWiki\Storage\SqlBlobStore::getBlob + */ + public function testSimpleStoreGetBlobSimpleRoundtrip( $blob ) { + $store = $this->getBlobStore(); + $address = $store->storeBlob( $blob ); + $this->assertSame( $blob, $store->getBlob( $address ) ); + } + + /** + * @dataProvider provideBlobs + * @covers \MediaWiki\Storage\SqlBlobStore::storeBlob + * @covers \MediaWiki\Storage\SqlBlobStore::getBlob + */ + public function testSimpleStoreGetBlobSimpleRoundtripWindowsLegacyEncoding( $blob ) { + $store = $this->getBlobStore( 'windows-1252' ); + $address = $store->storeBlob( $blob ); + $this->assertSame( $blob, $store->getBlob( $address ) ); + } + + /** + * @dataProvider provideBlobs + * @covers \MediaWiki\Storage\SqlBlobStore::storeBlob + * @covers \MediaWiki\Storage\SqlBlobStore::getBlob + */ + public function testSimpleStoreGetBlobSimpleRoundtripWindowsLegacyEncodingGzip( $blob ) { + $store = $this->getBlobStore( 'windows-1252', true ); + $address = $store->storeBlob( $blob ); + $this->assertSame( $blob, $store->getBlob( $address ) ); + } + +} diff --git a/www/wiki/tests/phpunit/includes/TemplateCategoriesTest.php b/www/wiki/tests/phpunit/includes/TemplateCategoriesTest.php index ab231363..ebd8dbd3 100644 --- a/www/wiki/tests/phpunit/includes/TemplateCategoriesTest.php +++ b/www/wiki/tests/phpunit/includes/TemplateCategoriesTest.php @@ -1,10 +1,10 @@ <?php +require __DIR__ . "/../../../maintenance/runJobs.php"; + /** * @group Database */ -require __DIR__ . "/../../../maintenance/runJobs.php"; - class TemplateCategoriesTest extends MediaWikiLangTestCase { /** diff --git a/www/wiki/tests/phpunit/includes/TemplateParserTest.php b/www/wiki/tests/phpunit/includes/TemplateParserTest.php index c161f853..ccccf0f9 100644 --- a/www/wiki/tests/phpunit/includes/TemplateParserTest.php +++ b/www/wiki/tests/phpunit/includes/TemplateParserTest.php @@ -2,6 +2,7 @@ /** * @group Templates + * @covers TemplateParser */ class TemplateParserTest extends MediaWikiTestCase { @@ -19,9 +20,6 @@ class TemplateParserTest extends MediaWikiTestCase { /** * @dataProvider provideProcessTemplate - * @covers TemplateParser::processTemplate - * @covers TemplateParser::getTemplate - * @covers TemplateParser::getTemplateFilename */ public function testProcessTemplate( $name, $args, $result, $exception = false ) { if ( $exception ) { @@ -118,7 +116,7 @@ class TemplateParserTest extends MediaWikiTestCase { $this->assertEquals( 'rrr', $tp->processTemplate( 'recurse', $data ) ); $tp->enableRecursivePartials( false ); - $this->setExpectedException( 'Exception' ); + $this->setExpectedException( Exception::class ); $tp->processTemplate( 'recurse', $data ); } diff --git a/www/wiki/tests/phpunit/includes/TestUserRegistry.php b/www/wiki/tests/phpunit/includes/TestUserRegistry.php index 4818b49a..0c178ca1 100644 --- a/www/wiki/tests/phpunit/includes/TestUserRegistry.php +++ b/www/wiki/tests/phpunit/includes/TestUserRegistry.php @@ -107,4 +107,19 @@ class TestUserRegistry { public static function clear() { self::$testUsers = []; } + + /** + * @todo It would be nice if this were a non-static method of TestUser + * instead, but that doesn't seem possible without friends? + * + * @return bool True if it's safe to modify the user + */ + public static function isMutable( User $user ) { + foreach ( self::$testUsers as $key => $testUser ) { + if ( $user === $testUser->getUser() ) { + return false; + } + } + return true; + } } diff --git a/www/wiki/tests/phpunit/includes/TestingAccessWrapper.php b/www/wiki/tests/phpunit/includes/TestingAccessWrapper.php deleted file mode 100644 index 7332e15e..00000000 --- a/www/wiki/tests/phpunit/includes/TestingAccessWrapper.php +++ /dev/null @@ -1,128 +0,0 @@ -<?php -/** - * Circumvent access restrictions on object internals - * - * This can be helpful for writing tests that can probe object internals, - * without having to modify the class under test to accomodate. - * - * Wrap an object with private methods as follows: - * $title = TestingAccessWrapper::newFromObject( Title::newFromDBkey( $key ) ); - * - * You can access private and protected instance methods and variables: - * $formatter = $title->getTitleFormatter(); - * - * TODO: - * - Organize other helper classes in tests/testHelpers.inc into a directory. - */ -class TestingAccessWrapper { - /** @var mixed The object, or the class name for static-only access */ - public $object; - - /** - * Return the same object, without access restrictions. - */ - public static function newFromObject( $object ) { - if ( !is_object( $object ) ) { - throw new InvalidArgumentException( __METHOD__ . ' must be called with an object' ); - } - $wrapper = new TestingAccessWrapper(); - $wrapper->object = $object; - return $wrapper; - } - - /** - * Allow access to non-public static methods and properties of the class. - * Use non-static access, - */ - public static function newFromClass( $className ) { - if ( !is_string( $className ) ) { - throw new InvalidArgumentException( __METHOD__ . ' must be called with a class name' ); - } - $wrapper = new TestingAccessWrapper(); - $wrapper->object = $className; - return $wrapper; - } - - public function __call( $method, $args ) { - $methodReflection = $this->getMethod( $method ); - - if ( $this->isStatic() && !$methodReflection->isStatic() ) { - throw new DomainException( __METHOD__ . ': Cannot call non-static when wrapping static class' ); - } - - return $methodReflection->invokeArgs( $methodReflection->isStatic() ? null : $this->object, - $args ); - } - - public function __set( $name, $value ) { - $propertyReflection = $this->getProperty( $name ); - - if ( $this->isStatic() && !$propertyReflection->isStatic() ) { - throw new DomainException( __METHOD__ . ': Cannot set property when wrapping static class' ); - } - - $propertyReflection->setValue( $this->object, $value ); - } - - public function __get( $name ) { - $propertyReflection = $this->getProperty( $name ); - - if ( $this->isStatic() && !$propertyReflection->isStatic() ) { - throw new DomainException( __METHOD__ . ': Cannot get property when wrapping static class' ); - } - - return $propertyReflection->getValue( $this->object ); - } - - private function isStatic() { - return is_string( $this->object ); - } - - /** - * Return a property and make it accessible. - * @param string $name - * @return ReflectionMethod - */ - private function getMethod( $name ) { - $classReflection = new ReflectionClass( $this->object ); - $methodReflection = $classReflection->getMethod( $name ); - $methodReflection->setAccessible( true ); - return $methodReflection; - } - - /** - * Return a property and make it accessible. - * - * ReflectionClass::getProperty() fails if the private property is defined - * in a parent class. This works more like ReflectionClass::getMethod(). - * - * @param string $name - * @return ReflectionProperty - * @throws ReflectionException - */ - private function getProperty( $name ) { - $classReflection = new ReflectionClass( $this->object ); - try { - $propertyReflection = $classReflection->getProperty( $name ); - } catch ( ReflectionException $ex ) { - while ( true ) { - $classReflection = $classReflection->getParentClass(); - if ( !$classReflection ) { - throw $ex; - } - try { - $propertyReflection = $classReflection->getProperty( $name ); - } catch ( ReflectionException $ex2 ) { - continue; - } - if ( $propertyReflection->isPrivate() ) { - break; - } else { - throw $ex; - } - } - } - $propertyReflection->setAccessible( true ); - return $propertyReflection; - } -} diff --git a/www/wiki/tests/phpunit/includes/TestingAccessWrapperTest.php b/www/wiki/tests/phpunit/includes/TestingAccessWrapperTest.php deleted file mode 100644 index 23eb023a..00000000 --- a/www/wiki/tests/phpunit/includes/TestingAccessWrapperTest.php +++ /dev/null @@ -1,119 +0,0 @@ -<?php - -class TestingAccessWrapperTest extends MediaWikiTestCase { - protected $raw; - protected $wrapped; - protected $wrappedStatic; - - function setUp() { - parent::setUp(); - - require_once __DIR__ . '/../data/helpers/WellProtectedClass.php'; - $this->raw = new WellProtectedClass(); - $this->wrapped = TestingAccessWrapper::newFromObject( $this->raw ); - $this->wrappedStatic = TestingAccessWrapper::newFromClass( 'WellProtectedClass' ); - } - - /** - * @expectedException InvalidArgumentException - */ - function testConstructorException() { - TestingAccessWrapper::newFromObject( 'WellProtectedClass' ); - } - - /** - * @expectedException InvalidArgumentException - */ - function testStaticConstructorException() { - TestingAccessWrapper::newFromClass( new WellProtectedClass() ); - } - - function testGetProperty() { - $this->assertSame( 1, $this->wrapped->property ); - $this->assertSame( 42, $this->wrapped->privateProperty ); - $this->assertSame( 9000, $this->wrapped->privateParentProperty ); - $this->assertSame( 'sp', $this->wrapped->staticProperty ); - $this->assertSame( 'spp', $this->wrapped->staticPrivateProperty ); - $this->assertSame( 'sp', $this->wrappedStatic->staticProperty ); - $this->assertSame( 'spp', $this->wrappedStatic->staticPrivateProperty ); - } - - /** - * @expectedException DomainException - */ - function testGetException() { - $this->wrappedStatic->property; - } - - function testSetProperty() { - $this->wrapped->property = 10; - $this->assertSame( 10, $this->wrapped->property ); - $this->assertSame( 10, $this->raw->getProperty() ); - - $this->wrapped->privateProperty = 11; - $this->assertSame( 11, $this->wrapped->privateProperty ); - $this->assertSame( 11, $this->raw->getPrivateProperty() ); - - $this->wrapped->privateParentProperty = 12; - $this->assertSame( 12, $this->wrapped->privateParentProperty ); - $this->assertSame( 12, $this->raw->getPrivateParentProperty() ); - - $this->wrapped->staticProperty = 'x'; - $this->assertSame( 'x', $this->wrapped->staticProperty ); - $this->assertSame( 'x', $this->wrappedStatic->staticProperty ); - - $this->wrapped->staticPrivateProperty = 'y'; - $this->assertSame( 'y', $this->wrapped->staticPrivateProperty ); - $this->assertSame( 'y', $this->wrappedStatic->staticPrivateProperty ); - - $this->wrappedStatic->staticProperty = 'X'; - $this->assertSame( 'X', $this->wrapped->staticProperty ); - $this->assertSame( 'X', $this->wrappedStatic->staticProperty ); - - $this->wrappedStatic->staticPrivateProperty = 'Y'; - $this->assertSame( 'Y', $this->wrapped->staticPrivateProperty ); - $this->assertSame( 'Y', $this->wrappedStatic->staticPrivateProperty ); - - // don't rely on PHPUnit to restore static properties - $this->wrapped->staticProperty = 'sp'; - $this->wrapped->staticPrivateProperty = 'spp'; - } - - /** - * @expectedException DomainException - */ - function testSetException() { - $this->wrappedStatic->property = 1; - } - - function testCallMethod() { - $this->wrapped->incrementPropertyValue(); - $this->assertSame( 2, $this->wrapped->property ); - $this->assertSame( 2, $this->raw->getProperty() ); - - $this->wrapped->incrementPrivatePropertyValue(); - $this->assertSame( 43, $this->wrapped->privateProperty ); - $this->assertSame( 43, $this->raw->getPrivateProperty() ); - - $this->wrapped->incrementPrivateParentPropertyValue(); - $this->assertSame( 9001, $this->wrapped->privateParentProperty ); - $this->assertSame( 9001, $this->raw->getPrivateParentProperty() ); - - $this->assertSame( 'sm', $this->wrapped->staticMethod() ); - $this->assertSame( 'spm', $this->wrapped->staticPrivateMethod() ); - $this->assertSame( 'sm', $this->wrappedStatic->staticMethod() ); - $this->assertSame( 'spm', $this->wrappedStatic->staticPrivateMethod() ); - } - - function testCallMethodTwoArgs() { - $this->assertSame( 'two', $this->wrapped->whatSecondArg( 'one', 'two' ) ); - } - - /** - * @expectedException DomainException - */ - function testCallMethodException() { - $this->wrappedStatic->incrementPropertyValue(); - } - -} diff --git a/www/wiki/tests/phpunit/includes/TitleArrayFromResultTest.php b/www/wiki/tests/phpunit/includes/TitleArrayFromResultTest.php index 7c2973f9..af49ecf7 100644 --- a/www/wiki/tests/phpunit/includes/TitleArrayFromResultTest.php +++ b/www/wiki/tests/phpunit/includes/TitleArrayFromResultTest.php @@ -4,10 +4,12 @@ * @author Addshore * @covers TitleArrayFromResult */ -class TitleArrayFromResultTest extends PHPUnit_Framework_TestCase { +class TitleArrayFromResultTest extends PHPUnit\Framework\TestCase { + + use MediaWikiCoversValidator; private function getMockResultWrapper( $row = null, $numRows = 1 ) { - $resultWrapper = $this->getMockBuilder( 'ResultWrapper' ) + $resultWrapper = $this->getMockBuilder( Wikimedia\Rdbms\ResultWrapper::class ) ->disableOriginalConstructor(); $resultWrapper = $resultWrapper->getMock(); @@ -59,7 +61,7 @@ class TitleArrayFromResultTest extends PHPUnit_Framework_TestCase { $this->assertEquals( $resultWrapper, $object->res ); $this->assertSame( 0, $object->key ); - $this->assertInstanceOf( 'Title', $object->current ); + $this->assertInstanceOf( Title::class, $object->current ); $this->assertEquals( $namespace, $object->current->mNamespace ); $this->assertEquals( $title, $object->current->mTextform ); } @@ -92,7 +94,7 @@ class TitleArrayFromResultTest extends PHPUnit_Framework_TestCase { $title = 'foo'; $row = $this->getRowWithTitle( $namespace, $title ); $object = $this->getTitleArrayFromResult( $this->getMockResultWrapper( $row ) ); - $this->assertInstanceOf( 'Title', $object->current() ); + $this->assertInstanceOf( Title::class, $object->current() ); $this->assertEquals( $namespace, $object->current->mNamespace ); $this->assertEquals( $title, $object->current->mTextform ); } diff --git a/www/wiki/tests/phpunit/includes/TitleMethodsTest.php b/www/wiki/tests/phpunit/includes/TitleMethodsTest.php index d9c01cb9..4032b3a1 100644 --- a/www/wiki/tests/phpunit/includes/TitleMethodsTest.php +++ b/www/wiki/tests/phpunit/includes/TitleMethodsTest.php @@ -29,7 +29,7 @@ class TitleMethodsTest extends MediaWikiLangTestCase { ] ); - MWNamespace::getCanonicalNamespaces( true ); # reset namespace cache + MWNamespace::clearCaches(); $wgContLang->resetNamespaces(); # reset namespace cache } @@ -38,7 +38,7 @@ class TitleMethodsTest extends MediaWikiLangTestCase { parent::tearDown(); - MWNamespace::getCanonicalNamespaces( true ); # reset namespace cache + MWNamespace::clearCaches(); $wgContLang->resetNamespaces(); # reset namespace cache } @@ -164,7 +164,7 @@ class TitleMethodsTest extends MediaWikiLangTestCase { $this->assertTrue( $title->hasContentModel( $expectedModelId ) ); } - public static function provideIsCssOrJsPage() { + public static function provideIsSiteConfigPage() { return [ [ 'Help:Foo', false ], [ 'Help:Foo.js', false ], @@ -172,14 +172,21 @@ class TitleMethodsTest extends MediaWikiLangTestCase { [ 'User:Foo', false ], [ 'User:Foo.js', false ], [ 'User:Foo/bar.js', false ], + [ 'User:Foo/bar.json', false ], [ 'User:Foo/bar.css', false ], + [ 'User:Foo/bar.JS', false ], + [ 'User:Foo/bar.JSON', false ], + [ 'User:Foo/bar.CSS', false ], [ 'User talk:Foo/bar.css', false ], [ 'User:Foo/bar.js.xxx', false ], [ 'User:Foo/bar.xxx', false ], [ 'MediaWiki:Foo.js', true ], + [ 'MediaWiki:Foo.json', true ], [ 'MediaWiki:Foo.css', true ], [ 'MediaWiki:Foo.JS', false ], + [ 'MediaWiki:Foo.JSON', false ], [ 'MediaWiki:Foo.CSS', false ], + [ 'MediaWiki:Foo/bar.css', true ], [ 'MediaWiki:Foo.css.xxx', false ], [ 'TEST-JS:Foo', false ], [ 'TEST-JS:Foo.js', false ], @@ -187,15 +194,15 @@ class TitleMethodsTest extends MediaWikiLangTestCase { } /** - * @dataProvider provideIsCssOrJsPage - * @covers Title::isCssOrJsPage + * @dataProvider provideIsSiteConfigPage + * @covers Title::isSiteConfigPage */ - public function testIsCssOrJsPage( $title, $expectedBool ) { + public function testSiteConfigPage( $title, $expectedBool ) { $title = Title::newFromText( $title ); - $this->assertEquals( $expectedBool, $title->isCssOrJsPage() ); + $this->assertEquals( $expectedBool, $title->isSiteConfigPage() ); } - public static function provideIsCssJsSubpage() { + public static function provideIsUserConfigPage() { return [ [ 'Help:Foo', false ], [ 'Help:Foo.js', false ], @@ -203,67 +210,79 @@ class TitleMethodsTest extends MediaWikiLangTestCase { [ 'User:Foo', false ], [ 'User:Foo.js', false ], [ 'User:Foo/bar.js', true ], + [ 'User:Foo/bar.JS', false ], + [ 'User:Foo/bar.json', true ], + [ 'User:Foo/bar.JSON', false ], [ 'User:Foo/bar.css', true ], + [ 'User:Foo/bar.CSS', false ], [ 'User talk:Foo/bar.css', false ], [ 'User:Foo/bar.js.xxx', false ], [ 'User:Foo/bar.xxx', false ], [ 'MediaWiki:Foo.js', false ], - [ 'User:Foo/bar.JS', false ], - [ 'User:Foo/bar.CSS', false ], + [ 'MediaWiki:Foo.json', false ], + [ 'MediaWiki:Foo.css', false ], + [ 'MediaWiki:Foo.JS', false ], + [ 'MediaWiki:Foo.JSON', false ], + [ 'MediaWiki:Foo.CSS', false ], + [ 'MediaWiki:Foo.css.xxx', false ], [ 'TEST-JS:Foo', false ], [ 'TEST-JS:Foo.js', false ], ]; } /** - * @dataProvider provideIsCssJsSubpage - * @covers Title::isCssJsSubpage + * @dataProvider provideIsUserConfigPage + * @covers Title::isUserConfigPage */ - public function testIsCssJsSubpage( $title, $expectedBool ) { + public function testIsUserConfigPage( $title, $expectedBool ) { $title = Title::newFromText( $title ); - $this->assertEquals( $expectedBool, $title->isCssJsSubpage() ); + $this->assertEquals( $expectedBool, $title->isUserConfigPage() ); } - public static function provideIsCssSubpage() { + public static function provideIsUserCssConfigPage() { return [ [ 'Help:Foo', false ], [ 'Help:Foo.css', false ], [ 'User:Foo', false ], [ 'User:Foo.js', false ], + [ 'User:Foo.json', false ], [ 'User:Foo.css', false ], [ 'User:Foo/bar.js', false ], + [ 'User:Foo/bar.json', false ], [ 'User:Foo/bar.css', true ], ]; } /** - * @dataProvider provideIsCssSubpage - * @covers Title::isCssSubpage + * @dataProvider provideIsUserCssConfigPage + * @covers Title::isUserCssConfigPage */ - public function testIsCssSubpage( $title, $expectedBool ) { + public function testIsUserCssConfigPage( $title, $expectedBool ) { $title = Title::newFromText( $title ); - $this->assertEquals( $expectedBool, $title->isCssSubpage() ); + $this->assertEquals( $expectedBool, $title->isUserCssConfigPage() ); } - public static function provideIsJsSubpage() { + public static function provideIsUserJsConfigPage() { return [ [ 'Help:Foo', false ], [ 'Help:Foo.css', false ], [ 'User:Foo', false ], [ 'User:Foo.js', false ], + [ 'User:Foo.json', false ], [ 'User:Foo.css', false ], [ 'User:Foo/bar.js', true ], + [ 'User:Foo/bar.json', false ], [ 'User:Foo/bar.css', false ], ]; } /** - * @dataProvider provideIsJsSubpage - * @covers Title::isJsSubpage + * @dataProvider provideIsUserJsConfigPage + * @covers Title::isUserJsConfigPage */ - public function testIsJsSubpage( $title, $expectedBool ) { + public function testIsUserJsConfigPage( $title, $expectedBool ) { $title = Title::newFromText( $title ); - $this->assertEquals( $expectedBool, $title->isJsSubpage() ); + $this->assertEquals( $expectedBool, $title->isUserJsConfigPage() ); } public static function provideIsWikitextPage() { @@ -274,18 +293,23 @@ class TitleMethodsTest extends MediaWikiLangTestCase { [ 'User:Foo', true ], [ 'User:Foo.js', true ], [ 'User:Foo/bar.js', false ], + [ 'User:Foo/bar.json', false ], [ 'User:Foo/bar.css', false ], [ 'User talk:Foo/bar.css', true ], [ 'User:Foo/bar.js.xxx', true ], [ 'User:Foo/bar.xxx', true ], [ 'MediaWiki:Foo.js', false ], - [ 'MediaWiki:Foo.css', false ], - [ 'MediaWiki:Foo/bar.css', false ], [ 'User:Foo/bar.JS', true ], + [ 'User:Foo/bar.JSON', true ], [ 'User:Foo/bar.CSS', true ], + [ 'MediaWiki:Foo.json', false ], + [ 'MediaWiki:Foo.css', false ], + [ 'MediaWiki:Foo.JS', true ], + [ 'MediaWiki:Foo.JSON', true ], + [ 'MediaWiki:Foo.CSS', true ], + [ 'MediaWiki:Foo.css.xxx', true ], [ 'TEST-JS:Foo', false ], [ 'TEST-JS:Foo.js', false ], - [ 'TEST-JS_TALK:Foo.js', true ], ]; } @@ -318,13 +342,16 @@ class TitleMethodsTest extends MediaWikiLangTestCase { */ public function testGetOtherPage( $text, $expected ) { if ( $expected === null ) { - $this->setExpectedException( 'MWException' ); + $this->setExpectedException( MWException::class ); } $title = Title::newFromText( $text ); $this->assertEquals( $expected, $title->getOtherPage()->getPrefixedText() ); } + /** + * @covers Title::clearCaches + */ public function testClearCaches() { $linkCache = LinkCache::singleton(); diff --git a/www/wiki/tests/phpunit/includes/TitlePermissionTest.php b/www/wiki/tests/phpunit/includes/TitlePermissionTest.php index c2516935..6600aa23 100644 --- a/www/wiki/tests/phpunit/includes/TitlePermissionTest.php +++ b/www/wiki/tests/phpunit/includes/TitlePermissionTest.php @@ -96,6 +96,7 @@ class TitlePermissionTest extends MediaWikiLangTestCase { /** * @todo This test method should be split up into separate test methods and * data providers + * @covers Title::checkQuickPermissions */ public function testQuickPermissions() { global $wgContLang; @@ -386,6 +387,7 @@ class TitlePermissionTest extends MediaWikiLangTestCase { /** * @todo This test method should be split up into separate test methods and * data providers + * @covers Title::checkSpecialsAndNSPermissions */ public function testSpecialsAndNSPermissions() { global $wgNamespaceProtection; @@ -442,91 +444,203 @@ class TitlePermissionTest extends MediaWikiLangTestCase { /** * @todo This test method should be split up into separate test methods and * data providers + * @covers Title::checkUserConfigPermissions */ - public function testCssAndJavascriptPermissions() { + public function testJsConfigEditPermissions() { $this->setUser( $this->userName ); $this->setTitle( NS_USER, $this->userName . '/test.js' ); - $this->runCSSandJSPermissions( + $this->runConfigEditPermissions( + [ [ 'badaccess-group0' ], [ 'mycustomjsprotected', 'bogus' ] ], + [ [ 'badaccess-group0' ], [ 'mycustomjsprotected', 'bogus' ] ], [ [ 'badaccess-group0' ], [ 'mycustomjsprotected', 'bogus' ] ], [ [ 'badaccess-group0' ] ], + + [ [ 'badaccess-group0' ], [ 'mycustomjsprotected', 'bogus' ] ], [ [ 'badaccess-group0' ], [ 'mycustomjsprotected', 'bogus' ] ], [ [ 'badaccess-group0' ] ] ); + } + + /** + * @todo This test method should be split up into separate test methods and + * data providers + * @covers Title::checkUserConfigPermissions + */ + public function testJsonConfigEditPermissions() { + $this->setUser( $this->userName ); + + $this->setTitle( NS_USER, $this->userName . '/test.json' ); + $this->runConfigEditPermissions( + [ [ 'badaccess-group0' ], [ 'mycustomjsonprotected', 'bogus' ] ], + + [ [ 'badaccess-group0' ], [ 'mycustomjsonprotected', 'bogus' ] ], + [ [ 'badaccess-group0' ] ], + [ [ 'badaccess-group0' ], [ 'mycustomjsonprotected', 'bogus' ] ], + + [ [ 'badaccess-group0' ], [ 'mycustomjsonprotected', 'bogus' ] ], + [ [ 'badaccess-group0' ] ], + [ [ 'badaccess-group0' ], [ 'mycustomjsonprotected', 'bogus' ] ] + ); + } + + /** + * @todo This test method should be split up into separate test methods and + * data providers + * @covers Title::checkUserConfigPermissions + */ + public function testCssConfigEditPermissions() { + $this->setUser( $this->userName ); $this->setTitle( NS_USER, $this->userName . '/test.css' ); - $this->runCSSandJSPermissions( + $this->runConfigEditPermissions( [ [ 'badaccess-group0' ], [ 'mycustomcssprotected', 'bogus' ] ], + [ [ 'badaccess-group0' ] ], [ [ 'badaccess-group0' ], [ 'mycustomcssprotected', 'bogus' ] ], + [ [ 'badaccess-group0' ], [ 'mycustomcssprotected', 'bogus' ] ], + [ [ 'badaccess-group0' ] ], + [ [ 'badaccess-group0' ], [ 'mycustomcssprotected', 'bogus' ] ], [ [ 'badaccess-group0' ], [ 'mycustomcssprotected', 'bogus' ] ] ); + } + + /** + * @todo This test method should be split up into separate test methods and + * data providers + * @covers Title::checkUserConfigPermissions + */ + public function testOtherJsConfigEditPermissions() { + $this->setUser( $this->userName ); $this->setTitle( NS_USER, $this->altUserName . '/test.js' ); - $this->runCSSandJSPermissions( + $this->runConfigEditPermissions( + [ [ 'badaccess-group0' ], [ 'customjsprotected', 'bogus' ] ], + + [ [ 'badaccess-group0' ], [ 'customjsprotected', 'bogus' ] ], [ [ 'badaccess-group0' ], [ 'customjsprotected', 'bogus' ] ], [ [ 'badaccess-group0' ], [ 'customjsprotected', 'bogus' ] ], + [ [ 'badaccess-group0' ], [ 'customjsprotected', 'bogus' ] ], [ [ 'badaccess-group0' ], [ 'customjsprotected', 'bogus' ] ], [ [ 'badaccess-group0' ] ] ); + } + + /** + * @todo This test method should be split up into separate test methods and + * data providers + * @covers Title::checkUserConfigPermissions + */ + public function testOtherJsonConfigEditPermissions() { + $this->setUser( $this->userName ); + + $this->setTitle( NS_USER, $this->altUserName . '/test.json' ); + $this->runConfigEditPermissions( + [ [ 'badaccess-group0' ], [ 'customjsonprotected', 'bogus' ] ], + + [ [ 'badaccess-group0' ], [ 'customjsonprotected', 'bogus' ] ], + [ [ 'badaccess-group0' ], [ 'customjsonprotected', 'bogus' ] ], + [ [ 'badaccess-group0' ], [ 'customjsonprotected', 'bogus' ] ], + + [ [ 'badaccess-group0' ], [ 'customjsonprotected', 'bogus' ] ], + [ [ 'badaccess-group0' ] ], + [ [ 'badaccess-group0' ], [ 'customjsonprotected', 'bogus' ] ] + ); + } + + /** + * @todo This test method should be split up into separate test methods and + * data providers + * @covers Title::checkUserConfigPermissions + */ + public function testOtherCssConfigEditPermissions() { + $this->setUser( $this->userName ); $this->setTitle( NS_USER, $this->altUserName . '/test.css' ); - $this->runCSSandJSPermissions( + $this->runConfigEditPermissions( + [ [ 'badaccess-group0' ], [ 'customcssprotected', 'bogus' ] ], + [ [ 'badaccess-group0' ], [ 'customcssprotected', 'bogus' ] ], [ [ 'badaccess-group0' ], [ 'customcssprotected', 'bogus' ] ], [ [ 'badaccess-group0' ], [ 'customcssprotected', 'bogus' ] ], + [ [ 'badaccess-group0' ] ], + [ [ 'badaccess-group0' ], [ 'customcssprotected', 'bogus' ] ], [ [ 'badaccess-group0' ], [ 'customcssprotected', 'bogus' ] ] ); + } + + /** + * @todo This test method should be split up into separate test methods and + * data providers + * @covers Title::checkUserConfigPermissions + */ + public function testOtherNonConfigEditPermissions() { + $this->setUser( $this->userName ); $this->setTitle( NS_USER, $this->altUserName . '/tempo' ); - $this->runCSSandJSPermissions( + $this->runConfigEditPermissions( + [ [ 'badaccess-group0' ] ], + + [ [ 'badaccess-group0' ] ], [ [ 'badaccess-group0' ] ], [ [ 'badaccess-group0' ] ], + [ [ 'badaccess-group0' ] ], [ [ 'badaccess-group0' ] ], [ [ 'badaccess-group0' ] ] ); } - protected function runCSSandJSPermissions( $result0, $result1, $result2, $result3, $result4 ) { + protected function runConfigEditPermissions( + $resultNone, + $resultMyCss, + $resultMyJson, + $resultMyJs, + $resultUserCss, + $resultUserJson, + $resultUserJs + ) { $this->setUserPerm( '' ); - $this->assertEquals( $result0, - $this->title->getUserPermissionsErrors( 'bogus', - $this->user ) ); + $result = $this->title->getUserPermissionsErrors( 'bogus', $this->user ); + $this->assertEquals( $resultNone, $result ); $this->setUserPerm( 'editmyusercss' ); - $this->assertEquals( $result1, - $this->title->getUserPermissionsErrors( 'bogus', - $this->user ) ); + $result = $this->title->getUserPermissionsErrors( 'bogus', $this->user ); + $this->assertEquals( $resultMyCss, $result ); + + $this->setUserPerm( 'editmyuserjson' ); + $result = $this->title->getUserPermissionsErrors( 'bogus', $this->user ); + $this->assertEquals( $resultMyJson, $result ); $this->setUserPerm( 'editmyuserjs' ); - $this->assertEquals( $result2, - $this->title->getUserPermissionsErrors( 'bogus', - $this->user ) ); + $result = $this->title->getUserPermissionsErrors( 'bogus', $this->user ); + $this->assertEquals( $resultMyJs, $result ); $this->setUserPerm( 'editusercss' ); - $this->assertEquals( $result3, - $this->title->getUserPermissionsErrors( 'bogus', - $this->user ) ); + $result = $this->title->getUserPermissionsErrors( 'bogus', $this->user ); + $this->assertEquals( $resultUserCss, $result ); + + $this->setUserPerm( 'edituserjson' ); + $result = $this->title->getUserPermissionsErrors( 'bogus', $this->user ); + $this->assertEquals( $resultUserJson, $result ); $this->setUserPerm( 'edituserjs' ); - $this->assertEquals( $result4, - $this->title->getUserPermissionsErrors( 'bogus', - $this->user ) ); + $result = $this->title->getUserPermissionsErrors( 'bogus', $this->user ); + $this->assertEquals( $resultUserJs, $result ); - $this->setUserPerm( [ 'edituserjs', 'editusercss' ] ); - $this->assertEquals( [ [ 'badaccess-group0' ] ], - $this->title->getUserPermissionsErrors( 'bogus', - $this->user ) ); + $this->setUserPerm( [ 'edituserjs', 'edituserjson', 'editusercss' ] ); + $result = $this->title->getUserPermissionsErrors( 'bogus', $this->user ); + $this->assertEquals( [ [ 'badaccess-group0' ] ], $result ); } /** * @todo This test method should be split up into separate test methods and * data providers + * @covers Title::checkPageRestrictions */ public function testPageRestrictions() { global $wgContLang; @@ -619,6 +733,9 @@ class TitlePermissionTest extends MediaWikiLangTestCase { $this->user ) ); } + /** + * @covers Title::checkCascadingSourcesRestrictions + */ public function testCascadingSourcesRestrictions() { $this->setTitle( NS_MAIN, "test page" ); $this->setUserPerm( [ "edit", "bogus" ] ); @@ -648,6 +765,7 @@ class TitlePermissionTest extends MediaWikiLangTestCase { /** * @todo This test method should be split up into separate test methods and * data providers + * @covers Title::checkActionPermissions */ public function testActionPermissions() { $this->setUserPerm( [ "createpage" ] ); @@ -720,6 +838,9 @@ class TitlePermissionTest extends MediaWikiLangTestCase { $this->title->userCan( 'move-target', $this->user ) ); } + /** + * @covers Title::checkUserBlock + */ public function testUserBlock() { $this->setMwGlobals( [ 'wgEmailConfirmToEdit' => true, diff --git a/www/wiki/tests/phpunit/includes/TitleTest.php b/www/wiki/tests/phpunit/includes/TitleTest.php index b0febe8d..c81a0787 100644 --- a/www/wiki/tests/phpunit/includes/TitleTest.php +++ b/www/wiki/tests/phpunit/includes/TitleTest.php @@ -163,7 +163,7 @@ class TitleTest extends MediaWikiTestCase { */ public function testSecureAndSplitValid( $text ) { $this->secureAndSplitGlobals(); - $this->assertInstanceOf( 'Title', Title::newFromText( $text ), "Valid: $text" ); + $this->assertInstanceOf( Title::class, Title::newFromText( $text ), "Valid: $text" ); } /** @@ -282,7 +282,6 @@ class TitleTest extends MediaWikiTestCase { /** * Auth-less test of Title::isValidMoveOperation * - * @group Database * @param string $source * @param string $target * @param array|string|bool $expected Required error @@ -435,7 +434,7 @@ class TitleTest extends MediaWikiTestCase { $this->setContentLang( $contLang ); $title = Title::newFromText( $titleText ); - $this->assertInstanceOf( 'Title', $title, + $this->assertInstanceOf( Title::class, $title, "Test must be passed a valid title text, you gave '$titleText'" ); $this->assertEquals( $expected, @@ -553,6 +552,7 @@ class TitleTest extends MediaWikiTestCase { } /** + * @covers Title::newFromTitleValue * @dataProvider provideNewFromTitleValue */ public function testNewFromTitleValue( TitleValue $value ) { @@ -573,6 +573,7 @@ class TitleTest extends MediaWikiTestCase { } /** + * @covers Title::getTitleValue * @dataProvider provideGetTitleValue */ public function testGetTitleValue( $text ) { @@ -604,6 +605,7 @@ class TitleTest extends MediaWikiTestCase { } /** + * @covers Title::getFragment * @dataProvider provideGetFragment * * @param string $full @@ -912,4 +914,55 @@ class TitleTest extends MediaWikiTestCase { public function testGetPrefixedDBKey( Title $title, $expected ) { $this->assertEquals( $expected, $title->getPrefixedDBkey() ); } + + /** + * @covers Title::getFragmentForURL + * @dataProvider provideGetFragmentForURL + * + * @param string $titleStr + * @param string $expected + */ + public function testGetFragmentForURL( $titleStr, $expected ) { + $this->setMwGlobals( [ + 'wgFragmentMode' => [ 'html5' ], + 'wgExternalInterwikiFragmentMode' => 'legacy', + ] ); + $dbw = wfGetDB( DB_MASTER ); + $dbw->insert( 'interwiki', + [ + [ + 'iw_prefix' => 'de', + 'iw_url' => 'http://de.wikipedia.org/wiki/', + 'iw_api' => 'http://de.wikipedia.org/w/api.php', + 'iw_wikiid' => 'dewiki', + 'iw_local' => 1, + 'iw_trans' => 0, + ], + [ + 'iw_prefix' => 'zz', + 'iw_url' => 'http://zzwiki.org/wiki/', + 'iw_api' => 'http://zzwiki.org/w/api.php', + 'iw_wikiid' => 'zzwiki', + 'iw_local' => 0, + 'iw_trans' => 0, + ], + ], + __METHOD__, + [ 'IGNORE' ] + ); + + $title = Title::newFromText( $titleStr ); + self::assertEquals( $expected, $title->getFragmentForURL() ); + + $dbw->delete( 'interwiki', '*', __METHOD__ ); + } + + public function provideGetFragmentForURL() { + return [ + [ 'Foo', '' ], + [ 'Foo#ümlåût', '#ümlåût' ], + [ 'de:Foo#Bå®', '#Bå®' ], + [ 'zz:Foo#тест', '#.D1.82.D0.B5.D1.81.D1.82' ], + ]; + } } diff --git a/www/wiki/tests/phpunit/includes/WatchedItemIntegrationTest.php b/www/wiki/tests/phpunit/includes/WatchedItemIntegrationTest.php deleted file mode 100644 index 01e7ecb9..00000000 --- a/www/wiki/tests/phpunit/includes/WatchedItemIntegrationTest.php +++ /dev/null @@ -1,145 +0,0 @@ -<?php -use MediaWiki\MediaWikiServices; - -/** - * @author Addshore - * - * @group Database - * - * @covers WatchedItem - */ -class WatchedItemIntegrationTest extends MediaWikiTestCase { - - public function setUp() { - parent::setUp(); - self::$users['WatchedItemIntegrationTestUser'] - = new TestUser( 'WatchedItemIntegrationTestUser' ); - - $this->hideDeprecated( 'WatchedItem::fromUserTitle' ); - $this->hideDeprecated( 'WatchedItem::addWatch' ); - $this->hideDeprecated( 'WatchedItem::removeWatch' ); - $this->hideDeprecated( 'WatchedItem::isWatched' ); - $this->hideDeprecated( 'WatchedItem::duplicateEntries' ); - $this->hideDeprecated( 'WatchedItem::batchAddWatch' ); - } - - private function getUser() { - return self::$users['WatchedItemIntegrationTestUser']->getUser(); - } - - public function testWatchAndUnWatchItem() { - $user = $this->getUser(); - $title = Title::newFromText( 'WatchedItemIntegrationTestPage' ); - // Cleanup after previous tests - WatchedItem::fromUserTitle( $user, $title )->removeWatch(); - - $this->assertFalse( - WatchedItem::fromUserTitle( $user, $title )->isWatched(), - 'Page should not initially be watched' - ); - WatchedItem::fromUserTitle( $user, $title )->addWatch(); - $this->assertTrue( - WatchedItem::fromUserTitle( $user, $title )->isWatched(), - 'Page should be watched' - ); - WatchedItem::fromUserTitle( $user, $title )->removeWatch(); - $this->assertFalse( - WatchedItem::fromUserTitle( $user, $title )->isWatched(), - 'Page should be unwatched' - ); - } - - public function testUpdateAndResetNotificationTimestamp() { - $user = $this->getUser(); - $otherUser = ( new TestUser( 'WatchedItemIntegrationTestUser_otherUser' ) )->getUser(); - $title = Title::newFromText( 'WatchedItemIntegrationTestPage' ); - WatchedItem::fromUserTitle( $user, $title )->addWatch(); - $this->assertNull( WatchedItem::fromUserTitle( $user, $title )->getNotificationTimestamp() ); - - EmailNotification::updateWatchlistTimestamp( $otherUser, $title, '20150202010101' ); - $this->assertEquals( - '20150202010101', - WatchedItem::fromUserTitle( $user, $title )->getNotificationTimestamp() - ); - - MediaWikiServices::getInstance()->getWatchedItemStore()->resetNotificationTimestamp( - $user, $title - ); - $this->assertNull( WatchedItem::fromUserTitle( $user, $title )->getNotificationTimestamp() ); - } - - public function testDuplicateAllAssociatedEntries() { - $user = $this->getUser(); - $titleOld = Title::newFromText( 'WatchedItemIntegrationTestPageOld' ); - $titleNew = Title::newFromText( 'WatchedItemIntegrationTestPageNew' ); - WatchedItem::fromUserTitle( $user, $titleOld->getSubjectPage() )->addWatch(); - WatchedItem::fromUserTitle( $user, $titleOld->getTalkPage() )->addWatch(); - // Cleanup after previous tests - WatchedItem::fromUserTitle( $user, $titleNew->getSubjectPage() )->removeWatch(); - WatchedItem::fromUserTitle( $user, $titleNew->getTalkPage() )->removeWatch(); - - WatchedItem::duplicateEntries( $titleOld, $titleNew ); - - $this->assertTrue( - WatchedItem::fromUserTitle( $user, $titleOld->getSubjectPage() )->isWatched() - ); - $this->assertTrue( - WatchedItem::fromUserTitle( $user, $titleOld->getTalkPage() )->isWatched() - ); - $this->assertTrue( - WatchedItem::fromUserTitle( $user, $titleNew->getSubjectPage() )->isWatched() - ); - $this->assertTrue( - WatchedItem::fromUserTitle( $user, $titleNew->getTalkPage() )->isWatched() - ); - } - - public function testIsWatched_falseOnNotAllowed() { - $user = $this->getUser(); - $title = Title::newFromText( 'WatchedItemIntegrationTestPage' ); - WatchedItem::fromUserTitle( $user, $title )->addWatch(); - - $this->assertTrue( WatchedItem::fromUserTitle( $user, $title )->isWatched() ); - $user->mRights = []; - $this->assertFalse( WatchedItem::fromUserTitle( $user, $title )->isWatched() ); - } - - public function testGetNotificationTimestamp_falseOnNotAllowed() { - $user = $this->getUser(); - $title = Title::newFromText( 'WatchedItemIntegrationTestPage' ); - WatchedItem::fromUserTitle( $user, $title )->addWatch(); - MediaWikiServices::getInstance()->getWatchedItemStore()->resetNotificationTimestamp( - $user, $title - ); - - $this->assertEquals( - null, - WatchedItem::fromUserTitle( $user, $title )->getNotificationTimestamp() - ); - $user->mRights = []; - $this->assertFalse( WatchedItem::fromUserTitle( $user, $title )->getNotificationTimestamp() ); - } - - public function testRemoveWatch_falseOnNotAllowed() { - $user = $this->getUser(); - $title = Title::newFromText( 'WatchedItemIntegrationTestPage' ); - WatchedItem::fromUserTitle( $user, $title )->addWatch(); - - $previousRights = $user->mRights; - $user->mRights = []; - $this->assertFalse( WatchedItem::fromUserTitle( $user, $title )->removeWatch() ); - $user->mRights = $previousRights; - $this->assertTrue( WatchedItem::fromUserTitle( $user, $title )->removeWatch() ); - } - - public function testGetNotificationTimestamp_falseOnNotWatched() { - $user = $this->getUser(); - $title = Title::newFromText( 'WatchedItemIntegrationTestPage' ); - - WatchedItem::fromUserTitle( $user, $title )->removeWatch(); - $this->assertFalse( WatchedItem::fromUserTitle( $user, $title )->isWatched() ); - - $this->assertFalse( WatchedItem::fromUserTitle( $user, $title )->getNotificationTimestamp() ); - } - -} diff --git a/www/wiki/tests/phpunit/includes/WatchedItemUnitTest.php b/www/wiki/tests/phpunit/includes/WatchedItemUnitTest.php deleted file mode 100644 index 4544db49..00000000 --- a/www/wiki/tests/phpunit/includes/WatchedItemUnitTest.php +++ /dev/null @@ -1,161 +0,0 @@ -<?php -use MediaWiki\Linker\LinkTarget; - -/** - * @author Addshore - * - * @covers WatchedItem - */ -class WatchedItemUnitTest extends MediaWikiTestCase { - - public function setUp() { - parent::setUp(); - - $this->hideDeprecated( 'WatchedItem::fromUserTitle' ); - $this->hideDeprecated( 'WatchedItem::addWatch' ); - $this->hideDeprecated( 'WatchedItem::removeWatch' ); - $this->hideDeprecated( 'WatchedItem::isWatched' ); - $this->hideDeprecated( 'WatchedItem::duplicateEntries' ); - $this->hideDeprecated( 'WatchedItem::batchAddWatch' ); - } - - /** - * @param int $id - * - * @return PHPUnit_Framework_MockObject_MockObject|User - */ - private function getMockUser( $id ) { - $user = $this->createMock( User::class ); - $user->expects( $this->any() ) - ->method( 'getId' ) - ->will( $this->returnValue( $id ) ); - $user->expects( $this->any() ) - ->method( 'isAllowed' ) - ->will( $this->returnValue( true ) ); - return $user; - } - - public function provideUserTitleTimestamp() { - $user = $this->getMockUser( 111 ); - return [ - [ $user, Title::newFromText( 'SomeTitle' ), null ], - [ $user, Title::newFromText( 'SomeTitle' ), '20150101010101' ], - [ $user, new TitleValue( 0, 'TVTitle', 'frag' ), '20150101010101' ], - ]; - } - - /** - * @return PHPUnit_Framework_MockObject_MockObject|WatchedItemStore - */ - private function getMockWatchedItemStore() { - return $this->getMockBuilder( WatchedItemStore::class ) - ->disableOriginalConstructor() - ->getMock(); - } - - /** - * @dataProvider provideUserTitleTimestamp - */ - public function testConstruction( $user, LinkTarget $linkTarget, $notifTimestamp ) { - $item = new WatchedItem( $user, $linkTarget, $notifTimestamp ); - - $this->assertSame( $user, $item->getUser() ); - $this->assertSame( $linkTarget, $item->getLinkTarget() ); - $this->assertSame( $notifTimestamp, $item->getNotificationTimestamp() ); - - // The below tests the internal WatchedItem::getTitle method - $this->assertInstanceOf( 'Title', $item->getTitle() ); - $this->assertSame( $linkTarget->getDBkey(), $item->getTitle()->getDBkey() ); - $this->assertSame( $linkTarget->getFragment(), $item->getTitle()->getFragment() ); - $this->assertSame( $linkTarget->getNamespace(), $item->getTitle()->getNamespace() ); - $this->assertSame( $linkTarget->getText(), $item->getTitle()->getText() ); - } - - /** - * @dataProvider provideUserTitleTimestamp - */ - public function testFromUserTitle( $user, $linkTarget, $timestamp ) { - $store = $this->getMockWatchedItemStore(); - $store->expects( $this->once() ) - ->method( 'loadWatchedItem' ) - ->with( $user, $linkTarget ) - ->will( $this->returnValue( new WatchedItem( $user, $linkTarget, $timestamp ) ) ); - $this->setService( 'WatchedItemStore', $store ); - - $item = WatchedItem::fromUserTitle( $user, $linkTarget, User::IGNORE_USER_RIGHTS ); - - $this->assertEquals( $user, $item->getUser() ); - $this->assertEquals( $linkTarget, $item->getLinkTarget() ); - $this->assertEquals( $timestamp, $item->getNotificationTimestamp() ); - } - - public function testAddWatch() { - $title = Title::newFromText( 'SomeTitle' ); - $timestamp = null; - $checkRights = 0; - - /** @var User|PHPUnit_Framework_MockObject_MockObject $user */ - $user = $this->createMock( User::class ); - $user->expects( $this->once() ) - ->method( 'addWatch' ) - ->with( $title, $checkRights ); - - $item = new WatchedItem( $user, $title, $timestamp, $checkRights ); - $this->assertTrue( $item->addWatch() ); - } - - public function testRemoveWatch() { - $title = Title::newFromText( 'SomeTitle' ); - $timestamp = null; - $checkRights = 0; - - /** @var User|PHPUnit_Framework_MockObject_MockObject $user */ - $user = $this->createMock( User::class ); - $user->expects( $this->once() ) - ->method( 'removeWatch' ) - ->with( $title, $checkRights ); - - $item = new WatchedItem( $user, $title, $timestamp, $checkRights ); - $this->assertTrue( $item->removeWatch() ); - } - - public function provideBooleans() { - return [ - [ true ], - [ false ], - ]; - } - - /** - * @dataProvider provideBooleans - */ - public function testIsWatched( $returnValue ) { - $title = Title::newFromText( 'SomeTitle' ); - $timestamp = null; - $checkRights = 0; - - /** @var User|PHPUnit_Framework_MockObject_MockObject $user */ - $user = $this->createMock( User::class ); - $user->expects( $this->once() ) - ->method( 'isWatched' ) - ->with( $title, $checkRights ) - ->will( $this->returnValue( $returnValue ) ); - - $item = new WatchedItem( $user, $title, $timestamp, $checkRights ); - $this->assertEquals( $returnValue, $item->isWatched() ); - } - - public function testDuplicateEntries() { - $oldTitle = Title::newFromText( 'OldTitle' ); - $newTitle = Title::newFromText( 'NewTitle' ); - - $store = $this->getMockWatchedItemStore(); - $store->expects( $this->once() ) - ->method( 'duplicateAllAssociatedEntries' ) - ->with( $oldTitle, $newTitle ); - $this->setService( 'WatchedItemStore', $store ); - - WatchedItem::duplicateEntries( $oldTitle, $newTitle ); - } - -} diff --git a/www/wiki/tests/phpunit/includes/WebRequestTest.php b/www/wiki/tests/phpunit/includes/WebRequestTest.php index 041e7e3c..9583921d 100644 --- a/www/wiki/tests/phpunit/includes/WebRequestTest.php +++ b/www/wiki/tests/phpunit/includes/WebRequestTest.php @@ -26,7 +26,7 @@ class WebRequestTest extends MediaWikiTestCase { public function testDetectServer( $expected, $input, $description ) { $this->setMwGlobals( 'wgAssumeProxiesUseDefaultProtocolPorts', true ); - $_SERVER = $input; + $this->setServerVars( $input ); $result = WebRequest::detectServer(); $this->assertEquals( $expected, $result, $description ); } @@ -126,7 +126,7 @@ class WebRequestTest extends MediaWikiTestCase { protected function mockWebRequest( $data = [] ) { // Cannot use PHPUnit getMockBuilder() as it does not support // overriding protected properties afterwards - $reflection = new ReflectionClass( 'WebRequest' ); + $reflection = new ReflectionClass( WebRequest::class ); $req = $reflection->newInstanceWithoutConstructor(); $prop = $reflection->getProperty( 'data' ); @@ -363,7 +363,7 @@ class WebRequestTest extends MediaWikiTestCase { * @covers WebRequest::getIP */ public function testGetIP( $expected, $input, $squid, $xffList, $private, $description ) { - $_SERVER = $input; + $this->setServerVars( $input ); $this->setMwGlobals( [ 'wgUsePrivateIPs' => $private, 'wgHooks' => [ @@ -608,8 +608,19 @@ class WebRequestTest extends MediaWikiTestCase { * @covers WebRequest::getAcceptLang */ public function testAcceptLang( $acceptLanguageHeader, $expectedLanguages, $description ) { - $_SERVER = [ 'HTTP_ACCEPT_LANGUAGE' => $acceptLanguageHeader ]; + $this->setServerVars( [ 'HTTP_ACCEPT_LANGUAGE' => $acceptLanguageHeader ] ); $request = new WebRequest(); $this->assertSame( $request->getAcceptLang(), $expectedLanguages, $description ); } + + protected function setServerVars( $vars ) { + // Don't remove vars which should be available in all SAPI. + if ( !isset( $vars['REQUEST_TIME_FLOAT'] ) ) { + $vars['REQUEST_TIME_FLOAT'] = $_SERVER['REQUEST_TIME_FLOAT']; + } + if ( !isset( $vars['REQUEST_TIME'] ) ) { + $vars['REQUEST_TIME'] = $_SERVER['REQUEST_TIME']; + } + $_SERVER = $vars; + } } diff --git a/www/wiki/tests/phpunit/includes/WikiReferenceTest.php b/www/wiki/tests/phpunit/includes/WikiReferenceTest.php index 724dd605..e4b21ce5 100644 --- a/www/wiki/tests/phpunit/includes/WikiReferenceTest.php +++ b/www/wiki/tests/phpunit/includes/WikiReferenceTest.php @@ -3,8 +3,9 @@ /** * @covers WikiReference */ +class WikiReferenceTest extends PHPUnit\Framework\TestCase { -class WikiReferenceTest extends PHPUnit_Framework_TestCase { + use MediaWikiCoversValidator; public function provideGetDisplayName() { return [ diff --git a/www/wiki/tests/phpunit/includes/XmlJsTest.php b/www/wiki/tests/phpunit/includes/XmlJsTest.php index 29e97eb6..c7975efa 100644 --- a/www/wiki/tests/phpunit/includes/XmlJsTest.php +++ b/www/wiki/tests/phpunit/includes/XmlJsTest.php @@ -3,7 +3,9 @@ /** * @group Xml */ -class XmlJs extends PHPUnit_Framework_TestCase { +class XmlJsTest extends PHPUnit\Framework\TestCase { + + use MediaWikiCoversValidator; /** * @covers XmlJsCode::__construct diff --git a/www/wiki/tests/phpunit/includes/XmlTest.php b/www/wiki/tests/phpunit/includes/XmlTest.php index c5572b46..e46fc67f 100644 --- a/www/wiki/tests/phpunit/includes/XmlTest.php +++ b/www/wiki/tests/phpunit/includes/XmlTest.php @@ -34,6 +34,11 @@ class XmlTest extends MediaWikiTestCase { ] ); } + protected function tearDown() { + Language::factory( 'en' )->resetNamespaces(); + parent::tearDown(); + } + /** * @covers Xml::expandAttributes */ @@ -50,7 +55,7 @@ class XmlTest extends MediaWikiTestCase { * @covers Xml::expandAttributes */ public function testExpandAttributesException() { - $this->setExpectedException( 'MWException' ); + $this->setExpectedException( MWException::class ); Xml::expandAttributes( 'string' ); } @@ -136,6 +141,57 @@ class XmlTest extends MediaWikiTestCase { $this->assertEquals( '</element>', Xml::closeElement( 'element' ), 'closeElement() shortcut' ); } + public function provideMonthSelector() { + global $wgLang; + + $header = '<select name="month" id="month" class="mw-month-selector">'; + $header2 = '<select name="month" id="monthSelector" class="mw-month-selector">'; + $monthsString = ''; + for ( $i = 1; $i < 13; $i++ ) { + $monthName = $wgLang->getMonthName( $i ); + $monthsString .= "<option value=\"{$i}\">{$monthName}</option>"; + if ( $i !== 12 ) { + $monthsString .= "\n"; + } + } + $monthsString2 = str_replace( + '<option value="12">December</option>', + '<option value="12" selected="">December</option>', + $monthsString + ); + $end = '</select>'; + + $allMonths = "<option value=\"AllMonths\">all</option>\n"; + return [ + [ $header . $monthsString . $end, '', null, 'month' ], + [ $header . $monthsString2 . $end, 12, null, 'month' ], + [ $header2 . $monthsString . $end, '', null, 'monthSelector' ], + [ $header . $allMonths . $monthsString . $end, '', 'AllMonths', 'month' ], + + ]; + } + + /** + * @covers Xml::monthSelector + * @dataProvider provideMonthSelector + */ + public function testMonthSelector( $expected, $selected, $allmonths, $id ) { + $this->assertEquals( + $expected, + Xml::monthSelector( $selected, $allmonths, $id ) + ); + } + + /** + * @covers Xml::span + */ + public function testSpan() { + $this->assertEquals( + '<span class="foo" id="testSpan">element</span>', + Xml::span( 'element', 'foo', [ 'id' => 'testSpan' ] ) + ); + } + /** * @covers Xml::dateMenu */ @@ -528,4 +584,34 @@ class XmlTest extends MediaWikiTestCase { 'Entire element with legend and attributes' ); } + + /** + * @covers Xml::buildTable + */ + public function testBuildTable() { + $firstRow = [ 'foo', 'bar' ]; + $secondRow = [ 'Berlin', 'Tehran' ]; + $headers = [ 'header1', 'header2' ]; + $expected = '<table id="testTable"><thead id="testTable"><th>header1</th>' . + '<th>header2</th></thead><tr><td>foo</td><td>bar</td></tr><tr><td>Berlin</td>' . + '<td>Tehran</td></tr></table>'; + $this->assertEquals( + $expected, + Xml::buildTable( + [ $firstRow, $secondRow ], + [ 'id' => 'testTable' ], + $headers + ) + ); + } + + /** + * @covers Xml::buildTableRow + */ + public function testBuildTableRow() { + $this->assertEquals( + '<tr id="testRow"><td>foo</td><td>bar</td></tr>', + Xml::buildTableRow( [ 'id' => 'testRow' ], [ 'foo', 'bar' ] ) + ); + } } diff --git a/www/wiki/tests/phpunit/includes/actions/ActionTest.php b/www/wiki/tests/phpunit/includes/actions/ActionTest.php index 4a302920..b96b4914 100644 --- a/www/wiki/tests/phpunit/includes/actions/ActionTest.php +++ b/www/wiki/tests/phpunit/includes/actions/ActionTest.php @@ -3,10 +3,11 @@ /** * @covers Action * - * @author Thiemo Mättig - * * @group Action * @group Database + * + * @license GNU GPL v2+ + * @author Thiemo Kreuz */ class ActionTest extends MediaWikiTestCase { @@ -19,7 +20,7 @@ class ActionTest extends MediaWikiTestCase { 'disabled' => false, 'view' => true, 'edit' => true, - 'revisiondelete' => 'SpecialPageAction', + 'revisiondelete' => SpecialPageAction::class, 'dummy' => true, 'string' => 'NamedDummyAction', 'declared' => 'NonExistingClassName', diff --git a/www/wiki/tests/phpunit/includes/api/ApiBaseTest.php b/www/wiki/tests/phpunit/includes/api/ApiBaseTest.php index ee0ad946..4bffc742 100644 --- a/www/wiki/tests/phpunit/includes/api/ApiBaseTest.php +++ b/www/wiki/tests/phpunit/includes/api/ApiBaseTest.php @@ -6,12 +6,45 @@ use Wikimedia\TestingAccessWrapper; * @group API * @group Database * @group medium + * + * @covers ApiBase */ class ApiBaseTest extends ApiTestCase { - /** - * @covers ApiBase::requireOnlyOneParameter + * This covers a variety of stub methods that return a fixed value. + * + * @param string|array $method Name of method, or [ name, params... ] + * @param string $value Expected value + * + * @dataProvider provideStubMethods */ + public function testStubMethods( $expected, $method, $args = [] ) { + // Some of these are protected + $mock = TestingAccessWrapper::newFromObject( new MockApi() ); + $result = call_user_func_array( [ $mock, $method ], $args ); + $this->assertSame( $expected, $result ); + } + + public function provideStubMethods() { + return [ + [ null, 'getModuleManager' ], + [ null, 'getCustomPrinter' ], + [ [], 'getHelpUrls' ], + // @todo This is actually overriden by MockApi + // [ [], 'getAllowedParams' ], + [ true, 'shouldCheckMaxLag' ], + [ true, 'isReadMode' ], + [ false, 'isWriteMode' ], + [ false, 'mustBePosted' ], + [ false, 'isDeprecated' ], + [ false, 'isInternal' ], + [ false, 'needsToken' ], + [ null, 'getWebUITokenSalt', [ [] ] ], + [ null, 'getConditionalRequestData', [ 'etag' ] ], + [ null, 'dynamicParameterDocumentation' ], + ]; + } + public function testRequireOnlyOneParameterDefault() { $mock = new MockApi(); $mock->requireOnlyOneParameter( @@ -23,7 +56,6 @@ class ApiBaseTest extends ApiTestCase { /** * @expectedException ApiUsageException - * @covers ApiBase::requireOnlyOneParameter */ public function testRequireOnlyOneParameterZero() { $mock = new MockApi(); @@ -35,7 +67,6 @@ class ApiBaseTest extends ApiTestCase { /** * @expectedException ApiUsageException - * @covers ApiBase::requireOnlyOneParameter */ public function testRequireOnlyOneParameterTrue() { $mock = new MockApi(); @@ -45,37 +76,230 @@ class ApiBaseTest extends ApiTestCase { ); } + public function testRequireOnlyOneParameterMissing() { + $this->setExpectedException( ApiUsageException::class, + 'One of the parameters "foo" and "bar" is required.' ); + $mock = new MockApi(); + $mock->requireOnlyOneParameter( + [ "filename" => "foo.txt", "enablechunks" => false ], + "foo", "bar" ); + } + + public function testRequireMaxOneParameterZero() { + $mock = new MockApi(); + $mock->requireMaxOneParameter( + [ 'foo' => 'bar', 'baz' => 'quz' ], + 'squirrel' ); + $this->assertTrue( true ); + } + + public function testRequireMaxOneParameterOne() { + $mock = new MockApi(); + $mock->requireMaxOneParameter( + [ 'foo' => 'bar', 'baz' => 'quz' ], + 'foo', 'squirrel' ); + $this->assertTrue( true ); + } + + public function testRequireMaxOneParameterTwo() { + $this->setExpectedException( ApiUsageException::class, + 'The parameters "foo" and "baz" can not be used together.' ); + $mock = new MockApi(); + $mock->requireMaxOneParameter( + [ 'foo' => 'bar', 'baz' => 'quz' ], + 'foo', 'baz' ); + } + + public function testRequireAtLeastOneParameterZero() { + $this->setExpectedException( ApiUsageException::class, + 'At least one of the parameters "foo" and "bar" is required.' ); + $mock = new MockApi(); + $mock->requireAtLeastOneParameter( + [ 'a' => 'b', 'c' => 'd' ], + 'foo', 'bar' ); + } + + public function testRequireAtLeastOneParameterOne() { + $mock = new MockApi(); + $mock->requireAtLeastOneParameter( + [ 'a' => 'b', 'c' => 'd' ], + 'foo', 'a' ); + $this->assertTrue( true ); + } + + public function testRequireAtLeastOneParameterTwo() { + $mock = new MockApi(); + $mock->requireAtLeastOneParameter( + [ 'a' => 'b', 'c' => 'd' ], + 'a', 'c' ); + $this->assertTrue( true ); + } + + public function testGetTitleOrPageIdBadParams() { + $this->setExpectedException( ApiUsageException::class, + 'The parameters "title" and "pageid" can not be used together.' ); + $mock = new MockApi(); + $mock->getTitleOrPageId( [ 'title' => 'a', 'pageid' => 7 ] ); + } + + public function testGetTitleOrPageIdTitle() { + $mock = new MockApi(); + $result = $mock->getTitleOrPageId( [ 'title' => 'Foo' ] ); + $this->assertInstanceOf( WikiPage::class, $result ); + $this->assertSame( 'Foo', $result->getTitle()->getPrefixedText() ); + } + + public function testGetTitleOrPageIdInvalidTitle() { + $this->setExpectedException( ApiUsageException::class, + 'Bad title "|".' ); + $mock = new MockApi(); + $mock->getTitleOrPageId( [ 'title' => '|' ] ); + } + + public function testGetTitleOrPageIdSpecialTitle() { + $this->setExpectedException( ApiUsageException::class, + "Namespace doesn't allow actual pages." ); + $mock = new MockApi(); + $mock->getTitleOrPageId( [ 'title' => 'Special:RandomPage' ] ); + } + + public function testGetTitleOrPageIdPageId() { + $result = ( new MockApi() )->getTitleOrPageId( + [ 'pageid' => Title::newFromText( 'UTPage' )->getArticleId() ] ); + $this->assertInstanceOf( WikiPage::class, $result ); + $this->assertSame( 'UTPage', $result->getTitle()->getPrefixedText() ); + } + + public function testGetTitleOrPageIdInvalidPageId() { + $this->setExpectedException( ApiUsageException::class, + 'There is no page with ID 2147483648.' ); + $mock = new MockApi(); + $mock->getTitleOrPageId( [ 'pageid' => 2147483648 ] ); + } + + public function testGetTitleFromTitleOrPageIdBadParams() { + $this->setExpectedException( ApiUsageException::class, + 'The parameters "title" and "pageid" can not be used together.' ); + $mock = new MockApi(); + $mock->getTitleFromTitleOrPageId( [ 'title' => 'a', 'pageid' => 7 ] ); + } + + public function testGetTitleFromTitleOrPageIdTitle() { + $mock = new MockApi(); + $result = $mock->getTitleFromTitleOrPageId( [ 'title' => 'Foo' ] ); + $this->assertInstanceOf( Title::class, $result ); + $this->assertSame( 'Foo', $result->getPrefixedText() ); + } + + public function testGetTitleFromTitleOrPageIdInvalidTitle() { + $this->setExpectedException( ApiUsageException::class, + 'Bad title "|".' ); + $mock = new MockApi(); + $mock->getTitleFromTitleOrPageId( [ 'title' => '|' ] ); + } + + public function testGetTitleFromTitleOrPageIdPageId() { + $result = ( new MockApi() )->getTitleFromTitleOrPageId( + [ 'pageid' => Title::newFromText( 'UTPage' )->getArticleId() ] ); + $this->assertInstanceOf( Title::class, $result ); + $this->assertSame( 'UTPage', $result->getPrefixedText() ); + } + + public function testGetTitleFromTitleOrPageIdInvalidPageId() { + $this->setExpectedException( ApiUsageException::class, + 'There is no page with ID 298401643.' ); + $mock = new MockApi(); + $mock->getTitleFromTitleOrPageId( [ 'pageid' => 298401643 ] ); + } + /** * @dataProvider provideGetParameterFromSettings * @param string|null $input * @param array $paramSettings * @param mixed $expected + * @param array $options Key-value pairs: + * 'parseLimits': true|false + * 'apihighlimits': true|false + * 'internalmode': true|false * @param string[] $warnings */ - public function testGetParameterFromSettings( $input, $paramSettings, $expected, $warnings ) { + public function testGetParameterFromSettings( + $input, $paramSettings, $expected, $warnings, $options = [] + ) { $mock = new MockApi(); $wrapper = TestingAccessWrapper::newFromObject( $mock ); $context = new DerivativeContext( $mock ); - $context->setRequest( new FauxRequest( $input !== null ? [ 'foo' => $input ] : [] ) ); + $context->setRequest( new FauxRequest( + $input !== null ? [ 'myParam' => $input ] : [] ) ); $wrapper->mMainModule = new ApiMain( $context ); - if ( $expected instanceof ApiUsageException ) { + $parseLimits = isset( $options['parseLimits'] ) ? + $options['parseLimits'] : true; + + if ( !empty( $options['apihighlimits'] ) ) { + $context->setUser( self::$users['sysop']->getUser() ); + } + + if ( isset( $options['internalmode'] ) && !$options['internalmode'] ) { + $mainWrapper = TestingAccessWrapper::newFromObject( $wrapper->mMainModule ); + $mainWrapper->mInternalMode = false; + } + + // If we're testing tags, set up some tags + if ( isset( $paramSettings[ApiBase::PARAM_TYPE] ) && + $paramSettings[ApiBase::PARAM_TYPE] === 'tags' + ) { + ChangeTags::defineTag( 'tag1' ); + ChangeTags::defineTag( 'tag2' ); + } + + if ( $expected instanceof Exception ) { try { - $wrapper->getParameterFromSettings( 'foo', $paramSettings, true ); - } catch ( ApiUsageException $ex ) { + $wrapper->getParameterFromSettings( 'myParam', $paramSettings, + $parseLimits ); + $this->fail( 'No exception thrown' ); + } catch ( Exception $ex ) { $this->assertEquals( $expected, $ex ); } } else { - $result = $wrapper->getParameterFromSettings( 'foo', $paramSettings, true ); - $this->assertSame( $expected, $result ); - $this->assertSame( $warnings, $mock->warnings ); + $result = $wrapper->getParameterFromSettings( 'myParam', + $paramSettings, $parseLimits ); + if ( isset( $paramSettings[ApiBase::PARAM_TYPE] ) && + $paramSettings[ApiBase::PARAM_TYPE] === 'timestamp' && + $expected === 'now' + ) { + // Allow one second of fuzziness. Make sure the formats are + // correct! + $this->assertRegExp( '/^\d{14}$/', $result ); + $this->assertLessThanOrEqual( 1, + abs( wfTimestamp( TS_UNIX, $result ) - time() ), + "Result $result differs from expected $expected by " . + 'more than one second' ); + } else { + $this->assertSame( $expected, $result ); + } + $actualWarnings = array_map( function ( $warn ) { + return $warn instanceof Message + ? array_merge( [ $warn->getKey() ], $warn->getParams() ) + : $warn; + }, $mock->warnings ); + $this->assertSame( $warnings, $actualWarnings ); + } + + if ( !empty( $paramSettings[ApiBase::PARAM_SENSITIVE] ) || + ( isset( $paramSettings[ApiBase::PARAM_TYPE] ) && + $paramSettings[ApiBase::PARAM_TYPE] === 'password' ) + ) { + $mainWrapper = TestingAccessWrapper::newFromObject( $wrapper->getMain() ); + $this->assertSame( [ 'myParam' ], + $mainWrapper->getSensitiveParams() ); } } public static function provideGetParameterFromSettings() { $warnings = [ - [ 'apiwarn-badutf8', 'foo' ], + [ 'apiwarn-badutf8', 'myParam' ], ]; $c0 = ''; @@ -87,7 +311,7 @@ class ApiBaseTest extends ApiTestCase { : '�'; } - return [ + $returnArray = [ 'Basic param' => [ 'bar', null, 'bar', [] ], 'Basic param, C0 controls' => [ $c0, null, $enc, $warnings ], 'String param' => [ 'bar', '', 'bar', [] ], @@ -96,7 +320,8 @@ class ApiBaseTest extends ApiTestCase { 'String param, required, empty' => [ '', [ ApiBase::PARAM_DFLT => 'default', ApiBase::PARAM_REQUIRED => true ], - ApiUsageException::newWithMessage( null, [ 'apierror-missingparam', 'foo' ] ), + ApiUsageException::newWithMessage( null, + [ 'apierror-missingparam', 'myParam' ] ), [] ], 'Multi-valued parameter' => [ @@ -123,7 +348,843 @@ class ApiBaseTest extends ApiTestCase { [ substr( $enc, 0, -3 ), '' ], $warnings ], + 'Multi-valued parameter with limits' => [ + 'a|b|c', + [ + ApiBase::PARAM_ISMULTI => true, + ApiBase::PARAM_ISMULTI_LIMIT1 => 3, + ], + [ 'a', 'b', 'c' ], + [], + ], + 'Multi-valued parameter with exceeded limits' => [ + 'a|b|c', + [ + ApiBase::PARAM_ISMULTI => true, + ApiBase::PARAM_ISMULTI_LIMIT1 => 2, + ], + [ 'a', 'b' ], + [ [ 'apiwarn-toomanyvalues', 'myParam', 2 ] ], + ], + 'Multi-valued parameter with exceeded limits for non-bot' => [ + 'a|b|c', + [ + ApiBase::PARAM_ISMULTI => true, + ApiBase::PARAM_ISMULTI_LIMIT1 => 2, + ApiBase::PARAM_ISMULTI_LIMIT2 => 3, + ], + [ 'a', 'b' ], + [ [ 'apiwarn-toomanyvalues', 'myParam', 2 ] ], + ], + 'Multi-valued parameter with non-exceeded limits for bot' => [ + 'a|b|c', + [ + ApiBase::PARAM_ISMULTI => true, + ApiBase::PARAM_ISMULTI_LIMIT1 => 2, + ApiBase::PARAM_ISMULTI_LIMIT2 => 3, + ], + [ 'a', 'b', 'c' ], + [], + [ 'apihighlimits' => true ], + ], + 'Multi-valued parameter with prohibited duplicates' => [ + 'a|b|a|c', + [ ApiBase::PARAM_ISMULTI => true ], + // Note that the keys are not sequential! This matches + // array_unique, but might be unexpected. + [ 0 => 'a', 1 => 'b', 3 => 'c' ], + [], + ], + 'Multi-valued parameter with allowed duplicates' => [ + 'a|a', + [ + ApiBase::PARAM_ISMULTI => true, + ApiBase::PARAM_ALLOW_DUPLICATES => true, + ], + [ 'a', 'a' ], + [], + ], + 'Empty boolean param' => [ + '', + [ ApiBase::PARAM_TYPE => 'boolean' ], + true, + [], + ], + 'Boolean param 0' => [ + '0', + [ ApiBase::PARAM_TYPE => 'boolean' ], + true, + [], + ], + 'Boolean param false' => [ + 'false', + [ ApiBase::PARAM_TYPE => 'boolean' ], + true, + [], + ], + 'Boolean multi-param' => [ + 'true|false', + [ + ApiBase::PARAM_TYPE => 'boolean', + ApiBase::PARAM_ISMULTI => true, + ], + new MWException( + 'Internal error in ApiBase::getParameterFromSettings: ' . + 'Multi-values not supported for myParam' + ), + [], + ], + 'Empty boolean param with non-false default' => [ + '', + [ + ApiBase::PARAM_TYPE => 'boolean', + ApiBase::PARAM_DFLT => true, + ], + new MWException( + 'Internal error in ApiBase::getParameterFromSettings: ' . + "Boolean param myParam's default is set to '1'. " . + 'Boolean parameters must default to false.' ), + [], + ], + 'Deprecated parameter' => [ + 'foo', + [ ApiBase::PARAM_DEPRECATED => true ], + 'foo', + [ [ 'apiwarn-deprecation-parameter', 'myParam' ] ], + ], + 'Deprecated parameter value' => [ + 'a', + [ ApiBase::PARAM_DEPRECATED_VALUES => [ 'a' => true ] ], + 'a', + [ [ 'apiwarn-deprecation-parameter', 'myParam=a' ] ], + ], + 'Multiple deprecated parameter values' => [ + 'a|b|c|d', + [ ApiBase::PARAM_DEPRECATED_VALUES => + [ 'b' => true, 'd' => true ], + ApiBase::PARAM_ISMULTI => true ], + [ 'a', 'b', 'c', 'd' ], + [ + [ 'apiwarn-deprecation-parameter', 'myParam=b' ], + [ 'apiwarn-deprecation-parameter', 'myParam=d' ], + ], + ], + 'Deprecated parameter value with custom warning' => [ + 'a', + [ ApiBase::PARAM_DEPRECATED_VALUES => [ 'a' => 'my-msg' ] ], + 'a', + [ 'my-msg' ], + ], + '"*" when wildcard not allowed' => [ + '*', + [ ApiBase::PARAM_ISMULTI => true, + ApiBase::PARAM_TYPE => [ 'a', 'b', 'c' ] ], + [], + [ [ 'apiwarn-unrecognizedvalues', 'myParam', + [ 'list' => [ '*' ], 'type' => 'comma' ], 1 ] ], + ], + 'Wildcard "*"' => [ + '*', + [ + ApiBase::PARAM_ISMULTI => true, + ApiBase::PARAM_TYPE => [ 'a', 'b', 'c' ], + ApiBase::PARAM_ALL => true, + ], + [ 'a', 'b', 'c' ], + [], + ], + 'Wildcard "*" with multiples not allowed' => [ + '*', + [ + ApiBase::PARAM_TYPE => [ 'a', 'b', 'c' ], + ApiBase::PARAM_ALL => true, + ], + ApiUsageException::newWithMessage( null, + [ 'apierror-unrecognizedvalue', 'myParam', '*' ], + 'unknown_myParam' ), + [], + ], + 'Wildcard "*" with unrestricted type' => [ + '*', + [ + ApiBase::PARAM_ISMULTI => true, + ApiBase::PARAM_ALL => true, + ], + [ '*' ], + [], + ], + 'Wildcard "x"' => [ + 'x', + [ + ApiBase::PARAM_ISMULTI => true, + ApiBase::PARAM_TYPE => [ 'a', 'b', 'c' ], + ApiBase::PARAM_ALL => 'x', + ], + [ 'a', 'b', 'c' ], + [], + ], + 'Wildcard conflicting with allowed value' => [ + 'a', + [ + ApiBase::PARAM_ISMULTI => true, + ApiBase::PARAM_TYPE => [ 'a', 'b', 'c' ], + ApiBase::PARAM_ALL => 'a', + ], + new MWException( + 'Internal error in ApiBase::getParameterFromSettings: ' . + 'For param myParam, PARAM_ALL collides with a possible ' . + 'value' ), + [], + ], + 'Namespace with wildcard' => [ + '*', + [ + ApiBase::PARAM_ISMULTI => true, + ApiBase::PARAM_TYPE => 'namespace', + ], + MWNamespace::getValidNamespaces(), + [], + ], + // PARAM_ALL is ignored with namespace types. + 'Namespace with wildcard suppressed' => [ + '*', + [ + ApiBase::PARAM_ISMULTI => true, + ApiBase::PARAM_TYPE => 'namespace', + ApiBase::PARAM_ALL => false, + ], + MWNamespace::getValidNamespaces(), + [], + ], + 'Namespace with wildcard "x"' => [ + 'x', + [ + ApiBase::PARAM_ISMULTI => true, + ApiBase::PARAM_TYPE => 'namespace', + ApiBase::PARAM_ALL => 'x', + ], + [], + [ [ 'apiwarn-unrecognizedvalues', 'myParam', + [ 'list' => [ 'x' ], 'type' => 'comma' ], 1 ] ], + ], + 'Password' => [ + 'dDy+G?e?txnr.1:(@[Ru', + [ ApiBase::PARAM_TYPE => 'password' ], + 'dDy+G?e?txnr.1:(@[Ru', + [], + ], + 'Sensitive field' => [ + 'I am fond of pineapples', + [ ApiBase::PARAM_SENSITIVE => true ], + 'I am fond of pineapples', + [], + ], + 'Upload with default' => [ + '', + [ + ApiBase::PARAM_TYPE => 'upload', + ApiBase::PARAM_DFLT => '', + ], + new MWException( + 'Internal error in ApiBase::getParameterFromSettings: ' . + "File upload param myParam's default is set to ''. " . + 'File upload parameters may not have a default.' ), + [], + ], + 'Multiple upload' => [ + '', + [ + ApiBase::PARAM_TYPE => 'upload', + ApiBase::PARAM_ISMULTI => true, + ], + new MWException( + 'Internal error in ApiBase::getParameterFromSettings: ' . + 'Multi-values not supported for myParam' ), + [], + ], + // @todo Test actual upload + 'Namespace -1' => [ + '-1', + [ ApiBase::PARAM_TYPE => 'namespace' ], + ApiUsageException::newWithMessage( null, + [ 'apierror-unrecognizedvalue', 'myParam', '-1' ], + 'unknown_myParam' ), + [], + ], + 'Extra namespace -1' => [ + '-1', + [ + ApiBase::PARAM_TYPE => 'namespace', + ApiBase::PARAM_EXTRA_NAMESPACES => [ '-1' ], + ], + '-1', + [], + ], + // @todo Test with PARAM_SUBMODULE_MAP unset, need + // getModuleManager() to return something real + 'Nonexistent module' => [ + 'not-a-module-name', + [ + ApiBase::PARAM_TYPE => 'submodule', + ApiBase::PARAM_SUBMODULE_MAP => + [ 'foo' => 'foo', 'bar' => 'foo+bar' ], + ], + ApiUsageException::newWithMessage( + null, + [ + 'apierror-unrecognizedvalue', + 'myParam', + 'not-a-module-name', + ], + 'unknown_myParam' + ), + [], + ], + '\\x1f with multiples not allowed' => [ + "\x1f", + [], + ApiUsageException::newWithMessage( null, + 'apierror-badvalue-notmultivalue', + 'badvalue_notmultivalue' ), + [], + ], + 'Integer with unenforced min' => [ + '-2', + [ + ApiBase::PARAM_TYPE => 'integer', + ApiBase::PARAM_MIN => -1, + ], + -1, + [ [ 'apierror-integeroutofrange-belowminimum', 'myParam', -1, + -2 ] ], + ], + 'Integer with enforced min' => [ + '-2', + [ + ApiBase::PARAM_TYPE => 'integer', + ApiBase::PARAM_MIN => -1, + ApiBase::PARAM_RANGE_ENFORCE => true, + ], + ApiUsageException::newWithMessage( null, + [ 'apierror-integeroutofrange-belowminimum', 'myParam', + '-1', '-2' ], 'integeroutofrange', + [ 'min' => -1, 'max' => null, 'botMax' => null ] ), + [], + ], + 'Integer with unenforced max (internal mode)' => [ + '8', + [ + ApiBase::PARAM_TYPE => 'integer', + ApiBase::PARAM_MAX => 7, + ], + 8, + [], + ], + 'Integer with enforced max (internal mode)' => [ + '8', + [ + ApiBase::PARAM_TYPE => 'integer', + ApiBase::PARAM_MAX => 7, + ApiBase::PARAM_RANGE_ENFORCE => true, + ], + 8, + [], + ], + 'Integer with unenforced max (non-internal mode)' => [ + '8', + [ + ApiBase::PARAM_TYPE => 'integer', + ApiBase::PARAM_MAX => 7, + ], + 7, + [ [ 'apierror-integeroutofrange-abovemax', 'myParam', 7, 8 ] ], + [ 'internalmode' => false ], + ], + 'Integer with enforced max (non-internal mode)' => [ + '8', + [ + ApiBase::PARAM_TYPE => 'integer', + ApiBase::PARAM_MAX => 7, + ApiBase::PARAM_RANGE_ENFORCE => true, + ], + ApiUsageException::newWithMessage( + null, + [ 'apierror-integeroutofrange-abovemax', 'myParam', '7', '8' ], + 'integeroutofrange', + [ 'min' => null, 'max' => 7, 'botMax' => 7 ] + ), + [], + [ 'internalmode' => false ], + ], + 'Array of integers' => [ + '3|12|966|-1', + [ + ApiBase::PARAM_ISMULTI => true, + ApiBase::PARAM_TYPE => 'integer', + ], + [ 3, 12, 966, -1 ], + [], + ], + 'Array of integers with unenforced min/max (internal mode)' => [ + '3|12|966|-1', + [ + ApiBase::PARAM_ISMULTI => true, + ApiBase::PARAM_TYPE => 'integer', + ApiBase::PARAM_MIN => 0, + ApiBase::PARAM_MAX => 100, + ], + [ 3, 12, 966, 0 ], + [ [ 'apierror-integeroutofrange-belowminimum', 'myParam', 0, -1 ] ], + ], + 'Array of integers with enforced min/max (internal mode)' => [ + '3|12|966|-1', + [ + ApiBase::PARAM_ISMULTI => true, + ApiBase::PARAM_TYPE => 'integer', + ApiBase::PARAM_MIN => 0, + ApiBase::PARAM_MAX => 100, + ApiBase::PARAM_RANGE_ENFORCE => true, + ], + ApiUsageException::newWithMessage( + null, + [ 'apierror-integeroutofrange-belowminimum', 'myParam', 0, -1 ], + 'integeroutofrange', + [ 'min' => 0, 'max' => 100, 'botMax' => 100 ] + ), + [], + ], + 'Array of integers with unenforced min/max (non-internal mode)' => [ + '3|12|966|-1', + [ + ApiBase::PARAM_ISMULTI => true, + ApiBase::PARAM_TYPE => 'integer', + ApiBase::PARAM_MIN => 0, + ApiBase::PARAM_MAX => 100, + ], + [ 3, 12, 100, 0 ], + [ + [ 'apierror-integeroutofrange-abovemax', 'myParam', 100, 966 ], + [ 'apierror-integeroutofrange-belowminimum', 'myParam', 0, -1 ] + ], + [ 'internalmode' => false ], + ], + 'Array of integers with enforced min/max (non-internal mode)' => [ + '3|12|966|-1', + [ + ApiBase::PARAM_ISMULTI => true, + ApiBase::PARAM_TYPE => 'integer', + ApiBase::PARAM_MIN => 0, + ApiBase::PARAM_MAX => 100, + ApiBase::PARAM_RANGE_ENFORCE => true, + ], + ApiUsageException::newWithMessage( + null, + [ 'apierror-integeroutofrange-abovemax', 'myParam', 100, 966 ], + 'integeroutofrange', + [ 'min' => 0, 'max' => 100, 'botMax' => 100 ] + ), + [], + [ 'internalmode' => false ], + ], + 'Limit with parseLimits false' => [ + '100', + [ ApiBase::PARAM_TYPE => 'limit' ], + '100', + [], + [ 'parseLimits' => false ], + ], + 'Limit with no max' => [ + '100', + [ + ApiBase::PARAM_TYPE => 'limit', + ApiBase::PARAM_MAX2 => 10, + ApiBase::PARAM_ISMULTI => true, + ], + new MWException( + 'Internal error in ApiBase::getParameterFromSettings: ' . + 'MAX1 or MAX2 are not defined for the limit myParam' ), + [], + ], + 'Limit with no max2' => [ + '100', + [ + ApiBase::PARAM_TYPE => 'limit', + ApiBase::PARAM_MAX => 10, + ApiBase::PARAM_ISMULTI => true, + ], + new MWException( + 'Internal error in ApiBase::getParameterFromSettings: ' . + 'MAX1 or MAX2 are not defined for the limit myParam' ), + [], + ], + 'Limit with multi-value' => [ + '100', + [ + ApiBase::PARAM_TYPE => 'limit', + ApiBase::PARAM_MAX => 10, + ApiBase::PARAM_MAX2 => 10, + ApiBase::PARAM_ISMULTI => true, + ], + new MWException( + 'Internal error in ApiBase::getParameterFromSettings: ' . + 'Multi-values not supported for myParam' ), + [], + ], + 'Valid limit' => [ + '100', + [ + ApiBase::PARAM_TYPE => 'limit', + ApiBase::PARAM_MAX => 100, + ApiBase::PARAM_MAX2 => 100, + ], + 100, + [], + ], + 'Limit max' => [ + 'max', + [ + ApiBase::PARAM_TYPE => 'limit', + ApiBase::PARAM_MAX => 100, + ApiBase::PARAM_MAX2 => 101, + ], + 100, + [], + ], + 'Limit max for apihighlimits' => [ + 'max', + [ + ApiBase::PARAM_TYPE => 'limit', + ApiBase::PARAM_MAX => 100, + ApiBase::PARAM_MAX2 => 101, + ], + 101, + [], + [ 'apihighlimits' => true ], + ], + 'Limit too large (internal mode)' => [ + '101', + [ + ApiBase::PARAM_TYPE => 'limit', + ApiBase::PARAM_MAX => 100, + ApiBase::PARAM_MAX2 => 101, + ], + 101, + [], + ], + 'Limit okay for apihighlimits (internal mode)' => [ + '101', + [ + ApiBase::PARAM_TYPE => 'limit', + ApiBase::PARAM_MAX => 100, + ApiBase::PARAM_MAX2 => 101, + ], + 101, + [], + [ 'apihighlimits' => true ], + ], + 'Limit too large for apihighlimits (internal mode)' => [ + '102', + [ + ApiBase::PARAM_TYPE => 'limit', + ApiBase::PARAM_MAX => 100, + ApiBase::PARAM_MAX2 => 101, + ], + 102, + [], + [ 'apihighlimits' => true ], + ], + 'Limit too large (non-internal mode)' => [ + '101', + [ + ApiBase::PARAM_TYPE => 'limit', + ApiBase::PARAM_MAX => 100, + ApiBase::PARAM_MAX2 => 101, + ], + 100, + [ [ 'apierror-integeroutofrange-abovemax', 'myParam', 100, 101 ] ], + [ 'internalmode' => false ], + ], + 'Limit okay for apihighlimits (non-internal mode)' => [ + '101', + [ + ApiBase::PARAM_TYPE => 'limit', + ApiBase::PARAM_MAX => 100, + ApiBase::PARAM_MAX2 => 101, + ], + 101, + [], + [ 'internalmode' => false, 'apihighlimits' => true ], + ], + 'Limit too large for apihighlimits (non-internal mode)' => [ + '102', + [ + ApiBase::PARAM_TYPE => 'limit', + ApiBase::PARAM_MAX => 100, + ApiBase::PARAM_MAX2 => 101, + ], + 101, + [ [ 'apierror-integeroutofrange-abovebotmax', 'myParam', 101, 102 ] ], + [ 'internalmode' => false, 'apihighlimits' => true ], + ], + 'Limit too small' => [ + '-2', + [ + ApiBase::PARAM_TYPE => 'limit', + ApiBase::PARAM_MIN => -1, + ApiBase::PARAM_MAX => 100, + ApiBase::PARAM_MAX2 => 100, + ], + -1, + [ [ 'apierror-integeroutofrange-belowminimum', 'myParam', -1, + -2 ] ], + ], + 'Timestamp' => [ + wfTimestamp( TS_UNIX, '20211221122112' ), + [ ApiBase::PARAM_TYPE => 'timestamp' ], + '20211221122112', + [], + ], + 'Timestamp 0' => [ + '0', + [ ApiBase::PARAM_TYPE => 'timestamp' ], + // Magic keyword + 'now', + [ [ 'apiwarn-unclearnowtimestamp', 'myParam', '0' ] ], + ], + 'Timestamp empty' => [ + '', + [ ApiBase::PARAM_TYPE => 'timestamp' ], + 'now', + [ [ 'apiwarn-unclearnowtimestamp', 'myParam', '' ] ], + ], + // wfTimestamp() interprets this as Unix time + 'Timestamp 00' => [ + '00', + [ ApiBase::PARAM_TYPE => 'timestamp' ], + '19700101000000', + [], + ], + 'Timestamp now' => [ + 'now', + [ ApiBase::PARAM_TYPE => 'timestamp' ], + 'now', + [], + ], + 'Invalid timestamp' => [ + 'a potato', + [ ApiBase::PARAM_TYPE => 'timestamp' ], + ApiUsageException::newWithMessage( + null, + [ 'apierror-badtimestamp', 'myParam', 'a potato' ], + 'badtimestamp_myParam' + ), + [], + ], + 'Timestamp array' => [ + '100|101', + [ + ApiBase::PARAM_TYPE => 'timestamp', + ApiBase::PARAM_ISMULTI => 1, + ], + [ wfTimestamp( TS_MW, 100 ), wfTimestamp( TS_MW, 101 ) ], + [], + ], + 'User' => [ + 'foo_bar', + [ ApiBase::PARAM_TYPE => 'user' ], + 'Foo bar', + [], + ], + 'Invalid username "|"' => [ + '|', + [ ApiBase::PARAM_TYPE => 'user' ], + ApiUsageException::newWithMessage( null, + [ 'apierror-baduser', 'myParam', '|' ], + 'baduser_myParam' ), + [], + ], + 'Invalid username "300.300.300.300"' => [ + '300.300.300.300', + [ ApiBase::PARAM_TYPE => 'user' ], + ApiUsageException::newWithMessage( null, + [ 'apierror-baduser', 'myParam', '300.300.300.300' ], + 'baduser_myParam' ), + [], + ], + 'IP range as username' => [ + '10.0.0.0/8', + [ ApiBase::PARAM_TYPE => 'user' ], + '10.0.0.0/8', + [], + ], + 'IPv6 as username' => [ + '::1', + [ ApiBase::PARAM_TYPE => 'user' ], + '0:0:0:0:0:0:0:1', + [], + ], + 'Obsolete cloaked usemod IP address as username' => [ + '1.2.3.xxx', + [ ApiBase::PARAM_TYPE => 'user' ], + '1.2.3.xxx', + [], + ], + 'Invalid username containing IP address' => [ + 'This is [not] valid 1.2.3.xxx, ha!', + [ ApiBase::PARAM_TYPE => 'user' ], + ApiUsageException::newWithMessage( + null, + [ 'apierror-baduser', 'myParam', 'This is [not] valid 1.2.3.xxx, ha!' ], + 'baduser_myParam' + ), + [], + ], + 'External username' => [ + 'M>Foo bar', + [ ApiBase::PARAM_TYPE => 'user' ], + 'M>Foo bar', + [], + ], + 'Array of usernames' => [ + 'foo|bar', + [ + ApiBase::PARAM_TYPE => 'user', + ApiBase::PARAM_ISMULTI => true, + ], + [ 'Foo', 'Bar' ], + [], + ], + 'tag' => [ + 'tag1', + [ ApiBase::PARAM_TYPE => 'tags' ], + [ 'tag1' ], + [], + ], + 'Array of one tag' => [ + 'tag1', + [ + ApiBase::PARAM_TYPE => 'tags', + ApiBase::PARAM_ISMULTI => true, + ], + [ 'tag1' ], + [], + ], + 'Array of tags' => [ + 'tag1|tag2', + [ + ApiBase::PARAM_TYPE => 'tags', + ApiBase::PARAM_ISMULTI => true, + ], + [ 'tag1', 'tag2' ], + [], + ], + 'Invalid tag' => [ + 'invalid tag', + [ ApiBase::PARAM_TYPE => 'tags' ], + new ApiUsageException( null, + Status::newFatal( 'tags-apply-not-allowed-one', + 'invalid tag', 1 ) ), + [], + ], + 'Unrecognized type' => [ + 'foo', + [ ApiBase::PARAM_TYPE => 'nonexistenttype' ], + new MWException( + 'Internal error in ApiBase::getParameterFromSettings: ' . + "Param myParam's type is unknown - nonexistenttype" ), + [], + ], + 'Too many bytes' => [ + '1', + [ + ApiBase::PARAM_MAX_BYTES => 0, + ApiBase::PARAM_MAX_CHARS => 0, + ], + ApiUsageException::newWithMessage( null, + [ 'apierror-maxbytes', 'myParam', 0 ] ), + [], + ], + 'Too many chars' => [ + '§§', + [ + ApiBase::PARAM_MAX_BYTES => 4, + ApiBase::PARAM_MAX_CHARS => 1, + ], + ApiUsageException::newWithMessage( null, + [ 'apierror-maxchars', 'myParam', 1 ] ), + [], + ], + 'Omitted required param' => [ + null, + [ ApiBase::PARAM_REQUIRED => true ], + ApiUsageException::newWithMessage( null, + [ 'apierror-missingparam', 'myParam' ] ), + [], + ], + 'Empty multi-value' => [ + '', + [ ApiBase::PARAM_ISMULTI => true ], + [], + [], + ], + 'Multi-value \x1f' => [ + "\x1f", + [ ApiBase::PARAM_ISMULTI => true ], + [], + [], + ], + 'Allowed non-multi-value with "|"' => [ + 'a|b', + [ ApiBase::PARAM_TYPE => [ 'a|b' ] ], + 'a|b', + [], + ], + 'Prohibited multi-value' => [ + 'a|b', + [ ApiBase::PARAM_TYPE => [ 'a', 'b' ] ], + ApiUsageException::newWithMessage( null, + [ + 'apierror-multival-only-one-of', + 'myParam', + Message::listParam( [ '<kbd>a</kbd>', '<kbd>b</kbd>' ] ), + 2 + ], + 'multival_myParam' + ), + [], + ], + ]; + + // The following really just test PHP's string-to-int conversion. + $integerTests = [ + [ '+1', 1 ], + [ '-1', -1 ], + [ '1.5', 1 ], + [ '-1.5', -1 ], + [ '1abc', 1 ], + [ ' 1', 1 ], + [ "\t1", 1, '\t1' ], + [ "\r1", 1, '\r1' ], + [ "\f1", 0, '\f1', 'badutf-8' ], + [ "\n1", 1, '\n1' ], + [ "\v1", 0, '\v1', 'badutf-8' ], + [ "\e1", 0, '\e1', 'badutf-8' ], + [ "\x001", 0, '\x001', 'badutf-8' ], ]; + + foreach ( $integerTests as $test ) { + $desc = isset( $test[2] ) ? $test[2] : $test[0]; + $warnings = isset( $test[3] ) ? + [ [ 'apiwarn-badutf8', 'myParam' ] ] : []; + $returnArray["\"$desc\" as integer"] = [ + $test[0], + [ ApiBase::PARAM_TYPE => 'integer' ], + $test[1], + $warnings, + ]; + } + + return $returnArray; } public function testErrorArrayToStatus() { @@ -153,6 +1214,7 @@ class ApiBaseTest extends ApiTestCase { $block = new \Block( [ 'address' => $user->getName(), 'user' => $user->getID(), + 'by' => $this->getTestSysop()->getUser()->getId(), 'reason' => __METHOD__, 'expiry' => time() + 100500, ] ); @@ -174,9 +1236,6 @@ class ApiBaseTest extends ApiTestCase { ], $user ) ); } - /** - * @covers ApiBase::dieStatus - */ public function testDieStatus() { $mock = new MockApi(); diff --git a/www/wiki/tests/phpunit/includes/api/ApiBlockTest.php b/www/wiki/tests/phpunit/includes/api/ApiBlockTest.php index 832a113f..efefc09d 100644 --- a/www/wiki/tests/phpunit/includes/api/ApiBlockTest.php +++ b/www/wiki/tests/phpunit/includes/api/ApiBlockTest.php @@ -8,13 +8,20 @@ * @covers ApiBlock */ class ApiBlockTest extends ApiTestCase { + protected $mUser = null; + protected function setUp() { parent::setUp(); - $this->doLogin(); + + $this->mUser = $this->getMutableTestUser()->getUser(); + $this->setMwGlobals( 'wgBlockCIDRLimit', [ + 'IPv4' => 16, + 'IPv6' => 19, + ] ); } protected function tearDown() { - $block = Block::newFromTarget( 'UTApiBlockee' ); + $block = Block::newFromTarget( $this->mUser->getName() ); if ( !is_null( $block ) ) { $block->delete(); } @@ -25,80 +32,191 @@ class ApiBlockTest extends ApiTestCase { return $this->getTokenList( self::$users['sysop'] ); } - function addDBDataOnce() { - $user = User::newFromName( 'UTApiBlockee' ); - - if ( $user->getId() == 0 ) { - $user->addToDatabase(); - TestUser::setPasswordForUser( $user, 'UTApiBlockeePassword' ); - - $user->saveSettings(); - } - } - /** - * This test has probably always been broken and use an invalid token - * Bug tracking brokenness is https://phabricator.wikimedia.org/T37646 - * - * Root cause is https://gerrit.wikimedia.org/r/3434 - * Which made the Block/Unblock API to actually verify the token - * previously always considered valid (T36212). + * @param array $extraParams Extra API parameters to pass to doApiRequest + * @param User $blocker User to do the blocking, null to pick + * arbitrarily */ - public function testMakeNormalBlock() { - $tokens = $this->getTokens(); + private function doBlock( array $extraParams = [], User $blocker = null ) { + if ( $blocker === null ) { + $blocker = self::$users['sysop']->getUser(); + } - $user = User::newFromName( 'UTApiBlockee' ); + $tokens = $this->getTokens(); - if ( !$user->getId() ) { - $this->markTestIncomplete( "The user UTApiBlockee does not exist" ); - } + $this->assertNotNull( $this->mUser, 'Sanity check' ); - if ( !array_key_exists( 'blocktoken', $tokens ) ) { - $this->markTestIncomplete( "No block token found" ); - } + $this->assertArrayHasKey( 'blocktoken', $tokens, 'Sanity check' ); - $this->doApiRequest( [ + $params = [ 'action' => 'block', - 'user' => 'UTApiBlockee', + 'user' => $this->mUser->getName(), 'reason' => 'Some reason', - 'token' => $tokens['blocktoken'] ], null, false, self::$users['sysop']->getUser() ); + 'token' => $tokens['blocktoken'], + ]; + if ( array_key_exists( 'userid', $extraParams ) ) { + // Make sure we don't have both user and userid + unset( $params['user'] ); + } + $ret = $this->doApiRequest( array_merge( $params, $extraParams ), null, + false, $blocker ); - $block = Block::newFromTarget( 'UTApiBlockee' ); + $block = Block::newFromTarget( $this->mUser->getName() ); $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 ); + $this->assertSame( $this->mUser->getName(), (string)$block->getTarget() ); + $this->assertSame( 'Some reason', $block->mReason ); + + return $ret; + } + + /** + * Block by username + */ + public function testNormalBlock() { + $this->doBlock(); } /** * Block by user ID */ - public function testMakeNormalBlockId() { - $tokens = $this->getTokens(); - $user = User::newFromName( 'UTApiBlockee' ); + public function testBlockById() { + $this->doBlock( [ 'userid' => $this->mUser->getId() ] ); + } - if ( !$user->getId() ) { - $this->markTestIncomplete( "The user UTApiBlockee does not exist." ); - } + /** + * A blocked user can't block + */ + public function testBlockByBlockedUser() { + $this->setExpectedException( ApiUsageException::class, + 'You cannot block or unblock other users because you are yourself blocked.' ); + + $blocked = $this->getMutableTestUser( [ 'sysop' ] )->getUser(); + $block = new Block( [ + 'address' => $blocked->getName(), + 'by' => self::$users['sysop']->getUser()->getId(), + 'reason' => 'Capriciousness', + 'timestamp' => '19370101000000', + 'expiry' => 'infinity', + ] ); + $block->insert(); + + $this->doBlock( [], $blocked ); + } - if ( !array_key_exists( 'blocktoken', $tokens ) ) { - $this->markTestIncomplete( "No block token found" ); - } + public function testBlockOfNonexistentUser() { + $this->setExpectedException( ApiUsageException::class, + 'There is no user by the name "Nonexistent". Check your spelling.' ); - $data = $this->doApiRequest( [ - 'action' => 'block', - 'userid' => $user->getId(), - 'reason' => 'Some reason', - 'token' => $tokens['blocktoken'] ], null, false, self::$users['sysop']->getUser() ); + $this->doBlock( [ 'user' => 'Nonexistent' ] ); + } + + public function testBlockOfNonexistentUserId() { + $id = 948206325; + $this->setExpectedException( ApiUsageException::class, + "There is no user with ID $id." ); + + $this->assertFalse( User::whoIs( $id ), 'Sanity check' ); + + $this->doBlock( [ 'userid' => $id ] ); + } + + public function testBlockWithTag() { + ChangeTags::defineTag( 'custom tag' ); - $block = Block::newFromTarget( 'UTApiBlockee' ); + $this->doBlock( [ 'tags' => 'custom tag' ] ); - $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 ); + $dbw = wfGetDB( DB_MASTER ); + $this->assertSame( 'custom tag', $dbw->selectField( + [ 'change_tag', 'logging' ], + 'ct_tag', + [ 'log_type' => 'block' ], + __METHOD__, + [], + [ 'change_tag' => [ 'INNER JOIN', 'ct_log_id = log_id' ] ] + ) ); + } + + public function testBlockWithProhibitedTag() { + $this->setExpectedException( ApiUsageException::class, + 'You do not have permission to apply change tags along with your changes.' ); + + ChangeTags::defineTag( 'custom tag' ); + + $this->setMwGlobals( 'wgRevokePermissions', + [ 'user' => [ 'applychangetags' => true ] ] ); + + $this->doBlock( [ 'tags' => 'custom tag' ] ); + } + + public function testBlockWithHide() { + global $wgGroupPermissions; + $newPermissions = $wgGroupPermissions['sysop']; + $newPermissions['hideuser'] = true; + $this->mergeMwGlobalArrayValue( 'wgGroupPermissions', + [ 'sysop' => $newPermissions ] ); + + $res = $this->doBlock( [ 'hidename' => '' ] ); + + $dbw = wfGetDB( DB_MASTER ); + $this->assertSame( '1', $dbw->selectField( + 'ipblocks', + 'ipb_deleted', + [ 'ipb_id' => $res[0]['block']['id'] ], + __METHOD__ + ) ); + } + + public function testBlockWithProhibitedHide() { + $this->setExpectedException( ApiUsageException::class, + "You don't have permission to hide user names from the block log." ); + + $this->doBlock( [ 'hidename' => '' ] ); + } + + public function testBlockWithEmailBlock() { + $res = $this->doBlock( [ 'noemail' => '' ] ); + + $dbw = wfGetDB( DB_MASTER ); + $this->assertSame( '1', $dbw->selectField( + 'ipblocks', + 'ipb_block_email', + [ 'ipb_id' => $res[0]['block']['id'] ], + __METHOD__ + ) ); + } + + public function testBlockWithProhibitedEmailBlock() { + $this->setExpectedException( ApiUsageException::class, + "You don't have permission to block users from sending email through the wiki." ); + + $this->setMwGlobals( 'wgRevokePermissions', + [ 'sysop' => [ 'blockemail' => true ] ] ); + + $this->doBlock( [ 'noemail' => '' ] ); + } + + public function testBlockWithExpiry() { + $res = $this->doBlock( [ 'expiry' => '1 day' ] ); + + $dbw = wfGetDB( DB_MASTER ); + $expiry = $dbw->selectField( + 'ipblocks', + 'ipb_expiry', + [ 'ipb_id' => $res[0]['block']['id'] ], + __METHOD__ + ); + + // Allow flakiness up to one second + $this->assertLessThanOrEqual( 1, + abs( wfTimestamp( TS_UNIX, $expiry ) - ( time() + 86400 ) ) ); + } + + public function testBlockWithInvalidExpiry() { + $this->setExpectedException( ApiUsageException::class, "Expiry time invalid." ); + + $this->doBlock( [ 'expiry' => '' ] ); } /** @@ -109,7 +227,7 @@ class ApiBlockTest extends ApiTestCase { $this->doApiRequest( [ 'action' => 'block', - 'user' => 'UTApiBlockee', + 'user' => $this->mUser->getName(), 'reason' => 'Some reason', ], null, @@ -117,4 +235,18 @@ class ApiBlockTest extends ApiTestCase { self::$users['sysop']->getUser() ); } + + public function testRangeBlock() { + $this->mUser = User::newFromName( '128.0.0.0/16', false ); + $this->doBlock(); + } + + /** + * @expectedException ApiUsageException + * @expectedExceptionMessage Range blocks larger than /16 are not allowed. + */ + public function testVeryLargeRangeBlock() { + $this->mUser = User::newFromName( '128.0.0.0/1', false ); + $this->doBlock(); + } } diff --git a/www/wiki/tests/phpunit/includes/api/ApiCheckTokenTest.php b/www/wiki/tests/phpunit/includes/api/ApiCheckTokenTest.php new file mode 100644 index 00000000..f1d95d03 --- /dev/null +++ b/www/wiki/tests/phpunit/includes/api/ApiCheckTokenTest.php @@ -0,0 +1,95 @@ +<?php + +use MediaWiki\Session\Token; + +/** + * @group API + * @group medium + * @covers ApiCheckToken + */ +class ApiCheckTokenTest extends ApiTestCase { + + /** + * Test result of checking previously queried token (should be valid) + */ + public function testCheckTokenValid() { + // Query token which will be checked later + $tokens = $this->doApiRequest( [ + 'action' => 'query', + 'meta' => 'tokens', + ] ); + + $data = $this->doApiRequest( [ + 'action' => 'checktoken', + 'type' => 'csrf', + 'token' => $tokens[0]['query']['tokens']['csrftoken'], + ], $tokens[1]->getSessionArray() ); + + $this->assertEquals( 'valid', $data[0]['checktoken']['result'] ); + $this->assertArrayHasKey( 'generated', $data[0]['checktoken'] ); + } + + /** + * Test result of checking invalid token + */ + public function testCheckTokenInvalid() { + $session = []; + $data = $this->doApiRequest( [ + 'action' => 'checktoken', + 'type' => 'csrf', + 'token' => 'invalid_token', + ], $session ); + + $this->assertEquals( 'invalid', $data[0]['checktoken']['result'] ); + } + + /** + * Test result of checking token with negative max age (should be expired) + */ + public function testCheckTokenExpired() { + // Query token which will be checked later + $tokens = $this->doApiRequest( [ + 'action' => 'query', + 'meta' => 'tokens', + ] ); + + $data = $this->doApiRequest( [ + 'action' => 'checktoken', + 'type' => 'csrf', + 'token' => $tokens[0]['query']['tokens']['csrftoken'], + 'maxtokenage' => -1, + ], $tokens[1]->getSessionArray() ); + + $this->assertEquals( 'expired', $data[0]['checktoken']['result'] ); + $this->assertArrayHasKey( 'generated', $data[0]['checktoken'] ); + } + + /** + * Test if using token with incorrect suffix will produce a warning + */ + public function testCheckTokenSuffixWarning() { + // Query token which will be checked later + $tokens = $this->doApiRequest( [ + 'action' => 'query', + 'meta' => 'tokens', + ] ); + + // Get token and change the suffix + $token = $tokens[0]['query']['tokens']['csrftoken']; + $token = substr( $token, 0, -strlen( Token::SUFFIX ) ) . urldecode( Token::SUFFIX ); + + $data = $this->doApiRequest( [ + 'action' => 'checktoken', + 'type' => 'csrf', + 'token' => $token, + 'errorformat' => 'raw', + ], $tokens[1]->getSessionArray() ); + + $this->assertEquals( 'invalid', $data[0]['checktoken']['result'] ); + $this->assertArrayHasKey( 'warnings', $data[0] ); + $this->assertCount( 1, $data[0]['warnings'] ); + $this->assertEquals( 'checktoken', $data[0]['warnings'][0]['module'] ); + $this->assertEquals( 'checktoken-percentencoding', $data[0]['warnings'][0]['code'] ); + } + +} diff --git a/www/wiki/tests/phpunit/includes/api/ApiClearHasMsgTest.php b/www/wiki/tests/phpunit/includes/api/ApiClearHasMsgTest.php new file mode 100644 index 00000000..5b124074 --- /dev/null +++ b/www/wiki/tests/phpunit/includes/api/ApiClearHasMsgTest.php @@ -0,0 +1,24 @@ +<?php + +/** + * @group API + * @group medium + * @covers ApiClearHasMsg + */ +class ApiClearHasMsgTest extends ApiTestCase { + + /** + * Test clearing hasmsg flag for current user + */ + public function testClearFlag() { + $user = self::$users['sysop']->getUser(); + $user->setNewtalk( true ); + $this->assertTrue( $user->getNewtalk(), 'sanity check' ); + + $data = $this->doApiRequest( [ 'action' => 'clearhasmsg' ], [] ); + + $this->assertEquals( 'success', $data[0]['clearhasmsg'] ); + $this->assertFalse( $user->getNewtalk() ); + } + +} diff --git a/www/wiki/tests/phpunit/includes/api/ApiComparePagesTest.php b/www/wiki/tests/phpunit/includes/api/ApiComparePagesTest.php index 989d6bb5..ea13a0d3 100644 --- a/www/wiki/tests/phpunit/includes/api/ApiComparePagesTest.php +++ b/www/wiki/tests/phpunit/includes/api/ApiComparePagesTest.php @@ -29,7 +29,7 @@ class ApiComparePagesTest extends ApiTestCase { $status = $page->doEditContent( $content, 'Test for ApiComparePagesTest: ' . $text, 0, false, $user ); - if ( !$status->isOk() ) { + if ( !$status->isOK() ) { $this->fail( "Failed to create $title: " . $status->getWikiText( false, false, 'en' ) ); } return $status->value['revision']->getId(); @@ -70,6 +70,9 @@ class ApiComparePagesTest extends ApiTestCase { 'page', [ 'page_latest' => 0 ], [ 'page_id' => self::$repl['pageE'] ] ); + self::$repl['revF1'] = $this->addPage( 'F', "== Section 1 ==\nF 1.1\n\n== Section 2 ==\nF 1.2" ); + self::$repl['pageF'] = Title::newFromText( 'ApiComparePagesTest F' )->getArticleId(); + WikiPage::factory( Title::newFromText( 'ApiComparePagesTest C' ) ) ->doDeleteArticleReal( 'Test for ApiComparePagesTest' ); @@ -151,8 +154,8 @@ class ApiComparePagesTest extends ApiTestCase { } public static function provideDiff() { + // phpcs:disable Generic.Files.LineLength.TooLong return [ - // @codingStandardsIgnoreStart Ignore Generic.Files.LineLength.TooLong 'Basic diff, titles' => [ [ 'fromtitle' => 'ApiComparePagesTest A', @@ -372,6 +375,26 @@ class ApiComparePagesTest extends ApiTestCase { ], false, true ], + 'Basic diff, test with sections' => [ + [ + 'fromtitle' => 'ApiComparePagesTest F', + 'fromsection' => 1, + 'totext' => "== Section 1 ==\nTo text\n\n== Section 2 ==\nTo text?", + 'tosection' => 2, + ], + [ + 'compare' => [ + 'body' => '<tr><td colspan="2" class="diff-lineno" id="mw-diff-left-l1" >Line 1:</td>' . "\n" + . '<td colspan="2" class="diff-lineno">Line 1:</td></tr>' . "\n" + . '<tr><td class=\'diff-marker\'>−</td><td class=\'diff-deletedline\'><div>== Section <del class="diffchange diffchange-inline">1 </del>==</div></td><td class=\'diff-marker\'>+</td><td class=\'diff-addedline\'><div>== Section <ins class="diffchange diffchange-inline">2 </ins>==</div></td></tr>' . "\n" + . '<tr><td class=\'diff-marker\'>−</td><td class=\'diff-deletedline\'><div><del class="diffchange diffchange-inline">F 1.1</del></div></td><td class=\'diff-marker\'>+</td><td class=\'diff-addedline\'><div><ins class="diffchange diffchange-inline">To text?</ins></div></td></tr>' . "\n", + 'fromid' => '{{REPL:pageF}}', + 'fromrevid' => '{{REPL:revF1}}', + 'fromns' => '0', + 'fromtitle' => 'ApiComparePagesTest F', + ] + ], + ], 'Diff with all props' => [ [ 'fromrev' => '{{REPL:revB1}}', @@ -568,6 +591,26 @@ class ApiComparePagesTest extends ApiTestCase { [], 'compare-no-title', ], + 'Error, test with invalid from section ID' => [ + [ + 'fromtitle' => 'ApiComparePagesTest F', + 'fromsection' => 5, + 'totext' => "== Section 1 ==\nTo text\n\n== Section 2 ==\nTo text?", + 'tosection' => 2, + ], + [], + 'nosuchfromsection', + ], + 'Error, test with invalid to section ID' => [ + [ + 'fromtitle' => 'ApiComparePagesTest F', + 'fromsection' => 1, + 'totext' => "== Section 1 ==\nTo text\n\n== Section 2 ==\nTo text?", + 'tosection' => 5, + ], + [], + 'nosuchtosection', + ], 'Error, Relative diff, no from revision' => [ [ 'fromtext' => 'Foo', @@ -604,8 +647,7 @@ class ApiComparePagesTest extends ApiTestCase { [], 'missingcontent' ], - - // @codingStandardsIgnoreEnd ]; + // phpcs:enable } } diff --git a/www/wiki/tests/phpunit/includes/api/ApiContinuationManagerTest.php b/www/wiki/tests/phpunit/includes/api/ApiContinuationManagerTest.php index bb4ea758..788d120c 100644 --- a/www/wiki/tests/phpunit/includes/api/ApiContinuationManagerTest.php +++ b/www/wiki/tests/phpunit/includes/api/ApiContinuationManagerTest.php @@ -22,7 +22,7 @@ class ApiContinuationManagerTest extends MediaWikiTestCase { $generator = new MockApiQueryBase( 'generator' ); $manager = self::getManager( '', $allModules, [ 'mock1', 'mock2' ] ); - $this->assertSame( 'ApiMain', $manager->getSource() ); + $this->assertSame( ApiMain::class, $manager->getSource() ); $this->assertSame( false, $manager->isGeneratorDone() ); $this->assertSame( $allModules, $manager->getRunModules() ); $manager->addContinueParam( $allModules[0], 'm1continue', [ 1, 2 ] ); diff --git a/www/wiki/tests/phpunit/includes/api/ApiDeleteTest.php b/www/wiki/tests/phpunit/includes/api/ApiDeleteTest.php new file mode 100644 index 00000000..0f2bcc61 --- /dev/null +++ b/www/wiki/tests/phpunit/includes/api/ApiDeleteTest.php @@ -0,0 +1,168 @@ +<?php + +/** + * Tests for MediaWiki api.php?action=delete. + * + * @author Yifei He + * + * @group API + * @group Database + * @group medium + * + * @covers ApiDelete + */ +class ApiDeleteTest extends ApiTestCase { + public function testDelete() { + $name = 'Help:' . ucfirst( __FUNCTION__ ); + + // create new page + $this->editPage( $name, 'Some text' ); + + // test deletion + $apiResult = $this->doApiRequestWithToken( [ + 'action' => 'delete', + 'title' => $name, + ] )[0]; + + $this->assertArrayHasKey( 'delete', $apiResult ); + $this->assertArrayHasKey( 'title', $apiResult['delete'] ); + $this->assertSame( $name, $apiResult['delete']['title'] ); + $this->assertArrayHasKey( 'logid', $apiResult['delete'] ); + + $this->assertFalse( Title::newFromText( $name )->exists() ); + } + + public function testDeleteNonexistent() { + $this->setExpectedException( ApiUsageException::class, + "The page you specified doesn't exist." ); + + $this->doApiRequestWithToken( [ + 'action' => 'delete', + 'title' => 'This page deliberately left nonexistent', + ] ); + } + + public function testDeletionWithoutPermission() { + $this->setExpectedException( ApiUsageException::class, + 'The action you have requested is limited to users in the group:' ); + + $name = 'Help:' . ucfirst( __FUNCTION__ ); + + // create new page + $this->editPage( $name, 'Some text' ); + + // test deletion without permission + try { + $user = new User(); + $apiResult = $this->doApiRequest( [ + 'action' => 'delete', + 'title' => $name, + 'token' => $user->getEditToken(), + ], null, null, $user ); + } finally { + $this->assertTrue( Title::newFromText( $name )->exists() ); + } + } + + public function testDeleteWithTag() { + $name = 'Help:' . ucfirst( __FUNCTION__ ); + + ChangeTags::defineTag( 'custom tag' ); + + $this->editPage( $name, 'Some text' ); + + $this->doApiRequestWithToken( [ + 'action' => 'delete', + 'title' => $name, + 'tags' => 'custom tag', + ] ); + + $this->assertFalse( Title::newFromText( $name )->exists() ); + + $dbw = wfGetDB( DB_MASTER ); + $this->assertSame( 'custom tag', $dbw->selectField( + [ 'change_tag', 'logging' ], + 'ct_tag', + [ + 'log_namespace' => NS_HELP, + 'log_title' => ucfirst( __FUNCTION__ ), + ], + __METHOD__, + [], + [ 'change_tag' => [ 'INNER JOIN', 'ct_log_id = log_id' ] ] + ) ); + } + + public function testDeleteWithoutTagPermission() { + $this->setExpectedException( ApiUsageException::class, + 'You do not have permission to apply change tags along with your changes.' ); + + $name = 'Help:' . ucfirst( __FUNCTION__ ); + + ChangeTags::defineTag( 'custom tag' ); + $this->setMwGlobals( 'wgRevokePermissions', + [ 'user' => [ 'applychangetags' => true ] ] ); + + $this->editPage( $name, 'Some text' ); + + try { + $this->doApiRequestWithToken( [ + 'action' => 'delete', + 'title' => $name, + 'tags' => 'custom tag', + ] ); + } finally { + $this->assertTrue( Title::newFromText( $name )->exists() ); + } + } + + public function testDeleteAbortedByHook() { + $this->setExpectedException( ApiUsageException::class, + 'Deletion aborted by hook. It gave no explanation.' ); + + $name = 'Help:' . ucfirst( __FUNCTION__ ); + + $this->editPage( $name, 'Some text' ); + + $this->setTemporaryHook( 'ArticleDelete', + function () { + return false; + } + ); + + try { + $this->doApiRequestWithToken( [ 'action' => 'delete', 'title' => $name ] ); + } finally { + $this->assertTrue( Title::newFromText( $name )->exists() ); + } + } + + public function testDeleteWatch() { + $name = 'Help:' . ucfirst( __FUNCTION__ ); + $user = self::$users['sysop']->getUser(); + + $this->editPage( $name, 'Some text' ); + $this->assertTrue( Title::newFromText( $name )->exists() ); + $this->assertFalse( $user->isWatched( Title::newFromText( $name ) ) ); + + $this->doApiRequestWithToken( [ 'action' => 'delete', 'title' => $name, 'watch' => '' ] ); + + $this->assertFalse( Title::newFromText( $name )->exists() ); + $this->assertTrue( $user->isWatched( Title::newFromText( $name ) ) ); + } + + public function testDeleteUnwatch() { + $name = 'Help:' . ucfirst( __FUNCTION__ ); + $user = self::$users['sysop']->getUser(); + + $this->editPage( $name, 'Some text' ); + $this->assertTrue( Title::newFromText( $name )->exists() ); + $user->addWatch( Title::newFromText( $name ) ); + $this->assertTrue( $user->isWatched( Title::newFromText( $name ) ) ); + + $this->doApiRequestWithToken( [ 'action' => 'delete', 'title' => $name, 'unwatch' => '' ] ); + + $this->assertFalse( Title::newFromText( $name )->exists() ); + $this->assertFalse( $user->isWatched( Title::newFromText( $name ) ) ); + } +} diff --git a/www/wiki/tests/phpunit/includes/api/ApiDisabledTest.php b/www/wiki/tests/phpunit/includes/api/ApiDisabledTest.php new file mode 100644 index 00000000..cfdd57b8 --- /dev/null +++ b/www/wiki/tests/phpunit/includes/api/ApiDisabledTest.php @@ -0,0 +1,19 @@ +<?php + +/** + * @group API + * @group medium + * + * @covers ApiDisabled + */ +class ApiDisabledTest extends ApiTestCase { + public function testDisabled() { + $this->mergeMwGlobalArrayValue( 'wgAPIModules', + [ 'login' => 'ApiDisabled' ] ); + + $this->setExpectedException( ApiUsageException::class, + 'The "login" module has been disabled.' ); + + $this->doApiRequest( [ 'action' => 'login' ] ); + } +} diff --git a/www/wiki/tests/phpunit/includes/api/ApiEditPageTest.php b/www/wiki/tests/phpunit/includes/api/ApiEditPageTest.php index e0911532..c1963389 100644 --- a/www/wiki/tests/phpunit/includes/api/ApiEditPageTest.php +++ b/www/wiki/tests/phpunit/includes/api/ApiEditPageTest.php @@ -35,15 +35,19 @@ class ApiEditPageTest extends ApiTestCase { $wgContentHandlers["testing"] = 'DummyContentHandlerForTesting'; $wgContentHandlers["testing-nontext"] = 'DummyNonTextContentHandler'; + $wgContentHandlers["testing-serialize-error"] = + 'DummySerializeErrorContentHandler'; - MWNamespace::getCanonicalNamespaces( true ); # reset namespace cache + MWNamespace::clearCaches(); $wgContLang->resetNamespaces(); # reset namespace cache - - $this->doLogin(); } protected function tearDown() { - MWNamespace::getCanonicalNamespaces( true ); # reset namespace cache + global $wgContLang; + + MWNamespace::clearCaches(); + $wgContLang->resetNamespaces(); # reset namespace cache + parent::tearDown(); } @@ -61,7 +65,7 @@ class ApiEditPageTest extends ApiTestCase { // Validate API result data $this->assertArrayHasKey( 'edit', $apiResult ); $this->assertArrayHasKey( 'result', $apiResult['edit'] ); - $this->assertEquals( 'Success', $apiResult['edit']['result'] ); + $this->assertSame( 'Success', $apiResult['edit']['result'] ); $this->assertArrayHasKey( 'new', $apiResult['edit'] ); $this->assertArrayNotHasKey( 'nochange', $apiResult['edit'] ); @@ -75,7 +79,7 @@ class ApiEditPageTest extends ApiTestCase { 'text' => 'some text', ] ); - $this->assertEquals( 'Success', $data[0]['edit']['result'] ); + $this->assertSame( 'Success', $data[0]['edit']['result'] ); $this->assertArrayNotHasKey( 'new', $data[0]['edit'] ); $this->assertArrayHasKey( 'nochange', $data[0]['edit'] ); @@ -87,7 +91,7 @@ class ApiEditPageTest extends ApiTestCase { 'text' => 'different text' ] ); - $this->assertEquals( 'Success', $data[0]['edit']['result'] ); + $this->assertSame( 'Success', $data[0]['edit']['result'] ); $this->assertArrayNotHasKey( 'new', $data[0]['edit'] ); $this->assertArrayNotHasKey( 'nochange', $data[0]['edit'] ); @@ -144,7 +148,7 @@ class ApiEditPageTest extends ApiTestCase { 'title' => $name, 'text' => $text, ] ); - $this->assertEquals( 'Success', $re['edit']['result'] ); // sanity + $this->assertSame( 'Success', $re['edit']['result'] ); // sanity } // -- try append/prepend -------------------------------------------- @@ -153,7 +157,7 @@ class ApiEditPageTest extends ApiTestCase { 'title' => $name, $op . 'text' => $append, ] ); - $this->assertEquals( 'Success', $re['edit']['result'] ); + $this->assertSame( 'Success', $re['edit']['result'] ); // -- validate ----------------------------------------------------- $page = new WikiPage( Title::newFromText( $name ) ); @@ -162,7 +166,7 @@ class ApiEditPageTest extends ApiTestCase { $text = $content->getNativeData(); - $this->assertEquals( $expected, $text ); + $this->assertSame( $expected, $text ); } /** @@ -181,11 +185,11 @@ class ApiEditPageTest extends ApiTestCase { 'section' => '1', 'text' => "==section 1==\nnew content 1", ] ); - $this->assertEquals( 'Success', $re['edit']['result'] ); + $this->assertSame( 'Success', $re['edit']['result'] ); $newtext = WikiPage::factory( Title::newFromText( $name ) ) ->getContent( Revision::RAW ) ->getNativeData(); - $this->assertEquals( "==section 1==\nnew content 1\n\n==section 2==\ncontent2", $newtext ); + $this->assertSame( "==section 1==\nnew content 1\n\n==section 2==\ncontent2", $newtext ); // Test that we raise a 'nosuchsection' error try { @@ -220,12 +224,12 @@ class ApiEditPageTest extends ApiTestCase { 'summary' => 'header', ] ); - $this->assertEquals( 'Success', $re['edit']['result'] ); + $this->assertSame( 'Success', $re['edit']['result'] ); // Check the page text is correct $text = WikiPage::factory( Title::newFromText( $name ) ) ->getContent( Revision::RAW ) ->getNativeData(); - $this->assertEquals( "== header ==\n\ntest", $text ); + $this->assertSame( "== header ==\n\ntest", $text ); // Now on one that does $this->assertTrue( Title::newFromText( $name )->exists() ); @@ -237,11 +241,11 @@ class ApiEditPageTest extends ApiTestCase { 'summary' => 'header', ] ); - $this->assertEquals( 'Success', $re2['edit']['result'] ); + $this->assertSame( 'Success', $re2['edit']['result'] ); $text = WikiPage::factory( Title::newFromText( $name ) ) ->getContent( Revision::RAW ) ->getNativeData(); - $this->assertEquals( "== header ==\n\ntest\n\n== header ==\n\ntest", $text ); + $this->assertSame( "== header ==\n\ntest\n\n== header ==\n\ntest", $text ); } /** @@ -284,9 +288,9 @@ class ApiEditPageTest extends ApiTestCase { 'basetimestamp' => $baseTime, 'section' => 'new', 'redirect' => true, - ], null, self::$users['sysop']->getUser() ); + ] ); - $this->assertEquals( 'Success', $re['edit']['result'], + $this->assertSame( 'Success', $re['edit']['result'], "no problems expected when following redirect" ); } @@ -330,7 +334,7 @@ class ApiEditPageTest extends ApiTestCase { 'text' => 'nix bar!', 'basetimestamp' => $baseTime, 'redirect' => true, - ], null, self::$users['sysop']->getUser() ); + ] ); $this->fail( 'redirect-appendonly error expected' ); } catch ( ApiUsageException $ex ) { @@ -366,7 +370,7 @@ class ApiEditPageTest extends ApiTestCase { 'title' => $name, 'text' => 'nix bar!', 'basetimestamp' => $baseTime, - ], null, self::$users['sysop']->getUser() ); + ] ); $this->fail( 'edit conflict expected' ); } catch ( ApiUsageException $ex ) { @@ -405,9 +409,9 @@ class ApiEditPageTest extends ApiTestCase { 'text' => 'nix bar!', 'basetimestamp' => $baseTime, 'section' => 'new', - ], null, self::$users['sysop']->getUser() ); + ] ); - $this->assertEquals( 'Success', $re['edit']['result'], + $this->assertSame( 'Success', $re['edit']['result'], "no edit conflict expected here" ); } @@ -452,9 +456,9 @@ class ApiEditPageTest extends ApiTestCase { 'text' => 'nix bar!', 'section' => 'new', 'redirect' => true, - ], null, self::$users['sysop']->getUser() ); + ] ); - $this->assertEquals( 'Success', $re['edit']['result'], + $this->assertSame( 'Success', $re['edit']['result'], "no edit conflict expected here" ); } @@ -474,7 +478,7 @@ class ApiEditPageTest extends ApiTestCase { public function testCheckDirectApiEditingDisallowed_forNonTextContent() { $this->setExpectedException( - 'ApiUsageException', + ApiUsageException::class, 'Direct editing via API is not supported for content model ' . 'testing used by Dummy:ApiEditPageTest_nonTextPageEdit' ); @@ -501,7 +505,7 @@ class ApiEditPageTest extends ApiTestCase { // Validate API result data $this->assertArrayHasKey( 'edit', $apiResult ); $this->assertArrayHasKey( 'result', $apiResult['edit'] ); - $this->assertEquals( 'Success', $apiResult['edit']['result'] ); + $this->assertSame( 'Success', $apiResult['edit']['result'] ); $this->assertArrayHasKey( 'new', $apiResult['edit'] ); $this->assertArrayNotHasKey( 'nochange', $apiResult['edit'] ); @@ -510,8 +514,8 @@ class ApiEditPageTest extends ApiTestCase { // validate resulting revision $page = WikiPage::factory( Title::newFromText( $name ) ); - $this->assertEquals( "testing-nontext", $page->getContentModel() ); - $this->assertEquals( $data, $page->getContent()->serialize() ); + $this->assertSame( "testing-nontext", $page->getContentModel() ); + $this->assertSame( $data, $page->getContent()->serialize() ); } /** @@ -523,6 +527,7 @@ class ApiEditPageTest extends ApiTestCase { $name = 'Help:' . __FUNCTION__; $uploader = self::$users['uploader']->getUser(); $sysop = self::$users['sysop']->getUser(); + $apiResult = $this->doApiRequestWithToken( [ 'action' => 'edit', 'title' => $name, @@ -532,10 +537,10 @@ class ApiEditPageTest extends ApiTestCase { // Check success $this->assertArrayHasKey( 'edit', $apiResult ); $this->assertArrayHasKey( 'result', $apiResult['edit'] ); - $this->assertEquals( 'Success', $apiResult['edit']['result'] ); + $this->assertSame( 'Success', $apiResult['edit']['result'] ); $this->assertArrayHasKey( 'contentmodel', $apiResult['edit'] ); // Content model is wikitext - $this->assertEquals( 'wikitext', $apiResult['edit']['contentmodel'] ); + $this->assertSame( 'wikitext', $apiResult['edit']['contentmodel'] ); // Convert the page to JSON $apiResult = $this->doApiRequestWithToken( [ @@ -548,9 +553,9 @@ class ApiEditPageTest extends ApiTestCase { // Check success $this->assertArrayHasKey( 'edit', $apiResult ); $this->assertArrayHasKey( 'result', $apiResult['edit'] ); - $this->assertEquals( 'Success', $apiResult['edit']['result'] ); + $this->assertSame( 'Success', $apiResult['edit']['result'] ); $this->assertArrayHasKey( 'contentmodel', $apiResult['edit'] ); - $this->assertEquals( 'json', $apiResult['edit']['contentmodel'] ); + $this->assertSame( 'json', $apiResult['edit']['contentmodel'] ); $apiResult = $this->doApiRequestWithToken( [ 'action' => 'edit', @@ -561,9 +566,1039 @@ class ApiEditPageTest extends ApiTestCase { // Check success $this->assertArrayHasKey( 'edit', $apiResult ); $this->assertArrayHasKey( 'result', $apiResult['edit'] ); - $this->assertEquals( 'Success', $apiResult['edit']['result'] ); + $this->assertSame( 'Success', $apiResult['edit']['result'] ); $this->assertArrayHasKey( 'contentmodel', $apiResult['edit'] ); // Check that the contentmodel is back to wikitext now. - $this->assertEquals( 'wikitext', $apiResult['edit']['contentmodel'] ); + $this->assertSame( 'wikitext', $apiResult['edit']['contentmodel'] ); + } + + // The tests below are mostly not commented because they do exactly what + // you'd expect from the name. + + public function testCorrectContentFormat() { + $name = 'Help:' . ucfirst( __FUNCTION__ ); + + $this->doApiRequestWithToken( [ + 'action' => 'edit', + 'title' => $name, + 'text' => 'some text', + 'contentmodel' => 'wikitext', + 'contentformat' => 'text/x-wiki', + ] ); + + $this->assertTrue( Title::newFromText( $name )->exists() ); + } + + public function testUnsupportedContentFormat() { + $name = 'Help:' . ucfirst( __FUNCTION__ ); + + $this->setExpectedException( ApiUsageException::class, + 'Unrecognized value for parameter "contentformat": nonexistent format.' ); + + try { + $this->doApiRequestWithToken( [ + 'action' => 'edit', + 'title' => $name, + 'text' => 'some text', + 'contentformat' => 'nonexistent format', + ] ); + } finally { + $this->assertFalse( Title::newFromText( $name )->exists() ); + } + } + + public function testMismatchedContentFormat() { + $name = 'Help:' . ucfirst( __FUNCTION__ ); + + $this->setExpectedException( ApiUsageException::class, + 'The requested format text/plain is not supported for content ' . + "model wikitext used by $name." ); + + try { + $this->doApiRequestWithToken( [ + 'action' => 'edit', + 'title' => $name, + 'text' => 'some text', + 'contentmodel' => 'wikitext', + 'contentformat' => 'text/plain', + ] ); + } finally { + $this->assertFalse( Title::newFromText( $name )->exists() ); + } + } + + public function testUndoToInvalidRev() { + $name = 'Help:' . ucfirst( __FUNCTION__ ); + + $revId = $this->editPage( $name, 'Some text' )->value['revision'] + ->getId(); + $revId++; + + $this->setExpectedException( ApiUsageException::class, + "There is no revision with ID $revId." ); + + $this->doApiRequestWithToken( [ + 'action' => 'edit', + 'title' => $name, + 'undo' => $revId, + ] ); + } + + /** + * Tests what happens if the undo parameter is a valid revision, but + * the undoafter parameter doesn't refer to a revision that exists in the + * database. + */ + public function testUndoAfterToInvalidRev() { + // We can't just pick a large number for undoafter (as in + // testUndoToInvalidRev above), because then MediaWiki will helpfully + // assume we switched around undo and undoafter and we'll test the code + // path for undo being invalid, not undoafter. So instead we delete + // the revision from the database. In real life this case could come + // up if a revision number was skipped, e.g., if two transactions try + // to insert new revision rows at once and the first one to succeed + // gets rolled back. + $name = 'Help:' . ucfirst( __FUNCTION__ ); + $titleObj = Title::newFromText( $name ); + + $revId1 = $this->editPage( $name, '1' )->value['revision']->getId(); + $revId2 = $this->editPage( $name, '2' )->value['revision']->getId(); + $revId3 = $this->editPage( $name, '3' )->value['revision']->getId(); + + // Make the middle revision disappear + $dbw = wfGetDB( DB_MASTER ); + $dbw->delete( 'revision', [ 'rev_id' => $revId2 ], __METHOD__ ); + $dbw->update( 'revision', [ 'rev_parent_id' => $revId1 ], + [ 'rev_id' => $revId3 ], __METHOD__ ); + + $this->setExpectedException( ApiUsageException::class, + "There is no revision with ID $revId2." ); + + $this->doApiRequestWithToken( [ + 'action' => 'edit', + 'title' => $name, + 'undo' => $revId3, + 'undoafter' => $revId2, + ] ); + } + + /** + * Tests what happens if the undo parameter is a valid revision, but + * undoafter is hidden (rev_deleted). + */ + public function testUndoAfterToHiddenRev() { + $name = 'Help:' . ucfirst( __FUNCTION__ ); + $titleObj = Title::newFromText( $name ); + + $this->editPage( $name, '0' ); + + $revId1 = $this->editPage( $name, '1' )->value['revision']->getId(); + + $revId2 = $this->editPage( $name, '2' )->value['revision']->getId(); + + // Hide the middle revision + $list = RevisionDeleter::createList( 'revision', + RequestContext::getMain(), $titleObj, [ $revId1 ] ); + $list->setVisibility( [ + 'value' => [ Revision::DELETED_TEXT => 1 ], + 'comment' => 'Bye-bye', + ] ); + + $this->setExpectedException( ApiUsageException::class, + "There is no revision with ID $revId1." ); + + $this->doApiRequestWithToken( [ + 'action' => 'edit', + 'title' => $name, + 'undo' => $revId2, + 'undoafter' => $revId1, + ] ); + } + + /** + * Test undo when a revision with a higher id has an earlier timestamp. + * This can happen if importing an old revision. + */ + public function testUndoWithSwappedRevisions() { + $name = 'Help:' . ucfirst( __FUNCTION__ ); + $titleObj = Title::newFromText( $name ); + + $this->editPage( $name, '0' ); + + $revId2 = $this->editPage( $name, '2' )->value['revision']->getId(); + + $revId1 = $this->editPage( $name, '1' )->value['revision']->getId(); + + // Now monkey with the timestamp + $dbw = wfGetDB( DB_MASTER ); + $dbw->update( + 'revision', + [ 'rev_timestamp' => wfTimestamp( TS_MW, time() - 86400 ) ], + [ 'rev_id' => $revId1 ], + __METHOD__ + ); + + $this->doApiRequestWithToken( [ + 'action' => 'edit', + 'title' => $name, + 'undo' => $revId2, + 'undoafter' => $revId1, + ] ); + + $text = ( new WikiPage( $titleObj ) )->getContent()->getNativeData(); + + // This is wrong! It should be 1. But let's test for our incorrect + // behavior for now, so if someone fixes it they'll fix the test as + // well to expect 1. If we disabled the test, it might stay disabled + // even once the bug is fixed, which would be a shame. + $this->assertSame( '2', $text ); + } + + public function testUndoWithConflicts() { + $name = 'Help:' . ucfirst( __FUNCTION__ ); + + $this->setExpectedException( ApiUsageException::class, + 'The edit could not be undone due to conflicting intermediate edits.' ); + + $this->editPage( $name, '1' ); + + $revId = $this->editPage( $name, '2' )->value['revision']->getId(); + + $this->editPage( $name, '3' ); + + $this->doApiRequestWithToken( [ + 'action' => 'edit', + 'title' => $name, + 'undo' => $revId, + ] ); + + $text = ( new WikiPage( Title::newFromText( $name ) ) )->getContent() + ->getNativeData(); + $this->assertSame( '3', $text ); + } + + /** + * undoafter is supposed to be less than undo. If not, we reverse their + * meaning, so that the two are effectively interchangeable. + */ + public function testReversedUndoAfter() { + $name = 'Help:' . ucfirst( __FUNCTION__ ); + + $this->editPage( $name, '0' ); + $revId1 = $this->editPage( $name, '1' )->value['revision']->getId(); + $revId2 = $this->editPage( $name, '2' )->value['revision']->getId(); + + $this->doApiRequestWithToken( [ + 'action' => 'edit', + 'title' => $name, + 'undo' => $revId1, + 'undoafter' => $revId2, + ] ); + + $text = ( new WikiPage( Title::newFromText( $name ) ) )->getContent() + ->getNativeData(); + $this->assertSame( '1', $text ); + } + + public function testUndoToRevFromDifferentPage() { + $name = 'Help:' . ucfirst( __FUNCTION__ ); + + $this->editPage( "$name-1", 'Some text' ); + $revId = $this->editPage( "$name-1", 'Some more text' ) + ->value['revision']->getId(); + + $this->editPage( "$name-2", 'Some text' ); + + $this->setExpectedException( ApiUsageException::class, + "r$revId is not a revision of $name-2." ); + + $this->doApiRequestWithToken( [ + 'action' => 'edit', + 'title' => "$name-2", + 'undo' => $revId, + ] ); + } + + public function testUndoAfterToRevFromDifferentPage() { + $name = 'Help:' . ucfirst( __FUNCTION__ ); + + $revId1 = $this->editPage( "$name-1", 'Some text' ) + ->value['revision']->getId(); + + $revId2 = $this->editPage( "$name-2", 'Some text' ) + ->value['revision']->getId(); + + $this->setExpectedException( ApiUsageException::class, + "r$revId1 is not a revision of $name-2." ); + + $this->doApiRequestWithToken( [ + 'action' => 'edit', + 'title' => "$name-2", + 'undo' => $revId2, + 'undoafter' => $revId1, + ] ); + } + + public function testMd5Text() { + $name = 'Help:' . ucfirst( __FUNCTION__ ); + + $this->assertFalse( Title::newFromText( $name )->exists() ); + + $this->doApiRequestWithToken( [ + 'action' => 'edit', + 'title' => $name, + 'text' => 'Some text', + 'md5' => md5( 'Some text' ), + ] ); + + $this->assertTrue( Title::newFromText( $name )->exists() ); + } + + public function testMd5PrependText() { + $name = 'Help:' . ucfirst( __FUNCTION__ ); + + $this->editPage( $name, 'Some text' ); + + $this->doApiRequestWithToken( [ + 'action' => 'edit', + 'title' => $name, + 'prependtext' => 'Alert: ', + 'md5' => md5( 'Alert: ' ), + ] ); + + $text = ( new WikiPage( Title::newFromText( $name ) ) ) + ->getContent()->getNativeData(); + $this->assertSame( 'Alert: Some text', $text ); + } + + public function testMd5AppendText() { + $name = 'Help:' . ucfirst( __FUNCTION__ ); + + $this->editPage( $name, 'Some text' ); + + $this->doApiRequestWithToken( [ + 'action' => 'edit', + 'title' => $name, + 'appendtext' => ' is nice', + 'md5' => md5( ' is nice' ), + ] ); + + $text = ( new WikiPage( Title::newFromText( $name ) ) ) + ->getContent()->getNativeData(); + $this->assertSame( 'Some text is nice', $text ); + } + + public function testMd5PrependAndAppendText() { + $name = 'Help:' . ucfirst( __FUNCTION__ ); + + $this->editPage( $name, 'Some text' ); + + $this->doApiRequestWithToken( [ + 'action' => 'edit', + 'title' => $name, + 'prependtext' => 'Alert: ', + 'appendtext' => ' is nice', + 'md5' => md5( 'Alert: is nice' ), + ] ); + + $text = ( new WikiPage( Title::newFromText( $name ) ) ) + ->getContent()->getNativeData(); + $this->assertSame( 'Alert: Some text is nice', $text ); + } + + public function testIncorrectMd5Text() { + $name = 'Help:' . ucfirst( __FUNCTION__ ); + + $this->setExpectedException( ApiUsageException::class, + 'The supplied MD5 hash was incorrect.' ); + + $this->doApiRequestWithToken( [ + 'action' => 'edit', + 'title' => $name, + 'text' => 'Some text', + 'md5' => md5( '' ), + ] ); + } + + public function testIncorrectMd5PrependText() { + $name = 'Help:' . ucfirst( __FUNCTION__ ); + + $this->setExpectedException( ApiUsageException::class, + 'The supplied MD5 hash was incorrect.' ); + + $this->doApiRequestWithToken( [ + 'action' => 'edit', + 'title' => $name, + 'prependtext' => 'Some ', + 'appendtext' => 'text', + 'md5' => md5( 'Some ' ), + ] ); + } + + public function testIncorrectMd5AppendText() { + $name = 'Help:' . ucfirst( __FUNCTION__ ); + + $this->setExpectedException( ApiUsageException::class, + 'The supplied MD5 hash was incorrect.' ); + + $this->doApiRequestWithToken( [ + 'action' => 'edit', + 'title' => $name, + 'prependtext' => 'Some ', + 'appendtext' => 'text', + 'md5' => md5( 'text' ), + ] ); + } + + public function testCreateOnly() { + $name = 'Help:' . ucfirst( __FUNCTION__ ); + + $this->setExpectedException( ApiUsageException::class, + 'The article you tried to create has been created already.' ); + + $this->editPage( $name, 'Some text' ); + $this->assertTrue( Title::newFromText( $name )->exists() ); + + try { + $this->doApiRequestWithToken( [ + 'action' => 'edit', + 'title' => $name, + 'text' => 'Some more text', + 'createonly' => '', + ] ); + } finally { + // Validate that content was not changed + $text = ( new WikiPage( Title::newFromText( $name ) ) ) + ->getContent()->getNativeData(); + + $this->assertSame( 'Some text', $text ); + } + } + + public function testNoCreate() { + $name = 'Help:' . ucfirst( __FUNCTION__ ); + + $this->setExpectedException( ApiUsageException::class, + "The page you specified doesn't exist." ); + + $this->assertFalse( Title::newFromText( $name )->exists() ); + + try { + $this->doApiRequestWithToken( [ + 'action' => 'edit', + 'title' => $name, + 'text' => 'Some text', + 'nocreate' => '', + ] ); + } finally { + $this->assertFalse( Title::newFromText( $name )->exists() ); + } + } + + /** + * Appending/prepending is currently only supported for TextContent. We + * test this right now, and when support is added this test should be + * replaced by tests that the support is correct. + */ + public function testAppendWithNonTextContentHandler() { + $name = 'MediaWiki:' . ucfirst( __FUNCTION__ ); + + $this->setExpectedException( ApiUsageException::class, + "Can't append to pages using content model testing-nontext." ); + + $this->setTemporaryHook( 'ContentHandlerDefaultModelFor', + function ( Title $title, &$model ) use ( $name ) { + if ( $title->getPrefixedText() === $name ) { + $model = 'testing-nontext'; + } + return true; + } + ); + + $this->doApiRequestWithToken( [ + 'action' => 'edit', + 'title' => $name, + 'appendtext' => 'Some text', + ] ); + } + + public function testAppendInMediaWikiNamespace() { + $name = 'MediaWiki:' . ucfirst( __FUNCTION__ ); + + $this->assertFalse( Title::newFromText( $name )->exists() ); + + $this->doApiRequestWithToken( [ + 'action' => 'edit', + 'title' => $name, + 'appendtext' => 'Some text', + ] ); + + $this->assertTrue( Title::newFromText( $name )->exists() ); + } + + public function testAppendInMediaWikiNamespaceWithSerializationError() { + $name = 'MediaWiki:' . ucfirst( __FUNCTION__ ); + + $this->setExpectedException( ApiUsageException::class, + 'Content serialization failed: Could not unserialize content' ); + + $this->setTemporaryHook( 'ContentHandlerDefaultModelFor', + function ( Title $title, &$model ) use ( $name ) { + if ( $title->getPrefixedText() === $name ) { + $model = 'testing-serialize-error'; + } + return true; + } + ); + + $this->doApiRequestWithToken( [ + 'action' => 'edit', + 'title' => $name, + 'appendtext' => 'Some text', + ] ); + } + + public function testAppendNewSection() { + $name = 'Help:' . ucfirst( __FUNCTION__ ); + + $this->editPage( $name, 'Initial content' ); + + $this->doApiRequestWithToken( [ + 'action' => 'edit', + 'title' => $name, + 'appendtext' => '== New section ==', + 'section' => 'new', + ] ); + + $text = ( new WikiPage( Title::newFromText( $name ) ) ) + ->getContent()->getNativeData(); + + $this->assertSame( "Initial content\n\n== New section ==", $text ); + } + + public function testAppendNewSectionWithInvalidContentModel() { + $name = 'Help:' . ucfirst( __FUNCTION__ ); + + $this->setExpectedException( ApiUsageException::class, + 'Sections are not supported for content model text.' ); + + $this->editPage( $name, 'Initial content' ); + + $this->doApiRequestWithToken( [ + 'action' => 'edit', + 'title' => $name, + 'appendtext' => '== New section ==', + 'section' => 'new', + 'contentmodel' => 'text', + ] ); + } + + public function testAppendNewSectionWithTitle() { + $name = 'Help:' . ucfirst( __FUNCTION__ ); + + $this->editPage( $name, 'Initial content' ); + + $this->doApiRequestWithToken( [ + 'action' => 'edit', + 'title' => $name, + 'sectiontitle' => 'My section', + 'appendtext' => 'More content', + 'section' => 'new', + ] ); + + $page = new WikiPage( Title::newFromText( $name ) ); + + $this->assertSame( "Initial content\n\n== My section ==\n\nMore content", + $page->getContent()->getNativeData() ); + $this->assertSame( '/* My section */ new section', + $page->getRevision()->getComment() ); + } + + public function testAppendNewSectionWithSummary() { + $name = 'Help:' . ucfirst( __FUNCTION__ ); + + $this->editPage( $name, 'Initial content' ); + + $this->doApiRequestWithToken( [ + 'action' => 'edit', + 'title' => $name, + 'appendtext' => 'More content', + 'section' => 'new', + 'summary' => 'Add new section', + ] ); + + $page = new WikiPage( Title::newFromText( $name ) ); + + $this->assertSame( "Initial content\n\n== Add new section ==\n\nMore content", + $page->getContent()->getNativeData() ); + // EditPage actually assumes the summary is the section name here + $this->assertSame( '/* Add new section */ new section', + $page->getRevision()->getComment() ); + } + + public function testAppendNewSectionWithTitleAndSummary() { + $name = 'Help:' . ucfirst( __FUNCTION__ ); + + $this->editPage( $name, 'Initial content' ); + + $this->doApiRequestWithToken( [ + 'action' => 'edit', + 'title' => $name, + 'sectiontitle' => 'My section', + 'appendtext' => 'More content', + 'section' => 'new', + 'summary' => 'Add new section', + ] ); + + $page = new WikiPage( Title::newFromText( $name ) ); + + $this->assertSame( "Initial content\n\n== My section ==\n\nMore content", + $page->getContent()->getNativeData() ); + $this->assertSame( 'Add new section', + $page->getRevision()->getComment() ); + } + + public function testAppendToSection() { + $name = 'Help:' . ucfirst( __FUNCTION__ ); + + $this->editPage( $name, "== Section 1 ==\n\nContent\n\n" . + "== Section 2 ==\n\nFascinating!" ); + + $this->doApiRequestWithToken( [ + 'action' => 'edit', + 'title' => $name, + 'appendtext' => ' and more content', + 'section' => '1', + ] ); + + $text = ( new WikiPage( Title::newFromText( $name ) ) ) + ->getContent()->getNativeData(); + + $this->assertSame( "== Section 1 ==\n\nContent and more content\n\n" . + "== Section 2 ==\n\nFascinating!", $text ); + } + + public function testAppendToFirstSection() { + $name = 'Help:' . ucfirst( __FUNCTION__ ); + + $this->editPage( $name, "Content\n\n== Section 1 ==\n\nFascinating!" ); + + $this->doApiRequestWithToken( [ + 'action' => 'edit', + 'title' => $name, + 'appendtext' => ' and more content', + 'section' => '0', + ] ); + + $text = ( new WikiPage( Title::newFromText( $name ) ) ) + ->getContent()->getNativeData(); + + $this->assertSame( "Content and more content\n\n== Section 1 ==\n\n" . + "Fascinating!", $text ); + } + + public function testAppendToNonexistentSection() { + $name = 'Help:' . ucfirst( __FUNCTION__ ); + + $this->setExpectedException( ApiUsageException::class, 'There is no section 1.' ); + + $this->editPage( $name, 'Content' ); + + try { + $this->doApiRequestWithToken( [ + 'action' => 'edit', + 'title' => $name, + 'appendtext' => ' and more content', + 'section' => '1', + ] ); + } finally { + $text = ( new WikiPage( Title::newFromText( $name ) ) ) + ->getContent()->getNativeData(); + + $this->assertSame( 'Content', $text ); + } + } + + public function testEditMalformedSection() { + $name = 'Help:' . ucfirst( __FUNCTION__ ); + + $this->setExpectedException( ApiUsageException::class, + 'The "section" parameter must be a valid section ID or "new".' ); + $this->editPage( $name, 'Content' ); + + try { + $this->doApiRequestWithToken( [ + 'action' => 'edit', + 'title' => $name, + 'text' => 'Different content', + 'section' => 'It is unlikely that this is valid', + ] ); + } finally { + $text = ( new WikiPage( Title::newFromText( $name ) ) ) + ->getContent()->getNativeData(); + + $this->assertSame( 'Content', $text ); + } + } + + public function testEditWithStartTimestamp() { + $name = 'Help:' . ucfirst( __FUNCTION__ ); + $this->setExpectedException( ApiUsageException::class, + 'The page has been deleted since you fetched its timestamp.' ); + + $startTime = MWTimestamp::convert( TS_MW, time() - 1 ); + + $this->editPage( $name, 'Some text' ); + + $pageObj = new WikiPage( Title::newFromText( $name ) ); + $pageObj->doDeleteArticle( 'Bye-bye' ); + + $this->assertFalse( $pageObj->exists() ); + + try { + $this->doApiRequestWithToken( [ + 'action' => 'edit', + 'title' => $name, + 'text' => 'Different text', + 'starttimestamp' => $startTime, + ] ); + } finally { + $this->assertFalse( $pageObj->exists() ); + } + } + + public function testEditMinor() { + $name = 'Help:' . ucfirst( __FUNCTION__ ); + + $this->editPage( $name, 'Some text' ); + + $this->doApiRequestWithToken( [ + 'action' => 'edit', + 'title' => $name, + 'text' => 'Different text', + 'minor' => '', + ] ); + + $revisionStore = \MediaWiki\MediaWikiServices::getInstance()->getRevisionStore(); + $revision = $revisionStore->getRevisionByTitle( Title::newFromText( $name ) ); + $this->assertTrue( $revision->isMinor() ); + } + + public function testEditRecreate() { + $name = 'Help:' . ucfirst( __FUNCTION__ ); + + $startTime = MWTimestamp::convert( TS_MW, time() - 1 ); + + $this->editPage( $name, 'Some text' ); + + $pageObj = new WikiPage( Title::newFromText( $name ) ); + $pageObj->doDeleteArticle( 'Bye-bye' ); + + $this->assertFalse( $pageObj->exists() ); + + $this->doApiRequestWithToken( [ + 'action' => 'edit', + 'title' => $name, + 'text' => 'Different text', + 'starttimestamp' => $startTime, + 'recreate' => '', + ] ); + + $this->assertTrue( Title::newFromText( $name )->exists() ); + } + + public function testEditWatch() { + $name = 'Help:' . ucfirst( __FUNCTION__ ); + $user = self::$users['sysop']->getUser(); + + $this->doApiRequestWithToken( [ + 'action' => 'edit', + 'title' => $name, + 'text' => 'Some text', + 'watch' => '', + ] ); + + $this->assertTrue( Title::newFromText( $name )->exists() ); + $this->assertTrue( $user->isWatched( Title::newFromText( $name ) ) ); + } + + public function testEditUnwatch() { + $name = 'Help:' . ucfirst( __FUNCTION__ ); + $user = self::$users['sysop']->getUser(); + $titleObj = Title::newFromText( $name ); + + $user->addWatch( $titleObj ); + + $this->assertFalse( $titleObj->exists() ); + $this->assertTrue( $user->isWatched( $titleObj ) ); + + $this->doApiRequestWithToken( [ + 'action' => 'edit', + 'title' => $name, + 'text' => 'Some text', + 'unwatch' => '', + ] ); + + $this->assertTrue( $titleObj->exists() ); + $this->assertFalse( $user->isWatched( $titleObj ) ); + } + + public function testEditWithTag() { + $name = 'Help:' . ucfirst( __FUNCTION__ ); + + ChangeTags::defineTag( 'custom tag' ); + + $revId = $this->doApiRequestWithToken( [ + 'action' => 'edit', + 'title' => $name, + 'text' => 'Some text', + 'tags' => 'custom tag', + ] )[0]['edit']['newrevid']; + + $dbw = wfGetDB( DB_MASTER ); + $this->assertSame( 'custom tag', $dbw->selectField( + 'change_tag', 'ct_tag', [ 'ct_rev_id' => $revId ], __METHOD__ ) ); + } + + public function testEditWithoutTagPermission() { + $name = 'Help:' . ucfirst( __FUNCTION__ ); + + $this->setExpectedException( ApiUsageException::class, + 'You do not have permission to apply change tags along with your changes.' ); + + $this->assertFalse( Title::newFromText( $name )->exists() ); + + ChangeTags::defineTag( 'custom tag' ); + $this->setMwGlobals( 'wgRevokePermissions', + [ 'user' => [ 'applychangetags' => true ] ] ); + try { + $this->doApiRequestWithToken( [ + 'action' => 'edit', + 'title' => $name, + 'text' => 'Some text', + 'tags' => 'custom tag', + ] ); + } finally { + $this->assertFalse( Title::newFromText( $name )->exists() ); + } + } + + public function testEditAbortedByHook() { + $name = 'Help:' . ucfirst( __FUNCTION__ ); + + $this->setExpectedException( ApiUsageException::class, + 'The modification you tried to make was aborted by an extension.' ); + + $this->hideDeprecated( 'APIEditBeforeSave hook (used in ' . + 'hook-APIEditBeforeSave-closure)' ); + + $this->setTemporaryHook( 'APIEditBeforeSave', + function () { + return false; + } + ); + + try { + $this->doApiRequestWithToken( [ + 'action' => 'edit', + 'title' => $name, + 'text' => 'Some text', + ] ); + } finally { + $this->assertFalse( Title::newFromText( $name )->exists() ); + } + } + + public function testEditAbortedByHookWithCustomOutput() { + $name = 'Help:' . ucfirst( __FUNCTION__ ); + + $this->hideDeprecated( 'APIEditBeforeSave hook (used in ' . + 'hook-APIEditBeforeSave-closure)' ); + + $this->setTemporaryHook( 'APIEditBeforeSave', + function ( $unused1, $unused2, &$r ) { + $r['msg'] = 'Some message'; + return false; + } ); + + $result = $this->doApiRequestWithToken( [ + 'action' => 'edit', + 'title' => $name, + 'text' => 'Some text', + ] ); + Wikimedia\restoreWarnings(); + + $this->assertSame( [ 'msg' => 'Some message', 'result' => 'Failure' ], + $result[0]['edit'] ); + + $this->assertFalse( Title::newFromText( $name )->exists() ); + } + + public function testEditAbortedByEditPageHookWithResult() { + $name = 'Help:' . ucfirst( __FUNCTION__ ); + + $this->setTemporaryHook( 'EditFilterMergedContent', + function ( $unused1, $unused2, Status $status ) { + $status->apiHookResult = [ 'msg' => 'A message for you!' ]; + return false; + } ); + + $res = $this->doApiRequestWithToken( [ + 'action' => 'edit', + 'title' => $name, + 'text' => 'Some text', + ] ); + + $this->assertFalse( Title::newFromText( $name )->exists() ); + $this->assertSame( [ 'edit' => [ 'msg' => 'A message for you!', + 'result' => 'Failure' ] ], $res[0] ); + } + + public function testEditAbortedByEditPageHookWithNoResult() { + $name = 'Help:' . ucfirst( __FUNCTION__ ); + + $this->setExpectedException( ApiUsageException::class, + 'The modification you tried to make was aborted by an extension.' ); + + $this->setTemporaryHook( 'EditFilterMergedContent', + function () { + return false; + } + ); + + try { + $this->doApiRequestWithToken( [ + 'action' => 'edit', + 'title' => $name, + 'text' => 'Some text', + ] ); + } finally { + $this->assertFalse( Title::newFromText( $name )->exists() ); + } + } + + public function testEditWhileBlocked() { + $name = 'Help:' . ucfirst( __FUNCTION__ ); + + $this->setExpectedException( ApiUsageException::class, + 'You have been blocked from editing.' ); + + $block = new Block( [ + 'address' => self::$users['sysop']->getUser()->getName(), + 'by' => self::$users['sysop']->getUser()->getId(), + 'reason' => 'Capriciousness', + 'timestamp' => '19370101000000', + 'expiry' => 'infinity', + ] ); + $block->insert(); + + try { + $this->doApiRequestWithToken( [ + 'action' => 'edit', + 'title' => $name, + 'text' => 'Some text', + ] ); + } finally { + $block->delete(); + self::$users['sysop']->getUser()->clearInstanceCache(); + } + } + + public function testEditWhileReadOnly() { + $name = 'Help:' . ucfirst( __FUNCTION__ ); + + $this->setExpectedException( ApiUsageException::class, + 'The wiki is currently in read-only mode.' ); + + $svc = \MediaWiki\MediaWikiServices::getInstance()->getReadOnlyMode(); + $svc->setReason( "Read-only for testing" ); + + try { + $this->doApiRequestWithToken( [ + 'action' => 'edit', + 'title' => $name, + 'text' => 'Some text', + ] ); + } finally { + $svc->setReason( false ); + } + } + + public function testCreateImageRedirectAnon() { + $name = 'File:' . ucfirst( __FUNCTION__ ); + + $this->setExpectedException( ApiUsageException::class, + "Anonymous users can't create image redirects." ); + + $this->doApiRequestWithToken( [ + 'action' => 'edit', + 'title' => $name, + 'text' => '#REDIRECT [[File:Other file.png]]', + ], null, new User() ); + } + + public function testCreateImageRedirectLoggedIn() { + $name = 'File:' . ucfirst( __FUNCTION__ ); + + $this->setExpectedException( ApiUsageException::class, + "You don't have permission to create image redirects." ); + + $this->setMwGlobals( 'wgRevokePermissions', + [ 'user' => [ 'upload' => true ] ] ); + + $this->doApiRequestWithToken( [ + 'action' => 'edit', + 'title' => $name, + 'text' => '#REDIRECT [[File:Other file.png]]', + ] ); + } + + public function testTooBigEdit() { + $name = 'Help:' . ucfirst( __FUNCTION__ ); + + $this->setExpectedException( ApiUsageException::class, + 'The content you supplied exceeds the article size limit of 1 kilobyte.' ); + + $this->setMwGlobals( 'wgMaxArticleSize', 1 ); + + $text = str_repeat( '!', 1025 ); + + $this->doApiRequestWithToken( [ + 'action' => 'edit', + 'title' => $name, + 'text' => $text, + ] ); + } + + public function testProhibitedAnonymousEdit() { + $name = 'Help:' . ucfirst( __FUNCTION__ ); + + $this->setExpectedException( ApiUsageException::class, + 'The action you have requested is limited to users in the group: ' ); + + $this->setMwGlobals( 'wgRevokePermissions', [ '*' => [ 'edit' => true ] ] ); + + $this->doApiRequestWithToken( [ + 'action' => 'edit', + 'title' => $name, + 'text' => 'Some text', + ], null, new User() ); + } + + public function testProhibitedChangeContentModel() { + $name = 'Help:' . ucfirst( __FUNCTION__ ); + + $this->setExpectedException( ApiUsageException::class, + "You don't have permission to change the content model of a page." ); + + $this->setMwGlobals( 'wgRevokePermissions', + [ 'user' => [ 'editcontentmodel' => true ] ] ); + + $this->doApiRequestWithToken( [ + 'action' => 'edit', + 'title' => $name, + 'text' => 'Some text', + 'contentmodel' => 'json', + ] ); } } diff --git a/www/wiki/tests/phpunit/includes/api/ApiErrorFormatterTest.php b/www/wiki/tests/phpunit/includes/api/ApiErrorFormatterTest.php index d47481cb..aa579ab0 100644 --- a/www/wiki/tests/phpunit/includes/api/ApiErrorFormatterTest.php +++ b/www/wiki/tests/phpunit/includes/api/ApiErrorFormatterTest.php @@ -575,11 +575,11 @@ class ApiErrorFormatterTest extends MediaWikiLangTestCase { } public static function provideGetMessageFromException() { - MediaWiki\suppressWarnings(); + Wikimedia\suppressWarnings(); $usageException = new UsageException( '<b>Something broke!</b>', 'ue-code', 0, [ 'xxx' => 'yyy', 'baz' => 23 ] ); - MediaWiki\restoreWarnings(); + Wikimedia\restoreWarnings(); return [ 'Normal exception' => [ diff --git a/www/wiki/tests/phpunit/includes/api/ApiLoginTest.php b/www/wiki/tests/phpunit/includes/api/ApiLoginTest.php index 3cf1fdee..d382c83c 100644 --- a/www/wiki/tests/phpunit/includes/api/ApiLoginTest.php +++ b/www/wiki/tests/phpunit/includes/api/ApiLoginTest.php @@ -142,7 +142,7 @@ class ApiLoginTest extends ApiTestCase { libxml_use_internal_errors( true ); $sxe = simplexml_load_string( $req->getContent() ); $this->assertNotInternalType( "bool", $sxe ); - $this->assertThat( $sxe, $this->isInstanceOf( "SimpleXMLElement" ) ); + $this->assertThat( $sxe, $this->isInstanceOf( SimpleXMLElement::class ) ); $this->assertNotInternalType( "null", $sxe->login[0] ); $a = $sxe->login[0]->attributes()->result[0]; @@ -282,4 +282,20 @@ class ApiLoginTest extends ApiTestCase { $this->assertEquals( 'Success', $a ); } + public function testLoginWithNoSameOriginSecurity() { + $this->setTemporaryHook( 'RequestHasSameOriginSecurity', + function () { + return false; + } + ); + + $result = $this->doApiRequest( [ + 'action' => 'login', + ] )[0]['login']; + + $this->assertSame( [ + 'result' => 'Aborted', + 'reason' => 'Cannot log in when the same-origin policy is not applied.', + ], $result ); + } } diff --git a/www/wiki/tests/phpunit/includes/api/ApiLogoutTest.php b/www/wiki/tests/phpunit/includes/api/ApiLogoutTest.php new file mode 100644 index 00000000..8254fdba --- /dev/null +++ b/www/wiki/tests/phpunit/includes/api/ApiLogoutTest.php @@ -0,0 +1,75 @@ +<?php + +/** + * @group API + * @group Database + * @group medium + * + * @covers ApiLogout + */ +class ApiLogoutTest extends ApiTestCase { + + protected function setUp() { + global $wgRequest, $wgUser; + + parent::setUp(); + + // Link the user to the Session properly so User::doLogout() doesn't complain. + $wgRequest->getSession()->setUser( $wgUser ); + $wgUser = User::newFromSession( $wgRequest ); + $this->apiContext->setUser( $wgUser ); + } + + public function testUserLogoutBadToken() { + global $wgUser; + + $this->setExpectedApiException( 'apierror-badtoken' ); + + try { + $token = 'invalid token'; + $this->doUserLogout( $token ); + } finally { + $this->assertTrue( $wgUser->isLoggedIn(), 'not logged out' ); + } + } + + public function testUserLogout() { + global $wgUser; + + $this->assertTrue( $wgUser->isLoggedIn(), 'sanity check' ); + $token = $this->getUserCsrfTokenFromApi(); + $this->doUserLogout( $token ); + $this->assertFalse( $wgUser->isLoggedIn() ); + } + + public function testUserLogoutWithWebToken() { + global $wgUser, $wgRequest; + + $this->assertTrue( $wgUser->isLoggedIn(), 'sanity check' ); + + // Logic copied from SkinTemplate. + $token = $wgUser->getEditToken( 'logoutToken', $wgRequest ); + + $this->doUserLogout( $token ); + $this->assertFalse( $wgUser->isLoggedIn() ); + } + + private function getUserCsrfTokenFromApi() { + $retToken = $this->doApiRequest( [ + 'action' => 'query', + 'meta' => 'tokens', + 'type' => 'csrf' + ] ); + + $this->assertArrayNotHasKey( 'warnings', $retToken ); + + return $retToken[0]['query']['tokens']['csrftoken']; + } + + private function doUserLogout( $logoutToken ) { + return $this->doApiRequest( [ + 'action' => 'logout', + 'token' => $logoutToken + ] ); + } +} diff --git a/www/wiki/tests/phpunit/includes/api/ApiMainTest.php b/www/wiki/tests/phpunit/includes/api/ApiMainTest.php index ad334e94..d17334bb 100644 --- a/www/wiki/tests/phpunit/includes/api/ApiMainTest.php +++ b/www/wiki/tests/phpunit/includes/api/ApiMainTest.php @@ -24,6 +24,356 @@ class ApiMainTest extends ApiTestCase { $this->assertArrayHasKey( 'query', $data ); } + public function testApiNoParam() { + $api = new ApiMain(); + $api->execute(); + $data = $api->getResult()->getResultData(); + $this->assertInternalType( 'array', $data ); + } + + /** + * ApiMain behaves differently if passed a FauxRequest (mInternalMode set + * to true) or a proper WebRequest (mInternalMode false). For most tests + * we can just set mInternalMode to false using TestingAccessWrapper, but + * this doesn't work for the constructor. This method returns an ApiMain + * that's been set up in non-internal mode. + * + * Note that calling execute() will print to the console. Wrap it in + * ob_start()/ob_end_clean() to prevent this. + * + * @param array $requestData Query parameters for the WebRequest + * @param array $headers Headers for the WebRequest + */ + private function getNonInternalApiMain( array $requestData, array $headers = [] ) { + $req = $this->getMockBuilder( WebRequest::class ) + ->setMethods( [ 'response', 'getRawIP' ] ) + ->getMock(); + $response = new FauxResponse(); + $req->method( 'response' )->willReturn( $response ); + $req->method( 'getRawIP' )->willReturn( '127.0.0.1' ); + + $wrapper = TestingAccessWrapper::newFromObject( $req ); + $wrapper->data = $requestData; + if ( $headers ) { + $wrapper->headers = $headers; + } + + return new ApiMain( $req ); + } + + public function testUselang() { + global $wgLang; + + $api = $this->getNonInternalApiMain( [ + 'action' => 'query', + 'meta' => 'siteinfo', + 'uselang' => 'fr', + ] ); + + ob_start(); + $api->execute(); + ob_end_clean(); + + $this->assertSame( 'fr', $wgLang->getCode() ); + } + + public function testNonWhitelistedCorsWithCookies() { + $logFile = $this->getNewTempFile(); + + $this->mergeMwGlobalArrayValue( '_COOKIE', [ 'forceHTTPS' => '1' ] ); + $logger = new TestLogger( true ); + $this->setLogger( 'cors', $logger ); + + $api = $this->getNonInternalApiMain( [ + 'action' => 'query', + 'meta' => 'siteinfo', + // For some reason multiple origins (which are not allowed in the + // WHATWG Fetch spec that supersedes the RFC) are always considered to + // be problematic. + ], [ 'ORIGIN' => 'https://www.example.com https://www.com.example' ] ); + + $this->assertSame( + [ [ Psr\Log\LogLevel::WARNING, 'Non-whitelisted CORS request with session cookies' ] ], + $logger->getBuffer() + ); + } + + public function testSuppressedLogin() { + global $wgUser; + $origUser = $wgUser; + + $api = $this->getNonInternalApiMain( [ + 'action' => 'query', + 'meta' => 'siteinfo', + 'origin' => '*', + ] ); + + ob_start(); + $api->execute(); + ob_end_clean(); + + $this->assertNotSame( $origUser, $wgUser ); + $this->assertSame( 'true', $api->getContext()->getRequest()->response() + ->getHeader( 'MediaWiki-Login-Suppressed' ) ); + } + + public function testSetContinuationManager() { + $api = new ApiMain(); + $manager = $this->createMock( ApiContinuationManager::class ); + $api->setContinuationManager( $manager ); + $this->assertTrue( true, 'No exception' ); + return [ $api, $manager ]; + } + + /** + * @depends testSetContinuationManager + */ + public function testSetContinuationManagerTwice( $args ) { + $this->setExpectedException( UnexpectedValueException::class, + 'ApiMain::setContinuationManager: tried to set manager from ' . + 'when a manager is already set from ' ); + + list( $api, $manager ) = $args; + $api->setContinuationManager( $manager ); + } + + public function testSetCacheModeUnrecognized() { + $api = new ApiMain(); + $api->setCacheMode( 'unrecognized' ); + $this->assertSame( + 'private', + TestingAccessWrapper::newFromObject( $api )->mCacheMode, + 'Unrecognized params must be silently ignored' + ); + } + + public function testSetCacheModePrivateWiki() { + $this->setGroupPermissions( '*', 'read', false ); + + $wrappedApi = TestingAccessWrapper::newFromObject( new ApiMain() ); + $wrappedApi->setCacheMode( 'public' ); + $this->assertSame( 'private', $wrappedApi->mCacheMode ); + $wrappedApi->setCacheMode( 'anon-public-user-private' ); + $this->assertSame( 'private', $wrappedApi->mCacheMode ); + } + + public function testAddRequestedFieldsRequestId() { + $req = new FauxRequest( [ + 'action' => 'query', + 'meta' => 'siteinfo', + 'requestid' => '123456', + ] ); + $api = new ApiMain( $req ); + $api->execute(); + $this->assertSame( '123456', $api->getResult()->getResultData()['requestid'] ); + } + + public function testAddRequestedFieldsCurTimestamp() { + $req = new FauxRequest( [ + 'action' => 'query', + 'meta' => 'siteinfo', + 'curtimestamp' => '', + ] ); + $api = new ApiMain( $req ); + $api->execute(); + $timestamp = $api->getResult()->getResultData()['curtimestamp']; + $this->assertLessThanOrEqual( 1, abs( strtotime( $timestamp ) - time() ) ); + } + + public function testAddRequestedFieldsResponseLangInfo() { + $req = new FauxRequest( [ + 'action' => 'query', + 'meta' => 'siteinfo', + // errorlang is ignored if errorformat is not specified + 'errorformat' => 'plaintext', + 'uselang' => 'FR', + 'errorlang' => 'ja', + 'responselanginfo' => '', + ] ); + $api = new ApiMain( $req ); + $api->execute(); + $data = $api->getResult()->getResultData(); + $this->assertSame( 'fr', $data['uselang'] ); + $this->assertSame( 'ja', $data['errorlang'] ); + } + + public function testSetupModuleUnknown() { + $this->setExpectedException( ApiUsageException::class, + 'Unrecognized value for parameter "action": unknownaction.' ); + + $req = new FauxRequest( [ 'action' => 'unknownaction' ] ); + $api = new ApiMain( $req ); + $api->execute(); + } + + public function testSetupModuleNoTokenProvided() { + $this->setExpectedException( ApiUsageException::class, + 'The "token" parameter must be set.' ); + + $req = new FauxRequest( [ + 'action' => 'edit', + 'title' => 'New page', + 'text' => 'Some text', + ] ); + $api = new ApiMain( $req ); + $api->execute(); + } + + public function testSetupModuleInvalidTokenProvided() { + $this->setExpectedException( ApiUsageException::class, 'Invalid CSRF token.' ); + + $req = new FauxRequest( [ + 'action' => 'edit', + 'title' => 'New page', + 'text' => 'Some text', + 'token' => "This isn't a real token!", + ] ); + $api = new ApiMain( $req ); + $api->execute(); + } + + public function testSetupModuleNeedsTokenTrue() { + $this->setExpectedException( MWException::class, + "Module 'testmodule' must be updated for the new token handling. " . + "See documentation for ApiBase::needsToken for details." ); + + $mock = $this->createMock( ApiBase::class ); + $mock->method( 'getModuleName' )->willReturn( 'testmodule' ); + $mock->method( 'needsToken' )->willReturn( true ); + + $api = new ApiMain( new FauxRequest( [ 'action' => 'testmodule' ] ) ); + $api->getModuleManager()->addModule( 'testmodule', 'action', get_class( $mock ), + function () use ( $mock ) { + return $mock; + } + ); + $api->execute(); + } + + public function testSetupModuleNeedsTokenNeedntBePosted() { + $this->setExpectedException( MWException::class, + "Module 'testmodule' must require POST to use tokens." ); + + $mock = $this->createMock( ApiBase::class ); + $mock->method( 'getModuleName' )->willReturn( 'testmodule' ); + $mock->method( 'needsToken' )->willReturn( 'csrf' ); + $mock->method( 'mustBePosted' )->willReturn( false ); + + $api = new ApiMain( new FauxRequest( [ 'action' => 'testmodule' ] ) ); + $api->getModuleManager()->addModule( 'testmodule', 'action', get_class( $mock ), + function () use ( $mock ) { + return $mock; + } + ); + $api->execute(); + } + + public function testCheckMaxLagFailed() { + // It's hard to mock the LoadBalancer properly, so instead we'll mock + // checkMaxLag (which is tested directly in other tests below). + $req = new FauxRequest( [ + 'action' => 'query', + 'meta' => 'siteinfo', + ] ); + + $mock = $this->getMockBuilder( ApiMain::class ) + ->setConstructorArgs( [ $req ] ) + ->setMethods( [ 'checkMaxLag' ] ) + ->getMock(); + $mock->method( 'checkMaxLag' )->willReturn( false ); + + $mock->execute(); + + $this->assertArrayNotHasKey( 'query', $mock->getResult()->getResultData() ); + } + + public function testCheckConditionalRequestHeadersFailed() { + // The detailed checking of all cases of checkConditionalRequestHeaders + // is below in testCheckConditionalRequestHeaders(), which calls the + // method directly. Here we just check that it will stop execution if + // it does fail. + $now = time(); + + $this->setMwGlobals( 'wgCacheEpoch', '20030516000000' ); + + $mock = $this->createMock( ApiBase::class ); + $mock->method( 'getModuleName' )->willReturn( 'testmodule' ); + $mock->method( 'getConditionalRequestData' ) + ->willReturn( wfTimestamp( TS_MW, $now - 3600 ) ); + $mock->expects( $this->exactly( 0 ) )->method( 'execute' ); + + $req = new FauxRequest( [ + 'action' => 'testmodule', + ] ); + $req->setHeader( 'If-Modified-Since', wfTimestamp( TS_RFC2822, $now - 3600 ) ); + $req->setRequestURL( "http://localhost" ); + + $api = new ApiMain( $req ); + $api->getModuleManager()->addModule( 'testmodule', 'action', get_class( $mock ), + function () use ( $mock ) { + return $mock; + } + ); + + $wrapper = TestingAccessWrapper::newFromObject( $api ); + $wrapper->mInternalMode = false; + + ob_start(); + $api->execute(); + ob_end_clean(); + } + + private function doTestCheckMaxLag( $lag ) { + $mockLB = $this->getMockBuilder( LoadBalancer::class ) + ->disableOriginalConstructor() + ->setMethods( [ 'getMaxLag', '__destruct' ] ) + ->getMock(); + $mockLB->method( 'getMaxLag' )->willReturn( [ 'somehost', $lag ] ); + $this->setService( 'DBLoadBalancer', $mockLB ); + + $req = new FauxRequest(); + + $api = new ApiMain( $req ); + $wrapper = TestingAccessWrapper::newFromObject( $api ); + + $mockModule = $this->createMock( ApiBase::class ); + $mockModule->method( 'shouldCheckMaxLag' )->willReturn( true ); + + try { + $wrapper->checkMaxLag( $mockModule, [ 'maxlag' => 3 ] ); + } finally { + if ( $lag > 3 ) { + $this->assertSame( '5', $req->response()->getHeader( 'Retry-After' ) ); + $this->assertSame( (string)$lag, $req->response()->getHeader( 'X-Database-Lag' ) ); + } + } + } + + public function testCheckMaxLagOkay() { + $this->doTestCheckMaxLag( 3 ); + + // No exception, we're happy + $this->assertTrue( true ); + } + + public function testCheckMaxLagExceeded() { + $this->setExpectedException( ApiUsageException::class, + 'Waiting for a database server: 4 seconds lagged.' ); + + $this->setMwGlobals( 'wgShowHostnames', false ); + + $this->doTestCheckMaxLag( 4 ); + } + + public function testCheckMaxLagExceededWithHostNames() { + $this->setExpectedException( ApiUsageException::class, + 'Waiting for somehost: 4 seconds lagged.' ); + + $this->setMwGlobals( 'wgShowHostnames', true ); + + $this->doTestCheckMaxLag( 4 ); + } + public static function provideAssert() { return [ [ false, [], 'user', 'assertuserfailed' ], @@ -37,7 +387,6 @@ class ApiMainTest extends ApiTestCase { /** * Tests the assert={user|bot} functionality * - * @covers ApiMain::checkAsserts * @dataProvider provideAssert * @param bool $registered * @param array $rights @@ -66,8 +415,6 @@ class ApiMainTest extends ApiTestCase { /** * Tests the assertuser= functionality - * - * @covers ApiMain::checkAsserts */ public function testAssertUser() { $user = $this->getTestUser()->getUser(); @@ -107,17 +454,21 @@ class ApiMainTest extends ApiTestCase { /** * Test HTTP precondition headers * - * @covers ApiMain::checkConditionalRequestHeaders * @dataProvider provideCheckConditionalRequestHeaders * @param array $headers HTTP headers * @param array $conditions Return data for ApiBase::getConditionalRequestData * @param int $status Expected response status - * @param bool $post Request is a POST + * @param array $options Array of options: + * post => true Request is a POST + * cdn => true CDN is enabled ($wgUseSquid) */ public function testCheckConditionalRequestHeaders( - $headers, $conditions, $status, $post = false + $headers, $conditions, $status, $options = [] ) { - $request = new FauxRequest( [ 'action' => 'query', 'meta' => 'siteinfo' ], $post ); + $request = new FauxRequest( + [ 'action' => 'query', 'meta' => 'siteinfo' ], + !empty( $options['post'] ) + ); $request->setHeaders( $headers ); $request->response()->statusHeader( 200 ); // Why doesn't it default? @@ -126,7 +477,14 @@ class ApiMainTest extends ApiTestCase { $priv = TestingAccessWrapper::newFromObject( $api ); $priv->mInternalMode = false; - $module = $this->getMockBuilder( 'ApiBase' ) + if ( !empty( $options['cdn'] ) ) { + $this->setMwGlobals( 'wgUseSquid', true ); + } + + // Can't do this in TestSetup.php because Setup.php will override it + $this->setMwGlobals( 'wgCacheEpoch', '20030516000000' ); + + $module = $this->getMockBuilder( ApiBase::class ) ->setConstructorArgs( [ $api, 'mock' ] ) ->setMethods( [ 'getConditionalRequestData' ] ) ->getMockForAbstractClass(); @@ -143,65 +501,99 @@ class ApiMainTest extends ApiTestCase { } public static function provideCheckConditionalRequestHeaders() { + global $wgSquidMaxage; $now = time(); return [ // Non-existing from module is ignored - [ [ 'If-None-Match' => '"foo", "bar"' ], [], 200 ], - [ [ 'If-Modified-Since' => 'Tue, 18 Aug 2015 00:00:00 GMT' ], [], 200 ], + 'If-None-Match' => [ [ 'If-None-Match' => '"foo", "bar"' ], [], 200 ], + 'If-Modified-Since' => + [ [ 'If-Modified-Since' => 'Tue, 18 Aug 2015 00:00:00 GMT' ], [], 200 ], // No headers - [ - [], - [ - 'etag' => '""', - 'last-modified' => '20150815000000', - ], - 200 - ], + 'No headers' => [ [], [ 'etag' => '""', 'last-modified' => '20150815000000', ], 200 ], // Basic If-None-Match - [ [ 'If-None-Match' => '"foo", "bar"' ], [ 'etag' => '"bar"' ], 304 ], - [ [ 'If-None-Match' => '"foo", "bar"' ], [ 'etag' => '"baz"' ], 200 ], - [ [ 'If-None-Match' => '"foo"' ], [ 'etag' => 'W/"foo"' ], 304 ], - [ [ 'If-None-Match' => 'W/"foo"' ], [ 'etag' => '"foo"' ], 304 ], - [ [ 'If-None-Match' => 'W/"foo"' ], [ 'etag' => 'W/"foo"' ], 304 ], + 'If-None-Match with matching etag' => + [ [ 'If-None-Match' => '"foo", "bar"' ], [ 'etag' => '"bar"' ], 304 ], + 'If-None-Match with non-matching etag' => + [ [ 'If-None-Match' => '"foo", "bar"' ], [ 'etag' => '"baz"' ], 200 ], + 'Strong If-None-Match with weak matching etag' => + [ [ 'If-None-Match' => '"foo"' ], [ 'etag' => 'W/"foo"' ], 304 ], + 'Weak If-None-Match with strong matching etag' => + [ [ 'If-None-Match' => 'W/"foo"' ], [ 'etag' => '"foo"' ], 304 ], + 'Weak If-None-Match with weak matching etag' => + [ [ 'If-None-Match' => 'W/"foo"' ], [ 'etag' => 'W/"foo"' ], 304 ], - // Pointless, but supported - [ [ 'If-None-Match' => '*' ], [], 304 ], + // Pointless for GET, but supported + 'If-None-Match: *' => [ [ 'If-None-Match' => '*' ], [], 304 ], // Basic If-Modified-Since - [ [ 'If-Modified-Since' => wfTimestamp( TS_RFC2822, $now ) ], - [ 'last-modified' => wfTimestamp( TS_MW, $now - 1 ) ], 304 ], - [ [ 'If-Modified-Since' => wfTimestamp( TS_RFC2822, $now ) ], - [ 'last-modified' => wfTimestamp( TS_MW, $now ) ], 304 ], - [ [ 'If-Modified-Since' => wfTimestamp( TS_RFC2822, $now ) ], - [ 'last-modified' => wfTimestamp( TS_MW, $now + 1 ) ], 200 ], + 'If-Modified-Since, modified one second earlier' => + [ [ 'If-Modified-Since' => wfTimestamp( TS_RFC2822, $now ) ], + [ 'last-modified' => wfTimestamp( TS_MW, $now - 1 ) ], 304 ], + 'If-Modified-Since, modified now' => + [ [ 'If-Modified-Since' => wfTimestamp( TS_RFC2822, $now ) ], + [ 'last-modified' => wfTimestamp( TS_MW, $now ) ], 304 ], + 'If-Modified-Since, modified one second later' => + [ [ 'If-Modified-Since' => wfTimestamp( TS_RFC2822, $now ) ], + [ 'last-modified' => wfTimestamp( TS_MW, $now + 1 ) ], 200 ], // If-Modified-Since ignored when If-None-Match is given too - [ [ 'If-None-Match' => '""', 'If-Modified-Since' => wfTimestamp( TS_RFC2822, $now ) ], - [ 'etag' => '"x"', 'last-modified' => wfTimestamp( TS_MW, $now - 1 ) ], 200 ], - [ [ 'If-None-Match' => '""', 'If-Modified-Since' => wfTimestamp( TS_RFC2822, $now ) ], - [ 'last-modified' => wfTimestamp( TS_MW, $now - 1 ) ], 304 ], + 'Non-matching If-None-Match and matching If-Modified-Since' => + [ [ 'If-None-Match' => '""', + 'If-Modified-Since' => wfTimestamp( TS_RFC2822, $now ) ], + [ 'etag' => '"x"', 'last-modified' => wfTimestamp( TS_MW, $now - 1 ) ], 200 ], + 'Non-matching If-None-Match and matching If-Modified-Since with no ETag' => + [ + [ + 'If-None-Match' => '""', + 'If-Modified-Since' => wfTimestamp( TS_RFC2822, $now ) + ], + [ 'last-modified' => wfTimestamp( TS_MW, $now - 1 ) ], + 304 + ], // Ignored for POST - [ [ 'If-None-Match' => '"foo", "bar"' ], [ 'etag' => '"bar"' ], 200, true ], - [ [ 'If-Modified-Since' => wfTimestamp( TS_RFC2822, $now ) ], - [ 'last-modified' => wfTimestamp( TS_MW, $now - 1 ) ], 200, true ], + 'Matching If-None-Match with POST' => + [ [ 'If-None-Match' => '"foo", "bar"' ], [ 'etag' => '"bar"' ], 200, + [ 'post' => true ] ], + 'Matching If-Modified-Since with POST' => + [ [ 'If-Modified-Since' => wfTimestamp( TS_RFC2822, $now ) ], + [ 'last-modified' => wfTimestamp( TS_MW, $now - 1 ) ], 200, + [ 'post' => true ] ], // Other date formats allowed by the RFC - [ [ 'If-Modified-Since' => gmdate( 'l, d-M-y H:i:s', $now ) . ' GMT' ], - [ 'last-modified' => wfTimestamp( TS_MW, $now - 1 ) ], 304 ], - [ [ 'If-Modified-Since' => gmdate( 'D M j H:i:s Y', $now ) ], - [ 'last-modified' => wfTimestamp( TS_MW, $now - 1 ) ], 304 ], + 'If-Modified-Since with alternate date format 1' => + [ [ 'If-Modified-Since' => gmdate( 'l, d-M-y H:i:s', $now ) . ' GMT' ], + [ 'last-modified' => wfTimestamp( TS_MW, $now - 1 ) ], 304 ], + 'If-Modified-Since with alternate date format 2' => + [ [ 'If-Modified-Since' => gmdate( 'D M j H:i:s Y', $now ) ], + [ 'last-modified' => wfTimestamp( TS_MW, $now - 1 ) ], 304 ], // Old browser extension to HTTP/1.0 - [ [ 'If-Modified-Since' => wfTimestamp( TS_RFC2822, $now ) . '; length=123' ], - [ 'last-modified' => wfTimestamp( TS_MW, $now - 1 ) ], 304 ], + 'If-Modified-Since with length' => + [ [ 'If-Modified-Since' => wfTimestamp( TS_RFC2822, $now ) . '; length=123' ], + [ 'last-modified' => wfTimestamp( TS_MW, $now - 1 ) ], 304 ], // Invalid date formats should be ignored - [ [ 'If-Modified-Since' => gmdate( 'Y-m-d H:i:s', $now ) . ' GMT' ], - [ 'last-modified' => wfTimestamp( TS_MW, $now - 1 ) ], 200 ], + 'If-Modified-Since with invalid date format' => + [ [ 'If-Modified-Since' => gmdate( 'Y-m-d H:i:s', $now ) . ' GMT' ], + [ 'last-modified' => wfTimestamp( TS_MW, $now - 1 ) ], 200 ], + 'If-Modified-Since with entirely unparseable date' => + [ [ 'If-Modified-Since' => 'a potato' ], + [ 'last-modified' => wfTimestamp( TS_MW, $now - 1 ) ], 200 ], + + // Anything before $wgSquidMaxage seconds ago should be considered + // expired. + 'If-Modified-Since with CDN post-expiry' => + [ [ 'If-Modified-Since' => wfTimestamp( TS_RFC2822, $now - $wgSquidMaxage * 2 ) ], + [ 'last-modified' => wfTimestamp( TS_MW, $now - $wgSquidMaxage * 3 ) ], + 200, [ 'cdn' => true ] ], + 'If-Modified-Since with CDN pre-expiry' => + [ [ 'If-Modified-Since' => wfTimestamp( TS_RFC2822, $now - $wgSquidMaxage / 2 ) ], + [ 'last-modified' => wfTimestamp( TS_MW, $now - $wgSquidMaxage * 3 ) ], + 304, [ 'cdn' => true ] ], ]; } @@ -223,7 +615,7 @@ class ApiMainTest extends ApiTestCase { $priv = TestingAccessWrapper::newFromObject( $api ); $priv->mInternalMode = false; - $module = $this->getMockBuilder( 'ApiBase' ) + $module = $this->getMockBuilder( ApiBase::class ) ->setConstructorArgs( [ $api, 'mock' ] ) ->setMethods( [ 'getConditionalRequestData' ] ) ->getMockForAbstractClass(); @@ -277,9 +669,101 @@ class ApiMainTest extends ApiTestCase { ]; } - /** - * @covers ApiMain::lacksSameOriginSecurity - */ + public function testCheckExecutePermissionsReadProhibited() { + $this->setExpectedException( ApiUsageException::class, + 'You need read permission to use this module.' ); + + $this->setGroupPermissions( '*', 'read', false ); + + $main = new ApiMain( new FauxRequest( [ 'action' => 'query', 'meta' => 'siteinfo' ] ) ); + $main->execute(); + } + + public function testCheckExecutePermissionWriteDisabled() { + $this->setExpectedException( ApiUsageException::class, + 'Editing of this wiki through the API is disabled. Make sure the ' . + '"$wgEnableWriteAPI=true;" statement is included in the wiki\'s ' . + '"LocalSettings.php" file.' ); + $main = new ApiMain( new FauxRequest( [ + 'action' => 'edit', + 'title' => 'Some page', + 'text' => 'Some text', + 'token' => '+\\', + ] ) ); + $main->execute(); + } + + public function testCheckExecutePermissionWriteApiProhibited() { + $this->setExpectedException( ApiUsageException::class, + "You're not allowed to edit this wiki through the API." ); + $this->setGroupPermissions( '*', 'writeapi', false ); + + $main = new ApiMain( new FauxRequest( [ + 'action' => 'edit', + 'title' => 'Some page', + 'text' => 'Some text', + 'token' => '+\\', + ] ), /* enableWrite = */ true ); + $main->execute(); + } + + public function testCheckExecutePermissionPromiseNonWrite() { + $this->setExpectedException( ApiUsageException::class, + 'The "Promise-Non-Write-API-Action" HTTP header cannot be sent ' . + 'to write-mode API modules.' ); + + $req = new FauxRequest( [ + 'action' => 'edit', + 'title' => 'Some page', + 'text' => 'Some text', + 'token' => '+\\', + ] ); + $req->setHeaders( [ 'Promise-Non-Write-API-Action' => '1' ] ); + $main = new ApiMain( $req, /* enableWrite = */ true ); + $main->execute(); + } + + public function testCheckExecutePermissionHookAbort() { + $this->setExpectedException( ApiUsageException::class, 'Main Page' ); + + $this->setTemporaryHook( 'ApiCheckCanExecute', function ( $unused1, $unused2, &$message ) { + $message = 'mainpage'; + return false; + } ); + + $main = new ApiMain( new FauxRequest( [ + 'action' => 'edit', + 'title' => 'Some page', + 'text' => 'Some text', + 'token' => '+\\', + ] ), /* enableWrite = */ true ); + $main->execute(); + } + + public function testGetValUnsupportedArray() { + $main = new ApiMain( new FauxRequest( [ + 'action' => 'query', + 'meta' => 'siteinfo', + 'siprop' => [ 'general', 'namespaces' ], + ] ) ); + $this->assertSame( 'myDefault', $main->getVal( 'siprop', 'myDefault' ) ); + $main->execute(); + $this->assertSame( 'Parameter "siprop" uses unsupported PHP array syntax.', + $main->getResult()->getResultData()['warnings']['main']['warnings'] ); + } + + public function testReportUnusedParams() { + $main = new ApiMain( new FauxRequest( [ + 'action' => 'query', + 'meta' => 'siteinfo', + 'unusedparam' => 'unusedval', + 'anotherunusedparam' => 'anotherval', + ] ) ); + $main->execute(); + $this->assertSame( 'Unrecognized parameters: unusedparam, anotherunusedparam.', + $main->getResult()->getResultData()['warnings']['main']['warnings'] ); + } + public function testLacksSameOriginSecurity() { // Basic test $main = new ApiMain( new FauxRequest( [ 'action' => 'query', 'meta' => 'siteinfo' ] ) ); @@ -309,7 +793,7 @@ class ApiMainTest extends ApiTestCase { /** * Test proper creation of the ApiErrorFormatter - * @covers ApiMain::__construct + * * @dataProvider provideApiErrorFormatterCreation * @param array $request Request parameters * @param array $expect Expected data @@ -443,8 +927,6 @@ class ApiMainTest extends ApiTestCase { } /** - * @covers ApiMain::errorMessagesFromException - * @covers ApiMain::substituteResultWithError * @dataProvider provideExceptionErrors * @param Exception $exception * @param array $expectReturn @@ -491,7 +973,7 @@ class ApiMainTest extends ApiTestCase { )->inLanguage( 'en' )->useDatabase( false )->text(); $dbex = new DBQueryError( - $this->createMock( 'IDatabase' ), + $this->createMock( \Wikimedia\Rdbms\IDatabase::class ), 'error', 1234, 'SELECT 1', __METHOD__ ); $dbtrace = wfMessage( 'api-exception-trace', get_class( $dbex ), @@ -500,9 +982,9 @@ class ApiMainTest extends ApiTestCase { MWExceptionHandler::getRedactedTraceAsString( $dbex ) )->inLanguage( 'en' )->useDatabase( false )->text(); - MediaWiki\suppressWarnings(); + Wikimedia\suppressWarnings(); $usageEx = new UsageException( 'Usage exception!', 'ue', 0, [ 'foo' => 'bar' ] ); - MediaWiki\restoreWarnings(); + Wikimedia\restoreWarnings(); $apiEx1 = new ApiUsageException( null, StatusValue::newFatal( new ApiRawMessage( 'An error', 'sv-error1' ) ) ); diff --git a/www/wiki/tests/phpunit/includes/api/ApiModuleManagerTest.php b/www/wiki/tests/phpunit/includes/api/ApiModuleManagerTest.php index be17bba2..b01b90e8 100644 --- a/www/wiki/tests/phpunit/includes/api/ApiModuleManagerTest.php +++ b/www/wiki/tests/phpunit/includes/api/ApiModuleManagerTest.php @@ -24,21 +24,21 @@ class ApiModuleManagerTest extends MediaWikiTestCase { 'plain class' => [ 'login', 'action', - 'ApiLogin', + ApiLogin::class, null, ], 'with factory' => [ 'login', 'action', - 'ApiLogin', + ApiLogin::class, [ $this, 'newApiLogin' ], ], 'with closure' => [ 'logout', 'action', - 'ApiLogout', + ApiLogout::class, function ( ApiMain $main, $action ) { return new ApiLogout( $main, $action ); }, @@ -66,8 +66,8 @@ class ApiModuleManagerTest extends MediaWikiTestCase { 'simple' => [ [ - 'login' => 'ApiLogin', - 'logout' => 'ApiLogout', + 'login' => ApiLogin::class, + 'logout' => ApiLogout::class, ], 'action', ], @@ -75,11 +75,11 @@ class ApiModuleManagerTest extends MediaWikiTestCase { 'with factories' => [ [ 'login' => [ - 'class' => 'ApiLogin', + 'class' => ApiLogin::class, 'factory' => [ $this, 'newApiLogin' ], ], 'logout' => [ - 'class' => 'ApiLogout', + 'class' => ApiLogout::class, 'factory' => function ( ApiMain $main, $action ) { return new ApiLogout( $main, $action ); }, @@ -107,14 +107,14 @@ class ApiModuleManagerTest extends MediaWikiTestCase { public function getModuleProvider() { $modules = [ - 'feedrecentchanges' => 'ApiFeedRecentChanges', - 'feedcontributions' => [ 'class' => 'ApiFeedContributions' ], + 'feedrecentchanges' => ApiFeedRecentChanges::class, + 'feedcontributions' => [ 'class' => ApiFeedContributions::class ], 'login' => [ - 'class' => 'ApiLogin', + 'class' => ApiLogin::class, 'factory' => [ $this, 'newApiLogin' ], ], 'logout' => [ - 'class' => 'ApiLogout', + 'class' => ApiLogout::class, 'factory' => function ( ApiMain $main, $action ) { return new ApiLogout( $main, $action ); }, @@ -125,25 +125,25 @@ class ApiModuleManagerTest extends MediaWikiTestCase { 'legacy entry' => [ $modules, 'feedrecentchanges', - 'ApiFeedRecentChanges', + ApiFeedRecentChanges::class, ], 'just a class' => [ $modules, 'feedcontributions', - 'ApiFeedContributions', + ApiFeedContributions::class, ], 'with factory' => [ $modules, 'login', - 'ApiLogin', + ApiLogin::class, ], 'with closure' => [ $modules, 'logout', - 'ApiLogout', + ApiLogout::class, ], ]; } @@ -178,8 +178,8 @@ class ApiModuleManagerTest extends MediaWikiTestCase { */ public function testGetModule_null() { $modules = [ - 'login' => 'ApiLogin', - 'logout' => 'ApiLogout', + 'login' => ApiLogin::class, + 'logout' => ApiLogout::class, ]; $moduleManager = $this->getModuleManager(); @@ -194,13 +194,13 @@ class ApiModuleManagerTest extends MediaWikiTestCase { */ public function testGetNames() { $fooModules = [ - 'login' => 'ApiLogin', - 'logout' => 'ApiLogout', + 'login' => ApiLogin::class, + 'logout' => ApiLogout::class, ]; $barModules = [ - 'feedcontributions' => [ 'class' => 'ApiFeedContributions' ], - 'feedrecentchanges' => [ 'class' => 'ApiFeedRecentChanges' ], + 'feedcontributions' => [ 'class' => ApiFeedContributions::class ], + 'feedrecentchanges' => [ 'class' => ApiFeedRecentChanges::class ], ]; $moduleManager = $this->getModuleManager(); @@ -220,13 +220,13 @@ class ApiModuleManagerTest extends MediaWikiTestCase { */ public function testGetNamesWithClasses() { $fooModules = [ - 'login' => 'ApiLogin', - 'logout' => 'ApiLogout', + 'login' => ApiLogin::class, + 'logout' => ApiLogout::class, ]; $barModules = [ - 'feedcontributions' => [ 'class' => 'ApiFeedContributions' ], - 'feedrecentchanges' => [ 'class' => 'ApiFeedRecentChanges' ], + 'feedcontributions' => [ 'class' => ApiFeedContributions::class ], + 'feedrecentchanges' => [ 'class' => ApiFeedRecentChanges::class ], ]; $moduleManager = $this->getModuleManager(); @@ -238,8 +238,8 @@ class ApiModuleManagerTest extends MediaWikiTestCase { $allNamesWithClasses = $moduleManager->getNamesWithClasses(); $allModules = array_merge( $fooModules, [ - 'feedcontributions' => 'ApiFeedContributions', - 'feedrecentchanges' => 'ApiFeedRecentChanges', + 'feedcontributions' => ApiFeedContributions::class, + 'feedrecentchanges' => ApiFeedRecentChanges::class, ] ); $this->assertArrayEquals( $allModules, $allNamesWithClasses ); } @@ -249,13 +249,13 @@ class ApiModuleManagerTest extends MediaWikiTestCase { */ public function testGetModuleGroup() { $fooModules = [ - 'login' => 'ApiLogin', - 'logout' => 'ApiLogout', + 'login' => ApiLogin::class, + 'logout' => ApiLogout::class, ]; $barModules = [ - 'feedcontributions' => [ 'class' => 'ApiFeedContributions' ], - 'feedrecentchanges' => [ 'class' => 'ApiFeedRecentChanges' ], + 'feedcontributions' => [ 'class' => ApiFeedContributions::class ], + 'feedrecentchanges' => [ 'class' => ApiFeedRecentChanges::class ], ]; $moduleManager = $this->getModuleManager(); @@ -272,13 +272,13 @@ class ApiModuleManagerTest extends MediaWikiTestCase { */ public function testGetGroups() { $fooModules = [ - 'login' => 'ApiLogin', - 'logout' => 'ApiLogout', + 'login' => ApiLogin::class, + 'logout' => ApiLogout::class, ]; $barModules = [ - 'feedcontributions' => [ 'class' => 'ApiFeedContributions' ], - 'feedrecentchanges' => [ 'class' => 'ApiFeedRecentChanges' ], + 'feedcontributions' => [ 'class' => ApiFeedContributions::class ], + 'feedrecentchanges' => [ 'class' => ApiFeedRecentChanges::class ], ]; $moduleManager = $this->getModuleManager(); @@ -294,13 +294,13 @@ class ApiModuleManagerTest extends MediaWikiTestCase { */ public function testGetClassName() { $fooModules = [ - 'login' => 'ApiLogin', - 'logout' => 'ApiLogout', + 'login' => ApiLogin::class, + 'logout' => ApiLogout::class, ]; $barModules = [ - 'feedcontributions' => [ 'class' => 'ApiFeedContributions' ], - 'feedrecentchanges' => [ 'class' => 'ApiFeedRecentChanges' ], + 'feedcontributions' => [ 'class' => ApiFeedContributions::class ], + 'feedrecentchanges' => [ 'class' => ApiFeedRecentChanges::class ], ]; $moduleManager = $this->getModuleManager(); @@ -308,19 +308,19 @@ class ApiModuleManagerTest extends MediaWikiTestCase { $moduleManager->addModules( $barModules, 'bar' ); $this->assertEquals( - 'ApiLogin', + ApiLogin::class, $moduleManager->getClassName( 'login' ) ); $this->assertEquals( - 'ApiLogout', + ApiLogout::class, $moduleManager->getClassName( 'logout' ) ); $this->assertEquals( - 'ApiFeedContributions', + ApiFeedContributions::class, $moduleManager->getClassName( 'feedcontributions' ) ); $this->assertEquals( - 'ApiFeedRecentChanges', + ApiFeedRecentChanges::class, $moduleManager->getClassName( 'feedrecentchanges' ) ); $this->assertFalse( diff --git a/www/wiki/tests/phpunit/includes/api/ApiMoveTest.php b/www/wiki/tests/phpunit/includes/api/ApiMoveTest.php new file mode 100644 index 00000000..fb697ffd --- /dev/null +++ b/www/wiki/tests/phpunit/includes/api/ApiMoveTest.php @@ -0,0 +1,393 @@ +<?php + +/** + * @group API + * @group Database + * @group medium + * + * @covers ApiMove + */ +class ApiMoveTest extends ApiTestCase { + /** + * @param string $from Prefixed name of source + * @param string $to Prefixed name of destination + * @param string $id Page id of the page to move + * @param array|string|null $opts Options: 'noredirect' to expect no redirect + */ + protected function assertMoved( $from, $to, $id, $opts = null ) { + $opts = (array)$opts; + + $fromTitle = Title::newFromText( $from ); + $toTitle = Title::newFromText( $to ); + + $this->assertTrue( $toTitle->exists(), + "Destination {$toTitle->getPrefixedText()} does not exist" ); + + if ( in_array( 'noredirect', $opts ) ) { + $this->assertFalse( $fromTitle->exists(), + "Source {$fromTitle->getPrefixedText()} exists" ); + } else { + $this->assertTrue( $fromTitle->exists(), + "Source {$fromTitle->getPrefixedText()} does not exist" ); + $this->assertTrue( $fromTitle->isRedirect(), + "Source {$fromTitle->getPrefixedText()} is not a redirect" ); + + $target = Revision::newFromTitle( $fromTitle )->getContent()->getRedirectTarget(); + $this->assertSame( $toTitle->getPrefixedText(), $target->getPrefixedText() ); + } + + $this->assertSame( $id, $toTitle->getArticleId() ); + } + + /** + * Shortcut function to create a page and return its id. + * + * @param string $name Page to create + * @return int ID of created page + */ + protected function createPage( $name ) { + return $this->editPage( $name, 'Content' )->value['revision']->getPage(); + } + + public function testFromWithFromid() { + $this->setExpectedException( ApiUsageException::class, + 'The parameters "from" and "fromid" can not be used together.' ); + + $this->doApiRequestWithToken( [ + 'action' => 'move', + 'from' => 'Some page', + 'fromid' => 123, + 'to' => 'Some other page', + ] ); + } + + public function testMove() { + $name = ucfirst( __FUNCTION__ ); + + $id = $this->createPage( $name ); + + $res = $this->doApiRequestWithToken( [ + 'action' => 'move', + 'from' => $name, + 'to' => "$name 2", + ] ); + + $this->assertMoved( $name, "$name 2", $id ); + $this->assertArrayNotHasKey( 'warnings', $res[0] ); + } + + public function testMoveById() { + $name = ucfirst( __FUNCTION__ ); + + $id = $this->createPage( $name ); + + $res = $this->doApiRequestWithToken( [ + 'action' => 'move', + 'fromid' => $id, + 'to' => "$name 2", + ] ); + + $this->assertMoved( $name, "$name 2", $id ); + $this->assertArrayNotHasKey( 'warnings', $res[0] ); + } + + public function testMoveNonexistent() { + $this->setExpectedException( ApiUsageException::class, + "The page you specified doesn't exist." ); + + $this->doApiRequestWithToken( [ + 'action' => 'move', + 'from' => 'Nonexistent page', + 'to' => 'Different page' + ] ); + } + + public function testMoveNonexistentId() { + $this->setExpectedException( ApiUsageException::class, + 'There is no page with ID 2147483647.' ); + + $this->doApiRequestWithToken( [ + 'action' => 'move', + 'fromid' => pow( 2, 31 ) - 1, + 'to' => 'Different page', + ] ); + } + + public function testMoveToInvalidPageName() { + $this->setExpectedException( ApiUsageException::class, 'Bad title "[".' ); + + $name = ucfirst( __FUNCTION__ ); + $id = $this->createPage( $name ); + + try { + $this->doApiRequestWithToken( [ + 'action' => 'move', + 'from' => $name, + 'to' => '[', + ] ); + } finally { + $this->assertSame( $id, Title::newFromText( $name )->getArticleId() ); + } + } + + // @todo File moving + + public function testPingLimiter() { + global $wgRateLimits; + + $this->setExpectedException( ApiUsageException::class, + "You've exceeded your rate limit. Please wait some time and try again." ); + + $name = ucfirst( __FUNCTION__ ); + + $this->setMwGlobals( 'wgMainCacheType', 'hash' ); + + $this->stashMwGlobals( 'wgRateLimits' ); + $wgRateLimits['move'] = [ '&can-bypass' => false, 'user' => [ 1, 60 ] ]; + + $id = $this->createPage( $name ); + + $res = $this->doApiRequestWithToken( [ + 'action' => 'move', + 'from' => $name, + 'to' => "$name 2", + ] ); + + $this->assertMoved( $name, "$name 2", $id ); + $this->assertArrayNotHasKey( 'warnings', $res[0] ); + + try { + $this->doApiRequestWithToken( [ + 'action' => 'move', + 'from' => "$name 2", + 'to' => "$name 3", + ] ); + } finally { + $this->assertSame( $id, Title::newFromText( "$name 2" )->getArticleId() ); + $this->assertFalse( Title::newFromText( "$name 3" )->exists(), + "\"$name 3\" should not exist" ); + } + } + + public function testTagsNoPermission() { + $this->setExpectedException( ApiUsageException::class, + 'You do not have permission to apply change tags along with your changes.' ); + + $name = ucfirst( __FUNCTION__ ); + + ChangeTags::defineTag( 'custom tag' ); + + $this->setGroupPermissions( 'user', 'applychangetags', false ); + + $id = $this->createPage( $name ); + + try { + $this->doApiRequestWithToken( [ + 'action' => 'move', + 'from' => $name, + 'to' => "$name 2", + 'tags' => 'custom tag', + ] ); + } finally { + $this->assertSame( $id, Title::newFromText( $name )->getArticleId() ); + $this->assertFalse( Title::newFromText( "$name 2" )->exists(), + "\"$name 2\" should not exist" ); + } + } + + public function testSelfMove() { + $this->setExpectedException( ApiUsageException::class, + 'The title is the same; cannot move a page over itself.' ); + + $name = ucfirst( __FUNCTION__ ); + $this->createPage( $name ); + + $this->doApiRequestWithToken( [ + 'action' => 'move', + 'from' => $name, + 'to' => $name, + ] ); + } + + public function testMoveTalk() { + $name = ucfirst( __FUNCTION__ ); + + $id = $this->createPage( $name ); + $talkId = $this->createPage( "Talk:$name" ); + + $res = $this->doApiRequestWithToken( [ + 'action' => 'move', + 'from' => $name, + 'to' => "$name 2", + 'movetalk' => '', + ] ); + + $this->assertMoved( $name, "$name 2", $id ); + $this->assertMoved( "Talk:$name", "Talk:$name 2", $talkId ); + + $this->assertArrayNotHasKey( 'warnings', $res[0] ); + } + + public function testMoveTalkFailed() { + $name = ucfirst( __FUNCTION__ ); + + $id = $this->createPage( $name ); + $talkId = $this->createPage( "Talk:$name" ); + $talkDestinationId = $this->createPage( "Talk:$name 2" ); + + $res = $this->doApiRequestWithToken( [ + 'action' => 'move', + 'from' => $name, + 'to' => "$name 2", + 'movetalk' => '', + ] ); + + $this->assertMoved( $name, "$name 2", $id ); + $this->assertSame( $talkId, Title::newFromText( "Talk:$name" )->getArticleId() ); + $this->assertSame( $talkDestinationId, + Title::newFromText( "Talk:$name 2" )->getArticleId() ); + $this->assertSame( [ [ + 'message' => 'articleexists', + 'params' => [], + 'code' => 'articleexists', + 'type' => 'error', + ] ], $res[0]['move']['talkmove-errors'] ); + + $this->assertArrayNotHasKey( 'warnings', $res[0] ); + } + + public function testMoveSubpages() { + global $wgNamespacesWithSubpages; + + $name = ucfirst( __FUNCTION__ ); + + $this->stashMwGlobals( 'wgNamespacesWithSubpages' ); + $wgNamespacesWithSubpages[NS_MAIN] = true; + + $pages = [ $name, "$name/1", "$name/2", "Talk:$name", "Talk:$name/1", "Talk:$name/3" ]; + $ids = []; + foreach ( array_merge( $pages, [ "$name/error", "$name 2/error" ] ) as $page ) { + $ids[$page] = $this->createPage( $page ); + } + + $res = $this->doApiRequestWithToken( [ + 'action' => 'move', + 'from' => $name, + 'to' => "$name 2", + 'movetalk' => '', + 'movesubpages' => '', + ] ); + + foreach ( $pages as $page ) { + $this->assertMoved( $page, str_replace( $name, "$name 2", $page ), $ids[$page] ); + } + + $this->assertSame( $ids["$name/error"], + Title::newFromText( "$name/error" )->getArticleId() ); + $this->assertSame( $ids["$name 2/error"], + Title::newFromText( "$name 2/error" )->getArticleId() ); + + $results = array_merge( $res[0]['move']['subpages'], $res[0]['move']['subpages-talk'] ); + foreach ( $results as $arr ) { + if ( $arr['from'] === "$name/error" ) { + $this->assertSame( [ [ + 'message' => 'articleexists', + 'params' => [], + 'code' => 'articleexists', + 'type' => 'error' + ] ], $arr['errors'] ); + } else { + $this->assertSame( str_replace( $name, "$name 2", $arr['from'] ), $arr['to'] ); + } + $this->assertCount( 2, $arr ); + } + + $this->assertArrayNotHasKey( 'warnings', $res[0] ); + } + + public function testMoveNoPermission() { + $this->setExpectedException( ApiUsageException::class, + 'You must be a registered user and [[Special:UserLogin|logged in]] to move a page.' ); + + $name = ucfirst( __FUNCTION__ ); + + $id = $this->createPage( $name ); + + $user = new User(); + + try { + $this->doApiRequestWithToken( [ + 'action' => 'move', + 'from' => $name, + 'to' => "$name 2", + ], null, $user ); + } finally { + $this->assertSame( $id, Title::newFromText( "$name" )->getArticleId() ); + $this->assertFalse( Title::newFromText( "$name 2" )->exists(), + "\"$name 2\" should not exist" ); + } + } + + public function testSuppressRedirect() { + $name = ucfirst( __FUNCTION__ ); + + $id = $this->createPage( $name ); + + $res = $this->doApiRequestWithToken( [ + 'action' => 'move', + 'from' => $name, + 'to' => "$name 2", + 'noredirect' => '', + ] ); + + $this->assertMoved( $name, "$name 2", $id, 'noredirect' ); + $this->assertArrayNotHasKey( 'warnings', $res[0] ); + } + + public function testSuppressRedirectNoPermission() { + $name = ucfirst( __FUNCTION__ ); + + $this->setGroupPermissions( 'sysop', 'suppressredirect', false ); + + $id = $this->createPage( $name ); + + $res = $this->doApiRequestWithToken( [ + 'action' => 'move', + 'from' => $name, + 'to' => "$name 2", + 'noredirect' => '', + ] ); + + $this->assertMoved( $name, "$name 2", $id ); + $this->assertArrayNotHasKey( 'warnings', $res[0] ); + } + + public function testMoveSubpagesError() { + $name = ucfirst( __FUNCTION__ ); + + // Subpages are allowed in talk but not main + $idBase = $this->createPage( "Talk:$name" ); + $idSub = $this->createPage( "Talk:$name/1" ); + + $res = $this->doApiRequestWithToken( [ + 'action' => 'move', + 'from' => "Talk:$name", + 'to' => $name, + 'movesubpages' => '', + ] ); + + $this->assertMoved( "Talk:$name", $name, $idBase ); + $this->assertSame( $idSub, Title::newFromText( "Talk:$name/1" )->getArticleId() ); + $this->assertFalse( Title::newFromText( "$name/1" )->exists(), + "\"$name/1\" should not exist" ); + + $this->assertSame( [ 'errors' => [ [ + 'message' => 'namespace-nosubpages', + 'params' => [ '' ], + 'code' => 'namespace-nosubpages', + 'type' => 'error', + ] ] ], $res[0]['move']['subpages'] ); + + $this->assertArrayNotHasKey( 'warnings', $res[0] ); + } +} diff --git a/www/wiki/tests/phpunit/includes/api/ApiOpenSearchTest.php b/www/wiki/tests/phpunit/includes/api/ApiOpenSearchTest.php index 23fa7bcb..209ca07b 100644 --- a/www/wiki/tests/phpunit/includes/api/ApiOpenSearchTest.php +++ b/www/wiki/tests/phpunit/includes/api/ApiOpenSearchTest.php @@ -1,5 +1,8 @@ <?php +/** + * @covers ApiOpenSearch + */ class ApiOpenSearchTest extends MediaWikiTestCase { public function testGetAllowedParams() { $config = $this->replaceSearchEngineConfig(); @@ -33,7 +36,7 @@ class ApiOpenSearchTest extends MediaWikiTestCase { } private function replaceSearchEngineConfig() { - $config = $this->getMockBuilder( 'SearchEngineConfig' ) + $config = $this->getMockBuilder( SearchEngineConfig::class ) ->disableOriginalConstructor() ->getMock(); $this->setService( 'SearchEngineConfig', $config ); @@ -42,10 +45,10 @@ class ApiOpenSearchTest extends MediaWikiTestCase { } private function replaceSearchEngine() { - $engine = $this->getMockBuilder( 'SearchEngine' ) + $engine = $this->getMockBuilder( SearchEngine::class ) ->disableOriginalConstructor() ->getMock(); - $engineFactory = $this->getMockBuilder( 'SearchEngineFactory' ) + $engineFactory = $this->getMockBuilder( SearchEngineFactory::class ) ->disableOriginalConstructor() ->getMock(); $engineFactory->expects( $this->any() ) diff --git a/www/wiki/tests/phpunit/includes/api/ApiOptionsTest.php b/www/wiki/tests/phpunit/includes/api/ApiOptionsTest.php index ef706261..c0fecf06 100644 --- a/www/wiki/tests/phpunit/includes/api/ApiOptionsTest.php +++ b/www/wiki/tests/phpunit/includes/api/ApiOptionsTest.php @@ -22,7 +22,7 @@ class ApiOptionsTest extends MediaWikiLangTestCase { protected function setUp() { parent::setUp(); - $this->mUserMock = $this->getMockBuilder( 'User' ) + $this->mUserMock = $this->getMockBuilder( User::class ) ->disableOriginalConstructor() ->getMock(); @@ -278,26 +278,13 @@ class ApiOptionsTest extends MediaWikiLangTestCase { $this->mUserMock->expects( $this->never() ) ->method( 'resetOptions' ); - $this->mUserMock->expects( $this->at( 2 ) ) - ->method( 'getOptions' ); - - $this->mUserMock->expects( $this->at( 5 ) ) + $this->mUserMock->expects( $this->exactly( 3 ) ) ->method( 'setOption' ) - ->with( $this->equalTo( 'willBeNull' ), $this->identicalTo( null ) ); - - $this->mUserMock->expects( $this->at( 6 ) ) - ->method( 'getOptions' ); - - $this->mUserMock->expects( $this->at( 7 ) ) - ->method( 'setOption' ) - ->with( $this->equalTo( 'willBeEmpty' ), $this->equalTo( '' ) ); - - $this->mUserMock->expects( $this->at( 8 ) ) - ->method( 'getOptions' ); - - $this->mUserMock->expects( $this->at( 9 ) ) - ->method( 'setOption' ) - ->with( $this->equalTo( 'willBeHappy' ), $this->equalTo( 'Happy' ) ); + ->withConsecutive( + [ $this->equalTo( 'willBeNull' ), $this->identicalTo( null ) ], + [ $this->equalTo( 'willBeEmpty' ), $this->equalTo( '' ) ], + [ $this->equalTo( 'willBeHappy' ), $this->equalTo( 'Happy' ) ] + ); $this->mUserMock->expects( $this->once() ) ->method( 'saveSettings' ); @@ -315,19 +302,12 @@ class ApiOptionsTest extends MediaWikiLangTestCase { $this->mUserMock->expects( $this->once() ) ->method( 'resetOptions' ); - $this->mUserMock->expects( $this->at( 5 ) ) - ->method( 'getOptions' ); - - $this->mUserMock->expects( $this->at( 6 ) ) - ->method( 'setOption' ) - ->with( $this->equalTo( 'willBeHappy' ), $this->equalTo( 'Happy' ) ); - - $this->mUserMock->expects( $this->at( 7 ) ) - ->method( 'getOptions' ); - - $this->mUserMock->expects( $this->at( 8 ) ) + $this->mUserMock->expects( $this->exactly( 2 ) ) ->method( 'setOption' ) - ->with( $this->equalTo( 'name' ), $this->equalTo( 'value' ) ); + ->withConsecutive( + [ $this->equalTo( 'willBeHappy' ), $this->equalTo( 'Happy' ) ], + [ $this->equalTo( 'name' ), $this->equalTo( 'value' ) ] + ); $this->mUserMock->expects( $this->once() ) ->method( 'saveSettings' ); @@ -348,21 +328,14 @@ class ApiOptionsTest extends MediaWikiLangTestCase { $this->mUserMock->expects( $this->never() ) ->method( 'resetOptions' ); - $this->mUserMock->expects( $this->at( 4 ) ) - ->method( 'setOption' ) - ->with( $this->equalTo( 'testmultiselect-opt1' ), $this->identicalTo( true ) ); - - $this->mUserMock->expects( $this->at( 5 ) ) - ->method( 'setOption' ) - ->with( $this->equalTo( 'testmultiselect-opt2' ), $this->identicalTo( null ) ); - - $this->mUserMock->expects( $this->at( 6 ) ) - ->method( 'setOption' ) - ->with( $this->equalTo( 'testmultiselect-opt3' ), $this->identicalTo( false ) ); - - $this->mUserMock->expects( $this->at( 7 ) ) + $this->mUserMock->expects( $this->exactly( 4 ) ) ->method( 'setOption' ) - ->with( $this->equalTo( 'testmultiselect-opt4' ), $this->identicalTo( false ) ); + ->withConsecutive( + [ $this->equalTo( 'testmultiselect-opt1' ), $this->identicalTo( true ) ], + [ $this->equalTo( 'testmultiselect-opt2' ), $this->identicalTo( null ) ], + [ $this->equalTo( 'testmultiselect-opt3' ), $this->identicalTo( false ) ], + [ $this->equalTo( 'testmultiselect-opt4' ), $this->identicalTo( false ) ] + ); $this->mUserMock->expects( $this->once() ) ->method( 'saveSettings' ); diff --git a/www/wiki/tests/phpunit/includes/api/ApiPageSetTest.php b/www/wiki/tests/phpunit/includes/api/ApiPageSetTest.php index 1aa0a133..b9e4645d 100644 --- a/www/wiki/tests/phpunit/includes/api/ApiPageSetTest.php +++ b/www/wiki/tests/phpunit/includes/api/ApiPageSetTest.php @@ -4,6 +4,7 @@ * @group API * @group medium * @group Database + * @covers ApiPageSet */ class ApiPageSetTest extends ApiTestCase { public static function provideRedirectMergePolicy() { @@ -107,7 +108,7 @@ class ApiPageSetTest extends ApiTestCase { $userName = $user->getName(); $userDbkey = str_replace( ' ', '_', $userName ); $request = new FauxRequest( [ - 'titles' => join( '|', [ + 'titles' => implode( '|', [ 'Special:MyContributions', 'Special:MyPage', 'Special:MyTalk/subpage', diff --git a/www/wiki/tests/phpunit/includes/api/ApiParseTest.php b/www/wiki/tests/phpunit/includes/api/ApiParseTest.php index 028d3b41..a04271f6 100644 --- a/www/wiki/tests/phpunit/includes/api/ApiParseTest.php +++ b/www/wiki/tests/phpunit/includes/api/ApiParseTest.php @@ -13,48 +13,136 @@ class ApiParseTest extends ApiTestCase { protected static $revIds = []; public function addDBDataOnce() { - $user = static::getTestSysop()->getUser(); $title = Title::newFromText( __CLASS__ ); - $page = WikiPage::factory( $title ); - $status = $page->doEditContent( - ContentHandler::makeContent( 'Test for revdel', $title, CONTENT_MODEL_WIKITEXT ), - __METHOD__ . ' Test for revdel', 0, false, $user - ); - if ( !$status->isOk() ) { - $this->fail( "Failed to create $title: " . $status->getWikiText( false, false, 'en' ) ); - } + $status = $this->editPage( __CLASS__, 'Test for revdel' ); self::$pageId = $status->value['revision']->getPage(); self::$revIds['revdel'] = $status->value['revision']->getId(); - $status = $page->doEditContent( - ContentHandler::makeContent( 'Test for oldid', $title, CONTENT_MODEL_WIKITEXT ), - __METHOD__ . ' Test for oldid', 0, false, $user - ); - if ( !$status->isOk() ) { - $this->fail( "Failed to edit $title: " . $status->getWikiText( false, false, 'en' ) ); - } + $status = $this->editPage( __CLASS__, 'Test for suppressed' ); + self::$revIds['suppressed'] = $status->value['revision']->getId(); + + $status = $this->editPage( __CLASS__, 'Test for oldid' ); self::$revIds['oldid'] = $status->value['revision']->getId(); - $status = $page->doEditContent( - ContentHandler::makeContent( 'Test for latest', $title, CONTENT_MODEL_WIKITEXT ), - __METHOD__ . ' Test for latest', 0, false, $user + $status = $this->editPage( __CLASS__, 'Test for latest' ); + self::$revIds['latest'] = $status->value['revision']->getId(); + + $this->revisionDelete( self::$revIds['revdel'] ); + $this->revisionDelete( + self::$revIds['suppressed'], + [ Revision::DELETED_TEXT => 1, Revision::DELETED_RESTRICTED => 1 ] ); - if ( !$status->isOk() ) { - $this->fail( "Failed to edit $title: " . $status->getWikiText( false, false, 'en' ) ); + + Title::clearCaches(); // Otherwise it has the wrong latest revision for some reason + } + + /** + * Assert that the given result of calling $this->doApiRequest() with + * action=parse resulted in $html, accounting for the boilerplate that the + * parser adds around the parsed page. Also asserts that warnings match + * the provided $warning. + * + * @param string $html Expected HTML + * @param array $res Returned from doApiRequest() + * @param string|null $warnings Exact value of expected warnings, null for + * no warnings + */ + protected function assertParsedTo( $expected, array $res, $warnings = null ) { + $this->doAssertParsedTo( $expected, $res, $warnings, [ $this, 'assertSame' ] ); + } + + /** + * Same as above, but asserts that the HTML matches a regexp instead of a + * literal string match. + * + * @param string $html Expected HTML + * @param array $res Returned from doApiRequest() + * @param string|null $warnings Exact value of expected warnings, null for + * no warnings + */ + protected function assertParsedToRegExp( $expected, array $res, $warnings = null ) { + $this->doAssertParsedTo( $expected, $res, $warnings, [ $this, 'assertRegExp' ] ); + } + + private function doAssertParsedTo( $expected, array $res, $warnings, callable $callback ) { + $html = $res[0]['parse']['text']; + + $expectedStart = '<div class="mw-parser-output">'; + $this->assertSame( $expectedStart, substr( $html, 0, strlen( $expectedStart ) ) ); + + $html = substr( $html, strlen( $expectedStart ) ); + + if ( $res[1]->getBool( 'disablelimitreport' ) ) { + $expectedEnd = "</div>"; + $this->assertSame( $expectedEnd, substr( $html, -strlen( $expectedEnd ) ) ); + + $html = substr( $html, 0, strlen( $html ) - strlen( $expectedEnd ) ); + } else { + $expectedEnd = '#\n<!-- \nNewPP limit report\n(?>.+?\n-->)\n' . + '<!--\nTransclusion expansion time report \(%,ms,calls,template\)\n(?>.*?\n-->)\n' . + '</div>(\n<!-- Saved in parser cache (?>.*?\n -->)\n)?$#s'; + $this->assertRegExp( $expectedEnd, $html ); + + $html = preg_replace( $expectedEnd, '', $html ); } - self::$revIds['latest'] = $status->value['revision']->getId(); - RevisionDeleter::createList( - 'revision', RequestContext::getMain(), $title, [ self::$revIds['revdel'] ] - )->setVisibility( [ - 'value' => [ - Revision::DELETED_TEXT => 1, + call_user_func( $callback, $expected, $html ); + + if ( $warnings === null ) { + $this->assertCount( 1, $res[0] ); + } else { + $this->assertCount( 2, $res[0] ); + // This deliberately fails if there are extra warnings + $this->assertSame( [ 'parse' => [ 'warnings' => $warnings ] ], $res[0]['warnings'] ); + } + } + + /** + * Set up an interwiki entry for testing. + */ + protected function setupInterwiki() { + $dbw = wfGetDB( DB_MASTER ); + $dbw->insert( + 'interwiki', + [ + 'iw_prefix' => 'madeuplanguage', + 'iw_url' => "https://example.com/wiki/$1", + 'iw_api' => '', + 'iw_wikiid' => '', + 'iw_local' => false, ], - 'comment' => 'Test for revdel', - ] ); + __METHOD__, + 'IGNORE' + ); - Title::clearCaches(); // Otherwise it has the wrong latest revision for some reason + $this->setMwGlobals( 'wgExtraInterlanguageLinkPrefixes', [ 'madeuplanguage' ] ); + $this->tablesUsed[] = 'interwiki'; + } + + /** + * Set up a skin for testing. + * + * @todo Should this code be in MediaWikiTestCase or something? + */ + protected function setupSkin() { + $factory = new SkinFactory(); + $factory->register( 'testing', 'Testing', function () { + $skin = $this->getMockBuilder( SkinFallback::class ) + ->setMethods( [ 'getDefaultModules', 'setupSkinUserCss' ] ) + ->getMock(); + $skin->expects( $this->once() )->method( 'getDefaultModules' ) + ->willReturn( [ + 'core' => [ 'foo', 'bar' ], + 'content' => [ 'baz' ] + ] ); + $skin->expects( $this->once() )->method( 'setupSkinUserCss' ) + ->will( $this->returnCallback( function ( OutputPage $out ) { + $out->addModuleStyles( 'foo.styles' ); + } ) ); + return $skin; + } ); + $this->setService( 'SkinFactory', $factory ); } public function testParseByName() { @@ -62,14 +150,14 @@ class ApiParseTest extends ApiTestCase { 'action' => 'parse', 'page' => __CLASS__, ] ); - $this->assertContains( 'Test for latest', $res[0]['parse']['text'] ); + $this->assertParsedTo( "<p>Test for latest\n</p>", $res ); $res = $this->doApiRequest( [ 'action' => 'parse', 'page' => __CLASS__, 'disablelimitreport' => 1, ] ); - $this->assertContains( 'Test for latest', $res[0]['parse']['text'] ); + $this->assertParsedTo( "<p>Test for latest\n</p>", $res ); } public function testParseById() { @@ -77,7 +165,7 @@ class ApiParseTest extends ApiTestCase { 'action' => 'parse', 'pageid' => self::$pageId, ] ); - $this->assertContains( 'Test for latest', $res[0]['parse']['text'] ); + $this->assertParsedTo( "<p>Test for latest\n</p>", $res ); } public function testParseByOldId() { @@ -85,36 +173,46 @@ class ApiParseTest extends ApiTestCase { 'action' => 'parse', 'oldid' => self::$revIds['oldid'], ] ); - $this->assertContains( 'Test for oldid', $res[0]['parse']['text'] ); + $this->assertParsedTo( "<p>Test for oldid\n</p>", $res ); $this->assertArrayNotHasKey( 'textdeleted', $res[0]['parse'] ); $this->assertArrayNotHasKey( 'textsuppressed', $res[0]['parse'] ); } - public function testParseRevDel() { - $user = static::getTestUser()->getUser(); - $sysop = static::getTestSysop()->getUser(); - - try { - $this->doApiRequest( [ - 'action' => 'parse', - 'oldid' => self::$revIds['revdel'], - ], null, null, $user ); - $this->fail( "API did not return an error as expected" ); - } catch ( ApiUsageException $ex ) { - $this->assertTrue( ApiTestCase::apiExceptionHasCode( $ex, 'permissiondenied' ), - "API failed with error 'permissiondenied'" ); - } - + public function testRevDel() { $res = $this->doApiRequest( [ 'action' => 'parse', 'oldid' => self::$revIds['revdel'], - ], null, null, $sysop ); - $this->assertContains( 'Test for revdel', $res[0]['parse']['text'] ); + ] ); + + $this->assertParsedTo( "<p>Test for revdel\n</p>", $res ); $this->assertArrayHasKey( 'textdeleted', $res[0]['parse'] ); $this->assertArrayNotHasKey( 'textsuppressed', $res[0]['parse'] ); } - public function testParseNonexistentPage() { + public function testRevDelNoPermission() { + $this->setExpectedException( ApiUsageException::class, + "You don't have permission to view deleted revision text." ); + + $this->doApiRequest( [ + 'action' => 'parse', + 'oldid' => self::$revIds['revdel'], + ], null, null, static::getTestUser()->getUser() ); + } + + public function testSuppressed() { + $this->setGroupPermissions( 'sysop', 'viewsuppressed', true ); + + $res = $this->doApiRequest( [ + 'action' => 'parse', + 'oldid' => self::$revIds['suppressed'] + ] ); + + $this->assertParsedTo( "<p>Test for suppressed\n</p>", $res ); + $this->assertArrayHasKey( 'textsuppressed', $res[0]['parse'] ); + $this->assertArrayHasKey( 'textdeleted', $res[0]['parse'] ); + } + + public function testNonexistentPage() { try { $this->doApiRequest( [ 'action' => 'parse', @@ -129,4 +227,623 @@ class ApiParseTest extends ApiTestCase { ); } } + + public function testTitleProvided() { + $res = $this->doApiRequest( [ + 'action' => 'parse', + 'title' => 'Some interesting page', + 'text' => '{{PAGENAME}} has attracted my attention', + ] ); + + $this->assertParsedTo( "<p>Some interesting page has attracted my attention\n</p>", $res ); + } + + public function testSection() { + $name = ucfirst( __FUNCTION__ ); + + $this->editPage( $name, + "Intro\n\n== Section 1 ==\n\nContent 1\n\n== Section 2 ==\n\nContent 2" ); + + $res = $this->doApiRequest( [ + 'action' => 'parse', + 'page' => $name, + 'section' => 1, + ] ); + + $this->assertParsedToRegExp( '!<h2>.*Section 1.*</h2>\n<p>Content 1\n</p>!', $res ); + } + + public function testInvalidSection() { + $this->setExpectedException( ApiUsageException::class, + 'The "section" parameter must be a valid section ID or "new".' ); + + $this->doApiRequest( [ + 'action' => 'parse', + 'section' => 'T-new', + ] ); + } + + public function testSectionNoContent() { + $name = ucfirst( __FUNCTION__ ); + + $status = $this->editPage( $name, + "Intro\n\n== Section 1 ==\n\nContent 1\n\n== Section 2 ==\n\nContent 2" ); + + $this->setExpectedException( ApiUsageException::class, + "Missing content for page ID {$status->value['revision']->getPage()}." ); + + $this->db->delete( 'revision', [ 'rev_id' => $status->value['revision']->getId() ] ); + + // Suppress warning in WikiPage::getContentModel + Wikimedia\suppressWarnings(); + try { + $this->doApiRequest( [ + 'action' => 'parse', + 'page' => $name, + 'section' => 1, + ] ); + } finally { + Wikimedia\restoreWarnings(); + } + } + + public function testNewSectionWithPage() { + $this->setExpectedException( ApiUsageException::class, + '"section=new" cannot be combined with the "oldid", "pageid" or "page" ' . + 'parameters. Please use "title" and "text".' ); + + $this->doApiRequest( [ + 'action' => 'parse', + 'page' => __CLASS__, + 'section' => 'new', + ] ); + } + + public function testNonexistentOldId() { + $this->setExpectedException( ApiUsageException::class, + 'There is no revision with ID 2147483647.' ); + + $this->doApiRequest( [ + 'action' => 'parse', + 'oldid' => pow( 2, 31 ) - 1, + ] ); + } + + public function testUnfollowedRedirect() { + $name = ucfirst( __FUNCTION__ ); + + $this->editPage( $name, "#REDIRECT [[$name 2]]" ); + $this->editPage( "$name 2", "Some ''text''" ); + + $res = $this->doApiRequest( [ + 'action' => 'parse', + 'page' => $name, + ] ); + + // Can't use assertParsedTo because the parser output is different for + // redirects + $this->assertRegExp( "/Redirect to:.*$name 2/", $res[0]['parse']['text'] ); + $this->assertArrayNotHasKey( 'warnings', $res[0] ); + } + + public function testFollowedRedirect() { + $name = ucfirst( __FUNCTION__ ); + + $this->editPage( $name, "#REDIRECT [[$name 2]]" ); + $this->editPage( "$name 2", "Some ''text''" ); + + $res = $this->doApiRequest( [ + 'action' => 'parse', + 'page' => $name, + 'redirects' => true, + ] ); + + $this->assertParsedTo( "<p>Some <i>text</i>\n</p>", $res ); + } + + public function testFollowedRedirectById() { + $name = ucfirst( __FUNCTION__ ); + + $id = $this->editPage( $name, "#REDIRECT [[$name 2]]" )->value['revision']->getPage(); + $this->editPage( "$name 2", "Some ''text''" ); + + $res = $this->doApiRequest( [ + 'action' => 'parse', + 'pageid' => $id, + 'redirects' => true, + ] ); + + $this->assertParsedTo( "<p>Some <i>text</i>\n</p>", $res ); + } + + public function testInvalidTitle() { + $this->setExpectedException( ApiUsageException::class, 'Bad title "|".' ); + + $this->doApiRequest( [ + 'action' => 'parse', + 'title' => '|', + ] ); + } + + public function testTitleWithNonexistentRevId() { + $this->setExpectedException( ApiUsageException::class, + 'There is no revision with ID 2147483647.' ); + + $this->doApiRequest( [ + 'action' => 'parse', + 'title' => __CLASS__, + 'revid' => pow( 2, 31 ) - 1, + ] ); + } + + public function testTitleWithNonMatchingRevId() { + $name = ucfirst( __FUNCTION__ ); + + $res = $this->doApiRequest( [ + 'action' => 'parse', + 'title' => $name, + 'revid' => self::$revIds['latest'], + 'text' => 'Some text', + ] ); + + $this->assertParsedTo( "<p>Some text\n</p>", $res, + 'r' . self::$revIds['latest'] . " is not a revision of $name." ); + } + + public function testRevId() { + $res = $this->doApiRequest( [ + 'action' => 'parse', + 'revid' => self::$revIds['latest'], + 'text' => 'My revid is {{REVISIONID}}!', + ] ); + + $this->assertParsedTo( "<p>My revid is " . self::$revIds['latest'] . "!\n</p>", $res ); + } + + public function testTitleNoText() { + $res = $this->doApiRequest( [ + 'action' => 'parse', + 'title' => 'Special:AllPages', + ] ); + + $this->assertParsedTo( '', $res, + '"title" used without "text", and parsed page properties were requested. ' . + 'Did you mean to use "page" instead of "title"?' ); + } + + public function testRevidNoText() { + $res = $this->doApiRequest( [ + 'action' => 'parse', + 'revid' => self::$revIds['latest'], + ] ); + + $this->assertParsedTo( '', $res, + '"revid" used without "text", and parsed page properties were requested. ' . + 'Did you mean to use "oldid" instead of "revid"?' ); + } + + public function testTextNoContentModel() { + $res = $this->doApiRequest( [ + 'action' => 'parse', + 'text' => "Some ''text''", + ] ); + + $this->assertParsedTo( "<p>Some <i>text</i>\n</p>", $res, + 'No "title" or "contentmodel" was given, assuming wikitext.' ); + } + + public function testSerializationError() { + $this->setExpectedException( APIUsageException::class, + 'Content serialization failed: Could not unserialize content' ); + + $this->mergeMwGlobalArrayValue( 'wgContentHandlers', + [ 'testing-serialize-error' => 'DummySerializeErrorContentHandler' ] ); + + $this->doApiRequest( [ + 'action' => 'parse', + 'text' => "Some ''text''", + 'contentmodel' => 'testing-serialize-error', + ] ); + } + + public function testNewSection() { + $res = $this->doApiRequest( [ + 'action' => 'parse', + 'title' => __CLASS__, + 'section' => 'new', + 'sectiontitle' => 'Title', + 'text' => 'Content', + ] ); + + $this->assertParsedToRegExp( '!<h2>.*Title.*</h2>\n<p>Content\n</p>!', $res ); + } + + public function testExistingSection() { + $res = $this->doApiRequest( [ + 'action' => 'parse', + 'title' => __CLASS__, + 'section' => 1, + 'text' => "Intro\n\n== Section 1 ==\n\nContent\n\n== Section 2 ==\n\nMore content", + ] ); + + $this->assertParsedToRegExp( '!<h2>.*Section 1.*</h2>\n<p>Content\n</p>!', $res ); + } + + public function testNoPst() { + $name = ucfirst( __FUNCTION__ ); + + $this->editPage( "Template:$name", "Template ''text''" ); + + $res = $this->doApiRequest( [ + 'action' => 'parse', + 'text' => "{{subst:$name}}", + 'contentmodel' => 'wikitext', + ] ); + + $this->assertParsedTo( "<p>{{subst:$name}}\n</p>", $res ); + } + + public function testPst() { + $name = ucfirst( __FUNCTION__ ); + + $this->editPage( "Template:$name", "Template ''text''" ); + + $res = $this->doApiRequest( [ + 'action' => 'parse', + 'pst' => '', + 'text' => "{{subst:$name}}", + 'contentmodel' => 'wikitext', + 'prop' => 'text|wikitext', + ] ); + + $this->assertParsedTo( "<p>Template <i>text</i>\n</p>", $res ); + $this->assertSame( "{{subst:$name}}", $res[0]['parse']['wikitext'] ); + } + + public function testOnlyPst() { + $name = ucfirst( __FUNCTION__ ); + + $this->editPage( "Template:$name", "Template ''text''" ); + + $res = $this->doApiRequest( [ + 'action' => 'parse', + 'onlypst' => '', + 'text' => "{{subst:$name}}", + 'contentmodel' => 'wikitext', + 'prop' => 'text|wikitext', + 'summary' => 'Summary', + ] ); + + $this->assertSame( + [ 'parse' => [ + 'text' => "Template ''text''", + 'wikitext' => "{{subst:$name}}", + 'parsedsummary' => 'Summary', + ] ], + $res[0] + ); + } + + public function testHeadHtml() { + $res = $this->doApiRequest( [ + 'action' => 'parse', + 'page' => __CLASS__, + 'prop' => 'headhtml', + ] ); + + // Just do a rough sanity check + $this->assertRegExp( '#<!DOCTYPE.*<html.*<head.*</head>.*<body#s', + $res[0]['parse']['headhtml'] ); + $this->assertArrayNotHasKey( 'warnings', $res[0] ); + } + + public function testCategoriesHtml() { + $name = ucfirst( __FUNCTION__ ); + + $this->editPage( $name, "[[Category:$name]]" ); + + $res = $this->doApiRequest( [ + 'action' => 'parse', + 'page' => $name, + 'prop' => 'categorieshtml', + ] ); + + $this->assertRegExp( "#Category.*Category:$name.*$name#", + $res[0]['parse']['categorieshtml'] ); + $this->assertArrayNotHasKey( 'warnings', $res[0] ); + } + + public function testEffectiveLangLinks() { + $hookRan = false; + $this->setTemporaryHook( 'LanguageLinks', + function () use ( &$hookRan ) { + $hookRan = true; + } + ); + + $res = $this->doApiRequest( [ + 'action' => 'parse', + 'title' => __CLASS__, + 'text' => '[[zh:' . __CLASS__ . ']]', + 'effectivelanglinks' => '', + ] ); + + $this->assertTrue( $hookRan ); + $this->assertSame( 'The parameter "effectivelanglinks" has been deprecated.', + $res[0]['warnings']['parse']['warnings'] ); + } + + /** + * @param array $arr Extra params to add to API request + */ + private function doTestLangLinks( array $arr = [] ) { + $this->setupInterwiki(); + + $res = $this->doApiRequest( array_merge( [ + 'action' => 'parse', + 'title' => 'Omelette', + 'text' => '[[madeuplanguage:Omelette]]', + 'prop' => 'langlinks', + ], $arr ) ); + + $langLinks = $res[0]['parse']['langlinks']; + + $this->assertCount( 1, $langLinks ); + $this->assertSame( 'madeuplanguage', $langLinks[0]['lang'] ); + $this->assertSame( 'Omelette', $langLinks[0]['title'] ); + $this->assertSame( 'https://example.com/wiki/Omelette', $langLinks[0]['url'] ); + $this->assertArrayNotHasKey( 'warnings', $res[0] ); + } + + public function testLangLinks() { + $this->doTestLangLinks(); + } + + public function testLangLinksWithSkin() { + $this->setupSkin(); + $this->doTestLangLinks( [ 'useskin' => 'testing' ] ); + } + + public function testHeadItems() { + $res = $this->doApiRequest( [ + 'action' => 'parse', + 'title' => __CLASS__, + 'text' => '', + 'prop' => 'headitems', + ] ); + + $this->assertSame( [], $res[0]['parse']['headitems'] ); + $this->assertSame( + '"prop=headitems" is deprecated since MediaWiki 1.28. ' . + 'Use "prop=headhtml" when creating new HTML documents, ' . + 'or "prop=modules|jsconfigvars" when updating a document client-side.', + $res[0]['warnings']['parse']['warnings'] + ); + } + + public function testHeadItemsWithSkin() { + $this->setupSkin(); + + $res = $this->doApiRequest( [ + 'action' => 'parse', + 'title' => __CLASS__, + 'text' => '', + 'prop' => 'headitems', + 'useskin' => 'testing', + ] ); + + $this->assertSame( [], $res[0]['parse']['headitems'] ); + $this->assertSame( + '"prop=headitems" is deprecated since MediaWiki 1.28. ' . + 'Use "prop=headhtml" when creating new HTML documents, ' . + 'or "prop=modules|jsconfigvars" when updating a document client-side.', + $res[0]['warnings']['parse']['warnings'] + ); + } + + public function testModules() { + $this->setTemporaryHook( 'ParserAfterParse', + function ( $parser ) { + $output = $parser->getOutput(); + $output->addModules( [ 'foo', 'bar' ] ); + $output->addModuleScripts( [ 'baz', 'quuz' ] ); + $output->addModuleStyles( [ 'aaa', 'zzz' ] ); + $output->addJsConfigVars( [ 'x' => 'y', 'z' => -3 ] ); + } + ); + $res = $this->doApiRequest( [ + 'action' => 'parse', + 'title' => __CLASS__, + 'text' => 'Content', + 'prop' => 'modules|jsconfigvars|encodedjsconfigvars', + ] ); + + $this->assertSame( [ 'foo', 'bar' ], $res[0]['parse']['modules'] ); + $this->assertSame( [ 'baz', 'quuz' ], $res[0]['parse']['modulescripts'] ); + $this->assertSame( [ 'aaa', 'zzz' ], $res[0]['parse']['modulestyles'] ); + $this->assertSame( [ 'x' => 'y', 'z' => -3 ], $res[0]['parse']['jsconfigvars'] ); + $this->assertSame( '{"x":"y","z":-3}', $res[0]['parse']['encodedjsconfigvars'] ); + $this->assertArrayNotHasKey( 'warnings', $res[0] ); + } + + public function testModulesWithSkin() { + $this->setupSkin(); + + $res = $this->doApiRequest( [ + 'action' => 'parse', + 'pageid' => self::$pageId, + 'useskin' => 'testing', + 'prop' => 'modules', + ] ); + $this->assertSame( + [ 'foo', 'bar', 'baz' ], + $res[0]['parse']['modules'], + 'resp.parse.modules' + ); + $this->assertSame( + [], + $res[0]['parse']['modulescripts'], + 'resp.parse.modulescripts' + ); + $this->assertSame( + [ 'foo.styles' ], + $res[0]['parse']['modulestyles'], + 'resp.parse.modulestyles' + ); + $this->assertSame( + [ 'parse' => + [ 'warnings' => + 'Property "modules" was set but not "jsconfigvars" or ' . + '"encodedjsconfigvars". Configuration variables are necessary for ' . + 'proper module usage.' + ] + ], + $res[0]['warnings'] + ); + } + + public function testIndicators() { + $res = $this->doApiRequest( [ + 'action' => 'parse', + 'title' => __CLASS__, + 'text' => + '<indicator name="b">BBB!</indicator>Some text<indicator name="a">aaa</indicator>', + 'prop' => 'indicators', + ] ); + + $this->assertSame( + // It seems we return in markup order and not display order + [ 'b' => 'BBB!', 'a' => 'aaa' ], + $res[0]['parse']['indicators'] + ); + $this->assertArrayNotHasKey( 'warnings', $res[0] ); + } + + public function testIndicatorsWithSkin() { + $this->setupSkin(); + + $res = $this->doApiRequest( [ + 'action' => 'parse', + 'title' => __CLASS__, + 'text' => + '<indicator name="b">BBB!</indicator>Some text<indicator name="a">aaa</indicator>', + 'prop' => 'indicators', + 'useskin' => 'testing', + ] ); + + $this->assertSame( + // Now we return in display order rather than markup order + [ 'a' => 'aaa', 'b' => 'BBB!' ], + $res[0]['parse']['indicators'] + ); + $this->assertArrayNotHasKey( 'warnings', $res[0] ); + } + + public function testIwlinks() { + $this->setupInterwiki(); + + $res = $this->doApiRequest( [ + 'action' => 'parse', + 'title' => 'Omelette', + 'text' => '[[:madeuplanguage:Omelette]][[madeuplanguage:Spaghetti]]', + 'prop' => 'iwlinks', + ] ); + + $iwlinks = $res[0]['parse']['iwlinks']; + + $this->assertCount( 1, $iwlinks ); + $this->assertSame( 'madeuplanguage', $iwlinks[0]['prefix'] ); + $this->assertSame( 'https://example.com/wiki/Omelette', $iwlinks[0]['url'] ); + $this->assertSame( 'madeuplanguage:Omelette', $iwlinks[0]['title'] ); + $this->assertArrayNotHasKey( 'warnings', $res[0] ); + } + + public function testLimitReports() { + $res = $this->doApiRequest( [ + 'action' => 'parse', + 'pageid' => self::$pageId, + 'prop' => 'limitreportdata|limitreporthtml', + ] ); + + // We don't bother testing the actual values here + $this->assertInternalType( 'array', $res[0]['parse']['limitreportdata'] ); + $this->assertInternalType( 'string', $res[0]['parse']['limitreporthtml'] ); + $this->assertArrayNotHasKey( 'warnings', $res[0] ); + } + + public function testParseTreeNonWikitext() { + $this->setExpectedException( ApiUsageException::class, + '"prop=parsetree" is only supported for wikitext content.' ); + + $this->doApiRequest( [ + 'action' => 'parse', + 'text' => '', + 'contentmodel' => 'json', + 'prop' => 'parsetree', + ] ); + } + + public function testParseTree() { + $res = $this->doApiRequest( [ + 'action' => 'parse', + 'text' => "Some ''text'' is {{nice|to have|i=think}}", + 'contentmodel' => 'wikitext', + 'prop' => 'parsetree', + ] ); + + // Preprocessor_DOM and Preprocessor_Hash give different results here, + // so we'll accept either + $this->assertRegExp( + '#^<root>Some \'\'text\'\' is <template><title>nice</title>' . + '<part><name index="1"/><value>to have</value></part>' . + '<part><name>i</name>(?:<equals>)?=(?:</equals>)?<value>think</value></part>' . + '</template></root>$#', + $res[0]['parse']['parsetree'] + ); + $this->assertArrayNotHasKey( 'warnings', $res[0] ); + } + + public function testDisableTidy() { + $this->setMwGlobals( 'wgTidyConfig', [ 'driver' => 'RemexHtml' ] ); + + // Check that disabletidy doesn't have an effect just because tidying + // doesn't work for some other reason + $res1 = $this->doApiRequest( [ + 'action' => 'parse', + 'text' => "<b>Mixed <i>up</b></i>", + 'contentmodel' => 'wikitext', + ] ); + $this->assertParsedTo( "<p><b>Mixed <i>up</i></b>\n</p>", $res1 ); + + $res2 = $this->doApiRequest( [ + 'action' => 'parse', + 'text' => "<b>Mixed <i>up</b></i>", + 'contentmodel' => 'wikitext', + 'disabletidy' => '', + ] ); + + $this->assertParsedTo( "<p><b>Mixed <i>up</b></i>\n</p>", $res2 ); + } + + public function testFormatCategories() { + $name = ucfirst( __FUNCTION__ ); + + $this->editPage( "Category:$name", 'Content' ); + $this->editPage( 'Category:Hidden', '__HIDDENCAT__' ); + + $res = $this->doApiRequest( [ + 'action' => 'parse', + 'title' => __CLASS__, + 'text' => "[[Category:$name]][[Category:Foo|Sort me]][[Category:Hidden]]", + 'prop' => 'categories', + ] ); + + $this->assertSame( + [ [ 'sortkey' => '', 'category' => $name ], + [ 'sortkey' => 'Sort me', 'category' => 'Foo', 'missing' => true ], + [ 'sortkey' => '', 'category' => 'Hidden', 'hidden' => true ] ], + $res[0]['parse']['categories'] + ); + $this->assertArrayNotHasKey( 'warnings', $res[0] ); + } } diff --git a/www/wiki/tests/phpunit/includes/api/ApiPurgeTest.php b/www/wiki/tests/phpunit/includes/api/ApiPurgeTest.php index 9e1d3a18..96d9a384 100644 --- a/www/wiki/tests/phpunit/includes/api/ApiPurgeTest.php +++ b/www/wiki/tests/phpunit/includes/api/ApiPurgeTest.php @@ -9,11 +9,6 @@ */ class ApiPurgeTest extends ApiTestCase { - protected function setUp() { - parent::setUp(); - $this->doLogin(); - } - /** * @group Broken */ diff --git a/www/wiki/tests/phpunit/includes/api/ApiQueryAllPagesTest.php b/www/wiki/tests/phpunit/includes/api/ApiQueryAllPagesTest.php index 9f28aaf5..2d89aa54 100644 --- a/www/wiki/tests/phpunit/includes/api/ApiQueryAllPagesTest.php +++ b/www/wiki/tests/phpunit/includes/api/ApiQueryAllPagesTest.php @@ -4,18 +4,14 @@ * @group API * @group Database * @group medium + * + * @covers ApiQueryAllPages */ class ApiQueryAllPagesTest extends ApiTestCase { - - protected function setUp() { - parent::setUp(); - $this->doLogin(); - } - /** - *Test T27702 - *Prefixes of API search requests are not handled with case sensitivity and may result - *in wrong search results + * Test T27702 + * Prefixes of API search requests are not handled with case sensitivity and may result + * in wrong search results */ public function testPrefixNormalizationSearchBug() { $title = Title::newFromText( 'Category:Template:xyz' ); diff --git a/www/wiki/tests/phpunit/includes/api/ApiQueryRecentChangesIntegrationTest.php b/www/wiki/tests/phpunit/includes/api/ApiQueryRecentChangesIntegrationTest.php new file mode 100644 index 00000000..5b43dd1b --- /dev/null +++ b/www/wiki/tests/phpunit/includes/api/ApiQueryRecentChangesIntegrationTest.php @@ -0,0 +1,976 @@ +<?php + +use MediaWiki\Linker\LinkTarget; +use MediaWiki\MediaWikiServices; + +/** + * @group API + * @group Database + * @group medium + * + * @covers ApiQueryRecentChanges + */ +class ApiQueryRecentChangesIntegrationTest extends ApiTestCase { + + public function __construct( $name = null, array $data = [], $dataName = '' ) { + parent::__construct( $name, $data, $dataName ); + + $this->tablesUsed[] = 'recentchanges'; + $this->tablesUsed[] = 'page'; + } + + protected function setUp() { + parent::setUp(); + + self::$users['ApiQueryRecentChangesIntegrationTestUser'] = $this->getMutableTestUser(); + wfGetDB( DB_MASTER )->delete( 'recentchanges', '*', __METHOD__ ); + } + + private function getLoggedInTestUser() { + return self::$users['ApiQueryRecentChangesIntegrationTestUser']->getUser(); + } + + private function doPageEdit( User $user, LinkTarget $target, $summary ) { + static $i = 0; + + $title = Title::newFromLinkTarget( $target ); + $page = WikiPage::factory( $title ); + $page->doEditContent( + ContentHandler::makeContent( __CLASS__ . $i++, $title ), + $summary, + 0, + false, + $user + ); + } + + private function doMinorPageEdit( User $user, LinkTarget $target, $summary ) { + $title = Title::newFromLinkTarget( $target ); + $page = WikiPage::factory( $title ); + $page->doEditContent( + ContentHandler::makeContent( __CLASS__, $title ), + $summary, + EDIT_MINOR, + false, + $user + ); + } + + private function doBotPageEdit( User $user, LinkTarget $target, $summary ) { + $title = Title::newFromLinkTarget( $target ); + $page = WikiPage::factory( $title ); + $page->doEditContent( + ContentHandler::makeContent( __CLASS__, $title ), + $summary, + EDIT_FORCE_BOT, + false, + $user + ); + } + + private function doAnonPageEdit( LinkTarget $target, $summary ) { + $title = Title::newFromLinkTarget( $target ); + $page = WikiPage::factory( $title ); + $page->doEditContent( + ContentHandler::makeContent( __CLASS__, $title ), + $summary, + 0, + false, + User::newFromId( 0 ) + ); + } + + private function deletePage( LinkTarget $target, $reason ) { + $title = Title::newFromLinkTarget( $target ); + $page = WikiPage::factory( $title ); + $page->doDeleteArticleReal( $reason ); + } + + /** + * Performs a batch of page edits as a specified user + * @param User $user + * @param array $editData associative array, keys: + * - target => LinkTarget page to edit + * - summary => string edit summary + * - minorEdit => bool mark as minor edit if true (defaults to false) + * - botEdit => bool mark as bot edit if true (defaults to false) + */ + private function doPageEdits( User $user, array $editData ) { + foreach ( $editData as $singleEditData ) { + if ( array_key_exists( 'minorEdit', $singleEditData ) && $singleEditData['minorEdit'] ) { + $this->doMinorPageEdit( + $user, + $singleEditData['target'], + $singleEditData['summary'] + ); + continue; + } + if ( array_key_exists( 'botEdit', $singleEditData ) && $singleEditData['botEdit'] ) { + $this->doBotPageEdit( + $user, + $singleEditData['target'], + $singleEditData['summary'] + ); + continue; + } + $this->doPageEdit( + $user, + $singleEditData['target'], + $singleEditData['summary'] + ); + } + } + + private function doListRecentChangesRequest( array $params = [] ) { + return $this->doApiRequest( + array_merge( + [ 'action' => 'query', 'list' => 'recentchanges' ], + $params + ), + null, + false, + $this->getLoggedInTestUser() + ); + } + + private function doGeneratorRecentChangesRequest( array $params = [] ) { + return $this->doApiRequest( + array_merge( + [ 'action' => 'query', 'generator' => 'recentchanges' ], + $params + ), + null, + false, + $this->getLoggedInTestUser() + ); + } + + private function getItemsFromApiResponse( array $response ) { + return $response[0]['query']['recentchanges']; + } + + private function getTitleFormatter() { + return new MediaWikiTitleCodec( + Language::factory( 'en' ), + MediaWikiServices::getInstance()->getGenderCache() + ); + } + + private function getPrefixedText( LinkTarget $target ) { + $formatter = $this->getTitleFormatter(); + return $formatter->getPrefixedText( $target ); + } + + public function testListRecentChanges_returnsRCInfo() { + $target = new TitleValue( 0, 'ApiQueryRecentChangesIntegrationTestPage' ); + $this->doPageEdit( $this->getLoggedInTestUser(), $target, 'Create the page' ); + + $result = $this->doListRecentChangesRequest(); + + $this->assertArrayHasKey( 'query', $result[0] ); + $this->assertArrayHasKey( 'recentchanges', $result[0]['query'] ); + + $items = $this->getItemsFromApiResponse( $result ); + $this->assertCount( 1, $items ); + $item = $items[0]; + $this->assertArraySubset( + [ + 'type' => 'new', + 'ns' => $target->getNamespace(), + 'title' => $this->getPrefixedText( $target ), + ], + $item + ); + $this->assertArrayNotHasKey( 'bot', $item ); + $this->assertArrayNotHasKey( 'new', $item ); + $this->assertArrayNotHasKey( 'minor', $item ); + $this->assertArrayHasKey( 'pageid', $item ); + $this->assertArrayHasKey( 'revid', $item ); + $this->assertArrayHasKey( 'old_revid', $item ); + } + + public function testIdsPropParameter() { + $target = new TitleValue( 0, 'ApiQueryRecentChangesIntegrationTestPage' ); + $this->doPageEdit( $this->getLoggedInTestUser(), $target, 'Create the page' ); + + $result = $this->doListRecentChangesRequest( [ 'rcprop' => 'ids', ] ); + $items = $this->getItemsFromApiResponse( $result ); + + $this->assertCount( 1, $items ); + $this->assertArrayHasKey( 'pageid', $items[0] ); + $this->assertArrayHasKey( 'revid', $items[0] ); + $this->assertArrayHasKey( 'old_revid', $items[0] ); + $this->assertEquals( 'new', $items[0]['type'] ); + } + + public function testTitlePropParameter() { + $subjectTarget = new TitleValue( 0, 'ApiQueryRecentChangesIntegrationTestPage' ); + $talkTarget = new TitleValue( 1, 'ApiQueryRecentChangesIntegrationTestPage' ); + $this->doPageEdits( + $this->getLoggedInTestUser(), + [ + [ + 'target' => $subjectTarget, + 'summary' => 'Create the page', + ], + [ + 'target' => $talkTarget, + 'summary' => 'Create Talk page', + ], + ] + ); + + $result = $this->doListRecentChangesRequest( [ 'rcprop' => 'title', ] ); + + $this->assertEquals( + [ + [ + 'type' => 'new', + 'ns' => $talkTarget->getNamespace(), + 'title' => $this->getPrefixedText( $talkTarget ), + ], + [ + 'type' => 'new', + 'ns' => $subjectTarget->getNamespace(), + 'title' => $this->getPrefixedText( $subjectTarget ), + ], + ], + $this->getItemsFromApiResponse( $result ) + ); + } + + public function testFlagsPropParameter() { + $normalEditTarget = new TitleValue( 0, 'ApiQueryRecentChangesIntegrationTestPage' ); + $minorEditTarget = new TitleValue( 0, 'ApiQueryRecentChangesIntegrationTestPageM' ); + $botEditTarget = new TitleValue( 0, 'ApiQueryRecentChangesIntegrationTestPageB' ); + $this->doPageEdits( + $this->getLoggedInTestUser(), + [ + [ + 'target' => $normalEditTarget, + 'summary' => 'Create the page', + ], + [ + 'target' => $minorEditTarget, + 'summary' => 'Create the page', + ], + [ + 'target' => $minorEditTarget, + 'summary' => 'Change content', + 'minorEdit' => true, + ], + [ + 'target' => $botEditTarget, + 'summary' => 'Create the page with a bot', + 'botEdit' => true, + ], + ] + ); + + $result = $this->doListRecentChangesRequest( [ 'rcprop' => 'flags', ] ); + + $this->assertEquals( + [ + [ + 'type' => 'new', + 'new' => true, + 'minor' => false, + 'bot' => true, + ], + [ + 'type' => 'edit', + 'new' => false, + 'minor' => true, + 'bot' => false, + ], + [ + 'type' => 'new', + 'new' => true, + 'minor' => false, + 'bot' => false, + ], + [ + 'type' => 'new', + 'new' => true, + 'minor' => false, + 'bot' => false, + ], + ], + $this->getItemsFromApiResponse( $result ) + ); + } + + public function testUserPropParameter() { + $userEditTarget = new TitleValue( 0, 'ApiQueryRecentChangesIntegrationTestPage' ); + $anonEditTarget = new TitleValue( 0, 'ApiQueryRecentChangesIntegrationTestPageA' ); + $this->doPageEdit( $this->getLoggedInTestUser(), $userEditTarget, 'Create the page' ); + $this->doAnonPageEdit( $anonEditTarget, 'Create the page' ); + + $result = $this->doListRecentChangesRequest( [ 'rcprop' => 'user', ] ); + + $this->assertEquals( + [ + [ + 'type' => 'new', + 'anon' => true, + 'user' => User::newFromId( 0 )->getName(), + ], + [ + 'type' => 'new', + 'user' => $this->getLoggedInTestUser()->getName(), + ], + ], + $this->getItemsFromApiResponse( $result ) + ); + } + + public function testUserIdPropParameter() { + $user = $this->getLoggedInTestUser(); + $userEditTarget = new TitleValue( 0, 'ApiQueryRecentChangesIntegrationTestPage' ); + $anonEditTarget = new TitleValue( 0, 'ApiQueryRecentChangesIntegrationTestPageA' ); + $this->doPageEdit( $user, $userEditTarget, 'Create the page' ); + $this->doAnonPageEdit( $anonEditTarget, 'Create the page' ); + + $result = $this->doListRecentChangesRequest( [ 'rcprop' => 'userid', ] ); + + $this->assertEquals( + [ + [ + 'type' => 'new', + 'anon' => true, + 'userid' => 0, + ], + [ + 'type' => 'new', + 'userid' => $user->getId(), + ], + ], + $this->getItemsFromApiResponse( $result ) + ); + } + + public function testCommentPropParameter() { + $target = new TitleValue( 0, 'ApiQueryRecentChangesIntegrationTestPage' ); + $this->doPageEdit( $this->getLoggedInTestUser(), $target, 'Create the <b>page</b>' ); + + $result = $this->doListRecentChangesRequest( [ 'rcprop' => 'comment', ] ); + + $this->assertEquals( + [ + [ + 'type' => 'new', + 'comment' => 'Create the <b>page</b>', + ], + ], + $this->getItemsFromApiResponse( $result ) + ); + } + + public function testParsedCommentPropParameter() { + $target = new TitleValue( 0, 'ApiQueryRecentChangesIntegrationTestPage' ); + $this->doPageEdit( $this->getLoggedInTestUser(), $target, 'Create the <b>page</b>' ); + + $result = $this->doListRecentChangesRequest( [ 'rcprop' => 'parsedcomment', ] ); + + $this->assertEquals( + [ + [ + 'type' => 'new', + 'parsedcomment' => 'Create the <b>page</b>', + ], + ], + $this->getItemsFromApiResponse( $result ) + ); + } + + public function testTimestampPropParameter() { + $target = new TitleValue( 0, 'ApiQueryRecentChangesIntegrationTestPage' ); + $this->doPageEdit( $this->getLoggedInTestUser(), $target, 'Create the page' ); + + $result = $this->doListRecentChangesRequest( [ 'rcprop' => 'timestamp', ] ); + $items = $this->getItemsFromApiResponse( $result ); + + $this->assertCount( 1, $items ); + $this->assertArrayHasKey( 'timestamp', $items[0] ); + $this->assertInternalType( 'string', $items[0]['timestamp'] ); + } + + public function testSizesPropParameter() { + $target = new TitleValue( 0, 'ApiQueryRecentChangesIntegrationTestPage' ); + $this->doPageEdit( $this->getLoggedInTestUser(), $target, 'Create the page' ); + + $result = $this->doListRecentChangesRequest( [ 'rcprop' => 'sizes', ] ); + + $this->assertEquals( + [ + [ + 'type' => 'new', + 'oldlen' => 0, + 'newlen' => 38, + ], + ], + $this->getItemsFromApiResponse( $result ) + ); + } + + private function createPageAndDeleteIt( LinkTarget $target ) { + $this->doPageEdit( $this->getLoggedInTestUser(), + $target, + 'Create the page that will be deleted' + ); + $this->deletePage( $target, 'Important Reason' ); + } + + public function testLoginfoPropParameter() { + $target = new TitleValue( 0, 'ApiQueryRecentChangesIntegrationTestPage' ); + $this->createPageAndDeleteIt( $target ); + + $result = $this->doListRecentChangesRequest( [ 'rcprop' => 'loginfo', ] ); + + $items = $this->getItemsFromApiResponse( $result ); + $this->assertCount( 1, $items ); + $this->assertArraySubset( + [ + 'type' => 'log', + 'logtype' => 'delete', + 'logaction' => 'delete', + 'logparams' => [], + ], + $items[0] + ); + $this->assertArrayHasKey( 'logid', $items[0] ); + } + + public function testEmptyPropParameter() { + $user = $this->getLoggedInTestUser(); + $target = new TitleValue( 0, 'ApiQueryRecentChangesIntegrationTestPage' ); + $this->doPageEdit( $user, $target, 'Create the page' ); + + $result = $this->doListRecentChangesRequest( [ 'rcprop' => '', ] ); + + $this->assertEquals( + [ + [ + 'type' => 'new', + ] + ], + $this->getItemsFromApiResponse( $result ) + ); + } + + public function testNamespaceParam() { + $subjectTarget = new TitleValue( 0, 'ApiQueryRecentChangesIntegrationTestPage' ); + $talkTarget = new TitleValue( 1, 'ApiQueryRecentChangesIntegrationTestPage' ); + $this->doPageEdits( + $this->getLoggedInTestUser(), + [ + [ + 'target' => $subjectTarget, + 'summary' => 'Create the page', + ], + [ + 'target' => $talkTarget, + 'summary' => 'Create the talk page', + ], + ] + ); + + $result = $this->doListRecentChangesRequest( [ 'rcnamespace' => '0', ] ); + + $items = $this->getItemsFromApiResponse( $result ); + $this->assertCount( 1, $items ); + $this->assertArraySubset( + [ + 'ns' => 0, + 'title' => $this->getPrefixedText( $subjectTarget ), + ], + $items[0] + ); + } + + public function testShowAnonParams() { + $target = new TitleValue( 0, 'ApiQueryRecentChangesIntegrationTestPage' ); + $this->doAnonPageEdit( $target, 'Create the page' ); + + $resultAnon = $this->doListRecentChangesRequest( [ + 'rcprop' => 'user', + 'rcshow' => WatchedItemQueryService::FILTER_ANON + ] ); + $resultNotAnon = $this->doListRecentChangesRequest( [ + 'rcprop' => 'user', + 'rcshow' => WatchedItemQueryService::FILTER_NOT_ANON + ] ); + + $items = $this->getItemsFromApiResponse( $resultAnon ); + $this->assertCount( 1, $items ); + $this->assertArraySubset( [ 'anon' => true ], $items[0] ); + $this->assertEmpty( $this->getItemsFromApiResponse( $resultNotAnon ) ); + } + + public function testNewAndEditTypeParameters() { + $subjectTarget = new TitleValue( 0, 'ApiQueryRecentChangesIntegrationTestPage' ); + $talkTarget = new TitleValue( 1, 'ApiQueryRecentChangesIntegrationTestPage' ); + $this->doPageEdits( + $this->getLoggedInTestUser(), + [ + [ + 'target' => $subjectTarget, + 'summary' => 'Create the page', + ], + [ + 'target' => $subjectTarget, + 'summary' => 'Change the content', + ], + [ + 'target' => $talkTarget, + 'summary' => 'Create Talk page', + ], + ] + ); + + $resultNew = $this->doListRecentChangesRequest( [ 'rcprop' => 'title', 'rctype' => 'new' ] ); + $resultEdit = $this->doListRecentChangesRequest( [ 'rcprop' => 'title', 'rctype' => 'edit' ] ); + + $this->assertEquals( + [ + [ + 'type' => 'new', + 'ns' => $talkTarget->getNamespace(), + 'title' => $this->getPrefixedText( $talkTarget ), + ], + [ + 'type' => 'new', + 'ns' => $subjectTarget->getNamespace(), + 'title' => $this->getPrefixedText( $subjectTarget ), + ], + ], + $this->getItemsFromApiResponse( $resultNew ) + ); + $this->assertEquals( + [ + [ + 'type' => 'edit', + 'ns' => $subjectTarget->getNamespace(), + 'title' => $this->getPrefixedText( $subjectTarget ), + ], + ], + $this->getItemsFromApiResponse( $resultEdit ) + ); + } + + public function testLogTypeParameters() { + $subjectTarget = new TitleValue( 0, 'ApiQueryRecentChangesIntegrationTestPage' ); + $talkTarget = new TitleValue( 1, 'ApiQueryRecentChangesIntegrationTestPage' ); + $this->createPageAndDeleteIt( $subjectTarget ); + $this->doPageEdit( $this->getLoggedInTestUser(), $talkTarget, 'Create Talk page' ); + + $result = $this->doListRecentChangesRequest( [ 'rcprop' => 'title', 'rctype' => 'log' ] ); + + $this->assertEquals( + [ + [ + 'type' => 'log', + 'ns' => $subjectTarget->getNamespace(), + 'title' => $this->getPrefixedText( $subjectTarget ), + ], + ], + $this->getItemsFromApiResponse( $result ) + ); + } + + private function getExternalRC( LinkTarget $target ) { + $title = Title::newFromLinkTarget( $target ); + + $rc = new RecentChange; + $rc->mTitle = $title; + $rc->mAttribs = [ + 'rc_timestamp' => wfTimestamp( TS_MW ), + 'rc_namespace' => $title->getNamespace(), + 'rc_title' => $title->getDBkey(), + 'rc_type' => RC_EXTERNAL, + 'rc_source' => 'foo', + 'rc_minor' => 0, + 'rc_cur_id' => $title->getArticleID(), + 'rc_user' => 0, + 'rc_user_text' => 'm>External User', + 'rc_comment' => '', + 'rc_comment_text' => '', + 'rc_comment_data' => null, + 'rc_this_oldid' => $title->getLatestRevID(), + 'rc_last_oldid' => $title->getLatestRevID(), + 'rc_bot' => 0, + 'rc_ip' => '', + 'rc_patrolled' => 0, + 'rc_new' => 0, + 'rc_old_len' => $title->getLength(), + 'rc_new_len' => $title->getLength(), + 'rc_deleted' => 0, + 'rc_logid' => 0, + 'rc_log_type' => null, + 'rc_log_action' => '', + 'rc_params' => '', + ]; + $rc->mExtra = [ + 'prefixedDBkey' => $title->getPrefixedDBkey(), + 'lastTimestamp' => 0, + 'oldSize' => $title->getLength(), + 'newSize' => $title->getLength(), + 'pageStatus' => 'changed' + ]; + + return $rc; + } + + public function testExternalTypeParameters() { + $user = $this->getLoggedInTestUser(); + $subjectTarget = new TitleValue( 0, 'ApiQueryRecentChangesIntegrationTestPage' ); + $talkTarget = new TitleValue( 1, 'ApiQueryRecentChangesIntegrationTestPage' ); + $this->doPageEdit( $user, $subjectTarget, 'Create the page' ); + $this->doPageEdit( $user, $talkTarget, 'Create Talk page' ); + + $rc = $this->getExternalRC( $subjectTarget ); + $rc->save(); + + $result = $this->doListRecentChangesRequest( [ 'rcprop' => 'title', 'rctype' => 'external' ] ); + + $this->assertEquals( + [ + [ + 'type' => 'external', + 'ns' => $subjectTarget->getNamespace(), + 'title' => $this->getPrefixedText( $subjectTarget ), + ], + ], + $this->getItemsFromApiResponse( $result ) + ); + } + + public function testCategorizeTypeParameter() { + $user = $this->getLoggedInTestUser(); + $subjectTarget = new TitleValue( 0, 'ApiQueryRecentChangesIntegrationTestPage' ); + $categoryTarget = new TitleValue( NS_CATEGORY, 'ApiQueryRecentChangesIntegrationTestCategory' ); + $this->doPageEdits( + $user, + [ + [ + 'target' => $categoryTarget, + 'summary' => 'Create the category', + ], + [ + 'target' => $subjectTarget, + 'summary' => 'Create the page and add it to the category', + ], + ] + ); + $title = Title::newFromLinkTarget( $subjectTarget ); + $revision = Revision::newFromTitle( $title ); + + $rc = RecentChange::newForCategorization( + $revision->getTimestamp(), + Title::newFromLinkTarget( $categoryTarget ), + $user, + $revision->getComment(), + $title, + 0, + $revision->getId(), + null, + false + ); + $rc->save(); + + $result = $this->doListRecentChangesRequest( [ 'rcprop' => 'title', 'rctype' => 'categorize' ] ); + + $this->assertEquals( + [ + [ + 'type' => 'categorize', + 'ns' => $categoryTarget->getNamespace(), + 'title' => $this->getPrefixedText( $categoryTarget ), + ], + ], + $this->getItemsFromApiResponse( $result ) + ); + } + + public function testLimitParam() { + $target1 = new TitleValue( 0, 'ApiQueryRecentChangesIntegrationTestPage' ); + $target2 = new TitleValue( 1, 'ApiQueryRecentChangesIntegrationTestPage' ); + $target3 = new TitleValue( 0, 'ApiQueryRecentChangesIntegrationTestPage2' ); + $this->doPageEdits( + $this->getLoggedInTestUser(), + [ + [ + 'target' => $target1, + 'summary' => 'Create the page', + ], + [ + 'target' => $target2, + 'summary' => 'Create Talk page', + ], + [ + 'target' => $target3, + 'summary' => 'Create the page', + ], + ] + ); + + $resultWithoutLimit = $this->doListRecentChangesRequest( [ 'rcprop' => 'title' ] ); + $resultWithLimit = $this->doListRecentChangesRequest( [ 'rclimit' => 2, 'rcprop' => 'title' ] ); + + $this->assertEquals( + [ + [ + 'type' => 'new', + 'ns' => $target3->getNamespace(), + 'title' => $this->getPrefixedText( $target3 ) + ], + [ + 'type' => 'new', + 'ns' => $target2->getNamespace(), + 'title' => $this->getPrefixedText( $target2 ) + ], + [ + 'type' => 'new', + 'ns' => $target1->getNamespace(), + 'title' => $this->getPrefixedText( $target1 ) + ], + ], + $this->getItemsFromApiResponse( $resultWithoutLimit ) + ); + $this->assertEquals( + [ + [ + 'type' => 'new', + 'ns' => $target3->getNamespace(), + 'title' => $this->getPrefixedText( $target3 ) + ], + [ + 'type' => 'new', + 'ns' => $target2->getNamespace(), + 'title' => $this->getPrefixedText( $target2 ) + ], + ], + $this->getItemsFromApiResponse( $resultWithLimit ) + ); + $this->assertArrayHasKey( 'continue', $resultWithLimit[0] ); + $this->assertArrayHasKey( 'rccontinue', $resultWithLimit[0]['continue'] ); + } + + public function testAllRevParam() { + $target = new TitleValue( 0, 'ApiQueryRecentChangesIntegrationTestPage' ); + $this->doPageEdits( + $this->getLoggedInTestUser(), + [ + [ + 'target' => $target, + 'summary' => 'Create the page', + ], + [ + 'target' => $target, + 'summary' => 'Change the content', + ], + ] + ); + + $resultAllRev = $this->doListRecentChangesRequest( [ 'rcprop' => 'title', 'rcallrev' => '', ] ); + $resultNoAllRev = $this->doListRecentChangesRequest( [ 'rcprop' => 'title' ] ); + + $this->assertEquals( + [ + [ + 'type' => 'edit', + 'ns' => $target->getNamespace(), + 'title' => $this->getPrefixedText( $target ), + ], + [ + 'type' => 'new', + 'ns' => $target->getNamespace(), + 'title' => $this->getPrefixedText( $target ), + ], + ], + $this->getItemsFromApiResponse( $resultNoAllRev ) + ); + $this->assertEquals( + [ + [ + 'type' => 'edit', + 'ns' => $target->getNamespace(), + 'title' => $this->getPrefixedText( $target ), + ], + [ + 'type' => 'new', + 'ns' => $target->getNamespace(), + 'title' => $this->getPrefixedText( $target ), + ], + ], + $this->getItemsFromApiResponse( $resultAllRev ) + ); + } + + public function testDirParams() { + $subjectTarget = new TitleValue( 0, 'ApiQueryRecentChangesIntegrationTestPage' ); + $talkTarget = new TitleValue( 1, 'ApiQueryRecentChangesIntegrationTestPage' ); + $this->doPageEdits( + $this->getLoggedInTestUser(), + [ + [ + 'target' => $subjectTarget, + 'summary' => 'Create the page', + ], + [ + 'target' => $talkTarget, + 'summary' => 'Create Talk page', + ], + ] + ); + + $resultDirOlder = $this->doListRecentChangesRequest( + [ 'rcdir' => 'older', 'rcprop' => 'title' ] + ); + $resultDirNewer = $this->doListRecentChangesRequest( + [ 'rcdir' => 'newer', 'rcprop' => 'title' ] + ); + + $this->assertEquals( + [ + [ + 'type' => 'new', + 'ns' => $talkTarget->getNamespace(), + 'title' => $this->getPrefixedText( $talkTarget ) + ], + [ + 'type' => 'new', + 'ns' => $subjectTarget->getNamespace(), + 'title' => $this->getPrefixedText( $subjectTarget ) + ], + ], + $this->getItemsFromApiResponse( $resultDirOlder ) + ); + $this->assertEquals( + [ + [ + 'type' => 'new', + 'ns' => $subjectTarget->getNamespace(), + 'title' => $this->getPrefixedText( $subjectTarget ) + ], + [ + 'type' => 'new', + 'ns' => $talkTarget->getNamespace(), + 'title' => $this->getPrefixedText( $talkTarget ) + ], + ], + $this->getItemsFromApiResponse( $resultDirNewer ) + ); + } + + public function testStartEndParams() { + $target = new TitleValue( 0, 'ApiQueryRecentChangesIntegrationTestPage' ); + $this->doPageEdit( $this->getLoggedInTestUser(), $target, 'Create the page' ); + + $resultStart = $this->doListRecentChangesRequest( [ + 'rcstart' => '20010115000000', + 'rcdir' => 'newer', + 'rcprop' => 'title', + ] ); + $resultEnd = $this->doListRecentChangesRequest( [ + 'rcend' => '20010115000000', + 'rcdir' => 'newer', + 'rcprop' => 'title', + ] ); + + $this->assertEquals( + [ + [ + 'type' => 'new', + 'ns' => $target->getNamespace(), + 'title' => $this->getPrefixedText( $target ), + ] + ], + $this->getItemsFromApiResponse( $resultStart ) + ); + $this->assertEmpty( $this->getItemsFromApiResponse( $resultEnd ) ); + } + + public function testContinueParam() { + $target1 = new TitleValue( 0, 'ApiQueryRecentChangesIntegrationTestPage' ); + $target2 = new TitleValue( 1, 'ApiQueryRecentChangesIntegrationTestPage' ); + $target3 = new TitleValue( 0, 'ApiQueryRecentChangesIntegrationTestPage2' ); + $this->doPageEdits( + $this->getLoggedInTestUser(), + [ + [ + 'target' => $target1, + 'summary' => 'Create the page', + ], + [ + 'target' => $target2, + 'summary' => 'Create Talk page', + ], + [ + 'target' => $target3, + 'summary' => 'Create the page', + ], + ] + ); + + $firstResult = $this->doListRecentChangesRequest( [ 'rclimit' => 2, 'rcprop' => 'title' ] ); + $this->assertArrayHasKey( 'continue', $firstResult[0] ); + $this->assertArrayHasKey( 'rccontinue', $firstResult[0]['continue'] ); + + $continuationParam = $firstResult[0]['continue']['rccontinue']; + + $continuedResult = $this->doListRecentChangesRequest( + [ 'rccontinue' => $continuationParam, 'rcprop' => 'title' ] + ); + + $this->assertEquals( + [ + [ + 'type' => 'new', + 'ns' => $target3->getNamespace(), + 'title' => $this->getPrefixedText( $target3 ), + ], + [ + 'type' => 'new', + 'ns' => $target2->getNamespace(), + 'title' => $this->getPrefixedText( $target2 ), + ], + ], + $this->getItemsFromApiResponse( $firstResult ) + ); + $this->assertEquals( + [ + [ + 'type' => 'new', + 'ns' => $target1->getNamespace(), + 'title' => $this->getPrefixedText( $target1 ) + ] + ], + $this->getItemsFromApiResponse( $continuedResult ) + ); + } + + public function testGeneratorRecentChangesPropInfo_returnsRCPages() { + $target = new TitleValue( 0, 'ApiQueryRecentChangesIntegrationTestPage' ); + $this->doPageEdit( $this->getLoggedInTestUser(), $target, 'Create the page' ); + + $result = $this->doGeneratorRecentChangesRequest( [ 'prop' => 'info' ] ); + + $this->assertArrayHasKey( 'query', $result[0] ); + $this->assertArrayHasKey( 'pages', $result[0]['query'] ); + + // $result[0]['query']['pages'] uses page ids as keys. Page ids don't matter here, so drop them + $pages = array_values( $result[0]['query']['pages'] ); + + $this->assertCount( 1, $pages ); + $this->assertArraySubset( + [ + 'ns' => $target->getNamespace(), + 'title' => $this->getPrefixedText( $target ), + 'new' => true, + ], + $pages[0] + ); + } + +} diff --git a/www/wiki/tests/phpunit/includes/api/ApiQueryWatchlistIntegrationTest.php b/www/wiki/tests/phpunit/includes/api/ApiQueryWatchlistIntegrationTest.php index fdbedede..5f59d6fb 100644 --- a/www/wiki/tests/phpunit/includes/api/ApiQueryWatchlistIntegrationTest.php +++ b/www/wiki/tests/phpunit/includes/api/ApiQueryWatchlistIntegrationTest.php @@ -4,9 +4,9 @@ use MediaWiki\Linker\LinkTarget; use MediaWiki\MediaWikiServices; /** + * @group medium * @group API * @group Database - * @group medium * * @covers ApiQueryWatchlist */ @@ -23,7 +23,6 @@ class ApiQueryWatchlistIntegrationTest extends ApiTestCase { parent::setUp(); self::$users['ApiQueryWatchlistIntegrationTestUser'] = $this->getMutableTestUser(); self::$users['ApiQueryWatchlistIntegrationTestUser2'] = $this->getMutableTestUser(); - $this->doLogin( 'ApiQueryWatchlistIntegrationTestUser' ); } private function getLoggedInTestUser() { @@ -163,6 +162,9 @@ class ApiQueryWatchlistIntegrationTest extends ApiTestCase { } private function doListWatchlistRequest( array $params = [], $user = null ) { + if ( $user === null ) { + $user = $this->getLoggedInTestUser(); + } return $this->doApiRequest( array_merge( [ 'action' => 'query', 'list' => 'watchlist' ], @@ -176,7 +178,7 @@ class ApiQueryWatchlistIntegrationTest extends ApiTestCase { array_merge( [ 'action' => 'query', 'generator' => 'watchlist' ], $params - ) + ), null, false, $this->getLoggedInTestUser() ); } @@ -629,6 +631,7 @@ class ApiQueryWatchlistIntegrationTest extends ApiTestCase { 'type' => 'new', 'patrolled' => true, 'unpatrolled' => false, + 'autopatrolled' => false, ] ], $this->getItemsFromApiResponse( $result ) @@ -973,6 +976,7 @@ class ApiQueryWatchlistIntegrationTest extends ApiTestCase { 'type' => 'new', 'patrolled' => true, 'unpatrolled' => false, + 'autopatrolled' => false, ] ], $this->getItemsFromApiResponse( $resultPatrolled ) @@ -1072,7 +1076,7 @@ class ApiQueryWatchlistIntegrationTest extends ApiTestCase { 'rc_minor' => 0, 'rc_cur_id' => $title->getArticleID(), 'rc_user' => 0, - 'rc_user_text' => 'External User', + 'rc_user_text' => 'ext>External User', 'rc_comment' => '', 'rc_comment_text' => '', 'rc_comment_data' => null, diff --git a/www/wiki/tests/phpunit/includes/api/ApiQueryWatchlistRawIntegrationTest.php b/www/wiki/tests/phpunit/includes/api/ApiQueryWatchlistRawIntegrationTest.php index 0f01664e..2af63c49 100644 --- a/www/wiki/tests/phpunit/includes/api/ApiQueryWatchlistRawIntegrationTest.php +++ b/www/wiki/tests/phpunit/includes/api/ApiQueryWatchlistRawIntegrationTest.php @@ -17,7 +17,6 @@ class ApiQueryWatchlistRawIntegrationTest extends ApiTestCase { = $this->getMutableTestUser(); self::$users['ApiQueryWatchlistRawIntegrationTestUser2'] = $this->getMutableTestUser(); - $this->doLogin( 'ApiQueryWatchlistRawIntegrationTestUser' ); } private function getLoggedInTestUser() { @@ -36,14 +35,14 @@ class ApiQueryWatchlistRawIntegrationTest extends ApiTestCase { return $this->doApiRequest( array_merge( [ 'action' => 'query', 'list' => 'watchlistraw' ], $params - ) ); + ), null, false, $this->getLoggedInTestUser() ); } private function doGeneratorWatchlistRawRequest( array $params = [] ) { return $this->doApiRequest( array_merge( [ 'action' => 'query', 'generator' => 'watchlistraw' ], $params - ) ); + ), null, false, $this->getLoggedInTestUser() ); } private function getItemsFromApiResponse( array $response ) { diff --git a/www/wiki/tests/phpunit/includes/api/ApiSetNotificationTimestampIntegrationTest.php b/www/wiki/tests/phpunit/includes/api/ApiSetNotificationTimestampIntegrationTest.php index ef4f5139..dacd48f6 100644 --- a/www/wiki/tests/phpunit/includes/api/ApiSetNotificationTimestampIntegrationTest.php +++ b/www/wiki/tests/phpunit/includes/api/ApiSetNotificationTimestampIntegrationTest.php @@ -13,7 +13,6 @@ class ApiSetNotificationTimestampIntegrationTest extends ApiTestCase { protected function setUp() { parent::setUp(); self::$users[__CLASS__] = new TestUser( __CLASS__ ); - $this->doLogin( __CLASS__ ); } public function testStuff() { diff --git a/www/wiki/tests/phpunit/includes/api/ApiStashEditTest.php b/www/wiki/tests/phpunit/includes/api/ApiStashEditTest.php index e2462c61..60cda090 100644 --- a/www/wiki/tests/phpunit/includes/api/ApiStashEditTest.php +++ b/www/wiki/tests/phpunit/includes/api/ApiStashEditTest.php @@ -9,7 +9,6 @@ class ApiStashEditTest extends ApiTestCase { public function testBasicEdit() { - $this->doLogin(); $apiResult = $this->doApiRequestWithToken( [ 'action' => 'stashedit', diff --git a/www/wiki/tests/phpunit/includes/api/ApiTestCase.php b/www/wiki/tests/phpunit/includes/api/ApiTestCase.php index abef1c92..974e9a2d 100644 --- a/www/wiki/tests/phpunit/includes/api/ApiTestCase.php +++ b/www/wiki/tests/phpunit/includes/api/ApiTestCase.php @@ -1,5 +1,7 @@ <?php +use MediaWiki\Session\SessionManager; + abstract class ApiTestCase extends MediaWikiLangTestCase { protected static $apiUrl; @@ -55,6 +57,28 @@ abstract class ApiTestCase extends MediaWikiLangTestCase { } /** + * Revision-deletes a revision. + * + * @param Revision|int $rev Revision to delete + * @param array $value Keys are Revision::DELETED_* flags. Values are 1 to set the bit, 0 to + * clear, -1 to leave alone. (All other values also clear the bit.) + * @param string $comment Deletion comment + */ + protected function revisionDelete( + $rev, array $value = [ Revision::DELETED_TEXT => 1 ], $comment = '' + ) { + if ( is_int( $rev ) ) { + $rev = Revision::newFromId( $rev ); + } + RevisionDeleter::createList( + 'revision', RequestContext::getMain(), $rev->getTitle(), [ $rev->getId() ] + )->setVisibility( [ + 'value' => $value, + 'comment' => $comment, + ] ); + } + + /** * Does the API request and returns the result. * * The returned value is an array containing @@ -67,11 +91,14 @@ abstract class ApiTestCase extends MediaWikiLangTestCase { * @param array|null $session * @param bool $appendModule * @param User|null $user + * @param string|null $tokenType Set to a string like 'csrf' to send an + * appropriate token * + * @throws ApiUsageException * @return array */ protected function doApiRequest( array $params, array $session = null, - $appendModule = false, User $user = null + $appendModule = false, User $user = null, $tokenType = null ) { global $wgRequest, $wgUser; @@ -80,12 +107,30 @@ abstract class ApiTestCase extends MediaWikiLangTestCase { $session = $wgRequest->getSessionArray(); } + $sessionObj = SessionManager::singleton()->getEmptySession(); + + if ( $session !== null ) { + foreach ( $session as $key => $value ) { + $sessionObj->set( $key, $value ); + } + } + // set up global environment if ( $user ) { $wgUser = $user; } - $wgRequest = new FauxRequest( $params, true, $session ); + if ( $tokenType !== null ) { + if ( $tokenType === 'auto' ) { + $tokenType = ( new ApiMain() )->getModuleManager() + ->getModule( $params['action'], 'action' )->needsToken(); + } + $params['token'] = ApiQueryTokens::getToken( + $wgUser, $sessionObj, ApiQueryTokens::getTokenTypeSalts()[$tokenType] + )->toString(); + } + + $wgRequest = new FauxRequest( $params, true, $sessionObj ); RequestContext::getMain()->setRequest( $wgRequest ); RequestContext::getMain()->setUser( $wgUser ); MediaWiki\Auth\AuthManager::resetCache(); @@ -113,76 +158,44 @@ abstract class ApiTestCase extends MediaWikiLangTestCase { } /** - * 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. + * Convenience function to access the token parameter of doApiRequest() + * more succinctly. * * @param array $params Key-value API params * @param array|null $session Session array * @param User|null $user A User object for the context + * @param string $tokenType Which token type to pass * @return array 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 + User $user = null, $tokenType = 'auto' ) { - global $wgRequest; - - if ( $session === null ) { - $session = $wgRequest->getSessionArray(); - } - - if ( isset( $session['wsToken'] ) && $session['wsToken'] ) { - // @todo Why does this directly mess with the session? Fix that. - // add edit token to fake session - $session['wsTokenSecrets']['default'] = $session['wsToken']; - // add token to request parameters - $timestamp = wfTimestamp(); - $params['token'] = hash_hmac( 'md5', $timestamp, $session['wsToken'] ) . - dechex( $timestamp ) . - MediaWiki\Session\Token::SUFFIX; - - return $this->doApiRequest( $params, $session, false, $user ); - } else { - throw new Exception( "Session token not available" ); - } + return $this->doApiRequest( $params, $session, false, $user, $tokenType ); } - protected function doLogin( $testUser = 'sysop' ) { + /** + * Previously this would do API requests to log in, as well as setting $wgUser and the request + * context's user. The API requests are unnecessary, and the global-setting is unwanted, so + * this method should not be called. Instead, pass appropriate User values directly to + * functions that need them. For functions that still rely on $wgUser, set that directly. If + * you just want to log in the test sysop user, don't do anything -- that's the default. + * + * @param TestUser|string $testUser Object, or key to self::$users such as 'sysop' or 'uploader' + * @deprecated since 1.31 + */ + protected function doLogin( $testUser = null ) { + global $wgUser; + if ( $testUser === null ) { $testUser = static::getTestSysop(); } elseif ( is_string( $testUser ) && array_key_exists( $testUser, self::$users ) ) { - $testUser = self::$users[ $testUser ]; + $testUser = self::$users[$testUser]; } elseif ( !$testUser instanceof TestUser ) { - throw new MWException( "Can not log in to undefined user $testUser" ); + throw new MWException( "Can't log in to undefined user $testUser" ); } - $data = $this->doApiRequest( [ - 'action' => 'login', - 'lgname' => $testUser->getUser()->getName(), - 'lgpassword' => $testUser->getPassword() ] ); - - $token = $data[0]['login']['token']; - - $data = $this->doApiRequest( - [ - 'action' => 'login', - 'lgtoken' => $token, - 'lgname' => $testUser->getUser()->getName(), - 'lgpassword' => $testUser->getPassword(), - ], - $data[2] - ); - - if ( $data[0]['login']['result'] === 'Success' ) { - // DWIM - global $wgUser; - $wgUser = $testUser->getUser(); - RequestContext::getMain()->setUser( $wgUser ); - } - - return $data; + $wgUser = $testUser->getUser(); + RequestContext::getMain()->setUser( $wgUser ); } protected function getTokenList( TestUser $user, $session = null ) { @@ -218,6 +231,9 @@ abstract class ApiTestCase extends MediaWikiLangTestCase { ); } + /** + * @coversNothing + */ public function testApiTestGroup() { $groups = PHPUnit_Util_Test::getGroups( static::class ); $constraint = PHPUnit_Framework_Assert::logicalOr( @@ -228,4 +244,17 @@ abstract class ApiTestCase extends MediaWikiLangTestCase { 'ApiTestCase::setUp can be slow, tests must be "medium" or "large"' ); } + + /** + * Expect an ApiUsageException to be thrown with the given parameters, which are the same as + * ApiUsageException::newWithMessage()'s parameters. This allows checking for an exception + * whose text is given by a message key instead of text, so as not to hard-code the message's + * text into test code. + */ + protected function setExpectedApiException( + $msg, $code = null, array $data = null, $httpCode = 0 + ) { + $expected = ApiUsageException::newWithMessage( null, $msg, $code, $data, $httpCode ); + $this->setExpectedException( ApiUsageException::class, $expected->getMessage() ); + } } diff --git a/www/wiki/tests/phpunit/includes/api/ApiTestCaseUpload.php b/www/wiki/tests/phpunit/includes/api/ApiTestCaseUpload.php index f15da2ee..3670fad8 100644 --- a/www/wiki/tests/phpunit/includes/api/ApiTestCaseUpload.php +++ b/www/wiki/tests/phpunit/includes/api/ApiTestCaseUpload.php @@ -1,153 +1,8 @@ <?php /** - * Abstract class to support upload tests + * For backward compatibility since 1.31 */ -abstract class ApiTestCaseUpload extends ApiTestCase { - /** - * Fixture -- run before every test - */ - protected function setUp() { - parent::setUp(); +abstract class ApiTestCaseUpload extends ApiUploadTestCase { - $this->setMwGlobals( [ - 'wgEnableUploads' => true, - 'wgEnableAPI' => true, - ] ); - - $this->clearFakeUploads(); - } - - /** - * Helper function -- remove files and associated articles by Title - * - * @param Title $title Title to be removed - * - * @return bool - */ - public function deleteFileByTitle( $title ) { - if ( $title->exists() ) { - $file = wfFindFile( $title, [ '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 string $fileName Filename to be removed - * - * @return bool - */ - 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 string $filePath Path to file on the filesystem - * - * @return bool - */ - 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 string $fieldName Name this would have in the upload form - * @param string $fileName Name to title this - * @param string $type MIME type - * @param string $filePath Path where to find file contents - * - * @throws Exception - * @return bool - */ - function fakeUploadFile( $fieldName, $fileName, $type, $filePath ) { - $tmpName = $this->getNewTempFile(); - 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] = [ - 'name' => $fileName, - 'type' => $type, - 'tmp_name' => $tmpName, - 'size' => $size, - 'error' => null - ]; - - return true; - } - - function fakeUploadChunk( $fieldName, $fileName, $type, & $chunkData ) { - $tmpName = $this->getNewTempFile(); - // 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] = [ - 'name' => $fileName, - 'type' => $type, - 'tmp_name' => $tmpName, - 'size' => $size, - 'error' => null - ]; - } - - /** - * Remove traces of previous fake uploads - */ - function clearFakeUploads() { - $_FILES = []; - } } diff --git a/www/wiki/tests/phpunit/includes/api/ApiUnblockTest.php b/www/wiki/tests/phpunit/includes/api/ApiUnblockTest.php index 971b63c3..d20de0dc 100644 --- a/www/wiki/tests/phpunit/includes/api/ApiUnblockTest.php +++ b/www/wiki/tests/phpunit/includes/api/ApiUnblockTest.php @@ -8,11 +8,6 @@ * @covers ApiUnblock */ class ApiUnblockTest extends ApiTestCase { - protected function setUp() { - parent::setUp(); - $this->doLogin(); - } - /** * @expectedException ApiUsageException */ @@ -22,10 +17,7 @@ class ApiUnblockTest extends ApiTestCase { 'action' => 'unblock', 'user' => 'UTApiBlockee', 'reason' => 'Some reason', - ], - null, - false, - self::$users['sysop']->getUser() + ] ); } } diff --git a/www/wiki/tests/phpunit/includes/api/ApiUploadTest.php b/www/wiki/tests/phpunit/includes/api/ApiUploadTest.php index 9b79e6c5..41c9aed4 100644 --- a/www/wiki/tests/phpunit/includes/api/ApiUploadTest.php +++ b/www/wiki/tests/phpunit/includes/api/ApiUploadTest.php @@ -19,8 +19,10 @@ * @group Database * @group medium * @group Broken + * + * @covers ApiUpload */ -class ApiUploadTest extends ApiTestCaseUpload { +class ApiUploadTest extends ApiUploadTestCase { /** * Testing login * XXX this is a funny way of getting session context @@ -51,7 +53,6 @@ class ApiUploadTest extends ApiTestCaseUpload { $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' ); @@ -69,7 +70,7 @@ class ApiUploadTest extends ApiTestCaseUpload { ] ); } catch ( ApiUsageException $e ) { $exception = true; - $this->assertEquals( 'The "token" parameter must be set', $e->getMessage() ); + $this->assertContains( 'The "token" parameter must be set', $e->getMessage() ); } $this->assertTrue( $exception, "Got exception" ); } @@ -85,8 +86,10 @@ class ApiUploadTest extends ApiTestCaseUpload { ], $session, self::$users['uploader']->getUser() ); } catch ( ApiUsageException $e ) { $exception = true; - $this->assertEquals( "One of the parameters filekey, file, url is required", - $e->getMessage() ); + $this->assertEquals( + 'One of the parameters "filekey", "file" and "url" is required.', + $e->getMessage() + ); } $this->assertTrue( $exception, "Got exception" ); } @@ -453,9 +456,9 @@ class ApiUploadTest extends ApiTestCaseUpload { $chunkSessionKey = false; $resultOffset = 0; // Open the file: - MediaWiki\suppressWarnings(); + Wikimedia\suppressWarnings(); $handle = fopen( $filePath, "r" ); - MediaWiki\restoreWarnings(); + Wikimedia\restoreWarnings(); if ( $handle === false ) { $this->markTestIncomplete( "could not open file: $filePath" ); @@ -463,9 +466,9 @@ class ApiUploadTest extends ApiTestCaseUpload { while ( !feof( $handle ) ) { // Get the current chunk - MediaWiki\suppressWarnings(); + Wikimedia\suppressWarnings(); $chunkData = fread( $handle, $chunkSize ); - MediaWiki\restoreWarnings(); + Wikimedia\restoreWarnings(); // Upload the current chunk into the $_FILE object: $this->fakeUploadChunk( 'chunk', 'blob', $mimeType, $chunkData ); diff --git a/www/wiki/tests/phpunit/includes/api/ApiUploadTestCase.php b/www/wiki/tests/phpunit/includes/api/ApiUploadTestCase.php new file mode 100644 index 00000000..3c7efd57 --- /dev/null +++ b/www/wiki/tests/phpunit/includes/api/ApiUploadTestCase.php @@ -0,0 +1,153 @@ +<?php + +/** + * Abstract class to support upload tests + */ +abstract class ApiUploadTestCase extends ApiTestCase { + /** + * Fixture -- run before every test + */ + protected function setUp() { + parent::setUp(); + + $this->setMwGlobals( [ + 'wgEnableUploads' => true, + 'wgEnableAPI' => true, + ] ); + + $this->clearFakeUploads(); + } + + /** + * Helper function -- remove files and associated articles by Title + * + * @param Title $title Title to be removed + * + * @return bool + */ + public function deleteFileByTitle( $title ) { + if ( $title->exists() ) { + $file = wfFindFile( $title, [ '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 string $fileName Filename to be removed + * + * @return bool + */ + 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 string $filePath Path to file on the filesystem + * + * @return bool + */ + 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 string $fieldName Name this would have in the upload form + * @param string $fileName Name to title this + * @param string $type MIME type + * @param string $filePath Path where to find file contents + * + * @throws Exception + * @return bool + */ + function fakeUploadFile( $fieldName, $fileName, $type, $filePath ) { + $tmpName = $this->getNewTempFile(); + 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] = [ + 'name' => $fileName, + 'type' => $type, + 'tmp_name' => $tmpName, + 'size' => $size, + 'error' => null + ]; + + return true; + } + + function fakeUploadChunk( $fieldName, $fileName, $type, & $chunkData ) { + $tmpName = $this->getNewTempFile(); + // 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] = [ + 'name' => $fileName, + 'type' => $type, + 'tmp_name' => $tmpName, + 'size' => $size, + 'error' => null + ]; + } + + /** + * Remove traces of previous fake uploads + */ + function clearFakeUploads() { + $_FILES = []; + } +} diff --git a/www/wiki/tests/phpunit/includes/api/ApiUsageExceptionTest.php b/www/wiki/tests/phpunit/includes/api/ApiUsageExceptionTest.php new file mode 100644 index 00000000..bb720211 --- /dev/null +++ b/www/wiki/tests/phpunit/includes/api/ApiUsageExceptionTest.php @@ -0,0 +1,44 @@ +<?php + +/** + * @covers ApiUsageException + */ +class ApiUsageExceptionTest extends MediaWikiTestCase { + + public function testCreateWithStatusValue_CanGetAMessageObject() { + $messageKey = 'some-message-key'; + $messageParameter = 'some-parameter'; + $statusValue = new StatusValue(); + $statusValue->fatal( $messageKey, $messageParameter ); + + $apiUsageException = new ApiUsageException( null, $statusValue ); + /** @var \Message $gotMessage */ + $gotMessage = $apiUsageException->getMessageObject(); + + $this->assertInstanceOf( \Message::class, $gotMessage ); + $this->assertEquals( $messageKey, $gotMessage->getKey() ); + $this->assertEquals( [ $messageParameter ], $gotMessage->getParams() ); + } + + public function testNewWithMessage_ThenGetMessageObject_ReturnsApiMessageWithProvidedData() { + $expectedMessage = new Message( 'some-message-key', [ 'some message parameter' ] ); + $expectedCode = 'some-error-code'; + $expectedData = [ 'some-error-data' ]; + + $apiUsageException = ApiUsageException::newWithMessage( + null, + $expectedMessage, + $expectedCode, + $expectedData + ); + /** @var \ApiMessage $gotMessage */ + $gotMessage = $apiUsageException->getMessageObject(); + + $this->assertInstanceOf( \ApiMessage::class, $gotMessage ); + $this->assertEquals( $expectedMessage->getKey(), $gotMessage->getKey() ); + $this->assertEquals( $expectedMessage->getParams(), $gotMessage->getParams() ); + $this->assertEquals( $expectedCode, $gotMessage->getApiCode() ); + $this->assertEquals( $expectedData, $gotMessage->getApiData() ); + } + +} diff --git a/www/wiki/tests/phpunit/includes/api/ApiUserrightsTest.php b/www/wiki/tests/phpunit/includes/api/ApiUserrightsTest.php new file mode 100644 index 00000000..0229e767 --- /dev/null +++ b/www/wiki/tests/phpunit/includes/api/ApiUserrightsTest.php @@ -0,0 +1,358 @@ +<?php + +/** + * @group API + * @group Database + * @group medium + * + * @covers ApiUserrights + */ +class ApiUserrightsTest extends ApiTestCase { + /** + * Unsets $wgGroupPermissions['bureaucrat']['userrights'], and sets + * $wgAddGroups['bureaucrat'] and $wgRemoveGroups['bureaucrat'] to the + * specified values. + * + * @param array|bool $add Groups bureaucrats should be allowed to add, true for all + * @param array|bool $remove Groups bureaucrats should be allowed to remove, true for all + */ + protected function setPermissions( $add = [], $remove = [] ) { + global $wgAddGroups, $wgRemoveGroups; + + $this->setGroupPermissions( 'bureaucrat', 'userrights', false ); + + if ( $add ) { + $this->stashMwGlobals( 'wgAddGroups' ); + $wgAddGroups['bureaucrat'] = $add; + } + if ( $remove ) { + $this->stashMwGlobals( 'wgRemoveGroups' ); + $wgRemoveGroups['bureaucrat'] = $remove; + } + } + + /** + * Perform an API userrights request that's expected to be successful. + * + * @param array|string $expectedGroups Group(s) that the user is expected + * to have after the API request + * @param array $params Array to pass to doApiRequestWithToken(). 'action' + * => 'userrights' is implicit. If no 'user' or 'userid' is specified, + * we add a 'user' parameter. If no 'add' or 'remove' is specified, we + * add 'add' => 'sysop'. + * @param User|null $user The user that we're modifying. The user must be + * mutable, because we're going to change its groups! null means that + * we'll make up our own user to modify, and doesn't make sense if 'user' + * or 'userid' is specified in $params. + */ + protected function doSuccessfulRightsChange( + $expectedGroups = 'sysop', array $params = [], User $user = null + ) { + $expectedGroups = (array)$expectedGroups; + $params['action'] = 'userrights'; + + if ( !$user ) { + $user = $this->getMutableTestUser()->getUser(); + } + + $this->assertTrue( TestUserRegistry::isMutable( $user ), + 'Immutable user passed to doSuccessfulRightsChange!' ); + + if ( !isset( $params['user'] ) && !isset( $params['userid'] ) ) { + $params['user'] = $user->getName(); + } + if ( !isset( $params['add'] ) && !isset( $params['remove'] ) ) { + $params['add'] = 'sysop'; + } + + $res = $this->doApiRequestWithToken( $params ); + + $user->clearInstanceCache(); + $this->assertSame( $expectedGroups, $user->getGroups() ); + + $this->assertArrayNotHasKey( 'warnings', $res[0] ); + } + + /** + * Perform an API userrights request that's expected to fail. + * + * @param string $expectedException Expected exception text + * @param array $params As for doSuccessfulRightsChange() + * @param User|null $user As for doSuccessfulRightsChange(). If there's no + * user who will possibly be affected (such as if an invalid username is + * provided in $params), pass null. + */ + protected function doFailedRightsChange( + $expectedException, array $params = [], User $user = null + ) { + $params['action'] = 'userrights'; + + $this->setExpectedException( ApiUsageException::class, $expectedException ); + + if ( !$user ) { + // If 'user' or 'userid' is specified and $user was not specified, + // the user we're creating now will have nothing to do with the API + // request, but that's okay, since we're just testing that it has + // no groups. + $user = $this->getMutableTestUser()->getUser(); + } + + $this->assertTrue( TestUserRegistry::isMutable( $user ), + 'Immutable user passed to doFailedRightsChange!' ); + + if ( !isset( $params['user'] ) && !isset( $params['userid'] ) ) { + $params['user'] = $user->getName(); + } + if ( !isset( $params['add'] ) && !isset( $params['remove'] ) ) { + $params['add'] = 'sysop'; + } + $expectedGroups = $user->getGroups(); + + try { + $this->doApiRequestWithToken( $params ); + } finally { + $user->clearInstanceCache(); + $this->assertSame( $expectedGroups, $user->getGroups() ); + } + } + + public function testAdd() { + $this->doSuccessfulRightsChange(); + } + + public function testBlockedWithUserrights() { + global $wgUser; + + $block = new Block( [ 'address' => $wgUser, 'by' => $wgUser->getId(), ] ); + $block->insert(); + + try { + $this->doSuccessfulRightsChange(); + } finally { + $block->delete(); + $wgUser->clearInstanceCache(); + } + } + + public function testBlockedWithoutUserrights() { + $user = $this->getTestSysop()->getUser(); + + $this->setPermissions( true, true ); + + $block = new Block( [ 'address' => $user, 'by' => $user->getId() ] ); + $block->insert(); + + try { + $this->doFailedRightsChange( 'You have been blocked from editing.' ); + } finally { + $block->delete(); + $user->clearInstanceCache(); + } + } + + public function testAddMultiple() { + $this->doSuccessfulRightsChange( + [ 'bureaucrat', 'sysop' ], + [ 'add' => 'bureaucrat|sysop' ] + ); + } + + public function testTooFewExpiries() { + $this->doFailedRightsChange( + '2 expiry timestamps were provided where 3 were needed.', + [ 'add' => 'sysop|bureaucrat|bot', 'expiry' => 'infinity|tomorrow' ] + ); + } + + public function testTooManyExpiries() { + $this->doFailedRightsChange( + '3 expiry timestamps were provided where 2 were needed.', + [ 'add' => 'sysop|bureaucrat', 'expiry' => 'infinity|tomorrow|never' ] + ); + } + + public function testInvalidExpiry() { + $this->doFailedRightsChange( 'Invalid expiry time', [ 'expiry' => 'yummy lollipops!' ] ); + } + + public function testMultipleInvalidExpiries() { + $this->doFailedRightsChange( + 'Invalid expiry time "foo".', + [ 'add' => 'sysop|bureaucrat', 'expiry' => 'foo|bar' ] + ); + } + + public function testWithTag() { + ChangeTags::defineTag( 'custom tag' ); + + $user = $this->getMutableTestUser()->getUser(); + + $this->doSuccessfulRightsChange( 'sysop', [ 'tags' => 'custom tag' ], $user ); + + $dbr = wfGetDB( DB_REPLICA ); + $this->assertSame( + 'custom tag', + $dbr->selectField( + [ 'change_tag', 'logging' ], + 'ct_tag', + [ + 'ct_log_id = log_id', + 'log_namespace' => NS_USER, + 'log_title' => strtr( $user->getName(), ' ', '_' ) + ], + __METHOD__ + ) + ); + } + + public function testWithoutTagPermission() { + global $wgGroupPermissions; + + ChangeTags::defineTag( 'custom tag' ); + + $this->stashMwGlobals( 'wgGroupPermissions' ); + $wgGroupPermissions['user']['applychangetags'] = false; + + $this->doFailedRightsChange( + 'You do not have permission to apply change tags along with your changes.', + [ 'tags' => 'custom tag' ] + ); + } + + public function testNonexistentUser() { + $this->doFailedRightsChange( + 'There is no user by the name "Nonexistent user". Check your spelling.', + [ 'user' => 'Nonexistent user' ] + ); + } + + public function testWebToken() { + $sysop = $this->getTestSysop()->getUser(); + $user = $this->getMutableTestUser()->getUser(); + + $token = $sysop->getEditToken( $user->getName() ); + + $res = $this->doApiRequest( [ + 'action' => 'userrights', + 'user' => $user->getName(), + 'add' => 'sysop', + 'token' => $token, + ] ); + + $user->clearInstanceCache(); + $this->assertSame( [ 'sysop' ], $user->getGroups() ); + + $this->assertArrayNotHasKey( 'warnings', $res[0] ); + } + + /** + * Helper for testCanProcessExpiries that returns a mock ApiUserrights that either can or cannot + * process expiries. Although the regular page can process expiries, we use a mock here to + * ensure that it's the result of canProcessExpiries() that makes a difference, and not some + * error in the way we construct the mock. + * + * @param bool $canProcessExpiries + */ + private function getMockForProcessingExpiries( $canProcessExpiries ) { + $sysop = $this->getTestSysop()->getUser(); + $user = $this->getMutableTestUser()->getUser(); + + $token = $sysop->getEditToken( 'userrights' ); + + $main = new ApiMain( new FauxRequest( [ + 'action' => 'userrights', + 'user' => $user->getName(), + 'add' => 'sysop', + 'token' => $token, + ] ) ); + + $mockUserRightsPage = $this->getMockBuilder( UserrightsPage::class ) + ->setMethods( [ 'canProcessExpiries' ] ) + ->getMock(); + $mockUserRightsPage->method( 'canProcessExpiries' )->willReturn( $canProcessExpiries ); + + $mockApi = $this->getMockBuilder( ApiUserrights::class ) + ->setConstructorArgs( [ $main, 'userrights' ] ) + ->setMethods( [ 'getUserRightsPage' ] ) + ->getMock(); + $mockApi->method( 'getUserRightsPage' )->willReturn( $mockUserRightsPage ); + + return $mockApi; + } + + public function testCanProcessExpiries() { + $mock1 = $this->getMockForProcessingExpiries( true ); + $this->assertArrayHasKey( 'expiry', $mock1->getAllowedParams() ); + + $mock2 = $this->getMockForProcessingExpiries( false ); + $this->assertArrayNotHasKey( 'expiry', $mock2->getAllowedParams() ); + } + + /** + * Tests adding and removing various groups with various permissions. + * + * @dataProvider addAndRemoveGroupsProvider + * @param array|null $permissions [ [ $wgAddGroups, $wgRemoveGroups ] ] or null for 'userrights' + * to be set in $wgGroupPermissions + * @param array $groupsToChange [ [ groups to add ], [ groups to remove ] ] + * @param array $expectedGroups Array of expected groups + */ + public function testAddAndRemoveGroups( + array $permissions = null, array $groupsToChange, array $expectedGroups + ) { + if ( $permissions !== null ) { + $this->setPermissions( $permissions[0], $permissions[1] ); + } + + $params = [ + 'add' => implode( '|', $groupsToChange[0] ), + 'remove' => implode( '|', $groupsToChange[1] ), + ]; + + // We'll take a bot so we have a group to remove + $user = $this->getMutableTestUser( [ 'bot' ] )->getUser(); + + $this->doSuccessfulRightsChange( $expectedGroups, $params, $user ); + } + + public function addAndRemoveGroupsProvider() { + return [ + 'Simple add' => [ + [ [ 'sysop' ], [] ], + [ [ 'sysop' ], [] ], + [ 'bot', 'sysop' ] + ], 'Add with only remove permission' => [ + [ [], [ 'sysop' ] ], + [ [ 'sysop' ], [] ], + [ 'bot' ], + ], 'Add with global remove permission' => [ + [ [], true ], + [ [ 'sysop' ], [] ], + [ 'bot' ], + ], 'Simple remove' => [ + [ [], [ 'bot' ] ], + [ [], [ 'bot' ] ], + [], + ], 'Remove with only add permission' => [ + [ [ 'bot' ], [] ], + [ [], [ 'bot' ] ], + [ 'bot' ], + ], 'Remove with global add permission' => [ + [ true, [] ], + [ [], [ 'bot' ] ], + [ 'bot' ], + ], 'Add and remove same new group' => [ + null, + [ [ 'sysop' ], [ 'sysop' ] ], + // The userrights code does removals before adds, so it doesn't remove the sysop + // group here and only adds it. + [ 'bot', 'sysop' ], + ], 'Add and remove same existing group' => [ + null, + [ [ 'bot' ], [ 'bot' ] ], + // But here it first removes the existing group and then re-adds it. + [ 'bot' ], + ], + ]; + } +} diff --git a/www/wiki/tests/phpunit/includes/api/ApiWatchTest.php b/www/wiki/tests/phpunit/includes/api/ApiWatchTest.php index 7b91094b..6d64a178 100644 --- a/www/wiki/tests/phpunit/includes/api/ApiWatchTest.php +++ b/www/wiki/tests/phpunit/includes/api/ApiWatchTest.php @@ -5,13 +5,10 @@ * @group Database * @group medium * @todo This test suite is severly broken and need a full review + * + * @covers ApiWatch */ class ApiWatchTest extends ApiTestCase { - protected function setUp() { - parent::setUp(); - $this->doLogin(); - } - function getTokens() { return $this->getTokenList( self::$users['sysop'] ); } diff --git a/www/wiki/tests/phpunit/includes/api/format/ApiFormatBaseTest.php b/www/wiki/tests/phpunit/includes/api/format/ApiFormatBaseTest.php new file mode 100644 index 00000000..55f760f6 --- /dev/null +++ b/www/wiki/tests/phpunit/includes/api/format/ApiFormatBaseTest.php @@ -0,0 +1,388 @@ +<?php + +use Wikimedia\TestingAccessWrapper; + +/** + * @group API + * @covers ApiFormatBase + */ +class ApiFormatBaseTest extends ApiFormatTestBase { + + protected $printerName = 'mockbase'; + + public function getMockFormatter( ApiMain $main = null, $format, $methods = [] ) { + if ( $main === null ) { + $context = new RequestContext; + $context->setRequest( new FauxRequest( [], true ) ); + $main = new ApiMain( $context ); + } + + $mock = $this->getMockBuilder( ApiFormatBase::class ) + ->setConstructorArgs( [ $main, $format ] ) + ->setMethods( array_unique( array_merge( $methods, [ 'getMimeType', 'execute' ] ) ) ) + ->getMock(); + if ( !in_array( 'getMimeType', $methods, true ) ) { + $mock->method( 'getMimeType' )->willReturn( 'text/x-mock' ); + } + return $mock; + } + + protected function encodeData( array $params, array $data, $options = [] ) { + $options += [ + 'name' => 'mock', + 'class' => ApiFormatBase::class, + 'factory' => function ( ApiMain $main, $format ) use ( $options ) { + $mock = $this->getMockFormatter( $main, $format ); + $mock->expects( $this->once() )->method( 'execute' ) + ->willReturnCallback( function () use ( $mock ) { + $mock->printText( "Format {$mock->getFormat()}: " ); + $mock->printText( "<b>ok</b>" ); + } ); + + if ( isset( $options['status'] ) ) { + $mock->setHttpStatus( $options['status'] ); + } + + return $mock; + }, + 'returnPrinter' => true, + ]; + + $this->setMwGlobals( [ + 'wgApiFrameOptions' => 'DENY', + ] ); + + $ret = parent::encodeData( $params, $data, $options ); + $printer = TestingAccessWrapper::newFromObject( $ret['printer'] ); + $text = $ret['text']; + + if ( $options['name'] !== 'mockfm' ) { + $ct = 'text/x-mock'; + $file = 'api-result.mock'; + $status = isset( $options['status'] ) ? $options['status'] : null; + } elseif ( isset( $params['wrappedhtml'] ) ) { + $ct = 'text/mediawiki-api-prettyprint-wrapped'; + $file = 'api-result-wrapped.json'; + $status = null; + + // Replace varying field + $text = preg_replace( '/"time":\d+/', '"time":1234', $text ); + } else { + $ct = 'text/html'; + $file = 'api-result.html'; + $status = null; + + // Strip OutputPage-generated HTML + if ( preg_match( '!<pre class="api-pretty-content">.*</pre>!s', $text, $m ) ) { + $text = $m[0]; + } + } + + $response = $printer->getMain()->getRequest()->response(); + $this->assertSame( "$ct; charset=utf-8", strtolower( $response->getHeader( 'Content-Type' ) ) ); + $this->assertSame( 'DENY', $response->getHeader( 'X-Frame-Options' ) ); + $this->assertSame( $file, $printer->getFilename() ); + $this->assertSame( "inline; filename=$file", $response->getHeader( 'Content-Disposition' ) ); + $this->assertSame( $status, $response->getStatusCode() ); + + return $text; + } + + public static function provideGeneralEncoding() { + return [ + 'normal' => [ + [], + "Format MOCK: <b>ok</b>", + [], + [ 'name' => 'mock' ] + ], + 'normal ignores wrappedhtml' => [ + [], + "Format MOCK: <b>ok</b>", + [ 'wrappedhtml' => 1 ], + [ 'name' => 'mock' ] + ], + 'HTML format' => [ + [], + '<pre class="api-pretty-content">Format MOCK: <b>ok</b></pre>', + [], + [ 'name' => 'mockfm' ] + ], + 'wrapped HTML format' => [ + [], + // phpcs:ignore Generic.Files.LineLength.TooLong + '{"status":200,"statustext":"OK","html":"<pre class=\"api-pretty-content\">Format MOCK: <b>ok</b></pre>","modules":["mediawiki.apipretty"],"continue":null,"time":1234}', + [ 'wrappedhtml' => 1 ], + [ 'name' => 'mockfm' ] + ], + 'normal, with set status' => [ + [], + "Format MOCK: <b>ok</b>", + [], + [ 'name' => 'mock', 'status' => 400 ] + ], + 'HTML format, with set status' => [ + [], + '<pre class="api-pretty-content">Format MOCK: <b>ok</b></pre>', + [], + [ 'name' => 'mockfm', 'status' => 400 ] + ], + 'wrapped HTML format, with set status' => [ + [], + // phpcs:ignore Generic.Files.LineLength.TooLong + '{"status":400,"statustext":"Bad Request","html":"<pre class=\"api-pretty-content\">Format MOCK: <b>ok</b></pre>","modules":["mediawiki.apipretty"],"continue":null,"time":1234}', + [ 'wrappedhtml' => 1 ], + [ 'name' => 'mockfm', 'status' => 400 ] + ], + 'wrapped HTML format, cross-domain-policy' => [ + [ 'continue' => '< CrOsS-DoMaIn-PoLiCy >' ], + // phpcs:ignore Generic.Files.LineLength.TooLong + '{"status":200,"statustext":"OK","html":"<pre class=\"api-pretty-content\">Format MOCK: <b>ok</b></pre>","modules":["mediawiki.apipretty"],"continue":"\u003C CrOsS-DoMaIn-PoLiCy \u003E","time":1234}', + [ 'wrappedhtml' => 1 ], + [ 'name' => 'mockfm' ] + ], + ]; + } + + /** + * @dataProvider provideFilenameEncoding + */ + public function testFilenameEncoding( $filename, $expect ) { + $ret = parent::encodeData( [], [], [ + 'name' => 'mock', + 'class' => ApiFormatBase::class, + 'factory' => function ( ApiMain $main, $format ) use ( $filename ) { + $mock = $this->getMockFormatter( $main, $format, [ 'getFilename' ] ); + $mock->method( 'getFilename' )->willReturn( $filename ); + return $mock; + }, + 'returnPrinter' => true, + ] ); + $response = $ret['printer']->getMain()->getRequest()->response(); + + $this->assertSame( "inline; $expect", $response->getHeader( 'Content-Disposition' ) ); + } + + public static function provideFilenameEncoding() { + return [ + 'something simple' => [ + 'foo.xyz', 'filename=foo.xyz' + ], + 'more complicated, but still simple' => [ + 'foo.!#$%&\'*+-^_`|~', 'filename=foo.!#$%&\'*+-^_`|~' + ], + 'Needs quoting' => [ + 'foo\\bar.xyz', 'filename="foo\\\\bar.xyz"' + ], + 'Needs quoting (2)' => [ + 'foo (bar).xyz', 'filename="foo (bar).xyz"' + ], + 'Needs quoting (3)' => [ + "foo\t\"b\x5car\"\0.xyz", "filename=\"foo\x5c\t\x5c\"b\x5c\x5car\x5c\"\x5c\0.xyz\"" + ], + 'Non-ASCII characters' => [ + 'fóo bár.🙌!', + "filename=\"f\xF3o b\xE1r.?!\"; filename*=UTF-8''f%C3%B3o%20b%C3%A1r.%F0%9F%99%8C!" + ] + ]; + } + + public function testBasics() { + $printer = $this->getMockFormatter( null, 'mock' ); + $this->assertTrue( $printer->canPrintErrors() ); + $this->assertSame( + 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Data_formats', + $printer->getHelpUrls() + ); + } + + public function testDisable() { + $this->setMwGlobals( [ + 'wgApiFrameOptions' => 'DENY', + ] ); + + $printer = $this->getMockFormatter( null, 'mock' ); + $printer->method( 'execute' )->willReturnCallback( function () use ( $printer ) { + $printer->printText( 'Foo' ); + } ); + $this->assertFalse( $printer->isDisabled() ); + $printer->disable(); + $this->assertTrue( $printer->isDisabled() ); + + $printer->setHttpStatus( 400 ); + $printer->initPrinter(); + $printer->execute(); + ob_start(); + $printer->closePrinter(); + $this->assertSame( '', ob_get_clean() ); + $response = $printer->getMain()->getRequest()->response(); + $this->assertNull( $response->getHeader( 'Content-Type' ) ); + $this->assertNull( $response->getHeader( 'X-Frame-Options' ) ); + $this->assertNull( $response->getHeader( 'Content-Disposition' ) ); + $this->assertNull( $response->getStatusCode() ); + } + + public function testNullMimeType() { + $this->setMwGlobals( [ + 'wgApiFrameOptions' => 'DENY', + ] ); + + $printer = $this->getMockFormatter( null, 'mock', [ 'getMimeType' ] ); + $printer->method( 'execute' )->willReturnCallback( function () use ( $printer ) { + $printer->printText( 'Foo' ); + } ); + $printer->method( 'getMimeType' )->willReturn( null ); + $this->assertNull( $printer->getMimeType(), 'sanity check' ); + + $printer->initPrinter(); + $printer->execute(); + ob_start(); + $printer->closePrinter(); + $this->assertSame( 'Foo', ob_get_clean() ); + $response = $printer->getMain()->getRequest()->response(); + $this->assertNull( $response->getHeader( 'Content-Type' ) ); + $this->assertNull( $response->getHeader( 'X-Frame-Options' ) ); + $this->assertNull( $response->getHeader( 'Content-Disposition' ) ); + + $printer = $this->getMockFormatter( null, 'mockfm', [ 'getMimeType' ] ); + $printer->method( 'execute' )->willReturnCallback( function () use ( $printer ) { + $printer->printText( 'Foo' ); + } ); + $printer->method( 'getMimeType' )->willReturn( null ); + $this->assertNull( $printer->getMimeType(), 'sanity check' ); + $this->assertTrue( $printer->getIsHtml(), 'sanity check' ); + + $printer->initPrinter(); + $printer->execute(); + ob_start(); + $printer->closePrinter(); + $this->assertSame( 'Foo', ob_get_clean() ); + $response = $printer->getMain()->getRequest()->response(); + $this->assertSame( + 'text/html; charset=utf-8', strtolower( $response->getHeader( 'Content-Type' ) ) + ); + $this->assertSame( 'DENY', $response->getHeader( 'X-Frame-Options' ) ); + $this->assertSame( + 'inline; filename=api-result.html', $response->getHeader( 'Content-Disposition' ) + ); + } + + public function testApiFrameOptions() { + $this->setMwGlobals( [ 'wgApiFrameOptions' => 'DENY' ] ); + $printer = $this->getMockFormatter( null, 'mock' ); + $printer->initPrinter(); + $this->assertSame( + 'DENY', + $printer->getMain()->getRequest()->response()->getHeader( 'X-Frame-Options' ) + ); + + $this->setMwGlobals( [ 'wgApiFrameOptions' => 'SAMEORIGIN' ] ); + $printer = $this->getMockFormatter( null, 'mock' ); + $printer->initPrinter(); + $this->assertSame( + 'SAMEORIGIN', + $printer->getMain()->getRequest()->response()->getHeader( 'X-Frame-Options' ) + ); + + $this->setMwGlobals( [ 'wgApiFrameOptions' => false ] ); + $printer = $this->getMockFormatter( null, 'mock' ); + $printer->initPrinter(); + $this->assertNull( + $printer->getMain()->getRequest()->response()->getHeader( 'X-Frame-Options' ) + ); + } + + public function testForceDefaultParams() { + $context = new RequestContext; + $context->setRequest( new FauxRequest( [ 'foo' => '1', 'bar' => '2', 'baz' => '3' ], true ) ); + $main = new ApiMain( $context ); + $allowedParams = [ + 'foo' => [], + 'bar' => [ ApiBase::PARAM_DFLT => 'bar?' ], + 'baz' => 'baz!', + ]; + + $printer = $this->getMockFormatter( $main, 'mock', [ 'getAllowedParams' ] ); + $printer->method( 'getAllowedParams' )->willReturn( $allowedParams ); + $this->assertEquals( + [ 'foo' => '1', 'bar' => '2', 'baz' => '3' ], + $printer->extractRequestParams(), + 'sanity check' + ); + + $printer = $this->getMockFormatter( $main, 'mock', [ 'getAllowedParams' ] ); + $printer->method( 'getAllowedParams' )->willReturn( $allowedParams ); + $printer->forceDefaultParams(); + $this->assertEquals( + [ 'foo' => null, 'bar' => 'bar?', 'baz' => 'baz!' ], + $printer->extractRequestParams() + ); + } + + public function testGetAllowedParams() { + $printer = $this->getMockFormatter( null, 'mock' ); + $this->assertSame( [], $printer->getAllowedParams() ); + + $printer = $this->getMockFormatter( null, 'mockfm' ); + $this->assertSame( [ + 'wrappedhtml' => [ + ApiBase::PARAM_DFLT => false, + ApiBase::PARAM_HELP_MSG => 'apihelp-format-param-wrappedhtml', + ] + ], $printer->getAllowedParams() ); + } + + public function testGetExamplesMessages() { + $printer = TestingAccessWrapper::newFromObject( $this->getMockFormatter( null, 'mock' ) ); + $this->assertSame( [ + 'action=query&meta=siteinfo&siprop=namespaces&format=mock' + => [ 'apihelp-format-example-generic', 'MOCK' ] + ], $printer->getExamplesMessages() ); + + $printer = TestingAccessWrapper::newFromObject( $this->getMockFormatter( null, 'mockfm' ) ); + $this->assertSame( [ + 'action=query&meta=siteinfo&siprop=namespaces&format=mockfm' + => [ 'apihelp-format-example-generic', 'MOCK' ] + ], $printer->getExamplesMessages() ); + } + + /** + * @dataProvider provideHtmlHeader + */ + public function testHtmlHeader( $post, $registerNonHtml, $expect ) { + $context = new RequestContext; + $request = new FauxRequest( [ 'a' => 1, 'b' => 2 ], $post ); + $request->setRequestURL( 'http://example.org/wx/api.php' ); + $context->setRequest( $request ); + $context->setLanguage( 'qqx' ); + $main = new ApiMain( $context ); + $printer = $this->getMockFormatter( $main, 'mockfm' ); + $mm = $printer->getMain()->getModuleManager(); + $mm->addModule( 'mockfm', 'format', ApiFormatBase::class, function () { + return $mock; + } ); + if ( $registerNonHtml ) { + $mm->addModule( 'mock', 'format', ApiFormatBase::class, function () { + return $mock; + } ); + } + + $printer->initPrinter(); + $printer->execute(); + ob_start(); + $printer->closePrinter(); + $text = ob_get_clean(); + $this->assertContains( $expect, $text ); + } + + public static function provideHtmlHeader() { + return [ + [ false, false, '(api-format-prettyprint-header-only-html: MOCK)' ], + [ true, false, '(api-format-prettyprint-header-only-html: MOCK)' ], + // phpcs:ignore Generic.Files.LineLength.TooLong + [ false, true, '(api-format-prettyprint-header-hyperlinked: MOCK, mock, <a rel="nofollow" class="external free" href="http://example.org/wx/api.php?a=1&b=2&format=mock">http://example.org/wx/api.php?a=1&b=2&format=mock</a>)' ], + [ true, true, '(api-format-prettyprint-header: MOCK, mock)' ], + ]; + } + +} diff --git a/www/wiki/tests/phpunit/includes/api/format/ApiFormatPhpTest.php b/www/wiki/tests/phpunit/includes/api/format/ApiFormatPhpTest.php index 3aa1db30..66e620e8 100644 --- a/www/wiki/tests/phpunit/includes/api/format/ApiFormatPhpTest.php +++ b/www/wiki/tests/phpunit/includes/api/format/ApiFormatPhpTest.php @@ -20,7 +20,7 @@ class ApiFormatPhpTest extends ApiFormatTestBase { } public static function provideGeneralEncoding() { - // @codingStandardsIgnoreStart Generic.Files.LineLength + // phpcs:disable Generic.Files.LineLength return array_merge( self::addFormatVersion( 1, [ // Basic types @@ -97,7 +97,7 @@ class ApiFormatPhpTest extends ApiFormatTestBase { 'a:1:{s:3:"foo";s:3:"foo";}' ], ] ) ); - // @codingStandardsIgnoreEnd + // phpcs:enable } public function testCrossDomainMangling() { @@ -110,14 +110,8 @@ class ApiFormatPhpTest extends ApiFormatTestBase { $main = new ApiMain( $context ); $main->getResult()->addValue( null, null, '< Cross-Domain-Policy >' ); - if ( !function_exists( 'wfOutputHandler' ) ) { - function wfOutputHandler( $s ) { - return $s; - } - } - $printer = $main->createPrinterByName( 'php' ); - ob_start( 'wfOutputHandler' ); + ob_start( 'MediaWiki\\OutputHandler::handle' ); $printer->initPrinter(); $printer->execute(); $printer->closePrinter(); @@ -126,7 +120,7 @@ class ApiFormatPhpTest extends ApiFormatTestBase { $config->set( 'MangleFlashPolicy', true ); $printer = $main->createPrinterByName( 'php' ); - ob_start( 'wfOutputHandler' ); + ob_start( 'MediaWiki\\OutputHandler::handle' ); try { $printer->initPrinter(); $printer->execute(); diff --git a/www/wiki/tests/phpunit/includes/api/format/ApiFormatRawTest.php b/www/wiki/tests/phpunit/includes/api/format/ApiFormatRawTest.php new file mode 100644 index 00000000..f64af6d3 --- /dev/null +++ b/www/wiki/tests/phpunit/includes/api/format/ApiFormatRawTest.php @@ -0,0 +1,120 @@ +<?php + +/** + * @group API + * @covers ApiFormatRaw + */ +class ApiFormatRawTest extends ApiFormatTestBase { + + protected $printerName = 'raw'; + + /** + * Test basic encoding and missing mime and text exceptions + * @return array datasets + */ + public static function provideGeneralEncoding() { + $options = [ + 'class' => ApiFormatRaw::class, + 'factory' => function ( ApiMain $main ) { + return new ApiFormatRaw( $main, new ApiFormatJson( $main, 'json' ) ); + } + ]; + + return [ + [ + [ 'mime' => 'text/plain', 'text' => 'foo' ], + 'foo', + [], + $options + ], + [ + [ 'mime' => 'text/plain', 'text' => 'fóo' ], + 'fóo', + [], + $options + ], + [ + [ 'text' => 'some text' ], + new MWException( 'No MIME type set for raw formatter' ), + [], + $options + ], + [ + [ 'mime' => 'text/plain' ], + new MWException( 'No text given for raw formatter' ), + [], + $options + ], + 'test error fallback' => [ + [ 'mime' => 'text/plain', 'text' => 'some text', 'error' => 'some error' ], + '{"mime":"text/plain","text":"some text","error":"some error"}', + [], + $options + ] + ]; + } + + /** + * Test specifying filename + */ + public function testFilename() { + $printer = new ApiFormatRaw( new ApiMain ); + $printer->getResult()->addValue( null, 'filename', 'whatever.raw' ); + $this->assertSame( 'whatever.raw', $printer->getFilename() ); + } + + /** + * Test specifying filename with error fallback printer + */ + public function testErrorFallbackFilename() { + $apiMain = new ApiMain; + $printer = new ApiFormatRaw( $apiMain, new ApiFormatJson( $apiMain, 'json' ) ); + $printer->getResult()->addValue( null, 'error', 'some error' ); + $printer->getResult()->addValue( null, 'filename', 'whatever.raw' ); + $this->assertSame( 'api-result.json', $printer->getFilename() ); + } + + /** + * Test specifying mime + */ + public function testMime() { + $printer = new ApiFormatRaw( new ApiMain ); + $printer->getResult()->addValue( null, 'mime', 'text/plain' ); + $this->assertSame( 'text/plain', $printer->getMimeType() ); + } + + /** + * Test specifying mime with error fallback printer + */ + public function testErrorFallbackMime() { + $apiMain = new ApiMain; + $printer = new ApiFormatRaw( $apiMain, new ApiFormatJson( $apiMain, 'json' ) ); + $printer->getResult()->addValue( null, 'error', 'some error' ); + $printer->getResult()->addValue( null, 'mime', 'text/plain' ); + $this->assertSame( 'application/json', $printer->getMimeType() ); + } + + /** + * Check that setting failWithHTTPError to true will result in 400 response status code + */ + public function testFailWithHTTPError() { + $apiMain = null; + + $this->testGeneralEncoding( + [ 'mime' => 'text/plain', 'text' => 'some text', 'error' => 'some error' ], + '{"mime":"text/plain","text":"some text","error":"some error"}', + [], + [ + 'class' => ApiFormatRaw::class, + 'factory' => function ( ApiMain $main ) use ( &$apiMain ) { + $apiMain = $main; + $printer = new ApiFormatRaw( $main, new ApiFormatJson( $main, 'json' ) ); + $printer->setFailWithHTTPError( true ); + return $printer; + } + ] + ); + $this->assertEquals( 400, $apiMain->getRequest()->response()->getStatusCode() ); + } + +} diff --git a/www/wiki/tests/phpunit/includes/api/format/ApiFormatTestBase.php b/www/wiki/tests/phpunit/includes/api/format/ApiFormatTestBase.php index fb086e99..4169dab2 100644 --- a/www/wiki/tests/phpunit/includes/api/format/ApiFormatTestBase.php +++ b/www/wiki/tests/phpunit/includes/api/format/ApiFormatTestBase.php @@ -11,26 +11,40 @@ abstract class ApiFormatTestBase extends MediaWikiTestCase { /** * Return general data to be encoded for testing * @return array See self::testGeneralEncoding - * @throws Exception + * @throws BadMethodCallException */ public static function provideGeneralEncoding() { - throw new Exception( 'Subclass must implement ' . __METHOD__ ); + throw new BadMethodCallException( static::class . ' must implement ' . __METHOD__ ); } /** * Get the formatter output for the given input data * @param array $params Query parameters * @param array $data Data to encode - * @param string $class Printer class to use instead of the normal one - * @return string + * @param array $options Options. If passed a string, the string is treated + * as the 'class' option. + * - name: Format name, rather than $this->printerName + * - class: If set, register 'name' with this class (and 'factory', if that's set) + * - factory: Used with 'class' to register at runtime + * - returnPrinter: Return the printer object + * @param callable|null $factory Factory to use instead of the normal one + * @return string|array The string if $options['returnPrinter'] isn't set, or an array if it is: + * - text: Output text string + * - printer: ApiFormatBase * @throws Exception */ - protected function encodeData( array $params, array $data, $class = null ) { + protected function encodeData( array $params, array $data, $options = [] ) { + if ( is_string( $options ) ) { + $options = [ 'class' => $options ]; + } + $printerName = isset( $options['name'] ) ? $options['name'] : $this->printerName; + $context = new RequestContext; $context->setRequest( new FauxRequest( $params, true ) ); $main = new ApiMain( $context ); - if ( $class !== null ) { - $main->getModuleManager()->addModule( $this->printerName, 'format', $class ); + if ( isset( $options['class'] ) ) { + $factory = isset( $options['factory'] ) ? $options['factory'] : null; + $main->getModuleManager()->addModule( $printerName, 'format', $options['class'], $factory ); } $result = $main->getResult(); $result->addArrayType( null, 'default' ); @@ -38,27 +52,42 @@ abstract class ApiFormatTestBase extends MediaWikiTestCase { $result->addValue( null, $k, $v ); } - $printer = $main->createPrinterByName( $this->printerName ); + $ret = []; + $printer = $main->createPrinterByName( $printerName ); $printer->initPrinter(); $printer->execute(); ob_start(); try { $printer->closePrinter(); - return ob_get_clean(); + $ret['text'] = ob_get_clean(); } catch ( Exception $ex ) { ob_end_clean(); throw $ex; } + + if ( !empty( $options['returnPrinter'] ) ) { + $ret['printer'] = $printer; + } + + return count( $ret ) === 1 ? $ret['text'] : $ret; } /** * @dataProvider provideGeneralEncoding + * @param array $data Data to be encoded + * @param string|Exception $expect String to expect, or exception expected to be thrown + * @param array $params Query parameters to set in the FauxRequest + * @param array $options Options to pass to self::encodeData() */ - public function testGeneralEncoding( array $data, $expect, array $params = [] ) { - if ( isset( $params['SKIP'] ) ) { - $this->markTestSkipped( $expect ); + public function testGeneralEncoding( + array $data, $expect, array $params = [], array $options = [] + ) { + if ( $expect instanceof Exception ) { + $this->setExpectedException( get_class( $expect ), $expect->getMessage() ); + $this->encodeData( $params, $data, $options ); // Should throw + } else { + $this->assertSame( $expect, $this->encodeData( $params, $data, $options ) ); } - $this->assertSame( $expect, $this->encodeData( $params, $data ) ); } } diff --git a/www/wiki/tests/phpunit/includes/api/format/ApiFormatXmlTest.php b/www/wiki/tests/phpunit/includes/api/format/ApiFormatXmlTest.php index 0f8c8ee6..915fb5c5 100644 --- a/www/wiki/tests/phpunit/includes/api/format/ApiFormatXmlTest.php +++ b/www/wiki/tests/phpunit/includes/api/format/ApiFormatXmlTest.php @@ -12,11 +12,11 @@ class ApiFormatXmlTest extends ApiFormatTestBase { public static function setUpBeforeClass() { parent::setUpBeforeClass(); $page = WikiPage::factory( Title::newFromText( 'MediaWiki:ApiFormatXmlTest.xsl' ) ); - // @codingStandardsIgnoreStart Generic.Files.LineLength + // phpcs:disable Generic.Files.LineLength $page->doEditContent( new WikitextContent( '<?xml version="1.0"?><xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" />' ), 'Summary' ); - // @codingStandardsIgnoreEnd + // phpcs:enable $page = WikiPage::factory( Title::newFromText( 'MediaWiki:ApiFormatXmlTest' ) ); $page->doEditContent( new WikitextContent( 'Bogus' ), 'Summary' ); $page = WikiPage::factory( Title::newFromText( 'ApiFormatXmlTest' ) ); @@ -24,7 +24,7 @@ class ApiFormatXmlTest extends ApiFormatTestBase { } public static function provideGeneralEncoding() { - // @codingStandardsIgnoreStart Generic.Files.LineLength + // phpcs:disable Generic.Files.LineLength return [ // Basic types [ [ null, 'a' => null ], '<?xml version="1.0"?><api><_v _idx="0" /></api>' ], @@ -117,7 +117,7 @@ class ApiFormatXmlTest extends ApiFormatTestBase { '" type="text/xsl" ?><api />', [ 'xslt' => 'MediaWiki:ApiFormatXmlTest.xsl' ] ], ]; - // @codingStandardsIgnoreEnd + // phpcs:enable } } diff --git a/www/wiki/tests/phpunit/includes/api/query/ApiQueryBasicTest.php b/www/wiki/tests/phpunit/includes/api/query/ApiQueryBasicTest.php index c612f265..e49e1d8b 100644 --- a/www/wiki/tests/phpunit/includes/api/query/ApiQueryBasicTest.php +++ b/www/wiki/tests/phpunit/includes/api/query/ApiQueryBasicTest.php @@ -1,8 +1,5 @@ <?php /** - * - * Created on Feb 6, 2013 - * * Copyright © 2013 Yuri Astrakhan "<Firstname><Lastname>@gmail.com" * * This program is free software; you can redistribute it and/or modify @@ -134,7 +131,7 @@ class ApiQueryBasicTest extends ApiQueryTestBase { private static $allcategories = [ [ 'list' => 'allcategories', 'acprefix' => 'AQBT-' ], [ 'allcategories' => [ - [ '*' => 'AQBT-Cat' ], + [ 'category' => 'AQBT-Cat' ], ] ] ]; @@ -236,9 +233,7 @@ class ApiQueryBasicTest extends ApiQueryTestBase { $this->check( self::$allpages ); $this->check( self::$alllinks ); $this->check( self::$alltransclusions ); - // This test is temporarily disabled until a sqlite bug is fixed - // Confirmed still broken 15-nov-2013 - // $this->check( self::$allcategories ); + $this->check( self::$allcategories ); $this->check( self::$backlinks ); $this->check( self::$embeddedin ); $this->check( self::$categorymembers ); diff --git a/www/wiki/tests/phpunit/includes/api/query/ApiQueryContinue2Test.php b/www/wiki/tests/phpunit/includes/api/query/ApiQueryContinue2Test.php index 944d31c8..334fd5da 100644 --- a/www/wiki/tests/phpunit/includes/api/query/ApiQueryContinue2Test.php +++ b/www/wiki/tests/phpunit/includes/api/query/ApiQueryContinue2Test.php @@ -46,7 +46,7 @@ class ApiQueryContinue2Test extends ApiQueryContinueTestBase { } /** - * @medium + * @group medium */ public function testA() { $this->mVerbose = false; diff --git a/www/wiki/tests/phpunit/includes/api/query/ApiQueryContinueTest.php b/www/wiki/tests/phpunit/includes/api/query/ApiQueryContinueTest.php index b31627b4..7259bb81 100644 --- a/www/wiki/tests/phpunit/includes/api/query/ApiQueryContinueTest.php +++ b/www/wiki/tests/phpunit/includes/api/query/ApiQueryContinueTest.php @@ -56,7 +56,7 @@ class ApiQueryContinueTest extends ApiQueryContinueTestBase { /** * Test smart continue - list=allpages - * @medium + * @group medium */ public function test1List() { $this->mVerbose = false; @@ -80,7 +80,7 @@ class ApiQueryContinueTest extends ApiQueryContinueTestBase { /** * Test smart continue - list=allpages|alltransclusions - * @medium + * @group medium */ public function test2Lists() { $this->mVerbose = false; @@ -106,7 +106,7 @@ class ApiQueryContinueTest extends ApiQueryContinueTestBase { /** * Test smart continue - generator=allpages, prop=links - * @medium + * @group medium */ public function testGen1Prop() { $this->mVerbose = false; @@ -131,7 +131,7 @@ class ApiQueryContinueTest extends ApiQueryContinueTestBase { /** * Test smart continue - generator=allpages, prop=links|templates - * @medium + * @group medium */ public function testGen2Prop() { $this->mVerbose = false; @@ -162,7 +162,7 @@ class ApiQueryContinueTest extends ApiQueryContinueTestBase { /** * Test smart continue - generator=allpages, prop=links, list=alltransclusions - * @medium + * @group medium */ public function testGen1Prop1List() { $this->mVerbose = false; @@ -194,7 +194,7 @@ class ApiQueryContinueTest extends ApiQueryContinueTestBase { /** * Test smart continue - generator=allpages, prop=links|templates, * list=alllinks|alltransclusions, meta=siteinfo - * @medium + * @group medium */ public function testGen2Prop2List1Meta() { $this->mVerbose = false; @@ -233,7 +233,7 @@ class ApiQueryContinueTest extends ApiQueryContinueTestBase { /** * Test smart continue - generator=templates, prop=templates - * @medium + * @group medium */ public function testSameGenAndProp() { $this->mVerbose = false; @@ -279,7 +279,7 @@ class ApiQueryContinueTest extends ApiQueryContinueTestBase { /** * Test smart continue - generator=allpages, list=allpages - * @medium + * @group medium */ public function testSameGenList() { $this->mVerbose = false; diff --git a/www/wiki/tests/phpunit/includes/api/query/ApiQueryContinueTestBase.php b/www/wiki/tests/phpunit/includes/api/query/ApiQueryContinueTestBase.php index 704c4172..d2bdb496 100644 --- a/www/wiki/tests/phpunit/includes/api/query/ApiQueryContinueTestBase.php +++ b/www/wiki/tests/phpunit/includes/api/query/ApiQueryContinueTestBase.php @@ -1,7 +1,5 @@ <?php /** - * Created on Jan 1, 2013 - * * Copyright © 2013 Yuri Astrakhan "<Firstname><Lastname>@gmail.com" * * This program is free software; you can redistribute it and/or modify @@ -159,7 +157,7 @@ abstract class ApiQueryContinueTestBase extends ApiQueryTestBase { /** * Recursively merge the new result returned from the query to the previous results. - * @param mixed $results + * @param mixed &$results * @param mixed $newResult * @param bool $numericIds If true, treat keys as ids to be merged instead of appending */ diff --git a/www/wiki/tests/phpunit/includes/api/query/ApiQueryTest.php b/www/wiki/tests/phpunit/includes/api/query/ApiQueryTest.php index 8026e544..de8d8156 100644 --- a/www/wiki/tests/phpunit/includes/api/query/ApiQueryTest.php +++ b/www/wiki/tests/phpunit/includes/api/query/ApiQueryTest.php @@ -9,7 +9,6 @@ class ApiQueryTest extends ApiTestCase { protected function setUp() { parent::setUp(); - $this->doLogin(); // Setup apiquerytestiw: as interwiki prefix $this->setMwGlobals( 'wgHooks', [ @@ -81,6 +80,19 @@ class ApiQueryTest extends ApiTestCase { $this->assertArrayHasKey( 'invalid', $data[0]['query']['pages'][-1] ); } + public function testTitlesWithWhitespaces() { + $data = $this->doApiRequest( [ + 'action' => 'query', + 'titles' => ' ' + ] ); + + $this->assertArrayHasKey( 'query', $data[0] ); + $this->assertArrayHasKey( 'pages', $data[0]['query'] ); + $this->assertEquals( 1, count( $data[0]['query']['pages'] ) ); + $this->assertArrayHasKey( -1, $data[0]['query']['pages'] ); + $this->assertArrayHasKey( 'invalid', $data[0]['query']['pages'][-1] ); + } + /** * Test the ApiBase::titlePartToKey function * diff --git a/www/wiki/tests/phpunit/includes/api/query/ApiQueryTestBase.php b/www/wiki/tests/phpunit/includes/api/query/ApiQueryTestBase.php index f3d7cb6e..e7588cb5 100644 --- a/www/wiki/tests/phpunit/includes/api/query/ApiQueryTestBase.php +++ b/www/wiki/tests/phpunit/includes/api/query/ApiQueryTestBase.php @@ -1,7 +1,5 @@ <?php /** - * Created on Feb 10, 2013 - * * Copyright © 2013 Yuri Astrakhan "<Firstname><Lastname>@gmail.com" * * This program is free software; you can redistribute it and/or modify diff --git a/www/wiki/tests/phpunit/includes/api/query/ApiQueryUserContributionsTest.php b/www/wiki/tests/phpunit/includes/api/query/ApiQueryUserContributionsTest.php new file mode 100644 index 00000000..ca6a929a --- /dev/null +++ b/www/wiki/tests/phpunit/includes/api/query/ApiQueryUserContributionsTest.php @@ -0,0 +1,194 @@ +<?php + +/** + * @group API + * @group Database + * @group medium + * @covers ApiQueryContributions + */ +class ApiQueryContributionsTest extends ApiTestCase { + public function addDBDataOnce() { + global $wgActorTableSchemaMigrationStage; + + $reset = new \Wikimedia\ScopedCallback( function ( $v ) { + global $wgActorTableSchemaMigrationStage; + $wgActorTableSchemaMigrationStage = $v; + $this->overrideMwServices(); + }, [ $wgActorTableSchemaMigrationStage ] ); + $wgActorTableSchemaMigrationStage = MIGRATION_WRITE_BOTH; + $this->overrideMwServices(); + + $users = [ + User::newFromName( '192.168.2.2', false ), + User::newFromName( '192.168.2.1', false ), + User::newFromName( '192.168.2.3', false ), + User::createNew( __CLASS__ . ' B' ), + User::createNew( __CLASS__ . ' A' ), + User::createNew( __CLASS__ . ' C' ), + User::newFromName( 'IW>' . __CLASS__, false ), + ]; + + $title = Title::newFromText( __CLASS__ ); + $page = WikiPage::factory( $title ); + for ( $i = 0; $i < 3; $i++ ) { + foreach ( array_reverse( $users ) as $user ) { + $status = $page->doEditContent( + ContentHandler::makeContent( "Test revision $user #$i", $title ), 'Test edit', 0, false, $user + ); + if ( !$status->isOK() ) { + $this->fail( "Failed to edit $title: " . $status->getWikiText( false, false, 'en' ) ); + } + } + } + } + + /** + * @dataProvider provideSorting + * @param int $stage One of the MIGRATION_* constants for $wgActorTableSchemaMigrationStage + * @param array $params Extra parameters for the query + * @param bool $reverse Reverse order? + * @param int $revs Number of revisions to expect + */ + public function testSorting( $stage, $params, $reverse, $revs ) { + if ( isset( $params['ucuserprefix'] ) && + ( $stage === MIGRATION_WRITE_BOTH || $stage === MIGRATION_WRITE_NEW ) && + $this->db->getType() === 'mysql' && $this->usesTemporaryTables() + ) { + // https://bugs.mysql.com/bug.php?id=10327 + $this->markTestSkipped( 'MySQL bug 10327 - can\'t reopen temporary tables' ); + } + + $this->setMwGlobals( 'wgActorTableSchemaMigrationStage', $stage ); + $this->overrideMwServices(); + + if ( isset( $params['ucuserids'] ) ) { + $params['ucuserids'] = implode( '|', array_map( 'User::idFromName', $params['ucuserids'] ) ); + } + if ( isset( $params['ucuser'] ) ) { + $params['ucuser'] = implode( '|', $params['ucuser'] ); + } + + $sort = 'rsort'; + if ( $reverse ) { + $params['ucdir'] = 'newer'; + $sort = 'sort'; + } + + $params += [ + 'action' => 'query', + 'list' => 'usercontribs', + 'ucprop' => 'ids', + ]; + + $apiResult = $this->doApiRequest( $params + [ 'uclimit' => 500 ] ); + $this->assertArrayNotHasKey( 'continue', $apiResult[0] ); + $this->assertArrayHasKey( 'query', $apiResult[0] ); + $this->assertArrayHasKey( 'usercontribs', $apiResult[0]['query'] ); + + $count = 0; + $ids = []; + foreach ( $apiResult[0]['query']['usercontribs'] as $page ) { + $count++; + $ids[$page['user']][] = $page['revid']; + } + $this->assertSame( $revs, $count, 'Expected number of revisions' ); + foreach ( $ids as $user => $revids ) { + $sorted = $revids; + call_user_func_array( $sort, [ &$sorted ] ); + $this->assertSame( $sorted, $revids, "IDs for $user are sorted" ); + } + + for ( $limit = 1; $limit < $revs; $limit++ ) { + $continue = []; + $count = 0; + $batchedIds = []; + while ( $continue !== null ) { + $apiResult = $this->doApiRequest( $params + [ 'uclimit' => $limit ] + $continue ); + $this->assertArrayHasKey( 'query', $apiResult[0], "Batching with limit $limit" ); + $this->assertArrayHasKey( 'usercontribs', $apiResult[0]['query'], + "Batching with limit $limit" ); + $continue = isset( $apiResult[0]['continue'] ) ? $apiResult[0]['continue'] : null; + foreach ( $apiResult[0]['query']['usercontribs'] as $page ) { + $count++; + $batchedIds[$page['user']][] = $page['revid']; + } + $this->assertLessThanOrEqual( $revs, $count, "Batching with limit $limit" ); + } + $this->assertSame( $ids, $batchedIds, "Result set is the same when batching with limit $limit" ); + } + } + + public static function provideSorting() { + $users = [ __CLASS__ . ' A', __CLASS__ . ' B', __CLASS__ . ' C' ]; + $users2 = [ __CLASS__ . ' A', __CLASS__ . ' B', __CLASS__ . ' D' ]; + $ips = [ '192.168.2.1', '192.168.2.2', '192.168.2.3', '192.168.2.4' ]; + + foreach ( + [ + 'old' => MIGRATION_OLD, + 'write both' => MIGRATION_WRITE_BOTH, + 'write new' => MIGRATION_WRITE_NEW, + 'new' => MIGRATION_NEW, + ] as $stageName => $stage + ) { + foreach ( [ false, true ] as $reverse ) { + $name = $stageName . ( $reverse ? ', reverse' : '' ); + yield "Named users, $name" => [ $stage, [ 'ucuser' => $users ], $reverse, 9 ]; + yield "Named users including a no-edit user, $name" => [ + $stage, [ 'ucuser' => $users2 ], $reverse, 6 + ]; + yield "IP users, $name" => [ $stage, [ 'ucuser' => $ips ], $reverse, 9 ]; + yield "All users, $name" => [ + $stage, [ 'ucuser' => array_merge( $users, $ips ) ], $reverse, 18 + ]; + yield "User IDs, $name" => [ $stage, [ 'ucuserids' => $users ], $reverse, 9 ]; + yield "Users by prefix, $name" => [ $stage, [ 'ucuserprefix' => __CLASS__ ], $reverse, 9 ]; + yield "IPs by prefix, $name" => [ $stage, [ 'ucuserprefix' => '192.168.2.' ], $reverse, 9 ]; + } + } + } + + /** + * @dataProvider provideInterwikiUser + * @param int $stage One of the MIGRATION_* constants for $wgActorTableSchemaMigrationStage + */ + public function testInterwikiUser( $stage ) { + $this->setMwGlobals( 'wgActorTableSchemaMigrationStage', $stage ); + $this->overrideMwServices(); + + $params = [ + 'action' => 'query', + 'list' => 'usercontribs', + 'ucuser' => 'IW>' . __CLASS__, + 'ucprop' => 'ids', + 'uclimit' => 'max', + ]; + + $apiResult = $this->doApiRequest( $params ); + $this->assertArrayNotHasKey( 'continue', $apiResult[0] ); + $this->assertArrayHasKey( 'query', $apiResult[0] ); + $this->assertArrayHasKey( 'usercontribs', $apiResult[0]['query'] ); + + $count = 0; + $ids = []; + foreach ( $apiResult[0]['query']['usercontribs'] as $page ) { + $count++; + $this->assertSame( 'IW>' . __CLASS__, $page['user'], 'Correct user returned' ); + $ids[] = $page['revid']; + } + $this->assertSame( 3, $count, 'Expected number of revisions' ); + $sorted = $ids; + rsort( $sorted ); + $this->assertSame( $sorted, $ids, "IDs are sorted" ); + } + + public static function provideInterwikiUser() { + return [ + 'old' => [ MIGRATION_OLD ], + 'write both' => [ MIGRATION_WRITE_BOTH ], + 'write new' => [ MIGRATION_WRITE_NEW ], + 'new' => [ MIGRATION_NEW ], + ]; + } + +} diff --git a/www/wiki/tests/phpunit/includes/auth/AbstractAuthenticationProviderTest.php b/www/wiki/tests/phpunit/includes/auth/AbstractAuthenticationProviderTest.php index a3b0df52..b271b701 100644 --- a/www/wiki/tests/phpunit/includes/auth/AbstractAuthenticationProviderTest.php +++ b/www/wiki/tests/phpunit/includes/auth/AbstractAuthenticationProviderTest.php @@ -13,7 +13,7 @@ class AbstractAuthenticationProviderTest extends \MediaWikiTestCase { $provider = $this->getMockForAbstractClass( AbstractAuthenticationProvider::class ); $providerPriv = TestingAccessWrapper::newFromObject( $provider ); - $obj = $this->getMockForAbstractClass( 'Psr\Log\LoggerInterface' ); + $obj = $this->getMockForAbstractClass( \Psr\Log\LoggerInterface::class ); $provider->setLogger( $obj ); $this->assertSame( $obj, $providerPriv->logger, 'setLogger' ); @@ -21,7 +21,7 @@ class AbstractAuthenticationProviderTest extends \MediaWikiTestCase { $provider->setManager( $obj ); $this->assertSame( $obj, $providerPriv->manager, 'setManager' ); - $obj = $this->getMockForAbstractClass( 'Config' ); + $obj = $this->getMockForAbstractClass( \Config::class ); $provider->setConfig( $obj ); $this->assertSame( $obj, $providerPriv->config, 'setConfig' ); diff --git a/www/wiki/tests/phpunit/includes/auth/AbstractPasswordPrimaryAuthenticationProviderTest.php b/www/wiki/tests/phpunit/includes/auth/AbstractPasswordPrimaryAuthenticationProviderTest.php index 76d8ee93..cb015df6 100644 --- a/www/wiki/tests/phpunit/includes/auth/AbstractPasswordPrimaryAuthenticationProviderTest.php +++ b/www/wiki/tests/phpunit/includes/auth/AbstractPasswordPrimaryAuthenticationProviderTest.php @@ -33,7 +33,7 @@ class AbstractPasswordPrimaryAuthenticationProviderTest extends \MediaWikiTestCa $providerPriv = TestingAccessWrapper::newFromObject( $provider ); $obj = $providerPriv->getPasswordFactory(); - $this->assertInstanceOf( 'PasswordFactory', $obj ); + $this->assertInstanceOf( \PasswordFactory::class, $obj ); $this->assertSame( $obj, $providerPriv->getPasswordFactory() ); } @@ -46,10 +46,10 @@ class AbstractPasswordPrimaryAuthenticationProviderTest extends \MediaWikiTestCa $providerPriv = TestingAccessWrapper::newFromObject( $provider ); $obj = $providerPriv->getPassword( null ); - $this->assertInstanceOf( 'Password', $obj ); + $this->assertInstanceOf( \Password::class, $obj ); $obj = $providerPriv->getPassword( 'invalid' ); - $this->assertInstanceOf( 'Password', $obj ); + $this->assertInstanceOf( \Password::class, $obj ); } public function testGetNewPasswordExpiry() { diff --git a/www/wiki/tests/phpunit/includes/auth/AuthManagerTest.php b/www/wiki/tests/phpunit/includes/auth/AuthManagerTest.php index c18af8b3..cc162487 100644 --- a/www/wiki/tests/phpunit/includes/auth/AuthManagerTest.php +++ b/www/wiki/tests/phpunit/includes/auth/AuthManagerTest.php @@ -2,10 +2,13 @@ namespace MediaWiki\Auth; +use Config; use MediaWiki\Session\SessionInfo; use MediaWiki\Session\UserInfo; +use Psr\Log\LoggerInterface; use Psr\Log\LogLevel; use StatusValue; +use WebRequest; use Wikimedia\ScopedCallback; use Wikimedia\TestingAccessWrapper; @@ -19,7 +22,7 @@ class AuthManagerTest extends \MediaWikiTestCase { protected $request; /** @var Config */ protected $config; - /** @var \\Psr\\Log\\LoggerInterface */ + /** @var LoggerInterface */ protected $logger; protected $preauthMocks = []; @@ -149,7 +152,7 @@ class AuthManagerTest extends \MediaWikiTestCase { if ( $canChangeUser !== null ) { $methods[] = 'canChangeUser'; } - $provider = $this->getMockBuilder( 'DummySessionProvider' ) + $provider = $this->getMockBuilder( \DummySessionProvider::class ) ->setMethods( $methods ) ->getMock(); $provider->expects( $this->any() )->method( '__toString' ) @@ -876,14 +879,10 @@ class AuthManagerTest extends \MediaWikiTestCase { ); $mocks[$key]->expects( $this->any() )->method( 'getUniqueId' ) ->will( $this->returnValue( $key ) ); - $mocks[$key . '2'] = $this->getMockForAbstractClass( - "MediaWiki\\Auth\\$class", [], "Mock$class" - ); + $mocks[$key . '2'] = $this->getMockForAbstractClass( "MediaWiki\\Auth\\$class" ); $mocks[$key . '2']->expects( $this->any() )->method( 'getUniqueId' ) ->will( $this->returnValue( $key . '2' ) ); - $mocks[$key . '3'] = $this->getMockForAbstractClass( - "MediaWiki\\Auth\\$class", [], "Mock$class" - ); + $mocks[$key . '3'] = $this->getMockForAbstractClass( "MediaWiki\\Auth\\$class" ); $mocks[$key . '3']->expects( $this->any() )->method( 'getUniqueId' ) ->will( $this->returnValue( $key . '3' ) ); } @@ -968,7 +967,7 @@ class AuthManagerTest extends \MediaWikiTestCase { $p->expects( $this->atMost( 1 ) )->method( 'postAuthentication' ) ->willReturnCallback( function ( $user, $response ) use ( $constraint, $p ) { if ( $user !== null ) { - $this->assertInstanceOf( 'User', $user ); + $this->assertInstanceOf( \User::class, $user ); $this->assertSame( 'UTSysop', $user->getName() ); } $this->assertInstanceOf( AuthenticationResponse::class, $response ); @@ -1433,6 +1432,7 @@ class AuthManagerTest extends \MediaWikiTestCase { $blockOptions = [ 'address' => 'UTBlockee', 'user' => $user->getID(), + 'by' => $this->getTestSysop()->getUser()->getId(), 'reason' => __METHOD__, 'expiry' => time() + 100500, 'createAccount' => true, @@ -1445,6 +1445,7 @@ class AuthManagerTest extends \MediaWikiTestCase { $blockOptions = [ 'address' => '127.0.0.0/24', + 'by' => $this->getTestSysop()->getUser()->getId(), 'reason' => __METHOD__, 'expiry' => time() + 100500, 'createAccount' => true, @@ -1896,9 +1897,7 @@ class AuthManagerTest extends \MediaWikiTestCase { ) ); for ( $i = 2; $i <= 3; $i++ ) { - $mocks[$key . $i] = $this->getMockForAbstractClass( - "MediaWiki\\Auth\\$class", [], "Mock$class" - ); + $mocks[$key . $i] = $this->getMockForAbstractClass( "MediaWiki\\Auth\\$class" ); $mocks[$key . $i]->expects( $this->any() )->method( 'getUniqueId' ) ->will( $this->returnValue( $key . $i ) ); $mocks[$key . $i]->expects( $this->any() )->method( 'testUserForCreation' ) @@ -1998,7 +1997,7 @@ class AuthManagerTest extends \MediaWikiTestCase { ->willReturnCallback( function ( $user, $creator, $response ) use ( $constraint, $p, $username ) { - $this->assertInstanceOf( 'User', $user ); + $this->assertInstanceOf( \User::class, $user ); $this->assertSame( $username, $user->getName() ); $this->assertSame( 'UTSysop', $creator->getName() ); $this->assertInstanceOf( AuthenticationResponse::class, $response ); @@ -2262,7 +2261,7 @@ class AuthManagerTest extends \MediaWikiTestCase { // Set up lots of mocks... $mock = $this->getMockForAbstractClass( - "MediaWiki\\Auth\\PrimaryAuthenticationProvider", [] + \MediaWiki\Auth\PrimaryAuthenticationProvider::class, [] ); $mock->expects( $this->any() )->method( 'getUniqueId' ) ->will( $this->returnValue( 'primary' ) ); @@ -2363,9 +2362,7 @@ class AuthManagerTest extends \MediaWikiTestCase { $mocks = []; foreach ( [ 'pre', 'primary', 'secondary' ] as $key ) { $class = ucfirst( $key ) . 'AuthenticationProvider'; - $mocks[$key] = $this->getMockForAbstractClass( - "MediaWiki\\Auth\\$class", [], "Mock$class" - ); + $mocks[$key] = $this->getMockForAbstractClass( "MediaWiki\\Auth\\$class" ); $mocks[$key]->expects( $this->any() )->method( 'getUniqueId' ) ->will( $this->returnValue( $key ) ); } @@ -2668,7 +2665,7 @@ class AuthManagerTest extends \MediaWikiTestCase { // Test addToDatabase fails $session->clear(); - $user = $this->getMockBuilder( 'User' ) + $user = $this->getMockBuilder( \User::class ) ->setMethods( [ 'addToDatabase' ] )->getMock(); $user->expects( $this->once() )->method( 'addToDatabase' ) ->will( $this->returnValue( \Status::newFatal( 'because' ) ) ); @@ -2690,7 +2687,7 @@ class AuthManagerTest extends \MediaWikiTestCase { $backoffKey = wfMemcKey( 'AuthManager', 'autocreate-failed', md5( $username ) ); $this->assertFalse( $cache->get( $backoffKey ), 'sanity check' ); $session->clear(); - $user = $this->getMockBuilder( 'User' ) + $user = $this->getMockBuilder( \User::class ) ->setMethods( [ 'addToDatabase' ] )->getMock(); $user->expects( $this->once() )->method( 'addToDatabase' ) ->will( $this->throwException( new \Exception( 'Excepted' ) ) ); @@ -2714,7 +2711,7 @@ class AuthManagerTest extends \MediaWikiTestCase { // Test addToDatabase fails because the user already exists. $session->clear(); - $user = $this->getMockBuilder( 'User' ) + $user = $this->getMockBuilder( \User::class ) ->setMethods( [ 'addToDatabase' ] )->getMock(); $user->expects( $this->once() )->method( 'addToDatabase' ) ->will( $this->returnCallback( function () use ( $username, &$user ) { @@ -2843,9 +2840,11 @@ class AuthManagerTest extends \MediaWikiTestCase { $mocks = []; foreach ( [ 'pre', 'primary', 'secondary' ] as $key ) { $class = ucfirst( $key ) . 'AuthenticationProvider'; - $mocks[$key] = $this->getMockForAbstractClass( - "MediaWiki\\Auth\\$class", [], "Mock$class" - ); + $mocks[$key] = $this->getMockBuilder( "MediaWiki\\Auth\\$class" ) + ->setMethods( [ + 'getUniqueId', 'getAuthenticationRequests', 'providerAllowsAuthenticationDataChange', + ] ) + ->getMockForAbstractClass(); $mocks[$key]->expects( $this->any() )->method( 'getUniqueId' ) ->will( $this->returnValue( $key ) ); $mocks[$key]->expects( $this->any() )->method( 'getAuthenticationRequests' ) @@ -2863,9 +2862,12 @@ class AuthManagerTest extends \MediaWikiTestCase { PrimaryAuthenticationProvider::TYPE_LINK ] as $type ) { $class = 'PrimaryAuthenticationProvider'; - $mocks["primary-$type"] = $this->getMockForAbstractClass( - "MediaWiki\\Auth\\$class", [], "Mock$class" - ); + $mocks["primary-$type"] = $this->getMockBuilder( "MediaWiki\\Auth\\$class" ) + ->setMethods( [ + 'getUniqueId', 'accountCreationType', 'getAuthenticationRequests', + 'providerAllowsAuthenticationDataChange', + ] ) + ->getMockForAbstractClass(); $mocks["primary-$type"]->expects( $this->any() )->method( 'getUniqueId' ) ->will( $this->returnValue( "primary-$type" ) ); $mocks["primary-$type"]->expects( $this->any() )->method( 'accountCreationType' ) @@ -2880,9 +2882,12 @@ class AuthManagerTest extends \MediaWikiTestCase { $this->primaryauthMocks[] = $mocks["primary-$type"]; } - $mocks['primary2'] = $this->getMockForAbstractClass( - PrimaryAuthenticationProvider::class, [], "MockPrimaryAuthenticationProvider" - ); + $mocks['primary2'] = $this->getMockBuilder( PrimaryAuthenticationProvider::class ) + ->setMethods( [ + 'getUniqueId', 'accountCreationType', 'getAuthenticationRequests', + 'providerAllowsAuthenticationDataChange', + ] ) + ->getMockForAbstractClass(); $mocks['primary2']->expects( $this->any() )->method( 'getUniqueId' ) ->will( $this->returnValue( 'primary2' ) ); $mocks['primary2']->expects( $this->any() )->method( 'accountCreationType' ) @@ -3133,9 +3138,7 @@ class AuthManagerTest extends \MediaWikiTestCase { $mocks = []; foreach ( [ 'primary', 'secondary' ] as $key ) { $class = ucfirst( $key ) . 'AuthenticationProvider'; - $mocks[$key] = $this->getMockForAbstractClass( - "MediaWiki\\Auth\\$class", [], "Mock$class" - ); + $mocks[$key] = $this->getMockForAbstractClass( "MediaWiki\\Auth\\$class" ); $mocks[$key]->expects( $this->any() )->method( 'getUniqueId' ) ->will( $this->returnValue( $key ) ); $mocks[$key]->expects( $this->any() )->method( 'providerAllowsPropertyChange' ) @@ -3219,8 +3222,7 @@ class AuthManagerTest extends \MediaWikiTestCase { public function testAutoCreateFailOnLogin() { $username = self::usernameForCreation(); - $mock = $this->getMockForAbstractClass( - PrimaryAuthenticationProvider::class, [], "MockPrimaryAuthenticationProvider" ); + $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class ); $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'primary' ) ); $mock->expects( $this->any() )->method( 'beginPrimaryAuthentication' ) ->will( $this->returnValue( AuthenticationResponse::newPass( $username ) ) ); @@ -3474,7 +3476,7 @@ class AuthManagerTest extends \MediaWikiTestCase { $p->postCalled = false; $p->expects( $this->atMost( 1 ) )->method( 'postAccountLink' ) ->willReturnCallback( function ( $user, $response ) use ( $constraint, $p ) { - $this->assertInstanceOf( 'User', $user ); + $this->assertInstanceOf( \User::class, $user ); $this->assertSame( 'UTSysop', $user->getName() ); $this->assertInstanceOf( AuthenticationResponse::class, $response ); $this->assertThat( $response->status, $constraint ); diff --git a/www/wiki/tests/phpunit/includes/auth/AuthPluginPrimaryAuthenticationProviderTest.php b/www/wiki/tests/phpunit/includes/auth/AuthPluginPrimaryAuthenticationProviderTest.php index 69703134..57c3e7eb 100644 --- a/www/wiki/tests/phpunit/includes/auth/AuthPluginPrimaryAuthenticationProviderTest.php +++ b/www/wiki/tests/phpunit/includes/auth/AuthPluginPrimaryAuthenticationProviderTest.php @@ -20,7 +20,7 @@ class AuthPluginPrimaryAuthenticationProviderTest extends \MediaWikiTestCase { ); } - $plugin = $this->createMock( 'AuthPlugin' ); + $plugin = $this->createMock( \AuthPlugin::class ); $plugin->expects( $this->any() )->method( 'domainList' )->willReturn( [] ); $provider = new AuthPluginPrimaryAuthenticationProvider( $plugin ); @@ -51,7 +51,7 @@ class AuthPluginPrimaryAuthenticationProviderTest extends \MediaWikiTestCase { public function testOnUserSaveSettings() { $user = \User::newFromName( 'UTSysop' ); - $plugin = $this->createMock( 'AuthPlugin' ); + $plugin = $this->createMock( \AuthPlugin::class ); $plugin->expects( $this->any() )->method( 'domainList' )->willReturn( [] ); $plugin->expects( $this->once() )->method( 'updateExternalDB' ) ->with( $this->identicalTo( $user ) ); @@ -63,7 +63,7 @@ class AuthPluginPrimaryAuthenticationProviderTest extends \MediaWikiTestCase { public function testOnUserGroupsChanged() { $user = \User::newFromName( 'UTSysop' ); - $plugin = $this->createMock( 'AuthPlugin' ); + $plugin = $this->createMock( \AuthPlugin::class ); $plugin->expects( $this->any() )->method( 'domainList' )->willReturn( [] ); $plugin->expects( $this->once() )->method( 'updateExternalDBGroups' ) ->with( @@ -73,20 +73,20 @@ class AuthPluginPrimaryAuthenticationProviderTest extends \MediaWikiTestCase { ); $provider = new AuthPluginPrimaryAuthenticationProvider( $plugin ); - \Hooks::run( 'UserGroupsChanged', [ $user, [ 'added' ], [ 'removed' ] ] ); + \Hooks::run( 'UserGroupsChanged', [ $user, [ 'added' ], [ 'removed' ], false, false, [], [] ] ); } public function testOnUserLoggedIn() { $user = \User::newFromName( 'UTSysop' ); - $plugin = $this->createMock( 'AuthPlugin' ); + $plugin = $this->createMock( \AuthPlugin::class ); $plugin->expects( $this->any() )->method( 'domainList' )->willReturn( [] ); $plugin->expects( $this->exactly( 2 ) )->method( 'updateUser' ) ->with( $this->identicalTo( $user ) ); $provider = new AuthPluginPrimaryAuthenticationProvider( $plugin ); \Hooks::run( 'UserLoggedIn', [ $user ] ); - $plugin = $this->createMock( 'AuthPlugin' ); + $plugin = $this->createMock( \AuthPlugin::class ); $plugin->expects( $this->any() )->method( 'domainList' )->willReturn( [] ); $plugin->expects( $this->once() )->method( 'updateUser' ) ->will( $this->returnCallback( function ( &$user ) { @@ -107,14 +107,14 @@ class AuthPluginPrimaryAuthenticationProviderTest extends \MediaWikiTestCase { public function testOnLocalUserCreated() { $user = \User::newFromName( 'UTSysop' ); - $plugin = $this->createMock( 'AuthPlugin' ); + $plugin = $this->createMock( \AuthPlugin::class ); $plugin->expects( $this->any() )->method( 'domainList' )->willReturn( [] ); $plugin->expects( $this->exactly( 2 ) )->method( 'initUser' ) ->with( $this->identicalTo( $user ), $this->identicalTo( false ) ); $provider = new AuthPluginPrimaryAuthenticationProvider( $plugin ); \Hooks::run( 'LocalUserCreated', [ $user, false ] ); - $plugin = $this->createMock( 'AuthPlugin' ); + $plugin = $this->createMock( \AuthPlugin::class ); $plugin->expects( $this->any() )->method( 'domainList' )->willReturn( [] ); $plugin->expects( $this->once() )->method( 'initUser' ) ->will( $this->returnCallback( function ( &$user ) { @@ -133,7 +133,7 @@ class AuthPluginPrimaryAuthenticationProviderTest extends \MediaWikiTestCase { } public function testGetUniqueId() { - $plugin = $this->createMock( 'AuthPlugin' ); + $plugin = $this->createMock( \AuthPlugin::class ); $plugin->expects( $this->any() )->method( 'domainList' )->willReturn( [] ); $provider = new AuthPluginPrimaryAuthenticationProvider( $plugin ); $this->assertSame( @@ -149,7 +149,7 @@ class AuthPluginPrimaryAuthenticationProviderTest extends \MediaWikiTestCase { * @param bool $allowPasswordChange */ public function testGetAuthenticationRequests( $action, $response, $allowPasswordChange ) { - $plugin = $this->createMock( 'AuthPlugin' ); + $plugin = $this->createMock( \AuthPlugin::class ); $plugin->expects( $this->any() )->method( 'domainList' )->willReturn( [] ); $plugin->expects( $this->any() )->method( 'allowPasswordChange' ) ->will( $this->returnValue( $allowPasswordChange ) ); @@ -178,7 +178,7 @@ class AuthPluginPrimaryAuthenticationProviderTest extends \MediaWikiTestCase { $req->action = AuthManager::ACTION_LOGIN; $reqs = [ PasswordAuthenticationRequest::class => $req ]; - $plugin = $this->getMockBuilder( 'AuthPlugin' ) + $plugin = $this->getMockBuilder( \AuthPlugin::class ) ->setMethods( [ 'authenticate' ] ) ->getMock(); $plugin->expects( $this->never() )->method( 'authenticate' ); @@ -206,7 +206,7 @@ class AuthPluginPrimaryAuthenticationProviderTest extends \MediaWikiTestCase { $req->username = 'foo'; $req->password = 'bar'; - $plugin = $this->getMockBuilder( 'AuthPlugin' ) + $plugin = $this->getMockBuilder( \AuthPlugin::class ) ->setMethods( [ 'userExists', 'authenticate' ] ) ->getMock(); $plugin->expects( $this->once() )->method( 'userExists' ) @@ -220,7 +220,7 @@ class AuthPluginPrimaryAuthenticationProviderTest extends \MediaWikiTestCase { $provider->beginPrimaryAuthentication( $reqs ) ); - $plugin = $this->getMockBuilder( 'AuthPlugin' ) + $plugin = $this->getMockBuilder( \AuthPlugin::class ) ->setMethods( [ 'userExists', 'authenticate' ] ) ->getMock(); $plugin->expects( $this->once() )->method( 'userExists' ) @@ -232,13 +232,13 @@ class AuthPluginPrimaryAuthenticationProviderTest extends \MediaWikiTestCase { $provider->beginPrimaryAuthentication( $reqs ) ); - $pluginUser = $this->getMockBuilder( 'AuthPluginUser' ) + $pluginUser = $this->getMockBuilder( \AuthPluginUser::class ) ->setMethods( [ 'isLocked' ] ) ->disableOriginalConstructor() ->getMock(); $pluginUser->expects( $this->once() )->method( 'isLocked' ) ->will( $this->returnValue( true ) ); - $plugin = $this->getMockBuilder( 'AuthPlugin' ) + $plugin = $this->getMockBuilder( \AuthPlugin::class ) ->setMethods( [ 'userExists', 'getUserInstance', 'authenticate' ] ) ->getMock(); $plugin->expects( $this->once() )->method( 'userExists' ) @@ -252,7 +252,7 @@ class AuthPluginPrimaryAuthenticationProviderTest extends \MediaWikiTestCase { $provider->beginPrimaryAuthentication( $reqs ) ); - $plugin = $this->getMockBuilder( 'AuthPlugin' ) + $plugin = $this->getMockBuilder( \AuthPlugin::class ) ->setMethods( [ 'userExists', 'authenticate' ] ) ->getMock(); $plugin->expects( $this->once() )->method( 'userExists' ) @@ -266,7 +266,7 @@ class AuthPluginPrimaryAuthenticationProviderTest extends \MediaWikiTestCase { $provider->beginPrimaryAuthentication( $reqs ) ); - $plugin = $this->getMockBuilder( 'AuthPlugin' ) + $plugin = $this->getMockBuilder( \AuthPlugin::class ) ->setMethods( [ 'userExists', 'authenticate', 'strict' ] ) ->getMock(); $plugin->expects( $this->once() )->method( 'userExists' ) @@ -280,7 +280,7 @@ class AuthPluginPrimaryAuthenticationProviderTest extends \MediaWikiTestCase { $this->assertSame( AuthenticationResponse::FAIL, $ret->status ); $this->assertSame( 'wrongpassword', $ret->message->getKey() ); - $plugin = $this->getMockBuilder( 'AuthPlugin' ) + $plugin = $this->getMockBuilder( \AuthPlugin::class ) ->setMethods( [ 'userExists', 'authenticate', 'strictUserAuth' ] ) ->getMock(); $plugin->expects( $this->once() )->method( 'userExists' ) @@ -296,7 +296,7 @@ class AuthPluginPrimaryAuthenticationProviderTest extends \MediaWikiTestCase { $this->assertSame( AuthenticationResponse::FAIL, $ret->status ); $this->assertSame( 'wrongpassword', $ret->message->getKey() ); - $plugin = $this->getMockBuilder( 'AuthPlugin' ) + $plugin = $this->getMockBuilder( \AuthPlugin::class ) ->setMethods( [ 'domainList', 'validDomain', 'setDomain', 'userExists', 'authenticate' ] ) ->getMock(); $plugin->expects( $this->any() )->method( 'domainList' ) @@ -321,7 +321,7 @@ class AuthPluginPrimaryAuthenticationProviderTest extends \MediaWikiTestCase { } public function testTestUserExists() { - $plugin = $this->createMock( 'AuthPlugin' ); + $plugin = $this->createMock( \AuthPlugin::class ); $plugin->expects( $this->any() )->method( 'domainList' )->willReturn( [] ); $plugin->expects( $this->once() )->method( 'userExists' ) ->with( $this->equalTo( 'Foo' ) ) @@ -330,7 +330,7 @@ class AuthPluginPrimaryAuthenticationProviderTest extends \MediaWikiTestCase { $this->assertTrue( $provider->testUserExists( 'foo' ) ); - $plugin = $this->createMock( 'AuthPlugin' ); + $plugin = $this->createMock( \AuthPlugin::class ); $plugin->expects( $this->any() )->method( 'domainList' )->willReturn( [] ); $plugin->expects( $this->once() )->method( 'userExists' ) ->with( $this->equalTo( 'Foo' ) ) @@ -341,7 +341,7 @@ class AuthPluginPrimaryAuthenticationProviderTest extends \MediaWikiTestCase { } public function testTestUserCanAuthenticate() { - $plugin = $this->createMock( 'AuthPlugin' ); + $plugin = $this->createMock( \AuthPlugin::class ); $plugin->expects( $this->any() )->method( 'domainList' )->willReturn( [] ); $plugin->expects( $this->once() )->method( 'userExists' ) ->with( $this->equalTo( 'Foo' ) ) @@ -350,19 +350,19 @@ class AuthPluginPrimaryAuthenticationProviderTest extends \MediaWikiTestCase { $provider = new AuthPluginPrimaryAuthenticationProvider( $plugin ); $this->assertFalse( $provider->testUserCanAuthenticate( 'foo' ) ); - $pluginUser = $this->getMockBuilder( 'AuthPluginUser' ) + $pluginUser = $this->getMockBuilder( \AuthPluginUser::class ) ->disableOriginalConstructor() ->getMock(); $pluginUser->expects( $this->once() )->method( 'isLocked' ) ->will( $this->returnValue( true ) ); - $plugin = $this->createMock( 'AuthPlugin' ); + $plugin = $this->createMock( \AuthPlugin::class ); $plugin->expects( $this->any() )->method( 'domainList' )->willReturn( [] ); $plugin->expects( $this->once() )->method( 'userExists' ) ->with( $this->equalTo( 'Foo' ) ) ->will( $this->returnValue( true ) ); $plugin->expects( $this->once() )->method( 'getUserInstance' ) ->with( $this->callback( function ( $user ) { - $this->assertInstanceOf( 'User', $user ); + $this->assertInstanceOf( \User::class, $user ); $this->assertEquals( 'Foo', $user->getName() ); return true; } ) ) @@ -370,19 +370,19 @@ class AuthPluginPrimaryAuthenticationProviderTest extends \MediaWikiTestCase { $provider = new AuthPluginPrimaryAuthenticationProvider( $plugin ); $this->assertFalse( $provider->testUserCanAuthenticate( 'foo' ) ); - $pluginUser = $this->getMockBuilder( 'AuthPluginUser' ) + $pluginUser = $this->getMockBuilder( \AuthPluginUser::class ) ->disableOriginalConstructor() ->getMock(); $pluginUser->expects( $this->once() )->method( 'isLocked' ) ->will( $this->returnValue( false ) ); - $plugin = $this->createMock( 'AuthPlugin' ); + $plugin = $this->createMock( \AuthPlugin::class ); $plugin->expects( $this->any() )->method( 'domainList' )->willReturn( [] ); $plugin->expects( $this->once() )->method( 'userExists' ) ->with( $this->equalTo( 'Foo' ) ) ->will( $this->returnValue( true ) ); $plugin->expects( $this->once() )->method( 'getUserInstance' ) ->with( $this->callback( function ( $user ) { - $this->assertInstanceOf( 'User', $user ); + $this->assertInstanceOf( \User::class, $user ); $this->assertEquals( 'Foo', $user->getName() ); return true; } ) ) @@ -392,7 +392,7 @@ class AuthPluginPrimaryAuthenticationProviderTest extends \MediaWikiTestCase { } public function testProviderRevokeAccessForUser() { - $plugin = $this->getMockBuilder( 'AuthPlugin' ) + $plugin = $this->getMockBuilder( \AuthPlugin::class ) ->setMethods( [ 'userExists', 'setPassword' ] ) ->getMock(); $plugin->expects( $this->once() )->method( 'userExists' )->willReturn( true ); @@ -404,7 +404,7 @@ class AuthPluginPrimaryAuthenticationProviderTest extends \MediaWikiTestCase { $provider = new AuthPluginPrimaryAuthenticationProvider( $plugin ); $provider->providerRevokeAccessForUser( 'foo' ); - $plugin = $this->getMockBuilder( 'AuthPlugin' ) + $plugin = $this->getMockBuilder( \AuthPlugin::class ) ->setMethods( [ 'domainList', 'userExists', 'setPassword' ] ) ->getMock(); $plugin->expects( $this->any() )->method( 'domainList' )->willReturn( [ 'D1', 'D2', 'D3' ] ); @@ -433,7 +433,7 @@ class AuthPluginPrimaryAuthenticationProviderTest extends \MediaWikiTestCase { } public function testProviderAllowsPropertyChange() { - $plugin = $this->createMock( 'AuthPlugin' ); + $plugin = $this->createMock( \AuthPlugin::class ); $plugin->expects( $this->any() )->method( 'domainList' )->willReturn( [] ); $plugin->expects( $this->any() )->method( 'allowPropChange' ) ->will( $this->returnCallback( function ( $prop ) { @@ -453,7 +453,7 @@ class AuthPluginPrimaryAuthenticationProviderTest extends \MediaWikiTestCase { */ public function testProviderAllowsAuthenticationDataChange( $type, $allow, $expect ) { $domains = $type instanceof PasswordDomainAuthenticationRequest ? [ 'foo', 'bar' ] : []; - $plugin = $this->createMock( 'AuthPlugin' ); + $plugin = $this->createMock( \AuthPlugin::class ); $plugin->expects( $this->any() )->method( 'domainList' )->willReturn( $domains ); $plugin->expects( $allow === null ? $this->never() : $this->once() ) ->method( 'allowPasswordChange' )->will( $this->returnValue( $allow ) ); @@ -502,7 +502,7 @@ class AuthPluginPrimaryAuthenticationProviderTest extends \MediaWikiTestCase { } public function testProviderChangeAuthenticationData() { - $plugin = $this->createMock( 'AuthPlugin' ); + $plugin = $this->createMock( \AuthPlugin::class ); $plugin->expects( $this->any() )->method( 'domainList' )->willReturn( [] ); $plugin->expects( $this->never() )->method( 'setPassword' ); $provider = new AuthPluginPrimaryAuthenticationProvider( $plugin ); @@ -515,7 +515,7 @@ class AuthPluginPrimaryAuthenticationProviderTest extends \MediaWikiTestCase { $req->username = 'foo'; $req->password = 'bar'; - $plugin = $this->createMock( 'AuthPlugin' ); + $plugin = $this->createMock( \AuthPlugin::class ); $plugin->expects( $this->any() )->method( 'domainList' )->willReturn( [] ); $plugin->expects( $this->once() )->method( 'setPassword' ) ->with( $this->callback( function ( $u ) { @@ -525,7 +525,7 @@ class AuthPluginPrimaryAuthenticationProviderTest extends \MediaWikiTestCase { $provider = new AuthPluginPrimaryAuthenticationProvider( $plugin ); $provider->providerChangeAuthenticationData( $req ); - $plugin = $this->createMock( 'AuthPlugin' ); + $plugin = $this->createMock( \AuthPlugin::class ); $plugin->expects( $this->any() )->method( 'domainList' )->willReturn( [] ); $plugin->expects( $this->once() )->method( 'setPassword' ) ->with( $this->callback( function ( $u ) { @@ -541,7 +541,7 @@ class AuthPluginPrimaryAuthenticationProviderTest extends \MediaWikiTestCase { $this->assertSame( 'authmanager-authplugin-setpass-failed-message', $e->msg ); } - $plugin = $this->createMock( 'AuthPlugin' ); + $plugin = $this->createMock( \AuthPlugin::class ); $plugin->expects( $this->any() )->method( 'domainList' ) ->will( $this->returnValue( [ 'Domain1', 'Domain2' ] ) ); $plugin->expects( $this->any() )->method( 'validDomain' ) @@ -569,7 +569,7 @@ class AuthPluginPrimaryAuthenticationProviderTest extends \MediaWikiTestCase { * @param string $expect */ public function testAccountCreationType( $can, $expect ) { - $plugin = $this->createMock( 'AuthPlugin' ); + $plugin = $this->createMock( \AuthPlugin::class ); $plugin->expects( $this->any() )->method( 'domainList' )->willReturn( [] ); $plugin->expects( $this->once() ) ->method( 'canCreateAccounts' )->will( $this->returnValue( $can ) ); @@ -588,7 +588,7 @@ class AuthPluginPrimaryAuthenticationProviderTest extends \MediaWikiTestCase { public function testTestForAccountCreation() { $user = \User::newFromName( 'foo' ); - $plugin = $this->createMock( 'AuthPlugin' ); + $plugin = $this->createMock( \AuthPlugin::class ); $plugin->expects( $this->any() )->method( 'domainList' )->willReturn( [] ); $provider = new AuthPluginPrimaryAuthenticationProvider( $plugin ); $this->assertEquals( @@ -606,7 +606,7 @@ class AuthPluginPrimaryAuthenticationProviderTest extends \MediaWikiTestCase { $req->action = AuthManager::ACTION_CREATE; $reqs = [ PasswordAuthenticationRequest::class => $req ]; - $plugin = $this->createMock( 'AuthPlugin' ); + $plugin = $this->createMock( \AuthPlugin::class ); $plugin->expects( $this->any() )->method( 'domainList' )->willReturn( [] ); $plugin->expects( $this->any() )->method( 'canCreateAccounts' ) ->will( $this->returnValue( false ) ); @@ -621,7 +621,7 @@ class AuthPluginPrimaryAuthenticationProviderTest extends \MediaWikiTestCase { ); } - $plugin = $this->createMock( 'AuthPlugin' ); + $plugin = $this->createMock( \AuthPlugin::class ); $plugin->expects( $this->any() )->method( 'domainList' )->willReturn( [] ); $plugin->expects( $this->any() )->method( 'canCreateAccounts' ) ->will( $this->returnValue( true ) ); @@ -650,7 +650,7 @@ class AuthPluginPrimaryAuthenticationProviderTest extends \MediaWikiTestCase { $req->username = 'foo'; $req->password = 'bar'; - $plugin = $this->createMock( 'AuthPlugin' ); + $plugin = $this->createMock( \AuthPlugin::class ); $plugin->expects( $this->any() )->method( 'domainList' )->willReturn( [] ); $plugin->expects( $this->any() )->method( 'canCreateAccounts' ) ->will( $this->returnValue( true ) ); @@ -670,7 +670,7 @@ class AuthPluginPrimaryAuthenticationProviderTest extends \MediaWikiTestCase { $provider->beginPrimaryAccountCreation( $user, $user, $reqs ) ); - $plugin = $this->createMock( 'AuthPlugin' ); + $plugin = $this->createMock( \AuthPlugin::class ); $plugin->expects( $this->any() )->method( 'domainList' )->willReturn( [] ); $plugin->expects( $this->any() )->method( 'canCreateAccounts' ) ->will( $this->returnValue( true ) ); @@ -689,7 +689,7 @@ class AuthPluginPrimaryAuthenticationProviderTest extends \MediaWikiTestCase { $this->assertSame( AuthenticationResponse::FAIL, $ret->status ); $this->assertSame( 'authmanager-authplugin-create-fail', $ret->message->getKey() ); - $plugin = $this->createMock( 'AuthPlugin' ); + $plugin = $this->createMock( \AuthPlugin::class ); $plugin->expects( $this->any() )->method( 'canCreateAccounts' ) ->will( $this->returnValue( true ) ); $plugin->expects( $this->any() )->method( 'domainList' ) diff --git a/www/wiki/tests/phpunit/includes/auth/AuthenticationRequestTest.php b/www/wiki/tests/phpunit/includes/auth/AuthenticationRequestTest.php index 0e549a5c..1bc0f31f 100644 --- a/www/wiki/tests/phpunit/includes/auth/AuthenticationRequestTest.php +++ b/www/wiki/tests/phpunit/includes/auth/AuthenticationRequestTest.php @@ -17,9 +17,9 @@ class AuthenticationRequestTest extends \MediaWikiTestCase { $ret = $mock->describeCredentials(); $this->assertInternalType( 'array', $ret ); $this->assertArrayHasKey( 'provider', $ret ); - $this->assertInstanceOf( 'Message', $ret['provider'] ); + $this->assertInstanceOf( \Message::class, $ret['provider'] ); $this->assertArrayHasKey( 'account', $ret ); - $this->assertInstanceOf( 'Message', $ret['account'] ); + $this->assertInstanceOf( \Message::class, $ret['account'] ); } public function testLoadRequestsFromSubmission() { diff --git a/www/wiki/tests/phpunit/includes/auth/AuthenticationRequestTestCase.php b/www/wiki/tests/phpunit/includes/auth/AuthenticationRequestTestCase.php index b5c8a36c..f483b9b6 100644 --- a/www/wiki/tests/phpunit/includes/auth/AuthenticationRequestTestCase.php +++ b/www/wiki/tests/phpunit/includes/auth/AuthenticationRequestTestCase.php @@ -19,11 +19,11 @@ abstract class AuthenticationRequestTestCase extends \MediaWikiTestCase { $this->assertType( 'array', $data, "Field $field" ); $this->assertArrayHasKey( 'type', $data, "Field $field" ); $this->assertArrayHasKey( 'label', $data, "Field $field" ); - $this->assertInstanceOf( 'Message', $data['label'], "Field $field, label" ); + $this->assertInstanceOf( \Message::class, $data['label'], "Field $field, label" ); if ( $data['type'] !== 'null' ) { $this->assertArrayHasKey( 'help', $data, "Field $field" ); - $this->assertInstanceOf( 'Message', $data['help'], "Field $field, help" ); + $this->assertInstanceOf( \Message::class, $data['help'], "Field $field, help" ); } if ( isset( $data['optional'] ) ) { @@ -50,7 +50,7 @@ abstract class AuthenticationRequestTestCase extends \MediaWikiTestCase { $this->assertArrayHasKey( 'options', $data, "Field $field" ); $this->assertType( 'array', $data['options'], "Field $field, options" ); foreach ( $data['options'] as $val => $msg ) { - $this->assertInstanceOf( 'Message', $msg, "Field $field, option $val" ); + $this->assertInstanceOf( \Message::class, $msg, "Field $field, option $val" ); } break; case 'checkbox': diff --git a/www/wiki/tests/phpunit/includes/auth/CheckBlocksSecondaryAuthenticationProviderTest.php b/www/wiki/tests/phpunit/includes/auth/CheckBlocksSecondaryAuthenticationProviderTest.php index 111c8554..e8b61c59 100644 --- a/www/wiki/tests/phpunit/includes/auth/CheckBlocksSecondaryAuthenticationProviderTest.php +++ b/www/wiki/tests/phpunit/includes/auth/CheckBlocksSecondaryAuthenticationProviderTest.php @@ -76,6 +76,7 @@ class CheckBlocksSecondaryAuthenticationProviderTest extends \MediaWikiTestCase $blockOptions = [ 'address' => 'UTBlockee', 'user' => $user->getID(), + 'by' => $this->getTestSysop()->getUser()->getId(), 'reason' => __METHOD__, 'expiry' => time() + 100500, 'createAccount' => true, @@ -135,12 +136,12 @@ class CheckBlocksSecondaryAuthenticationProviderTest extends \MediaWikiTestCase ); $status = $provider->testUserForCreation( $blockedUser, AuthManager::AUTOCREATE_SOURCE_SESSION ); - $this->assertInstanceOf( 'StatusValue', $status ); + $this->assertInstanceOf( \StatusValue::class, $status ); $this->assertFalse( $status->isOK() ); $this->assertTrue( $status->hasMessage( 'cantcreateaccount-text' ) ); $status = $provider->testUserForCreation( $blockedUser, false ); - $this->assertInstanceOf( 'StatusValue', $status ); + $this->assertInstanceOf( \StatusValue::class, $status ); $this->assertFalse( $status->isOK() ); $this->assertTrue( $status->hasMessage( 'cantcreateaccount-text' ) ); } @@ -149,6 +150,7 @@ class CheckBlocksSecondaryAuthenticationProviderTest extends \MediaWikiTestCase $blockOptions = [ 'address' => '127.0.0.0/24', 'reason' => __METHOD__, + 'by' => $this->getTestSysop()->getUser()->getId(), 'expiry' => time() + 100500, 'createAccount' => true, ]; @@ -163,6 +165,7 @@ class CheckBlocksSecondaryAuthenticationProviderTest extends \MediaWikiTestCase $user->saveSettings(); } $this->setMwGlobals( [ 'wgUser' => $user ] ); + \RequestContext::getMain()->setUser( $user ); $newuser = \User::newFromName( 'RandomUser' ); $provider = new CheckBlocksSecondaryAuthenticationProvider( @@ -176,12 +179,12 @@ class CheckBlocksSecondaryAuthenticationProviderTest extends \MediaWikiTestCase $this->assertEquals( AuthenticationResponse::FAIL, $ret->status ); $status = $provider->testUserForCreation( $newuser, AuthManager::AUTOCREATE_SOURCE_SESSION ); - $this->assertInstanceOf( 'StatusValue', $status ); + $this->assertInstanceOf( \StatusValue::class, $status ); $this->assertFalse( $status->isOK() ); $this->assertTrue( $status->hasMessage( 'cantcreateaccount-range-text' ) ); $status = $provider->testUserForCreation( $newuser, false ); - $this->assertInstanceOf( 'StatusValue', $status ); + $this->assertInstanceOf( \StatusValue::class, $status ); $this->assertFalse( $status->isOK() ); $this->assertTrue( $status->hasMessage( 'cantcreateaccount-range-text' ) ); } diff --git a/www/wiki/tests/phpunit/includes/auth/EmailNotificationSecondaryAuthenticationProviderTest.php b/www/wiki/tests/phpunit/includes/auth/EmailNotificationSecondaryAuthenticationProviderTest.php index 3757069e..1a7ed12d 100644 --- a/www/wiki/tests/phpunit/includes/auth/EmailNotificationSecondaryAuthenticationProviderTest.php +++ b/www/wiki/tests/phpunit/includes/auth/EmailNotificationSecondaryAuthenticationProviderTest.php @@ -5,7 +5,7 @@ namespace MediaWiki\Auth; use Psr\Log\LoggerInterface; use Wikimedia\TestingAccessWrapper; -class EmailNotificationSecondaryAuthenticationProviderTest extends \PHPUnit_Framework_TestCase { +class EmailNotificationSecondaryAuthenticationProviderTest extends \PHPUnit\Framework\TestCase { public function testConstructor() { $config = new \HashConfig( [ 'EnableEmail' => true, @@ -58,24 +58,24 @@ class EmailNotificationSecondaryAuthenticationProviderTest extends \PHPUnit_Fram public function testBeginSecondaryAccountCreation() { $authManager = new AuthManager( new \FauxRequest(), new \HashConfig() ); - $creator = $this->getMockBuilder( 'User' )->getMock(); - $userWithoutEmail = $this->getMockBuilder( 'User' )->getMock(); + $creator = $this->getMockBuilder( \User::class )->getMock(); + $userWithoutEmail = $this->getMockBuilder( \User::class )->getMock(); $userWithoutEmail->expects( $this->any() )->method( 'getEmail' )->willReturn( '' ); $userWithoutEmail->expects( $this->any() )->method( 'getInstanceForUpdate' )->willReturnSelf(); $userWithoutEmail->expects( $this->never() )->method( 'sendConfirmationMail' ); - $userWithEmailError = $this->getMockBuilder( 'User' )->getMock(); + $userWithEmailError = $this->getMockBuilder( \User::class )->getMock(); $userWithEmailError->expects( $this->any() )->method( 'getEmail' )->willReturn( 'foo@bar.baz' ); $userWithEmailError->expects( $this->any() )->method( 'getInstanceForUpdate' )->willReturnSelf(); $userWithEmailError->expects( $this->any() )->method( 'sendConfirmationMail' ) ->willReturn( \Status::newFatal( 'fail' ) ); - $userExpectsConfirmation = $this->getMockBuilder( 'User' )->getMock(); + $userExpectsConfirmation = $this->getMockBuilder( \User::class )->getMock(); $userExpectsConfirmation->expects( $this->any() )->method( 'getEmail' ) ->willReturn( 'foo@bar.baz' ); $userExpectsConfirmation->expects( $this->any() )->method( 'getInstanceForUpdate' ) ->willReturnSelf(); $userExpectsConfirmation->expects( $this->once() )->method( 'sendConfirmationMail' ) ->willReturn( \Status::newGood() ); - $userNotExpectsConfirmation = $this->getMockBuilder( 'User' )->getMock(); + $userNotExpectsConfirmation = $this->getMockBuilder( \User::class )->getMock(); $userNotExpectsConfirmation->expects( $this->any() )->method( 'getEmail' ) ->willReturn( 'foo@bar.baz' ); $userNotExpectsConfirmation->expects( $this->any() )->method( 'getInstanceForUpdate' ) diff --git a/www/wiki/tests/phpunit/includes/auth/LegacyHookPreAuthenticationProviderTest.php b/www/wiki/tests/phpunit/includes/auth/LegacyHookPreAuthenticationProviderTest.php index a4b980f0..38ccb8a3 100644 --- a/www/wiki/tests/phpunit/includes/auth/LegacyHookPreAuthenticationProviderTest.php +++ b/www/wiki/tests/phpunit/includes/auth/LegacyHookPreAuthenticationProviderTest.php @@ -15,7 +15,7 @@ class LegacyHookPreAuthenticationProviderTest extends \MediaWikiTestCase { * @return LegacyHookPreAuthenticationProvider */ protected function getProvider() { - $request = $this->getMockBuilder( 'FauxRequest' ) + $request = $this->getMockBuilder( \FauxRequest::class ) ->setMethods( [ 'getIP' ] )->getMock(); $request->expects( $this->any() )->method( 'getIP' )->will( $this->returnValue( '127.0.0.42' ) ); @@ -101,7 +101,7 @@ class LegacyHookPreAuthenticationProviderTest extends \MediaWikiTestCase { if ( $msgForLoginUserMigrated !== null ) { $h->will( $this->returnCallback( function ( $user, &$msg ) use ( $username, $msgForLoginUserMigrated ) { - $this->assertInstanceOf( 'User', $user ); + $this->assertInstanceOf( \User::class, $user ); $this->assertSame( $username, $user->getName() ); $msg = $msgForLoginUserMigrated; return false; @@ -111,7 +111,7 @@ class LegacyHookPreAuthenticationProviderTest extends \MediaWikiTestCase { } else { $h->will( $this->returnCallback( function ( $user, &$msg ) use ( $username ) { - $this->assertInstanceOf( 'User', $user ); + $this->assertInstanceOf( \User::class, $user ); $this->assertSame( $username, $user->getName() ); return true; } @@ -122,7 +122,7 @@ class LegacyHookPreAuthenticationProviderTest extends \MediaWikiTestCase { function ( $user, $pass, &$abort, &$msg ) use ( $username, $password, $abortForAbortLogin, $msgForAbortLogin ) { - $this->assertInstanceOf( 'User', $user ); + $this->assertInstanceOf( \User::class, $user ); $this->assertSame( $username, $user->getName() ); if ( $password !== null ) { $this->assertSame( $password, $pass ); @@ -137,7 +137,7 @@ class LegacyHookPreAuthenticationProviderTest extends \MediaWikiTestCase { } else { $h2->will( $this->returnCallback( function ( $user, $pass, &$abort, &$msg ) use ( $username, $password ) { - $this->assertInstanceOf( 'User', $user ); + $this->assertInstanceOf( \User::class, $user ); $this->assertSame( $username, $user->getName() ); if ( $password !== null ) { $this->assertSame( $password, $pass ); @@ -160,7 +160,7 @@ class LegacyHookPreAuthenticationProviderTest extends \MediaWikiTestCase { if ( $failMsg === null ) { $this->assertEquals( \StatusValue::newGood(), $status, 'should succeed' ); } else { - $this->assertInstanceOf( 'StatusValue', $status, 'should fail (type)' ); + $this->assertInstanceOf( \StatusValue::class, $status, 'should fail (type)' ); $this->assertFalse( $status->isOk(), 'should fail (ok)' ); $errors = $status->getErrors(); $this->assertEquals( $failMsg, $errors[0]['message'], 'should fail (message)' ); @@ -282,14 +282,14 @@ class LegacyHookPreAuthenticationProviderTest extends \MediaWikiTestCase { * @dataProvider provideTestForAccountCreation * @param string $msg * @param Status|null $status - * @param StatusValue $result Result + * @param StatusValue $result */ public function testTestForAccountCreation( $msg, $status, $result ) { $this->hook( 'AbortNewAccount', $this->once() ) ->will( $this->returnCallback( function ( $user, &$error, &$abortStatus ) use ( $msg, $status ) { - $this->assertInstanceOf( 'User', $user ); + $this->assertInstanceOf( \User::class, $user ); $this->assertSame( 'User', $user->getName() ); $error = $msg; $abortStatus = $status; @@ -336,7 +336,7 @@ class LegacyHookPreAuthenticationProviderTest extends \MediaWikiTestCase { $this->hook( 'AbortNewAccount', $this->never() ); $this->hook( 'AbortAutoAccount', $this->once() ) ->will( $this->returnCallback( function ( $user, &$abortError ) use ( $testUser, $error ) { - $this->assertInstanceOf( 'User', $user ); + $this->assertInstanceOf( \User::class, $user ); $this->assertSame( $testUser->getName(), $user->getName() ); $abortError = $error; return $error === null; @@ -349,7 +349,7 @@ class LegacyHookPreAuthenticationProviderTest extends \MediaWikiTestCase { if ( $failMsg === null ) { $this->assertEquals( \StatusValue::newGood(), $status, 'should succeed' ); } else { - $this->assertInstanceOf( 'StatusValue', $status, 'should fail (type)' ); + $this->assertInstanceOf( \StatusValue::class, $status, 'should fail (type)' ); $this->assertFalse( $status->isOk(), 'should fail (ok)' ); $errors = $status->getErrors(); $this->assertEquals( $failMsg, $errors[0]['message'], 'should fail (message)' ); diff --git a/www/wiki/tests/phpunit/includes/auth/PasswordAuthenticationRequestTest.php b/www/wiki/tests/phpunit/includes/auth/PasswordAuthenticationRequestTest.php index 3387e7c9..1ef675b6 100644 --- a/www/wiki/tests/phpunit/includes/auth/PasswordAuthenticationRequestTest.php +++ b/www/wiki/tests/phpunit/includes/auth/PasswordAuthenticationRequestTest.php @@ -129,10 +129,10 @@ class PasswordAuthenticationRequestTest extends AuthenticationRequestTestCase { $ret = $req->describeCredentials(); $this->assertInternalType( 'array', $ret ); $this->assertArrayHasKey( 'provider', $ret ); - $this->assertInstanceOf( 'Message', $ret['provider'] ); + $this->assertInstanceOf( \Message::class, $ret['provider'] ); $this->assertSame( 'authmanager-provider-password', $ret['provider']->getKey() ); $this->assertArrayHasKey( 'account', $ret ); - $this->assertInstanceOf( 'Message', $ret['account'] ); + $this->assertInstanceOf( \Message::class, $ret['account'] ); $this->assertSame( [ 'UTSysop' ], $ret['account']->getParams() ); } } diff --git a/www/wiki/tests/phpunit/includes/auth/PasswordDomainAuthenticationRequestTest.php b/www/wiki/tests/phpunit/includes/auth/PasswordDomainAuthenticationRequestTest.php index f746515b..36be4243 100644 --- a/www/wiki/tests/phpunit/includes/auth/PasswordDomainAuthenticationRequestTest.php +++ b/www/wiki/tests/phpunit/includes/auth/PasswordDomainAuthenticationRequestTest.php @@ -149,10 +149,10 @@ class PasswordDomainAuthenticationRequestTest extends AuthenticationRequestTestC $ret = $req->describeCredentials(); $this->assertInternalType( 'array', $ret ); $this->assertArrayHasKey( 'provider', $ret ); - $this->assertInstanceOf( 'Message', $ret['provider'] ); + $this->assertInstanceOf( \Message::class, $ret['provider'] ); $this->assertSame( 'authmanager-provider-password-domain', $ret['provider']->getKey() ); $this->assertArrayHasKey( 'account', $ret ); - $this->assertInstanceOf( 'Message', $ret['account'] ); + $this->assertInstanceOf( \Message::class, $ret['account'] ); $this->assertSame( 'authmanager-account-password-domain', $ret['account']->getKey() ); $this->assertSame( [ 'UTSysop', 'd2' ], $ret['account']->getParams() ); } diff --git a/www/wiki/tests/phpunit/includes/auth/TemporaryPasswordAuthenticationRequestTest.php b/www/wiki/tests/phpunit/includes/auth/TemporaryPasswordAuthenticationRequestTest.php index 05c5165b..ab4a174e 100644 --- a/www/wiki/tests/phpunit/includes/auth/TemporaryPasswordAuthenticationRequestTest.php +++ b/www/wiki/tests/phpunit/includes/auth/TemporaryPasswordAuthenticationRequestTest.php @@ -70,10 +70,10 @@ class TemporaryPasswordAuthenticationRequestTest extends AuthenticationRequestTe $ret = $req->describeCredentials(); $this->assertInternalType( 'array', $ret ); $this->assertArrayHasKey( 'provider', $ret ); - $this->assertInstanceOf( 'Message', $ret['provider'] ); + $this->assertInstanceOf( \Message::class, $ret['provider'] ); $this->assertSame( 'authmanager-provider-temporarypassword', $ret['provider']->getKey() ); $this->assertArrayHasKey( 'account', $ret ); - $this->assertInstanceOf( 'Message', $ret['account'] ); + $this->assertInstanceOf( \Message::class, $ret['account'] ); $this->assertSame( [ 'UTSysop' ], $ret['account']->getParams() ); } } diff --git a/www/wiki/tests/phpunit/includes/auth/ThrottlePreAuthenticationProviderTest.php b/www/wiki/tests/phpunit/includes/auth/ThrottlePreAuthenticationProviderTest.php index 58982de2..d03b1515 100644 --- a/www/wiki/tests/phpunit/includes/auth/ThrottlePreAuthenticationProviderTest.php +++ b/www/wiki/tests/phpunit/includes/auth/ThrottlePreAuthenticationProviderTest.php @@ -121,7 +121,7 @@ class ThrottlePreAuthenticationProviderTest extends \MediaWikiTestCase { $user = \User::newFromName( 'RandomUser' ); $creator = \User::newFromName( $creatorname ); if ( $hook ) { - $mock = $this->getMockBuilder( 'stdClass' ) + $mock = $this->getMockBuilder( stdClass::class ) ->setMethods( [ 'onExemptFromAccountCreationThrottle' ] ) ->getMock(); $mock->expects( $this->any() )->method( 'onExemptFromAccountCreationThrottle' ) diff --git a/www/wiki/tests/phpunit/includes/auth/ThrottlerTest.php b/www/wiki/tests/phpunit/includes/auth/ThrottlerTest.php index f52048ad..f963ad9c 100644 --- a/www/wiki/tests/phpunit/includes/auth/ThrottlerTest.php +++ b/www/wiki/tests/phpunit/includes/auth/ThrottlerTest.php @@ -4,7 +4,6 @@ namespace MediaWiki\Auth; use BagOStuff; use HashBagOStuff; -use InvalidArgumentException; use Psr\Log\AbstractLogger; use Psr\Log\LoggerInterface; use Psr\Log\NullLogger; diff --git a/www/wiki/tests/phpunit/includes/cache/LocalisationCacheTest.php b/www/wiki/tests/phpunit/includes/cache/LocalisationCacheTest.php index 5eed01cb..42957b60 100644 --- a/www/wiki/tests/phpunit/includes/cache/LocalisationCacheTest.php +++ b/www/wiki/tests/phpunit/includes/cache/LocalisationCacheTest.php @@ -19,7 +19,7 @@ class LocalisationCacheTest extends MediaWikiTestCase { */ protected function getMockLocalisationCache() { global $IP; - $lc = $this->getMockBuilder( 'LocalisationCache' ) + $lc = $this->getMockBuilder( \LocalisationCache::class ) ->setConstructorArgs( [ [ 'store' => 'detect' ] ] ) ->setMethods( [ 'getMessagesDirs' ] ) ->getMock(); diff --git a/www/wiki/tests/phpunit/includes/changes/CategoryMembershipChangeTest.php b/www/wiki/tests/phpunit/includes/changes/CategoryMembershipChangeTest.php index e44de099..ca3ac1b6 100644 --- a/www/wiki/tests/phpunit/includes/changes/CategoryMembershipChangeTest.php +++ b/www/wiki/tests/phpunit/includes/changes/CategoryMembershipChangeTest.php @@ -48,7 +48,7 @@ class CategoryMembershipChangeTest extends MediaWikiLangTestCase { public function setUp() { parent::setUp(); self::$notifyCallCounter = 0; - self::$mockRecentChange = self::getMock( 'RecentChange' ); + self::$mockRecentChange = self::getMock( RecentChange::class ); $this->setContentLang( 'qqx' ); } diff --git a/www/wiki/tests/phpunit/includes/changes/ChangesListBooleanFilterGroupTest.php b/www/wiki/tests/phpunit/includes/changes/ChangesListBooleanFilterGroupTest.php index 07dec055..d80b6c10 100644 --- a/www/wiki/tests/phpunit/includes/changes/ChangesListBooleanFilterGroupTest.php +++ b/www/wiki/tests/phpunit/includes/changes/ChangesListBooleanFilterGroupTest.php @@ -63,6 +63,7 @@ class ChangesListBooleanFilterGroupTest extends MediaWikiTestCase { 'cssClass' => null, 'conflicts' => [], 'subset' => [], + 'defaultHighlightColor' => null, ], [ 'name' => 'hidefoo', @@ -73,6 +74,7 @@ class ChangesListBooleanFilterGroupTest extends MediaWikiTestCase { 'cssClass' => null, 'conflicts' => [], 'subset' => [], + 'defaultHighlightColor' => null, ], ], 'conflicts' => [], diff --git a/www/wiki/tests/phpunit/includes/changes/ChangesListBooleanFilterTest.php b/www/wiki/tests/phpunit/includes/changes/ChangesListBooleanFilterTest.php index 000f0177..35dc1a83 100644 --- a/www/wiki/tests/phpunit/includes/changes/ChangesListBooleanFilterTest.php +++ b/www/wiki/tests/phpunit/includes/changes/ChangesListBooleanFilterTest.php @@ -49,6 +49,7 @@ class ChangesListBooleanFilterTest extends MediaWikiTestCase { 'default' => 1, 'priority' => 1, 'cssClass' => null, + 'defaultHighlightColor' => null, 'conflicts' => [ [ 'group' => 'group', @@ -85,6 +86,7 @@ class ChangesListBooleanFilterTest extends MediaWikiTestCase { 'default' => 1, 'priority' => 1, 'cssClass' => null, + 'defaultHighlightColor' => null, 'conflicts' => [ [ 'group' => 'group', diff --git a/www/wiki/tests/phpunit/includes/changes/ChangesListFilterGroupTest.php b/www/wiki/tests/phpunit/includes/changes/ChangesListFilterGroupTest.php index 465a9d11..6190516e 100644 --- a/www/wiki/tests/phpunit/includes/changes/ChangesListFilterGroupTest.php +++ b/www/wiki/tests/phpunit/includes/changes/ChangesListFilterGroupTest.php @@ -4,12 +4,12 @@ * @covers ChangesListFilterGroup */ class ChangesListFilterGroupTest extends MediaWikiTestCase { - // @codingStandardsIgnoreStart /** + * phpcs:disable Generic.Files.LineLength * @expectedException MWException * @expectedExceptionMessage Group names may not contain '_'. Use the naming convention: 'camelCase' + * phpcs:enable */ - // @codingStandardsIgnoreEnd public function testReservedCharacter() { new MockChangesListFilterGroup( [ diff --git a/www/wiki/tests/phpunit/includes/changes/ChangesListFilterTest.php b/www/wiki/tests/phpunit/includes/changes/ChangesListFilterTest.php index 811c8c2b..039658e2 100644 --- a/www/wiki/tests/phpunit/includes/changes/ChangesListFilterTest.php +++ b/www/wiki/tests/phpunit/includes/changes/ChangesListFilterTest.php @@ -25,12 +25,12 @@ class ChangesListFilterTest extends MediaWikiTestCase { ); } - // @codingStandardsIgnoreStart /** + * phpcs:disable Generic.Files.LineLength * @expectedException MWException * @expectedExceptionMessage Filter names may not contain '_'. Use the naming convention: 'lowercase' + * phpcs:enable */ - // @codingStandardsIgnoreEnd public function testReservedCharacter() { $filter = new MockChangesListFilter( [ @@ -41,12 +41,10 @@ class ChangesListFilterTest extends MediaWikiTestCase { ); } - // @codingStandardsIgnoreStart /** * @expectedException MWException * @expectedExceptionMessage Two filters in a group cannot have the same name: 'somename' */ - // @codingStandardsIgnoreEnd public function testDuplicateName() { new MockChangesListFilter( [ diff --git a/www/wiki/tests/phpunit/includes/changes/ChangesListStringOptionsFilterGroupTest.php b/www/wiki/tests/phpunit/includes/changes/ChangesListStringOptionsFilterGroupTest.php index 4f917e9d..b627178a 100644 --- a/www/wiki/tests/phpunit/includes/changes/ChangesListStringOptionsFilterGroupTest.php +++ b/www/wiki/tests/phpunit/includes/changes/ChangesListStringOptionsFilterGroupTest.php @@ -168,7 +168,7 @@ class ChangesListStringOptionsFilterGroupTest extends MediaWikiTestCase { } protected function getSpecialPage() { - return $this->getMockBuilder( 'ChangesListSpecialPage' ) + return $this->getMockBuilder( ChangesListSpecialPage::class ) ->setConstructorArgs( [ 'ChangesListSpecialPage', '', @@ -179,17 +179,17 @@ class ChangesListStringOptionsFilterGroupTest extends MediaWikiTestCase { /** * @param array $groupDefinition Group definition * @param string $input Value in URL - * - * @dataProvider provideModifyQuery */ protected function modifyQueryHelper( $groupDefinition, $input ) { - $ctx = $this->createMock( 'IContextSource' ); - $dbr = $this->createMock( 'IDatabase' ); + $ctx = $this->createMock( IContextSource::class ); + $dbr = $this->createMock( Wikimedia\Rdbms\IDatabase::class ); $tables = $fields = $conds = $query_options = $join_conds = []; $group = new ChangesListStringOptionsFilterGroup( $groupDefinition ); $specialPage = $this->getSpecialPage(); + $opts = new FormOptions(); + $opts->add( $groupDefinition[ 'name' ], $input ); $group->modifyQuery( $dbr, @@ -199,7 +199,8 @@ class ChangesListStringOptionsFilterGroupTest extends MediaWikiTestCase { $conds, $query_options, $join_conds, - $input + $opts, + true ); } @@ -247,6 +248,7 @@ class ChangesListStringOptionsFilterGroupTest extends MediaWikiTestCase { 'cssClass' => null, 'conflicts' => [], 'subset' => [], + 'defaultHighlightColor' => null, ], [ 'name' => 'foo', @@ -256,6 +258,7 @@ class ChangesListStringOptionsFilterGroupTest extends MediaWikiTestCase { 'cssClass' => null, 'conflicts' => [], 'subset' => [], + 'defaultHighlightColor' => null, ], ], 'conflicts' => [], diff --git a/www/wiki/tests/phpunit/includes/changes/RCCacheEntryFactoryTest.php b/www/wiki/tests/phpunit/includes/changes/RCCacheEntryFactoryTest.php index 97b4c088..b1857ccc 100644 --- a/www/wiki/tests/phpunit/includes/changes/RCCacheEntryFactoryTest.php +++ b/www/wiki/tests/phpunit/includes/changes/RCCacheEntryFactoryTest.php @@ -18,7 +18,7 @@ class RCCacheEntryFactoryTest extends MediaWikiLangTestCase { private $testRecentChangesHelper; /** - * @var LinkRenderer; + * @var LinkRenderer */ private $linkRenderer; @@ -57,7 +57,7 @@ class RCCacheEntryFactoryTest extends MediaWikiLangTestCase { ); $cacheEntry = $cacheEntryFactory->newFromRecentChange( $recentChange, false ); - $this->assertInstanceOf( 'RCCacheEntry', $cacheEntry ); + $this->assertInstanceOf( RCCacheEntry::class, $cacheEntry ); $this->assertEquals( false, $cacheEntry->watched, 'watched' ); $this->assertEquals( '21:21', $cacheEntry->timestamp, 'timestamp' ); @@ -92,7 +92,7 @@ class RCCacheEntryFactoryTest extends MediaWikiLangTestCase { ); $cacheEntry = $cacheEntryFactory->newFromRecentChange( $recentChange, false ); - $this->assertInstanceOf( 'RCCacheEntry', $cacheEntry ); + $this->assertInstanceOf( RCCacheEntry::class, $cacheEntry ); $this->assertEquals( false, $cacheEntry->watched, 'watched' ); $this->assertEquals( '21:21', $cacheEntry->timestamp, 'timestamp' ); @@ -126,7 +126,7 @@ class RCCacheEntryFactoryTest extends MediaWikiLangTestCase { ); $cacheEntry = $cacheEntryFactory->newFromRecentChange( $recentChange, false ); - $this->assertInstanceOf( 'RCCacheEntry', $cacheEntry ); + $this->assertInstanceOf( RCCacheEntry::class, $cacheEntry ); $this->assertEquals( false, $cacheEntry->watched, 'watched' ); $this->assertEquals( '21:21', $cacheEntry->timestamp, 'timestamp' ); diff --git a/www/wiki/tests/phpunit/includes/changes/RecentChangeTest.php b/www/wiki/tests/phpunit/includes/changes/RecentChangeTest.php index d638d0ff..333eb286 100644 --- a/www/wiki/tests/phpunit/includes/changes/RecentChangeTest.php +++ b/www/wiki/tests/phpunit/includes/changes/RecentChangeTest.php @@ -27,12 +27,16 @@ class RecentChangeTest extends MediaWikiTestCase { * @covers RecentChange::loadFromRow */ public function testNewFromRow() { + $user = $this->getTestUser()->getUser(); + $actorId = $user->getActorId(); + $row = new stdClass(); $row->rc_foo = 'AAA'; $row->rc_timestamp = '20150921134808'; $row->rc_deleted = 'bar'; $row->rc_comment_text = 'comment'; $row->rc_comment_data = null; + $row->rc_user = $user->getId(); $rc = RecentChange::newFromRow( $row ); @@ -43,6 +47,9 @@ class RecentChangeTest extends MediaWikiTestCase { 'rc_comment' => 'comment', 'rc_comment_text' => 'comment', 'rc_comment_data' => null, + 'rc_user' => $user->getId(), + 'rc_user_text' => $user->getName(), + 'rc_actor' => $actorId, ]; $this->assertEquals( $expected, $rc->getAttributes() ); @@ -51,10 +58,11 @@ class RecentChangeTest extends MediaWikiTestCase { $row->rc_timestamp = '20150921134808'; $row->rc_deleted = 'bar'; $row->rc_comment = 'comment'; + $row->rc_user = $user->getId(); - MediaWiki\suppressWarnings(); + Wikimedia\suppressWarnings(); $rc = RecentChange::newFromRow( $row ); - MediaWiki\restoreWarnings(); + Wikimedia\restoreWarnings(); $expected = [ 'rc_foo' => 'AAA', @@ -63,6 +71,9 @@ class RecentChangeTest extends MediaWikiTestCase { 'rc_comment' => 'comment', 'rc_comment_text' => 'comment', 'rc_comment_data' => null, + 'rc_user' => $user->getId(), + 'rc_user_text' => $user->getName(), + 'rc_actor' => $actorId, ]; $this->assertEquals( $expected, $rc->getAttributes() ); } diff --git a/www/wiki/tests/phpunit/includes/changetags/ChangeTagsTest.php b/www/wiki/tests/phpunit/includes/changetags/ChangeTagsTest.php index 723d6856..63e0ec22 100644 --- a/www/wiki/tests/phpunit/includes/changetags/ChangeTagsTest.php +++ b/www/wiki/tests/phpunit/includes/changetags/ChangeTagsTest.php @@ -5,7 +5,7 @@ */ class ChangeTagsTest extends MediaWikiTestCase { - // TODO only modifyDisplayQuery is tested, nothing else is + // TODO only modifyDisplayQuery and getSoftwareTags are tested, nothing else is /** @dataProvider provideModifyDisplayQuery */ public function testModifyDisplayQuery( $origQuery, $filter_tag, $useTags, $modifiedQuery ) { @@ -244,4 +244,66 @@ class ChangeTagsTest extends MediaWikiTestCase { ]; } + public static function dataGetSoftwareTags() { + return [ + [ + [ + 'mw-contentModelChange' => true, + 'mw-redirect' => true, + 'mw-rollback' => true, + 'mw-blank' => true, + 'mw-replace' => true + ], + [ + 'mw-rollback', + 'mw-replace', + 'mw-blank' + ] + ], + + [ + [ + 'mw-contentmodelchanged' => true, + 'mw-replace' => true, + 'mw-new-redirects' => true, + 'mw-changed-redirect-target' => true, + 'mw-rolback' => true, + 'mw-blanking' => false + ], + [ + 'mw-replace', + 'mw-changed-redirect-target' + ] + ], + + [ + [ + null, + false, + 'Lorem ipsum', + 'mw-translation' + ], + [] + ], + + [ + [], + [] + ] + ]; + } + + /** + * @dataProvider dataGetSoftwareTags + * @covers ChangeTags::getSoftwareTags + */ + public function testGetSoftwareTags( $softwareTags, $expected ) { + $this->setMwGlobals( 'wgSoftwareTags', $softwareTags ); + + $actual = ChangeTags::getSoftwareTags(); + // Order of tags in arrays is not important + sort( $expected ); + sort( $actual ); + $this->assertEquals( $expected, $actual ); + } } diff --git a/www/wiki/tests/phpunit/includes/collation/CollationFaTest.php b/www/wiki/tests/phpunit/includes/collation/CollationFaTest.php index 53a4f7b7..f7455419 100644 --- a/www/wiki/tests/phpunit/includes/collation/CollationFaTest.php +++ b/www/wiki/tests/phpunit/includes/collation/CollationFaTest.php @@ -1,4 +1,8 @@ <?php + +/** + * @covers CollationFa + */ class CollationFaTest extends MediaWikiTestCase { /* @@ -9,9 +13,7 @@ class CollationFaTest extends MediaWikiTestCase { public function setUp() { parent::setUp(); - if ( !extension_loaded( 'intl' ) ) { - $this->markTestSkipped( "PHP extension 'intl' is not loaded, skipping." ); - } + $this->checkPHPExtension( 'intl' ); } /** diff --git a/www/wiki/tests/phpunit/includes/collation/CollationTest.php b/www/wiki/tests/phpunit/includes/collation/CollationTest.php index 25911a79..b92e651e 100644 --- a/www/wiki/tests/phpunit/includes/collation/CollationTest.php +++ b/www/wiki/tests/phpunit/includes/collation/CollationTest.php @@ -21,7 +21,7 @@ class CollationTest extends MediaWikiLangTestCase { * code makes this assumption. * * @param string $lang Language code for collator - * @param string $base Base string + * @param string $base * @param string $extended String containing base as a prefix. * * @dataProvider prefixDataProvider diff --git a/www/wiki/tests/phpunit/includes/collation/CustomUppercaseCollationTest.php b/www/wiki/tests/phpunit/includes/collation/CustomUppercaseCollationTest.php index 5d5317be..f9e0bc9b 100644 --- a/www/wiki/tests/phpunit/includes/collation/CustomUppercaseCollationTest.php +++ b/www/wiki/tests/phpunit/includes/collation/CustomUppercaseCollationTest.php @@ -1,11 +1,15 @@ <?php +/** + * @covers CustomUppercaseCollation + */ class CustomUppercaseCollationTest extends MediaWikiTestCase { public function setUp() { $this->collation = new CustomUppercaseCollation( [ 'D', 'C', + 'Cs', 'B' ], Language::factory( 'en' ) ); @@ -31,6 +35,7 @@ class CustomUppercaseCollationTest extends MediaWikiTestCase { [ '💩 ', 'C', 'Test relocated to end' ], [ 'c', 'b', 'lowercase' ], [ 'x', 'z', 'lowercase original' ], + [ 'Cz', 'Cs', 'digraphs' ], [ 'C50D', 'C100', 'Numbers' ] ]; } @@ -50,8 +55,14 @@ class CustomUppercaseCollationTest extends MediaWikiTestCase { [ 'afdsa', 'A' ], [ "\xF3\xB3\x80\x80Foo", 'D' ], [ "\xF3\xB3\x80\x81Foo", 'C' ], - [ "\xF3\xB3\x80\x82Foo", 'B' ], - [ "\xF3\xB3\x80\x83Foo", "\xF3\xB3\x80\x83" ], + [ "\xF3\xB3\x80\x82Foo", 'Cs' ], + [ "\xF3\xB3\x80\x83Foo", 'B' ], + [ "\xF3\xB3\x80\x84Foo", "\xF3\xB3\x80\x84" ], + [ 'C', 'C' ], + [ 'Cz', 'C' ], + [ 'Cs', 'Cs' ], + [ 'CS', 'Cs' ], + [ 'cs', 'Cs' ], ]; } } diff --git a/www/wiki/tests/phpunit/includes/composer/ComposerVersionNormalizerTest.php b/www/wiki/tests/phpunit/includes/composer/ComposerVersionNormalizerTest.php index 8a2ebaff..c5c0dc7d 100644 --- a/www/wiki/tests/phpunit/includes/composer/ComposerVersionNormalizerTest.php +++ b/www/wiki/tests/phpunit/includes/composer/ComposerVersionNormalizerTest.php @@ -7,7 +7,10 @@ * * @author Jeroen De Dauw < jeroendedauw@gmail.com > */ -class ComposerVersionNormalizerTest extends PHPUnit_Framework_TestCase { +class ComposerVersionNormalizerTest extends PHPUnit\Framework\TestCase { + + use MediaWikiCoversValidator; + use PHPUnit4And6Compat; /** * @dataProvider nonStringProvider @@ -15,7 +18,7 @@ class ComposerVersionNormalizerTest extends PHPUnit_Framework_TestCase { public function testGivenNonString_normalizeThrowsInvalidArgumentException( $nonString ) { $normalizer = new ComposerVersionNormalizer(); - $this->setExpectedException( 'InvalidArgumentException' ); + $this->setExpectedException( InvalidArgumentException::class ); $normalizer->normalizeSuffix( $nonString ); } diff --git a/www/wiki/tests/phpunit/includes/config/ConfigFactoryTest.php b/www/wiki/tests/phpunit/includes/config/ConfigFactoryTest.php index 608d8d94..ea747afa 100644 --- a/www/wiki/tests/phpunit/includes/config/ConfigFactoryTest.php +++ b/www/wiki/tests/phpunit/includes/config/ConfigFactoryTest.php @@ -18,13 +18,22 @@ class ConfigFactoryTest extends MediaWikiTestCase { */ public function testRegisterInvalid() { $factory = new ConfigFactory(); - $this->setExpectedException( 'InvalidArgumentException' ); + $this->setExpectedException( InvalidArgumentException::class ); $factory->register( 'invalid', 'Invalid callback' ); } /** * @covers ConfigFactory::register */ + public function testRegisterInvalidInstance() { + $factory = new ConfigFactory(); + $this->setExpectedException( InvalidArgumentException::class ); + $factory->register( 'invalidInstance', new stdClass ); + } + + /** + * @covers ConfigFactory::register + */ public function testRegisterInstance() { $config = GlobalVarConfig::newInstance(); $factory = new ConfigFactory(); @@ -78,7 +87,7 @@ class ConfigFactoryTest extends MediaWikiTestCase { $this->assertNotSame( $bar, $newBar, 'don\'t salvage if callbacks differ' ); // the new factory doesn't have quux defined, so the quux instance should not be salvaged - $this->setExpectedException( 'ConfigException' ); + $this->setExpectedException( ConfigException::class ); $newFactory->makeConfig( 'quux' ); } @@ -101,7 +110,7 @@ class ConfigFactoryTest extends MediaWikiTestCase { $factory->register( 'unittest', 'GlobalVarConfig::newInstance' ); $conf = $factory->makeConfig( 'unittest' ); - $this->assertInstanceOf( 'Config', $conf ); + $this->assertInstanceOf( Config::class, $conf ); $this->assertSame( $conf, $factory->makeConfig( 'unittest' ) ); } @@ -122,7 +131,7 @@ class ConfigFactoryTest extends MediaWikiTestCase { $factory = new ConfigFactory(); $factory->register( '*', 'GlobalVarConfig::newInstance' ); $conf = $factory->makeConfig( 'unittest' ); - $this->assertInstanceOf( 'Config', $conf ); + $this->assertInstanceOf( Config::class, $conf ); } /** @@ -130,7 +139,7 @@ class ConfigFactoryTest extends MediaWikiTestCase { */ public function testMakeConfigWithNoBuilders() { $factory = new ConfigFactory(); - $this->setExpectedException( 'ConfigException' ); + $this->setExpectedException( ConfigException::class ); $factory->makeConfig( 'nobuilderregistered' ); } @@ -142,7 +151,7 @@ class ConfigFactoryTest extends MediaWikiTestCase { $factory->register( 'unittest', function () { return true; // Not a Config object } ); - $this->setExpectedException( 'UnexpectedValueException' ); + $this->setExpectedException( UnexpectedValueException::class ); $factory->makeConfig( 'unittest' ); } @@ -153,7 +162,7 @@ class ConfigFactoryTest extends MediaWikiTestCase { // NOTE: the global config factory returned here has been overwritten // for operation in test mode. It may not reflect LocalSettings. $factory = MediaWikiServices::getInstance()->getConfigFactory(); - $this->assertInstanceOf( 'Config', $factory->makeConfig( 'main' ) ); + $this->assertInstanceOf( Config::class, $factory->makeConfig( 'main' ) ); } } diff --git a/www/wiki/tests/phpunit/includes/config/EtcdConfigTest.php b/www/wiki/tests/phpunit/includes/config/EtcdConfigTest.php index ebe19725..3eecf827 100644 --- a/www/wiki/tests/phpunit/includes/config/EtcdConfigTest.php +++ b/www/wiki/tests/phpunit/includes/config/EtcdConfigTest.php @@ -2,7 +2,10 @@ use Wikimedia\TestingAccessWrapper; -class EtcConfigTest extends PHPUnit_Framework_TestCase { +class EtcdConfigTest extends PHPUnit\Framework\TestCase { + + use MediaWikiCoversValidator; + use PHPUnit4And6Compat; private function createConfigMock( array $options = [] ) { return $this->getMockBuilder( EtcdConfig::class ) @@ -15,14 +18,23 @@ class EtcConfigTest extends PHPUnit_Framework_TestCase { ->getMock(); } - private function createSimpleConfigMock( array $config ) { + private static function createEtcdResponse( array $response ) { + $baseResponse = [ + 'config' => null, + 'error' => null, + 'retry' => false, + 'modifiedIndex' => 0, + ]; + return array_merge( $baseResponse, $response ); + } + + private function createSimpleConfigMock( array $config, $index = 0 ) { $mock = $this->createConfigMock(); $mock->expects( $this->once() )->method( 'fetchAllFromEtcd' ) - ->willReturn( [ - $config, - null, // error - false // retry? - ] ); + ->willReturn( self::createEtcdResponse( [ + 'config' => $config, + 'modifiedIndex' => $index, + ] ) ); return $mock; } @@ -69,6 +81,17 @@ class EtcConfigTest extends PHPUnit_Framework_TestCase { } /** + * @covers EtcdConfig::getModifiedIndex + */ + public function testGetModifiedIndex() { + $config = $this->createSimpleConfigMock( + [ 'some' => 'value' ], + 123 + ); + $this->assertSame( 123, $config->getModifiedIndex() ); + } + + /** * @covers EtcdConfig::__construct */ public function testConstructCacheObj() { @@ -79,6 +102,7 @@ class EtcConfigTest extends PHPUnit_Framework_TestCase { ->willReturn( [ 'config' => [ 'known' => 'from-cache' ], 'expires' => INF, + 'modifiedIndex' => 123 ] ); $config = $this->createConfigMock( [ 'cache' => $cache ] ); @@ -93,11 +117,8 @@ class EtcConfigTest extends PHPUnit_Framework_TestCase { 'class' => HashBagOStuff::class ] ] ); $config->expects( $this->once() )->method( 'fetchAllFromEtcd' ) - ->willReturn( [ - [ 'known' => 'from-fetch' ], - null, // error - false // retry? - ] ); + ->willReturn( self::createEtcdResponse( + [ 'config' => [ 'known' => 'from-fetch' ], ] ) ); $this->assertSame( 'from-fetch', $config->get( 'known' ) ); } @@ -164,7 +185,8 @@ class EtcConfigTest extends PHPUnit_Framework_TestCase { 'cache' => $cache, ] ); $mock->expects( $this->once() )->method( 'fetchAllFromEtcd' ) - ->willReturn( [ [ 'known' => 'from-fetch' ], null, false ] ); + ->willReturn( + self::createEtcdResponse( [ 'config' => [ 'known' => 'from-fetch' ] ] ) ); $this->assertSame( 'from-fetch', $mock->get( 'known' ) ); } @@ -189,7 +211,7 @@ class EtcConfigTest extends PHPUnit_Framework_TestCase { 'cache' => $cache, ] ); $mock->expects( $this->once() )->method( 'fetchAllFromEtcd' ) - ->willReturn( [ null, 'Fake error', false ] ); + ->willReturn( self::createEtcdResponse( [ 'error' => 'Fake error', ] ) ); $this->setExpectedException( ConfigException::class ); $mock->get( 'key' ); @@ -211,6 +233,7 @@ class EtcConfigTest extends PHPUnit_Framework_TestCase { [ 'config' => [ 'known' => 'from-cache' ], 'expires' => INF, + 'modifiedIndex' => 123 ] ) ); // .. misses lock @@ -239,6 +262,7 @@ class EtcConfigTest extends PHPUnit_Framework_TestCase { ->willReturn( [ 'config' => [ 'known' => 'from-cache' ], 'expires' => INF, + 'modifiedIndex' => 0, ] ); $cache->expects( $this->never() )->method( 'lock' ); @@ -264,6 +288,7 @@ class EtcConfigTest extends PHPUnit_Framework_TestCase { ->willReturn( [ 'config' => [ 'known' => 'from-cache' ], 'expires' => INF, + 'modifiedIndex' => 0, ] ); $cache->expects( $this->never() )->method( 'lock' ); @@ -290,6 +315,7 @@ class EtcConfigTest extends PHPUnit_Framework_TestCase { [ 'config' => [ 'known' => 'from-cache-expired' ], 'expires' => -INF, + 'modifiedIndex' => 0, ] ); // .. gets lock @@ -301,7 +327,7 @@ class EtcConfigTest extends PHPUnit_Framework_TestCase { 'cache' => $cache, ] ); $mock->expects( $this->once() )->method( 'fetchAllFromEtcd' ) - ->willReturn( [ [ 'known' => 'from-fetch' ], null, false ] ); + ->willReturn( self::createEtcdResponse( [ 'config' => [ 'known' => 'from-fetch' ] ] ) ); $this->assertSame( 'from-fetch', $mock->get( 'known' ) ); } @@ -319,6 +345,7 @@ class EtcConfigTest extends PHPUnit_Framework_TestCase { [ 'config' => [ 'known' => 'from-cache-expired' ], 'expires' => -INF, + 'modifiedIndex' => 0, ] ); // .. gets lock @@ -330,7 +357,7 @@ class EtcConfigTest extends PHPUnit_Framework_TestCase { 'cache' => $cache, ] ); $mock->expects( $this->once() )->method( 'fetchAllFromEtcd' ) - ->willReturn( [ null, 'Fake failure', true ] ); + ->willReturn( self::createEtcdResponse( [ 'error' => 'Fake failure', 'retry' => true ] ) ); $this->assertSame( 'from-cache-expired', $mock->get( 'known' ) ); } @@ -348,6 +375,7 @@ class EtcConfigTest extends PHPUnit_Framework_TestCase { ->willReturn( [ 'config' => [ 'known' => 'from-cache-expired' ], 'expires' => -INF, + 'modifiedIndex' => 0, ] ); // .. misses lock $cache->expects( $this->once() )->method( 'lock' ) @@ -372,16 +400,16 @@ class EtcConfigTest extends PHPUnit_Framework_TestCase { 'body' => json_encode( [ 'node' => [ 'nodes' => [ [ 'key' => '/example/foo', - 'value' => json_encode( [ 'val' => true ] ) + 'value' => json_encode( [ 'val' => true ] ), + 'modifiedIndex' => 123 ], ] ] ] ), 'error' => '', ], - 'expect' => [ - [ 'foo' => true ], // data - null, - false // retry - ], + 'expect' => self::createEtcdResponse( [ + 'config' => [ 'foo' => true ], // data + 'modifiedIndex' => 123 + ] ), ], '200 OK - Empty dir' => [ 'http' => [ @@ -391,25 +419,27 @@ class EtcConfigTest extends PHPUnit_Framework_TestCase { 'body' => json_encode( [ 'node' => [ 'nodes' => [ [ 'key' => '/example/foo', - 'value' => json_encode( [ 'val' => true ] ) + 'value' => json_encode( [ 'val' => true ] ), + 'modifiedIndex' => 123 ], [ 'key' => '/example/sub', 'dir' => true, + 'modifiedIndex' => 234, 'nodes' => [], ], [ 'key' => '/example/bar', - 'value' => json_encode( [ 'val' => false ] ) + 'value' => json_encode( [ 'val' => false ] ), + 'modifiedIndex' => 125 ], ] ] ] ), 'error' => '', ], - 'expect' => [ - [ 'foo' => true, 'bar' => false ], // data - null, - false // retry - ], + 'expect' => self::createEtcdResponse( [ + 'config' => [ 'foo' => true, 'bar' => false ], // data + 'modifiedIndex' => 125 // largest modified index + ] ), ], '200 OK - Recursive' => [ 'http' => [ @@ -420,25 +450,28 @@ class EtcConfigTest extends PHPUnit_Framework_TestCase { [ 'key' => '/example/a', 'dir' => true, + 'modifiedIndex' => 124, 'nodes' => [ [ 'key' => 'b', 'value' => json_encode( [ 'val' => true ] ), + 'modifiedIndex' => 123, + ], [ 'key' => 'c', 'value' => json_encode( [ 'val' => false ] ), + 'modifiedIndex' => 123, ], ], ], ] ] ] ), 'error' => '', ], - 'expect' => [ - [ 'a/b' => true, 'a/c' => false ], // data - null, - false // retry - ], + 'expect' => self::createEtcdResponse( [ + 'config' => [ 'a/b' => true, 'a/c' => false ], // data + 'modifiedIndex' => 123 // largest modified index + ] ), ], '200 OK - Missing nodes at second level' => [ 'http' => [ @@ -449,15 +482,32 @@ class EtcConfigTest extends PHPUnit_Framework_TestCase { [ 'key' => '/example/a', 'dir' => true, + 'modifiedIndex' => 0, ], ] ] ] ), 'error' => '', ], - 'expect' => [ - null, - "Unexpected JSON response in dir 'a'; missing 'nodes' list.", - false // retry + 'expect' => self::createEtcdResponse( [ + 'error' => "Unexpected JSON response in dir 'a'; missing 'nodes' list.", + ] ), + ], + '200 OK - Directory with non-array "nodes" key' => [ + 'http' => [ + 'code' => 200, + 'reason' => 'OK', + 'headers' => [], + 'body' => json_encode( [ 'node' => [ 'nodes' => [ + [ + 'key' => '/example/a', + 'dir' => true, + 'nodes' => 'not an array' + ], + ] ] ] ), + 'error' => '', ], + 'expect' => self::createEtcdResponse( [ + 'error' => "Unexpected JSON response in dir 'a'; 'nodes' is not an array.", + ] ), ], '200 OK - Correctly encoded garbage response' => [ 'http' => [ @@ -467,11 +517,9 @@ class EtcConfigTest extends PHPUnit_Framework_TestCase { 'body' => json_encode( [ 'foo' => 'bar' ] ), 'error' => '', ], - 'expect' => [ - null, - "Unexpected JSON response: Missing or invalid node at top level.", - false // retry - ], + 'expect' => self::createEtcdResponse( [ + 'error' => "Unexpected JSON response: Missing or invalid node at top level.", + ] ), ], '200 OK - Bad value' => [ 'http' => [ @@ -481,30 +529,27 @@ class EtcConfigTest extends PHPUnit_Framework_TestCase { 'body' => json_encode( [ 'node' => [ 'nodes' => [ [ 'key' => '/example/foo', - 'value' => ';"broken{value' + 'value' => ';"broken{value', + 'modifiedIndex' => 123, ] ] ] ] ), 'error' => '', ], - 'expect' => [ - null, // data - "Failed to parse value for 'foo'.", - false // retry - ], + 'expect' => self::createEtcdResponse( [ + 'error' => "Failed to parse value for 'foo'.", + ] ), ], '200 OK - Empty node list' => [ 'http' => [ 'code' => 200, 'reason' => 'OK', 'headers' => [], - 'body' => '{"node":{"nodes":[]}}', + 'body' => '{"node":{"nodes":[], "modifiedIndex": 12 }}', 'error' => '', ], - 'expect' => [ - [], // data - null, - false // retry - ], + 'expect' => self::createEtcdResponse( [ + 'config' => [], // data + ] ), ], '200 OK - Invalid JSON' => [ 'http' => [ @@ -514,11 +559,9 @@ class EtcConfigTest extends PHPUnit_Framework_TestCase { 'body' => '', 'error' => '(curl error: no status set)', ], - 'expect' => [ - null, // data - "Error unserializing JSON response.", - false // retry - ], + 'expect' => self::createEtcdResponse( [ + 'error' => "Error unserializing JSON response.", + ] ), ], '404 Not Found' => [ 'http' => [ @@ -528,11 +571,9 @@ class EtcConfigTest extends PHPUnit_Framework_TestCase { 'body' => '', 'error' => '', ], - 'expect' => [ - null, // data - 'HTTP 404 (Not Found)', - false // retry - ], + 'expect' => self::createEtcdResponse( [ + 'error' => 'HTTP 404 (Not Found)', + ] ), ], '400 Bad Request - custom error' => [ 'http' => [ @@ -542,11 +583,10 @@ class EtcConfigTest extends PHPUnit_Framework_TestCase { 'body' => '', 'error' => 'No good reason', ], - 'expect' => [ - null, // data - 'No good reason', - true // retry - ], + 'expect' => self::createEtcdResponse( [ + 'error' => 'No good reason', + 'retry' => true, // retry + ] ), ], ]; } diff --git a/www/wiki/tests/phpunit/includes/config/GlobalVarConfigTest.php b/www/wiki/tests/phpunit/includes/config/GlobalVarConfigTest.php index a6b220d6..db5f73d4 100644 --- a/www/wiki/tests/phpunit/includes/config/GlobalVarConfigTest.php +++ b/www/wiki/tests/phpunit/includes/config/GlobalVarConfigTest.php @@ -7,7 +7,7 @@ class GlobalVarConfigTest extends MediaWikiTestCase { */ public function testNewInstance() { $config = GlobalVarConfig::newInstance(); - $this->assertInstanceOf( 'GlobalVarConfig', $config ); + $this->assertInstanceOf( GlobalVarConfig::class, $config ); $this->maybeStashGlobal( 'wgBaz' ); $GLOBALS['wgBaz'] = 'somevalue'; // Check prefix is set to 'wg' @@ -24,7 +24,7 @@ class GlobalVarConfigTest extends MediaWikiTestCase { $this->maybeStashGlobal( $var ); $GLOBALS[$var] = $rand; $config = new GlobalVarConfig( $prefix ); - $this->assertInstanceOf( 'GlobalVarConfig', $config ); + $this->assertInstanceOf( GlobalVarConfig::class, $config ); $this->assertEquals( $rand, $config->get( 'GlobalVarConfigTest' ) ); } @@ -83,7 +83,7 @@ class GlobalVarConfigTest extends MediaWikiTestCase { public function testGet( $name, $prefix, $expected ) { $config = new GlobalVarConfig( $prefix ); if ( $expected === false ) { - $this->setExpectedException( 'ConfigException', 'GlobalVarConfig::get: undefined option:' ); + $this->setExpectedException( ConfigException::class, 'GlobalVarConfig::get: undefined option:' ); } $this->assertEquals( $config->get( $name ), $expected ); } diff --git a/www/wiki/tests/phpunit/includes/config/HashConfigTest.php b/www/wiki/tests/phpunit/includes/config/HashConfigTest.php index 19b412d2..bac8311c 100644 --- a/www/wiki/tests/phpunit/includes/config/HashConfigTest.php +++ b/www/wiki/tests/phpunit/includes/config/HashConfigTest.php @@ -7,7 +7,7 @@ class HashConfigTest extends MediaWikiTestCase { */ public function testNewInstance() { $conf = HashConfig::newInstance(); - $this->assertInstanceOf( 'HashConfig', $conf ); + $this->assertInstanceOf( HashConfig::class, $conf ); } /** @@ -15,7 +15,7 @@ class HashConfigTest extends MediaWikiTestCase { */ public function testConstructor() { $conf = new HashConfig(); - $this->assertInstanceOf( 'HashConfig', $conf ); + $this->assertInstanceOf( HashConfig::class, $conf ); // Test passing arguments to the constructor $conf2 = new HashConfig( [ @@ -32,7 +32,7 @@ class HashConfigTest extends MediaWikiTestCase { 'one' => '1', ] ); $this->assertEquals( '1', $conf->get( 'one' ) ); - $this->setExpectedException( 'ConfigException', 'HashConfig::get: undefined option' ); + $this->setExpectedException( ConfigException::class, 'HashConfig::get: undefined option' ); $conf->get( 'two' ); } diff --git a/www/wiki/tests/phpunit/includes/config/MultiConfigTest.php b/www/wiki/tests/phpunit/includes/config/MultiConfigTest.php index d1eb5102..fc283951 100644 --- a/www/wiki/tests/phpunit/includes/config/MultiConfigTest.php +++ b/www/wiki/tests/phpunit/includes/config/MultiConfigTest.php @@ -17,7 +17,7 @@ class MultiConfigTest extends MediaWikiTestCase { $this->assertEquals( 'bar', $multi->get( 'foo' ) ); $this->assertEquals( 'foo', $multi->get( 'bar' ) ); - $this->setExpectedException( 'ConfigException', 'MultiConfig::get: undefined option:' ); + $this->setExpectedException( ConfigException::class, 'MultiConfig::get: undefined option:' ); $multi->get( 'notset' ); } diff --git a/www/wiki/tests/phpunit/includes/content/ContentHandlerTest.php b/www/wiki/tests/phpunit/includes/content/ContentHandlerTest.php index ee79ffa4..309b7b11 100644 --- a/www/wiki/tests/phpunit/includes/content/ContentHandlerTest.php +++ b/www/wiki/tests/phpunit/includes/content/ContentHandlerTest.php @@ -22,12 +22,12 @@ class ContentHandlerTest extends MediaWikiTestCase { 12312 => 'testing', ], 'wgContentHandlers' => [ - CONTENT_MODEL_WIKITEXT => 'WikitextContentHandler', - CONTENT_MODEL_JAVASCRIPT => 'JavaScriptContentHandler', - CONTENT_MODEL_JSON => 'JsonContentHandler', - CONTENT_MODEL_CSS => 'CssContentHandler', - CONTENT_MODEL_TEXT => 'TextContentHandler', - 'testing' => 'DummyContentHandlerForTesting', + CONTENT_MODEL_WIKITEXT => WikitextContentHandler::class, + CONTENT_MODEL_JAVASCRIPT => JavaScriptContentHandler::class, + CONTENT_MODEL_JSON => JsonContentHandler::class, + CONTENT_MODEL_CSS => CssContentHandler::class, + CONTENT_MODEL_TEXT => TextContentHandler::class, + 'testing' => DummyContentHandlerForTesting::class, 'testing-callbacks' => function ( $modelId ) { return new DummyContentHandlerForTesting( $modelId ); } @@ -35,7 +35,7 @@ class ContentHandlerTest extends MediaWikiTestCase { ] ); // Reset namespace cache - MWNamespace::getCanonicalNamespaces( true ); + MWNamespace::clearCaches(); $wgContLang->resetNamespaces(); // And LinkCache MediaWikiServices::getInstance()->resetServiceForTesting( 'LinkCache' ); @@ -45,7 +45,7 @@ class ContentHandlerTest extends MediaWikiTestCase { global $wgContLang; // Reset namespace cache - MWNamespace::getCanonicalNamespaces( true ); + MWNamespace::clearCaches(); $wgContLang->resetNamespaces(); // And LinkCache MediaWikiServices::getInstance()->resetServiceForTesting( 'LinkCache' ); @@ -248,10 +248,6 @@ class ContentHandlerTest extends MediaWikiTestCase { $this->assertNull( $text ); } - /* - public static function makeContent( $text, Title $title, $modelId = null, $format = null ) {} - */ - public static function dataMakeContent() { return [ [ 'hallo', 'Help:Test', null, null, CONTENT_MODEL_WIKITEXT, 'hallo', false ], @@ -332,7 +328,9 @@ class ContentHandlerTest extends MediaWikiTestCase { } } - /* + /** + * @covers ContentHandler::getAutosummary + * * Test if we become a "Created blank page" summary from getAutoSummary if no Content added to * page. */ @@ -342,26 +340,43 @@ class ContentHandlerTest extends MediaWikiTestCase { $content = new DummyContentHandlerForTesting( CONTENT_MODEL_WIKITEXT ); $title = Title::newFromText( 'Help:Test' ); // Create a new content object with no content - $newContent = ContentHandler::makeContent( '', $title, null, null, CONTENT_MODEL_WIKITEXT ); + $newContent = ContentHandler::makeContent( '', $title, CONTENT_MODEL_WIKITEXT, null ); // first check, if we become a blank page created summary with the right bitmask $autoSummary = $content->getAutosummary( null, $newContent, 97 ); - $this->assertEquals( $autoSummary, 'Created blank page' ); + $this->assertEquals( $autoSummary, + wfMessage( 'autosumm-newblank' )->inContentLanguage()->text() ); // now check, what we become with another bitmask $autoSummary = $content->getAutosummary( null, $newContent, 92 ); $this->assertEquals( $autoSummary, '' ); } - /* - public function testSupportsSections() { - $this->markTestIncomplete( "not yet implemented" ); + /** + * Test software tag that is added when content model of the page changes + * @covers ContentHandler::getChangeTag + */ + public function testGetChangeTag() { + $this->setMwGlobals( 'wgSoftwareTags', [ 'mw-contentmodelchange' => true ] ); + $wikitextContentHandler = new DummyContentHandlerForTesting( CONTENT_MODEL_WIKITEXT ); + // Create old content object with javascript content model + $oldContent = ContentHandler::makeContent( '', null, CONTENT_MODEL_JAVASCRIPT, null ); + // Create new content object with wikitext content model + $newContent = ContentHandler::makeContent( '', null, CONTENT_MODEL_WIKITEXT, null ); + // Get the tag for this edit + $tag = $wikitextContentHandler->getChangeTag( $oldContent, $newContent, EDIT_UPDATE ); + $this->assertSame( $tag, 'mw-contentmodelchange' ); } - */ + /** + * @covers ContentHandler::supportsCategories + */ public function testSupportsCategories() { $handler = new DummyContentHandlerForTesting( CONTENT_MODEL_WIKITEXT ); $this->assertTrue( $handler->supportsCategories(), 'content model supports categories' ); } + /** + * @covers ContentHandler::supportsDirectEditing + */ public function testSupportsDirectEditing() { $handler = new DummyContentHandlerForTesting( CONTENT_MODEL_JSON ); $this->assertFalse( $handler->supportsDirectEditing(), 'direct editing is not supported' ); @@ -379,17 +394,18 @@ class ContentHandlerTest extends MediaWikiTestCase { public function provideGetModelForID() { return [ - [ CONTENT_MODEL_WIKITEXT, 'WikitextContentHandler' ], - [ CONTENT_MODEL_JAVASCRIPT, 'JavaScriptContentHandler' ], - [ CONTENT_MODEL_JSON, 'JsonContentHandler' ], - [ CONTENT_MODEL_CSS, 'CssContentHandler' ], - [ CONTENT_MODEL_TEXT, 'TextContentHandler' ], - [ 'testing', 'DummyContentHandlerForTesting' ], - [ 'testing-callbacks', 'DummyContentHandlerForTesting' ], + [ CONTENT_MODEL_WIKITEXT, WikitextContentHandler::class ], + [ CONTENT_MODEL_JAVASCRIPT, JavaScriptContentHandler::class ], + [ CONTENT_MODEL_JSON, JsonContentHandler::class ], + [ CONTENT_MODEL_CSS, CssContentHandler::class ], + [ CONTENT_MODEL_TEXT, TextContentHandler::class ], + [ 'testing', DummyContentHandlerForTesting::class ], + [ 'testing-callbacks', DummyContentHandlerForTesting::class ], ]; } /** + * @covers ContentHandler::getForModelID * @dataProvider provideGetModelForID */ public function testGetModelForID( $modelId, $handlerClass ) { @@ -398,6 +414,9 @@ class ContentHandlerTest extends MediaWikiTestCase { $this->assertInstanceOf( $handlerClass, $handler ); } + /** + * @covers ContentHandler::getFieldsForSearchIndex + */ public function testGetFieldsForSearchIndex() { $searchEngine = $this->newSearchEngine(); @@ -413,7 +432,7 @@ class ContentHandlerTest extends MediaWikiTestCase { } private function newSearchEngine() { - $searchEngine = $this->getMockBuilder( 'SearchEngine' ) + $searchEngine = $this->getMockBuilder( SearchEngine::class ) ->getMock(); $searchEngine->expects( $this->any() ) @@ -429,7 +448,7 @@ class ContentHandlerTest extends MediaWikiTestCase { * @covers ContentHandler::getDataForSearchIndex */ public function testDataIndexFields() { - $mockEngine = $this->createMock( 'SearchEngine' ); + $mockEngine = $this->createMock( SearchEngine::class ); $title = Title::newFromText( 'Not_Main_Page', NS_MAIN ); $page = new WikiPage( $title ); diff --git a/www/wiki/tests/phpunit/includes/content/CssContentHandlerTest.php b/www/wiki/tests/phpunit/includes/content/CssContentHandlerTest.php index d92fb8fc..7ca1afce 100644 --- a/www/wiki/tests/phpunit/includes/content/CssContentHandlerTest.php +++ b/www/wiki/tests/phpunit/includes/content/CssContentHandlerTest.php @@ -13,7 +13,7 @@ class CssContentHandlerTest extends MediaWikiLangTestCase { ] ); $ch = new CssContentHandler(); $content = $ch->makeRedirectContent( Title::newFromText( $title ) ); - $this->assertInstanceOf( 'CssContent', $content ); + $this->assertInstanceOf( CssContent::class, $content ); $this->assertEquals( $expected, $content->serialize( CONTENT_FORMAT_CSS ) ); } @@ -21,7 +21,7 @@ class CssContentHandlerTest extends MediaWikiLangTestCase { * Keep this in sync with CssContentTest::provideGetRedirectTarget() */ public static function provideMakeRedirectContent() { - // @codingStandardsIgnoreStart Generic.Files.LineLength + // phpcs:disable Generic.Files.LineLength return [ [ 'MediaWiki:MonoBook.css', @@ -36,6 +36,6 @@ class CssContentHandlerTest extends MediaWikiLangTestCase { "/* #REDIRECT */@import url(//example.org/w/index.php?title=Gadget:FooBaz.css&action=raw&ctype=text/css);" ], ]; - // @codingStandardsIgnoreEnd + // phpcs:enable } } diff --git a/www/wiki/tests/phpunit/includes/content/CssContentTest.php b/www/wiki/tests/phpunit/includes/content/CssContentTest.php index d2078d7a..f5cc05e0 100644 --- a/www/wiki/tests/phpunit/includes/content/CssContentTest.php +++ b/www/wiki/tests/phpunit/includes/content/CssContentTest.php @@ -83,6 +83,7 @@ class CssContentTest extends JavaScriptContentTest { } /** + * @covers CssContent::getRedirectTarget * @dataProvider provideGetRedirectTarget */ public function testGetRedirectTarget( $title, $text ) { @@ -100,7 +101,7 @@ class CssContentTest extends JavaScriptContentTest { * Keep this in sync with CssContentHandlerTest::provideMakeRedirectContent() */ public static function provideGetRedirectTarget() { - // @codingStandardsIgnoreStart Generic.Files.LineLength + // phpcs:disable Generic.Files.LineLength return [ [ 'MediaWiki:MonoBook.css', "/* #REDIRECT */@import url(//example.org/w/index.php?title=MediaWiki:MonoBook.css&action=raw&ctype=text/css);" ], [ 'User:FooBar/common.css', "/* #REDIRECT */@import url(//example.org/w/index.php?title=User:FooBar/common.css&action=raw&ctype=text/css);" ], @@ -110,7 +111,7 @@ class CssContentTest extends JavaScriptContentTest { # Wrong domain [ null, "/* #REDIRECT */@import url(//example.com/w/index.php?title=Gadget:FooBaz.css&action=raw&ctype=text/css);" ], ]; - // @codingStandardsIgnoreEnd + // phpcs:enable } public static function dataEquals() { diff --git a/www/wiki/tests/phpunit/includes/content/FileContentHandlerTest.php b/www/wiki/tests/phpunit/includes/content/FileContentHandlerTest.php index 65efcc9e..9149fc4f 100644 --- a/www/wiki/tests/phpunit/includes/content/FileContentHandlerTest.php +++ b/www/wiki/tests/phpunit/includes/content/FileContentHandlerTest.php @@ -2,6 +2,8 @@ /** * @group ContentHandler + * + * @covers FileContentHandler */ class FileContentHandlerTest extends MediaWikiLangTestCase { /** @@ -16,13 +18,13 @@ class FileContentHandlerTest extends MediaWikiLangTestCase { } public function testIndexMapping() { - $mockEngine = $this->createMock( 'SearchEngine' ); + $mockEngine = $this->createMock( SearchEngine::class ); $mockEngine->expects( $this->atLeastOnce() ) ->method( 'makeSearchFieldMapping' ) ->willReturnCallback( function ( $name, $type ) { $mockField = - $this->getMockBuilder( 'SearchIndexFieldDefinition' ) + $this->getMockBuilder( SearchIndexFieldDefinition::class ) ->setMethods( [ 'getMapping' ] ) ->setConstructorArgs( [ $name, $type ] ) ->getMock(); @@ -41,7 +43,7 @@ class FileContentHandlerTest extends MediaWikiLangTestCase { 'file_text' => 1, ]; foreach ( $map as $name => $field ) { - $this->assertInstanceOf( 'SearchIndexField', $field ); + $this->assertInstanceOf( SearchIndexField::class, $field ); $this->assertEquals( $name, $field->getName() ); unset( $expect[$name] ); } diff --git a/www/wiki/tests/phpunit/includes/content/JavaScriptContentHandlerTest.php b/www/wiki/tests/phpunit/includes/content/JavaScriptContentHandlerTest.php index d2296239..b5e3ab4a 100644 --- a/www/wiki/tests/phpunit/includes/content/JavaScriptContentHandlerTest.php +++ b/www/wiki/tests/phpunit/includes/content/JavaScriptContentHandlerTest.php @@ -13,7 +13,7 @@ class JavaScriptContentHandlerTest extends MediaWikiLangTestCase { ] ); $ch = new JavaScriptContentHandler(); $content = $ch->makeRedirectContent( Title::newFromText( $title ) ); - $this->assertInstanceOf( 'JavaScriptContent', $content ); + $this->assertInstanceOf( JavaScriptContent::class, $content ); $this->assertEquals( $expected, $content->serialize( CONTENT_FORMAT_JAVASCRIPT ) ); } @@ -21,7 +21,7 @@ class JavaScriptContentHandlerTest extends MediaWikiLangTestCase { * Keep this in sync with JavaScriptContentTest::provideGetRedirectTarget() */ public static function provideMakeRedirectContent() { - // @codingStandardsIgnoreStart Generic.Files.LineLength + // phpcs:disable Generic.Files.LineLength return [ [ 'MediaWiki:MonoBook.js', @@ -36,6 +36,6 @@ class JavaScriptContentHandlerTest extends MediaWikiLangTestCase { '/* #REDIRECT */mw.loader.load("//example.org/w/index.php?title=Gadget:FooBaz.js\u0026action=raw\u0026ctype=text/javascript");' ], ]; - // @codingStandardsIgnoreEnd + // phpcs:enable } } diff --git a/www/wiki/tests/phpunit/includes/content/JavaScriptContentTest.php b/www/wiki/tests/phpunit/includes/content/JavaScriptContentTest.php index 1c746bcd..823be6f7 100644 --- a/www/wiki/tests/phpunit/includes/content/JavaScriptContentTest.php +++ b/www/wiki/tests/phpunit/includes/content/JavaScriptContentTest.php @@ -155,16 +155,6 @@ class JavaScriptContentTest extends TextContentTest { ], [ 'Foo', null, - 'comma', - false - ], - [ 'Foo, bar', - null, - 'comma', - false - ], - [ 'Foo', - null, 'link', false ], @@ -190,11 +180,6 @@ class JavaScriptContentTest extends TextContentTest { ], [ '#REDIRECT [[bar]]', true, - 'comma', - false - ], - [ '#REDIRECT [[bar]]', - true, 'link', false ], @@ -251,19 +236,18 @@ class JavaScriptContentTest extends TextContentTest { } public static function provideUpdateRedirect() { + // phpcs:disable Generic.Files.LineLength return [ [ '#REDIRECT [[Someplace]]', '#REDIRECT [[Someplace]]', ], - - // @codingStandardsIgnoreStart Generic.Files.LineLength [ '/* #REDIRECT */mw.loader.load("//example.org/w/index.php?title=MediaWiki:MonoBook.js\u0026action=raw\u0026ctype=text/javascript");', '/* #REDIRECT */mw.loader.load("//example.org/w/index.php?title=TestUpdateRedirect_target\u0026action=raw\u0026ctype=text/javascript");' ] - // @codingStandardsIgnoreEnd ]; + // phpcs:enable } /** @@ -294,6 +278,7 @@ class JavaScriptContentTest extends TextContentTest { } /** + * @covers JavaScriptContent::getRedirectTarget * @dataProvider provideGetRedirectTarget */ public function testGetRedirectTarget( $title, $text ) { @@ -312,7 +297,7 @@ class JavaScriptContentTest extends TextContentTest { * Keep this in sync with JavaScriptContentHandlerTest::provideMakeRedirectContent() */ public static function provideGetRedirectTarget() { - // @codingStandardsIgnoreStart Generic.Files.LineLength + // phpcs:disable Generic.Files.LineLength return [ [ 'MediaWiki:MonoBook.js', @@ -337,6 +322,6 @@ class JavaScriptContentTest extends TextContentTest { '/* #REDIRECT */mw.loader.load("//example.com/w/index.php?title=MediaWiki:OtherWiki.js\u0026action=raw\u0026ctype=text/javascript");' ], ]; - // @codingStandardsIgnoreEnd + // phpcs:enable } } diff --git a/www/wiki/tests/phpunit/includes/content/JsonContentTest.php b/www/wiki/tests/phpunit/includes/content/JsonContentTest.php index de8e371e..7cddbad2 100644 --- a/www/wiki/tests/phpunit/includes/content/JsonContentTest.php +++ b/www/wiki/tests/phpunit/includes/content/JsonContentTest.php @@ -82,18 +82,18 @@ class JsonContentTest extends MediaWikiLangTestCase { } private function getMockTitle() { - return $this->getMockBuilder( 'Title' ) + return $this->getMockBuilder( Title::class ) ->disableOriginalConstructor() ->getMock(); } private function getMockUser() { - return $this->getMockBuilder( 'User' ) + return $this->getMockBuilder( User::class ) ->disableOriginalConstructor() ->getMock(); } private function getMockParserOptions() { - return $this->getMockBuilder( 'ParserOptions' ) + return $this->getMockBuilder( ParserOptions::class ) ->disableOriginalConstructor() ->getMock(); } @@ -146,7 +146,7 @@ class JsonContentTest extends MediaWikiLangTestCase { public function testFillParserOutput( $data, $expected ) { $obj = new JsonContent( FormatJson::encode( $data ) ); $parserOutput = $obj->getParserOutput( $this->getMockTitle(), null, null, true ); - $this->assertInstanceOf( 'ParserOutput', $parserOutput ); + $this->assertInstanceOf( ParserOutput::class, $parserOutput ); $this->assertEquals( $expected, $parserOutput->getText() ); } } diff --git a/www/wiki/tests/phpunit/includes/content/TextContentHandlerTest.php b/www/wiki/tests/phpunit/includes/content/TextContentHandlerTest.php index 7d9f74ec..6d0a3d5c 100644 --- a/www/wiki/tests/phpunit/includes/content/TextContentHandlerTest.php +++ b/www/wiki/tests/phpunit/includes/content/TextContentHandlerTest.php @@ -4,6 +4,9 @@ * @group ContentHandler */ class TextContentHandlerTest extends MediaWikiLangTestCase { + /** + * @covers TextContentHandler::supportsDirectEditing + */ public function testSupportsDirectEditing() { $handler = new TextContentHandler(); $this->assertTrue( $handler->supportsDirectEditing(), 'direct editing is supported' ); @@ -16,13 +19,13 @@ class TextContentHandlerTest extends MediaWikiLangTestCase { public function testFieldsForIndex() { $handler = new TextContentHandler(); - $mockEngine = $this->createMock( 'SearchEngine' ); + $mockEngine = $this->createMock( SearchEngine::class ); $mockEngine->expects( $this->atLeastOnce() ) ->method( 'makeSearchFieldMapping' ) ->willReturnCallback( function ( $name, $type ) { $mockField = - $this->getMockBuilder( 'SearchIndexFieldDefinition' ) + $this->getMockBuilder( SearchIndexFieldDefinition::class ) ->setConstructorArgs( [ $name, $type ] ) ->getMock(); $mockField->expects( $this->atLeastOnce() )->method( 'getMapping' )->willReturn( [ @@ -39,7 +42,7 @@ class TextContentHandlerTest extends MediaWikiLangTestCase { $fields = $handler->getFieldsForSearchIndex( $mockEngine ); $mappedFields = []; foreach ( $fields as $name => $field ) { - $this->assertInstanceOf( 'SearchIndexField', $field ); + $this->assertInstanceOf( SearchIndexField::class, $field ); /** * @var $field SearchIndexField */ diff --git a/www/wiki/tests/phpunit/includes/content/TextContentTest.php b/www/wiki/tests/phpunit/includes/content/TextContentTest.php index b290f8f2..406bc96b 100644 --- a/www/wiki/tests/phpunit/includes/content/TextContentTest.php +++ b/www/wiki/tests/phpunit/includes/content/TextContentTest.php @@ -197,22 +197,11 @@ class TextContentTest extends MediaWikiLangTestCase { 'any', true ], - [ 'Foo', - null, - 'comma', - false - ], - [ 'Foo, bar', - null, - 'comma', - false - ], ]; } /** * @dataProvider dataIsCountable - * @group Database * @covers TextContent::isCountable */ public function testIsCountable( $text, $hasLinks, $mode, $expected ) { @@ -455,7 +444,7 @@ class TextContentTest extends MediaWikiLangTestCase { if ( $expectedNative === false ) { $this->assertFalse( $converted, "conversion to $model was expected to fail!" ); } else { - $this->assertInstanceOf( 'Content', $converted ); + $this->assertInstanceOf( Content::class, $converted ); $this->assertEquals( $expectedNative, $converted->getNativeData() ); } } diff --git a/www/wiki/tests/phpunit/includes/content/WikitextContentHandlerTest.php b/www/wiki/tests/phpunit/includes/content/WikitextContentHandlerTest.php index 290b11ab..59984d85 100644 --- a/www/wiki/tests/phpunit/includes/content/WikitextContentHandlerTest.php +++ b/www/wiki/tests/phpunit/includes/content/WikitextContentHandlerTest.php @@ -115,6 +115,9 @@ class WikitextContentHandlerTest extends MediaWikiLangTestCase { $this->assertEquals( $supported, $this->handler->isSupportedFormat( $format ) ); } + /** + * @covers WikitextContentHandler::supportsDirectEditing + */ public function testSupportsDirectEditing() { $handler = new WikiTextContentHandler(); $this->assertTrue( $handler->supportsDirectEditing(), 'direct editing is supported' ); @@ -186,6 +189,13 @@ class WikitextContentHandlerTest extends MediaWikiLangTestCase { ], [ + null, + '', + EDIT_NEW, + '/^Created blank page$/' + ], + + [ 'Hello there, world!', '', 0, @@ -227,25 +237,109 @@ class WikitextContentHandlerTest extends MediaWikiLangTestCase { ); } - /** - * @todo Text case requires database, should be done by a test class in the Database group - */ - /* - public function testGetAutoDeleteReason( Title $title, &$hasHistory ) {} - */ + public static function dataGetChangeTag() { + return [ + [ + null, + '#REDIRECT [[Foo]]', + 0, + 'mw-new-redirect' + ], + + [ + 'Lorem ipsum dolor', + '#REDIRECT [[Foo]]', + 0, + 'mw-new-redirect' + ], + + [ + '#REDIRECT [[Foo]]', + 'Lorem ipsum dolor', + 0, + 'mw-removed-redirect' + ], + + [ + '#REDIRECT [[Foo]]', + '#REDIRECT [[Bar]]', + 0, + 'mw-changed-redirect-target' + ], + + [ + null, + 'Lorem ipsum dolor', + EDIT_NEW, + null // mw-newpage is not defined as a tag + ], + + [ + null, + '', + EDIT_NEW, + null // mw-newblank is not defined as a tag + ], + + [ + 'Lorem ipsum dolor', + '', + 0, + 'mw-blank' + ], + + [ + '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.', + 'Ipsum', + 0, + 'mw-replace' + ], + + [ + '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.', + 'Duis purus odio, rhoncus et finibus dapibus, facilisis ac urna. Pellentesque + arcu, tristique nec tempus nec, suscipit vel arcu. Sed non dolor nec ligula + congue tempor. Quisque pellentesque finibus orci a molestie. Nam maximus, purus + euismod finibus mollis, dui ante malesuada felis, dignissim rutrum diam sapien.', + 0, + null + ], + ]; + } /** - * @todo Text case requires database, should be done by a test class in the Database group + * @dataProvider dataGetChangeTag + * @covers WikitextContentHandler::getChangeTag */ - /* - public function testGetUndoContent( Revision $current, Revision $undo, - Revision $undoafter = null - ) { + public function testGetChangeTag( $old, $new, $flags, $expected ) { + $this->setMwGlobals( 'wgSoftwareTags', [ + 'mw-new-redirect' => true, + 'mw-removed-redirect' => true, + 'mw-changed-redirect-target' => true, + 'mw-newpage' => true, + 'mw-newblank' => true, + 'mw-blank' => true, + 'mw-replace' => true, + ] ); + $oldContent = is_null( $old ) ? null : new WikitextContent( $old ); + $newContent = is_null( $new ) ? null : new WikitextContent( $new ); + + $tag = $this->handler->getChangeTag( $oldContent, $newContent, $flags ); + + $this->assertSame( $expected, $tag ); } - */ + /** + * @covers WikitextContentHandler::getDataForSearchIndex + */ public function testDataIndexFieldsFile() { - $mockEngine = $this->createMock( 'SearchEngine' ); + $mockEngine = $this->createMock( SearchEngine::class ); $title = Title::newFromText( 'Somefile.jpg', NS_FILE ); $page = new WikiPage( $title ); diff --git a/www/wiki/tests/phpunit/includes/content/WikitextContentTest.php b/www/wiki/tests/phpunit/includes/content/WikitextContentTest.php index d0996e3c..1db6aab6 100644 --- a/www/wiki/tests/phpunit/includes/content/WikitextContentTest.php +++ b/www/wiki/tests/phpunit/includes/content/WikitextContentTest.php @@ -40,7 +40,7 @@ more stuff [ "WikitextContentTest_testGetSecondaryDataUpdates_1", CONTENT_MODEL_WIKITEXT, "hello ''world''\n", [ - 'LinksUpdate' => [ + LinksUpdate::class => [ 'mRecursive' => true, 'mLinks' => [] ] @@ -49,7 +49,7 @@ more stuff [ "WikitextContentTest_testGetSecondaryDataUpdates_2", CONTENT_MODEL_WIKITEXT, "hello [[world test 21344]]\n", [ - 'LinksUpdate' => [ + LinksUpdate::class => [ 'mRecursive' => true, 'mLinks' => [ [ 'World_test_21344' => 0 ] @@ -268,16 +268,6 @@ just a test" ], [ 'Foo', null, - 'comma', - false - ], - [ 'Foo, bar', - null, - 'comma', - true - ], - [ 'Foo', - null, 'link', false ], @@ -303,11 +293,6 @@ just a test" ], [ '#REDIRECT [[bar]]', true, - 'comma', - false - ], - [ '#REDIRECT [[bar]]', - true, 'link', false ], @@ -446,11 +431,11 @@ just a test" return [ [ "WikitextContentTest_testGetSecondaryDataUpdates_1", CONTENT_MODEL_WIKITEXT, "hello ''world''\n", - [ 'LinksDeletionUpdate' => [] ] + [ LinksDeletionUpdate::class => [] ] ], [ "WikitextContentTest_testGetSecondaryDataUpdates_2", CONTENT_MODEL_WIKITEXT, "hello [[world test 21344]]\n", - [ 'LinksDeletionUpdate' => [] ] + [ LinksDeletionUpdate::class => [] ] ], // @todo more...? ]; diff --git a/www/wiki/tests/phpunit/includes/content/WikitextStructureTest.php b/www/wiki/tests/phpunit/includes/content/WikitextStructureTest.php index f1b54f6a..88f4d8f7 100644 --- a/www/wiki/tests/phpunit/includes/content/WikitextStructureTest.php +++ b/www/wiki/tests/phpunit/includes/content/WikitextStructureTest.php @@ -1,5 +1,8 @@ <?php +/** + * @covers WikiTextStructure + */ class WikitextStructureTest extends MediaWikiLangTestCase { private function getMockTitle() { @@ -101,7 +104,7 @@ END; $this->assertEquals( "Opening text is opening.", $struct->getOpeningText() ); $this->assertEquals( "Opening text is opening. Then we got more text", $struct->getMainText() ); - $this->assertEquals( [ "Header table row in table another row in table" ], + $this->assertEquals( [ "Header table row in table another row in table" ], $struct->getAuxiliaryText() ); } } diff --git a/www/wiki/tests/phpunit/includes/db/DatabaseMysqlBaseTest.php b/www/wiki/tests/phpunit/includes/db/DatabaseMysqlBaseTest.php deleted file mode 100644 index bb747c7c..00000000 --- a/www/wiki/tests/phpunit/includes/db/DatabaseMysqlBaseTest.php +++ /dev/null @@ -1,364 +0,0 @@ -<?php -/** - * Holds tests for DatabaseMysqlBase MediaWiki class. - * - * 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 - * @author Antoine Musso - * @author Bryan Davis - * @copyright © 2013 Antoine Musso - * @copyright © 2013 Bryan Davis - * @copyright © 2013 Wikimedia Foundation Inc. - */ - -/** - * Fake class around abstract class so we can call concrete methods. - */ -class FakeDatabaseMysqlBase extends DatabaseMysqlBase { - // From DatabaseBase - function __construct() { - } - - protected function closeConnection() { - } - - protected function doQuery( $sql ) { - } - - // From DatabaseMysql - protected function mysqlConnect( $realServer ) { - } - - protected function mysqlSetCharset( $charset ) { - } - - protected function mysqlFreeResult( $res ) { - } - - protected function mysqlFetchObject( $res ) { - } - - protected function mysqlFetchArray( $res ) { - } - - protected function mysqlNumRows( $res ) { - } - - protected function mysqlNumFields( $res ) { - } - - protected function mysqlFieldName( $res, $n ) { - } - - protected function mysqlFieldType( $res, $n ) { - } - - protected function mysqlDataSeek( $res, $row ) { - } - - protected function mysqlError( $conn = null ) { - } - - protected function mysqlFetchField( $res, $n ) { - } - - protected function mysqlPing() { - } - - protected function mysqlRealEscapeString( $s ) { - - } - - // From interface DatabaseType - function insertId() { - } - - function lastErrno() { - } - - function affectedRows() { - } - - function getServerVersion() { - } -} - -class DatabaseMysqlBaseTest extends MediaWikiTestCase { - /** - * @dataProvider provideDiapers - * @covers DatabaseMysqlBase::addIdentifierQuotes - */ - public function testAddIdentifierQuotes( $expected, $in ) { - $db = new FakeDatabaseMysqlBase(); - $quoted = $db->addIdentifierQuotes( $in ); - $this->assertEquals( $expected, $quoted ); - } - - /** - * Feeds testAddIdentifierQuotes - * - * Named per bug 20281 convention. - */ - function provideDiapers() { - return [ - // Format: expected, input - [ '``', '' ], - - // Yeah I really hate loosely typed PHP idiocies nowadays - [ '``', null ], - - // Dear codereviewer, guess what addIdentifierQuotes() - // will return with thoses: - [ '``', false ], - [ '`1`', true ], - - // We never know what could happen - [ '`0`', 0 ], - [ '`1`', 1 ], - - // Whatchout! Should probably use something more meaningful - [ "`'`", "'" ], # single quote - [ '`"`', '"' ], # double quote - [ '````', '`' ], # backtick - [ '`’`', '’' ], # apostrophe (look at your encyclopedia) - - // sneaky NUL bytes are lurking everywhere - [ '``', "\0" ], - [ '`xyzzy`', "\0x\0y\0z\0z\0y\0" ], - - // unicode chars - [ - self::createUnicodeString( '`\u0001a\uFFFFb`' ), - self::createUnicodeString( '\u0001a\uFFFFb' ) - ], - [ - self::createUnicodeString( '`\u0001\uFFFF`' ), - self::createUnicodeString( '\u0001\u0000\uFFFF\u0000' ) - ], - [ '`☃`', '☃' ], - [ '`メインページ`', 'メインページ' ], - [ '`Басты_бет`', 'Басты_бет' ], - - // Real world: - [ '`Alix`', 'Alix' ], # while( ! $recovered ) { sleep(); } - [ '`Backtick: ```', 'Backtick: `' ], - [ '`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( [ '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( - [ 'Tables_in_' => 'view1' ], - [ 'Tables_in_' => 'view2' ], - [ '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( [ 'view1', 'view2', 'myview' ], - $db->listViews() ); - $this->assertEquals( [ 'view1', 'view2', 'myview' ], - $db->listViews() ); - - // Prefix filtering - $this->assertEquals( [ 'view1', 'view2' ], - $db->listViews( 'view' ) ); - $this->assertEquals( [ 'myview' ], - $db->listViews( 'my' ) ); - $this->assertEquals( [], - $db->listViews( 'UNUSED_PREFIX' ) ); - $this->assertEquals( [ '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 [ - // format: whether it is a view, view name - [ true, 'view1' ], - [ true, 'view2' ], - [ true, 'myview' ], - - [ false, 'user' ], - - [ false, 'view10' ], - [ false, 'my' ], - [ false, 'OH_MY_GOD' ], # they killed kenny! - ]; - } - - /** - * @dataProvider provideComparePositions - */ - function testHasReached( MySQLMasterPos $lowerPos, MySQLMasterPos $higherPos ) { - $this->assertTrue( $higherPos->hasReached( $lowerPos ) ); - $this->assertTrue( $higherPos->hasReached( $higherPos ) ); - $this->assertTrue( $lowerPos->hasReached( $lowerPos ) ); - $this->assertFalse( $lowerPos->hasReached( $higherPos ) ); - } - - function provideComparePositions() { - return [ - [ - new MySQLMasterPos( 'db1034-bin.000976', '843431247' ), - new MySQLMasterPos( 'db1034-bin.000976', '843431248' ) - ], - [ - new MySQLMasterPos( 'db1034-bin.000976', '999' ), - new MySQLMasterPos( 'db1034-bin.000976', '1000' ) - ], - [ - new MySQLMasterPos( 'db1034-bin.000976', '999' ), - new MySQLMasterPos( 'db1035-bin.000976', '1000' ) - ], - ]; - } - - /** - * @dataProvider provideChannelPositions - */ - function testChannelsMatch( MySQLMasterPos $pos1, MySQLMasterPos $pos2, $matches ) { - $this->assertEquals( $matches, $pos1->channelsMatch( $pos2 ) ); - $this->assertEquals( $matches, $pos2->channelsMatch( $pos1 ) ); - } - - function provideChannelPositions() { - return [ - [ - new MySQLMasterPos( 'db1034-bin.000876', '44' ), - new MySQLMasterPos( 'db1034-bin.000976', '74' ), - true - ], - [ - new MySQLMasterPos( 'db1052-bin.000976', '999' ), - new MySQLMasterPos( 'db1052-bin.000976', '1000' ), - true - ], - [ - new MySQLMasterPos( 'db1066-bin.000976', '9999' ), - new MySQLMasterPos( 'db1035-bin.000976', '10000' ), - false - ], - [ - new MySQLMasterPos( 'db1066-bin.000976', '9999' ), - new MySQLMasterPos( 'trump2016.000976', '10000' ), - false - ], - ]; - } - - /** - * @dataProvider provideLagAmounts - */ - function testPtHeartbeat( $lag ) { - $db = $this->getMockBuilder( 'DatabaseMysql' ) - ->disableOriginalConstructor() - ->setMethods( [ - 'getLagDetectionMethod', 'getHeartbeatData', 'getMasterServerInfo' ] ) - ->getMock(); - - $db->expects( $this->any() ) - ->method( 'getLagDetectionMethod' ) - ->will( $this->returnValue( 'pt-heartbeat' ) ); - - $db->expects( $this->any() ) - ->method( 'getMasterServerInfo' ) - ->will( $this->returnValue( [ 'serverId' => 172, 'asOf' => time() ] ) ); - - // Fake the current time. - list( $nowSecFrac, $nowSec ) = explode( ' ', microtime() ); - $now = (float)$nowSec + (float)$nowSecFrac; - // Fake the heartbeat time. - // Work arounds for weak DataTime microseconds support. - $ptTime = $now - $lag; - $ptSec = (int)$ptTime; - $ptSecFrac = ( $ptTime - $ptSec ); - $ptDateTime = new DateTime( "@$ptSec" ); - $ptTimeISO = $ptDateTime->format( 'Y-m-d\TH:i:s' ); - $ptTimeISO .= ltrim( number_format( $ptSecFrac, 6 ), '0' ); - - $db->expects( $this->any() ) - ->method( 'getHeartbeatData' ) - ->with( [ 'server_id' => 172 ] ) - ->will( $this->returnValue( [ $ptTimeISO, $now ] ) ); - - $db->setLBInfo( 'clusterMasterHost', 'db1052' ); - $lagEst = $db->getLag(); - - $this->assertGreaterThan( $lag - .010, $lagEst, "Correct heatbeat lag" ); - $this->assertLessThan( $lag + .010, $lagEst, "Correct heatbeat lag" ); - } - - function provideLagAmounts() { - return [ - [ 0 ], - [ 0.3 ], - [ 6.5 ], - [ 10.1 ], - [ 200.2 ], - [ 400.7 ], - [ 600.22 ], - [ 1000.77 ], - ]; - } -} diff --git a/www/wiki/tests/phpunit/includes/db/DatabaseOracleTest.php b/www/wiki/tests/phpunit/includes/db/DatabaseOracleTest.php new file mode 100644 index 00000000..061e121a --- /dev/null +++ b/www/wiki/tests/phpunit/includes/db/DatabaseOracleTest.php @@ -0,0 +1,52 @@ +<?php + +class DatabaseOracleTest extends PHPUnit\Framework\TestCase { + + use MediaWikiCoversValidator; + use PHPUnit4And6Compat; + + /** + * @return PHPUnit_Framework_MockObject_MockObject|DatabaseOracle + */ + private function getMockDb() { + return $this->getMockBuilder( DatabaseOracle::class ) + ->disableOriginalConstructor() + ->setMethods( null ) + ->getMock(); + } + + public function provideBuildSubstring() { + yield [ 'someField', 1, 2, 'SUBSTR(someField,1,2)' ]; + yield [ 'someField', 1, null, 'SUBSTR(someField,1)' ]; + } + + /** + * @covers DatabaseOracle::buildSubstring + * @dataProvider provideBuildSubstring + */ + public function testBuildSubstring( $input, $start, $length, $expected ) { + $mockDb = $this->getMockDb(); + $output = $mockDb->buildSubstring( $input, $start, $length ); + $this->assertSame( $expected, $output ); + } + + public function provideBuildSubstring_invalidParams() { + yield [ -1, 1 ]; + yield [ 1, -1 ]; + yield [ 1, 'foo' ]; + yield [ 'foo', 1 ]; + yield [ null, 1 ]; + yield [ 0, 1 ]; + } + + /** + * @covers DatabaseOracle::buildSubstring + * @dataProvider provideBuildSubstring_invalidParams + */ + public function testBuildSubstring_invalidParams( $start, $length ) { + $mockDb = $this->getMockDb(); + $this->setExpectedException( InvalidArgumentException::class ); + $mockDb->buildSubstring( 'foo', $start, $length ); + } + +} diff --git a/www/wiki/tests/phpunit/includes/db/DatabasePostgresTest.php b/www/wiki/tests/phpunit/includes/db/DatabasePostgresTest.php new file mode 100644 index 00000000..5c2aa2bb --- /dev/null +++ b/www/wiki/tests/phpunit/includes/db/DatabasePostgresTest.php @@ -0,0 +1,177 @@ +<?php + +use Wikimedia\Rdbms\IDatabase; +use Wikimedia\Rdbms\DatabasePostgres; +use Wikimedia\ScopedCallback; +use Wikimedia\TestingAccessWrapper; + +/** + * @group Database + */ +class DatabasePostgresTest extends MediaWikiTestCase { + + private function doTestInsertIgnore() { + $reset = new ScopedCallback( function () { + if ( $this->db->explicitTrxActive() ) { + $this->db->rollback( __METHOD__ ); + } + $this->db->query( 'DROP TABLE IF EXISTS ' . $this->db->tableName( 'foo' ) ); + } ); + + $this->db->query( + "CREATE TEMPORARY TABLE {$this->db->tableName( 'foo' )} (i INTEGER NOT NULL PRIMARY KEY)" + ); + $this->db->insert( 'foo', [ [ 'i' => 1 ], [ 'i' => 2 ] ], __METHOD__ ); + + // Normal INSERT IGNORE + $this->db->begin( __METHOD__ ); + $this->db->insert( + 'foo', [ [ 'i' => 3 ], [ 'i' => 2 ], [ 'i' => 5 ] ], __METHOD__, [ 'IGNORE' ] + ); + $this->assertSame( 2, $this->db->affectedRows() ); + $this->assertSame( + [ '1', '2', '3', '5' ], + $this->db->selectFieldValues( 'foo', 'i', [], __METHOD__, [ 'ORDER BY' => 'i' ] ) + ); + $this->db->rollback( __METHOD__ ); + + // INSERT IGNORE doesn't ignore stuff like NOT NULL violations + $this->db->begin( __METHOD__ ); + $this->db->startAtomic( __METHOD__, IDatabase::ATOMIC_CANCELABLE ); + try { + $this->db->insert( + 'foo', [ [ 'i' => 7 ], [ 'i' => null ] ], __METHOD__, [ 'IGNORE' ] + ); + $this->db->endAtomic( __METHOD__ ); + $this->fail( 'Expected exception not thrown' ); + } catch ( DBQueryError $e ) { + $this->assertSame( 0, $this->db->affectedRows() ); + $this->db->cancelAtomic( __METHOD__ ); + } + $this->assertSame( + [ '1', '2' ], + $this->db->selectFieldValues( 'foo', 'i', [], __METHOD__, [ 'ORDER BY' => 'i' ] ) + ); + $this->db->rollback( __METHOD__ ); + } + + /** + * @covers Wikimedia\Rdbms\DatabasePostgres::insert + */ + public function testInsertIgnoreOld() { + if ( !$this->db instanceof DatabasePostgres ) { + $this->markTestSkipped( 'Not PostgreSQL' ); + } + if ( $this->db->getServerVersion() < 9.5 ) { + $this->doTestInsertIgnore(); + } else { + // Hack version to make it take the old code path + $w = TestingAccessWrapper::newFromObject( $this->db ); + $oldVer = $w->numericVersion; + $w->numericVersion = 9.4; + try { + $this->doTestInsertIgnore(); + } finally { + $w->numericVersion = $oldVer; + } + } + } + + /** + * @covers Wikimedia\Rdbms\DatabasePostgres::insert + */ + public function testInsertIgnoreNew() { + if ( !$this->db instanceof DatabasePostgres ) { + $this->markTestSkipped( 'Not PostgreSQL' ); + } + if ( $this->db->getServerVersion() < 9.5 ) { + $this->markTestSkipped( 'PostgreSQL version is ' . $this->db->getServerVersion() ); + } + + $this->doTestInsertIgnore(); + } + + private function doTestInsertSelectIgnore() { + $reset = new ScopedCallback( function () { + if ( $this->db->explicitTrxActive() ) { + $this->db->rollback( __METHOD__ ); + } + $this->db->query( 'DROP TABLE IF EXISTS ' . $this->db->tableName( 'foo' ) ); + $this->db->query( 'DROP TABLE IF EXISTS ' . $this->db->tableName( 'bar' ) ); + } ); + + $this->db->query( + "CREATE TEMPORARY TABLE {$this->db->tableName( 'foo' )} (i INTEGER)" + ); + $this->db->query( + "CREATE TEMPORARY TABLE {$this->db->tableName( 'bar' )} (i INTEGER NOT NULL PRIMARY KEY)" + ); + $this->db->insert( 'bar', [ [ 'i' => 1 ], [ 'i' => 2 ] ], __METHOD__ ); + + // Normal INSERT IGNORE + $this->db->begin( __METHOD__ ); + $this->db->insert( 'foo', [ [ 'i' => 3 ], [ 'i' => 2 ], [ 'i' => 5 ] ], __METHOD__ ); + $this->db->insertSelect( 'bar', 'foo', [ 'i' => 'i' ], [], __METHOD__, [ 'IGNORE' ] ); + $this->assertSame( 2, $this->db->affectedRows() ); + $this->assertSame( + [ '1', '2', '3', '5' ], + $this->db->selectFieldValues( 'bar', 'i', [], __METHOD__, [ 'ORDER BY' => 'i' ] ) + ); + $this->db->rollback( __METHOD__ ); + + // INSERT IGNORE doesn't ignore stuff like NOT NULL violations + $this->db->begin( __METHOD__ ); + $this->db->insert( 'foo', [ [ 'i' => 7 ], [ 'i' => null ] ], __METHOD__ ); + $this->db->startAtomic( __METHOD__, IDatabase::ATOMIC_CANCELABLE ); + try { + $this->db->insertSelect( 'bar', 'foo', [ 'i' => 'i' ], [], __METHOD__, [ 'IGNORE' ] ); + $this->db->endAtomic( __METHOD__ ); + $this->fail( 'Expected exception not thrown' ); + } catch ( DBQueryError $e ) { + $this->assertSame( 0, $this->db->affectedRows() ); + $this->db->cancelAtomic( __METHOD__ ); + } + $this->assertSame( + [ '1', '2' ], + $this->db->selectFieldValues( 'bar', 'i', [], __METHOD__, [ 'ORDER BY' => 'i' ] ) + ); + $this->db->rollback( __METHOD__ ); + } + + /** + * @covers Wikimedia\Rdbms\DatabasePostgres::nativeInsertSelect + */ + public function testInsertSelectIgnoreOld() { + if ( !$this->db instanceof DatabasePostgres ) { + $this->markTestSkipped( 'Not PostgreSQL' ); + } + if ( $this->db->getServerVersion() < 9.5 ) { + $this->doTestInsertSelectIgnore(); + } else { + // Hack version to make it take the old code path + $w = TestingAccessWrapper::newFromObject( $this->db ); + $oldVer = $w->numericVersion; + $w->numericVersion = 9.4; + try { + $this->doTestInsertSelectIgnore(); + } finally { + $w->numericVersion = $oldVer; + } + } + } + + /** + * @covers Wikimedia\Rdbms\DatabasePostgres::nativeInsertSelect + */ + public function testInsertSelectIgnoreNew() { + if ( !$this->db instanceof DatabasePostgres ) { + $this->markTestSkipped( 'Not PostgreSQL' ); + } + if ( $this->db->getServerVersion() < 9.5 ) { + $this->markTestSkipped( 'PostgreSQL version is ' . $this->db->getServerVersion() ); + } + + $this->doTestInsertSelectIgnore(); + } + +} diff --git a/www/wiki/tests/phpunit/includes/db/DatabaseSQLTest.php b/www/wiki/tests/phpunit/includes/db/DatabaseSQLTest.php deleted file mode 100644 index 5d5521cc..00000000 --- a/www/wiki/tests/phpunit/includes/db/DatabaseSQLTest.php +++ /dev/null @@ -1,805 +0,0 @@ -<?php - -/** - * Test the abstract database layer - * This is a non DBMS depending test. - */ -class DatabaseSQLTest extends MediaWikiTestCase { - - /** - * @var DatabaseTestHelper - */ - private $database; - - protected function setUp() { - parent::setUp(); - $this->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'] : [], - __METHOD__, - isset( $sql['options'] ) ? $sql['options'] : [], - isset( $sql['join_conds'] ) ? $sql['join_conds'] : [] - ); - $this->assertLastSql( $sqlText ); - } - - public static function provideSelect() { - return [ - [ - [ - 'tables' => 'table', - 'fields' => [ 'field', 'alias' => 'field2' ], - 'conds' => [ 'alias' => 'text' ], - ], - "SELECT field,field2 AS alias " . - "FROM table " . - "WHERE alias = 'text'" - ], - [ - [ - 'tables' => 'table', - 'fields' => [ 'field', 'alias' => 'field2' ], - 'conds' => [ 'alias' => 'text' ], - 'options' => [ 'LIMIT' => 1, 'ORDER BY' => 'field' ], - ], - "SELECT field,field2 AS alias " . - "FROM table " . - "WHERE alias = 'text' " . - "ORDER BY field " . - "LIMIT 1" - ], - [ - [ - 'tables' => [ 'table', 't2' => 'table2' ], - 'fields' => [ 'tid', 'field', 'alias' => 'field2', 't2.id' ], - 'conds' => [ 'alias' => 'text' ], - 'options' => [ 'LIMIT' => 1, 'ORDER BY' => 'field' ], - 'join_conds' => [ 't2' => [ - '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" - ], - [ - [ - 'tables' => [ 'table', 't2' => 'table2' ], - 'fields' => [ 'tid', 'field', 'alias' => 'field2', 't2.id' ], - 'conds' => [ 'alias' => 'text' ], - 'options' => [ 'LIMIT' => 1, 'GROUP BY' => 'field', 'HAVING' => 'COUNT(*) > 1' ], - 'join_conds' => [ 't2' => [ - '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" - ], - [ - [ - 'tables' => [ 'table', 't2' => 'table2' ], - 'fields' => [ 'tid', 'field', 'alias' => 'field2', 't2.id' ], - 'conds' => [ 'alias' => 'text' ], - 'options' => [ - 'LIMIT' => 1, - 'GROUP BY' => [ 'field', 'field2' ], - 'HAVING' => [ 'COUNT(*) > 1', 'field' => 1 ] - ], - 'join_conds' => [ 't2' => [ - '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" - ], - [ - [ - 'tables' => [ 'table' ], - 'fields' => [ 'alias' => 'field' ], - 'conds' => [ 'alias' => [ 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'] : [] - ); - $this->assertLastSql( $sqlText ); - } - - public static function provideUpdate() { - return [ - [ - [ - 'table' => 'table', - 'values' => [ 'field' => 'text', 'field2' => 'text2' ], - 'conds' => [ 'alias' => 'text' ], - ], - "UPDATE table " . - "SET field = 'text'" . - ",field2 = 'text2' " . - "WHERE alias = 'text'" - ], - [ - [ - 'table' => 'table', - 'values' => [ 'field = other', 'field2' => 'text2' ], - 'conds' => [ 'id' => '1' ], - ], - "UPDATE table " . - "SET field = other" . - ",field2 = 'text2' " . - "WHERE id = '1'" - ], - [ - [ - 'table' => 'table', - 'values' => [ '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 [ - [ - [ - 'table' => 'table', - 'conds' => [ 'alias' => 'text' ], - ], - "DELETE FROM table " . - "WHERE alias = 'text'" - ], - [ - [ - '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 [ - [ - [ - 'table' => 'upsert_table', - 'rows' => [ 'field' => 'text', 'field2' => 'text2' ], - 'uniqueIndexes' => [ 'field' ], - 'set' => [ '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 [ - [ - [ - 'delTable' => 'table', - 'joinTable' => 'table_join', - 'delVar' => 'field', - 'joinVar' => 'field_join', - 'conds' => [ 'alias' => 'text' ], - ], - "DELETE FROM table " . - "WHERE field IN (" . - "SELECT field_join FROM table_join WHERE alias = 'text'" . - ")" - ], - [ - [ - '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'] : [] - ); - $this->assertLastSql( $sqlText ); - } - - public static function provideInsert() { - return [ - [ - [ - 'table' => 'table', - 'rows' => [ 'field' => 'text', 'field2' => 2 ], - ], - "INSERT INTO table " . - "(field,field2) " . - "VALUES ('text','2')" - ], - [ - [ - 'table' => 'table', - 'rows' => [ 'field' => 'text', 'field2' => 2 ], - 'options' => 'IGNORE', - ], - "INSERT IGNORE INTO table " . - "(field,field2) " . - "VALUES ('text','2')" - ], - [ - [ - 'table' => 'table', - 'rows' => [ - [ 'field' => 'text', 'field2' => 2 ], - [ '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'] : [], - isset( $sql['selectOptions'] ) ? $sql['selectOptions'] : [] - ); - $this->assertLastSql( $sqlText ); - } - - public static function provideInsertSelect() { - return [ - [ - [ - 'destTable' => 'insert_table', - 'srcTable' => 'select_table', - 'varMap' => [ 'field_insert' => 'field_select', 'field' => 'field2' ], - 'conds' => '*', - ], - "INSERT INTO insert_table " . - "(field_insert,field) " . - "SELECT field_select,field2 " . - "FROM select_table" - ], - [ - [ - 'destTable' => 'insert_table', - 'srcTable' => 'select_table', - 'varMap' => [ 'field_insert' => 'field_select', 'field' => 'field2' ], - 'conds' => [ 'field' => 2 ], - ], - "INSERT INTO insert_table " . - "(field_insert,field) " . - "SELECT field_select,field2 " . - "FROM select_table " . - "WHERE field = '2'" - ], - [ - [ - 'destTable' => 'insert_table', - 'srcTable' => 'select_table', - 'varMap' => [ 'field_insert' => 'field_select', 'field' => 'field2' ], - 'conds' => [ 'field' => 2 ], - 'insertOptions' => 'IGNORE', - 'selectOptions' => [ '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 [ - [ - [ - 'table' => 'replace_table', - 'uniqueIndexes' => [ 'field' ], - 'rows' => [ 'field' => 'text', 'field2' => 'text2' ], - ], - "DELETE FROM replace_table " . - "WHERE ( field='text' ); " . - "INSERT INTO replace_table " . - "(field,field2) " . - "VALUES ('text','text2')" - ], - [ - [ - 'table' => 'module_deps', - 'uniqueIndexes' => [ [ 'md_module', 'md_skin' ] ], - 'rows' => [ - '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')" - ], - [ - [ - 'table' => 'module_deps', - 'uniqueIndexes' => [ [ 'md_module', 'md_skin' ] ], - 'rows' => [ - [ - 'md_module' => 'module', - 'md_skin' => 'skin', - 'md_deps' => 'deps', - ], [ - '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')" - ], - [ - [ - 'table' => 'module_deps', - 'uniqueIndexes' => [ 'md_module', 'md_skin' ], - 'rows' => [ - [ - 'md_module' => 'module', - 'md_skin' => 'skin', - 'md_deps' => 'deps', - ], [ - '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')" - ], - [ - [ - 'table' => 'module_deps', - 'uniqueIndexes' => [], - 'rows' => [ - '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 [ - [ - [ - 'table' => 'replace_table', - 'rows' => [ '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 [ - [ - [ - 'conds' => [ 'field' => 'text' ], - 'true' => 1, - 'false' => 'NULL', - ], - "(CASE WHEN field = 'text' THEN 1 ELSE NULL END)" - ], - [ - [ - 'conds' => [ 'field' => 'text', 'field2' => 'anothertext' ], - 'true' => 1, - 'false' => 'NULL', - ], - "(CASE WHEN field = 'text' AND field2 = 'anothertext' THEN 1 ELSE NULL END)" - ], - [ - [ - '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 [ - [ - [ 'field', 'field2' ], - "CONCAT(field,field2)" - ], - [ - [ "'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 [ - [ - 'text', - "LIKE 'text'" - ], - [ - [ 'text', new LikeMatch( '%' ) ], - "LIKE 'text%'" - ], - [ - [ 'text', new LikeMatch( '%' ), 'text2' ], - "LIKE 'text%text2'" - ], - [ - [ '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 [ - [ - [ - 'sqls' => [ 'RAW SQL', 'RAW2SQL' ], - 'all' => true, - ], - "(RAW SQL) UNION ALL (RAW2SQL)" - ], - [ - [ - 'sqls' => [ 'RAW SQL', 'RAW2SQL' ], - 'all' => false, - ], - "(RAW SQL) UNION (RAW2SQL)" - ], - [ - [ - 'sqls' => [ '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( [ '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__ ) - ); - } - - /** - * @dataProvider provideMakeList - * @covers DatabaseBase::makeList - */ - public function testMakeList( $list, $mode, $sqlText ) { - $this->assertEquals( trim( $this->database->makeList( - $list, $mode - ) ), $sqlText ); - } - - public static function provideMakeList() { - return [ - [ - [ 'value', 'value2' ], - LIST_COMMA, - "'value','value2'" - ], - [ - [ 'field', 'field2' ], - LIST_NAMES, - "field,field2" - ], - [ - [ 'field' => 'value', 'field2' => 'value2' ], - LIST_AND, - "field = 'value' AND field2 = 'value2'" - ], - [ - [ 'field' => null, "field2 != 'value2'" ], - LIST_AND, - "field IS NULL AND (field2 != 'value2')" - ], - [ - [ 'field' => [ 'value', null, 'value2' ], 'field2' => 'value2' ], - LIST_AND, - "(field IN ('value','value2') OR field IS NULL) AND field2 = 'value2'" - ], - [ - [ 'field' => [ null ], 'field2' => null ], - LIST_AND, - "field IS NULL AND field2 IS NULL" - ], - [ - [ 'field' => 'value', 'field2' => 'value2' ], - LIST_OR, - "field = 'value' OR field2 = 'value2'" - ], - [ - [ 'field' => 'value', 'field2' => null ], - LIST_OR, - "field = 'value' OR field2 IS NULL" - ], - [ - [ 'field' => [ 'value', 'value2' ], 'field2' => [ 'value' ] ], - LIST_OR, - "field IN ('value','value2') OR field2 = 'value'" - ], - [ - [ 'field' => [ null, 'value', null, 'value2' ], "field2 != 'value2'" ], - LIST_OR, - "(field IN ('value','value2') OR field IS NULL) OR (field2 != 'value2')" - ], - [ - [ 'field' => 'value', 'field2' => 'value2' ], - LIST_SET, - "field = 'value',field2 = 'value2'" - ], - [ - [ 'field' => 'value', 'field2' => null ], - LIST_SET, - "field = 'value',field2 = NULL" - ], - [ - [ 'field' => 'value', "field2 != 'value2'" ], - LIST_SET, - "field = 'value',field2 != 'value2'" - ], - ]; - } -} diff --git a/www/wiki/tests/phpunit/includes/db/DatabaseSqliteTest.php b/www/wiki/tests/phpunit/includes/db/DatabaseSqliteTest.php index ae61070f..729b58c7 100644 --- a/www/wiki/tests/phpunit/includes/db/DatabaseSqliteTest.php +++ b/www/wiki/tests/phpunit/includes/db/DatabaseSqliteTest.php @@ -3,10 +3,9 @@ use Wikimedia\Rdbms\Blob; use Wikimedia\Rdbms\Database; use Wikimedia\Rdbms\DatabaseSqlite; +use Wikimedia\Rdbms\ResultWrapper; class DatabaseSqliteMock extends DatabaseSqlite { - private $lastQuery; - public static function newInstance( array $p = [] ) { $p['dbFilePath'] = ':memory:'; $p['schema'] = false; @@ -15,8 +14,6 @@ class DatabaseSqliteMock extends DatabaseSqlite { } function query( $sql, $fname = '', $tempIgnore = false ) { - $this->lastQuery = $sql; - return true; } @@ -285,6 +282,9 @@ class DatabaseSqliteTest extends MediaWikiTestCase { ); } + /** + * @coversNothing + */ public function testEntireSchema() { global $IP; @@ -298,6 +298,7 @@ class DatabaseSqliteTest extends MediaWikiTestCase { /** * Runs upgrades of older databases and compares results with current schema * @todo Currently only checks list of tables + * @coversNothing */ public function testUpgrades() { global $IP, $wgVersion, $wgProfiler; @@ -310,6 +311,11 @@ class DatabaseSqliteTest extends MediaWikiTestCase { '1.16', '1.17', '1.18', + '1.19', + '1.20', + '1.21', + '1.22', + '1.23', ]; // Mismatches for these columns we can safely ignore @@ -388,7 +394,7 @@ class DatabaseSqliteTest extends MediaWikiTestCase { $db = DatabaseSqlite::newStandaloneInstance( ':memory:' ); $databaseCreation = $db->query( 'CREATE TABLE a ( a_1 )', __METHOD__ ); - $this->assertInstanceOf( 'ResultWrapper', $databaseCreation, "Database creation" ); + $this->assertInstanceOf( ResultWrapper::class, $databaseCreation, "Database creation" ); $insertion = $db->insert( 'a', [ 'a_1' => 10 ], __METHOD__ ); $this->assertTrue( $insertion, "Insertion worked" ); @@ -481,7 +487,7 @@ class DatabaseSqliteTest extends MediaWikiTestCase { $db = DatabaseSqlite::newStandaloneInstance( ':memory:' ); $databaseCreation = $db->query( 'CREATE TABLE a ( a_1 )', __METHOD__ ); - $this->assertInstanceOf( 'ResultWrapper', $databaseCreation, "Failed to create table a" ); + $this->assertInstanceOf( ResultWrapper::class, $databaseCreation, "Failed to create table a" ); $res = $db->select( 'a', '*' ); $this->assertEquals( 0, $db->numFields( $res ), "expects to get 0 fields for an empty table" ); $insertion = $db->insert( 'a', [ 'a_1' => 10 ], __METHOD__ ); @@ -492,6 +498,9 @@ class DatabaseSqliteTest extends MediaWikiTestCase { $this->assertTrue( $db->close(), "closing database" ); } + /** + * @covers \Wikimedia\Rdbms\DatabaseSqlite::__toString + */ public function testToString() { $db = DatabaseSqlite::newStandaloneInstance( ':memory:' ); @@ -499,4 +508,12 @@ class DatabaseSqliteTest extends MediaWikiTestCase { $this->assertContains( 'SQLite ', $toString ); } + + /** + * @covers \Wikimedia\Rdbms\DatabaseSqlite::getAttributes() + */ + public function testsAttributes() { + $attributes = Database::attributesFromType( 'sqlite' ); + $this->assertTrue( $attributes[Database::ATTR_DB_LEVEL_LOCKING] ); + } } diff --git a/www/wiki/tests/phpunit/includes/db/DatabaseTest.php b/www/wiki/tests/phpunit/includes/db/DatabaseTest.php deleted file mode 100644 index 0730529b..00000000 --- a/www/wiki/tests/phpunit/includes/db/DatabaseTest.php +++ /dev/null @@ -1,263 +0,0 @@ -<?php - -/** - * @group Database - * @group DatabaseBase - */ -class DatabaseTest extends MediaWikiTestCase { - /** - * @var DatabaseBase - */ - protected $db; - - private $functionTest = false; - - protected function setUp() { - parent::setUp(); - $this->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 = [ $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', [] ); - $this->assertEquals( - "SELECT * FROM interwiki", - $sql ); - } - - public function testFillPreparedQuestion() { - $sql = $this->db->fillPrepared( - 'SELECT * FROM cur WHERE cur_namespace=? AND cur_title=?', - [ 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=?', - [ '"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\\?\\!'", - [ '"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(), [ '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', '*', [ 'page_id' => 1 ] ); - $this->assertFalse( $this->db->tableExists( 'foobarbaz' ) ); - $this->assertInternalType( 'int', $res->numRows() ); - } - - public function testTransactionIdle() { - $db = $this->db; - - $db->setFlag( DBO_TRX ); - $flagSet = null; - $db->onTransactionIdle( function() use ( $db, &$flagSet ) { - $flagSet = $db->getFlag( DBO_TRX ); - } ); - $this->assertFalse( $flagSet, 'DBO_TRX off in callback' ); - $this->assertTrue( $db->getFlag( DBO_TRX ), 'DBO_TRX restored to default' ); - - $db->clearFlag( DBO_TRX ); - $flagSet = null; - $db->onTransactionIdle( function() use ( $db, &$flagSet ) { - $flagSet = $db->getFlag( DBO_TRX ); - } ); - $this->assertFalse( $flagSet, 'DBO_TRX off in callback' ); - $this->assertFalse( $db->getFlag( DBO_TRX ), 'DBO_TRX restored to default' ); - - $db->clearFlag( DBO_TRX ); - $db->onTransactionIdle( function() use ( $db ) { - $db->setFlag( DBO_TRX ); - } ); - $this->assertFalse( $db->getFlag( DBO_TRX ), 'DBO_TRX restored to default' ); - } -} diff --git a/www/wiki/tests/phpunit/includes/db/DatabaseTestHelper.php b/www/wiki/tests/phpunit/includes/db/DatabaseTestHelper.php index d19d9981..e9fc34fa 100644 --- a/www/wiki/tests/phpunit/includes/db/DatabaseTestHelper.php +++ b/www/wiki/tests/phpunit/includes/db/DatabaseTestHelper.php @@ -2,6 +2,7 @@ use Wikimedia\Rdbms\TransactionProfiler; use Wikimedia\Rdbms\DatabaseDomain; +use Wikimedia\Rdbms\Database; /** * Helper for testing the methods from the Database class @@ -25,6 +26,11 @@ class DatabaseTestHelper extends Database { /** @var array List of row arrays */ protected $nextResult = []; + /** @var array|null */ + protected $nextError = null; + /** @var array|null */ + protected $lastError = null; + /** * Array of tables to be considered as existing by tableExist() * Use setExistingTables() to alter. @@ -47,7 +53,11 @@ class DatabaseTestHelper extends Database { $this->errorLogger = function ( Exception $e ) { wfWarn( get_class( $e ) . ": {$e->getMessage()}" ); }; + $this->deprecationLogger = function ( $msg ) { + wfWarn( $msg ); + }; $this->currentDomain = DatabaseDomain::newUnspecified(); + $this->open( 'localhost', 'testuser', 'password', 'testdb' ); } /** @@ -73,6 +83,16 @@ class DatabaseTestHelper extends Database { $this->nextResult = $res; } + /** + * @param int $errno Error number + * @param string $error Error text + * @param array $options + * - wasKnownStatementRollbackError: Return value for wasKnownStatementRollbackError() + */ + public function forceNextQueryError( $errno, $error, $options = [] ) { + $this->nextError = [ 'errno' => $errno, 'error' => $error ] + $options; + } + protected function addSql( $sql ) { // clean up spaces before and after some words and the whole string $this->lastSqls[] = trim( preg_replace( @@ -82,7 +102,17 @@ class DatabaseTestHelper extends Database { } protected function checkFunctionName( $fname ) { - if ( substr( $fname, 0, strlen( $this->testName ) ) !== $this->testName ) { + if ( $fname === 'Wikimedia\\Rdbms\\Database::close' ) { + return; // no $fname parameter + } + + // Handle some internal calls from the Database class + $check = $fname; + if ( preg_match( '/^Wikimedia\\\\Rdbms\\\\Database::query \((.+)\)$/', $fname, $m ) ) { + $check = $m[1]; + } + + if ( substr( $check, 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.' ); @@ -101,14 +131,13 @@ class DatabaseTestHelper extends Database { 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__ ) { $tableRaw = $this->tableName( $table, 'raw' ); - if ( isset( $this->mSessionTempTables[$tableRaw] ) ) { + if ( isset( $this->sessionTempTables[$tableRaw] ) ) { return true; // already known to exist } @@ -127,7 +156,9 @@ class DatabaseTestHelper extends Database { } function open( $server, $user, $password, $dbName ) { - return false; + $this->conn = (object)[ 'test' ]; + + return true; } function fetchObject( $res ) { @@ -159,11 +190,17 @@ class DatabaseTestHelper extends Database { } function lastErrno() { - return -1; + return $this->lastError ? $this->lastError['errno'] : -1; } function lastError() { - return 'test'; + return $this->lastError ? $this->lastError['error'] : 'test'; + } + + protected function wasKnownStatementRollbackError() { + return isset( $this->lastError['wasKnownStatementRollbackError'] ) + ? $this->lastError['wasKnownStatementRollbackError'] + : false; } function fieldInfo( $table, $field ) { @@ -174,7 +211,7 @@ class DatabaseTestHelper extends Database { return false; } - function affectedRows() { + function fetchAffectedRowCount() { return -1; } @@ -191,7 +228,7 @@ class DatabaseTestHelper extends Database { } function isOpen() { - return true; + return $this->conn ? true : false; } function ping( &$rtt = null ) { @@ -200,12 +237,22 @@ class DatabaseTestHelper extends Database { } protected function closeConnection() { - return false; + return true; } protected function doQuery( $sql ) { + $sql = preg_replace( '< /\* .+? \*/>', '', $sql ); + $this->addSql( $sql ); + + if ( $this->nextError ) { + $this->lastError = $this->nextError; + $this->nextError = null; + return false; + } + $res = $this->nextResult; $this->nextResult = []; + $this->lastError = null; return new FakeResultWrapper( $res ); } diff --git a/www/wiki/tests/phpunit/includes/db/LBFactoryTest.php b/www/wiki/tests/phpunit/includes/db/LBFactoryTest.php index 1efeeebd..ed4c977f 100644 --- a/www/wiki/tests/phpunit/includes/db/LBFactoryTest.php +++ b/www/wiki/tests/phpunit/includes/db/LBFactoryTest.php @@ -1,10 +1,4 @@ <?php - -use Wikimedia\Rdbms\LBFactorySimple; -use Wikimedia\Rdbms\LBFactoryMulti; -use Wikimedia\Rdbms\ChronologyProtector; -use Wikimedia\Rdbms\MySQLMasterPos; - /** * Holds tests for LBFactory abstract MediaWiki class. * @@ -23,19 +17,33 @@ use Wikimedia\Rdbms\MySQLMasterPos; * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * http://www.gnu.org/copyleft/gpl.html * - * @group Database * @file * @author Antoine Musso * @copyright © 2013 Antoine Musso * @copyright © 2013 Wikimedia Foundation Inc. */ + +use Wikimedia\Rdbms\LBFactorySimple; +use Wikimedia\Rdbms\LBFactoryMulti; +use Wikimedia\Rdbms\LoadBalancer; +use Wikimedia\Rdbms\ChronologyProtector; +use Wikimedia\Rdbms\DatabaseMysqli; +use Wikimedia\Rdbms\MySQLMasterPos; +use Wikimedia\Rdbms\DatabaseDomain; + +/** + * @group Database + * @covers \Wikimedia\Rdbms\LBFactorySimple + * @covers \Wikimedia\Rdbms\LBFactoryMulti + */ class LBFactoryTest extends MediaWikiTestCase { /** + * @covers MWLBFactory::getLBFactoryClass * @dataProvider getLBFactoryClassProvider */ public function testGetLBFactoryClass( $expected, $deprecated ) { - $mockDB = $this->getMockBuilder( 'DatabaseMysqli' ) + $mockDB = $this->getMockBuilder( DatabaseMysqli::class ) ->disableOriginalConstructor() ->getMock(); @@ -123,7 +131,7 @@ class LBFactoryTest extends MediaWikiTestCase { $factory = new LBFactorySimple( [ 'servers' => $servers, - 'loadMonitorClass' => 'LoadMonitorNull' + 'loadMonitorClass' => LoadMonitorNull::class ] ); $lb = $factory->getMainLB(); @@ -168,7 +176,7 @@ class LBFactoryTest extends MediaWikiTestCase { 'test-db1' => $wgDBserver, 'test-db2' => $wgDBserver ], - 'loadMonitorClass' => 'LoadMonitorNull' + 'loadMonitorClass' => LoadMonitorNull::class ] ); $lb = $factory->getMainLB(); @@ -182,35 +190,66 @@ class LBFactoryTest extends MediaWikiTestCase { $lb->closeAll(); } + /** + * @covers \Wikimedia\Rdbms\ChronologyProtector + */ public function testChronologyProtector() { + $now = microtime( true ); + // (a) First HTTP request - $mPos = new MySQLMasterPos( 'db1034-bin.000976', '843431247' ); + $m1Pos = new MySQLMasterPos( 'db1034-bin.000976/843431247', $now ); + $m2Pos = new MySQLMasterPos( 'db1064-bin.002400/794074907', $now ); - $now = microtime( true ); - $mockDB = $this->getMockBuilder( 'DatabaseMysqli' ) + // Master DB 1 + $mockDB1 = $this->getMockBuilder( DatabaseMysqli::class ) ->disableOriginalConstructor() ->getMock(); - $mockDB->method( 'writesOrCallbacksPending' )->willReturn( true ); - $mockDB->method( 'lastDoneWrites' )->willReturn( $now ); - $mockDB->method( 'getMasterPos' )->willReturn( $mPos ); - - $lb = $this->getMockBuilder( 'LoadBalancer' ) + $mockDB1->method( 'writesOrCallbacksPending' )->willReturn( true ); + $mockDB1->method( 'lastDoneWrites' )->willReturn( $now ); + $mockDB1->method( 'getMasterPos' )->willReturn( $m1Pos ); + // Load balancer for master DB 1 + $lb1 = $this->getMockBuilder( LoadBalancer::class ) ->disableOriginalConstructor() ->getMock(); - $lb->method( 'getConnection' )->willReturn( $mockDB ); - $lb->method( 'getServerCount' )->willReturn( 2 ); - $lb->method( 'parentInfo' )->willReturn( [ 'id' => "main-DEFAULT" ] ); - $lb->method( 'getAnyOpenConnection' )->willReturn( $mockDB ); - $lb->method( 'hasOrMadeRecentMasterChanges' )->will( $this->returnCallback( - function () use ( $mockDB ) { + $lb1->method( 'getConnection' )->willReturn( $mockDB1 ); + $lb1->method( 'getServerCount' )->willReturn( 2 ); + $lb1->method( 'getAnyOpenConnection' )->willReturn( $mockDB1 ); + $lb1->method( 'hasOrMadeRecentMasterChanges' )->will( $this->returnCallback( + function () use ( $mockDB1 ) { $p = 0; - $p |= call_user_func( [ $mockDB, 'writesOrCallbacksPending' ] ); - $p |= call_user_func( [ $mockDB, 'lastDoneWrites' ] ); + $p |= call_user_func( [ $mockDB1, 'writesOrCallbacksPending' ] ); + $p |= call_user_func( [ $mockDB1, 'lastDoneWrites' ] ); return (bool)$p; } ) ); - $lb->method( 'getMasterPos' )->willReturn( $mPos ); + $lb1->method( 'getMasterPos' )->willReturn( $m1Pos ); + $lb1->method( 'getServerName' )->with( 0 )->willReturn( 'master1' ); + // Master DB 2 + $mockDB2 = $this->getMockBuilder( DatabaseMysqli::class ) + ->disableOriginalConstructor() + ->getMock(); + $mockDB2->method( 'writesOrCallbacksPending' )->willReturn( true ); + $mockDB2->method( 'lastDoneWrites' )->willReturn( $now ); + $mockDB2->method( 'getMasterPos' )->willReturn( $m2Pos ); + // Load balancer for master DB 2 + $lb2 = $this->getMockBuilder( LoadBalancer::class ) + ->disableOriginalConstructor() + ->getMock(); + $lb2->method( 'getConnection' )->willReturn( $mockDB2 ); + $lb2->method( 'getServerCount' )->willReturn( 2 ); + $lb2->method( 'getAnyOpenConnection' )->willReturn( $mockDB2 ); + $lb2->method( 'hasOrMadeRecentMasterChanges' )->will( $this->returnCallback( + function () use ( $mockDB2 ) { + $p = 0; + $p |= call_user_func( [ $mockDB2, 'writesOrCallbacksPending' ] ); + $p |= call_user_func( [ $mockDB2, 'lastDoneWrites' ] ); + + return (bool)$p; + } + ) ); + $lb2->method( 'getMasterPos' )->willReturn( $m2Pos ); + $lb2->method( 'getServerName' )->with( 0 )->willReturn( 'master2' ); $bag = new HashBagOStuff(); $cp = new ChronologyProtector( @@ -221,36 +260,65 @@ class LBFactoryTest extends MediaWikiTestCase { ] ); - $mockDB->expects( $this->exactly( 2 ) )->method( 'writesOrCallbacksPending' ); - $mockDB->expects( $this->exactly( 2 ) )->method( 'lastDoneWrites' ); + $mockDB1->expects( $this->exactly( 1 ) )->method( 'writesOrCallbacksPending' ); + $mockDB1->expects( $this->exactly( 1 ) )->method( 'lastDoneWrites' ); + $mockDB2->expects( $this->exactly( 1 ) )->method( 'writesOrCallbacksPending' ); + $mockDB2->expects( $this->exactly( 1 ) )->method( 'lastDoneWrites' ); + + // Nothing to wait for on first HTTP request start + $cp->initLB( $lb1 ); + $cp->initLB( $lb2 ); + // Record positions in stash on first HTTP request end + $cp->shutdownLB( $lb1 ); + $cp->shutdownLB( $lb2 ); + $cpIndex = null; + $cp->shutdown( null, 'sync', $cpIndex ); - // Nothing to wait for - $cp->initLB( $lb ); - // Record in stash - $cp->shutdownLB( $lb ); - $cp->shutdown(); + $this->assertEquals( 1, $cpIndex, "CP write index set" ); // (b) Second HTTP request + + // Load balancer for master DB 1 + $lb1 = $this->getMockBuilder( LoadBalancer::class ) + ->disableOriginalConstructor() + ->getMock(); + $lb1->method( 'getServerCount' )->willReturn( 2 ); + $lb1->method( 'getServerName' )->with( 0 )->willReturn( 'master1' ); + $lb1->expects( $this->once() ) + ->method( 'waitFor' )->with( $this->equalTo( $m1Pos ) ); + // Load balancer for master DB 2 + $lb2 = $this->getMockBuilder( LoadBalancer::class ) + ->disableOriginalConstructor() + ->getMock(); + $lb2->method( 'getServerCount' )->willReturn( 2 ); + $lb2->method( 'getServerName' )->with( 0 )->willReturn( 'master2' ); + $lb2->expects( $this->once() ) + ->method( 'waitFor' )->with( $this->equalTo( $m2Pos ) ); + $cp = new ChronologyProtector( $bag, [ 'ip' => '127.0.0.1', 'agent' => "Totally-Not-FireFox" - ] + ], + $cpIndex ); - $lb->expects( $this->once() ) - ->method( 'waitFor' )->with( $this->equalTo( $mPos ) ); + // Wait for last positions to be reached on second HTTP request start + $cp->initLB( $lb1 ); + $cp->initLB( $lb2 ); + // Shutdown (nothing to record) + $cp->shutdownLB( $lb1 ); + $cp->shutdownLB( $lb2 ); + $cpIndex = null; + $cp->shutdown( null, 'sync', $cpIndex ); - // Wait - $cp->initLB( $lb ); - // Record in stash - $cp->shutdownLB( $lb ); - $cp->shutdown(); + $this->assertEquals( null, $cpIndex, "CP write index retained" ); } private function newLBFactoryMulti( array $baseOverride = [], array $serverOverride = [] ) { - global $wgDBserver, $wgDBuser, $wgDBpassword, $wgDBname, $wgDBtype, $wgSQLiteDataDir; + global $wgDBserver, $wgDBuser, $wgDBpassword, $wgDBname, $wgDBprefix, $wgDBtype; + global $wgSQLiteDataDir; return new LBFactoryMulti( $baseOverride + [ 'sectionsByDB' => [], @@ -261,6 +329,7 @@ class LBFactoryTest extends MediaWikiTestCase { ], 'serverTemplate' => $serverOverride + [ 'dbname' => $wgDBname, + 'tablePrefix' => $wgDBprefix, 'user' => $wgDBuser, 'password' => $wgDBpassword, 'type' => $wgDBtype, @@ -270,69 +339,74 @@ class LBFactoryTest extends MediaWikiTestCase { 'hostsByName' => [ 'test-db1' => $wgDBserver, ], - 'loadMonitorClass' => 'LoadMonitorNull', - 'localDomain' => wfWikiID() + 'loadMonitorClass' => LoadMonitorNull::class, + 'localDomain' => new DatabaseDomain( $wgDBname, null, $wgDBprefix ) ] ); } public function testNiceDomains() { - global $wgDBname, $wgDBtype; - - if ( $wgDBtype === 'sqlite' ) { - $tmpDir = $this->getNewTempDirectory(); - $dbPath = "$tmpDir/unit_test_db.sqlite"; - file_put_contents( $dbPath, '' ); - $tempFsFile = new TempFSFile( $dbPath ); - $tempFsFile->autocollect(); - } else { - $dbPath = null; + global $wgDBname; + + if ( wfGetDB( DB_MASTER )->databasesAreIndependent() ) { + self::markTestSkipped( "Skipping tests about selecting DBs: not applicable" ); + return; } $factory = $this->newLBFactoryMulti( [], - [ 'dbFilePath' => $dbPath ] + [] ); $lb = $factory->getMainLB(); - if ( $wgDBtype !== 'sqlite' ) { - $db = $lb->getConnectionRef( DB_MASTER ); - $this->assertEquals( - $wgDBname, - $db->getDomainID() - ); - unset( $db ); - } + $db = $lb->getConnectionRef( DB_MASTER ); + $this->assertEquals( + wfWikiID(), + $db->getDomainID() + ); + unset( $db ); /** @var Database $db */ $db = $lb->getConnection( DB_MASTER, [], '' ); - $lb->reuseConnection( $db ); // don't care $this->assertEquals( '', - $db->getDomainID() + $db->getDomainId(), + 'Null domain ID handle used' + ); + $this->assertEquals( + '', + $db->getDBname(), + 'Null domain ID handle used' + ); + $this->assertEquals( + '', + $db->tablePrefix(), + 'Main domain ID handle used; prefix is empty though' ); - $this->assertEquals( $this->quoteTable( $db, 'page' ), $db->tableName( 'page' ), "Correct full table name" ); - $this->assertEquals( $this->quoteTable( $db, $wgDBname ) . '.' . $this->quoteTable( $db, 'page' ), $db->tableName( "$wgDBname.page" ), "Correct full table name" ); - $this->assertEquals( $this->quoteTable( $db, 'nice_db' ) . '.' . $this->quoteTable( $db, 'page' ), $db->tableName( 'nice_db.page' ), "Correct full table name" ); + $lb->reuseConnection( $db ); // don't care + + $db = $lb->getConnection( DB_MASTER ); // local domain connection $factory->setDomainPrefix( 'my_' ); + + $this->assertEquals( $wgDBname, $db->getDBname() ); $this->assertEquals( - '', + "$wgDBname-my_", $db->getDomainID() ); $this->assertEquals( @@ -351,32 +425,25 @@ class LBFactoryTest extends MediaWikiTestCase { } public function testTrickyDomain() { - global $wgDBtype; - - if ( $wgDBtype === 'sqlite' ) { - $tmpDir = $this->getNewTempDirectory(); - $dbPath = "$tmpDir/unit_test_db.sqlite"; - file_put_contents( $dbPath, '' ); - $tempFsFile = new TempFSFile( $dbPath ); - $tempFsFile->autocollect(); - } else { - $dbPath = null; + global $wgDBname; + + if ( wfGetDB( DB_MASTER )->databasesAreIndependent() ) { + self::markTestSkipped( "Skipping tests about selecting DBs: not applicable" ); + return; } - $dbname = 'unittest-domain'; + $dbname = 'unittest-domain'; // explodes if DB is selected $factory = $this->newLBFactoryMulti( - [ 'localDomain' => $dbname ], - [ 'dbname' => $dbname, 'dbFilePath' => $dbPath ] + [ 'localDomain' => ( new DatabaseDomain( $dbname, null, '' ) )->getId() ], + [ + 'dbName' => 'do_not_select_me' // explodes if DB is selected + ] ); $lb = $factory->getMainLB(); /** @var Database $db */ $db = $lb->getConnection( DB_MASTER, [], '' ); - $lb->reuseConnection( $db ); // don't care - $this->assertEquals( - '', - $db->getDomainID() - ); + $this->assertEquals( '', $db->getDomainID(), "Null domain used" ); $this->assertEquals( $this->quoteTable( $db, 'page' ), @@ -396,7 +463,10 @@ class LBFactoryTest extends MediaWikiTestCase { "Correct full table name" ); + $lb->reuseConnection( $db ); // don't care + $factory->setDomainPrefix( 'my_' ); + $db = $lb->getConnection( DB_MASTER, [], "$wgDBname-my_" ); $this->assertEquals( $this->quoteTable( $db, 'my_page' ), @@ -408,30 +478,46 @@ class LBFactoryTest extends MediaWikiTestCase { $db->tableName( 'other_nice_db.page' ), "Correct full table name" ); - $this->assertEquals( $this->quoteTable( $db, 'garbage-db' ) . '.' . $this->quoteTable( $db, 'page' ), $db->tableName( 'garbage-db.page' ), "Correct full table name" ); - if ( $db->databasesAreIndependent() ) { + $lb->reuseConnection( $db ); // don't care + + $factory->closeAll(); + $factory->destroy(); + } + + public function testInvalidSelectDB() { + $dbname = 'unittest-domain'; // explodes if DB is selected + $factory = $this->newLBFactoryMulti( + [ 'localDomain' => ( new DatabaseDomain( $dbname, null, '' ) )->getId() ], + [ + 'dbName' => 'do_not_select_me' // explodes if DB is selected + ] + ); + $lb = $factory->getMainLB(); + /** @var Database $db */ + $db = $lb->getConnection( DB_MASTER, [], '' ); + + if ( $db->getType() === 'sqlite' ) { + $this->assertFalse( $db->selectDB( 'garbage-db' ) ); + } elseif ( $db->databasesAreIndependent() ) { try { $e = null; $db->selectDB( 'garbage-db' ); } catch ( \Wikimedia\Rdbms\DBConnectionError $e ) { // expected } - $this->assertInstanceOf( '\Wikimedia\Rdbms\DBConnectionError', $e ); + $this->assertInstanceOf( \Wikimedia\Rdbms\DBConnectionError::class, $e ); $this->assertFalse( $db->isOpen() ); } else { - \MediaWiki\suppressWarnings(); + \Wikimedia\suppressWarnings(); $this->assertFalse( $db->selectDB( 'garbage-db' ) ); - \MediaWiki\restoreWarnings(); + \Wikimedia\restoreWarnings(); } - - $factory->closeAll(); - $factory->destroy(); } private function quoteTable( Database $db, $table ) { diff --git a/www/wiki/tests/phpunit/includes/db/LoadBalancerTest.php b/www/wiki/tests/phpunit/includes/db/LoadBalancerTest.php index f8ab7f48..e054569d 100644 --- a/www/wiki/tests/phpunit/includes/db/LoadBalancerTest.php +++ b/www/wiki/tests/phpunit/includes/db/LoadBalancerTest.php @@ -1,7 +1,5 @@ <?php -use Wikimedia\Rdbms\LoadBalancer; - /** * Holds tests for LoadBalancer MediaWiki class. * @@ -20,62 +18,94 @@ use Wikimedia\Rdbms\LoadBalancer; * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * http://www.gnu.org/copyleft/gpl.html * - * @group Database * @file */ + +use Wikimedia\Rdbms\DBError; +use Wikimedia\Rdbms\DatabaseDomain; +use Wikimedia\Rdbms\Database; +use Wikimedia\Rdbms\LoadBalancer; +use Wikimedia\Rdbms\LoadMonitorNull; + +/** + * @group Database + * @covers \Wikimedia\Rdbms\LoadBalancer + */ class LoadBalancerTest extends MediaWikiTestCase { - public function testLBSimpleServer() { + private function makeServerConfig() { global $wgDBserver, $wgDBname, $wgDBuser, $wgDBpassword, $wgDBtype, $wgSQLiteDataDir; - $servers = [ - [ - 'host' => $wgDBserver, - 'dbname' => $wgDBname, - 'user' => $wgDBuser, - 'password' => $wgDBpassword, - 'type' => $wgDBtype, - 'dbDirectory' => $wgSQLiteDataDir, - 'load' => 0, - 'flags' => DBO_TRX // REPEATABLE-READ for consistency - ], + return [ + 'host' => $wgDBserver, + 'dbname' => $wgDBname, + 'tablePrefix' => $this->dbPrefix(), + 'user' => $wgDBuser, + 'password' => $wgDBpassword, + 'type' => $wgDBtype, + 'dbDirectory' => $wgSQLiteDataDir, + 'load' => 0, + 'flags' => DBO_TRX // REPEATABLE-READ for consistency ]; + } + + public function testWithoutReplica() { + global $wgDBname; + $called = false; $lb = new LoadBalancer( [ - 'servers' => $servers, - 'localDomain' => wfWikiID() + 'servers' => [ $this->makeServerConfig() ], + 'queryLogger' => MediaWiki\Logger\LoggerFactory::getInstance( 'DBQuery' ), + 'localDomain' => new DatabaseDomain( $wgDBname, null, $this->dbPrefix() ), + 'chronologyCallback' => function () use ( &$called ) { + $called = true; + } ] ); + $ld = DatabaseDomain::newFromId( $lb->getLocalDomainID() ); + $this->assertEquals( $wgDBname, $ld->getDatabase(), 'local domain DB set' ); + $this->assertEquals( $this->dbPrefix(), $ld->getTablePrefix(), 'local domain prefix set' ); + + $this->assertFalse( $called ); $dbw = $lb->getConnection( DB_MASTER ); + $this->assertTrue( $called ); $this->assertTrue( $dbw->getLBInfo( 'master' ), 'master shows as master' ); $this->assertTrue( $dbw->getFlag( $dbw::DBO_TRX ), "DBO_TRX set on master" ); + $this->assertWriteAllowed( $dbw ); $dbr = $lb->getConnection( DB_REPLICA ); $this->assertTrue( $dbr->getLBInfo( 'master' ), 'DB_REPLICA also gets the master' ); - $this->assertTrue( $dbw->getFlag( $dbw::DBO_TRX ), "DBO_TRX set on replica" ); + $this->assertTrue( $dbr->getFlag( $dbw::DBO_TRX ), "DBO_TRX set on replica" ); - $dbwAuto = $lb->getConnection( DB_MASTER, [], false, $lb::CONN_TRX_AUTO ); - $this->assertFalse( $dbwAuto->getFlag( $dbw::DBO_TRX ), "No DBO_TRX with CONN_TRX_AUTO" ); - $this->assertTrue( $dbw->getFlag( $dbw::DBO_TRX ), "DBO_TRX still set on master" ); - $this->assertNotEquals( $dbw, $dbwAuto, "CONN_TRX_AUTO uses separate connection" ); + if ( !$lb->getServerAttributes( $lb->getWriterIndex() )[$dbw::ATTR_DB_LEVEL_LOCKING] ) { + $dbwAuto = $lb->getConnection( DB_MASTER, [], false, $lb::CONN_TRX_AUTOCOMMIT ); + $this->assertFalse( + $dbwAuto->getFlag( $dbw::DBO_TRX ), "No DBO_TRX with CONN_TRX_AUTOCOMMIT" ); + $this->assertTrue( $dbw->getFlag( $dbw::DBO_TRX ), "DBO_TRX still set on master" ); + $this->assertNotEquals( + $dbw, $dbwAuto, "CONN_TRX_AUTOCOMMIT uses separate connection" ); - $dbrAuto = $lb->getConnection( DB_REPLICA, [], false, $lb::CONN_TRX_AUTO ); - $this->assertFalse( $dbrAuto->getFlag( $dbw::DBO_TRX ), "No DBO_TRX with CONN_TRX_AUTO" ); - $this->assertTrue( $dbr->getFlag( $dbw::DBO_TRX ), "DBO_TRX still set on replica" ); - $this->assertNotEquals( $dbr, $dbrAuto, "CONN_TRX_AUTO uses separate connection" ); + $dbrAuto = $lb->getConnection( DB_REPLICA, [], false, $lb::CONN_TRX_AUTOCOMMIT ); + $this->assertFalse( + $dbrAuto->getFlag( $dbw::DBO_TRX ), "No DBO_TRX with CONN_TRX_AUTOCOMMIT" ); + $this->assertTrue( $dbr->getFlag( $dbw::DBO_TRX ), "DBO_TRX still set on replica" ); + $this->assertNotEquals( + $dbr, $dbrAuto, "CONN_TRX_AUTOCOMMIT uses separate connection" ); - $dbwAuto2 = $lb->getConnection( DB_MASTER, [], false, $lb::CONN_TRX_AUTO ); - $this->assertEquals( $dbwAuto2, $dbwAuto, "CONN_TRX_AUTO reuses connections" ); + $dbwAuto2 = $lb->getConnection( DB_MASTER, [], false, $lb::CONN_TRX_AUTOCOMMIT ); + $this->assertEquals( $dbwAuto2, $dbwAuto, "CONN_TRX_AUTOCOMMIT reuses connections" ); + } $lb->closeAll(); } - public function testLBSimpleServers() { + public function testWithReplica() { global $wgDBserver, $wgDBname, $wgDBuser, $wgDBpassword, $wgDBtype, $wgSQLiteDataDir; $servers = [ [ // master 'host' => $wgDBserver, 'dbname' => $wgDBname, + 'tablePrefix' => $this->dbPrefix(), 'user' => $wgDBuser, 'password' => $wgDBpassword, 'type' => $wgDBtype, @@ -83,9 +113,10 @@ class LoadBalancerTest extends MediaWikiTestCase { 'load' => 0, 'flags' => DBO_TRX // REPEATABLE-READ for consistency ], - [ // emulated slave + [ // emulated replica 'host' => $wgDBserver, 'dbname' => $wgDBname, + 'tablePrefix' => $this->dbPrefix(), 'user' => $wgDBuser, 'password' => $wgDBpassword, 'type' => $wgDBtype, @@ -97,8 +128,9 @@ class LoadBalancerTest extends MediaWikiTestCase { $lb = new LoadBalancer( [ 'servers' => $servers, - 'localDomain' => wfWikiID(), - 'loadMonitorClass' => 'LoadMonitorNull' + 'localDomain' => new DatabaseDomain( $wgDBname, null, $this->dbPrefix() ), + 'queryLogger' => MediaWiki\Logger\LoggerFactory::getInstance( 'DBQuery' ), + 'loadMonitorClass' => LoadMonitorNull::class ] ); $dbw = $lb->getConnection( DB_MASTER ); @@ -108,27 +140,165 @@ class LoadBalancerTest extends MediaWikiTestCase { $dbw->getLBInfo( 'clusterMasterHost' ), 'cluster master set' ); $this->assertTrue( $dbw->getFlag( $dbw::DBO_TRX ), "DBO_TRX set on master" ); + $this->assertWriteAllowed( $dbw ); $dbr = $lb->getConnection( DB_REPLICA ); - $this->assertTrue( $dbr->getLBInfo( 'replica' ), 'slave shows as slave' ); + $this->assertTrue( $dbr->getLBInfo( 'replica' ), 'replica shows as replica' ); $this->assertEquals( ( $wgDBserver != '' ) ? $wgDBserver : 'localhost', $dbr->getLBInfo( 'clusterMasterHost' ), 'cluster master set' ); - $this->assertTrue( $dbw->getFlag( $dbw::DBO_TRX ), "DBO_TRX set on replica" ); + $this->assertTrue( $dbr->getFlag( $dbw::DBO_TRX ), "DBO_TRX set on replica" ); + $this->assertWriteForbidden( $dbr ); + + if ( !$lb->getServerAttributes( $lb->getWriterIndex() )[$dbw::ATTR_DB_LEVEL_LOCKING] ) { + $dbwAuto = $lb->getConnection( DB_MASTER, [], false, $lb::CONN_TRX_AUTOCOMMIT ); + $this->assertFalse( + $dbwAuto->getFlag( $dbw::DBO_TRX ), "No DBO_TRX with CONN_TRX_AUTOCOMMIT" ); + $this->assertTrue( $dbw->getFlag( $dbw::DBO_TRX ), "DBO_TRX still set on master" ); + $this->assertNotEquals( + $dbw, $dbwAuto, "CONN_TRX_AUTOCOMMIT uses separate connection" ); + + $dbrAuto = $lb->getConnection( DB_REPLICA, [], false, $lb::CONN_TRX_AUTOCOMMIT ); + $this->assertFalse( + $dbrAuto->getFlag( $dbw::DBO_TRX ), "No DBO_TRX with CONN_TRX_AUTOCOMMIT" ); + $this->assertTrue( $dbr->getFlag( $dbw::DBO_TRX ), "DBO_TRX still set on replica" ); + $this->assertNotEquals( + $dbr, $dbrAuto, "CONN_TRX_AUTOCOMMIT uses separate connection" ); + + $dbwAuto2 = $lb->getConnection( DB_MASTER, [], false, $lb::CONN_TRX_AUTOCOMMIT ); + $this->assertEquals( $dbwAuto2, $dbwAuto, "CONN_TRX_AUTOCOMMIT reuses connections" ); + } + + $lb->closeAll(); + } + + private function assertWriteForbidden( Database $db ) { + try { + $db->delete( 'some_table', [ 'id' => 57634126 ], __METHOD__ ); + $this->fail( 'Write operation should have failed!' ); + } catch ( DBError $ex ) { + // check that the exception message contains "Write operation" + $constraint = new PHPUnit_Framework_Constraint_StringContains( 'Write operation' ); - $dbwAuto = $lb->getConnection( DB_MASTER, [], false, $lb::CONN_TRX_AUTO ); - $this->assertFalse( $dbwAuto->getFlag( $dbw::DBO_TRX ), "No DBO_TRX with CONN_TRX_AUTO" ); - $this->assertTrue( $dbw->getFlag( $dbw::DBO_TRX ), "DBO_TRX still set on master" ); - $this->assertNotEquals( $dbw, $dbwAuto, "CONN_TRX_AUTO uses separate connection" ); + if ( !$constraint->evaluate( $ex->getMessage(), '', true ) ) { + // re-throw original error, to preserve stack trace + throw $ex; + } + } + } + + private function assertWriteAllowed( Database $db ) { + $table = $db->tableName( 'some_table' ); + try { + $db->dropTable( 'some_table' ); // clear for sanity + + // Trigger DBO_TRX to create a transaction so the flush below will + // roll everything here back in sqlite. But don't actually do the + // code below inside an atomic section becaue MySQL and Oracle + // auto-commit transactions for DDL statements like CREATE TABLE. + $db->startAtomic( __METHOD__ ); + $db->endAtomic( __METHOD__ ); + + // Use only basic SQL and trivial types for these queries for compatibility + $this->assertNotSame( + false, + $db->query( "CREATE TABLE $table (id INT, time INT)", __METHOD__ ), + "table created" + ); + $this->assertNotSame( + false, + $db->query( "DELETE FROM $table WHERE id=57634126", __METHOD__ ), + "delete query" + ); + } finally { + // Drop the table to clean up, ignoring any error. + $db->query( "DROP TABLE $table", __METHOD__, true ); + // Rollback the DBO_TRX transaction for sqlite's benefit. + $db->rollback( __METHOD__, 'flush' ); + } + } + + public function testServerAttributes() { + $servers = [ + [ // master + 'dbname' => 'my_unittest_wiki', + 'tablePrefix' => 'unittest_', + 'type' => 'sqlite', + 'dbDirectory' => "some_directory", + 'load' => 0 + ] + ]; + + $lb = new LoadBalancer( [ + 'servers' => $servers, + 'localDomain' => new DatabaseDomain( 'my_unittest_wiki', null, 'unittest_' ), + 'loadMonitorClass' => LoadMonitorNull::class + ] ); + + $this->assertTrue( $lb->getServerAttributes( 0 )[Database::ATTR_DB_LEVEL_LOCKING] ); - $dbrAuto = $lb->getConnection( DB_REPLICA, [], false, $lb::CONN_TRX_AUTO ); - $this->assertFalse( $dbrAuto->getFlag( $dbw::DBO_TRX ), "No DBO_TRX with CONN_TRX_AUTO" ); - $this->assertTrue( $dbr->getFlag( $dbw::DBO_TRX ), "DBO_TRX still set on replica" ); - $this->assertNotEquals( $dbr, $dbrAuto, "CONN_TRX_AUTO uses separate connection" ); + $servers = [ + [ // master + 'host' => 'db1001', + 'user' => 'wikiuser', + 'password' => 'none', + 'dbname' => 'my_unittest_wiki', + 'tablePrefix' => 'unittest_', + 'type' => 'mysql', + 'load' => 100 + ], + [ // emulated replica + 'host' => 'db1002', + 'user' => 'wikiuser', + 'password' => 'none', + 'dbname' => 'my_unittest_wiki', + 'tablePrefix' => 'unittest_', + 'type' => 'mysql', + 'load' => 100 + ] + ]; + + $lb = new LoadBalancer( [ + 'servers' => $servers, + 'localDomain' => new DatabaseDomain( 'my_unittest_wiki', null, 'unittest_' ), + 'loadMonitorClass' => LoadMonitorNull::class + ] ); + + $this->assertFalse( $lb->getServerAttributes( 1 )[Database::ATTR_DB_LEVEL_LOCKING] ); + } + + /** + * @covers LoadBalancer::openConnection() + * @covers LoadBalancer::getAnyOpenConnection() + */ + function testOpenConnection() { + global $wgDBname; + + $lb = new LoadBalancer( [ + 'servers' => [ $this->makeServerConfig() ], + 'localDomain' => new DatabaseDomain( $wgDBname, null, $this->dbPrefix() ) + ] ); - $dbwAuto2 = $lb->getConnection( DB_MASTER, [], false, $lb::CONN_TRX_AUTO ); - $this->assertEquals( $dbwAuto2, $dbwAuto, "CONN_TRX_AUTO reuses connections" ); + $i = $lb->getWriterIndex(); + $this->assertEquals( null, $lb->getAnyOpenConnection( $i ) ); + $conn1 = $lb->getConnection( $i ); + $this->assertNotEquals( null, $conn1 ); + $this->assertEquals( $conn1, $lb->getAnyOpenConnection( $i ) ); + $conn2 = $lb->getConnection( $i, [], false, $lb::CONN_TRX_AUTOCOMMIT ); + $this->assertNotEquals( null, $conn2 ); + if ( $lb->getServerAttributes( $i )[Database::ATTR_DB_LEVEL_LOCKING] ) { + $this->assertEquals( null, + $lb->getAnyOpenConnection( $i, $lb::CONN_TRX_AUTOCOMMIT ) ); + $this->assertEquals( $conn1, + $lb->getConnection( + $i, [], false, $lb::CONN_TRX_AUTOCOMMIT ), $lb::CONN_TRX_AUTOCOMMIT ); + } else { + $this->assertEquals( $conn2, + $lb->getAnyOpenConnection( $i, $lb::CONN_TRX_AUTOCOMMIT ) ); + $this->assertEquals( $conn2, + $lb->getConnection( $i, [], false, $lb::CONN_TRX_AUTOCOMMIT ) ); + } $lb->closeAll(); } diff --git a/www/wiki/tests/phpunit/includes/debug/MWDebugTest.php b/www/wiki/tests/phpunit/includes/debug/MWDebugTest.php index 5c654831..6f0b1db9 100644 --- a/www/wiki/tests/phpunit/includes/debug/MWDebugTest.php +++ b/www/wiki/tests/phpunit/includes/debug/MWDebugTest.php @@ -11,13 +11,13 @@ class MWDebugTest extends MediaWikiTestCase { public static function setUpBeforeClass() { parent::setUpBeforeClass(); MWDebug::init(); - MediaWiki\suppressWarnings(); + Wikimedia\suppressWarnings(); } public static function tearDownAfterClass() { parent::tearDownAfterClass(); MWDebug::deinit(); - MediaWiki\restoreWarnings(); + Wikimedia\restoreWarnings(); } /** @@ -99,7 +99,7 @@ class MWDebugTest extends MediaWikiTestCase { MWDebug::appendDebugInfoToApiResult( $context, $result ); - $this->assertInstanceOf( 'ApiResult', $result ); + $this->assertInstanceOf( ApiResult::class, $result ); $data = $result->getResultData(); $expectedKeys = [ 'mwVersion', 'phpEngine', 'phpVersion', 'gitRevision', 'gitBranch', @@ -110,7 +110,7 @@ class MWDebugTest extends MediaWikiTestCase { $this->assertArrayHasKey( $expectedKey, $data['debuginfo'], "debuginfo has $expectedKey" ); } - $xml = ApiFormatXml::recXmlPrint( 'help', $data ); + $xml = ApiFormatXml::recXmlPrint( 'help', $data, null ); // exception not thrown $this->assertInternalType( 'string', $xml ); @@ -123,7 +123,7 @@ class MWDebugTest extends MediaWikiTestCase { * @return FauxRequest */ private function newApiRequest( array $params, $requestUrl ) { - $request = $this->getMockBuilder( 'FauxRequest' ) + $request = $this->getMockBuilder( FauxRequest::class ) ->setMethods( [ 'getRequestURL' ] ) ->setConstructorArgs( [ $params diff --git a/www/wiki/tests/phpunit/includes/debug/logger/monolog/AvroFormatterTest.php b/www/wiki/tests/phpunit/includes/debug/logger/monolog/AvroFormatterTest.php index 938397ab..baa4df73 100644 --- a/www/wiki/tests/phpunit/includes/debug/logger/monolog/AvroFormatterTest.php +++ b/www/wiki/tests/phpunit/includes/debug/logger/monolog/AvroFormatterTest.php @@ -23,6 +23,9 @@ namespace MediaWiki\Logger\Monolog; use MediaWikiTestCase; use PHPUnit_Framework_Error_Notice; +/** + * @covers \MediaWiki\Logger\Monolog\AvroFormatter + */ class AvroFormatterTest extends MediaWikiTestCase { protected function setUp() { @@ -47,9 +50,9 @@ class AvroFormatterTest extends MediaWikiTestCase { // disable conversion of notices PHPUnit_Framework_Error_Notice::$enabled = false; // have to keep the user notice from being output - \MediaWiki\suppressWarnings(); + \Wikimedia\suppressWarnings(); $res = $formatter->format( [ 'channel' => 'marty' ] ); - \MediaWiki\restoreWarnings(); + \Wikimedia\restoreWarnings(); PHPUnit_Framework_Error_Notice::$enabled = $noticeEnabled; $this->assertNull( $res ); } diff --git a/www/wiki/tests/phpunit/includes/debug/logger/monolog/KafkaHandlerTest.php b/www/wiki/tests/phpunit/includes/debug/logger/monolog/KafkaHandlerTest.php index 88cd2dd2..4c0ca04f 100644 --- a/www/wiki/tests/phpunit/includes/debug/logger/monolog/KafkaHandlerTest.php +++ b/www/wiki/tests/phpunit/includes/debug/logger/monolog/KafkaHandlerTest.php @@ -24,6 +24,9 @@ use MediaWikiTestCase; use Monolog\Logger; use Wikimedia\TestingAccessWrapper; +/** + * @covers \MediaWiki\Logger\Monolog\KafkaHandler + */ class KafkaHandlerTest extends MediaWikiTestCase { protected function setUp() { @@ -155,13 +158,14 @@ class KafkaHandlerTest extends MediaWikiTestCase { ->method( 'send' ) ->will( $this->returnValue( true ) ); // evil hax - TestingAccessWrapper::newFromObject( $mockMethod )->matcher->parametersMatcher = + $matcher = TestingAccessWrapper::newFromObject( $mockMethod )->matcher; + TestingAccessWrapper::newFromObject( $matcher )->parametersMatcher = new \PHPUnit_Framework_MockObject_Matcher_ConsecutiveParameters( [ [ $this->anything(), $this->anything(), [ 'words' ] ], [ $this->anything(), $this->anything(), [ 'lines' ] ] ] ); - $formatter = $this->createMock( 'Monolog\Formatter\FormatterInterface' ); + $formatter = $this->createMock( \Monolog\Formatter\FormatterInterface::class ); $formatter->expects( $this->any() ) ->method( 'format' ) ->will( $this->onConsecutiveCalls( 'words', null, 'lines' ) ); @@ -192,7 +196,7 @@ class KafkaHandlerTest extends MediaWikiTestCase { ->method( 'send' ) ->will( $this->returnValue( true ) ); - $formatter = $this->createMock( 'Monolog\Formatter\FormatterInterface' ); + $formatter = $this->createMock( \Monolog\Formatter\FormatterInterface::class ); $formatter->expects( $this->any() ) ->method( 'format' ) ->will( $this->onConsecutiveCalls( 'words', null, 'lines' ) ); diff --git a/www/wiki/tests/phpunit/includes/debug/logger/monolog/LogstashFormatterTest.php b/www/wiki/tests/phpunit/includes/debug/logger/monolog/LogstashFormatterTest.php index 8086b4bf..1ee188e7 100644 --- a/www/wiki/tests/phpunit/includes/debug/logger/monolog/LogstashFormatterTest.php +++ b/www/wiki/tests/phpunit/includes/debug/logger/monolog/LogstashFormatterTest.php @@ -2,7 +2,7 @@ namespace MediaWiki\Logger\Monolog; -class LogstashFormatterTest extends \PHPUnit_Framework_TestCase { +class LogstashFormatterTest extends \PHPUnit\Framework\TestCase { /** * @dataProvider provideV1 * @param array $record The input record. diff --git a/www/wiki/tests/phpunit/includes/deferred/CdnCacheUpdateTest.php b/www/wiki/tests/phpunit/includes/deferred/CdnCacheUpdateTest.php index 11b869a7..f3c949d3 100644 --- a/www/wiki/tests/phpunit/includes/deferred/CdnCacheUpdateTest.php +++ b/www/wiki/tests/phpunit/includes/deferred/CdnCacheUpdateTest.php @@ -3,6 +3,10 @@ use Wikimedia\TestingAccessWrapper; class CdnCacheUpdateTest extends MediaWikiTestCase { + + /** + * @covers CdnCacheUpdate::merge + */ public function testPurgeMergeWeb() { $this->setMwGlobals( 'wgCommandLineMode', false ); diff --git a/www/wiki/tests/phpunit/includes/deferred/DeferredUpdatesTest.php b/www/wiki/tests/phpunit/includes/deferred/DeferredUpdatesTest.php index 3b423563..6b417073 100644 --- a/www/wiki/tests/phpunit/includes/deferred/DeferredUpdatesTest.php +++ b/www/wiki/tests/phpunit/includes/deferred/DeferredUpdatesTest.php @@ -1,12 +1,68 @@ <?php +use MediaWiki\MediaWikiServices; + class DeferredUpdatesTest extends MediaWikiTestCase { /** + * @covers DeferredUpdates::addUpdate + * @covers DeferredUpdates::push + * @covers DeferredUpdates::doUpdates + * @covers DeferredUpdates::execute + * @covers DeferredUpdates::runUpdate + */ + public function testAddAndRun() { + $update = $this->getMockBuilder( DeferrableUpdate::class ) + ->setMethods( [ 'doUpdate' ] )->getMock(); + $update->expects( $this->once() )->method( 'doUpdate' ); + + DeferredUpdates::addUpdate( $update ); + DeferredUpdates::doUpdates(); + } + + /** + * @covers DeferredUpdates::addUpdate + * @covers DeferredUpdates::push + */ + public function testAddMergeable() { + $this->setMwGlobals( 'wgCommandLineMode', false ); + + $update1 = $this->getMockBuilder( MergeableUpdate::class ) + ->setMethods( [ 'merge', 'doUpdate' ] )->getMock(); + $update1->expects( $this->once() )->method( 'merge' ); + $update1->expects( $this->never() )->method( 'doUpdate' ); + + $update2 = $this->getMockBuilder( MergeableUpdate::class ) + ->setMethods( [ 'merge', 'doUpdate' ] )->getMock(); + $update2->expects( $this->never() )->method( 'merge' ); + $update2->expects( $this->never() )->method( 'doUpdate' ); + + DeferredUpdates::addUpdate( $update1 ); + DeferredUpdates::addUpdate( $update2 ); + } + + /** + * @covers DeferredUpdates::addCallableUpdate + * @covers MWCallableUpdate::getOrigin + */ + public function testAddCallableUpdate() { + $this->setMwGlobals( 'wgCommandLineMode', true ); + + $ran = 0; + DeferredUpdates::addCallableUpdate( function () use ( &$ran ) { + $ran++; + } ); + DeferredUpdates::doUpdates(); + + $this->assertSame( 1, $ran, 'Update ran' ); + } + + /** * @covers DeferredUpdates::getPendingUpdates + * @covers DeferredUpdates::clearPendingUpdates */ public function testGetPendingUpdates() { - # Prevent updates from running + // Prevent updates from running $this->setMwGlobals( 'wgCommandLineMode', false ); $pre = DeferredUpdates::PRESEND; @@ -34,6 +90,11 @@ class DeferredUpdatesTest extends MediaWikiTestCase { $this->assertCount( 0, DeferredUpdates::getPendingUpdates() ); } + /** + * @covers DeferredUpdates::doUpdates + * @covers DeferredUpdates::execute + * @covers DeferredUpdates::addUpdate + */ public function testDoUpdatesWeb() { $this->setMwGlobals( 'wgCommandLineMode', false ); @@ -126,6 +187,11 @@ class DeferredUpdatesTest extends MediaWikiTestCase { $this->assertEquals( "Marychu", $y, "POSTSEND update ran" ); } + /** + * @covers DeferredUpdates::doUpdates + * @covers DeferredUpdates::execute + * @covers DeferredUpdates::addUpdate + */ public function testDoUpdatesCLI() { $this->setMwGlobals( 'wgCommandLineMode', true ); $updates = [ @@ -140,7 +206,9 @@ class DeferredUpdatesTest extends MediaWikiTestCase { '3-2-1' => "deferred update 1 within deferred update 2 with deferred update 3;\n", ]; - wfGetLBFactory()->commitMasterChanges( __METHOD__ ); // clear anything + // clear anything + $lbFactory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory(); + $lbFactory->commitMasterChanges( __METHOD__ ); DeferredUpdates::addCallableUpdate( function () use ( $updates ) { @@ -193,12 +261,19 @@ class DeferredUpdatesTest extends MediaWikiTestCase { DeferredUpdates::doUpdates(); } + /** + * @covers DeferredUpdates::doUpdates + * @covers DeferredUpdates::execute + * @covers DeferredUpdates::addUpdate + */ public function testPresendAddOnPostsendRun() { $this->setMwGlobals( 'wgCommandLineMode', true ); $x = false; $y = false; - wfGetLBFactory()->commitMasterChanges( __METHOD__ ); // clear anything + // clear anything + $lbFactory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory(); + $lbFactory->commitMasterChanges( __METHOD__ ); DeferredUpdates::addCallableUpdate( function () use ( &$x, &$y ) { @@ -218,4 +293,46 @@ class DeferredUpdatesTest extends MediaWikiTestCase { $this->assertTrue( $x, "Outer POSTSEND update ran" ); $this->assertTrue( $y, "Nested PRESEND update ran" ); } + + /** + * @covers DeferredUpdates::runUpdate + */ + public function testRunUpdateTransactionScope() { + $this->setMwGlobals( 'wgCommandLineMode', false ); + + $lbFactory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory(); + $this->assertFalse( $lbFactory->hasTransactionRound(), 'Initial state' ); + + $ran = 0; + DeferredUpdates::addCallableUpdate( function () use ( &$ran, $lbFactory ) { + $ran++; + $this->assertTrue( $lbFactory->hasTransactionRound(), 'Has transaction' ); + } ); + DeferredUpdates::doUpdates(); + + $this->assertSame( 1, $ran, 'Update ran' ); + $this->assertFalse( $lbFactory->hasTransactionRound(), 'Final state' ); + } + + /** + * @covers DeferredUpdates::runUpdate + * @covers TransactionRoundDefiningUpdate::getOrigin + */ + public function testRunOuterScopeUpdate() { + $this->setMwGlobals( 'wgCommandLineMode', false ); + + $lbFactory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory(); + $this->assertFalse( $lbFactory->hasTransactionRound(), 'Initial state' ); + + $ran = 0; + DeferredUpdates::addUpdate( new TransactionRoundDefiningUpdate( + function () use ( &$ran, $lbFactory ) { + $ran++; + $this->assertFalse( $lbFactory->hasTransactionRound(), 'No transaction' ); + } ) + ); + DeferredUpdates::doUpdates(); + + $this->assertSame( 1, $ran, 'Update ran' ); + } } diff --git a/www/wiki/tests/phpunit/includes/deferred/LinksUpdateTest.php b/www/wiki/tests/phpunit/includes/deferred/LinksUpdateTest.php index 4c0a5fa9..ddc0798f 100644 --- a/www/wiki/tests/phpunit/includes/deferred/LinksUpdateTest.php +++ b/www/wiki/tests/phpunit/includes/deferred/LinksUpdateTest.php @@ -1,6 +1,7 @@ <?php /** + * @covers LinksUpdate * @group LinksUpdate * @group Database * ^--- make sure temporary tables are used. diff --git a/www/wiki/tests/phpunit/includes/deferred/MWCallableUpdateTest.php b/www/wiki/tests/phpunit/includes/deferred/MWCallableUpdateTest.php new file mode 100644 index 00000000..3ab9b565 --- /dev/null +++ b/www/wiki/tests/phpunit/includes/deferred/MWCallableUpdateTest.php @@ -0,0 +1,82 @@ +<?php + +/** + * @covers MWCallableUpdate + */ +class MWCallableUpdateTest extends PHPUnit\Framework\TestCase { + + use MediaWikiCoversValidator; + + public function testDoUpdate() { + $ran = 0; + $update = new MWCallableUpdate( function () use ( &$ran ) { + $ran++; + } ); + $this->assertSame( 0, $ran ); + $update->doUpdate(); + $this->assertSame( 1, $ran ); + } + + public function testCancel() { + // Prepare update and DB + $db = new DatabaseTestHelper( __METHOD__ ); + $db->begin( __METHOD__ ); + $ran = 0; + $update = new MWCallableUpdate( function () use ( &$ran ) { + $ran++; + }, __METHOD__, $db ); + + // Emulate rollback + $db->rollback( __METHOD__ ); + + $update->doUpdate(); + + // Ensure it was cancelled + $this->assertSame( 0, $ran ); + } + + public function testCancelSome() { + // Prepare update and DB + $db1 = new DatabaseTestHelper( __METHOD__ ); + $db1->begin( __METHOD__ ); + $db2 = new DatabaseTestHelper( __METHOD__ ); + $db2->begin( __METHOD__ ); + $ran = 0; + $update = new MWCallableUpdate( function () use ( &$ran ) { + $ran++; + }, __METHOD__, [ $db1, $db2 ] ); + + // Emulate rollback + $db1->rollback( __METHOD__ ); + + $update->doUpdate(); + + // Prevents: "Notice: DB transaction writes or callbacks still pending" + $db2->rollback( __METHOD__ ); + + // Ensure it was cancelled + $this->assertSame( 0, $ran ); + } + + public function testCancelAll() { + // Prepare update and DB + $db1 = new DatabaseTestHelper( __METHOD__ ); + $db1->begin( __METHOD__ ); + $db2 = new DatabaseTestHelper( __METHOD__ ); + $db2->begin( __METHOD__ ); + $ran = 0; + $update = new MWCallableUpdate( function () use ( &$ran ) { + $ran++; + }, __METHOD__, [ $db1, $db2 ] ); + + // Emulate rollbacks + $db1->rollback( __METHOD__ ); + $db2->rollback( __METHOD__ ); + + $update->doUpdate(); + + // Ensure it was cancelled + $this->assertSame( 0, $ran ); + } + +} diff --git a/www/wiki/tests/phpunit/includes/deferred/SiteStatsUpdateTest.php b/www/wiki/tests/phpunit/includes/deferred/SiteStatsUpdateTest.php new file mode 100644 index 00000000..83e9a47c --- /dev/null +++ b/www/wiki/tests/phpunit/includes/deferred/SiteStatsUpdateTest.php @@ -0,0 +1,77 @@ +<?php + +use Wikimedia\TestingAccessWrapper; + +/** + * @group Database + */ +class SiteStatsUpdateTest extends MediaWikiTestCase { + /** + * @covers SiteStatsUpdate::factory + * @covers SiteStatsUpdate::merge + */ + public function testFactoryAndMerge() { + $update1 = SiteStatsUpdate::factory( [ 'pages' => 1, 'users' => 2 ] ); + $update2 = SiteStatsUpdate::factory( [ 'users' => 1, 'images' => 1 ] ); + + $update1->merge( $update2 ); + $wrapped = TestingAccessWrapper::newFromObject( $update1 ); + + $this->assertEquals( 1, $wrapped->pages ); + $this->assertEquals( 3, $wrapped->users ); + $this->assertEquals( 1, $wrapped->images ); + $this->assertEquals( 0, $wrapped->edits ); + $this->assertEquals( 0, $wrapped->articles ); + } + + /** + * @covers SiteStatsUpdate::doUpdate() + * @covers SiteStatsInit::refresh() + */ + public function testDoUpdate() { + $this->setMwGlobals( 'wgSiteStatsAsyncFactor', false ); + $this->setMwGlobals( 'wgCommandLineMode', false ); // disable opportunistic updates + + $dbw = wfGetDB( DB_MASTER ); + $statsInit = new SiteStatsInit( $dbw ); + $statsInit->refresh(); + + $ei = SiteStats::edits(); // trigger load + $pi = SiteStats::pages(); + $ui = SiteStats::users(); + $fi = SiteStats::images(); + $ai = SiteStats::articles(); + + $dbw->begin( __METHOD__ ); // block opportunistic updates + + $update = SiteStatsUpdate::factory( [ 'pages' => 2, 'images' => 1, 'edits' => 2 ] ); + $this->assertEquals( 0, DeferredUpdates::pendingUpdatesCount() ); + $update->doUpdate(); + $this->assertEquals( 1, DeferredUpdates::pendingUpdatesCount() ); + + // Still the same + SiteStats::unload(); + $this->assertEquals( $pi, SiteStats::pages(), 'page count' ); + $this->assertEquals( $ei, SiteStats::edits(), 'edit count' ); + $this->assertEquals( $ui, SiteStats::users(), 'user count' ); + $this->assertEquals( $fi, SiteStats::images(), 'file count' ); + $this->assertEquals( $ai, SiteStats::articles(), 'article count' ); + $this->assertEquals( 1, DeferredUpdates::pendingUpdatesCount() ); + + $dbw->commit( __METHOD__ ); + + $this->assertEquals( 1, DeferredUpdates::pendingUpdatesCount() ); + DeferredUpdates::doUpdates(); + $this->assertEquals( 0, DeferredUpdates::pendingUpdatesCount() ); + + SiteStats::unload(); + $this->assertEquals( $pi + 2, SiteStats::pages(), 'page count' ); + $this->assertEquals( $ei + 2, SiteStats::edits(), 'edit count' ); + $this->assertEquals( $ui, SiteStats::users(), 'user count' ); + $this->assertEquals( $fi + 1, SiteStats::images(), 'file count' ); + $this->assertEquals( $ai, SiteStats::articles(), 'article count' ); + + $statsInit = new SiteStatsInit(); + $statsInit->refresh(); + } +} diff --git a/www/wiki/tests/phpunit/includes/deferred/TransactionRoundDefiningUpdateTest.php b/www/wiki/tests/phpunit/includes/deferred/TransactionRoundDefiningUpdateTest.php new file mode 100644 index 00000000..693897e6 --- /dev/null +++ b/www/wiki/tests/phpunit/includes/deferred/TransactionRoundDefiningUpdateTest.php @@ -0,0 +1,19 @@ +<?php + +/** + * @covers TransactionRoundDefiningUpdate + */ +class TransactionRoundDefiningUpdateTest extends PHPUnit\Framework\TestCase { + + use MediaWikiCoversValidator; + + public function testDoUpdate() { + $ran = 0; + $update = new TransactionRoundDefiningUpdate( function () use ( &$ran ) { + $ran++; + } ); + $this->assertSame( 0, $ran ); + $update->doUpdate(); + $this->assertSame( 1, $ran ); + } +} diff --git a/www/wiki/tests/phpunit/includes/diff/ArrayDiffFormatterTest.php b/www/wiki/tests/phpunit/includes/diff/ArrayDiffFormatterTest.php index 106b86af..8d94404c 100644 --- a/www/wiki/tests/phpunit/includes/diff/ArrayDiffFormatterTest.php +++ b/www/wiki/tests/phpunit/includes/diff/ArrayDiffFormatterTest.php @@ -20,7 +20,7 @@ class ArrayDiffFormatterTest extends MediaWikiTestCase { } private function getMockDiff( $edits ) { - $diff = $this->getMockBuilder( 'Diff' ) + $diff = $this->getMockBuilder( Diff::class ) ->disableOriginalConstructor() ->getMock(); $diff->expects( $this->any() ) @@ -30,7 +30,7 @@ class ArrayDiffFormatterTest extends MediaWikiTestCase { } private function getMockDiffOp( $type = null, $orig = [], $closing = [] ) { - $diffOp = $this->getMockBuilder( 'DiffOp' ) + $diffOp = $this->getMockBuilder( DiffOp::class ) ->disableOriginalConstructor() ->getMock(); $diffOp->expects( $this->any() ) diff --git a/www/wiki/tests/phpunit/includes/diff/DifferenceEngineTest.php b/www/wiki/tests/phpunit/includes/diff/DifferenceEngineTest.php index 3a8f4db3..57aeb200 100644 --- a/www/wiki/tests/phpunit/includes/diff/DifferenceEngineTest.php +++ b/www/wiki/tests/phpunit/includes/diff/DifferenceEngineTest.php @@ -1,5 +1,7 @@ <?php +use Wikimedia\TestingAccessWrapper; + /** * @covers DifferenceEngine * @@ -117,4 +119,30 @@ class DifferenceEngineTest extends MediaWikiTestCase { $this->assertEquals( $revs[2], $diffEngine->getNewid(), 'diff get new id' ); } + public function provideLocaliseTitleTooltipsTestData() { + return [ + 'moved paragraph left shoud get new location title' => [ + '<a class="mw-diff-movedpara-left">⚫</a>', + '<a class="mw-diff-movedpara-left" title="(diff-paragraph-moved-tonew)">⚫</a>', + ], + 'moved paragraph right shoud get old location title' => [ + '<a class="mw-diff-movedpara-right">⚫</a>', + '<a class="mw-diff-movedpara-right" title="(diff-paragraph-moved-toold)">⚫</a>', + ], + 'nothing changed when key not hit' => [ + '<a class="mw-diff-movedpara-rightis">⚫</a>', + '<a class="mw-diff-movedpara-rightis">⚫</a>', + ], + ]; + } + + /** + * @dataProvider provideLocaliseTitleTooltipsTestData + */ + public function testAddLocalisedTitleTooltips( $input, $expected ) { + $this->setContentLang( 'qqx' ); + $diffEngine = TestingAccessWrapper::newFromObject( new DifferenceEngine() ); + $this->assertEquals( $expected, $diffEngine->addLocalisedTitleTooltips( $input ) ); + } + } diff --git a/www/wiki/tests/phpunit/includes/editpage/TextboxBuilderTest.php b/www/wiki/tests/phpunit/includes/editpage/TextboxBuilderTest.php new file mode 100644 index 00000000..4195f968 --- /dev/null +++ b/www/wiki/tests/phpunit/includes/editpage/TextboxBuilderTest.php @@ -0,0 +1,210 @@ +<?php +/** + * Copyright (C) 2017 Kunal Mehta <legoktm@member.fsf.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. + * + */ + +namespace MediaWiki\Tests\EditPage; + +use Language; +use MediaWiki\EditPage\TextboxBuilder; +use MediaWikiTestCase; +use Title; +use User; + +/** + * @covers \MediaWiki\EditPage\TextboxBuilder + */ +class TextboxBuilderTest extends MediaWikiTestCase { + + public function provideAddNewLineAtEnd() { + return [ + [ '', '' ], + [ 'foo', "foo\n" ], + ]; + } + + /** + * @dataProvider provideAddNewLineAtEnd + */ + public function testAddNewLineAtEnd( $input, $expected ) { + $builder = new TextboxBuilder(); + $this->assertSame( $expected, $builder->addNewLineAtEnd( $input ) ); + } + + public function testBuildTextboxAttribs() { + $user = new User(); + $user->setOption( 'editfont', 'monospace' ); + + $title = $this->getMockBuilder( Title::class ) + ->disableOriginalConstructor() + ->getMock(); + $title->expects( $this->any() ) + ->method( 'getPageLanguage' ) + ->will( $this->returnValue( Language::factory( 'en' ) ) ); + + $builder = new TextboxBuilder(); + $attribs = $builder->buildTextboxAttribs( + 'mw-textbox1', + [ 'class' => 'foo bar', 'data-foo' => '123', 'rows' => 30 ], + $user, + $title + ); + + $this->assertInternalType( 'array', $attribs ); + // custom attrib showed up + $this->assertArrayHasKey( 'data-foo', $attribs ); + // classes merged properly (string) + $this->assertSame( 'foo bar mw-editfont-monospace', $attribs['class'] ); + // overrides in custom attrib worked + $this->assertSame( 30, $attribs['rows'] ); + $this->assertSame( 'en', $attribs['lang'] ); + + $attribs2 = $builder->buildTextboxAttribs( + 'mw-textbox2', [ 'class' => [ 'foo', 'bar' ] ], $user, $title + ); + // classes merged properly (array) + $this->assertSame( [ 'foo', 'bar', 'mw-editfont-monospace' ], $attribs2['class'] ); + + $attribs3 = $builder->buildTextboxAttribs( + 'mw-textbox3', [], $user, $title + ); + // classes ok when nothing to be merged + $this->assertSame( 'mw-editfont-monospace', $attribs3['class'] ); + } + + public function provideMergeClassesIntoAttributes() { + return [ + [ + [], + [], + [], + ], + [ + [ 'mw-new-classname' ], + [], + [ 'class' => 'mw-new-classname' ], + ], + [ + [], + [ 'title' => 'My Title' ], + [ 'title' => 'My Title' ], + ], + [ + [ 'mw-new-classname' ], + [ 'title' => 'My Title' ], + [ 'title' => 'My Title', 'class' => 'mw-new-classname' ], + ], + [ + [ 'mw-new-classname' ], + [ 'class' => 'mw-existing-classname' ], + [ 'class' => 'mw-existing-classname mw-new-classname' ], + ], + [ + [ 'mw-new-classname', 'mw-existing-classname' ], + [ 'class' => 'mw-existing-classname' ], + [ 'class' => 'mw-existing-classname mw-new-classname' ], + ], + ]; + } + + /** + * @dataProvider provideMergeClassesIntoAttributes + */ + public function testMergeClassesIntoAttributes( $inputClasses, $inputAttributes, $expected ) { + $builder = new TextboxBuilder(); + $this->assertSame( + $expected, + $builder->mergeClassesIntoAttributes( $inputClasses, $inputAttributes ) + ); + } + + public function provideGetTextboxProtectionCSSClasses() { + return [ + [ + [ '' ], + [ 'isProtected' ], + [], + ], + [ + true, + [], + [], + ], + [ + true, + [ 'isProtected' ], + [ 'mw-textarea-protected' ] + ], + [ + true, + [ 'isProtected', 'isSemiProtected' ], + [ 'mw-textarea-sprotected' ], + ], + [ + true, + [ 'isProtected', 'isCascadeProtected' ], + [ 'mw-textarea-protected', 'mw-textarea-cprotected' ], + ], + [ + true, + [ 'isProtected', 'isCascadeProtected', 'isSemiProtected' ], + [ 'mw-textarea-sprotected', 'mw-textarea-cprotected' ], + ], + ]; + } + + /** + * @dataProvider provideGetTextboxProtectionCSSClasses + */ + public function testGetTextboxProtectionCSSClasses( + $restrictionLevels, + $protectionModes, + $expected + ) { + $this->setMwGlobals( [ + // set to trick MWNamespace::getRestrictionLevels + 'wgRestrictionLevels' => $restrictionLevels + ] ); + + $builder = new TextboxBuilder(); + $this->assertSame( $expected, $builder->getTextboxProtectionCSSClasses( + $this->mockProtectedTitle( $protectionModes ) + ) ); + } + + /** + * @return Title + */ + private function mockProtectedTitle( $methodsToReturnTrue ) { + $title = $this->getMockBuilder( Title::class ) + ->disableOriginalConstructor() + ->getMock(); + + $title->expects( $this->any() ) + ->method( 'getNamespace' ) + ->will( $this->returnValue( 1 ) ); + + foreach ( $methodsToReturnTrue as $method ) { + $title->expects( $this->any() ) + ->method( $method ) + ->will( $this->returnValue( true ) ); + } + + return $title; + } +} diff --git a/www/wiki/tests/phpunit/includes/exception/BadTitleErrorTest.php b/www/wiki/tests/phpunit/includes/exception/BadTitleErrorTest.php index e6a18125..b706face 100644 --- a/www/wiki/tests/phpunit/includes/exception/BadTitleErrorTest.php +++ b/www/wiki/tests/phpunit/includes/exception/BadTitleErrorTest.php @@ -10,13 +10,15 @@ class BadTitleErrorTest extends MediaWikiTestCase { try { throw new BadTitleError(); } catch ( BadTitleError $e ) { + ob_start(); $e->report(); - $this->assertTrue( true ); + $text = ob_get_clean(); + $this->assertContains( $e->getText(), $text ); } } private function getMockWgOut() { - $mock = $this->getMockBuilder( 'OutputPage' ) + $mock = $this->getMockBuilder( OutputPage::class ) ->disableOriginalConstructor() ->getMock(); $mock->expects( $this->once() ) diff --git a/www/wiki/tests/phpunit/includes/exception/ErrorPageErrorTest.php b/www/wiki/tests/phpunit/includes/exception/ErrorPageErrorTest.php index e72865f6..49d454e8 100644 --- a/www/wiki/tests/phpunit/includes/exception/ErrorPageErrorTest.php +++ b/www/wiki/tests/phpunit/includes/exception/ErrorPageErrorTest.php @@ -7,7 +7,7 @@ class ErrorPageErrorTest extends MediaWikiTestCase { private function getMockMessage() { - $mockMessage = $this->getMockBuilder( 'Message' ) + $mockMessage = $this->getMockBuilder( Message::class ) ->disableOriginalConstructor() ->getMock(); $mockMessage->expects( $this->once() ) @@ -34,7 +34,7 @@ class ErrorPageErrorTest extends MediaWikiTestCase { $title = 'Foo'; $params = [ 'Baz' ]; - $mock = $this->getMockBuilder( 'OutputPage' ) + $mock = $this->getMockBuilder( OutputPage::class ) ->disableOriginalConstructor() ->getMock(); $mock->expects( $this->once() ) diff --git a/www/wiki/tests/phpunit/includes/exception/MWExceptionTest.php b/www/wiki/tests/phpunit/includes/exception/MWExceptionTest.php index 614a1c98..b1605549 100644 --- a/www/wiki/tests/phpunit/includes/exception/MWExceptionTest.php +++ b/www/wiki/tests/phpunit/includes/exception/MWExceptionTest.php @@ -10,6 +10,7 @@ class MWExceptionTest extends MediaWikiTestCase { /** * @expectedException MWException + * @covers MWException */ public function testMwexceptionThrowing() { throw new MWException(); @@ -43,7 +44,7 @@ class MWExceptionTest extends MediaWikiTestCase { } private function getMockLanguage() { - return $this->getMockBuilder( 'Language' ) + return $this->getMockBuilder( Language::class ) ->disableOriginalConstructor() ->getMock(); } @@ -110,8 +111,8 @@ class MWExceptionTest extends MediaWikiTestCase { public static function provideExceptionClasses() { return [ - [ 'Exception' ], - [ 'MWException' ], + [ Exception::class ], + [ MWException::class ], ]; } @@ -146,7 +147,7 @@ class MWExceptionTest extends MediaWikiTestCase { */ public static function provideJsonSerializedKeys() { $testCases = []; - foreach ( [ 'Exception', 'MWException' ] as $exClass ) { + foreach ( [ Exception::class, MWException::class ] as $exClass ) { $exTests = [ [ 'string', $exClass, 'id' ], [ 'string', $exClass, 'file' ], diff --git a/www/wiki/tests/phpunit/includes/exception/ThrottledErrorTest.php b/www/wiki/tests/phpunit/includes/exception/ThrottledErrorTest.php index 23bb1e86..5214b6d4 100644 --- a/www/wiki/tests/phpunit/includes/exception/ThrottledErrorTest.php +++ b/www/wiki/tests/phpunit/includes/exception/ThrottledErrorTest.php @@ -11,13 +11,15 @@ class ThrottledErrorTest extends MediaWikiTestCase { try { throw new ThrottledError(); } catch ( ThrottledError $e ) { + ob_start(); $e->report(); - $this->assertTrue( true ); + $text = ob_get_clean(); + $this->assertContains( $e->getText(), $text ); } } private function getMockWgOut() { - $mock = $this->getMockBuilder( 'OutputPage' ) + $mock = $this->getMockBuilder( OutputPage::class ) ->disableOriginalConstructor() ->getMock(); $mock->expects( $this->once() ) diff --git a/www/wiki/tests/phpunit/includes/externalstore/ExternalStoreFactoryTest.php b/www/wiki/tests/phpunit/includes/externalstore/ExternalStoreFactoryTest.php new file mode 100644 index 00000000..f7626938 --- /dev/null +++ b/www/wiki/tests/phpunit/includes/externalstore/ExternalStoreFactoryTest.php @@ -0,0 +1,41 @@ +<?php + +/** + * @covers ExternalStoreFactory + */ +class ExternalStoreFactoryTest extends PHPUnit\Framework\TestCase { + + use MediaWikiCoversValidator; + + public function testExternalStoreFactory_noStores() { + $factory = new ExternalStoreFactory( [] ); + $this->assertFalse( $factory->getStoreObject( 'ForTesting' ) ); + $this->assertFalse( $factory->getStoreObject( 'foo' ) ); + } + + public function provideStoreNames() { + yield 'Same case as construction' => [ 'ForTesting' ]; + yield 'All lower case' => [ 'fortesting' ]; + yield 'All upper case' => [ 'FORTESTING' ]; + yield 'Mix of cases' => [ 'FOrTEsTInG' ]; + } + + /** + * @dataProvider provideStoreNames + */ + public function testExternalStoreFactory_someStore_protoMatch( $proto ) { + $factory = new ExternalStoreFactory( [ 'ForTesting' ] ); + $store = $factory->getStoreObject( $proto ); + $this->assertInstanceOf( ExternalStoreForTesting::class, $store ); + } + + /** + * @dataProvider provideStoreNames + */ + public function testExternalStoreFactory_someStore_noProtoMatch( $proto ) { + $factory = new ExternalStoreFactory( [ 'SomeOtherClassName' ] ); + $store = $factory->getStoreObject( $proto ); + $this->assertFalse( $store ); + } + +} diff --git a/www/wiki/tests/phpunit/includes/externalstore/ExternalStoreForTesting.php b/www/wiki/tests/phpunit/includes/externalstore/ExternalStoreForTesting.php new file mode 100644 index 00000000..50f1e523 --- /dev/null +++ b/www/wiki/tests/phpunit/includes/externalstore/ExternalStoreForTesting.php @@ -0,0 +1,46 @@ +<?php + +class ExternalStoreForTesting { + + protected $data = [ + 'cluster1' => [ + '200' => 'Hello', + '300' => [ + 'Hello', 'World', + ], + // gzip string below generated with gzdeflate( 'AAAABBAAA' ) + '12345' => "sttttr\002\022\000", + ], + ]; + + /** + * Fetch data from given URL + * @param string $url An url of the form FOO://cluster/id or FOO://cluster/id/itemid. + * @return mixed + */ + public 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/www/wiki/tests/phpunit/includes/externalstore/ExternalStoreTest.php b/www/wiki/tests/phpunit/includes/externalstore/ExternalStoreTest.php index a365c4de..7ca38749 100644 --- a/www/wiki/tests/phpunit/includes/externalstore/ExternalStoreTest.php +++ b/www/wiki/tests/phpunit/includes/externalstore/ExternalStoreTest.php @@ -1,31 +1,39 @@ <?php -/** - * External Store tests - */ class ExternalStoreTest extends MediaWikiTestCase { /** * @covers ExternalStore::fetchFromURL */ - public function testExternalFetchFromURL() { - $this->setMwGlobals( 'wgExternalStores', false ); + public function testExternalFetchFromURL_noExternalStores() { + $this->setService( + 'ExternalStoreFactory', + new ExternalStoreFactory( [] ) + ); $this->assertFalse( - ExternalStore::fetchFromURL( 'FOO://cluster1/200' ), + ExternalStore::fetchFromURL( 'ForTesting://cluster1/200' ), 'Deny if wgExternalStores is not set to a non-empty array' ); + } - $this->setMwGlobals( 'wgExternalStores', [ 'FOO' ] ); + /** + * @covers ExternalStore::fetchFromURL + */ + public function testExternalFetchFromURL_someExternalStore() { + $this->setService( + 'ExternalStoreFactory', + new ExternalStoreFactory( [ 'ForTesting' ] ) + ); $this->assertEquals( - ExternalStore::fetchFromURL( 'FOO://cluster1/200' ), 'Hello', + ExternalStore::fetchFromURL( 'ForTesting://cluster1/200' ), 'Allow FOO://cluster1/200' ); $this->assertEquals( - ExternalStore::fetchFromURL( 'FOO://cluster1/300/0' ), 'Hello', + ExternalStore::fetchFromURL( 'ForTesting://cluster1/300/0' ), 'Allow FOO://cluster1/300/0' ); # Assertions for r68900 @@ -43,45 +51,3 @@ class ExternalStoreTest extends MediaWikiTestCase { ); } } - -class ExternalStoreFOO { - - protected $data = [ - 'cluster1' => [ - '200' => 'Hello', - '300' => [ - 'Hello', 'World', - ], - ], - ]; - - /** - * Fetch data from given URL - * @param string $url 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/www/wiki/tests/phpunit/includes/filebackend/FileBackendTest.php b/www/wiki/tests/phpunit/includes/filebackend/FileBackendTest.php index ddcf19bd..2cd4ba6d 100644 --- a/www/wiki/tests/phpunit/includes/filebackend/FileBackendTest.php +++ b/www/wiki/tests/phpunit/includes/filebackend/FileBackendTest.php @@ -101,7 +101,7 @@ class FileBackendTest extends MediaWikiTestCase { 'backends' => [ [ 'name' => 'localmultitesting1', - 'class' => 'FSFileBackend', + 'class' => FSFileBackend::class, 'containerPaths' => [ 'unittest-cont1' => "{$tmpDir}/localtestingmulti1-cont1", 'unittest-cont2' => "{$tmpDir}/localtestingmulti1-cont2" ], @@ -109,7 +109,7 @@ class FileBackendTest extends MediaWikiTestCase { ], [ 'name' => 'localmultitesting2', - 'class' => 'FSFileBackend', + 'class' => FSFileBackend::class, 'containerPaths' => [ 'unittest-cont1' => "{$tmpDir}/localtestingmulti2-cont1", 'unittest-cont2' => "{$tmpDir}/localtestingmulti2-cont2" ], @@ -2411,7 +2411,7 @@ class FileBackendTest extends MediaWikiTestCase { $status = Status::newGood(); $sl = $this->backend->getScopedFileLocks( $paths, LockManager::LOCK_EX, $status ); - $this->assertInstanceOf( 'ScopedLock', $sl, + $this->assertInstanceOf( ScopedLock::class, $sl, "Scoped locking of files succeeded ($backendName)." ); $this->assertEquals( [], $status->getErrors(), "Scoped locking of files succeeded ($backendName)." ); @@ -2436,7 +2436,7 @@ class FileBackendTest extends MediaWikiTestCase { $be = TestingAccessWrapper::newFromObject( new MemoryFileBackend( [ 'name' => 'testing', - 'class' => 'MemoryFileBackend', + 'class' => MemoryFileBackend::class, 'wikiId' => 'meow', 'mimeCallback' => $mimeCallback ] @@ -2471,13 +2471,13 @@ class FileBackendTest extends MediaWikiTestCase { 'backends' => [ [ // backend 0 'name' => 'multitesting0', - 'class' => 'MemoryFileBackend', + 'class' => MemoryFileBackend::class, 'isMultiMaster' => false, 'readAffinity' => true ], [ // backend 1 'name' => 'multitesting1', - 'class' => 'MemoryFileBackend', + 'class' => MemoryFileBackend::class, 'isMultiMaster' => true ] ] @@ -2521,12 +2521,12 @@ class FileBackendTest extends MediaWikiTestCase { 'backends' => [ [ // backend 0 'name' => 'multitesting0', - 'class' => 'MemoryFileBackend', + 'class' => MemoryFileBackend::class, 'isMultiMaster' => false ], [ // backend 1 'name' => 'multitesting1', - 'class' => 'MemoryFileBackend', + 'class' => MemoryFileBackend::class, 'isMultiMaster' => true ] ], @@ -2584,9 +2584,9 @@ class FileBackendTest extends MediaWikiTestCase { ] ]; - MediaWiki\suppressWarnings(); + Wikimedia\suppressWarnings(); $actual = $be->sanitizeOpHeaders( $input ); - MediaWiki\restoreWarnings(); + Wikimedia\restoreWarnings(); $this->assertEquals( $expected, $actual, "Header sanitized properly" ); } diff --git a/www/wiki/tests/phpunit/includes/filebackend/SwiftFileBackendTest.php b/www/wiki/tests/phpunit/includes/filebackend/SwiftFileBackendTest.php index 9cd2b100..35eca28f 100644 --- a/www/wiki/tests/phpunit/includes/filebackend/SwiftFileBackendTest.php +++ b/www/wiki/tests/phpunit/includes/filebackend/SwiftFileBackendTest.php @@ -22,7 +22,7 @@ class SwiftFileBackendTest extends MediaWikiTestCase { $this->backend = TestingAccessWrapper::newFromObject( new SwiftFileBackend( [ 'name' => 'local-swift-testing', - 'class' => 'SwiftFileBackend', + 'class' => SwiftFileBackend::class, 'wikiId' => 'unit-testing', 'lockManager' => LockManagerGroup::singleton()->get( 'fsLockManager' ), 'swiftAuthUrl' => 'http://127.0.0.1:8080/auth', // unused @@ -34,6 +34,68 @@ class SwiftFileBackendTest extends MediaWikiTestCase { } /** + * @dataProvider provider_testSanitizeHdrsStrict + */ + public function testSanitizeHdrsStrict( $raw, $sanitized ) { + $hdrs = $this->backend->sanitizeHdrsStrict( [ 'headers' => $raw ] ); + + $this->assertEquals( $hdrs, $sanitized, 'sanitizeHdrsStrict() has expected result' ); + } + + public static function provider_testSanitizeHdrsStrict() { + return [ + [ + [ + 'content-length' => 345, + 'content-type' => 'image+bitmap/jpeg', + 'content-disposition' => 'inline', + 'content-duration' => 35.6363, + 'content-Custom' => 'hello', + 'x-content-custom' => 'hello' + ], + [ + 'content-disposition' => 'inline', + 'content-duration' => 35.6363, + 'content-custom' => 'hello', + 'x-content-custom' => 'hello' + ] + ], + [ + [ + 'content-length' => 345, + 'content-type' => 'image+bitmap/jpeg', + 'content-Disposition' => 'inline; filename=xxx; ' . str_repeat( 'o', 1024 ), + 'content-duration' => 35.6363, + 'content-custom' => 'hello', + 'x-content-custom' => 'hello' + ], + [ + 'content-disposition' => 'inline;filename=xxx', + 'content-duration' => 35.6363, + 'content-custom' => 'hello', + 'x-content-custom' => 'hello' + ] + ], + [ + [ + 'content-length' => 345, + 'content-type' => 'image+bitmap/jpeg', + 'content-disposition' => 'filename=' . str_repeat( 'o', 1024 ) . ';inline', + 'content-duration' => 35.6363, + 'content-custom' => 'hello', + 'x-content-custom' => 'hello' + ], + [ + 'content-disposition' => '', + 'content-duration' => 35.6363, + 'content-custom' => 'hello', + 'x-content-custom' => 'hello' + ] + ] + ]; + } + + /** * @dataProvider provider_testSanitizeHdrs */ public function testSanitizeHdrs( $raw, $sanitized ) { @@ -54,6 +116,7 @@ class SwiftFileBackendTest extends MediaWikiTestCase { 'x-content-custom' => 'hello' ], [ + 'content-type' => 'image+bitmap/jpeg', 'content-disposition' => 'inline', 'content-duration' => 35.6363, 'content-custom' => 'hello', @@ -70,6 +133,7 @@ class SwiftFileBackendTest extends MediaWikiTestCase { 'x-content-custom' => 'hello' ], [ + 'content-type' => 'image+bitmap/jpeg', 'content-disposition' => 'inline;filename=xxx', 'content-duration' => 35.6363, 'content-custom' => 'hello', @@ -86,6 +150,7 @@ class SwiftFileBackendTest extends MediaWikiTestCase { 'x-content-custom' => 'hello' ], [ + 'content-type' => 'image+bitmap/jpeg', 'content-disposition' => '', 'content-duration' => 35.6363, 'content-custom' => 'hello', diff --git a/www/wiki/tests/phpunit/includes/filerepo/FileBackendDBRepoWrapperTest.php b/www/wiki/tests/phpunit/includes/filerepo/FileBackendDBRepoWrapperTest.php index 0d00fbc2..4c9855b0 100644 --- a/www/wiki/tests/phpunit/includes/filerepo/FileBackendDBRepoWrapperTest.php +++ b/www/wiki/tests/phpunit/includes/filerepo/FileBackendDBRepoWrapperTest.php @@ -112,19 +112,19 @@ class FileBackendDBRepoWrapperTest extends MediaWikiTestCase { } protected function getMocks() { - $dbMock = $this->getMockBuilder( 'DatabaseMysqli' ) + $dbMock = $this->getMockBuilder( Wikimedia\Rdbms\DatabaseMysqli::class ) ->disableOriginalClone() ->disableOriginalConstructor() ->getMock(); - $backendMock = $this->getMockBuilder( 'FSFileBackend' ) + $backendMock = $this->getMockBuilder( FSFileBackend::class ) ->setConstructorArgs( [ [ 'name' => $this->backendName, 'wikiId' => wfWikiID() ] ] ) ->getMock(); - $wrapperMock = $this->getMockBuilder( 'FileBackendDBRepoWrapper' ) + $wrapperMock = $this->getMockBuilder( FileBackendDBRepoWrapper::class ) ->setMethods( [ 'getDB' ] ) ->setConstructorArgs( [ [ 'backend' => $backendMock, diff --git a/www/wiki/tests/phpunit/includes/filerepo/FileRepoTest.php b/www/wiki/tests/phpunit/includes/filerepo/FileRepoTest.php index d1e6dc39..0d3e679a 100644 --- a/www/wiki/tests/phpunit/includes/filerepo/FileRepoTest.php +++ b/www/wiki/tests/phpunit/includes/filerepo/FileRepoTest.php @@ -50,6 +50,6 @@ class FileRepoTest extends MediaWikiTestCase { 'containerPaths' => [] ] ) ] ); - $this->assertInstanceOf( 'FileRepo', $f ); + $this->assertInstanceOf( FileRepo::class, $f ); } } diff --git a/www/wiki/tests/phpunit/includes/filerepo/MigrateFileRepoLayoutTest.php b/www/wiki/tests/phpunit/includes/filerepo/MigrateFileRepoLayoutTest.php index 800c2fc7..9beea5b6 100644 --- a/www/wiki/tests/phpunit/includes/filerepo/MigrateFileRepoLayoutTest.php +++ b/www/wiki/tests/phpunit/includes/filerepo/MigrateFileRepoLayoutTest.php @@ -1,5 +1,8 @@ <?php +/** + * @covers MigrateFileRepoLayout + */ class MigrateFileRepoLayoutTest extends MediaWikiTestCase { protected $tmpPrefix; protected $migratorMock; @@ -25,7 +28,7 @@ class MigrateFileRepoLayoutTest extends MediaWikiTestCase { ] ] ); - $dbMock = $this->getMockBuilder( 'DatabaseMysqli' ) + $dbMock = $this->getMockBuilder( Wikimedia\Rdbms\DatabaseMysqli::class ) ->disableOriginalConstructor() ->getMock(); @@ -41,7 +44,7 @@ class MigrateFileRepoLayoutTest extends MediaWikiTestCase { new FakeResultWrapper( [] ) // filearchive ) ); - $repoMock = $this->getMockBuilder( 'LocalRepo' ) + $repoMock = $this->getMockBuilder( LocalRepo::class ) ->setMethods( [ 'getMasterDB' ] ) ->setConstructorArgs( [ [ 'name' => 'migratefilerepolayouttest', @@ -54,7 +57,7 @@ class MigrateFileRepoLayoutTest extends MediaWikiTestCase { ->method( 'getMasterDB' ) ->will( $this->returnValue( $dbMock ) ); - $this->migratorMock = $this->getMockBuilder( 'MigrateFileRepoLayout' ) + $this->migratorMock = $this->getMockBuilder( MigrateFileRepoLayout::class ) ->setMethods( [ 'getRepo' ] )->getMock(); $this->migratorMock ->expects( $this->any() ) diff --git a/www/wiki/tests/phpunit/includes/filerepo/RepoGroupTest.php b/www/wiki/tests/phpunit/includes/filerepo/RepoGroupTest.php index 82ff12e3..5a343f65 100644 --- a/www/wiki/tests/phpunit/includes/filerepo/RepoGroupTest.php +++ b/www/wiki/tests/phpunit/includes/filerepo/RepoGroupTest.php @@ -1,4 +1,8 @@ <?php + +/** + * @covers RepoGroup + */ class RepoGroupTest extends MediaWikiTestCase { function testHasForeignRepoNegative() { @@ -15,7 +19,7 @@ class RepoGroupTest extends MediaWikiTestCase { function testForEachForeignRepo() { $this->setUpForeignRepo(); - $fakeCallback = $this->createMock( 'RepoGroupTestHelper' ); + $fakeCallback = $this->createMock( RepoGroupTestHelper::class ); $fakeCallback->expects( $this->once() )->method( 'callback' ); RepoGroup::singleton()->forEachForeignRepo( [ $fakeCallback, 'callback' ], [ [] ] ); @@ -25,7 +29,7 @@ class RepoGroupTest extends MediaWikiTestCase { $this->setMwGlobals( 'wgForeignFileRepos', [] ); RepoGroup::destroySingleton(); FileBackendGroup::destroySingleton(); - $fakeCallback = $this->createMock( 'RepoGroupTestHelper' ); + $fakeCallback = $this->createMock( RepoGroupTestHelper::class ); $fakeCallback->expects( $this->never() )->method( 'callback' ); RepoGroup::singleton()->forEachForeignRepo( [ $fakeCallback, 'callback' ], [ [] ] ); @@ -34,7 +38,7 @@ class RepoGroupTest extends MediaWikiTestCase { private function setUpForeignRepo() { global $wgUploadDirectory; $this->setMwGlobals( 'wgForeignFileRepos', [ [ - 'class' => 'ForeignAPIRepo', + 'class' => ForeignAPIRepo::class, 'name' => 'wikimediacommons', 'backend' => 'wikimediacommons-backend', 'apibase' => 'https://commons.wikimedia.org/w/api.php', diff --git a/www/wiki/tests/phpunit/includes/filerepo/file/FileTest.php b/www/wiki/tests/phpunit/includes/filerepo/file/FileTest.php index 5b5f1b09..3f4e46b5 100644 --- a/www/wiki/tests/phpunit/includes/filerepo/file/FileTest.php +++ b/www/wiki/tests/phpunit/includes/filerepo/file/FileTest.php @@ -6,6 +6,7 @@ class FileTest extends MediaWikiMediaTestCase { * @param string $filename * @param bool $expected * @dataProvider providerCanAnimate + * @covers File::canAnimateThumbIfAppropriate */ function testCanAnimateThumbIfAppropriate( $filename, $expected ) { $this->setMwGlobals( 'wgMaxAnimatedGifArea', 9000 ); @@ -37,7 +38,7 @@ class FileTest extends MediaWikiMediaTestCase { $this->setMwGlobals( 'wgThumbnailBuckets', $data['buckets'] ); $this->setMwGlobals( 'wgThumbnailMinimumBucketDistance', $data['minimumBucketDistance'] ); - $fileMock = $this->getMockBuilder( 'File' ) + $fileMock = $this->getMockBuilder( File::class ) ->setConstructorArgs( [ 'fileMock', false ] ) ->setMethods( [ 'getWidth' ] ) ->getMockForAbstractClass(); @@ -136,11 +137,11 @@ class FileTest extends MediaWikiMediaTestCase { * @covers File::getThumbnailSource */ public function testGetThumbnailSource( $data ) { - $backendMock = $this->getMockBuilder( 'FSFileBackend' ) + $backendMock = $this->getMockBuilder( FSFileBackend::class ) ->setConstructorArgs( [ [ 'name' => 'backendMock', 'wikiId' => wfWikiID() ] ] ) ->getMock(); - $repoMock = $this->getMockBuilder( 'FileRepo' ) + $repoMock = $this->getMockBuilder( FileRepo::class ) ->setConstructorArgs( [ [ 'name' => 'repoMock', 'backend' => $backendMock ] ] ) ->setMethods( [ 'fileExists', 'getLocalReference' ] ) ->getMock(); @@ -155,13 +156,13 @@ class FileTest extends MediaWikiMediaTestCase { ->method( 'getLocalReference' ) ->will( $this->returnValue( $fsFile ) ); - $handlerMock = $this->getMockBuilder( 'BitmapHandler' ) + $handlerMock = $this->getMockBuilder( BitmapHandler::class ) ->setMethods( [ 'supportsBucketing' ] )->getMock(); $handlerMock->expects( $this->any() ) ->method( 'supportsBucketing' ) ->will( $this->returnValue( $data['supportsBucketing'] ) ); - $fileMock = $this->getMockBuilder( 'File' ) + $fileMock = $this->getMockBuilder( File::class ) ->setConstructorArgs( [ 'fileMock', $repoMock ] ) ->setMethods( [ 'getThumbnailBucket', 'getLocalRefPath', 'getHandler' ] ) ->getMockForAbstractClass(); @@ -247,22 +248,22 @@ class FileTest extends MediaWikiMediaTestCase { public function testGenerateBucketsIfNeeded( $data ) { $this->setMwGlobals( 'wgThumbnailBuckets', $data['buckets'] ); - $backendMock = $this->getMockBuilder( 'FSFileBackend' ) + $backendMock = $this->getMockBuilder( FSFileBackend::class ) ->setConstructorArgs( [ [ 'name' => 'backendMock', 'wikiId' => wfWikiID() ] ] ) ->getMock(); - $repoMock = $this->getMockBuilder( 'FileRepo' ) + $repoMock = $this->getMockBuilder( FileRepo::class ) ->setConstructorArgs( [ [ 'name' => 'repoMock', 'backend' => $backendMock ] ] ) ->setMethods( [ 'fileExists', 'getLocalReference' ] ) ->getMock(); - $fileMock = $this->getMockBuilder( 'File' ) + $fileMock = $this->getMockBuilder( File::class ) ->setConstructorArgs( [ 'fileMock', $repoMock ] ) ->setMethods( [ 'getWidth', 'getBucketThumbPath', 'makeTransformTmpFile', 'generateAndSaveThumb', 'getHandler' ] ) ->getMockForAbstractClass(); - $handlerMock = $this->getMockBuilder( 'JpegHandler' ) + $handlerMock = $this->getMockBuilder( JpegHandler::class ) ->setMethods( [ 'supportsBucketing' ] )->getMock(); $handlerMock->expects( $this->any() ) ->method( 'supportsBucketing' ) @@ -272,7 +273,7 @@ class FileTest extends MediaWikiMediaTestCase { ->method( 'getHandler' ) ->will( $this->returnValue( $handlerMock ) ); - $reflectionMethod = new ReflectionMethod( 'File', 'generateBucketsIfNeeded' ); + $reflectionMethod = new ReflectionMethod( File::class, 'generateBucketsIfNeeded' ); $reflectionMethod->setAccessible( true ); $fileMock->expects( $this->any() ) diff --git a/www/wiki/tests/phpunit/includes/filerepo/file/LocalFileTest.php b/www/wiki/tests/phpunit/includes/filerepo/file/LocalFileTest.php index ffaa2c32..e25e6064 100644 --- a/www/wiki/tests/phpunit/includes/filerepo/file/LocalFileTest.php +++ b/www/wiki/tests/phpunit/includes/filerepo/file/LocalFileTest.php @@ -176,7 +176,7 @@ class LocalFileTest extends MediaWikiTestCase { $file = wfLocalFile( "File:Some_file_that_probably_doesn't exist.png" ); $this->assertThat( $file, - $this->isInstanceOf( 'LocalFile' ), + $this->isInstanceOf( LocalFile::class ), 'wfLocalFile() returns LocalFile for valid Titles' ); } diff --git a/www/wiki/tests/phpunit/includes/htmlform/HTMLAutoCompleteSelectFieldTest.php b/www/wiki/tests/phpunit/includes/htmlform/HTMLAutoCompleteSelectFieldTest.php index 33e3a257..99bea68d 100644 --- a/www/wiki/tests/phpunit/includes/htmlform/HTMLAutoCompleteSelectFieldTest.php +++ b/www/wiki/tests/phpunit/includes/htmlform/HTMLAutoCompleteSelectFieldTest.php @@ -4,7 +4,7 @@ * * @covers HTMLAutoCompleteSelectField */ -class HtmlAutoCompleteSelectFieldTest extends MediaWikiTestCase { +class HTMLAutoCompleteSelectFieldTest extends MediaWikiTestCase { public $options = [ 'Bulgaria' => 'BGR', diff --git a/www/wiki/tests/phpunit/includes/htmlform/HTMLCheckMatrixTest.php b/www/wiki/tests/phpunit/includes/htmlform/HTMLCheckMatrixTest.php index f97716b9..e7922fd2 100644 --- a/www/wiki/tests/phpunit/includes/htmlform/HTMLCheckMatrixTest.php +++ b/www/wiki/tests/phpunit/includes/htmlform/HTMLCheckMatrixTest.php @@ -4,7 +4,7 @@ * Unit tests for the HTMLCheckMatrix * @covers HTMLCheckMatrix */ -class HtmlCheckMatrixTest extends MediaWikiTestCase { +class HTMLCheckMatrixTest extends MediaWikiTestCase { static private $defaultOptions = [ 'rows' => [ 'r1', 'r2' ], 'columns' => [ 'c1', 'c2' ], @@ -15,7 +15,7 @@ class HtmlCheckMatrixTest extends MediaWikiTestCase { try { new HTMLCheckMatrix( [] ); } catch ( MWException $e ) { - $this->assertInstanceOf( 'HTMLFormFieldRequiredOptionsException', $e ); + $this->assertInstanceOf( HTMLFormFieldRequiredOptionsException::class, $e ); return; } diff --git a/www/wiki/tests/phpunit/includes/htmlform/HTMLFormTest.php b/www/wiki/tests/phpunit/includes/htmlform/HTMLFormTest.php index b7e0053c..e20cf942 100644 --- a/www/wiki/tests/phpunit/includes/htmlform/HTMLFormTest.php +++ b/www/wiki/tests/phpunit/includes/htmlform/HTMLFormTest.php @@ -1,21 +1,57 @@ <?php - +/** + * @covers HTMLForm + * + * @license GNU GPL v2+ + * @author Gergő Tisza + * @author Thiemo Mättig + */ class HTMLFormTest extends MediaWikiTestCase { - public function testGetHTML_empty() { + + private function newInstance() { $form = new HTMLForm( [] ); $form->setTitle( Title::newFromText( 'Foo' ) ); + return $form; + } + + public function testGetHTML_empty() { + $form = $this->newInstance(); $form->prepareForm(); $html = $form->getHTML( false ); - $this->assertRegExp( '/<form\b/', $html ); + $this->assertStringStartsWith( '<form ', $html ); } /** * @expectedException LogicException */ public function testGetHTML_noPrepare() { - $form = new HTMLForm( [] ); - $form->setTitle( Title::newFromText( 'Foo' ) ); + $form = $this->newInstance(); $form->getHTML( false ); } + + public function testAutocompleteDefaultsToNull() { + $form = $this->newInstance(); + $this->assertNotContains( 'autocomplete', $form->wrapForm( '' ) ); + } + + public function testAutocompleteWhenSetToNull() { + $form = $this->newInstance(); + $form->setAutocomplete( null ); + $this->assertNotContains( 'autocomplete', $form->wrapForm( '' ) ); + } + + public function testAutocompleteWhenSetToFalse() { + $form = $this->newInstance(); + // Previously false was used instead of null to indicate the attribute should not be set + $form->setAutocomplete( false ); + $this->assertNotContains( 'autocomplete', $form->wrapForm( '' ) ); + } + + public function testAutocompleteWhenSetToOff() { + $form = $this->newInstance(); + $form->setAutocomplete( 'off' ); + $this->assertContains( ' autocomplete="off"', $form->wrapForm( '' ) ); + } + } diff --git a/www/wiki/tests/phpunit/includes/htmlform/HTMLRestrictionsFieldTest.php b/www/wiki/tests/phpunit/includes/htmlform/HTMLRestrictionsFieldTest.php index 9ec4f97f..c4290e1e 100644 --- a/www/wiki/tests/phpunit/includes/htmlform/HTMLRestrictionsFieldTest.php +++ b/www/wiki/tests/phpunit/includes/htmlform/HTMLRestrictionsFieldTest.php @@ -1,6 +1,12 @@ <?php -class HTMLRestrictionsFieldTest extends PHPUnit_Framework_TestCase { +/** + * @covers HTMLRestrictionsField + */ +class HTMLRestrictionsFieldTest extends PHPUnit\Framework\TestCase { + + use MediaWikiCoversValidator; + public function testConstruct() { $field = new HTMLRestrictionsField( [ 'fieldname' => 'restrictions' ] ); $this->assertNotEmpty( $field->getLabel(), 'has a default label' ); diff --git a/www/wiki/tests/phpunit/includes/http/HttpTest.php b/www/wiki/tests/phpunit/includes/http/HttpTest.php index 3693a277..f80d18c6 100644 --- a/www/wiki/tests/phpunit/includes/http/HttpTest.php +++ b/www/wiki/tests/phpunit/includes/http/HttpTest.php @@ -2,6 +2,7 @@ /** * @group Http + * @group small */ class HttpTest extends MediaWikiTestCase { /** @@ -495,8 +496,11 @@ class HttpTest extends MediaWikiTestCase { * where it did not define a cURL constant. T72570 * * @dataProvider provideCurlConstants + * @coversNothing */ public function testCurlConstants( $value ) { + $this->checkPHPExtension( 'curl' ); + $this->assertTrue( defined( $value ), $value . ' not defined' ); } } @@ -507,7 +511,7 @@ class HttpTest extends MediaWikiTestCase { class MWHttpRequestTester extends MWHttpRequest { // function derived from the MWHttpRequest factory function but // returns appropriate tester class here - public static function factory( $url, $options = null, $caller = __METHOD__ ) { + public static function factory( $url, array $options = null, $caller = __METHOD__ ) { if ( !Http::$httpEngine ) { Http::$httpEngine = function_exists( 'curl_init' ) ? 'curl' : 'php'; } elseif ( Http::$httpEngine == 'curl' && !function_exists( 'curl_init' ) ) { diff --git a/www/wiki/tests/phpunit/includes/import/ImportLinkCacheIntegrationTest.php b/www/wiki/tests/phpunit/includes/import/ImportLinkCacheIntegrationTest.php index bdb4831a..1db2215e 100644 --- a/www/wiki/tests/phpunit/includes/import/ImportLinkCacheIntegrationTest.php +++ b/www/wiki/tests/phpunit/includes/import/ImportLinkCacheIntegrationTest.php @@ -5,8 +5,10 @@ use MediaWiki\MediaWikiServices; * Integration test that checks import success and * LinkCache integration. * - * @group medium + * @group large * @group Database + * @covers ImportStreamSource + * @covers ImportReporter * * @author mwjames */ @@ -89,20 +91,11 @@ class ImportLinkCacheIntegrationTest extends MediaWikiTestCase { $reporter->setContext( new RequestContext() ); $reporter->open(); - $exception = false; - try { - $importer->doImport(); - } catch ( Exception $e ) { - $exception = $e; - } + $importer->doImport(); $result = $reporter->close(); - $this->assertFalse( - $exception - ); - $this->assertTrue( $result->isGood() ); diff --git a/www/wiki/tests/phpunit/includes/import/ImportTest.php b/www/wiki/tests/phpunit/includes/import/ImportTest.php index 53d91c65..3b91f5b3 100644 --- a/www/wiki/tests/phpunit/includes/import/ImportTest.php +++ b/www/wiki/tests/phpunit/includes/import/ImportTest.php @@ -37,7 +37,7 @@ class ImportTest extends MediaWikiLangTestCase { } public function getUnknownTagsXML() { - // @codingStandardsIgnoreStart Generic.Files.LineLength + // phpcs:disable Generic.Files.LineLength return [ [ <<< EOF @@ -71,7 +71,7 @@ EOF 'TestImportPage' ] ]; - // @codingStandardsIgnoreEnd + // phpcs:enable } /** @@ -102,7 +102,7 @@ EOF } public function getRedirectXML() { - // @codingStandardsIgnoreStart Generic.Files.LineLength + // phpcs:disable Generic.Files.LineLength return [ [ <<< EOF @@ -157,7 +157,7 @@ EOF null ], ]; - // @codingStandardsIgnoreEnd + // phpcs:enable } /** @@ -185,7 +185,7 @@ EOF } public function getSiteInfoXML() { - // @codingStandardsIgnoreStart Generic.Files.LineLength + // phpcs:disable Generic.Files.LineLength return [ [ <<< EOF @@ -217,7 +217,113 @@ EOF ] ], ]; - // @codingStandardsIgnoreEnd + // phpcs:enable + } + + /** + * @dataProvider provideUnknownUserHandling + * @param bool $assign + * @param bool $create + */ + public function testUnknownUserHandling( $assign, $create ) { + $hookId = -99; + $this->setMwGlobals( 'wgHooks', [ + 'ImportHandleUnknownUser' => [ function ( $name ) use ( $assign, $create, &$hookId ) { + if ( !$assign ) { + $this->fail( 'ImportHandleUnknownUser was called unexpectedly' ); + } + + $this->assertEquals( 'UserDoesNotExist', $name ); + if ( $create ) { + $user = User::createNew( $name ); + $this->assertNotNull( $user ); + $hookId = $user->getId(); + return false; + } + return true; + } ] + ] ); + + $user = $this->getTestUser()->getUser(); + + $n = ( $assign ? 1 : 0 ) + ( $create ? 2 : 0 ); + + // phpcs:disable Generic.Files.LineLength + $source = $this->getDataSource( <<<EOF +<mediawiki xmlns="http://www.mediawiki.org/xml/export-0.10/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.mediawiki.org/xml/export-0.10/ http://www.mediawiki.org/xml/export-0.10.xsd" version="0.10" xml:lang="en"> + <page> + <title>TestImportPage</title> + <ns>0</ns> + <id>14</id> + <revision> + <id>15</id> + <timestamp>2016-01-01T0$n:00:00Z</timestamp> + <contributor> + <username>UserDoesNotExist</username> + <id>1</id> + </contributor> + <model>wikitext</model> + <format>text/x-wiki</format> + <text xml:space="preserve" bytes="3">foo</text> + <sha1>1e6gpc3ehk0mu2jqu8cg42g009s796b</sha1> + </revision> + <revision> + <id>16</id> + <timestamp>2016-01-01T0$n:00:01Z</timestamp> + <contributor> + <username>{$user->getName()}</username> + <id>{$user->getId()}</id> + </contributor> + <model>wikitext</model> + <format>text/x-wiki</format> + <text xml:space="preserve" bytes="3">bar</text> + <sha1>bjhlo6dxh5wivnszm93u4b78fheiy4t</sha1> + </revision> + </page> +</mediawiki> +EOF + ); + // phpcs:enable + + $importer = new WikiImporter( $source, MediaWikiServices::getInstance()->getMainConfig() ); + $importer->setUsernamePrefix( 'Xxx', $assign ); + $importer->doImport(); + + $db = wfGetDB( DB_MASTER ); + $revQuery = Revision::getQueryInfo(); + + $row = $db->selectRow( + $revQuery['tables'], + $revQuery['fields'], + [ 'rev_timestamp' => $db->timestamp( "201601010{$n}0000" ) ], + __METHOD__, + [], + $revQuery['joins'] + ); + $this->assertSame( + $assign && $create ? 'UserDoesNotExist' : 'Xxx>UserDoesNotExist', + $row->rev_user_text + ); + $this->assertSame( $assign && $create ? $hookId : 0, (int)$row->rev_user ); + + $row = $db->selectRow( + $revQuery['tables'], + $revQuery['fields'], + [ 'rev_timestamp' => $db->timestamp( "201601010{$n}0001" ) ], + __METHOD__, + [], + $revQuery['joins'] + ); + $this->assertSame( ( $assign ? '' : 'Xxx>' ) . $user->getName(), $row->rev_user_text ); + $this->assertSame( $assign ? $user->getId() : 0, (int)$row->rev_user ); + } + + public static function provideUnknownUserHandling() { + return [ + 'no assign' => [ false, false ], + 'assign, no create' => [ true, false ], + 'assign, create' => [ true, true ], + ]; } } diff --git a/www/wiki/tests/phpunit/includes/installer/DatabaseUpdaterTest.php b/www/wiki/tests/phpunit/includes/installer/DatabaseUpdaterTest.php deleted file mode 100644 index 5e5e921a..00000000 --- a/www/wiki/tests/phpunit/includes/installer/DatabaseUpdaterTest.php +++ /dev/null @@ -1,279 +0,0 @@ -<?php - -class DatabaseUpdaterTest extends MediaWikiTestCase { - - public function testSetAppliedUpdates() { - $db = new FakeDatabase(); - $dbu = new FakeDatabaseUpdater( $db ); - $dbu->setAppliedUpdates( "test", [] ); - $expected = "updatelist-test-" . time() . "0"; - $actual = $db->lastInsertData['ul_key']; - $this->assertEquals( $expected, $actual, var_export( $db->lastInsertData, true ) ); - $dbu->setAppliedUpdates( "test", [] ); - $expected = "updatelist-test-" . time() . "1"; - $actual = $db->lastInsertData['ul_key']; - $this->assertEquals( $expected, $actual, var_export( $db->lastInsertData, true ) ); - } -} - -class FakeDatabase extends DatabaseBase { - public $lastInsertTable; - public $lastInsertData; - - function __construct() { - } - - function clearFlag( $arg ) { - } - - function setFlag( $arg ) { - } - - public function insert( $table, $a, $fname = __METHOD__, $options = [] ) { - $this->lastInsertTable = $table; - $this->lastInsertData = $a; - } - - /** - * Get the type of the DBMS, as it appears in $wgDBtype. - * - * @return string - */ - function getType() { - // TODO: Implement getType() method. - } - - /** - * Open a connection to the database. Usually aborts on failure - * - * @param string $server Database server host - * @param string $user Database user name - * @param string $password Database user password - * @param string $dbName Database name - * @return bool - * @throws DBConnectionError - */ - function open( $server, $user, $password, $dbName ) { - // TODO: Implement open() method. - } - - /** - * Fetch the next row from the given result object, in object form. - * Fields can be retrieved with $row->fieldname, with fields acting like - * member variables. - * If no more rows are available, false is returned. - * - * @param ResultWrapper|stdClass $res Object as returned from DatabaseBase::query(), etc. - * @return stdClass|bool - * @throws DBUnexpectedError Thrown if the database returns an error - */ - function fetchObject( $res ) { - // TODO: Implement fetchObject() method. - } - - /** - * Fetch the next row from the given result object, in associative array - * form. Fields are retrieved with $row['fieldname']. - * If no more rows are available, false is returned. - * - * @param ResultWrapper $res Result object as returned from DatabaseBase::query(), etc. - * @return array|bool - * @throws DBUnexpectedError Thrown if the database returns an error - */ - function fetchRow( $res ) { - // TODO: Implement fetchRow() method. - } - - /** - * Get the number of rows in a result object - * - * @param mixed $res A SQL result - * @return int - */ - function numRows( $res ) { - // TODO: Implement numRows() method. - } - - /** - * Get the number of fields in a result object - * @see http://www.php.net/mysql_num_fields - * - * @param mixed $res A SQL result - * @return int - */ - function numFields( $res ) { - // TODO: Implement numFields() method. - } - - /** - * Get a field name in a result object - * @see http://www.php.net/mysql_field_name - * - * @param mixed $res A SQL result - * @param int $n - * @return string - */ - function fieldName( $res, $n ) { - // TODO: Implement fieldName() method. - } - - /** - * Get the inserted value of an auto-increment row - * - * The value inserted should be fetched from nextSequenceValue() - * - * Example: - * $id = $dbw->nextSequenceValue( 'page_page_id_seq' ); - * $dbw->insert( 'page', array( 'page_id' => $id ) ); - * $id = $dbw->insertId(); - * - * @return int - */ - function insertId() { - // TODO: Implement insertId() method. - } - - /** - * Change the position of the cursor in a result object - * @see http://www.php.net/mysql_data_seek - * - * @param mixed $res A SQL result - * @param int $row - */ - function dataSeek( $res, $row ) { - // TODO: Implement dataSeek() method. - } - - /** - * Get the last error number - * @see http://www.php.net/mysql_errno - * - * @return int - */ - function lastErrno() { - // TODO: Implement lastErrno() method. - } - - /** - * Get a description of the last error - * @see http://www.php.net/mysql_error - * - * @return string - */ - function lastError() { - // TODO: Implement lastError() method. - } - - /** - * mysql_fetch_field() wrapper - * Returns false if the field doesn't exist - * - * @param string $table Table name - * @param string $field Field name - * - * @return Field - */ - function fieldInfo( $table, $field ) { - // TODO: Implement fieldInfo() method. - } - - /** - * Get information about an index into an object - * @param string $table Table name - * @param string $index Index name - * @param string $fname Calling function name - * @return mixed Database-specific index description class or false if the index does not exist - */ - function indexInfo( $table, $index, $fname = __METHOD__ ) { - // TODO: Implement indexInfo() method. - } - - /** - * Get the number of rows affected by the last write query - * @see http://www.php.net/mysql_affected_rows - * - * @return int - */ - function affectedRows() { - // TODO: Implement affectedRows() method. - } - - /** - * Wrapper for addslashes() - * - * @param string $s String to be slashed. - * @return string Slashed string. - */ - function strencode( $s ) { - // TODO: Implement strencode() method. - } - - /** - * Returns a wikitext link to the DB's website, e.g., - * return "[http://www.mysql.com/ MySQL]"; - * Should at least contain plain text, if for some reason - * your database has no website. - * - * @return string Wikitext of a link to the server software's web site - */ - function getSoftwareLink() { - // TODO: Implement getSoftwareLink() method. - } - - /** - * A string describing the current software version, like from - * mysql_get_server_info(). - * - * @return string Version information from the database server. - */ - function getServerVersion() { - // TODO: Implement getServerVersion() method. - } - - /** - * Closes underlying database connection - * @since 1.20 - * @return bool Whether connection was closed successfully - */ - protected function closeConnection() { - // TODO: Implement closeConnection() method. - } - - /** - * The DBMS-dependent part of query() - * - * @param string $sql SQL query. - * @return ResultWrapper|bool Result object to feed to fetchObject, - * fetchRow, ...; or false on failure - */ - protected function doQuery( $sql ) { - // TODO: Implement doQuery() method. - } -} - -class FakeDatabaseUpdater extends DatabaseUpdater { - function __construct( $db ) { - $this->db = $db; - self::$updateCounter = 0; - } - - /** - * Get an array of updates to perform on the database. Should return a - * multi-dimensional array. The main key is the MediaWiki version (1.12, - * 1.13...) with the values being arrays of updates, identical to how - * updaters.inc did it (for now) - * - * @return array - */ - protected function getCoreUpdateList() { - return []; - } - - public function canUseNewUpdatelog() { - return true; - } - - public function setAppliedUpdates( $version, $updates = [] ) { - parent::setAppliedUpdates( $version, $updates ); - } -} diff --git a/www/wiki/tests/phpunit/includes/installer/InstallDocFormatterTest.php b/www/wiki/tests/phpunit/includes/installer/InstallDocFormatterTest.php index 36b6d64f..9584d4b8 100644 --- a/www/wiki/tests/phpunit/includes/installer/InstallDocFormatterTest.php +++ b/www/wiki/tests/phpunit/includes/installer/InstallDocFormatterTest.php @@ -1,12 +1,8 @@ <?php -/* - * To change this template, choose Tools | Templates - * and open the template in the editor. - */ class InstallDocFormatterTest extends MediaWikiTestCase { /** - * @covers InstallDocFormatter::format + * @covers InstallDocFormatter * @dataProvider provideDocFormattingTests */ public function testFormat( $expected, $unformattedText, $message = '' ) { diff --git a/www/wiki/tests/phpunit/includes/installer/OracleInstallerTest.php b/www/wiki/tests/phpunit/includes/installer/OracleInstallerTest.php index bd1412eb..2811a9cf 100644 --- a/www/wiki/tests/phpunit/includes/installer/OracleInstallerTest.php +++ b/www/wiki/tests/phpunit/includes/installer/OracleInstallerTest.php @@ -6,7 +6,6 @@ * @group Database * @group Installer */ - class OracleInstallerTest extends MediaWikiTestCase { /** diff --git a/www/wiki/tests/phpunit/includes/interwiki/ClassicInterwikiLookupTest.php b/www/wiki/tests/phpunit/includes/interwiki/ClassicInterwikiLookupTest.php index fd3b0b85..7fb2cd49 100644 --- a/www/wiki/tests/phpunit/includes/interwiki/ClassicInterwikiLookupTest.php +++ b/www/wiki/tests/phpunit/includes/interwiki/ClassicInterwikiLookupTest.php @@ -68,7 +68,7 @@ class ClassicInterwikiLookupTest extends MediaWikiTestCase { $this->assertFalse( $lookup->fetch( 'xyz' ), 'unknown prefix' ); $interwiki = $lookup->fetch( 'de' ); - $this->assertInstanceOf( 'Interwiki', $interwiki ); + $this->assertInstanceOf( Interwiki::class, $interwiki ); $this->assertSame( $interwiki, $lookup->fetch( 'de' ), 'in-process caching' ); $this->assertSame( 'http://de.wikipedia.org/wiki/', $interwiki->getURL(), 'getURL' ); @@ -169,13 +169,13 @@ class ClassicInterwikiLookupTest extends MediaWikiTestCase { $this->assertTrue( $lookup->isValidInterwiki( 'zz' ), 'known prefix is valid' ); $interwiki = $lookup->fetch( 'de' ); - $this->assertInstanceOf( 'Interwiki', $interwiki ); + $this->assertInstanceOf( Interwiki::class, $interwiki ); $this->assertSame( 'http://de.wikipedia.org/wiki/', $interwiki->getURL(), 'getURL' ); $this->assertSame( true, $interwiki->isLocal(), 'isLocal' ); $interwiki = $lookup->fetch( 'zz' ); - $this->assertInstanceOf( 'Interwiki', $interwiki ); + $this->assertInstanceOf( Interwiki::class, $interwiki ); $this->assertSame( 'http://zzwiki.org/wiki/', $interwiki->getURL(), 'getURL' ); $this->assertSame( false, $interwiki->isLocal(), 'isLocal' ); @@ -220,13 +220,13 @@ class ClassicInterwikiLookupTest extends MediaWikiTestCase { $this->assertTrue( $lookup->isValidInterwiki( 'zz' ), 'known prefix is valid' ); $interwiki = $lookup->fetch( 'de' ); - $this->assertInstanceOf( 'Interwiki', $interwiki ); + $this->assertInstanceOf( Interwiki::class, $interwiki ); $this->assertSame( 'http://de.wikipedia.org/wiki/', $interwiki->getURL(), 'getURL' ); $this->assertSame( true, $interwiki->isLocal(), 'isLocal' ); $interwiki = $lookup->fetch( 'zz' ); - $this->assertInstanceOf( 'Interwiki', $interwiki ); + $this->assertInstanceOf( Interwiki::class, $interwiki ); $this->assertSame( 'http://zzwiki.org/wiki/', $interwiki->getURL(), 'getURL' ); $this->assertSame( false, $interwiki->isLocal(), 'isLocal' ); diff --git a/www/wiki/tests/phpunit/includes/interwiki/InterwikiLookupAdapterTest.php b/www/wiki/tests/phpunit/includes/interwiki/InterwikiLookupAdapterTest.php index 31c9e50c..0a13de1d 100644 --- a/www/wiki/tests/phpunit/includes/interwiki/InterwikiLookupAdapterTest.php +++ b/www/wiki/tests/phpunit/includes/interwiki/InterwikiLookupAdapterTest.php @@ -1,13 +1,13 @@ <?php +use MediaWiki\Interwiki\InterwikiLookupAdapter; + /** * @covers MediaWiki\Interwiki\InterwikiLookupAdapter * * @group MediaWiki * @group Interwiki */ -use MediaWiki\Interwiki\InterwikiLookupAdapter; - class InterwikiLookupAdapterTest extends MediaWikiTestCase { /** diff --git a/www/wiki/tests/phpunit/includes/interwiki/InterwikiTest.php b/www/wiki/tests/phpunit/includes/interwiki/InterwikiTest.php index 22b1304b..0d41c520 100644 --- a/www/wiki/tests/phpunit/includes/interwiki/InterwikiTest.php +++ b/www/wiki/tests/phpunit/includes/interwiki/InterwikiTest.php @@ -106,7 +106,7 @@ class InterwikiTest extends MediaWikiTestCase { $this->assertFalse( $interwikiLookup->fetch( 'xyz' ), 'unknown prefix' ); $interwiki = $interwikiLookup->fetch( 'de' ); - $this->assertInstanceOf( 'Interwiki', $interwiki ); + $this->assertInstanceOf( Interwiki::class, $interwiki ); $this->assertSame( $interwiki, $interwikiLookup->fetch( 'de' ), 'in-process caching' ); $this->assertSame( 'http://de.wikipedia.org/wiki/', $interwiki->getURL(), 'getURL' ); diff --git a/www/wiki/tests/phpunit/includes/jobqueue/JobQueueMemoryTest.php b/www/wiki/tests/phpunit/includes/jobqueue/JobQueueMemoryTest.php index 4b03fda7..bf8603dd 100644 --- a/www/wiki/tests/phpunit/includes/jobqueue/JobQueueMemoryTest.php +++ b/www/wiki/tests/phpunit/includes/jobqueue/JobQueueMemoryTest.php @@ -5,17 +5,19 @@ * * @group JobQueue * - * @licence GNU GPL v2+ - * @author Thiemo Mättig + * @license GNU GPL v2+ + * @author Thiemo Kreuz */ -class JobQueueMemoryTest extends PHPUnit_Framework_TestCase { +class JobQueueMemoryTest extends PHPUnit\Framework\TestCase { + + use MediaWikiCoversValidator; /** * @return JobQueueMemory */ private function newJobQueue() { return JobQueue::factory( [ - 'class' => 'JobQueueMemory', + 'class' => JobQueueMemory::class, 'wiki' => wfWikiID(), 'type' => 'null', ] ); @@ -52,7 +54,7 @@ class JobQueueMemoryTest extends PHPUnit_Framework_TestCase { public function testJobFromSpecInternal() { $queue = $this->newJobQueue(); $job = $queue->jobFromSpecInternal( $this->newJobSpecification() ); - $this->assertInstanceOf( 'Job', $job ); + $this->assertInstanceOf( Job::class, $job ); $this->assertSame( 'null', $job->getType() ); $this->assertArrayHasKey( 'customParameter', $job->getParams() ); $this->assertSame( 'Custom title', $job->getTitle()->getText() ); diff --git a/www/wiki/tests/phpunit/includes/jobqueue/JobQueueTest.php b/www/wiki/tests/phpunit/includes/jobqueue/JobQueueTest.php index 7b34b59b..64dde778 100644 --- a/www/wiki/tests/phpunit/includes/jobqueue/JobQueueTest.php +++ b/www/wiki/tests/phpunit/includes/jobqueue/JobQueueTest.php @@ -1,5 +1,7 @@ <?php +use MediaWiki\MediaWikiServices; + /** * @group JobQueue * @group medium @@ -26,7 +28,7 @@ class JobQueueTest extends MediaWikiTestCase { } $baseConfig = $wgJobTypeConf[$name]; } else { - $baseConfig = [ 'class' => 'JobQueueDB' ]; + $baseConfig = [ 'class' => JobQueueDBSingle::class ]; } $baseConfig['type'] = 'null'; $baseConfig['wiki'] = wfWikiID(); @@ -232,7 +234,7 @@ class JobQueueTest extends MediaWikiTestCase { $j = $queue->pop(); // Make sure ack() of the twin did not delete the sibling data - $this->assertType( 'NullJob', $j ); + $this->assertType( NullJob::class, $j ); } /** @@ -381,3 +383,11 @@ class JobQueueTest extends MediaWikiTestCase { [ 'lives' => 0, 'usleep' => 0, 'removeDuplicates' => 1, 'i' => $i ] + $rootJob ); } } + +class JobQueueDBSingle extends JobQueueDB { + protected function getDB( $index ) { + $lb = MediaWikiServices::getInstance()->getDBLoadBalancer(); + // Override to not use CONN_TRX_AUTOCOMMIT so that we see the same temporary `job` table + return $lb->getConnection( $index, [], $this->wiki ); + } +} diff --git a/www/wiki/tests/phpunit/includes/jobqueue/JobTest.php b/www/wiki/tests/phpunit/includes/jobqueue/JobTest.php index 6723a0bf..0cab7024 100644 --- a/www/wiki/tests/phpunit/includes/jobqueue/JobTest.php +++ b/www/wiki/tests/phpunit/includes/jobqueue/JobTest.php @@ -18,7 +18,7 @@ class JobTest extends MediaWikiTestCase { } public function provideTestToString() { - $mockToStringObj = $this->getMockBuilder( 'stdClass' ) + $mockToStringObj = $this->getMockBuilder( stdClass::class ) ->setMethods( [ '__toString' ] )->getMock(); $mockToStringObj->expects( $this->any() ) ->method( '__toString' ) @@ -75,8 +75,10 @@ class JobTest extends MediaWikiTestCase { 'someCommand pages={"932737":[0,"Robert_James_Waller"]} ' . 'rootJobSignature=45868e99bba89064e4483743ebb9b682ef95c1a7 ' . 'rootJobTimestamp=20160309110158 masterPos=' . - '{"file":"db1023-bin.001288","pos":"308257743","asOfTime":1457521464.3814} ' . - 'triggeredRecursive=1 ' . + '{"file":"db1023-bin.001288","pos":"308257743","asOfTime":' . + // Embed dynamically because TestSetup sets serialize_precision=17 + // which, in PHP 7.1 and 7.2, produces 1457521464.3814001 instead + json_encode( 1457521464.3814 ) . '} ' . 'triggeredRecursive=1 ' . $requestId ], ]; @@ -84,7 +86,7 @@ class JobTest extends MediaWikiTestCase { public function getMockJob( $params ) { $mock = $this->getMockForAbstractClass( - 'Job', + Job::class, [ 'someCommand', new Title(), $params ], 'SomeJob' ); @@ -99,7 +101,7 @@ class JobTest extends MediaWikiTestCase { * @covers Job::factory */ public function testJobFactory( $handler ) { - $this->mergeMWGlobalArrayValue( 'wgJobClasses', [ 'testdummy' => $handler ] ); + $this->mergeMwGlobalArrayValue( 'wgJobClasses', [ 'testdummy' => $handler ] ); $job = Job::factory( 'testdummy', Title::newMainPage(), [] ); $this->assertInstanceOf( NullJob::class, $job ); diff --git a/www/wiki/tests/phpunit/includes/jobqueue/RefreshLinksPartitionTest.php b/www/wiki/tests/phpunit/includes/jobqueue/RefreshLinksPartitionTest.php index 43d626db..f874f6de 100644 --- a/www/wiki/tests/phpunit/includes/jobqueue/RefreshLinksPartitionTest.php +++ b/www/wiki/tests/phpunit/includes/jobqueue/RefreshLinksPartitionTest.php @@ -16,6 +16,7 @@ class RefreshLinksPartitionTest extends MediaWikiTestCase { /** * @dataProvider provider_backlinks + * @covers BacklinkJobUtils::partitionBacklinkJob */ public function testRefreshLinks( $ns, $dbKey, $pages ) { $title = Title::makeTitle( $ns, $dbKey ); diff --git a/www/wiki/tests/phpunit/includes/jobqueue/jobs/CategoryMembershipChangeJobTest.php b/www/wiki/tests/phpunit/includes/jobqueue/jobs/CategoryMembershipChangeJobTest.php index 656be381..5960a16b 100644 --- a/www/wiki/tests/phpunit/includes/jobqueue/jobs/CategoryMembershipChangeJobTest.php +++ b/www/wiki/tests/phpunit/includes/jobqueue/jobs/CategoryMembershipChangeJobTest.php @@ -6,7 +6,7 @@ * @group JobQueue * @group Database * - * @licence GNU GPL v2+ + * @license GNU GPL v2+ * @author Addshore */ class CategoryMembershipChangeJobTest extends MediaWikiTestCase { diff --git a/www/wiki/tests/phpunit/includes/jobqueue/jobs/ClearUserWatchlistJobTest.php b/www/wiki/tests/phpunit/includes/jobqueue/jobs/ClearUserWatchlistJobTest.php new file mode 100644 index 00000000..6ae7d605 --- /dev/null +++ b/www/wiki/tests/phpunit/includes/jobqueue/jobs/ClearUserWatchlistJobTest.php @@ -0,0 +1,79 @@ +<?php +use MediaWiki\MediaWikiServices; + +/** + * @covers ClearUserWatchlistJob + * + * @group JobQueue + * @group Database + * + * @license GNU GPL v2+ + * @author Addshore + */ +class ClearUserWatchlistJobTest extends MediaWikiTestCase { + + public function setUp() { + parent::setUp(); + self::$users['ClearUserWatchlistJobTestUser'] + = new TestUser( 'ClearUserWatchlistJobTestUser' ); + $this->runJobs(); + JobQueueGroup::destroySingletons(); + } + + private function getUser() { + return self::$users['ClearUserWatchlistJobTestUser']->getUser(); + } + + private function runJobs( $jobLimit = 9999 ) { + $runJobs = new RunJobs; + $runJobs->loadParamsAndArgs( null, [ 'quiet' => true, 'maxjobs' => $jobLimit ] ); + $runJobs->execute(); + } + + private function getWatchedItemStore() { + return MediaWikiServices::getInstance()->getWatchedItemStore(); + } + + public function testRun() { + $user = $this->getUser(); + $watchedItemStore = $this->getWatchedItemStore(); + + $watchedItemStore->addWatch( $user, new TitleValue( 0, 'A' ) ); + $watchedItemStore->addWatch( $user, new TitleValue( 1, 'A' ) ); + $watchedItemStore->addWatch( $user, new TitleValue( 0, 'B' ) ); + $watchedItemStore->addWatch( $user, new TitleValue( 1, 'B' ) ); + + $maxId = $watchedItemStore->getMaxId(); + + $watchedItemStore->addWatch( $user, new TitleValue( 0, 'C' ) ); + $watchedItemStore->addWatch( $user, new TitleValue( 1, 'C' ) ); + + $this->setMwGlobals( 'wgUpdateRowsPerQuery', 2 ); + + JobQueueGroup::singleton()->push( + new ClearUserWatchlistJob( + null, + [ + 'userId' => $user->getId(), + 'maxWatchlistId' => $maxId, + ] + ) + ); + + $this->assertEquals( 1, JobQueueGroup::singleton()->getQueueSizes()['clearUserWatchlist'] ); + $this->assertEquals( 6, $watchedItemStore->countWatchedItems( $user ) ); + $this->runJobs( 1 ); + $this->assertEquals( 1, JobQueueGroup::singleton()->getQueueSizes()['clearUserWatchlist'] ); + $this->assertEquals( 4, $watchedItemStore->countWatchedItems( $user ) ); + $this->runJobs( 1 ); + $this->assertEquals( 1, JobQueueGroup::singleton()->getQueueSizes()['clearUserWatchlist'] ); + $this->assertEquals( 2, $watchedItemStore->countWatchedItems( $user ) ); + $this->runJobs( 1 ); + $this->assertEquals( 0, JobQueueGroup::singleton()->getQueueSizes()['clearUserWatchlist'] ); + $this->assertEquals( 2, $watchedItemStore->countWatchedItems( $user ) ); + + $this->assertTrue( $watchedItemStore->isWatched( $user, new TitleValue( 0, 'C' ) ) ); + $this->assertTrue( $watchedItemStore->isWatched( $user, new TitleValue( 1, 'C' ) ) ); + } + +} diff --git a/www/wiki/tests/phpunit/includes/json/FormatJsonTest.php b/www/wiki/tests/phpunit/includes/json/FormatJsonTest.php index d252c807..a4ab879f 100644 --- a/www/wiki/tests/phpunit/includes/json/FormatJsonTest.php +++ b/www/wiki/tests/phpunit/includes/json/FormatJsonTest.php @@ -159,12 +159,12 @@ class FormatJsonTest extends MediaWikiTestCase { $this->assertJson( $json ); $st = FormatJson::parse( $json ); - $this->assertInstanceOf( 'Status', $st ); + $this->assertInstanceOf( Status::class, $st ); $this->assertTrue( $st->isGood() ); $this->assertEquals( $expected, $st->getValue() ); $st = FormatJson::parse( $json, FormatJson::FORCE_ASSOC ); - $this->assertInstanceOf( 'Status', $st ); + $this->assertInstanceOf( Status::class, $st ); $this->assertTrue( $st->isGood() ); $this->assertEquals( $value, $st->getValue() ); } @@ -230,7 +230,7 @@ class FormatJsonTest extends MediaWikiTestCase { } $st = FormatJson::parse( $value, FormatJson::TRY_FIXING ); - $this->assertInstanceOf( 'Status', $st ); + $this->assertInstanceOf( Status::class, $st ); if ( $expected === false ) { $this->assertFalse( $st->isOK(), 'Expected isOK() == false' ); } else { @@ -256,7 +256,7 @@ class FormatJsonTest extends MediaWikiTestCase { */ public function testParseErrors( $value ) { $st = FormatJson::parse( $value ); - $this->assertInstanceOf( 'Status', $st ); + $this->assertInstanceOf( Status::class, $st ); $this->assertFalse( $st->isOK() ); } @@ -313,7 +313,7 @@ class FormatJsonTest extends MediaWikiTestCase { */ public function testParseStripComments( $json, $expect ) { $st = FormatJson::parse( $json, FormatJson::STRIP_COMMENTS ); - $this->assertInstanceOf( 'Status', $st ); + $this->assertInstanceOf( Status::class, $st ); $this->assertTrue( $st->isGood() ); $this->assertEquals( $expect, $st->getValue() ); } diff --git a/www/wiki/tests/phpunit/includes/libs/ArrayUtilsTest.php b/www/wiki/tests/phpunit/includes/libs/ArrayUtilsTest.php index 21472d55..d5ac77bb 100644 --- a/www/wiki/tests/phpunit/includes/libs/ArrayUtilsTest.php +++ b/www/wiki/tests/phpunit/includes/libs/ArrayUtilsTest.php @@ -4,9 +4,9 @@ * * @group Database */ +class ArrayUtilsTest extends PHPUnit\Framework\TestCase { -class ArrayUtilsTest extends PHPUnit_Framework_TestCase { - private $search; + use MediaWikiCoversValidator; /** * @covers ArrayUtils::findLowerBound diff --git a/www/wiki/tests/phpunit/includes/libs/CSSMinTest.php b/www/wiki/tests/phpunit/includes/libs/CSSMinTest.php index b06df976..46bf2c6c 100644 --- a/www/wiki/tests/phpunit/includes/libs/CSSMinTest.php +++ b/www/wiki/tests/phpunit/includes/libs/CSSMinTest.php @@ -1,9 +1,6 @@ <?php -/** - * This file test the CSSMin library shipped with Mediawiki. - * - * @author Timo Tijhof - */ + +use Wikimedia\TestingAccessWrapper; /** * @group ResourceLoader @@ -14,8 +11,8 @@ class CSSMinTest extends MediaWikiTestCase { protected function setUp() { parent::setUp(); - $server = 'http://doc.example.org'; - + // For wfExpandUrl + $server = 'https://expand.example'; $this->setMwGlobals( [ 'wgServer' => $server, 'wgCanonicalServer' => $server, @@ -23,7 +20,44 @@ class CSSMinTest extends MediaWikiTestCase { } /** - * @dataProvider mimeTypeProvider + * @dataProvider provideSerializeStringValue + * @covers CSSMin::serializeStringValue + */ + public function testSerializeStringValue( $input, $expected ) { + $output = CSSMin::serializeStringValue( $input ); + $this->assertEquals( + $expected, + $output, + 'Serialized output must be in the expected form.' + ); + } + + public static function provideSerializeStringValue() { + return [ + [ 'Hello World!', '"Hello World!"' ], + [ "Null\0Null", "\"Null\\fffd Null\"" ], + [ '"', '"\\""' ], + [ "'", '"\'"' ], + [ "\\", '"\\\\"' ], + [ "Tab\tTab", '"Tab\\9 Tab"' ], + [ "Space tab \t space", '"Space tab \\9 space"' ], + [ "Line\nfeed", '"Line\\a feed"' ], + [ "Return\rreturn", '"Return\\d return"' ], + [ "Next\xc2\x85line", "\"Next\xc2\x85line\"" ], + [ "Del\x7fDel", '"Del\\7f Del"' ], + [ "nb\xc2\xa0sp", "\"nb\xc2\xa0sp\"" ], + [ "AMP&AMP", "\"AMP&AMP\"" ], + [ '!"#$%&\'()*+,-./0123456789:;<=>?', '"!\\"#$%&\'()*+,-./0123456789:;<=>?"' ], + [ '@[\\]^_`{|}~', '"@[\\\\]^_`{|}~"' ], + [ 'ä', '"ä"' ], + [ 'Ä', '"Ä"' ], + [ '€', '"€"' ], + [ '𝒞', '"𝒞"' ], // U+1D49E 'MATHEMATICAL SCRIPT CAPITAL C' + ]; + } + + /** + * @dataProvider provideMimeType * @covers CSSMin::getMimeType */ public function testGetMimeType( $fileContents, $fileExtension, $expected ) { @@ -34,7 +68,7 @@ class CSSMinTest extends MediaWikiTestCase { $this->assertSame( $expected, CSSMin::getMimeType( $fileName ) ); } - public function mimeTypeProvider() { + public static function provideMimeType() { return [ 'JPEG with short extension' => [ "\xFF\xD8\xFF", @@ -149,6 +183,12 @@ class CSSMinTest extends MediaWikiTestCase { [ "foo { content: '\"'; }", "foo{content:'\"'}" ], // - Whitespace in string values [ 'foo { content: " "; }', 'foo{content:" "}' ], + + // Whitespaces after opening and before closing parentheses and brackets + [ 'a:not( [ href ] ) { prop: url( foobar.png ); }', 'a:not([href]){prop:url(foobar.png)}' ], + + // Ensure that the invalid "url (" will not become the valid "url(" by minification + [ 'foo { prop: url ( foobar.png ); }', 'foo{prop:url (foobar.png)}' ], ]; } @@ -171,7 +211,8 @@ class CSSMinTest extends MediaWikiTestCase { * @covers CSSMin::isRemoteUrl */ public function testIsRemoteUrl( $expect, $url ) { - $this->assertEquals( CSSMinTestable::isRemoteUrl( $url ), $expect ); + $class = TestingAccessWrapper::newFromClass( CSSMin::class ); + $this->assertEquals( $class->isRemoteUrl( $url ), $expect ); } public static function provideIsLocalUrls() { @@ -188,7 +229,8 @@ class CSSMinTest extends MediaWikiTestCase { * @covers CSSMin::isLocalUrl */ public function testIsLocalUrl( $expect, $url ) { - $this->assertEquals( CSSMinTestable::isLocalUrl( $url ), $expect ); + $class = TestingAccessWrapper::newFromClass( CSSMin::class ); + $this->assertEquals( $class->isLocalUrl( $url ), $expect ); } /** @@ -237,7 +279,7 @@ class CSSMinTest extends MediaWikiTestCase { [ 'Expand absolute paths', [ '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); }', + 'foo { prop: url(https://expand.example/w/skin/images/bar.png); }', ], [ "Don't barf at behavior: url(#default#behaviorName) - T162973", @@ -248,6 +290,36 @@ class CSSMinTest extends MediaWikiTestCase { } /** + * Cases with empty url() for CSSMin::remap. + * + * Regression test for T191237. + * + * @dataProvider provideRemapEmptyUrl + * @covers CSSMin + */ + public function testRemapEmptyUrl( $params, $expected ) { + $remapped = call_user_func_array( 'CSSMin::remap', $params ); + $this->assertEquals( $expected, $remapped, 'Ignore empty url' ); + } + + public static function provideRemapEmptyUrl() { + return [ + 'Empty' => [ + [ "background-image: url();", false, '/example', false ], + "background-image: url();", + ], + 'Single quote' => [ + [ "background-image: url('');", false, '/example', false ], + "background-image: url('');", + ], + 'Double quote' => [ + [ 'background-image: url("");', false, '/example', false ], + 'background-image: url("");', + ], + ]; + } + + /** * This tests the basic functionality of CSSMin::remap. * * @see testRemap for testing of funky parameters @@ -271,11 +343,12 @@ class CSSMinTest extends MediaWikiTestCase { // data: URIs for red.gif, green.gif, circle.svg $red = 'data:image/gif;base64,R0lGODlhAQABAIAAAP8AADAAACwAAAAAAQABAAACAkQBADs='; $green = 'data:image/gif;base64,R0lGODlhAQABAIAAAACAADAAACwAAAAAAQABAAACAkQBADs='; - $svg = 'data:image/svg+xml,%3C%3Fxml version=%221.0%22 encoding=%22UTF-8%22%3F%3E%0A' - . '%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 width=%228%22 height=' - . '%228%22%3E%0A%09%3Ccircle cx=%224%22 cy=%224%22 r=%222%22/%3E%0A%3C/svg%3E%0A'; + $svg = 'data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 width=%228' + . '%22 height=%228%22 viewBox=%220 0 8 8%22%3E %3Ccircle cx=%224%22 cy=%224%22 ' + . 'r=%222%22/%3E %3Ca xmlns:xlink=%22http://www.w3.org/1999/xlink%22 xlink:title=' + . '%22%3F%3E%22%3Etest%3C/a%3E %3C/svg%3E'; - // @codingStandardsIgnoreStart Generic.Files.LineLength + // phpcs:disable Generic.Files.LineLength return [ [ 'Regular file', @@ -310,12 +383,12 @@ class CSSMinTest extends MediaWikiTestCase { [ 'Domain-relative URL', 'foo { background: url(/static/foo.png); }', - 'foo { background: url(http://doc.example.org/static/foo.png); }', + 'foo { background: url(https://expand.example/static/foo.png); }', ], [ 'Domain-relative URL with query', 'foo { background: url(/static/foo.png?query=yes); }', - 'foo { background: url(http://doc.example.org/static/foo.png?query=yes); }', + 'foo { background: url(https://expand.example/static/foo.png?query=yes); }', ], [ 'Remote URL (unnecessary quotes not preserved)', @@ -419,12 +492,12 @@ class CSSMinTest extends MediaWikiTestCase { [ '@import rule to local file (should we remap this?)', '@import url(/styles.css)', - '@import url(http://doc.example.org/styles.css)', + '@import url(https://expand.example/styles.css)', ], [ '@import rule to local file (should we remap this?)', '@import url(/styles.css)', - '@import url(http://doc.example.org/styles.css)', + '@import url(https://expand.example/styles.css)', ], [ '@import rule to URL', @@ -442,7 +515,7 @@ class CSSMinTest extends MediaWikiTestCase { 'foo { background: url(//localhost/styles.css?quoted=single) }', ], [ - 'Background URL (containing parentheses; T60473)', + 'Background URL (double quoted, containing parentheses; T60473)', 'foo { background: url("//localhost/styles.css?query=(parens)") }', 'foo { background: url("//localhost/styles.css?query=(parens)") }', ], @@ -457,6 +530,11 @@ class CSSMinTest extends MediaWikiTestCase { 'foo { background: url("//localhost/styles.css?quote=\"") }', ], [ + 'Background URL (double quoted with outer spacing)', + 'foo { background: url( "http://localhost/styles.css?quoted=double" ) }', + 'foo { background: url(http://localhost/styles.css?quoted=double) }', + ], + [ 'Simple case with comments before url', 'foo { prop: /* some {funny;} comment */ url(bar.png); }', 'foo { prop: /* some {funny;} comment */ url(http://localhost/w/bar.png); }', @@ -492,7 +570,7 @@ class CSSMinTest extends MediaWikiTestCase { '.ui-state-default, .ui-widget-content .ui-state-default, .ui-widget-header .ui-state-default { border: 1px solid #d3d3d3/*{borderColorDefault}*/; background: #e6e6e6/*{bgColorDefault}*/ url(http://localhost/w/images/ui-bg_glass_75_e6e6e6_1x400.png)/*{bgImgUrlDefault}*/ 50%/*{bgDefaultXPos}*/ 50%/*{bgDefaultYPos}*/ repeat-x/*{bgDefaultRepeat}*/; font-weight: normal/*{fwDefault}*/; color: #555555/*{fcDefault}*/; }', ], ]; - // @codingStandardsIgnoreEnd + // phpcs:enable } /** @@ -560,13 +638,3 @@ class CSSMinTest extends MediaWikiTestCase { ]; } } - -class CSSMinTestable extends CSSMin { - // Make some protected methods public - public static function isRemoteUrl( $maybeUrl ) { - return parent::isRemoteUrl( $maybeUrl ); - } - public static function isLocalUrl( $maybeUrl ) { - return parent::isLocalUrl( $maybeUrl ); - } -} diff --git a/www/wiki/tests/phpunit/includes/libs/DeferredStringifierTest.php b/www/wiki/tests/phpunit/includes/libs/DeferredStringifierTest.php index cba29392..c9cdf583 100644 --- a/www/wiki/tests/phpunit/includes/libs/DeferredStringifierTest.php +++ b/www/wiki/tests/phpunit/includes/libs/DeferredStringifierTest.php @@ -1,13 +1,17 @@ <?php -class DeferredStringifierTest extends PHPUnit_Framework_TestCase { +/** + * @covers DeferredStringifier + */ +class DeferredStringifierTest extends PHPUnit\Framework\TestCase { + + use MediaWikiCoversValidator; /** - * @covers DeferredStringifier * @dataProvider provideToString */ public function testToString( $params, $expected ) { - $class = new ReflectionClass( 'DeferredStringifier' ); + $class = new ReflectionClass( DeferredStringifier::class ); $ds = $class->newInstanceArgs( $params ); $this->assertEquals( $expected, (string)$ds ); } diff --git a/www/wiki/tests/phpunit/includes/libs/DnsSrvDiscovererTest.php b/www/wiki/tests/phpunit/includes/libs/DnsSrvDiscovererTest.php index cfc2d91b..1b3397c1 100644 --- a/www/wiki/tests/phpunit/includes/libs/DnsSrvDiscovererTest.php +++ b/www/wiki/tests/phpunit/includes/libs/DnsSrvDiscovererTest.php @@ -1,8 +1,13 @@ <?php -class DnsSrvDiscovererTest extends PHPUnit_Framework_TestCase { +/** + * @covers DnsSrvDiscoverer + */ +class DnsSrvDiscovererTest extends PHPUnit\Framework\TestCase { + + use MediaWikiCoversValidator; + /** - * @covers DnsSrvDiscoverer * @dataProvider provideRecords */ public function testPickServer( $params, $expected ) { diff --git a/www/wiki/tests/phpunit/includes/libs/GenericArrayObjectTest.php b/www/wiki/tests/phpunit/includes/libs/GenericArrayObjectTest.php index 12c57871..3be2b064 100644 --- a/www/wiki/tests/phpunit/includes/libs/GenericArrayObjectTest.php +++ b/www/wiki/tests/phpunit/includes/libs/GenericArrayObjectTest.php @@ -26,7 +26,9 @@ * * @author Jeroen De Dauw < jeroendedauw@gmail.com > */ -abstract class GenericArrayObjectTest extends PHPUnit_Framework_TestCase { +abstract class GenericArrayObjectTest extends PHPUnit\Framework\TestCase { + + use MediaWikiCoversValidator; /** * Returns objects that can serve as elements in the concrete @@ -171,8 +173,6 @@ abstract class GenericArrayObjectTest extends PHPUnit_Framework_TestCase { * @since 1.20 * * @param callable $function - * - * @covers GenericArrayObject::getObjectType */ protected function checkTypeChecks( $function ) { $excption = null; @@ -204,7 +204,7 @@ abstract class GenericArrayObjectTest extends PHPUnit_Framework_TestCase { * @since 1.20 * * @param array $elements - * + * @covers GenericArrayObject::getObjectType * @covers GenericArrayObject::offsetSet */ public function testOffsetSet( array $elements ) { diff --git a/www/wiki/tests/phpunit/includes/libs/HashRingTest.php b/www/wiki/tests/phpunit/includes/libs/HashRingTest.php index 0822a8ac..ba288281 100644 --- a/www/wiki/tests/phpunit/includes/libs/HashRingTest.php +++ b/www/wiki/tests/phpunit/includes/libs/HashRingTest.php @@ -3,7 +3,10 @@ /** * @group HashRing */ -class HashRingTest extends PHPUnit_Framework_TestCase { +class HashRingTest extends PHPUnit\Framework\TestCase { + + use MediaWikiCoversValidator; + /** * @covers HashRing */ diff --git a/www/wiki/tests/phpunit/includes/libs/HtmlArmorTest.php b/www/wiki/tests/phpunit/includes/libs/HtmlArmorTest.php index 5f176e0c..c5e87e4e 100644 --- a/www/wiki/tests/phpunit/includes/libs/HtmlArmorTest.php +++ b/www/wiki/tests/phpunit/includes/libs/HtmlArmorTest.php @@ -3,9 +3,26 @@ /** * @covers HtmlArmor */ -class HtmlArmorTest extends PHPUnit_Framework_TestCase { +class HtmlArmorTest extends PHPUnit\Framework\TestCase { - public static function provideHtmlArmor() { + use MediaWikiCoversValidator; + + public static function provideConstructor() { + return [ + [ 'test' ], + [ null ], + [ '<em>some html!</em>' ] + ]; + } + + /** + * @dataProvider provideConstructor + */ + public function testConstructor( $value ) { + $this->assertInstanceOf( HtmlArmor::class, new HtmlArmor( $value ) ); + } + + public static function provideGetHtml() { return [ [ 'foobar', @@ -19,13 +36,17 @@ class HtmlArmorTest extends PHPUnit_Framework_TestCase { new HtmlArmor( '<script>alert("evil!");</script>' ), '<script>alert("evil!");</script>', ], + [ + new HtmlArmor( null ), + null, + ] ]; } /** - * @dataProvider provideHtmlArmor + * @dataProvider provideGetHtml */ - public function testHtmlArmor( $input, $expected ) { + public function testGetHtml( $input, $expected ) { $this->assertEquals( $expected, HtmlArmor::getHtml( $input ) diff --git a/www/wiki/tests/phpunit/includes/libs/IEUrlExtensionTest.php b/www/wiki/tests/phpunit/includes/libs/IEUrlExtensionTest.php index 57668e50..03c7b0c0 100644 --- a/www/wiki/tests/phpunit/includes/libs/IEUrlExtensionTest.php +++ b/www/wiki/tests/phpunit/includes/libs/IEUrlExtensionTest.php @@ -5,7 +5,10 @@ * @todo tests below for findIE6Extension should be split into... * ...a dataprovider and test method. */ -class IEUrlExtensionTest extends PHPUnit_Framework_TestCase { +class IEUrlExtensionTest extends PHPUnit\Framework\TestCase { + + use MediaWikiCoversValidator; + /** * @covers IEUrlExtension::findIE6Extension */ diff --git a/www/wiki/tests/phpunit/includes/libs/IPTest.php b/www/wiki/tests/phpunit/includes/libs/IPTest.php index e47c4ba1..9702c82c 100644 --- a/www/wiki/tests/phpunit/includes/libs/IPTest.php +++ b/www/wiki/tests/phpunit/includes/libs/IPTest.php @@ -8,13 +8,15 @@ * @todo Test methods in this call should be split into a method and a * dataprovider. */ +class IPTest extends PHPUnit\Framework\TestCase { + + use MediaWikiCoversValidator; -class IPTest extends PHPUnit_Framework_TestCase { /** * @covers IP::isIPAddress * @dataProvider provideInvalidIPs */ - public function isNotIPAddress( $val, $desc ) { + public function testIsNotIPAddress( $val, $desc ) { $this->assertFalse( IP::isIPAddress( $val ), $desc ); } @@ -561,7 +563,7 @@ class IPTest extends PHPUnit_Framework_TestCase { } /** - * Test for IP::splitHostAndPort(). + * @covers IP::splitHostAndPort() * @dataProvider provideSplitHostAndPort */ public function testSplitHostAndPort( $expected, $input, $description ) { @@ -588,7 +590,7 @@ class IPTest extends PHPUnit_Framework_TestCase { } /** - * Test for IP::combineHostAndPort() + * @covers IP::combineHostAndPort() * @dataProvider provideCombineHostAndPort */ public function testCombineHostAndPort( $expected, $input, $description ) { @@ -612,7 +614,7 @@ class IPTest extends PHPUnit_Framework_TestCase { } /** - * Test for IP::sanitizeRange() + * @covers IP::sanitizeRange() * @dataProvider provideIPCIDRs */ public function testSanitizeRange( $input, $expected, $description ) { @@ -636,7 +638,7 @@ class IPTest extends PHPUnit_Framework_TestCase { } /** - * Test for IP::prettifyIP() + * @covers IP::prettifyIP() * @dataProvider provideIPsToPrettify */ public function testPrettifyIP( $ip, $prettified ) { diff --git a/www/wiki/tests/phpunit/includes/libs/JavaScriptMinifierTest.php b/www/wiki/tests/phpunit/includes/libs/JavaScriptMinifierTest.php index ca12f6c7..61056784 100644 --- a/www/wiki/tests/phpunit/includes/libs/JavaScriptMinifierTest.php +++ b/www/wiki/tests/phpunit/includes/libs/JavaScriptMinifierTest.php @@ -1,6 +1,8 @@ <?php -class JavaScriptMinifierTest extends PHPUnit_Framework_TestCase { +class JavaScriptMinifierTest extends PHPUnit\Framework\TestCase { + + use MediaWikiCoversValidator; public static function provideCases() { return [ @@ -59,6 +61,20 @@ class JavaScriptMinifierTest extends PHPUnit_Framework_TestCase { [ "0xFF.\nx;", "0xFF.x;" ], [ "5.3.\nx;", "5.3.x;" ], + // Cover failure case for incomplete hex literal + [ "0x;", false, false ], + + // Cover failure case for number with no digits after E + [ "1.4E", false, false ], + + // Cover failure case for number with several E + [ "1.4EE2", false, false ], + [ "1.4EE", false, false ], + + // Cover failure case for number with several E (nonconsecutive) + // FIXME: This is invalid, but currently tolerated + [ "1.4E2E3", "1.4E2 E3", false ], + // Semicolon insertion between an expression having an inline // comment after it, and a statement on the next line (T29046). [ @@ -66,6 +82,18 @@ class JavaScriptMinifierTest extends PHPUnit_Framework_TestCase { "var a=this\nfor(b=0;c<d;b++){}" ], + // Cover failure case of incomplete regexp at end of file (T75556) + // FIXME: This is invalid, but currently tolerated + [ "*/", "*/", false ], + + // Cover failure case of incomplete char class in regexp (T75556) + // FIXME: This is invalid, but currently tolerated + [ "/a[b/.test", "/a[b/.test", false ], + + // Cover failure case of incomplete string at end of file (T75556) + // FIXME: This is invalid, but currently tolerated + [ "'a", "'a", false ], + // Token separation [ "x in y", "x in y" ], [ "/x/g in y", "/x/g in y" ], @@ -138,6 +166,7 @@ class JavaScriptMinifierTest extends PHPUnit_Framework_TestCase { [ "var a = 5.;", "var a=5.;" ], [ "5.0.toString();", "5.0.toString();" ], [ "5..toString();", "5..toString();" ], + // Cover failure case for too many decimal points [ "5...toString();", false ], [ "5.\n.toString();", '5..toString();' ], @@ -153,16 +182,17 @@ class JavaScriptMinifierTest extends PHPUnit_Framework_TestCase { /** * @dataProvider provideCases * @covers JavaScriptMinifier::minify + * @covers JavaScriptMinifier::parseError */ - public function testJavaScriptMinifierOutput( $code, $expectedOutput, $expectedValid = true ) { + public function testMinifyOutput( $code, $expectedOutput, $expectedValid = true ) { $minified = JavaScriptMinifier::minify( $code ); // JSMin+'s parser will throw an exception if output is not valid JS. // suppression of warnings needed for stupid crap if ( $expectedValid ) { - MediaWiki\suppressWarnings(); + Wikimedia\suppressWarnings(); $parser = new JSParser(); - MediaWiki\restoreWarnings(); + Wikimedia\restoreWarnings(); $parser->parse( $minified, 'minify-test.js', 1 ); } diff --git a/www/wiki/tests/phpunit/includes/libs/MWMessagePackTest.php b/www/wiki/tests/phpunit/includes/libs/MWMessagePackTest.php index 88dc67aa..695a7341 100644 --- a/www/wiki/tests/phpunit/includes/libs/MWMessagePackTest.php +++ b/www/wiki/tests/phpunit/includes/libs/MWMessagePackTest.php @@ -3,7 +3,9 @@ * PHP Unit tests for MWMessagePack * @covers MWMessagePack */ -class MWMessagePackTest extends PHPUnit_Framework_TestCase { +class MWMessagePackTest extends PHPUnit\Framework\TestCase { + + use MediaWikiCoversValidator; /** * Provides test cases for MWMessagePackTest::testMessagePack diff --git a/www/wiki/tests/phpunit/includes/libs/MapCacheLRUTest.php b/www/wiki/tests/phpunit/includes/libs/MapCacheLRUTest.php new file mode 100644 index 00000000..2a962b79 --- /dev/null +++ b/www/wiki/tests/phpunit/includes/libs/MapCacheLRUTest.php @@ -0,0 +1,117 @@ +<?php +/** + * @group Cache + */ +class MapCacheLRUTest extends PHPUnit\Framework\TestCase { + + use MediaWikiCoversValidator; + + /** + * @covers MapCacheLRU::newFromArray() + * @covers MapCacheLRU::toArray() + * @covers MapCacheLRU::getAllKeys() + * @covers MapCacheLRU::clear() + */ + function testArrayConversion() { + $raw = [ 'd' => 4, 'c' => 3, 'b' => 2, 'a' => 1 ]; + $cache = MapCacheLRU::newFromArray( $raw, 3 ); + + $this->assertSame( true, $cache->has( 'a' ) ); + $this->assertSame( true, $cache->has( 'b' ) ); + $this->assertSame( true, $cache->has( 'c' ) ); + $this->assertSame( 1, $cache->get( 'a' ) ); + $this->assertSame( 2, $cache->get( 'b' ) ); + $this->assertSame( 3, $cache->get( 'c' ) ); + + $this->assertSame( + [ 'a' => 1, 'b' => 2, 'c' => 3 ], + $cache->toArray() + ); + $this->assertSame( + [ 'a', 'b', 'c' ], + $cache->getAllKeys() + ); + + $cache->clear( 'a' ); + $this->assertSame( + [ 'b' => 2, 'c' => 3 ], + $cache->toArray() + ); + + $cache->clear(); + $this->assertSame( + [], + $cache->toArray() + ); + } + + /** + * @covers MapCacheLRU::has() + * @covers MapCacheLRU::get() + * @covers MapCacheLRU::set() + */ + function testLRU() { + $raw = [ 'a' => 1, 'b' => 2, 'c' => 3 ]; + $cache = MapCacheLRU::newFromArray( $raw, 3 ); + + $this->assertSame( true, $cache->has( 'c' ) ); + $this->assertSame( + [ 'a' => 1, 'b' => 2, 'c' => 3 ], + $cache->toArray() + ); + + $this->assertSame( 3, $cache->get( 'c' ) ); + $this->assertSame( + [ 'a' => 1, 'b' => 2, 'c' => 3 ], + $cache->toArray() + ); + + $this->assertSame( 1, $cache->get( 'a' ) ); + $this->assertSame( + [ 'b' => 2, 'c' => 3, 'a' => 1 ], + $cache->toArray() + ); + + $cache->set( 'a', 1 ); + $this->assertSame( + [ 'b' => 2, 'c' => 3, 'a' => 1 ], + $cache->toArray() + ); + + $cache->set( 'b', 22 ); + $this->assertSame( + [ 'c' => 3, 'a' => 1, 'b' => 22 ], + $cache->toArray() + ); + + $cache->set( 'd', 4 ); + $this->assertSame( + [ 'a' => 1, 'b' => 22, 'd' => 4 ], + $cache->toArray() + ); + + $cache->set( 'e', 5, 0.33 ); + $this->assertSame( + [ 'e' => 5, 'b' => 22, 'd' => 4 ], + $cache->toArray() + ); + + $cache->set( 'f', 6, 0.66 ); + $this->assertSame( + [ 'b' => 22, 'f' => 6, 'd' => 4 ], + $cache->toArray() + ); + + $cache->set( 'g', 7, 0.90 ); + $this->assertSame( + [ 'f' => 6, 'g' => 7, 'd' => 4 ], + $cache->toArray() + ); + + $cache->set( 'g', 7, 1.0 ); + $this->assertSame( + [ 'f' => 6, 'd' => 4, 'g' => 7 ], + $cache->toArray() + ); + } +} diff --git a/www/wiki/tests/phpunit/includes/libs/MemoizedCallableTest.php b/www/wiki/tests/phpunit/includes/libs/MemoizedCallableTest.php index d99c5878..9127a30f 100644 --- a/www/wiki/tests/phpunit/includes/libs/MemoizedCallableTest.php +++ b/www/wiki/tests/phpunit/includes/libs/MemoizedCallableTest.php @@ -24,14 +24,16 @@ class ArrayBackedMemoizedCallable extends MemoizedCallable { * PHP Unit tests for MemoizedCallable class. * @covers MemoizedCallable */ -class MemoizedCallableTest extends PHPUnit_Framework_TestCase { +class MemoizedCallableTest extends PHPUnit\Framework\TestCase { + + use MediaWikiCoversValidator; /** * The memoized callable should relate inputs to outputs in the same * way as the original underlying callable. */ public function testReturnValuePassedThrough() { - $mock = $this->getMockBuilder( 'stdClass' ) + $mock = $this->getMockBuilder( stdClass::class ) ->setMethods( [ 'reverse' ] )->getMock(); $mock->expects( $this->any() ) ->method( 'reverse' ) @@ -45,10 +47,10 @@ class MemoizedCallableTest extends PHPUnit_Framework_TestCase { * Consecutive calls to the memoized callable with the same arguments * should result in just one invocation of the underlying callable. * - * @requires function apc_store/apcu_store + * @requires extension apcu */ public function testCallableMemoized() { - $observer = $this->getMockBuilder( 'stdClass' ) + $observer = $this->getMockBuilder( stdClass::class ) ->setMethods( [ 'computeSomething' ] )->getMock(); $observer->expects( $this->once() ) ->method( 'computeSomething' ) diff --git a/www/wiki/tests/phpunit/includes/libs/ObjectFactoryTest.php b/www/wiki/tests/phpunit/includes/libs/ObjectFactoryTest.php deleted file mode 100644 index 35a7b602..00000000 --- a/www/wiki/tests/phpunit/includes/libs/ObjectFactoryTest.php +++ /dev/null @@ -1,180 +0,0 @@ -<?php -/** - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along - * with this program; if not, write to the Free Software Foundation, Inc., - * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - * http://www.gnu.org/copyleft/gpl.html - * - * @file - */ - -class ObjectFactoryTest extends PHPUnit_Framework_TestCase { - - /** - * @covers ObjectFactory::getObjectFromSpec - */ - public function testClosureExpansionDisabled() { - $obj = ObjectFactory::getObjectFromSpec( [ - 'class' => 'ObjectFactoryTestFixture', - 'args' => [ - function () { - return 'wrapped'; - }, - 'unwrapped', - ], - 'calls' => [ - 'setter' => [ function () { - return 'wrapped'; - }, ], - ], - 'closure_expansion' => false, - ] ); - $this->assertInstanceOf( 'Closure', $obj->args[0] ); - $this->assertSame( 'wrapped', $obj->args[0]() ); - $this->assertSame( 'unwrapped', $obj->args[1] ); - $this->assertInstanceOf( 'Closure', $obj->setterArgs[0] ); - $this->assertSame( 'wrapped', $obj->setterArgs[0]() ); - } - - /** - * @covers ObjectFactory::getObjectFromSpec - * @covers ObjectFactory::expandClosures - */ - public function testClosureExpansionEnabled() { - $obj = ObjectFactory::getObjectFromSpec( [ - 'class' => 'ObjectFactoryTestFixture', - 'args' => [ - function () { - return 'wrapped'; - }, - 'unwrapped', - ], - 'calls' => [ - 'setter' => [ function () { - return 'wrapped'; - }, ], - ], - 'closure_expansion' => true, - ] ); - $this->assertInternalType( 'string', $obj->args[0] ); - $this->assertSame( 'wrapped', $obj->args[0] ); - $this->assertSame( 'unwrapped', $obj->args[1] ); - $this->assertInternalType( 'string', $obj->setterArgs[0] ); - $this->assertSame( 'wrapped', $obj->setterArgs[0] ); - - $obj = ObjectFactory::getObjectFromSpec( [ - 'class' => 'ObjectFactoryTestFixture', - 'args' => [ function () { - return 'unwrapped'; - }, ], - 'calls' => [ - 'setter' => [ function () { - return 'unwrapped'; - }, ], - ], - ] ); - $this->assertInternalType( 'string', $obj->args[0] ); - $this->assertSame( 'unwrapped', $obj->args[0] ); - $this->assertInternalType( 'string', $obj->setterArgs[0] ); - $this->assertSame( 'unwrapped', $obj->setterArgs[0] ); - } - - /** - * @covers ObjectFactory::getObjectFromSpec - */ - public function testGetObjectFromFactory() { - $args = [ 'a', 'b' ]; - $obj = ObjectFactory::getObjectFromSpec( [ - 'factory' => function ( $a, $b ) { - return new ObjectFactoryTestFixture( $a, $b ); - }, - 'args' => $args, - ] ); - $this->assertSame( $args, $obj->args ); - } - - /** - * @covers ObjectFactory::getObjectFromSpec - * @expectedException InvalidArgumentException - */ - public function testGetObjectFromInvalid() { - $args = [ 'a', 'b' ]; - $obj = ObjectFactory::getObjectFromSpec( [ - // Missing 'class' or 'factory' - 'args' => $args, - ] ); - } - - /** - * @covers ObjectFactory::getObjectFromSpec - * @dataProvider provideConstructClassInstance - */ - public function testGetObjectFromClass( $args ) { - $obj = ObjectFactory::getObjectFromSpec( [ - 'class' => 'ObjectFactoryTestFixture', - 'args' => $args, - ] ); - $this->assertSame( $args, $obj->args ); - } - - /** - * @covers ObjectFactory::constructClassInstance - * @dataProvider provideConstructClassInstance - */ - public function testConstructClassInstance( $args ) { - $obj = ObjectFactory::constructClassInstance( - 'ObjectFactoryTestFixture', $args - ); - $this->assertSame( $args, $obj->args ); - } - - public static function provideConstructClassInstance() { - // These args go to 11. I thought about making 10 one louder, but 11! - return [ - '0 args' => [ [] ], - '1 args' => [ [ 1, ] ], - '2 args' => [ [ 1, 2, ] ], - '3 args' => [ [ 1, 2, 3, ] ], - '4 args' => [ [ 1, 2, 3, 4, ] ], - '5 args' => [ [ 1, 2, 3, 4, 5, ] ], - '6 args' => [ [ 1, 2, 3, 4, 5, 6, ] ], - '7 args' => [ [ 1, 2, 3, 4, 5, 6, 7, ] ], - '8 args' => [ [ 1, 2, 3, 4, 5, 6, 7, 8, ] ], - '9 args' => [ [ 1, 2, 3, 4, 5, 6, 7, 8, 9, ] ], - '10 args' => [ [ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, ] ], - '11 args' => [ [ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, ] ], - ]; - } - - /** - * @covers ObjectFactory::constructClassInstance - * @expectedException InvalidArgumentException - */ - public function testNamedArgs() { - $args = [ 'foo' => 1, 'bar' => 2, 'baz' => 3 ]; - $obj = ObjectFactory::constructClassInstance( - 'ObjectFactoryTestFixture', $args - ); - } -} - -class ObjectFactoryTestFixture { - public $args; - public $setterArgs; - public function __construct( /*...*/ ) { - $this->args = func_get_args(); - } - public function setter( /*...*/ ) { - $this->setterArgs = func_get_args(); - } -} diff --git a/www/wiki/tests/phpunit/includes/libs/ProcessCacheLRUTest.php b/www/wiki/tests/phpunit/includes/libs/ProcessCacheLRUTest.php index 9c189d12..c8940e5f 100644 --- a/www/wiki/tests/phpunit/includes/libs/ProcessCacheLRUTest.php +++ b/www/wiki/tests/phpunit/includes/libs/ProcessCacheLRUTest.php @@ -9,7 +9,9 @@ * * @group Cache */ -class ProcessCacheLRUTest extends PHPUnit_Framework_TestCase { +class ProcessCacheLRUTest extends PHPUnit\Framework\TestCase { + + use MediaWikiCoversValidator; /** * Helper to verify emptiness of a cache object. @@ -58,6 +60,7 @@ class ProcessCacheLRUTest extends PHPUnit_Framework_TestCase { /** * Highlight diff between assertEquals and assertNotSame + * @coversNothing */ public function testPhpUnitArrayEquality() { $one = [ 'A' => 1, 'B' => 2 ]; diff --git a/www/wiki/tests/phpunit/includes/libs/SamplingStatsdClientTest.php b/www/wiki/tests/phpunit/includes/libs/SamplingStatsdClientTest.php index c5bc03ea..7bd16115 100644 --- a/www/wiki/tests/phpunit/includes/libs/SamplingStatsdClientTest.php +++ b/www/wiki/tests/phpunit/includes/libs/SamplingStatsdClientTest.php @@ -3,7 +3,13 @@ use Liuggio\StatsdClient\Entity\StatsdData; use Liuggio\StatsdClient\Sender\SenderInterface; -class SamplingStatsdClientTest extends PHPUnit_Framework_TestCase { +/** + * @covers SamplingStatsdClient + */ +class SamplingStatsdClientTest extends PHPUnit\Framework\TestCase { + + use MediaWikiCoversValidator; + /** * @dataProvider samplingDataProvider */ @@ -16,7 +22,11 @@ class SamplingStatsdClientTest extends PHPUnit_Framework_TestCase { } else { $sender->expects( $this->never() )->method( 'write' ); } - mt_srand( $seed ); + if ( defined( 'MT_RAND_PHP' ) ) { + mt_srand( $seed, MT_RAND_PHP ); + } else { + mt_srand( $seed ); + } $client = new SamplingStatsdClient( $sender ); $client->send( $data, $sampleRate ); } diff --git a/www/wiki/tests/phpunit/includes/libs/StringUtilsTest.php b/www/wiki/tests/phpunit/includes/libs/StringUtilsTest.php index 8075944e..fcfa53e2 100644 --- a/www/wiki/tests/phpunit/includes/libs/StringUtilsTest.php +++ b/www/wiki/tests/phpunit/includes/libs/StringUtilsTest.php @@ -1,6 +1,8 @@ <?php -class StringUtilsTest extends PHPUnit_Framework_TestCase { +class StringUtilsTest extends PHPUnit\Framework\TestCase { + + use MediaWikiCoversValidator; /** * @covers StringUtils::isUtf8 diff --git a/www/wiki/tests/phpunit/includes/libs/TimingTest.php b/www/wiki/tests/phpunit/includes/libs/TimingTest.php index 4d719440..581a5186 100644 --- a/www/wiki/tests/phpunit/includes/libs/TimingTest.php +++ b/www/wiki/tests/phpunit/includes/libs/TimingTest.php @@ -19,7 +19,9 @@ * @author Ori Livneh <ori@wikimedia.org> */ -class TimingTest extends PHPUnit_Framework_TestCase { +class TimingTest extends PHPUnit\Framework\TestCase { + + use MediaWikiCoversValidator; /** * @covers Timing::clearMarks diff --git a/www/wiki/tests/phpunit/includes/libs/XhprofDataTest.php b/www/wiki/tests/phpunit/includes/libs/XhprofDataTest.php index 35e90052..1cbd86f1 100644 --- a/www/wiki/tests/phpunit/includes/libs/XhprofDataTest.php +++ b/www/wiki/tests/phpunit/includes/libs/XhprofDataTest.php @@ -24,7 +24,9 @@ * @copyright © 2014 Wikimedia Foundation and contributors * @since 1.25 */ -class XhprofDataTest extends PHPUnit_Framework_TestCase { +class XhprofDataTest extends PHPUnit\Framework\TestCase { + + use MediaWikiCoversValidator; /** * @covers XhprofData::splitKey diff --git a/www/wiki/tests/phpunit/includes/libs/XhprofTest.php b/www/wiki/tests/phpunit/includes/libs/XhprofTest.php index 67481154..0ea13289 100644 --- a/www/wiki/tests/phpunit/includes/libs/XhprofTest.php +++ b/www/wiki/tests/phpunit/includes/libs/XhprofTest.php @@ -18,7 +18,10 @@ * @file */ -class XhprofTest extends PHPUnit_Framework_TestCase { +class XhprofTest extends PHPUnit\Framework\TestCase { + + use MediaWikiCoversValidator; + /** * Trying to enable Xhprof when it is already enabled causes an exception * to be thrown. @@ -28,7 +31,7 @@ class XhprofTest extends PHPUnit_Framework_TestCase { * @covers Xhprof::enable */ public function testEnable() { - $xhprof = new ReflectionClass( 'Xhprof' ); + $xhprof = new ReflectionClass( Xhprof::class ); $enabled = $xhprof->getProperty( 'enabled' ); $enabled->setAccessible( true ); $enabled->setValue( true ); diff --git a/www/wiki/tests/phpunit/includes/libs/XmlTypeCheckTest.php b/www/wiki/tests/phpunit/includes/libs/XmlTypeCheckTest.php index 5c5eeaa7..8616b419 100644 --- a/www/wiki/tests/phpunit/includes/libs/XmlTypeCheckTest.php +++ b/www/wiki/tests/phpunit/includes/libs/XmlTypeCheckTest.php @@ -5,12 +5,14 @@ * @group Xml * @covers XMLTypeCheck */ -class XmlTypeCheckTest extends PHPUnit_Framework_TestCase { +class XmlTypeCheckTest extends PHPUnit\Framework\TestCase { + + use MediaWikiCoversValidator; + const WELL_FORMED_XML = "<root><child /></root>"; const MAL_FORMED_XML = "<root><child /></error>"; - // @codingStandardsIgnoreStart Generic.Files.LineLength + // phpcs:ignore Generic.Files.LineLength const XML_WITH_PIH = '<?xml version="1.0"?><?xml-stylesheet type="text/xsl" href="/w/index.php"?><svg><child /></svg>'; - // @codingStandardsIgnoreEnd /** * @covers XMLTypeCheck::newFromString diff --git a/www/wiki/tests/phpunit/includes/libs/composer/ComposerInstalledTest.php b/www/wiki/tests/phpunit/includes/libs/composer/ComposerInstalledTest.php new file mode 100644 index 00000000..05ae2a37 --- /dev/null +++ b/www/wiki/tests/phpunit/includes/libs/composer/ComposerInstalledTest.php @@ -0,0 +1,499 @@ +<?php + +class ComposerInstalledTest extends MediaWikiTestCase { + + private $installed; + + public function setUp() { + parent::setUp(); + global $IP; + $this->installed = "$IP/tests/phpunit/data/composer/installed.json"; + } + + /** + * @covers ComposerInstalled::__construct + * @covers ComposerInstalled::getInstalledDependencies + */ + public function testGetInstalledDependencies() { + $installed = new ComposerInstalled( $this->installed ); + $this->assertArrayEquals( [ + 'leafo/lessphp' => [ + 'version' => '0.5.0', + 'type' => 'library', + 'licenses' => [ 'MIT', 'GPL-3.0-only' ], + 'authors' => [ + [ + 'name' => 'Leaf Corcoran', + 'email' => 'leafot@gmail.com', + 'homepage' => 'http://leafo.net', + ], + ], + 'description' => 'lessphp is a compiler for LESS written in PHP.', + ], + 'psr/log' => [ + 'version' => '1.0.0', + 'type' => 'library', + 'licenses' => [ 'MIT' ], + 'authors' => [ + [ + 'name' => 'PHP-FIG', + 'homepage' => 'http://www.php-fig.org/', + ], + ], + 'description' => 'Common interface for logging libraries', + ], + 'cssjanus/cssjanus' => [ + 'version' => '1.1.1', + 'type' => 'library', + 'licenses' => [ 'Apache-2.0' ], + 'authors' => [ + ], + 'description' => 'Convert CSS stylesheets between left-to-right ' . + 'and right-to-left.', + ], + 'cdb/cdb' => [ + 'version' => '1.0.0', + 'type' => 'library', + 'licenses' => [ 'GPLv2' ], + 'authors' => [ + [ + 'name' => 'Tim Starling', + 'email' => 'tstarling@wikimedia.org', + ], + [ + 'name' => 'Chad Horohoe', + 'email' => 'chad@wikimedia.org', + ], + ], + 'description' => 'Constant Database (CDB) wrapper library for PHP. ' . + 'Provides pure-PHP fallback when dba_* functions are absent.', + ], + 'sebastian/version' => [ + 'version' => '2.0.1', + 'type' => 'library', + 'licenses' => [ 'BSD-3-Clause' ], + 'authors' => [ + [ + 'name' => 'Sebastian Bergmann', + 'email' => 'sebastian@phpunit.de', + 'role' => 'lead', + ], + ], + 'description' => 'Library that helps with managing the version ' . + 'number of Git-hosted PHP projects', + ], + 'sebastian/resource-operations' => [ + 'version' => '1.0.0', + 'type' => 'library', + 'licenses' => [ 'BSD-3-Clause' ], + 'authors' => [ + [ + 'name' => 'Sebastian Bergmann', + 'email' => 'sebastian@phpunit.de', + ], + ], + 'description' => 'Provides a list of PHP built-in functions that ' . + 'operate on resources', + ], + 'sebastian/recursion-context' => [ + 'version' => '3.0.0', + 'type' => 'library', + 'licenses' => [ 'BSD-3-Clause' ], + 'authors' => [ + [ + 'name' => 'Jeff Welch', + 'email' => 'whatthejeff@gmail.com', + ], + [ + 'name' => 'Sebastian Bergmann', + 'email' => 'sebastian@phpunit.de', + ], + [ + 'name' => 'Adam Harvey', + 'email' => 'aharvey@php.net', + ], + ], + 'description' => 'Provides functionality to recursively process PHP ' . + 'variables', + ], + 'sebastian/object-reflector' => [ + 'version' => '1.1.1', + 'type' => 'library', + 'licenses' => [ 'BSD-3-Clause' ], + 'authors' => [ + [ + 'name' => 'Sebastian Bergmann', + 'email' => 'sebastian@phpunit.de', + ], + ], + 'description' => 'Allows reflection of object attributes, including ' . + 'inherited and non-public ones', + ], + 'sebastian/object-enumerator' => [ + 'version' => '3.0.3', + 'type' => 'library', + 'licenses' => [ 'BSD-3-Clause' ], + 'authors' => [ + [ + 'name' => 'Sebastian Bergmann', + 'email' => 'sebastian@phpunit.de', + ], + ], + 'description' => 'Traverses array structures and object graphs ' . + 'to enumerate all referenced objects', + ], + 'sebastian/global-state' => [ + 'version' => '2.0.0', + 'type' => 'library', + 'licenses' => [ 'BSD-3-Clause' ], + 'authors' => [ + [ + 'name' => 'Sebastian Bergmann', + 'email' => 'sebastian@phpunit.de', + ], + ], + 'description' => 'Snapshotting of global state', + ], + 'sebastian/exporter' => [ + 'version' => '3.1.0', + 'type' => 'library', + 'licenses' => [ 'BSD-3-Clause' ], + 'authors' => [ + [ + 'name' => 'Jeff Welch', + 'email' => 'whatthejeff@gmail.com', + ], + [ + 'name' => 'Volker Dusch', + 'email' => 'github@wallbash.com', + ], + [ + 'name' => 'Bernhard Schussek', + 'email' => 'bschussek@2bepublished.at', + ], + [ + 'name' => 'Sebastian Bergmann', + 'email' => 'sebastian@phpunit.de', + ], + [ + 'name' => 'Adam Harvey', + 'email' => 'aharvey@php.net', + ], + ], + 'description' => 'Provides the functionality to export PHP ' . + 'variables for visualization', + ], + 'sebastian/environment' => [ + 'version' => '3.1.0', + 'type' => 'library', + 'licenses' => [ 'BSD-3-Clause' ], + 'authors' => [ + [ + 'name' => 'Sebastian Bergmann', + 'email' => 'sebastian@phpunit.de', + ], + ], + 'description' => 'Provides functionality to handle HHVM/PHP ' . + 'environments', + ], + 'sebastian/diff' => [ + 'version' => '2.0.1', + 'type' => 'library', + 'licenses' => [ 'BSD-3-Clause' ], + 'authors' => [ + [ + 'name' => 'Kore Nordmann', + 'email' => 'mail@kore-nordmann.de', + ], + [ + 'name' => 'Sebastian Bergmann', + 'email' => 'sebastian@phpunit.de', + ], + ], + 'description' => 'Diff implementation', + ], + 'sebastian/comparator' => [ + 'version' => '2.1.1', + 'type' => 'library', + 'licenses' => [ 'BSD-3-Clause' ], + 'authors' => [ + [ + 'name' => 'Jeff Welch', + 'email' => 'whatthejeff@gmail.com', + ], + [ + 'name' => 'Volker Dusch', + 'email' => 'github@wallbash.com', + ], + [ + 'name' => 'Bernhard Schussek', + 'email' => 'bschussek@2bepublished.at', + ], + [ + 'name' => 'Sebastian Bergmann', + 'email' => 'sebastian@phpunit.de', + ], + ], + 'description' => 'Provides the functionality to compare PHP ' . + 'values for equality', + ], + 'doctrine/instantiator' => [ + 'version' => '1.1.0', + 'type' => 'library', + 'licenses' => [ 'MIT' ], + 'authors' => [ + [ + 'name' => 'Marco Pivetta', + 'email' => 'ocramius@gmail.com', + 'homepage' => 'http://ocramius.github.com/', + ], + ], + 'description' => 'A small, lightweight utility to instantiate ' . + 'objects in PHP without invoking their constructors', + ], + 'phpunit/php-text-template' => [ + 'version' => '1.2.1', + 'type' => 'library', + 'licenses' => [ 'BSD-3-Clause' ], + 'authors' => [ + [ + 'name' => 'Sebastian Bergmann', + 'email' => 'sebastian@phpunit.de', + 'role' => 'lead', + ], + ], + 'description' => 'Simple template engine.', + ], + 'phpunit/phpunit-mock-objects' => [ + 'version' => '5.0.6', + 'type' => 'library', + 'licenses' => [ 'BSD-3-Clause' ], + 'authors' => [ + [ + 'name' => 'Sebastian Bergmann', + 'email' => 'sebastian@phpunit.de', + 'role' => 'lead', + ], + ], + 'description' => 'Mock Object library for PHPUnit', + ], + 'phpunit/php-timer' => [ + 'version' => '1.0.9', + 'type' => 'library', + 'licenses' => [ 'BSD-3-Clause' ], + 'authors' => [ + [ + 'name' => 'Sebastian Bergmann', + 'email' => 'sb@sebastian-bergmann.de', + 'role' => 'lead', + ], + ], + 'description' => 'Utility class for timing', + ], + 'phpunit/php-file-iterator' => [ + 'version' => '1.4.5', + 'type' => 'library', + 'licenses' => [ 'BSD-3-Clause' ], + 'authors' => [ + [ + 'name' => 'Sebastian Bergmann', + 'email' => 'sb@sebastian-bergmann.de', + 'role' => 'lead', + ], + ], + 'description' => 'FilterIterator implementation that filters ' . + 'files based on a list of suffixes.', + ], + 'theseer/tokenizer' => [ + 'version' => '1.1.0', + 'type' => 'library', + 'licenses' => [ 'BSD-3-Clause' ], + 'authors' => [ + [ + 'name' => 'Arne Blankerts', + 'email' => 'arne@blankerts.de', + 'role' => 'Developer', + ], + ], + 'description' => 'A small library for converting tokenized PHP ' . + 'source code into XML and potentially other formats', + ], + 'sebastian/code-unit-reverse-lookup' => [ + 'version' => '1.0.1', + 'type' => 'library', + 'licenses' => [ 'BSD-3-Clause' ], + 'authors' => [ + [ + 'name' => 'Sebastian Bergmann', + 'email' => 'sebastian@phpunit.de', + ], + ], + 'description' => 'Looks up which function or method a line of ' . + 'code belongs to', + ], + 'phpunit/php-token-stream' => [ + 'version' => '2.0.2', + 'type' => 'library', + 'licenses' => [ 'BSD-3-Clause' ], + 'authors' => [ + [ + 'name' => 'Sebastian Bergmann', + 'email' => 'sebastian@phpunit.de', + ], + ], + 'description' => 'Wrapper around PHP\'s tokenizer extension.', + ], + 'phpunit/php-code-coverage' => [ + 'version' => '5.3.0', + 'type' => 'library', + 'licenses' => [ 'BSD-3-Clause' ], + 'authors' => [ + [ + 'name' => 'Sebastian Bergmann', + 'email' => 'sebastian@phpunit.de', + 'role' => 'lead', + ], + ], + 'description' => 'Library that provides collection, processing, ' . + 'and rendering functionality for PHP code coverage information.', + ], + 'webmozart/assert' => [ + 'version' => '1.2.0', + 'type' => 'library', + 'licenses' => [ 'MIT' ], + 'authors' => [ + [ + 'name' => 'Bernhard Schussek', + 'email' => 'bschussek@gmail.com', + ], + ], + 'description' => 'Assertions to validate method input/output with ' . + 'nice error messages.', + ], + 'phpdocumentor/reflection-common' => [ + 'version' => '1.0.1', + 'type' => 'library', + 'licenses' => [ 'MIT' ], + 'authors' => [ + [ + 'name' => 'Jaap van Otterdijk', + 'email' => 'opensource@ijaap.nl', + ], + ], + 'description' => 'Common reflection classes used by phpdocumentor to ' . + 'reflect the code structure', + ], + 'phpdocumentor/type-resolver' => [ + 'version' => '0.4.0', + 'type' => 'library', + 'licenses' => [ 'MIT' ], + 'authors' => [ + [ + 'name' => 'Mike van Riel', + 'email' => 'me@mikevanriel.com', + ], + ], + 'description' => '', + ], + 'phpdocumentor/reflection-docblock' => [ + 'version' => '4.2.0', + 'type' => 'library', + 'licenses' => [ 'MIT' ], + 'authors' => [ + [ + 'name' => 'Mike van Riel', + 'email' => 'me@mikevanriel.com', + ], + ], + 'description' => 'With this component, a library can provide support for ' . + 'annotations via DocBlocks or otherwise retrieve information that ' . + 'is embedded in a DocBlock.', + ], + 'phpspec/prophecy' => [ + 'version' => '1.7.3', + 'type' => 'library', + 'licenses' => [ 'MIT' ], + 'authors' => [ + [ + 'name' => 'Konstantin Kudryashov', + 'email' => 'ever.zet@gmail.com', + 'homepage' => 'http://everzet.com', + ], + [ + 'name' => 'Marcello Duarte', + 'email' => 'marcello.duarte@gmail.com', + ], + ], + 'description' => 'Highly opinionated mocking framework for PHP 5.3+', + ], + 'phar-io/version' => [ + 'version' => '1.0.1', + 'type' => 'library', + 'licenses' => [ 'BSD-3-Clause' ], + 'authors' => [ + [ + 'name' => 'Arne Blankerts', + 'email' => 'arne@blankerts.de', + 'role' => 'Developer', + ], + [ + 'name' => 'Sebastian Heuer', + 'email' => 'sebastian@phpeople.de', + 'role' => 'Developer', + ], + [ + 'name' => 'Sebastian Bergmann', + 'email' => 'sebastian@phpunit.de', + 'role' => 'Developer', + ], + ], + 'description' => 'Library for handling version information and constraints', + ], + 'phar-io/manifest' => [ + 'version' => '1.0.1', + 'type' => 'library', + 'licenses' => [ 'BSD-3-Clause' ], + 'authors' => [ + [ + 'name' => 'Arne Blankerts', + 'email' => 'arne@blankerts.de', + 'role' => 'Developer', + ], + [ + 'name' => 'Sebastian Heuer', + 'email' => 'sebastian@phpeople.de', + 'role' => 'Developer', + ], + [ + 'name' => 'Sebastian Bergmann', + 'email' => 'sebastian@phpunit.de', + 'role' => 'Developer', + ], + ], + 'description' => 'Component for reading phar.io manifest ' . + 'information from a PHP Archive (PHAR)', + ], + 'myclabs/deep-copy' => [ + 'version' => '1.7.0', + 'type' => 'library', + 'licenses' => [ 'MIT' ], + 'authors' => [ + ], + 'description' => 'Create deep copies (clones) of your objects', + ], + 'phpunit/phpunit' => [ + 'version' => '6.5.5', + 'type' => 'library', + 'licenses' => [ 'BSD-3-Clause' ], + 'authors' => [ + [ + 'name' => 'Sebastian Bergmann', + 'email' => 'sebastian@phpunit.de', + 'role' => 'lead', + ], + ], + 'description' => 'The PHP Unit Testing framework.', + ], + ], $installed->getInstalledDependencies(), false, true ); + } +} diff --git a/www/wiki/tests/phpunit/includes/libs/composer/ComposerLockTest.php b/www/wiki/tests/phpunit/includes/libs/composer/ComposerLockTest.php index eef7e274..dc81e1d3 100644 --- a/www/wiki/tests/phpunit/includes/libs/composer/ComposerLockTest.php +++ b/www/wiki/tests/phpunit/includes/libs/composer/ComposerLockTest.php @@ -20,7 +20,7 @@ class ComposerLockTest extends MediaWikiTestCase { 'wikimedia/cdb' => [ 'version' => '1.0.1', 'type' => 'library', - 'licenses' => [ 'GPL-2.0' ], + 'licenses' => [ 'GPL-2.0-only' ], 'authors' => [ [ 'name' => 'Tim Starling', @@ -44,7 +44,7 @@ class ComposerLockTest extends MediaWikiTestCase { 'leafo/lessphp' => [ 'version' => '0.5.0', 'type' => 'library', - 'licenses' => [ 'MIT', 'GPL-3.0' ], + 'licenses' => [ 'MIT', 'GPL-3.0-only' ], 'authors' => [ [ 'name' => 'Leaf Corcoran', @@ -89,7 +89,7 @@ class ComposerLockTest extends MediaWikiTestCase { 'mediawiki/translate' => [ 'version' => '2014.12', 'type' => 'mediawiki-extension', - 'licenses' => [ 'GPL-2.0+' ], + 'licenses' => [ 'GPL-2.0-or-later' ], 'authors' => [ [ 'name' => 'Niklas Laxström', @@ -109,7 +109,7 @@ class ComposerLockTest extends MediaWikiTestCase { 'mediawiki/universal-language-selector' => [ 'version' => '2014.12', 'type' => 'mediawiki-extension', - 'licenses' => [ 'GPL-2.0+', 'MIT' ], + 'licenses' => [ 'GPL-2.0-or-later', 'MIT' ], 'authors' => [], 'description' => 'The primary aim is to allow users to select a language ' . 'and configure its support in an easy way. ' . diff --git a/www/wiki/tests/phpunit/includes/libs/http/HttpAcceptNegotiatorTest.php b/www/wiki/tests/phpunit/includes/libs/http/HttpAcceptNegotiatorTest.php index 4415bc97..02eac118 100644 --- a/www/wiki/tests/phpunit/includes/libs/http/HttpAcceptNegotiatorTest.php +++ b/www/wiki/tests/phpunit/includes/libs/http/HttpAcceptNegotiatorTest.php @@ -5,10 +5,9 @@ use Wikimedia\Http\HttpAcceptNegotiator; /** * @covers Wikimedia\Http\HttpAcceptNegotiator * - * @license GPL-2.0+ * @author Daniel Kinzler */ -class HttpAcceptNegotiatorTest extends \PHPUnit_Framework_TestCase { +class HttpAcceptNegotiatorTest extends \PHPUnit\Framework\TestCase { public function provideGetFirstSupportedValue() { return [ diff --git a/www/wiki/tests/phpunit/includes/libs/http/HttpAcceptParserTest.php b/www/wiki/tests/phpunit/includes/libs/http/HttpAcceptParserTest.php index 5bd94253..e4b47b46 100644 --- a/www/wiki/tests/phpunit/includes/libs/http/HttpAcceptParserTest.php +++ b/www/wiki/tests/phpunit/includes/libs/http/HttpAcceptParserTest.php @@ -5,10 +5,9 @@ use Wikimedia\Http\HttpAcceptParser; /** * @covers Wikimedia\Http\HttpAcceptParser * - * @license GPL-2.0+ * @author Daniel Kinzler */ -class HttpAcceptParserTest extends \PHPUnit_Framework_TestCase { +class HttpAcceptParserTest extends \PHPUnit\Framework\TestCase { public function provideParseWeights() { return [ diff --git a/www/wiki/tests/phpunit/includes/libs/mime/MimeAnalyzerTest.php b/www/wiki/tests/phpunit/includes/libs/mime/MimeAnalyzerTest.php index 64f49604..fbe5a2ba 100644 --- a/www/wiki/tests/phpunit/includes/libs/mime/MimeAnalyzerTest.php +++ b/www/wiki/tests/phpunit/includes/libs/mime/MimeAnalyzerTest.php @@ -1,9 +1,12 @@ <?php -/* +/** * @group Media * @covers MimeAnalyzer */ -class MimeMagicTest extends PHPUnit_Framework_TestCase { +class MimeAnalyzerTest extends PHPUnit\Framework\TestCase { + + use MediaWikiCoversValidator; + /** @var MimeAnalyzer */ private $mimeAnalyzer; diff --git a/www/wiki/tests/phpunit/includes/libs/objectcache/BagOStuffTest.php b/www/wiki/tests/phpunit/includes/libs/objectcache/BagOStuffTest.php index 51883fb1..10fba835 100644 --- a/www/wiki/tests/phpunit/includes/libs/objectcache/BagOStuffTest.php +++ b/www/wiki/tests/phpunit/includes/libs/objectcache/BagOStuffTest.php @@ -163,6 +163,9 @@ class BagOStuffTest extends MediaWikiTestCase { $this->assertTrue( $this->cache->add( $key, 'test' ) ); } + /** + * @covers BagOStuff::get + */ public function testGet() { $value = [ 'this' => 'is', 'a' => 'test' ]; diff --git a/www/wiki/tests/phpunit/includes/libs/objectcache/CachedBagOStuffTest.php b/www/wiki/tests/phpunit/includes/libs/objectcache/CachedBagOStuffTest.php index 8b9abbc6..d0360a99 100644 --- a/www/wiki/tests/phpunit/includes/libs/objectcache/CachedBagOStuffTest.php +++ b/www/wiki/tests/phpunit/includes/libs/objectcache/CachedBagOStuffTest.php @@ -5,7 +5,9 @@ use Wikimedia\TestingAccessWrapper; /** * @group BagOStuff */ -class CachedBagOStuffTest extends PHPUnit_Framework_TestCase { +class CachedBagOStuffTest extends PHPUnit\Framework\TestCase { + + use MediaWikiCoversValidator; /** * @covers CachedBagOStuff::__construct diff --git a/www/wiki/tests/phpunit/includes/libs/objectcache/HashBagOStuffTest.php b/www/wiki/tests/phpunit/includes/libs/objectcache/HashBagOStuffTest.php index b2278c34..332e23b2 100644 --- a/www/wiki/tests/phpunit/includes/libs/objectcache/HashBagOStuffTest.php +++ b/www/wiki/tests/phpunit/includes/libs/objectcache/HashBagOStuffTest.php @@ -5,7 +5,9 @@ use Wikimedia\TestingAccessWrapper; /** * @group BagOStuff */ -class HashBagOStuffTest extends PHPUnit_Framework_TestCase { +class HashBagOStuffTest extends PHPUnit\Framework\TestCase { + + use MediaWikiCoversValidator; /** * @covers HashBagOStuff::__construct @@ -103,7 +105,7 @@ class HashBagOStuffTest extends PHPUnit_Framework_TestCase { for ( $i = 10; $i < 20; $i++ ) { $cache->set( "key$i", 1 ); $this->assertEquals( 1, $cache->get( "key$i" ) ); - $this->assertEquals( false, $cache->get( "key" . $i - 10 ) ); + $this->assertEquals( false, $cache->get( "key" . ( $i - 10 ) ) ); } } diff --git a/www/wiki/tests/phpunit/includes/libs/objectcache/WANObjectCacheTest.php b/www/wiki/tests/phpunit/includes/libs/objectcache/WANObjectCacheTest.php index c762aa7d..662bb961 100644 --- a/www/wiki/tests/phpunit/includes/libs/objectcache/WANObjectCacheTest.php +++ b/www/wiki/tests/phpunit/includes/libs/objectcache/WANObjectCacheTest.php @@ -2,10 +2,28 @@ use Wikimedia\TestingAccessWrapper; -class WANObjectCacheTest extends PHPUnit_Framework_TestCase { +/** + * @covers WANObjectCache::wrap + * @covers WANObjectCache::unwrap + * @covers WANObjectCache::worthRefreshExpiring + * @covers WANObjectCache::worthRefreshPopular + * @covers WANObjectCache::isValid + * @covers WANObjectCache::getWarmupKeyMisses + * @covers WANObjectCache::prefixCacheKeys + * @covers WANObjectCache::getProcessCache + * @covers WANObjectCache::getNonProcessCachedKeys + * @covers WANObjectCache::getRawKeysForWarmup + * @covers WANObjectCache::getInterimValue + * @covers WANObjectCache::setInterimValue + */ +class WANObjectCacheTest extends PHPUnit\Framework\TestCase { + + use MediaWikiCoversValidator; + use PHPUnit4And6Compat; + /** @var WANObjectCache */ private $cache; - /**@var BagOStuff */ + /** @var BagOStuff */ private $internalCache; protected function setUp() { @@ -145,8 +163,9 @@ class WANObjectCacheTest extends PHPUnit_Framework_TestCase { $this->assertEquals( 9, $hit, "Values evicted" ); $key = reset( $keys ); - // Get into cache + // Get into cache (default process cache group) $this->cache->getWithSetCallback( $key, 100, $callback, [ 'pcTTL' => 5 ] ); + $this->assertEquals( 10, $hit, "Value calculated" ); $this->cache->getWithSetCallback( $key, 100, $callback, [ 'pcTTL' => 5 ] ); $this->assertEquals( 10, $hit, "Value cached" ); $outerCallback = function () use ( &$callback, $key ) { @@ -154,7 +173,8 @@ class WANObjectCacheTest extends PHPUnit_Framework_TestCase { return 43 + $v; }; - $this->cache->getWithSetCallback( $key, 100, $outerCallback ); + // Outer key misses and refuses inner key process cache value + $this->cache->getWithSetCallback( "$key-miss-outer", 100, $outerCallback ); $this->assertEquals( 11, $hit, "Nested callback value process cache skipped" ); } @@ -199,15 +219,17 @@ class WANObjectCacheTest extends PHPUnit_Framework_TestCase { $this->assertGreaterThanOrEqual( 19, $curTTL, 'Current TTL between 19-20 (overriden)' ); $wasSet = 0; - $v = $cache->getWithSetCallback( $key, 30, $func, [ - 'lowTTL' => 0, - 'lockTSE' => 5, - ] + $extOpts ); + $v = $cache->getWithSetCallback( + $key, 30, $func, [ 'lowTTL' => 0, 'lockTSE' => 5 ] + $extOpts ); $this->assertEquals( $value, $v, "Value returned" ); $this->assertEquals( 0, $wasSet, "Value not regenerated" ); - $priorTime = microtime( true ); - usleep( 1 ); + $mockWallClock = microtime( true ); + $priorTime = $mockWallClock; // reference time + $cache->setMockTime( $mockWallClock ); + + $mockWallClock += 1; + $wasSet = 0; $v = $cache->getWithSetCallback( $key, 30, $func, [ 'checkKeys' => [ $cKey1, $cKey2 ] ] + $extOpts @@ -221,7 +243,8 @@ class WANObjectCacheTest extends PHPUnit_Framework_TestCase { $t2 = $cache->getCheckKeyTime( $cKey2 ); $this->assertGreaterThanOrEqual( $priorTime, $t2, 'Check keys generated on miss' ); - $priorTime = microtime( true ); + $mockWallClock += 0.01; + $priorTime = $mockWallClock; // reference time $wasSet = 0; $v = $cache->getWithSetCallback( $key, 30, $func, [ 'checkKeys' => [ $cKey1, $cKey2 ] ] + $extOpts @@ -250,6 +273,96 @@ class WANObjectCacheTest extends PHPUnit_Framework_TestCase { $v = $cache->getWithSetCallback( $key, 30, $func, [ 'pcTTL' => 5 ] + $extOpts ); $this->assertEquals( $value, $v, "Value still returned after deleted" ); $this->assertEquals( 1, $wasSet, "Value process cached while deleted" ); + + $oldValReceived = -1; + $oldAsOfReceived = -1; + $checkFunc = function ( $oldVal, &$ttl, array $setOpts, $oldAsOf ) + use ( &$oldValReceived, &$oldAsOfReceived, &$wasSet ) { + ++$wasSet; + $oldValReceived = $oldVal; + $oldAsOfReceived = $oldAsOf; + + return 'xxx' . $wasSet; + }; + + $mockWallClock = microtime( true ); + $priorTime = $mockWallClock; // reference time + + $wasSet = 0; + $key = wfRandomString(); + $v = $cache->getWithSetCallback( + $key, 30, $checkFunc, [ 'staleTTL' => 50 ] + $extOpts ); + $this->assertEquals( 'xxx1', $v, "Value returned" ); + $this->assertEquals( false, $oldValReceived, "Callback got no stale value" ); + $this->assertEquals( null, $oldAsOfReceived, "Callback got no stale value" ); + + $mockWallClock += 40; + $v = $cache->getWithSetCallback( + $key, 30, $checkFunc, [ 'staleTTL' => 50 ] + $extOpts ); + $this->assertEquals( 'xxx2', $v, "Value still returned after expired" ); + $this->assertEquals( 2, $wasSet, "Value recalculated while expired" ); + $this->assertEquals( 'xxx1', $oldValReceived, "Callback got stale value" ); + $this->assertNotEquals( null, $oldAsOfReceived, "Callback got stale value" ); + + $mockWallClock += 260; + $v = $cache->getWithSetCallback( + $key, 30, $checkFunc, [ 'staleTTL' => 50 ] + $extOpts ); + $this->assertEquals( 'xxx3', $v, "Value still returned after expired" ); + $this->assertEquals( 3, $wasSet, "Value recalculated while expired" ); + $this->assertEquals( false, $oldValReceived, "Callback got no stale value" ); + $this->assertEquals( null, $oldAsOfReceived, "Callback got no stale value" ); + + $mockWallClock = ( $priorTime - $cache::HOLDOFF_TTL - 1 ); + $wasSet = 0; + $key = wfRandomString(); + $checkKey = $cache->makeKey( 'template', 'X' ); + $cache->touchCheckKey( $checkKey ); // init check key + $mockWallClock = $priorTime; + $v = $cache->getWithSetCallback( + $key, + $cache::TTL_INDEFINITE, + $checkFunc, + [ 'graceTTL' => $cache::TTL_WEEK, 'checkKeys' => [ $checkKey ] ] + $extOpts + ); + $this->assertEquals( 'xxx1', $v, "Value returned" ); + $this->assertEquals( 1, $wasSet, "Value computed" ); + $this->assertEquals( false, $oldValReceived, "Callback got no stale value" ); + $this->assertEquals( null, $oldAsOfReceived, "Callback got no stale value" ); + + $mockWallClock += $cache::TTL_HOUR; // some time passes + $v = $cache->getWithSetCallback( + $key, + $cache::TTL_INDEFINITE, + $checkFunc, + [ 'graceTTL' => $cache::TTL_WEEK, 'checkKeys' => [ $checkKey ] ] + $extOpts + ); + $this->assertEquals( 'xxx1', $v, "Cached value returned" ); + $this->assertEquals( 1, $wasSet, "Cached value returned" ); + + $cache->touchCheckKey( $checkKey ); // make key stale + $mockWallClock += 0.01; // ~1 week left of grace (barely stale to avoid refreshes) + + $v = $cache->getWithSetCallback( + $key, + $cache::TTL_INDEFINITE, + $checkFunc, + [ 'graceTTL' => $cache::TTL_WEEK, 'checkKeys' => [ $checkKey ] ] + $extOpts + ); + $this->assertEquals( 'xxx1', $v, "Value still returned after expired (in grace)" ); + $this->assertEquals( 1, $wasSet, "Value still returned after expired (in grace)" ); + + // Change of refresh increase to unity as staleness approaches graceTTL + $mockWallClock += $cache::TTL_WEEK; // 8 days of being stale + $v = $cache->getWithSetCallback( + $key, + $cache::TTL_INDEFINITE, + $checkFunc, + [ 'graceTTL' => $cache::TTL_WEEK, 'checkKeys' => [ $checkKey ] ] + $extOpts + ); + $this->assertEquals( 'xxx2', $v, "Value was recomputed (past grace)" ); + $this->assertEquals( 2, $wasSet, "Value was recomputed (past grace)" ); + $this->assertEquals( 'xxx1', $oldValReceived, "Callback got post-grace stale value" ); + $this->assertNotEquals( null, $oldAsOfReceived, "Callback got post-grace stale value" ); } public static function getWithSetCallback_provider() { @@ -259,10 +372,124 @@ class WANObjectCacheTest extends PHPUnit_Framework_TestCase { ]; } + public function testPreemtiveRefresh() { + $value = 'KatCafe'; + $wasSet = 0; + $func = function ( $old, &$ttl, &$opts, $asOf ) use ( &$wasSet, &$value ) + { + ++$wasSet; + return $value; + }; + + $cache = new NearExpiringWANObjectCache( [ + 'cache' => new HashBagOStuff(), + 'pool' => 'empty', + ] ); + + $wasSet = 0; + $key = wfRandomString(); + $opts = [ 'lowTTL' => 30 ]; + $v = $cache->getWithSetCallback( $key, 20, $func, $opts ); + $this->assertEquals( $value, $v, "Value returned" ); + $this->assertEquals( 1, $wasSet, "Value calculated" ); + $v = $cache->getWithSetCallback( $key, 20, $func, $opts ); + $this->assertEquals( 2, $wasSet, "Value re-calculated" ); + + $wasSet = 0; + $key = wfRandomString(); + $opts = [ 'lowTTL' => 1 ]; + $v = $cache->getWithSetCallback( $key, 30, $func, $opts ); + $this->assertEquals( $value, $v, "Value returned" ); + $this->assertEquals( 1, $wasSet, "Value calculated" ); + $v = $cache->getWithSetCallback( $key, 30, $func, $opts ); + $this->assertEquals( 1, $wasSet, "Value cached" ); + + $asycList = []; + $asyncHandler = function ( $callback ) use ( &$asycList ) { + $asycList[] = $callback; + }; + $cache = new NearExpiringWANObjectCache( [ + 'cache' => new HashBagOStuff(), + 'pool' => 'empty', + 'asyncHandler' => $asyncHandler + ] ); + + $mockWallClock = microtime( true ); + $priorTime = $mockWallClock; // reference time + $cache->setMockTime( $mockWallClock ); + + $wasSet = 0; + $key = wfRandomString(); + $opts = [ 'lowTTL' => 100 ]; + $v = $cache->getWithSetCallback( $key, 300, $func, $opts ); + $this->assertEquals( $value, $v, "Value returned" ); + $this->assertEquals( 1, $wasSet, "Value calculated" ); + $v = $cache->getWithSetCallback( $key, 300, $func, $opts ); + $this->assertEquals( 1, $wasSet, "Cached value used" ); + $this->assertEquals( $v, $value, "Value cached" ); + + $mockWallClock += 250; + $v = $cache->getWithSetCallback( $key, 300, $func, $opts ); + $this->assertEquals( $value, $v, "Value returned" ); + $this->assertEquals( 1, $wasSet, "Stale value used" ); + $this->assertEquals( 1, count( $asycList ), "Refresh deferred." ); + $value = 'NewCatsInTown'; // change callback return value + $asycList[0](); // run the refresh callback + $asycList = []; + $this->assertEquals( 2, $wasSet, "Value calculated at later time" ); + $this->assertEquals( 0, count( $asycList ), "No deferred refreshes added." ); + $v = $cache->getWithSetCallback( $key, 300, $func, $opts ); + $this->assertEquals( $value, $v, "New value stored" ); + + $cache = new PopularityRefreshingWANObjectCache( [ + 'cache' => new HashBagOStuff(), + 'pool' => 'empty' + ] ); + + $mockWallClock = $priorTime; + $cache->setMockTime( $mockWallClock ); + + $wasSet = 0; + $key = wfRandomString(); + $opts = [ 'hotTTR' => 900 ]; + $v = $cache->getWithSetCallback( $key, 60, $func, $opts ); + $this->assertEquals( $value, $v, "Value returned" ); + $this->assertEquals( 1, $wasSet, "Value calculated" ); + + $mockWallClock += 30; + + $v = $cache->getWithSetCallback( $key, 60, $func, $opts ); + $this->assertEquals( 1, $wasSet, "Value cached" ); + + $mockWallClock = $priorTime; + $wasSet = 0; + $key = wfRandomString(); + $opts = [ 'hotTTR' => 10 ]; + $v = $cache->getWithSetCallback( $key, 60, $func, $opts ); + $this->assertEquals( $value, $v, "Value returned" ); + $this->assertEquals( 1, $wasSet, "Value calculated" ); + + $mockWallClock += 30; + + $v = $cache->getWithSetCallback( $key, 60, $func, $opts ); + $this->assertEquals( $value, $v, "Value returned" ); + $this->assertEquals( 2, $wasSet, "Value re-calculated" ); + } + + /** + * @covers WANObjectCache::getWithSetCallback() + * @covers WANObjectCache::doGetWithSetCallback() + */ + public function testGetWithSetCallback_invalidCallback() { + $this->setExpectedException( InvalidArgumentException::class ); + $this->cache->getWithSetCallback( 'key', 30, 'invalid callback' ); + } + /** * @dataProvider getMultiWithSetCallback_provider - * @covers WANObjectCache::getMultiWithSetCallback() - * @covers WANObjectCache::makeMultiKeys() + * @covers WANObjectCache::getMultiWithSetCallback + * @covers WANObjectCache::makeMultiKeys + * @covers WANObjectCache::getMulti * @param array $extOpts * @param bool $versioned */ @@ -317,8 +544,12 @@ class WANObjectCacheTest extends PHPUnit_Framework_TestCase { $this->assertEquals( 1, $wasSet, "Value not regenerated" ); $this->assertEquals( 0, $cache->getWarmupKeyMisses(), "Keys warmed in process cache" ); - $priorTime = microtime( true ); - usleep( 1 ); + $mockWallClock = microtime( true ); + $priorTime = $mockWallClock; // reference time + $cache->setMockTime( $mockWallClock ); + + $mockWallClock += 1; + $wasSet = 0; $keyedIds = new ArrayIterator( [ $keyB => 'efef' ] ); $v = $cache->getMultiWithSetCallback( @@ -333,7 +564,8 @@ class WANObjectCacheTest extends PHPUnit_Framework_TestCase { $t2 = $cache->getCheckKeyTime( $cKey2 ); $this->assertGreaterThanOrEqual( $priorTime, $t2, 'Check keys generated on miss' ); - $priorTime = microtime( true ); + $mockWallClock += 0.01; + $priorTime = $mockWallClock; $value = "@43636$"; $wasSet = 0; $keyedIds = new ArrayIterator( [ $keyC => 43636 ] ); @@ -398,7 +630,7 @@ class WANObjectCacheTest extends PHPUnit_Framework_TestCase { $this->assertEquals( count( $ids ), $calls, "Values cached" ); // Mock the BagOStuff to assure only one getMulti() call given process caching - $localBag = $this->getMockBuilder( 'HashBagOStuff' ) + $localBag = $this->getMockBuilder( HashBagOStuff::class ) ->setMethods( [ 'getMulti' ] )->getMock(); $localBag->expects( $this->exactly( 1 ) )->method( 'getMulti' )->willReturn( [ WANObjectCache::VALUE_KEY_PREFIX . 'k1' => 'val-id1', @@ -483,8 +715,12 @@ class WANObjectCacheTest extends PHPUnit_Framework_TestCase { $this->assertEquals( 1, $wasSet, "Value not regenerated" ); $this->assertEquals( 0, $cache->getWarmupKeyMisses(), "Keys warmed in process cache" ); - $priorTime = microtime( true ); - usleep( 1 ); + $mockWallClock = microtime( true ); + $priorTime = $mockWallClock; // reference time + $cache->setMockTime( $mockWallClock ); + + $mockWallClock += 1; + $wasSet = 0; $keyedIds = new ArrayIterator( [ $keyB => 'efef' ] ); $v = $cache->getMultiWithUnionSetCallback( @@ -497,7 +733,8 @@ class WANObjectCacheTest extends PHPUnit_Framework_TestCase { $t2 = $cache->getCheckKeyTime( $cKey2 ); $this->assertGreaterThanOrEqual( $priorTime, $t2, 'Check keys generated on miss' ); - $priorTime = microtime( true ); + $mockWallClock += 0.01; + $priorTime = $mockWallClock; $value = "@43636$"; $wasSet = 0; $keyedIds = new ArrayIterator( [ $keyC => 43636 ] ); @@ -585,8 +822,6 @@ class WANObjectCacheTest extends PHPUnit_Framework_TestCase { $calls = 0; $func = function () use ( &$calls, $value, $cache, $key ) { ++$calls; - // Immediately kill any mutex rather than waiting a second - $cache->delete( $cache::MUTEX_KEY_PREFIX . $key ); return $value; }; @@ -594,7 +829,7 @@ class WANObjectCacheTest extends PHPUnit_Framework_TestCase { $this->assertEquals( $value, $ret ); $this->assertEquals( 1, $calls, 'Value was populated' ); - // Acquire a lock to verify that getWithSetCallback uses lockTSE properly + // Acquire the mutex to verify that getWithSetCallback uses lockTSE properly $this->internalCache->add( $cache::MUTEX_KEY_PREFIX . $key, 1, 0 ); $checkKeys = [ wfRandomString() ]; // new check keys => force misses @@ -611,13 +846,14 @@ class WANObjectCacheTest extends PHPUnit_Framework_TestCase { $ret = $cache->getWithSetCallback( $key, 30, $func, [ 'lockTSE' => 5, 'checkKeys' => $checkKeys ] ); - $this->assertEquals( $value, $ret, 'Callback was not used; used interim' ); - $this->assertEquals( 2, $calls, 'Callback was not used; used interim' ); + $this->assertEquals( $value, $ret, 'Callback was not used; used interim (mutex failed)' ); + $this->assertEquals( 2, $calls, 'Callback was not used; used interim (mutex failed)' ); } /** * @covers WANObjectCache::getWithSetCallback() * @covers WANObjectCache::doGetWithSetCallback() + * @covers WANObjectCache::set() */ public function testLockTSESlow() { $cache = $this->cache; @@ -733,8 +969,12 @@ class WANObjectCacheTest extends PHPUnit_Framework_TestCase { $cKey1 = wfRandomString(); $cKey2 = wfRandomString(); - $priorTime = microtime( true ); - usleep( 1 ); + $mockWallClock = microtime( true ); + $priorTime = $mockWallClock; // reference time + $cache->setMockTime( $mockWallClock ); + + $mockWallClock += 1; + $curTTLs = []; $this->assertEquals( [ $key1 => $value1, $key2 => $value2 ], @@ -749,7 +989,8 @@ class WANObjectCacheTest extends PHPUnit_Framework_TestCase { $this->assertLessThanOrEqual( 0, $curTTLs[$key1], 'Key 1 has current TTL <= 0' ); $this->assertLessThanOrEqual( 0, $curTTLs[$key2], 'Key 2 has current TTL <= 0' ); - usleep( 1 ); + $mockWallClock += 1; + $curTTLs = []; $this->assertEquals( [ $key1 => $value1, $key2 => $value2 ], @@ -775,12 +1016,16 @@ class WANObjectCacheTest extends PHPUnit_Framework_TestCase { $value1 = wfRandomString(); $value2 = wfRandomString(); + $mockWallClock = microtime( true ); + $cache->setMockTime( $mockWallClock ); + // Fake initial check key to be set in the past. Otherwise we'd have to sleep for // several seconds during the test to assert the behaviour. foreach ( [ $checkAll, $check1, $check2 ] as $checkKey ) { $cache->touchCheckKey( $checkKey, WANObjectCache::HOLDOFF_NONE ); } - usleep( 100 ); + + $mockWallClock += 0.100; $cache->set( 'key1', $value1, 10 ); $cache->set( 'key2', $value2, 10 ); @@ -802,6 +1047,7 @@ class WANObjectCacheTest extends PHPUnit_Framework_TestCase { $this->assertGreaterThanOrEqual( 9.5, $curTTLs['key2'], 'Initial ttls' ); $this->assertLessThanOrEqual( 10.5, $curTTLs['key2'], 'Initial ttls' ); + $mockWallClock += 0.100; $cache->touchCheckKey( $check1 ); $curTTLs = []; @@ -871,7 +1117,9 @@ class WANObjectCacheTest extends PHPUnit_Framework_TestCase { } /** - * @covers WANObjectCache::delete() + * @covers WANObjectCache::delete + * @covers WANObjectCache::relayDelete + * @covers WANObjectCache::relayPurge */ public function testDelete() { $key = wfRandomString(); @@ -911,6 +1159,8 @@ class WANObjectCacheTest extends PHPUnit_Framework_TestCase { /** * @dataProvider getWithSetCallback_versions_provider + * @covers WANObjectCache::getWithSetCallback() + * @covers WANObjectCache::doGetWithSetCallback() * @param array $extOpts * @param bool $versioned */ @@ -918,53 +1168,69 @@ class WANObjectCacheTest extends PHPUnit_Framework_TestCase { $cache = $this->cache; $key = wfRandomString(); - $value = wfRandomString(); + $valueV1 = wfRandomString(); + $valueV2 = [ wfRandomString() ]; $wasSet = 0; - $func = function ( $old, &$ttl ) use ( &$wasSet, $value ) { + $funcV1 = function () use ( &$wasSet, $valueV1 ) { ++$wasSet; - return $value; + + return $valueV1; + }; + + $priorValue = false; + $priorAsOf = null; + $funcV2 = function ( $oldValue, &$ttl, $setOpts, $oldAsOf ) + use ( &$wasSet, $valueV2, &$priorValue, &$priorAsOf ) { + $priorValue = $oldValue; + $priorAsOf = $oldAsOf; + ++$wasSet; + + return $valueV2; // new array format }; // Set the main key (version N if versioned) $wasSet = 0; - $v = $cache->getWithSetCallback( $key, 30, $func, $extOpts ); - $this->assertEquals( $value, $v, "Value returned" ); + $v = $cache->getWithSetCallback( $key, 30, $funcV1, $extOpts ); + $this->assertEquals( $valueV1, $v, "Value returned" ); $this->assertEquals( 1, $wasSet, "Value regenerated" ); - $cache->getWithSetCallback( $key, 30, $func, $extOpts ); + $cache->getWithSetCallback( $key, 30, $funcV1, $extOpts ); $this->assertEquals( 1, $wasSet, "Value not regenerated" ); - // Set the key for version N+1 (if versioned) + $this->assertEquals( $valueV1, $v, "Value not regenerated" ); + if ( $versioned ) { + // Set the key for version N+1 format $verOpts = [ 'version' => $extOpts['version'] + 1 ]; - - $wasSet = 0; - $v = $cache->getWithSetCallback( $key, 30, $func, $verOpts + $extOpts ); - $this->assertEquals( $value, $v, "Value returned" ); - $this->assertEquals( 1, $wasSet, "Value regenerated" ); - - $wasSet = 0; - $v = $cache->getWithSetCallback( $key, 30, $func, $verOpts + $extOpts ); - $this->assertEquals( $value, $v, "Value returned" ); - $this->assertEquals( 0, $wasSet, "Value not regenerated" ); + } else { + // Start versioning now with the unversioned key still there + $verOpts = [ 'version' => 1 ]; } + // Value goes to secondary key since V1 already used $key $wasSet = 0; - $cache->getWithSetCallback( $key, 30, $func, $extOpts ); - $this->assertEquals( 0, $wasSet, "Value not regenerated" ); + $v = $cache->getWithSetCallback( $key, 30, $funcV2, $verOpts + $extOpts ); + $this->assertEquals( $valueV2, $v, "Value returned" ); + $this->assertEquals( 1, $wasSet, "Value regenerated" ); + $this->assertEquals( false, $priorValue, "Old value not given due to old format" ); + $this->assertEquals( null, $priorAsOf, "Old value not given due to old format" ); $wasSet = 0; - $cache->delete( $key ); - $v = $cache->getWithSetCallback( $key, 30, $func, $extOpts ); - $this->assertEquals( $value, $v, "Value returned" ); + $v = $cache->getWithSetCallback( $key, 30, $funcV2, $verOpts + $extOpts ); + $this->assertEquals( $valueV2, $v, "Value not regenerated (secondary key)" ); + $this->assertEquals( 0, $wasSet, "Value not regenerated (secondary key)" ); + + // Clear out the older or unversioned key + $cache->delete( $key, 0 ); + + // Set the key for next/first versioned format + $wasSet = 0; + $v = $cache->getWithSetCallback( $key, 30, $funcV2, $verOpts + $extOpts ); + $this->assertEquals( $valueV2, $v, "Value returned" ); $this->assertEquals( 1, $wasSet, "Value regenerated" ); - if ( $versioned ) { - $wasSet = 0; - $verOpts = [ 'version' => $extOpts['version'] + 1 ]; - $v = $cache->getWithSetCallback( $key, 30, $func, $verOpts + $extOpts ); - $this->assertEquals( $value, $v, "Value returned" ); - $this->assertEquals( 1, $wasSet, "Value regenerated" ); - } + $v = $cache->getWithSetCallback( $key, 30, $funcV2, $verOpts + $extOpts ); + $this->assertEquals( $valueV2, $v, "Value not regenerated (main key)" ); + $this->assertEquals( 1, $wasSet, "Value not regenerated (main key)" ); } public static function getWithSetCallback_versions_provider() { @@ -975,41 +1241,100 @@ class WANObjectCacheTest extends PHPUnit_Framework_TestCase { } /** - * @covers WANObjectCache::touchCheckKey() - * @covers WANObjectCache::resetCheckKey() - * @covers WANObjectCache::getCheckKeyTime() + * @covers WANObjectCache::useInterimHoldOffCaching + * @covers WANObjectCache::getInterimValue + */ + public function testInterimHoldOffCaching() { + $cache = $this->cache; + + $value = 'CRL-40-940'; + $wasCalled = 0; + $func = function () use ( &$wasCalled, $value ) { + $wasCalled++; + + return $value; + }; + + $cache->useInterimHoldOffCaching( true ); + + $key = wfRandomString( 32 ); + $v = $cache->getWithSetCallback( $key, 60, $func ); + $v = $cache->getWithSetCallback( $key, 60, $func ); + $this->assertEquals( 1, $wasCalled, 'Value cached' ); + $cache->delete( $key ); + $v = $cache->getWithSetCallback( $key, 60, $func ); + $this->assertEquals( 2, $wasCalled, 'Value regenerated (got mutex)' ); // sets interim + $v = $cache->getWithSetCallback( $key, 60, $func ); + $this->assertEquals( 3, $wasCalled, 'Value regenerated (got mutex)' ); // sets interim + // Lock up the mutex so interim cache is used + $this->internalCache->add( $cache::MUTEX_KEY_PREFIX . $key, 1, 0 ); + $v = $cache->getWithSetCallback( $key, 60, $func ); + $this->assertEquals( 3, $wasCalled, 'Value interim cached (failed mutex)' ); + $this->internalCache->delete( $cache::MUTEX_KEY_PREFIX . $key ); + + $cache->useInterimHoldOffCaching( false ); + + $wasCalled = 0; + $key = wfRandomString( 32 ); + $v = $cache->getWithSetCallback( $key, 60, $func ); + $v = $cache->getWithSetCallback( $key, 60, $func ); + $this->assertEquals( 1, $wasCalled, 'Value cached' ); + $cache->delete( $key ); + $v = $cache->getWithSetCallback( $key, 60, $func ); + $this->assertEquals( 2, $wasCalled, 'Value regenerated (got mutex)' ); + $v = $cache->getWithSetCallback( $key, 60, $func ); + $this->assertEquals( 3, $wasCalled, 'Value still regenerated (got mutex)' ); + $v = $cache->getWithSetCallback( $key, 60, $func ); + $this->assertEquals( 4, $wasCalled, 'Value still regenerated (got mutex)' ); + // Lock up the mutex so interim cache is used + $this->internalCache->add( $cache::MUTEX_KEY_PREFIX . $key, 1, 0 ); + $v = $cache->getWithSetCallback( $key, 60, $func ); + $this->assertEquals( 5, $wasCalled, 'Value still regenerated (failed mutex)' ); + } + + /** + * @covers WANObjectCache::touchCheckKey + * @covers WANObjectCache::resetCheckKey + * @covers WANObjectCache::getCheckKeyTime + * @covers WANObjectCache::getMultiCheckKeyTime + * @covers WANObjectCache::makePurgeValue + * @covers WANObjectCache::parsePurgeValue */ public function testTouchKeys() { + $cache = $this->cache; $key = wfRandomString(); - $priorTime = microtime( true ); - usleep( 100 ); - $t0 = $this->cache->getCheckKeyTime( $key ); + $mockWallClock = microtime( true ); + $priorTime = $mockWallClock; // reference time + $cache->setMockTime( $mockWallClock ); + + $mockWallClock += 0.100; + $t0 = $cache->getCheckKeyTime( $key ); $this->assertGreaterThanOrEqual( $priorTime, $t0, 'Check key auto-created' ); - $priorTime = microtime( true ); - usleep( 100 ); - $this->cache->touchCheckKey( $key ); - $t1 = $this->cache->getCheckKeyTime( $key ); + $priorTime = $mockWallClock; + $mockWallClock += 0.100; + $cache->touchCheckKey( $key ); + $t1 = $cache->getCheckKeyTime( $key ); $this->assertGreaterThanOrEqual( $priorTime, $t1, 'Check key created' ); - $t2 = $this->cache->getCheckKeyTime( $key ); + $t2 = $cache->getCheckKeyTime( $key ); $this->assertEquals( $t1, $t2, 'Check key time did not change' ); - usleep( 100 ); - $this->cache->touchCheckKey( $key ); - $t3 = $this->cache->getCheckKeyTime( $key ); + $mockWallClock += 0.100; + $cache->touchCheckKey( $key ); + $t3 = $cache->getCheckKeyTime( $key ); $this->assertGreaterThan( $t2, $t3, 'Check key time increased' ); - $t4 = $this->cache->getCheckKeyTime( $key ); + $t4 = $cache->getCheckKeyTime( $key ); $this->assertEquals( $t3, $t4, 'Check key time did not change' ); - usleep( 100 ); - $this->cache->resetCheckKey( $key ); - $t5 = $this->cache->getCheckKeyTime( $key ); + $mockWallClock += 0.100; + $cache->resetCheckKey( $key ); + $t5 = $cache->getCheckKeyTime( $key ); $this->assertGreaterThan( $t4, $t5, 'Check key time increased' ); - $t6 = $this->cache->getCheckKeyTime( $key ); + $t6 = $cache->getCheckKeyTime( $key ); $this->assertEquals( $t5, $t6, 'Check key time did not change' ); } @@ -1101,6 +1426,34 @@ class WANObjectCacheTest extends PHPUnit_Framework_TestCase { } /** + * @covers WANObjectCache::reap() + */ + public function testReap_fail() { + $backend = $this->getMockBuilder( EmptyBagOStuff::class ) + ->setMethods( [ 'get', 'changeTTL' ] )->getMock(); + $backend->expects( $this->once() )->method( 'get' ) + ->willReturn( [ + WANObjectCache::FLD_VERSION => WANObjectCache::VERSION, + WANObjectCache::FLD_VALUE => 'value', + WANObjectCache::FLD_TTL => 3600, + WANObjectCache::FLD_TIME => 300, + ] ); + $backend->expects( $this->once() )->method( 'changeTTL' ) + ->willReturn( false ); + + $wanCache = new WANObjectCache( [ + 'cache' => $backend, + 'pool' => 'testcache-hash', + 'relayer' => new EventRelayerNull( [] ) + ] ); + + $isStale = null; + $ret = $wanCache->reap( 'key', 360, $isStale ); + $this->assertTrue( $isStale, 'value was stale' ); + $this->assertFalse( $ret, 'changeTTL failed' ); + } + + /** * @covers WANObjectCache::set() */ public function testSetWithLag() { @@ -1135,14 +1488,17 @@ class WANObjectCacheTest extends PHPUnit_Framework_TestCase { } public function testMcRouterSupport() { - $localBag = $this->getMockBuilder( 'EmptyBagOStuff' ) + $localBag = $this->getMockBuilder( EmptyBagOStuff::class ) ->setMethods( [ 'set', 'delete' ] )->getMock(); $localBag->expects( $this->never() )->method( 'set' ); $localBag->expects( $this->never() )->method( 'delete' ); $wanCache = new WANObjectCache( [ 'cache' => $localBag, 'pool' => 'testcache-hash', - 'relayer' => new EventRelayerNull( [] ) + 'relayer' => new EventRelayerNull( [] ), + 'mcrouterAware' => true, + 'region' => 'pmtpa', + 'cluster' => 'mw-wan' ] ); $valFunc = function () { return 1; @@ -1159,6 +1515,60 @@ class WANObjectCacheTest extends PHPUnit_Framework_TestCase { $wanCache->reap( 'zzz', time() - 300 ); } + public function testMcRouterSupportBroadcastDelete() { + $localBag = $this->getMockBuilder( EmptyBagOStuff::class ) + ->setMethods( [ 'set' ] )->getMock(); + $wanCache = new WANObjectCache( [ + 'cache' => $localBag, + 'pool' => 'testcache-hash', + 'relayer' => new EventRelayerNull( [] ), + 'mcrouterAware' => true, + 'region' => 'pmtpa', + 'cluster' => 'mw-wan' + ] ); + + $localBag->expects( $this->once() )->method( 'set' ) + ->with( "/*/mw-wan/" . $wanCache::VALUE_KEY_PREFIX . "test" ); + + $wanCache->delete( 'test' ); + } + + public function testMcRouterSupportBroadcastTouchCK() { + $localBag = $this->getMockBuilder( EmptyBagOStuff::class ) + ->setMethods( [ 'set' ] )->getMock(); + $wanCache = new WANObjectCache( [ + 'cache' => $localBag, + 'pool' => 'testcache-hash', + 'relayer' => new EventRelayerNull( [] ), + 'mcrouterAware' => true, + 'region' => 'pmtpa', + 'cluster' => 'mw-wan' + ] ); + + $localBag->expects( $this->once() )->method( 'set' ) + ->with( "/*/mw-wan/" . $wanCache::TIME_KEY_PREFIX . "test" ); + + $wanCache->touchCheckKey( 'test' ); + } + + public function testMcRouterSupportBroadcastResetCK() { + $localBag = $this->getMockBuilder( EmptyBagOStuff::class ) + ->setMethods( [ 'delete' ] )->getMock(); + $wanCache = new WANObjectCache( [ + 'cache' => $localBag, + 'pool' => 'testcache-hash', + 'relayer' => new EventRelayerNull( [] ), + 'mcrouterAware' => true, + 'region' => 'pmtpa', + 'cluster' => 'mw-wan' + ] ); + + $localBag->expects( $this->once() )->method( 'delete' ) + ->with( "/*/mw-wan/" . $wanCache::TIME_KEY_PREFIX . "test" ); + + $wanCache->resetCheckKey( 'test' ); + } + /** * @dataProvider provideAdaptiveTTL * @covers WANObjectCache::adaptiveTTL() @@ -1193,6 +1603,40 @@ class WANObjectCacheTest extends PHPUnit_Framework_TestCase { } /** + * @covers WANObjectCache::__construct + * @covers WANObjectCache::newEmpty + */ + public function testNewEmpty() { + $this->assertInstanceOf( + WANObjectCache::class, + WANObjectCache::newEmpty() + ); + } + + /** + * @covers WANObjectCache::setLogger + */ + public function testSetLogger() { + $this->assertSame( null, $this->cache->setLogger( new Psr\Log\NullLogger ) ); + } + + /** + * @covers WANObjectCache::getQoS + */ + public function testGetQoS() { + $backend = $this->getMockBuilder( HashBagOStuff::class ) + ->setMethods( [ 'getQoS' ] )->getMock(); + $backend->expects( $this->once() )->method( 'getQoS' ) + ->willReturn( BagOStuff::QOS_UNKNOWN ); + $wanCache = new WANObjectCache( [ 'cache' => $backend ] ); + + $this->assertSame( + $wanCache::QOS_UNKNOWN, + $wanCache->getQoS( $wanCache::ATTR_EMULATION ) + ); + } + + /** * @covers WANObjectCache::makeKey */ public function testMakeKey() { @@ -1227,4 +1671,41 @@ class WANObjectCacheTest extends PHPUnit_Framework_TestCase { $this->assertSame( 'special', $wanCache->makeGlobalKey( 'a', 'b' ) ); } + + public static function statsKeyProvider() { + return [ + [ 'domain:page:5', 'page' ], + [ 'domain:main-key', 'main-key' ], + [ 'domain:page:history', 'page' ], + [ 'missingdomainkey', 'missingdomainkey' ] + ]; + } + + /** + * @dataProvider statsKeyProvider + * @covers WANObjectCache::determineKeyClass + */ + public function testStatsKeyClass( $key, $class ) { + $wanCache = TestingAccessWrapper::newFromObject( new WANObjectCache( [ + 'cache' => new HashBagOStuff, + 'pool' => 'testcache-hash', + 'relayer' => new EventRelayerNull( [] ) + ] ) ); + + $this->assertEquals( $class, $wanCache->determineKeyClass( $key ) ); + } +} + +class NearExpiringWANObjectCache extends WANObjectCache { + const CLOCK_SKEW = 1; + + protected function worthRefreshExpiring( $curTTL, $lowTTL ) { + return ( $curTTL > 0 && ( $curTTL + self::CLOCK_SKEW ) < $lowTTL ); + } +} + +class PopularityRefreshingWANObjectCache extends WANObjectCache { + protected function worthRefreshPopular( $asOf, $ageNew, $timeTillRefresh, $now ) { + return ( ( $now - $asOf ) > $timeTillRefresh ); + } } diff --git a/www/wiki/tests/phpunit/includes/libs/rdbms/TransactionProfilerTest.php b/www/wiki/tests/phpunit/includes/libs/rdbms/TransactionProfilerTest.php index b6ea4265..538d625c 100644 --- a/www/wiki/tests/phpunit/includes/libs/rdbms/TransactionProfilerTest.php +++ b/www/wiki/tests/phpunit/includes/libs/rdbms/TransactionProfilerTest.php @@ -3,10 +3,16 @@ use Wikimedia\Rdbms\TransactionProfiler; use Psr\Log\LoggerInterface; -class TransactionProfilerTest extends PHPUnit_Framework_TestCase { +/** + * @covers \Wikimedia\Rdbms\TransactionProfiler + */ +class TransactionProfilerTest extends PHPUnit\Framework\TestCase { + + use MediaWikiCoversValidator; + public function testAffected() { $logger = $this->getMockBuilder( LoggerInterface::class )->getMock(); - $logger->expects( $this->exactly( 3 ) )->method( 'info' ); + $logger->expects( $this->exactly( 3 ) )->method( 'warning' ); $tp = new TransactionProfiler(); $tp->setLogger( $logger ); @@ -21,7 +27,7 @@ class TransactionProfilerTest extends PHPUnit_Framework_TestCase { public function testReadTime() { $logger = $this->getMockBuilder( LoggerInterface::class )->getMock(); // 1 per query - $logger->expects( $this->exactly( 2 ) )->method( 'info' ); + $logger->expects( $this->exactly( 2 ) )->method( 'warning' ); $tp = new TransactionProfiler(); $tp->setLogger( $logger ); @@ -36,7 +42,7 @@ class TransactionProfilerTest extends PHPUnit_Framework_TestCase { public function testWriteTime() { $logger = $this->getMockBuilder( LoggerInterface::class )->getMock(); // 1 per query, 1 per trx, and one "sub-optimal trx" entry - $logger->expects( $this->exactly( 4 ) )->method( 'info' ); + $logger->expects( $this->exactly( 4 ) )->method( 'warning' ); $tp = new TransactionProfiler(); $tp->setLogger( $logger ); @@ -50,7 +56,7 @@ class TransactionProfilerTest extends PHPUnit_Framework_TestCase { public function testAffectedTrx() { $logger = $this->getMockBuilder( LoggerInterface::class )->getMock(); - $logger->expects( $this->exactly( 1 ) )->method( 'info' ); + $logger->expects( $this->exactly( 1 ) )->method( 'warning' ); $tp = new TransactionProfiler(); $tp->setLogger( $logger ); @@ -63,7 +69,7 @@ class TransactionProfilerTest extends PHPUnit_Framework_TestCase { public function testWriteTimeTrx() { $logger = $this->getMockBuilder( LoggerInterface::class )->getMock(); // 1 per trx, and one "sub-optimal trx" entry - $logger->expects( $this->exactly( 2 ) )->method( 'info' ); + $logger->expects( $this->exactly( 2 ) )->method( 'warning' ); $tp = new TransactionProfiler(); $tp->setLogger( $logger ); @@ -75,7 +81,7 @@ class TransactionProfilerTest extends PHPUnit_Framework_TestCase { public function testConns() { $logger = $this->getMockBuilder( LoggerInterface::class )->getMock(); - $logger->expects( $this->exactly( 2 ) )->method( 'info' ); + $logger->expects( $this->exactly( 2 ) )->method( 'warning' ); $tp = new TransactionProfiler(); $tp->setLogger( $logger ); @@ -89,7 +95,7 @@ class TransactionProfilerTest extends PHPUnit_Framework_TestCase { public function testMasterConns() { $logger = $this->getMockBuilder( LoggerInterface::class )->getMock(); - $logger->expects( $this->exactly( 2 ) )->method( 'info' ); + $logger->expects( $this->exactly( 2 ) )->method( 'warning' ); $tp = new TransactionProfiler(); $tp->setLogger( $logger ); @@ -106,7 +112,7 @@ class TransactionProfilerTest extends PHPUnit_Framework_TestCase { public function testReadQueryCount() { $logger = $this->getMockBuilder( LoggerInterface::class )->getMock(); - $logger->expects( $this->exactly( 2 ) )->method( 'info' ); + $logger->expects( $this->exactly( 2 ) )->method( 'warning' ); $tp = new TransactionProfiler(); $tp->setLogger( $logger ); @@ -120,7 +126,7 @@ class TransactionProfilerTest extends PHPUnit_Framework_TestCase { public function testWriteQueryCount() { $logger = $this->getMockBuilder( LoggerInterface::class )->getMock(); - $logger->expects( $this->exactly( 2 ) )->method( 'info' ); + $logger->expects( $this->exactly( 2 ) )->method( 'warning' ); $tp = new TransactionProfiler(); $tp->setLogger( $logger ); diff --git a/www/wiki/tests/phpunit/includes/libs/rdbms/connectionmanager/ConnectionManagerTest.php b/www/wiki/tests/phpunit/includes/libs/rdbms/connectionmanager/ConnectionManagerTest.php index a3f39810..dd86a73e 100644 --- a/www/wiki/tests/phpunit/includes/libs/rdbms/connectionmanager/ConnectionManagerTest.php +++ b/www/wiki/tests/phpunit/includes/libs/rdbms/connectionmanager/ConnectionManagerTest.php @@ -2,7 +2,7 @@ namespace Wikimedia\Tests\Rdbms; -use IDatabase; +use Wikimedia\Rdbms\IDatabase; use Wikimedia\Rdbms\LoadBalancer; use PHPUnit_Framework_MockObject_MockObject; use Wikimedia\Rdbms\ConnectionManager; @@ -10,10 +10,9 @@ use Wikimedia\Rdbms\ConnectionManager; /** * @covers Wikimedia\Rdbms\ConnectionManager * - * @license GPL-2.0+ * @author Daniel Kinzler */ -class ConnectionManagerTest extends \PHPUnit_Framework_TestCase { +class ConnectionManagerTest extends \PHPUnit\Framework\TestCase { /** * @return IDatabase|PHPUnit_Framework_MockObject_MockObject diff --git a/www/wiki/tests/phpunit/includes/libs/rdbms/connectionmanager/SessionConsistentConnectionManagerTest.php b/www/wiki/tests/phpunit/includes/libs/rdbms/connectionmanager/SessionConsistentConnectionManagerTest.php index 4e76f2a8..8d7d104c 100644 --- a/www/wiki/tests/phpunit/includes/libs/rdbms/connectionmanager/SessionConsistentConnectionManagerTest.php +++ b/www/wiki/tests/phpunit/includes/libs/rdbms/connectionmanager/SessionConsistentConnectionManagerTest.php @@ -2,7 +2,7 @@ namespace Wikimedia\Tests\Rdbms; -use IDatabase; +use Wikimedia\Rdbms\IDatabase; use Wikimedia\Rdbms\LoadBalancer; use PHPUnit_Framework_MockObject_MockObject; use Wikimedia\Rdbms\SessionConsistentConnectionManager; @@ -10,10 +10,9 @@ use Wikimedia\Rdbms\SessionConsistentConnectionManager; /** * @covers Wikimedia\Rdbms\SessionConsistentConnectionManager * - * @license GPL-2.0+ * @author Daniel Kinzler */ -class SessionConsistentConnectionManagerTest extends \PHPUnit_Framework_TestCase { +class SessionConsistentConnectionManagerTest extends \PHPUnit\Framework\TestCase { /** * @return IDatabase|PHPUnit_Framework_MockObject_MockObject diff --git a/www/wiki/tests/phpunit/includes/libs/rdbms/database/DBConnRefTest.php b/www/wiki/tests/phpunit/includes/libs/rdbms/database/DBConnRefTest.php new file mode 100644 index 00000000..c3cddc61 --- /dev/null +++ b/www/wiki/tests/phpunit/includes/libs/rdbms/database/DBConnRefTest.php @@ -0,0 +1,148 @@ +<?php + +use Wikimedia\Rdbms\Database; +use Wikimedia\Rdbms\DBConnRef; +use Wikimedia\Rdbms\FakeResultWrapper; +use Wikimedia\Rdbms\IDatabase; +use Wikimedia\Rdbms\ILoadBalancer; +use Wikimedia\Rdbms\ResultWrapper; + +/** + * @covers Wikimedia\Rdbms\DBConnRef + */ +class DBConnRefTest extends PHPUnit\Framework\TestCase { + + use PHPUnit4And6Compat; + + /** + * @return ILoadBalancer + */ + private function getLoadBalancerMock() { + $lb = $this->getMock( ILoadBalancer::class ); + + $lb->method( 'getConnection' )->willReturnCallback( + function () { + return $this->getDatabaseMock(); + } + ); + + $lb->method( 'getConnectionRef' )->willReturnCallback( + function () use ( $lb ) { + return $this->getDBConnRef( $lb ); + } + ); + + return $lb; + } + + /** + * @return IDatabase + */ + private function getDatabaseMock() { + $db = $this->getMockBuilder( Database::class ) + ->disableOriginalConstructor() + ->getMock(); + + $db->method( 'select' )->willReturn( new FakeResultWrapper( [] ) ); + $db->method( '__toString' )->willReturn( 'MOCK_DB' ); + + return $db; + } + + /** + * @return IDatabase + */ + private function getDBConnRef( ILoadBalancer $lb = null ) { + $lb = $lb ?: $this->getLoadBalancerMock(); + return new DBConnRef( $lb, $this->getDatabaseMock() ); + } + + public function testConstruct() { + $lb = $this->getLoadBalancerMock(); + $ref = new DBConnRef( $lb, $this->getDatabaseMock() ); + + $this->assertInstanceOf( ResultWrapper::class, $ref->select( 'whatever', '*' ) ); + } + + public function testConstruct_params() { + $lb = $this->getMock( ILoadBalancer::class ); + + $lb->expects( $this->once() ) + ->method( 'getConnection' ) + ->with( DB_MASTER, [ 'test' ], 'dummy', ILoadBalancer::CONN_TRX_AUTOCOMMIT ) + ->willReturnCallback( + function () { + return $this->getDatabaseMock(); + } + ); + + $ref = new DBConnRef( + $lb, + [ DB_MASTER, [ 'test' ], 'dummy', ILoadBalancer::CONN_TRX_AUTOCOMMIT ] + ); + + $this->assertInstanceOf( ResultWrapper::class, $ref->select( 'whatever', '*' ) ); + } + + public function testDestruct() { + $lb = $this->getLoadBalancerMock(); + + $lb->expects( $this->once() ) + ->method( 'reuseConnection' ); + + $this->innerMethodForTestDestruct( $lb ); + } + + private function innerMethodForTestDestruct( ILoadBalancer $lb ) { + $ref = $lb->getConnectionRef( DB_REPLICA ); + + $this->assertInstanceOf( ResultWrapper::class, $ref->select( 'whatever', '*' ) ); + } + + public function testConstruct_failure() { + $this->setExpectedException( InvalidArgumentException::class, '' ); + + $lb = $this->getLoadBalancerMock(); + new DBConnRef( $lb, 17 ); // bad constructor argument + } + + public function testGetWikiID() { + $lb = $this->getMock( ILoadBalancer::class ); + + // getWikiID is optimized to not create a connection + $lb->expects( $this->never() ) + ->method( 'getConnection' ); + + $ref = new DBConnRef( $lb, [ DB_REPLICA, [], 'dummy', 0 ] ); + + $this->assertSame( 'dummy', $ref->getWikiID() ); + } + + public function testGetDomainID() { + $lb = $this->getMock( ILoadBalancer::class ); + + // getDomainID is optimized to not create a connection + $lb->expects( $this->never() ) + ->method( 'getConnection' ); + + $ref = new DBConnRef( $lb, [ DB_REPLICA, [], 'dummy', 0 ] ); + + $this->assertSame( 'dummy', $ref->getDomainID() ); + } + + public function testSelect() { + // select should get passed through normally + $ref = $this->getDBConnRef(); + $this->assertInstanceOf( ResultWrapper::class, $ref->select( 'whatever', '*' ) ); + } + + public function testToString() { + $ref = $this->getDBConnRef(); + $this->assertInternalType( 'string', $ref->__toString() ); + + $lb = $this->getLoadBalancerMock(); + $ref = new DBConnRef( $lb, [ DB_MASTER, [], 'test', 0 ] ); + $this->assertInternalType( 'string', $ref->__toString() ); + } + +} diff --git a/www/wiki/tests/phpunit/includes/libs/rdbms/database/DatabaseDomainTest.php b/www/wiki/tests/phpunit/includes/libs/rdbms/database/DatabaseDomainTest.php index a8dbdd39..b2e71554 100644 --- a/www/wiki/tests/phpunit/includes/libs/rdbms/database/DatabaseDomainTest.php +++ b/www/wiki/tests/phpunit/includes/libs/rdbms/database/DatabaseDomainTest.php @@ -5,7 +5,11 @@ use Wikimedia\Rdbms\DatabaseDomain; /** * @covers Wikimedia\Rdbms\DatabaseDomain */ -class DatabaseDomainTest extends PHPUnit_Framework_TestCase { +class DatabaseDomainTest extends PHPUnit\Framework\TestCase { + + use MediaWikiCoversValidator; + use PHPUnit4And6Compat; + public static function provideConstruct() { return [ 'All strings' => diff --git a/www/wiki/tests/phpunit/includes/libs/rdbms/database/DatabaseMssqlTest.php b/www/wiki/tests/phpunit/includes/libs/rdbms/database/DatabaseMssqlTest.php new file mode 100644 index 00000000..b28a5b9e --- /dev/null +++ b/www/wiki/tests/phpunit/includes/libs/rdbms/database/DatabaseMssqlTest.php @@ -0,0 +1,55 @@ +<?php + +use Wikimedia\Rdbms\DatabaseMssql; + +class DatabaseMssqlTest extends PHPUnit\Framework\TestCase { + + use MediaWikiCoversValidator; + use PHPUnit4And6Compat; + + /** + * @return PHPUnit_Framework_MockObject_MockObject|DatabaseMssql + */ + private function getMockDb() { + return $this->getMockBuilder( DatabaseMssql::class ) + ->disableOriginalConstructor() + ->setMethods( null ) + ->getMock(); + } + + public function provideBuildSubstring() { + yield [ 'someField', 1, 2, 'SUBSTRING(someField,1,2)' ]; + yield [ 'someField', 1, null, 'SUBSTRING(someField,1,2147483647)' ]; + yield [ 'someField', 1, 3333333333, 'SUBSTRING(someField,1,3333333333)' ]; + } + + /** + * @covers Wikimedia\Rdbms\DatabaseMssql::buildSubstring + * @dataProvider provideBuildSubstring + */ + public function testBuildSubstring( $input, $start, $length, $expected ) { + $mockDb = $this->getMockDb(); + $output = $mockDb->buildSubstring( $input, $start, $length ); + $this->assertSame( $expected, $output ); + } + + public function provideBuildSubstring_invalidParams() { + yield [ -1, 1 ]; + yield [ 1, -1 ]; + yield [ 1, 'foo' ]; + yield [ 'foo', 1 ]; + yield [ null, 1 ]; + yield [ 0, 1 ]; + } + + /** + * @covers Wikimedia\Rdbms\DatabaseMssql::buildSubstring + * @dataProvider provideBuildSubstring_invalidParams + */ + public function testBuildSubstring_invalidParams( $start, $length ) { + $mockDb = $this->getMockDb(); + $this->setExpectedException( InvalidArgumentException::class ); + $mockDb->buildSubstring( 'foo', $start, $length ); + } + +} diff --git a/www/wiki/tests/phpunit/includes/libs/rdbms/database/DatabaseMysqlBaseTest.php b/www/wiki/tests/phpunit/includes/libs/rdbms/database/DatabaseMysqlBaseTest.php index 456447f0..93192d01 100644 --- a/www/wiki/tests/phpunit/includes/libs/rdbms/database/DatabaseMysqlBaseTest.php +++ b/www/wiki/tests/phpunit/includes/libs/rdbms/database/DatabaseMysqlBaseTest.php @@ -23,94 +23,24 @@ * @copyright © 2013 Wikimedia Foundation and contributors */ -use Wikimedia\Rdbms\TransactionProfiler; -use Wikimedia\Rdbms\DatabaseDomain; use Wikimedia\Rdbms\MySQLMasterPos; -use Wikimedia\Rdbms\DatabaseMysqlBase; +use Wikimedia\TestingAccessWrapper; -/** - * Fake class around abstract class so we can call concrete methods. - */ -class FakeDatabaseMysqlBase extends DatabaseMysqlBase { - // From Database - function __construct() { - $this->profiler = new ProfilerStub( [] ); - $this->trxProfiler = new TransactionProfiler(); - $this->cliMode = true; - $this->connLogger = new \Psr\Log\NullLogger(); - $this->queryLogger = new \Psr\Log\NullLogger(); - $this->errorLogger = function ( Exception $e ) { - wfWarn( get_class( $e ) . ": {$e->getMessage()}" ); - }; - $this->currentDomain = DatabaseDomain::newUnspecified(); - } - - protected function closeConnection() { - } - - protected function doQuery( $sql ) { - } - - // From DatabaseMysql - protected function mysqlConnect( $realServer ) { - } - - protected function mysqlSetCharset( $charset ) { - } - - protected function mysqlFreeResult( $res ) { - } - - protected function mysqlFetchObject( $res ) { - } - - protected function mysqlFetchArray( $res ) { - } - - protected function mysqlNumRows( $res ) { - } - - protected function mysqlNumFields( $res ) { - } +class DatabaseMysqlBaseTest extends PHPUnit\Framework\TestCase { - protected function mysqlFieldName( $res, $n ) { - } - - protected function mysqlFieldType( $res, $n ) { - } + use MediaWikiCoversValidator; + use PHPUnit4And6Compat; - protected function mysqlDataSeek( $res, $row ) { - } - - protected function mysqlError( $conn = null ) { - } - - protected function mysqlFetchField( $res, $n ) { - } - - protected function mysqlRealEscapeString( $s ) { - } - - function insertId() { - } - - function lastErrno() { - } - - function affectedRows() { - } - - function getServerVersion() { - } -} - -class DatabaseMysqlBaseTest extends PHPUnit_Framework_TestCase { /** * @dataProvider provideDiapers * @covers Wikimedia\Rdbms\DatabaseMysqlBase::addIdentifierQuotes */ public function testAddIdentifierQuotes( $expected, $in ) { - $db = new FakeDatabaseMysqlBase(); + $db = $this->getMockBuilder( DatabaseMysqli::class ) + ->disableOriginalConstructor() + ->setMethods( null ) + ->getMock(); + $quoted = $db->addIdentifierQuotes( $in ); $this->assertEquals( $expected, $quoted ); } @@ -172,7 +102,7 @@ class DatabaseMysqlBaseTest extends PHPUnit_Framework_TestCase { } private function getMockForViews() { - $db = $this->getMockBuilder( 'DatabaseMysqli' ) + $db = $this->getMockBuilder( DatabaseMysqli::class ) ->disableOriginalConstructor() ->setMethods( [ 'fetchRow', 'query' ] ) ->getMock(); @@ -209,16 +139,34 @@ class DatabaseMysqlBaseTest extends PHPUnit_Framework_TestCase { } /** + * @covers Wikimedia\Rdbms\MySQLMasterPos + */ + public function testBinLogName() { + $pos = new MySQLMasterPos( "db1052.2424/4643", 1 ); + + $this->assertEquals( "db1052", $pos->getLogName() ); + $this->assertEquals( "db1052.2424", $pos->getLogFile() ); + $this->assertEquals( [ 2424, 4643 ], $pos->getLogPosition() ); + } + + /** * @dataProvider provideComparePositions * @covers Wikimedia\Rdbms\MySQLMasterPos */ - public function testHasReached( MySQLMasterPos $lowerPos, MySQLMasterPos $higherPos, $match ) { + public function testHasReached( + MySQLMasterPos $lowerPos, MySQLMasterPos $higherPos, $match, $hetero + ) { if ( $match ) { $this->assertTrue( $lowerPos->channelsMatch( $higherPos ) ); - $this->assertTrue( $higherPos->hasReached( $lowerPos ) ); - $this->assertTrue( $higherPos->hasReached( $higherPos ) ); + if ( $hetero ) { + // Each position is has one channel higher than the other + $this->assertFalse( $higherPos->hasReached( $lowerPos ) ); + } else { + $this->assertTrue( $higherPos->hasReached( $lowerPos ) ); + } $this->assertTrue( $lowerPos->hasReached( $lowerPos ) ); + $this->assertTrue( $higherPos->hasReached( $higherPos ) ); $this->assertFalse( $lowerPos->hasReached( $higherPos ) ); } else { // channels don't match $this->assertFalse( $lowerPos->channelsMatch( $higherPos ) ); @@ -229,53 +177,100 @@ class DatabaseMysqlBaseTest extends PHPUnit_Framework_TestCase { } public static function provideComparePositions() { + $now = microtime( true ); + return [ // Binlog style [ - new MySQLMasterPos( 'db1034-bin.000976', '843431247' ), - new MySQLMasterPos( 'db1034-bin.000976', '843431248' ), - true + new MySQLMasterPos( 'db1034-bin.000976/843431247', $now ), + new MySQLMasterPos( 'db1034-bin.000976/843431248', $now ), + true, + false ], [ - new MySQLMasterPos( 'db1034-bin.000976', '999' ), - new MySQLMasterPos( 'db1034-bin.000976', '1000' ), - true + new MySQLMasterPos( 'db1034-bin.000976/999', $now ), + new MySQLMasterPos( 'db1034-bin.000976/1000', $now ), + true, + false ], [ - new MySQLMasterPos( 'db1034-bin.000976', '999' ), - new MySQLMasterPos( 'db1035-bin.000976', '1000' ), + new MySQLMasterPos( 'db1034-bin.000976/999', $now ), + new MySQLMasterPos( 'db1035-bin.000976/1000', $now ), + false, false ], // MySQL GTID style [ - new MySQLMasterPos( 'db1-bin.2', '1', '3E11FA47-71CA-11E1-9E33-C80AA9429562:23' ), - new MySQLMasterPos( 'db1-bin.2', '2', '3E11FA47-71CA-11E1-9E33-C80AA9429562:24' ), - true + new MySQLMasterPos( '3E11FA47-71CA-11E1-9E33-C80AA9429562:1-23', $now ), + new MySQLMasterPos( '3E11FA47-71CA-11E1-9E33-C80AA9429562:5-24', $now ), + true, + false ], [ - new MySQLMasterPos( 'db1-bin.2', '1', '3E11FA47-71CA-11E1-9E33-C80AA9429562:99' ), - new MySQLMasterPos( 'db1-bin.2', '2', '3E11FA47-71CA-11E1-9E33-C80AA9429562:100' ), - true + new MySQLMasterPos( '3E11FA47-71CA-11E1-9E33-C80AA9429562:5-99', $now ), + new MySQLMasterPos( '3E11FA47-71CA-11E1-9E33-C80AA9429562:1-100', $now ), + true, + false ], [ - new MySQLMasterPos( 'db1-bin.2', '1', '3E11FA47-71CA-11E1-9E33-C80AA9429562:99' ), - new MySQLMasterPos( 'db1-bin.2', '2', '1E11FA47-71CA-11E1-9E33-C80AA9429562:100' ), + new MySQLMasterPos( '3E11FA47-71CA-11E1-9E33-C80AA9429562:1-99', $now ), + new MySQLMasterPos( '1E11FA47-71CA-11E1-9E33-C80AA9429562:1-100', $now ), + false, false ], // MariaDB GTID style [ - new MySQLMasterPos( 'db1-bin.2', '1', '255-11-23' ), - new MySQLMasterPos( 'db1-bin.2', '2', '255-11-24' ), - true + new MySQLMasterPos( '255-11-23', $now ), + new MySQLMasterPos( '255-11-24', $now ), + true, + false + ], + [ + new MySQLMasterPos( '255-11-99', $now ), + new MySQLMasterPos( '255-11-100', $now ), + true, + false + ], + [ + new MySQLMasterPos( '255-11-999', $now ), + new MySQLMasterPos( '254-11-1000', $now ), + false, + false + ], + [ + new MySQLMasterPos( '255-11-23,256-12-50', $now ), + new MySQLMasterPos( '255-11-24', $now ), + true, + false + ], + [ + new MySQLMasterPos( '255-11-99,256-12-50,257-12-50', $now ), + new MySQLMasterPos( '255-11-1000', $now ), + true, + false + ], + [ + new MySQLMasterPos( '255-11-23,256-12-50', $now ), + new MySQLMasterPos( '255-11-24,155-52-63', $now ), + true, + false + ], + [ + new MySQLMasterPos( '255-11-99,256-12-50,257-12-50', $now ), + new MySQLMasterPos( '255-11-1000,256-12-51', $now ), + true, + false ], [ - new MySQLMasterPos( 'db1-bin.2', '1', '255-11-99' ), - new MySQLMasterPos( 'db1-bin.2', '2', '255-11-100' ), + new MySQLMasterPos( '255-11-99,256-12-50', $now ), + new MySQLMasterPos( '255-13-1000,256-14-49', $now ), + true, true ], [ - new MySQLMasterPos( 'db1-bin.2', '1', '255-11-999' ), - new MySQLMasterPos( 'db1-bin.2', '2', '254-11-1000' ), + new MySQLMasterPos( '253-11-999,255-11-999', $now ), + new MySQLMasterPos( '254-11-1000', $now ), + false, false ], ]; @@ -288,40 +283,77 @@ class DatabaseMysqlBaseTest extends PHPUnit_Framework_TestCase { public function testChannelsMatch( MySQLMasterPos $pos1, MySQLMasterPos $pos2, $matches ) { $this->assertEquals( $matches, $pos1->channelsMatch( $pos2 ) ); $this->assertEquals( $matches, $pos2->channelsMatch( $pos1 ) ); + + $roundtripPos = new MySQLMasterPos( (string)$pos1, 1 ); + $this->assertEquals( (string)$pos1, (string)$roundtripPos ); } public static function provideChannelPositions() { + $now = microtime( true ); + return [ [ - new MySQLMasterPos( 'db1034-bin.000876', '44' ), - new MySQLMasterPos( 'db1034-bin.000976', '74' ), + new MySQLMasterPos( 'db1034-bin.000876/44', $now ), + new MySQLMasterPos( 'db1034-bin.000976/74', $now ), true ], [ - new MySQLMasterPos( 'db1052-bin.000976', '999' ), - new MySQLMasterPos( 'db1052-bin.000976', '1000' ), + new MySQLMasterPos( 'db1052-bin.000976/999', $now ), + new MySQLMasterPos( 'db1052-bin.000976/1000', $now ), true ], [ - new MySQLMasterPos( 'db1066-bin.000976', '9999' ), - new MySQLMasterPos( 'db1035-bin.000976', '10000' ), + new MySQLMasterPos( 'db1066-bin.000976/9999', $now ), + new MySQLMasterPos( 'db1035-bin.000976/10000', $now ), false ], [ - new MySQLMasterPos( 'db1066-bin.000976', '9999' ), - new MySQLMasterPos( 'trump2016.000976', '10000' ), + new MySQLMasterPos( 'db1066-bin.000976/9999', $now ), + new MySQLMasterPos( 'trump2016.000976/10000', $now ), false ], ]; } /** + * @dataProvider provideCommonDomainGTIDs + * @covers Wikimedia\Rdbms\MySQLMasterPos + */ + public function testCommonGtidDomains( MySQLMasterPos $pos, MySQLMasterPos $ref, $gtids ) { + $this->assertEquals( $gtids, MySQLMasterPos::getCommonDomainGTIDs( $pos, $ref ) ); + } + + public static function provideCommonDomainGTIDs() { + return [ + [ + new MySQLMasterPos( '255-13-99,256-12-50,257-14-50', 1 ), + new MySQLMasterPos( '255-11-1000', 1 ), + [ '255-13-99' ] + ], + [ + new MySQLMasterPos( + '2E11FA47-71CA-11E1-9E33-C80AA9429562:1-5,' . + '3E11FA47-71CA-11E1-9E33-C80AA9429562:20-99,' . + '7E11FA47-71CA-11E1-9E33-C80AA9429562:1-30', + 1 + ), + new MySQLMasterPos( + '1E11FA47-71CA-11E1-9E33-C80AA9429562:30-100,' . + '3E11FA47-71CA-11E1-9E33-C80AA9429562:30-66', + 1 + ), + [ '3E11FA47-71CA-11E1-9E33-C80AA9429562:20-99' ] + ] + ]; + } + + /** * @dataProvider provideLagAmounts * @covers Wikimedia\Rdbms\DatabaseMysqlBase::getLag * @covers Wikimedia\Rdbms\DatabaseMysqlBase::getLagFromPtHeartbeat */ public function testPtHeartbeat( $lag ) { - $db = $this->getMockBuilder( 'DatabaseMysqli' ) + $db = $this->getMockBuilder( DatabaseMysqli::class ) ->disableOriginalConstructor() ->setMethods( [ 'getLagDetectionMethod', 'getHeartbeatData', 'getMasterServerInfo' ] ) @@ -368,4 +400,344 @@ class DatabaseMysqlBaseTest extends PHPUnit_Framework_TestCase { [ 1000.77 ], ]; } + + /** + * @dataProvider provideGtidData + * @covers Wikimedia\Rdbms\MySQLMasterPos + * @covers Wikimedia\Rdbms\DatabaseMysqlBase::getReplicaPos + * @covers Wikimedia\Rdbms\DatabaseMysqlBase::getMasterPos + */ + public function testServerGtidTable( $gtable, $rBLtable, $mBLtable, $rGTIDs, $mGTIDs ) { + $db = $this->getMockBuilder( DatabaseMysqli::class ) + ->disableOriginalConstructor() + ->setMethods( [ + 'useGTIDs', + 'getServerGTIDs', + 'getServerRoleStatus', + 'getServerId', + 'getServerUUID' + ] ) + ->getMock(); + + $db->method( 'useGTIDs' )->willReturn( true ); + $db->method( 'getServerGTIDs' )->willReturn( $gtable ); + $db->method( 'getServerRoleStatus' )->willReturnCallback( + function ( $role ) use ( $rBLtable, $mBLtable ) { + if ( $role === 'SLAVE' ) { + return $rBLtable; + } elseif ( $role === 'MASTER' ) { + return $mBLtable; + } + + return null; + } + ); + $db->method( 'getServerId' )->willReturn( 1 ); + $db->method( 'getServerUUID' )->willReturn( '2E11FA47-71CA-11E1-9E33-C80AA9429562' ); + + if ( is_array( $rGTIDs ) ) { + $this->assertEquals( $rGTIDs, $db->getReplicaPos()->getGTIDs() ); + } else { + $this->assertEquals( false, $db->getReplicaPos() ); + } + if ( is_array( $mGTIDs ) ) { + $this->assertEquals( $mGTIDs, $db->getMasterPos()->getGTIDs() ); + } else { + $this->assertEquals( false, $db->getMasterPos() ); + } + } + + public static function provideGtidData() { + return [ + // MariaDB + [ + [ + 'gtid_domain_id' => 100, + 'gtid_current_pos' => '100-13-77', + 'gtid_binlog_pos' => '100-13-77', + 'gtid_slave_pos' => null // master + ], + [ + 'Relay_Master_Log_File' => 'host.1600', + 'Exec_Master_Log_Pos' => '77' + ], + [ + 'File' => 'host.1600', + 'Position' => '77' + ], + [], + [ '100' => '100-13-77' ] + ], + [ + [ + 'gtid_domain_id' => 100, + 'gtid_current_pos' => '100-13-77', + 'gtid_binlog_pos' => '100-13-77', + 'gtid_slave_pos' => '100-13-77' // replica + ], + [ + 'Relay_Master_Log_File' => 'host.1600', + 'Exec_Master_Log_Pos' => '77' + ], + [], + [ '100' => '100-13-77' ], + [ '100' => '100-13-77' ] + ], + [ + [ + 'gtid_current_pos' => '100-13-77', + 'gtid_binlog_pos' => '100-13-77', + 'gtid_slave_pos' => '100-13-77' // replica + ], + [ + 'Relay_Master_Log_File' => 'host.1600', + 'Exec_Master_Log_Pos' => '77' + ], + [], + [ '100' => '100-13-77' ], + [ '100' => '100-13-77' ] + ], + // MySQL + [ + [ + 'gtid_executed' => '2E11FA47-71CA-11E1-9E33-C80AA9429562:1-77' + ], + [ + 'Relay_Master_Log_File' => 'host.1600', + 'Exec_Master_Log_Pos' => '77' + ], + [], // only a replica + [ '2E11FA47-71CA-11E1-9E33-C80AA9429562' + => '2E11FA47-71CA-11E1-9E33-C80AA9429562:1-77' ], + // replica/master use same var + [ '2E11FA47-71CA-11E1-9E33-C80AA9429562' + => '2E11FA47-71CA-11E1-9E33-C80AA9429562:1-77' ], + ], + [ + [ + 'gtid_executed' => '2E11FA47-71CA-11E1-9E33-C80AA9429562:1-49,' . + '2E11FA47-71CA-11E1-9E33-C80AA9429562:51-77' + ], + [ + 'Relay_Master_Log_File' => 'host.1600', + 'Exec_Master_Log_Pos' => '77' + ], + [], // only a replica + [ '2E11FA47-71CA-11E1-9E33-C80AA9429562' + => '2E11FA47-71CA-11E1-9E33-C80AA9429562:51-77' ], + // replica/master use same var + [ '2E11FA47-71CA-11E1-9E33-C80AA9429562' + => '2E11FA47-71CA-11E1-9E33-C80AA9429562:51-77' ], + ], + [ + [ + 'gtid_executed' => null, // not enabled? + 'gtid_binlog_pos' => null + ], + [ + 'Relay_Master_Log_File' => 'host.1600', + 'Exec_Master_Log_Pos' => '77' + ], + [], // only a replica + [], // binlog fallback + false + ], + [ + [ + 'gtid_executed' => null, // not enabled? + 'gtid_binlog_pos' => null + ], + [], // no replication + [], // no replication + false, + false + ] + ]; + } + + /** + * @covers Wikimedia\Rdbms\MySQLMasterPos + */ + public function testSerialize() { + $pos = new MySQLMasterPos( '3E11FA47-71CA-11E1-9E33-C80AA9429562:99', 53636363 ); + $roundtripPos = unserialize( serialize( $pos ) ); + + $this->assertEquals( $pos, $roundtripPos ); + + $pos = new MySQLMasterPos( '255-11-23', 53636363 ); + $roundtripPos = unserialize( serialize( $pos ) ); + + $this->assertEquals( $pos, $roundtripPos ); + } + + /** + * @covers Wikimedia\Rdbms\DatabaseMysqlBase::isInsertSelectSafe + * @dataProvider provideInsertSelectCases + */ + public function testInsertSelectIsSafe( $insertOpts, $selectOpts, $row, $safe ) { + $db = $this->getMockBuilder( DatabaseMysqli::class ) + ->disableOriginalConstructor() + ->setMethods( [ 'getReplicationSafetyInfo' ] ) + ->getMock(); + $db->method( 'getReplicationSafetyInfo' )->willReturn( (object)$row ); + $dbw = TestingAccessWrapper::newFromObject( $db ); + + $this->assertEquals( $safe, $dbw->isInsertSelectSafe( $insertOpts, $selectOpts ) ); + } + + public function provideInsertSelectCases() { + return [ + [ + [], + [], + [ + 'innodb_autoinc_lock_mode' => '2', + 'binlog_format' => 'ROW', + ], + true + ], + [ + [], + [ 'LIMIT' => 100 ], + [ + 'innodb_autoinc_lock_mode' => '2', + 'binlog_format' => 'ROW', + ], + true + ], + [ + [], + [ 'LIMIT' => 100 ], + [ + 'innodb_autoinc_lock_mode' => '0', + 'binlog_format' => 'STATEMENT', + ], + false + ], + [ + [], + [], + [ + 'innodb_autoinc_lock_mode' => '2', + 'binlog_format' => 'STATEMENT', + ], + false + ], + [ + [ 'NO_AUTO_COLUMNS' ], + [ 'LIMIT' => 100 ], + [ + 'innodb_autoinc_lock_mode' => '0', + 'binlog_format' => 'STATEMENT', + ], + false + ], + [ + [], + [], + [ + 'innodb_autoinc_lock_mode' => 0, + 'binlog_format' => 'STATEMENT', + ], + true + ], + [ + [ 'NO_AUTO_COLUMNS' ], + [], + [ + 'innodb_autoinc_lock_mode' => 2, + 'binlog_format' => 'STATEMENT', + ], + true + ], + [ + [ 'NO_AUTO_COLUMNS' ], + [], + [ + 'innodb_autoinc_lock_mode' => 0, + 'binlog_format' => 'STATEMENT', + ], + true + ], + + ]; + } + + /** + * @covers \Wikimedia\Rdbms\DatabaseMysqlBase::buildIntegerCast + */ + public function testBuildIntegerCast() { + $db = $this->getMockBuilder( DatabaseMysqli::class ) + ->disableOriginalConstructor() + ->setMethods( null ) + ->getMock(); + $output = $db->buildIntegerCast( 'fieldName' ); + $this->assertSame( 'CAST( fieldName AS SIGNED )', $output ); + } + + /* + * @covers Wikimedia\Rdbms\Database::setIndexAliases + */ + public function testIndexAliases() { + $db = $this->getMockBuilder( DatabaseMysqli::class ) + ->disableOriginalConstructor() + ->setMethods( [ 'mysqlRealEscapeString' ] ) + ->getMock(); + $db->method( 'mysqlRealEscapeString' )->willReturnCallback( + function ( $s ) { + return str_replace( "'", "\\'", $s ); + } + ); + + $db->setIndexAliases( [ 'a_b_idx' => 'a_c_idx' ] ); + $sql = $db->selectSQLText( + 'zend', 'field', [ 'a' => 'x' ], __METHOD__, [ 'USE INDEX' => 'a_b_idx' ] ); + + $this->assertEquals( + "SELECT field FROM `zend` FORCE INDEX (a_c_idx) WHERE a = 'x' ", + $sql + ); + + $db->setIndexAliases( [] ); + $sql = $db->selectSQLText( + 'zend', 'field', [ 'a' => 'x' ], __METHOD__, [ 'USE INDEX' => 'a_b_idx' ] ); + + $this->assertEquals( + "SELECT field FROM `zend` FORCE INDEX (a_b_idx) WHERE a = 'x' ", + $sql + ); + } + + /** + * @covers Wikimedia\Rdbms\Database::setTableAliases + */ + public function testTableAliases() { + $db = $this->getMockBuilder( DatabaseMysqli::class ) + ->disableOriginalConstructor() + ->setMethods( [ 'mysqlRealEscapeString' ] ) + ->getMock(); + $db->method( 'mysqlRealEscapeString' )->willReturnCallback( + function ( $s ) { + return str_replace( "'", "\\'", $s ); + } + ); + + $db->setTableAliases( [ + 'meow' => [ 'dbname' => 'feline', 'schema' => null, 'prefix' => 'cat_' ] + ] ); + $sql = $db->selectSQLText( 'meow', 'field', [ 'a' => 'x' ], __METHOD__ ); + + $this->assertEquals( + "SELECT field FROM `feline`.`cat_meow` WHERE a = 'x' ", + $sql + ); + + $db->setTableAliases( [] ); + $sql = $db->selectSQLText( 'meow', 'field', [ 'a' => 'x' ], __METHOD__ ); + + $this->assertEquals( + "SELECT field FROM `meow` WHERE a = 'x' ", + $sql + ); + } } diff --git a/www/wiki/tests/phpunit/includes/libs/rdbms/database/DatabaseSQLTest.php b/www/wiki/tests/phpunit/includes/libs/rdbms/database/DatabaseSQLTest.php index 7b841172..ab2f11b5 100644 --- a/www/wiki/tests/phpunit/includes/libs/rdbms/database/DatabaseSQLTest.php +++ b/www/wiki/tests/phpunit/includes/libs/rdbms/database/DatabaseSQLTest.php @@ -1,13 +1,23 @@ <?php +use Wikimedia\Rdbms\IDatabase; use Wikimedia\Rdbms\LikeMatch; +use Wikimedia\Rdbms\Database; +use Wikimedia\TestingAccessWrapper; +use Wikimedia\Rdbms\DBTransactionStateError; +use Wikimedia\Rdbms\DBUnexpectedError; +use Wikimedia\Rdbms\DBTransactionError; /** * Test the parts of the Database abstract class that deal * with creating SQL text. */ -class DatabaseSQLTest extends PHPUnit_Framework_TestCase { - /** @var DatabaseTestHelper */ +class DatabaseSQLTest extends PHPUnit\Framework\TestCase { + + use MediaWikiCoversValidator; + use PHPUnit4And6Compat; + + /** @var DatabaseTestHelper|Database */ private $database; protected function setUp() { @@ -63,6 +73,44 @@ class DatabaseSQLTest extends PHPUnit_Framework_TestCase { ], [ [ + 'tables' => 'table', + 'fields' => [ 'field', 'alias' => 'field2' ], + 'conds' => 'alias = \'text\'', + ], + "SELECT field,field2 AS alias " . + "FROM table " . + "WHERE alias = 'text'" + ], + [ + [ + 'tables' => 'table', + 'fields' => [ 'field', 'alias' => 'field2' ], + 'conds' => [], + ], + "SELECT field,field2 AS alias " . + "FROM table" + ], + [ + [ + 'tables' => 'table', + 'fields' => [ 'field', 'alias' => 'field2' ], + 'conds' => '', + ], + "SELECT field,field2 AS alias " . + "FROM table" + ], + [ + [ + 'tables' => 'table', + 'fields' => [ 'field', 'alias' => 'field2' ], + 'conds' => '0', // T188314 + ], + "SELECT field,field2 AS alias " . + "FROM table " . + "WHERE 0" + ], + [ + [ // 'tables' with space prepended indicates pre-escaped table name 'tables' => ' table LEFT JOIN table2', 'fields' => [ 'field' ], @@ -199,6 +247,101 @@ class DatabaseSQLTest extends PHPUnit_Framework_TestCase { } /** + * @covers Wikimedia\Rdbms\Subquery + * @dataProvider provideSelectRowCount + * @param $sql + * @param $sqlText + */ + public function testSelectRowCount( $sql, $sqlText ) { + $this->database->selectRowCount( + $sql['tables'], + $sql['field'], + isset( $sql['conds'] ) ? $sql['conds'] : [], + __METHOD__, + isset( $sql['options'] ) ? $sql['options'] : [], + isset( $sql['join_conds'] ) ? $sql['join_conds'] : [] + ); + $this->assertLastSql( $sqlText ); + } + + public static function provideSelectRowCount() { + return [ + [ + [ + 'tables' => 'table', + 'field' => [ '*' ], + 'conds' => [ 'field' => 'text' ], + ], + "SELECT COUNT(*) AS rowcount FROM " . + "(SELECT 1 FROM table WHERE field = 'text' ) tmp_count" + ], + [ + [ + 'tables' => 'table', + 'field' => [ 'column' ], + 'conds' => [ 'field' => 'text' ], + ], + "SELECT COUNT(*) AS rowcount FROM " . + "(SELECT 1 FROM table WHERE field = 'text' AND (column IS NOT NULL) ) tmp_count" + ], + [ + [ + 'tables' => 'table', + 'field' => [ 'alias' => 'column' ], + 'conds' => [ 'field' => 'text' ], + ], + "SELECT COUNT(*) AS rowcount FROM " . + "(SELECT 1 FROM table WHERE field = 'text' AND (column IS NOT NULL) ) tmp_count" + ], + [ + [ + 'tables' => 'table', + 'field' => [ 'alias' => 'column' ], + 'conds' => '', + ], + "SELECT COUNT(*) AS rowcount FROM " . + "(SELECT 1 FROM table WHERE (column IS NOT NULL) ) tmp_count" + ], + [ + [ + 'tables' => 'table', + 'field' => [ 'alias' => 'column' ], + 'conds' => false, + ], + "SELECT COUNT(*) AS rowcount FROM " . + "(SELECT 1 FROM table WHERE (column IS NOT NULL) ) tmp_count" + ], + [ + [ + 'tables' => 'table', + 'field' => [ 'alias' => 'column' ], + 'conds' => null, + ], + "SELECT COUNT(*) AS rowcount FROM " . + "(SELECT 1 FROM table WHERE (column IS NOT NULL) ) tmp_count" + ], + [ + [ + 'tables' => 'table', + 'field' => [ 'alias' => 'column' ], + 'conds' => '1', + ], + "SELECT COUNT(*) AS rowcount FROM " . + "(SELECT 1 FROM table WHERE (1) AND (column IS NOT NULL) ) tmp_count" + ], + [ + [ + 'tables' => 'table', + 'field' => [ 'alias' => 'column' ], + 'conds' => '0', + ], + "SELECT COUNT(*) AS rowcount FROM " . + "(SELECT 1 FROM table WHERE (0) AND (column IS NOT NULL) ) tmp_count" + ], + ]; + } + + /** * @dataProvider provideUpdate * @covers Wikimedia\Rdbms\Database::update * @covers Wikimedia\Rdbms\Database::makeUpdateOptions @@ -454,7 +597,7 @@ class DatabaseSQLTest extends PHPUnit_Framework_TestCase { isset( $sql['selectOptions'] ) ? $sql['selectOptions'] : [], isset( $sql['selectJoinConds'] ) ? $sql['selectJoinConds'] : [] ); - $this->assertLastSqlDb( implode( '; ', [ $sqlSelect, $sqlInsert ] ), $dbWeb ); + $this->assertLastSqlDb( implode( '; ', [ $sqlSelect, 'BEGIN', $sqlInsert, 'COMMIT' ] ), $dbWeb ); } public static function provideInsertSelect() { @@ -515,6 +658,7 @@ class DatabaseSQLTest extends PHPUnit_Framework_TestCase { 'srcTable' => [ 'select_table1', 'select_table2' ], 'varMap' => [ 'field_insert' => 'field_select', 'field' => 'field2' ], 'conds' => [ 'field' => 2 ], + 'insertOptions' => [ 'NO_AUTO_COLUMNS' ], 'selectOptions' => [ 'ORDER BY' => 'field', 'FORCE INDEX' => [ 'select_table1' => 'index1' ] ], 'selectJoinConds' => [ 'select_table2' => [ 'LEFT JOIN', [ 'select_table1.foo = select_table2.bar' ] ], @@ -534,6 +678,30 @@ class DatabaseSQLTest extends PHPUnit_Framework_TestCase { ]; } + public function testInsertSelectBatching() { + $dbWeb = new DatabaseTestHelper( __CLASS__, [ 'cliMode' => false ] ); + $rows = []; + for ( $i = 0; $i <= 25000; $i++ ) { + $rows[] = [ 'field' => $i ]; + } + $dbWeb->forceNextResult( $rows ); + $dbWeb->insertSelect( + 'insert_table', + 'select_table', + [ 'field' => 'field2' ], + '*', + __METHOD__ + ); + $this->assertLastSqlDb( implode( '; ', [ + 'SELECT field2 AS field FROM select_table WHERE * FOR UPDATE', + 'BEGIN', + "INSERT INTO insert_table (field) VALUES ('" . implode( "'),('", range( 0, 9999 ) ) . "')", + "INSERT INTO insert_table (field) VALUES ('" . implode( "'),('", range( 10000, 19999 ) ) . "')", + "INSERT INTO insert_table (field) VALUES ('" . implode( "'),('", range( 20000, 25000 ) ) . "')", + 'COMMIT' + ] ), $dbWeb ); + } + /** * @dataProvider provideReplace * @covers Wikimedia\Rdbms\Database::replace @@ -556,11 +724,11 @@ class DatabaseSQLTest extends PHPUnit_Framework_TestCase { 'uniqueIndexes' => [ 'field' ], 'rows' => [ 'field' => 'text', 'field2' => 'text2' ], ], - "DELETE FROM replace_table " . - "WHERE ( field='text' ); " . + "BEGIN; DELETE FROM replace_table " . + "WHERE (field = 'text'); " . "INSERT INTO replace_table " . "(field,field2) " . - "VALUES ('text','text2')" + "VALUES ('text','text2'); COMMIT" ], [ [ @@ -572,11 +740,11 @@ class DatabaseSQLTest extends PHPUnit_Framework_TestCase { 'md_deps' => 'deps', ], ], - "DELETE FROM module_deps " . - "WHERE ( md_module='module' AND md_skin='skin' ); " . + "BEGIN; 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')" + "VALUES ('module','skin','deps'); COMMIT" ], [ [ @@ -594,16 +762,16 @@ class DatabaseSQLTest extends PHPUnit_Framework_TestCase { ], ], ], - "DELETE FROM module_deps " . - "WHERE ( md_module='module' AND md_skin='skin' ); " . + "BEGIN; 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' ); " . + "WHERE (md_module = 'module2' AND md_skin = 'skin2'); " . "INSERT INTO module_deps " . "(md_module,md_skin,md_deps) " . - "VALUES ('module2','skin2','deps2')" + "VALUES ('module2','skin2','deps2'); COMMIT" ], [ [ @@ -621,16 +789,16 @@ class DatabaseSQLTest extends PHPUnit_Framework_TestCase { ], ], ], - "DELETE FROM module_deps " . - "WHERE ( md_module='module' ) OR ( md_skin='skin' ); " . + "BEGIN; 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' ); " . + "WHERE (md_module = 'module2') OR (md_skin = 'skin2'); " . "INSERT INTO module_deps " . "(md_module,md_skin,md_deps) " . - "VALUES ('module2','skin2','deps2')" + "VALUES ('module2','skin2','deps2'); COMMIT" ], [ [ @@ -642,9 +810,9 @@ class DatabaseSQLTest extends PHPUnit_Framework_TestCase { 'md_deps' => 'deps', ], ], - "INSERT INTO module_deps " . + "BEGIN; INSERT INTO module_deps " . "(md_module,md_skin,md_deps) " . - "VALUES ('module','skin','deps')" + "VALUES ('module','skin','deps'); COMMIT" ], ]; } @@ -843,8 +1011,8 @@ class DatabaseSQLTest extends PHPUnit_Framework_TestCase { } public static function provideUnionConditionPermutations() { + // phpcs:disable Generic.Files.LineLength return [ - // @codingStandardsIgnoreStart Generic.Files.LineLength.TooLong [ [ 'table' => [ 'table1', 'table2' ], @@ -986,8 +1154,8 @@ class DatabaseSQLTest extends PHPUnit_Framework_TestCase { ], "SELECT foo_id FROM foo WHERE baz IS NULL ORDER BY foo_id LIMIT 150,25" ], - // @codingStandardsIgnoreEnd ]; + // phpcs:enable } /** @@ -1148,4 +1316,752 @@ class DatabaseSQLTest extends PHPUnit_Framework_TestCase { $this->assertFalse( $this->database->tableExists( "tmp_table_2", __METHOD__ ) ); $this->assertFalse( $this->database->tableExists( "tmp_table_3", __METHOD__ ) ); } + + public function provideBuildSubstring() { + yield [ 'someField', 1, 2, 'SUBSTRING(someField FROM 1 FOR 2)' ]; + yield [ 'someField', 1, null, 'SUBSTRING(someField FROM 1)' ]; + } + + /** + * @covers Wikimedia\Rdbms\Database::buildSubstring + * @dataProvider provideBuildSubstring + */ + public function testBuildSubstring( $input, $start, $length, $expected ) { + $output = $this->database->buildSubstring( $input, $start, $length ); + $this->assertSame( $expected, $output ); + } + + public function provideBuildSubstring_invalidParams() { + yield [ -1, 1 ]; + yield [ 1, -1 ]; + yield [ 1, 'foo' ]; + yield [ 'foo', 1 ]; + yield [ null, 1 ]; + yield [ 0, 1 ]; + } + + /** + * @covers Wikimedia\Rdbms\Database::buildSubstring + * @covers Wikimedia\Rdbms\Database::assertBuildSubstringParams + * @dataProvider provideBuildSubstring_invalidParams + */ + public function testBuildSubstring_invalidParams( $start, $length ) { + $this->setExpectedException( InvalidArgumentException::class ); + $this->database->buildSubstring( 'foo', $start, $length ); + } + + /** + * @covers \Wikimedia\Rdbms\Database::buildIntegerCast + */ + public function testBuildIntegerCast() { + $output = $this->database->buildIntegerCast( 'fieldName' ); + $this->assertSame( 'CAST( fieldName AS INTEGER )', $output ); + } + + /** + * @covers \Wikimedia\Rdbms\Database::doSavepoint + * @covers \Wikimedia\Rdbms\Database::doReleaseSavepoint + * @covers \Wikimedia\Rdbms\Database::doRollbackToSavepoint + * @covers \Wikimedia\Rdbms\Database::startAtomic + * @covers \Wikimedia\Rdbms\Database::endAtomic + * @covers \Wikimedia\Rdbms\Database::cancelAtomic + * @covers \Wikimedia\Rdbms\Database::doAtomicSection + */ + public function testAtomicSections() { + $this->database->startAtomic( __METHOD__ ); + $this->database->endAtomic( __METHOD__ ); + $this->assertLastSql( 'BEGIN; COMMIT' ); + + $this->database->startAtomic( __METHOD__, IDatabase::ATOMIC_CANCELABLE ); + $this->database->cancelAtomic( __METHOD__ ); + $this->assertLastSql( 'BEGIN; ROLLBACK' ); + + $this->database->begin( __METHOD__ ); + $this->database->startAtomic( __METHOD__ ); + $this->database->endAtomic( __METHOD__ ); + $this->database->commit( __METHOD__ ); + $this->assertLastSql( 'BEGIN; COMMIT' ); + + $this->database->begin( __METHOD__ ); + $this->database->startAtomic( __METHOD__, IDatabase::ATOMIC_CANCELABLE ); + $this->database->endAtomic( __METHOD__ ); + $this->database->commit( __METHOD__ ); + // phpcs:ignore Generic.Files.LineLength + $this->assertLastSql( 'BEGIN; SAVEPOINT wikimedia_rdbms_atomic1; RELEASE SAVEPOINT wikimedia_rdbms_atomic1; COMMIT' ); + + $this->database->begin( __METHOD__ ); + $this->database->startAtomic( __METHOD__, IDatabase::ATOMIC_CANCELABLE ); + $this->database->cancelAtomic( __METHOD__ ); + $this->database->commit( __METHOD__ ); + // phpcs:ignore Generic.Files.LineLength + $this->assertLastSql( 'BEGIN; SAVEPOINT wikimedia_rdbms_atomic1; ROLLBACK TO SAVEPOINT wikimedia_rdbms_atomic1; COMMIT' ); + + $this->database->startAtomic( __METHOD__, IDatabase::ATOMIC_CANCELABLE ); + $this->database->startAtomic( __METHOD__, IDatabase::ATOMIC_CANCELABLE ); + $this->database->cancelAtomic( __METHOD__ ); + $this->database->endAtomic( __METHOD__ ); + // phpcs:ignore Generic.Files.LineLength + $this->assertLastSql( 'BEGIN; SAVEPOINT wikimedia_rdbms_atomic1; ROLLBACK TO SAVEPOINT wikimedia_rdbms_atomic1; COMMIT' ); + + $noOpCallack = function () { + }; + + $this->database->doAtomicSection( __METHOD__, $noOpCallack, IDatabase::ATOMIC_CANCELABLE ); + $this->assertLastSql( 'BEGIN; COMMIT' ); + + $this->database->doAtomicSection( __METHOD__, $noOpCallack ); + $this->assertLastSql( 'BEGIN; COMMIT' ); + + $this->database->begin( __METHOD__ ); + $this->database->doAtomicSection( __METHOD__, $noOpCallack, IDatabase::ATOMIC_CANCELABLE ); + $this->database->rollback( __METHOD__ ); + // phpcs:ignore Generic.Files.LineLength + $this->assertLastSql( 'BEGIN; SAVEPOINT wikimedia_rdbms_atomic1; RELEASE SAVEPOINT wikimedia_rdbms_atomic1; ROLLBACK' ); + + $fname = __METHOD__; + $triggerMap = [ + '-' => '-', + IDatabase::TRIGGER_COMMIT => 'tCommit', + IDatabase::TRIGGER_ROLLBACK => 'tRollback' + ]; + $callback1 = function ( $trigger = '-' ) use ( $fname, $triggerMap ) { + $this->database->query( "SELECT 1, {$triggerMap[$trigger]} AS t", $fname ); + }; + $callback2 = function ( $trigger = '-' ) use ( $fname, $triggerMap ) { + $this->database->query( "SELECT 2, {$triggerMap[$trigger]} AS t", $fname ); + }; + $callback3 = function ( $trigger = '-' ) use ( $fname, $triggerMap ) { + $this->database->query( "SELECT 3, {$triggerMap[$trigger]} AS t", $fname ); + }; + + $this->database->startAtomic( __METHOD__, IDatabase::ATOMIC_CANCELABLE ); + $this->database->onTransactionPreCommitOrIdle( $callback1, __METHOD__ ); + $this->database->cancelAtomic( __METHOD__ ); + $this->assertLastSql( 'BEGIN; ROLLBACK' ); + + $this->database->startAtomic( __METHOD__, IDatabase::ATOMIC_CANCELABLE ); + $this->database->onTransactionIdle( $callback1, __METHOD__ ); + $this->database->cancelAtomic( __METHOD__ ); + $this->assertLastSql( 'BEGIN; ROLLBACK' ); + + $this->database->startAtomic( __METHOD__, IDatabase::ATOMIC_CANCELABLE ); + $this->database->onTransactionResolution( $callback1, __METHOD__ ); + $this->database->cancelAtomic( __METHOD__ ); + $this->assertLastSql( 'BEGIN; ROLLBACK; SELECT 1, tRollback AS t' ); + + $this->database->startAtomic( __METHOD__ . '_outer' ); + $this->database->onTransactionPreCommitOrIdle( $callback1, __METHOD__ ); + $this->database->startAtomic( __METHOD__, IDatabase::ATOMIC_CANCELABLE ); + $this->database->onTransactionPreCommitOrIdle( $callback2, __METHOD__ ); + $this->database->cancelAtomic( __METHOD__ ); + $this->database->onTransactionPreCommitOrIdle( $callback3, __METHOD__ ); + $this->database->endAtomic( __METHOD__ . '_outer' ); + $this->assertLastSql( implode( "; ", [ + 'BEGIN', + 'SAVEPOINT wikimedia_rdbms_atomic1', + 'ROLLBACK TO SAVEPOINT wikimedia_rdbms_atomic1', + 'SELECT 1, - AS t', + 'SELECT 3, - AS t', + 'COMMIT' + ] ) ); + + $this->database->startAtomic( __METHOD__ . '_outer' ); + $this->database->onTransactionIdle( $callback1, __METHOD__ ); + $this->database->startAtomic( __METHOD__, IDatabase::ATOMIC_CANCELABLE ); + $this->database->onTransactionIdle( $callback2, __METHOD__ ); + $this->database->cancelAtomic( __METHOD__ ); + $this->database->onTransactionIdle( $callback3, __METHOD__ ); + $this->database->endAtomic( __METHOD__ . '_outer' ); + $this->assertLastSql( implode( "; ", [ + 'BEGIN', + 'SAVEPOINT wikimedia_rdbms_atomic1', + 'ROLLBACK TO SAVEPOINT wikimedia_rdbms_atomic1', + 'COMMIT', + 'SELECT 1, tCommit AS t', + 'SELECT 3, tCommit AS t' + ] ) ); + + $this->database->startAtomic( __METHOD__ . '_outer' ); + $this->database->onTransactionResolution( $callback1, __METHOD__ ); + $this->database->startAtomic( __METHOD__, IDatabase::ATOMIC_CANCELABLE ); + $this->database->onTransactionResolution( $callback2, __METHOD__ ); + $this->database->cancelAtomic( __METHOD__ ); + $this->database->onTransactionResolution( $callback3, __METHOD__ ); + $this->database->endAtomic( __METHOD__ . '_outer' ); + $this->assertLastSql( implode( "; ", [ + 'BEGIN', + 'SAVEPOINT wikimedia_rdbms_atomic1', + 'ROLLBACK TO SAVEPOINT wikimedia_rdbms_atomic1', + 'COMMIT', + 'SELECT 1, tCommit AS t', + 'SELECT 2, tRollback AS t', + 'SELECT 3, tCommit AS t' + ] ) ); + + $makeCallback = function ( $id ) use ( $fname, $triggerMap ) { + return function ( $trigger = '-' ) use ( $id, $fname, $triggerMap ) { + $this->database->query( "SELECT $id, {$triggerMap[$trigger]} AS t", $fname ); + }; + }; + + $this->database->startAtomic( __METHOD__ . '_outer' ); + $this->database->startAtomic( __METHOD__, IDatabase::ATOMIC_CANCELABLE ); + $this->database->onTransactionResolution( $makeCallback( 1 ), __METHOD__ ); + $this->database->cancelAtomic( __METHOD__ ); + $this->database->endAtomic( __METHOD__ . '_outer' ); + $this->assertLastSql( implode( "; ", [ + 'BEGIN', + 'SAVEPOINT wikimedia_rdbms_atomic1', + 'ROLLBACK TO SAVEPOINT wikimedia_rdbms_atomic1', + 'COMMIT', + 'SELECT 1, tRollback AS t' + ] ) ); + + $this->database->startAtomic( __METHOD__ . '_level1', IDatabase::ATOMIC_CANCELABLE ); + $this->database->onTransactionResolution( $makeCallback( 1 ), __METHOD__ ); + $this->database->startAtomic( __METHOD__ . '_level2' ); + $this->database->startAtomic( __METHOD__ . '_level3', IDatabase::ATOMIC_CANCELABLE ); + $this->database->startAtomic( __METHOD__, IDatabase::ATOMIC_CANCELABLE ); + $this->database->onTransactionResolution( $makeCallback( 2 ), __METHOD__ ); + $this->database->endAtomic( __METHOD__ ); + $this->database->onTransactionResolution( $makeCallback( 3 ), __METHOD__ ); + $this->database->cancelAtomic( __METHOD__ . '_level3' ); + $this->database->endAtomic( __METHOD__ . '_level2' ); + $this->database->onTransactionResolution( $makeCallback( 4 ), __METHOD__ ); + $this->database->endAtomic( __METHOD__ . '_level1' ); + $this->assertLastSql( implode( "; ", [ + 'BEGIN', + 'SAVEPOINT wikimedia_rdbms_atomic1', + 'SAVEPOINT wikimedia_rdbms_atomic2', + 'RELEASE SAVEPOINT wikimedia_rdbms_atomic2', + 'ROLLBACK TO SAVEPOINT wikimedia_rdbms_atomic1', + 'COMMIT; SELECT 1, tCommit AS t', + 'SELECT 2, tRollback AS t', + 'SELECT 3, tRollback AS t', + 'SELECT 4, tCommit AS t' + ] ) ); + } + + /** + * @covers \Wikimedia\Rdbms\Database::doSavepoint + * @covers \Wikimedia\Rdbms\Database::doReleaseSavepoint + * @covers \Wikimedia\Rdbms\Database::doRollbackToSavepoint + * @covers \Wikimedia\Rdbms\Database::startAtomic + * @covers \Wikimedia\Rdbms\Database::endAtomic + * @covers \Wikimedia\Rdbms\Database::cancelAtomic + * @covers \Wikimedia\Rdbms\Database::doAtomicSection + */ + public function testAtomicSectionsRecovery() { + $this->database->begin( __METHOD__ ); + try { + $this->database->doAtomicSection( + __METHOD__, + function () { + $this->database->startAtomic( 'inner_func1' ); + $this->database->startAtomic( 'inner_func2' ); + + throw new RuntimeException( 'Test exception' ); + }, + IDatabase::ATOMIC_CANCELABLE + ); + $this->fail( 'Expected exception not thrown' ); + } catch ( RuntimeException $ex ) { + $this->assertSame( 'Test exception', $ex->getMessage() ); + } + $this->database->commit( __METHOD__ ); + // phpcs:ignore Generic.Files.LineLength + $this->assertLastSql( 'BEGIN; SAVEPOINT wikimedia_rdbms_atomic1; ROLLBACK TO SAVEPOINT wikimedia_rdbms_atomic1; COMMIT' ); + + $this->database->begin( __METHOD__ ); + try { + $this->database->doAtomicSection( + __METHOD__, + function () { + throw new RuntimeException( 'Test exception' ); + } + ); + $this->fail( 'Test exception not thrown' ); + } catch ( RuntimeException $ex ) { + $this->assertSame( 'Test exception', $ex->getMessage() ); + } + try { + $this->database->commit( __METHOD__ ); + $this->fail( 'Test exception not thrown' ); + } catch ( DBTransactionError $ex ) { + $this->assertSame( + 'Cannot execute query from ' . __METHOD__ . ' while transaction status is ERROR.', + $ex->getMessage() + ); + } + $this->database->rollback( __METHOD__ ); + $this->assertLastSql( 'BEGIN; ROLLBACK' ); + } + + /** + * @covers \Wikimedia\Rdbms\Database::doSavepoint + * @covers \Wikimedia\Rdbms\Database::doReleaseSavepoint + * @covers \Wikimedia\Rdbms\Database::doRollbackToSavepoint + * @covers \Wikimedia\Rdbms\Database::startAtomic + * @covers \Wikimedia\Rdbms\Database::endAtomic + * @covers \Wikimedia\Rdbms\Database::cancelAtomic + * @covers \Wikimedia\Rdbms\Database::doAtomicSection + */ + public function testAtomicSectionsCallbackCancellation() { + $fname = __METHOD__; + $callback1Called = null; + $callback1 = function ( $trigger = '-' ) use ( $fname, &$callback1Called ) { + $callback1Called = $trigger; + $this->database->query( "SELECT 1", $fname ); + }; + $callback2Called = null; + $callback2 = function ( $trigger = '-' ) use ( $fname, &$callback2Called ) { + $callback2Called = $trigger; + $this->database->query( "SELECT 2", $fname ); + }; + $callback3Called = null; + $callback3 = function ( $trigger = '-' ) use ( $fname, &$callback3Called ) { + $callback3Called = $trigger; + $this->database->query( "SELECT 3", $fname ); + }; + + $this->database->startAtomic( __METHOD__ . '_outer' ); + $this->database->startAtomic( __METHOD__, IDatabase::ATOMIC_CANCELABLE ); + $this->database->startAtomic( __METHOD__ . '_inner' ); + $this->database->onTransactionIdle( $callback1, __METHOD__ ); + $this->database->onTransactionPreCommitOrIdle( $callback2, __METHOD__ ); + $this->database->onTransactionResolution( $callback3, __METHOD__ ); + $this->database->endAtomic( __METHOD__ . '_inner' ); + $this->database->cancelAtomic( __METHOD__ ); + $this->database->endAtomic( __METHOD__ . '_outer' ); + $this->assertNull( $callback1Called ); + $this->assertNull( $callback2Called ); + $this->assertEquals( IDatabase::TRIGGER_ROLLBACK, $callback3Called ); + // phpcs:ignore Generic.Files.LineLength + $this->assertLastSql( 'BEGIN; SAVEPOINT wikimedia_rdbms_atomic1; ROLLBACK TO SAVEPOINT wikimedia_rdbms_atomic1; COMMIT; SELECT 3' ); + + $callback1Called = null; + $callback2Called = null; + $callback3Called = null; + $this->database->startAtomic( __METHOD__ . '_outer' ); + $this->database->startAtomic( __METHOD__, IDatabase::ATOMIC_CANCELABLE ); + $this->database->startAtomic( __METHOD__ . '_inner', IDatabase::ATOMIC_CANCELABLE ); + $this->database->onTransactionIdle( $callback1, __METHOD__ ); + $this->database->onTransactionPreCommitOrIdle( $callback2, __METHOD__ ); + $this->database->onTransactionResolution( $callback3, __METHOD__ ); + $this->database->endAtomic( __METHOD__ . '_inner' ); + $this->database->cancelAtomic( __METHOD__ ); + $this->database->endAtomic( __METHOD__ . '_outer' ); + $this->assertNull( $callback1Called ); + $this->assertNull( $callback2Called ); + $this->assertEquals( IDatabase::TRIGGER_ROLLBACK, $callback3Called ); + // phpcs:ignore Generic.Files.LineLength + $this->assertLastSql( 'BEGIN; SAVEPOINT wikimedia_rdbms_atomic1; SAVEPOINT wikimedia_rdbms_atomic2; RELEASE SAVEPOINT wikimedia_rdbms_atomic2; ROLLBACK TO SAVEPOINT wikimedia_rdbms_atomic1; COMMIT; SELECT 3' ); + + $callback1Called = null; + $callback2Called = null; + $callback3Called = null; + $this->database->startAtomic( __METHOD__ . '_outer' ); + $atomicId = $this->database->startAtomic( __METHOD__, IDatabase::ATOMIC_CANCELABLE ); + $this->database->startAtomic( __METHOD__ . '_inner' ); + $this->database->onTransactionIdle( $callback1, __METHOD__ ); + $this->database->onTransactionPreCommitOrIdle( $callback2, __METHOD__ ); + $this->database->onTransactionResolution( $callback3, __METHOD__ ); + $this->database->cancelAtomic( __METHOD__, $atomicId ); + $this->database->endAtomic( __METHOD__ . '_outer' ); + $this->assertNull( $callback1Called ); + $this->assertNull( $callback2Called ); + $this->assertEquals( IDatabase::TRIGGER_ROLLBACK, $callback3Called ); + + $callback1Called = null; + $callback2Called = null; + $callback3Called = null; + $this->database->startAtomic( __METHOD__ . '_outer' ); + $atomicId = $this->database->startAtomic( __METHOD__, IDatabase::ATOMIC_CANCELABLE ); + $this->database->startAtomic( __METHOD__ . '_inner' ); + $this->database->onTransactionIdle( $callback1, __METHOD__ ); + $this->database->onTransactionPreCommitOrIdle( $callback2, __METHOD__ ); + $this->database->onTransactionResolution( $callback3, __METHOD__ ); + try { + $this->database->cancelAtomic( __METHOD__ . '_X', $atomicId ); + } catch ( DBUnexpectedError $e ) { + $m = __METHOD__; + $this->assertSame( + "Invalid atomic section ended (got {$m}_X but expected {$m}).", + $e->getMessage() + ); + } + $this->database->cancelAtomic( __METHOD__ ); + $this->database->endAtomic( __METHOD__ . '_outer' ); + $this->assertNull( $callback1Called ); + $this->assertNull( $callback2Called ); + $this->assertEquals( IDatabase::TRIGGER_ROLLBACK, $callback3Called ); + + $this->database->startAtomic( __METHOD__ . '_outer' ); + $this->database->startAtomic( __METHOD__, IDatabase::ATOMIC_CANCELABLE ); + $this->database->startAtomic( __METHOD__ . '_inner' ); + $this->database->onTransactionIdle( $callback1, __METHOD__ ); + $this->database->onTransactionPreCommitOrIdle( $callback2, __METHOD__ ); + $this->database->onTransactionResolution( $callback3, __METHOD__ ); + $this->database->cancelAtomic( __METHOD__ . '_inner' ); + $this->database->cancelAtomic( __METHOD__ ); + $this->database->endAtomic( __METHOD__ . '_outer' ); + $this->assertNull( $callback1Called ); + $this->assertNull( $callback2Called ); + $this->assertEquals( IDatabase::TRIGGER_ROLLBACK, $callback3Called ); + + $wrapper = TestingAccessWrapper::newFromObject( $this->database ); + $callback1Called = null; + $callback2Called = null; + $callback3Called = null; + $this->database->startAtomic( __METHOD__ . '_outer' ); + $this->database->startAtomic( __METHOD__, IDatabase::ATOMIC_CANCELABLE ); + $this->database->startAtomic( __METHOD__ . '_inner' ); + $this->database->onTransactionIdle( $callback1, __METHOD__ ); + $this->database->onTransactionPreCommitOrIdle( $callback2, __METHOD__ ); + $this->database->onTransactionResolution( $callback3, __METHOD__ ); + $wrapper->trxStatus = Database::STATUS_TRX_ERROR; + $this->database->cancelAtomic( __METHOD__ . '_inner' ); + $this->database->cancelAtomic( __METHOD__ ); + $this->database->endAtomic( __METHOD__ . '_outer' ); + $this->assertNull( $callback1Called ); + $this->assertNull( $callback2Called ); + $this->assertEquals( IDatabase::TRIGGER_ROLLBACK, $callback3Called ); + } + + /** + * @covers \Wikimedia\Rdbms\Database::doSavepoint + * @covers \Wikimedia\Rdbms\Database::doReleaseSavepoint + * @covers \Wikimedia\Rdbms\Database::doRollbackToSavepoint + * @covers \Wikimedia\Rdbms\Database::startAtomic + * @covers \Wikimedia\Rdbms\Database::endAtomic + * @covers \Wikimedia\Rdbms\Database::cancelAtomic + * @covers \Wikimedia\Rdbms\Database::doAtomicSection + */ + public function testAtomicSectionsTrxRound() { + $this->database->setFlag( IDatabase::DBO_TRX ); + $this->database->startAtomic( __METHOD__, IDatabase::ATOMIC_CANCELABLE ); + $this->database->query( 'SELECT 1', __METHOD__ ); + $this->database->endAtomic( __METHOD__ ); + $this->database->commit( __METHOD__, IDatabase::FLUSHING_ALL_PEERS ); + // phpcs:ignore Generic.Files.LineLength + $this->assertLastSql( 'BEGIN; SAVEPOINT wikimedia_rdbms_atomic1; SELECT 1; RELEASE SAVEPOINT wikimedia_rdbms_atomic1; COMMIT' ); + } + + public static function provideAtomicSectionMethodsForErrors() { + return [ + [ 'endAtomic' ], + [ 'cancelAtomic' ], + ]; + } + + /** + * @dataProvider provideAtomicSectionMethodsForErrors + * @covers \Wikimedia\Rdbms\Database::endAtomic + * @covers \Wikimedia\Rdbms\Database::cancelAtomic + */ + public function testNoAtomicSection( $method ) { + try { + $this->database->$method( __METHOD__ ); + $this->fail( 'Expected exception not thrown' ); + } catch ( DBUnexpectedError $ex ) { + $this->assertSame( + 'No atomic section is open (got ' . __METHOD__ . ').', + $ex->getMessage() + ); + } + } + + /** + * @dataProvider provideAtomicSectionMethodsForErrors + * @covers \Wikimedia\Rdbms\Database::endAtomic + * @covers \Wikimedia\Rdbms\Database::cancelAtomic + */ + public function testInvalidAtomicSectionEnded( $method ) { + $this->database->startAtomic( __METHOD__ . 'X' ); + try { + $this->database->$method( __METHOD__ ); + $this->fail( 'Expected exception not thrown' ); + } catch ( DBUnexpectedError $ex ) { + $this->assertSame( + 'Invalid atomic section ended (got ' . __METHOD__ . ' but expected ' . + __METHOD__ . 'X' . ').', + $ex->getMessage() + ); + } + } + + /** + * @covers \Wikimedia\Rdbms\Database::cancelAtomic + */ + public function testUncancellableAtomicSection() { + $this->database->startAtomic( __METHOD__ ); + try { + $this->database->cancelAtomic( __METHOD__ ); + $this->database->select( 'test', '1', [], __METHOD__ ); + $this->fail( 'Expected exception not thrown' ); + } catch ( DBTransactionError $ex ) { + $this->assertSame( + 'Cannot execute query from ' . __METHOD__ . ' while transaction status is ERROR.', + $ex->getMessage() + ); + } + } + + /** + * @expectedException \Wikimedia\Rdbms\DBTransactionStateError + */ + public function testTransactionErrorState1() { + $wrapper = TestingAccessWrapper::newFromObject( $this->database ); + + $this->database->begin( __METHOD__ ); + $wrapper->trxStatus = Database::STATUS_TRX_ERROR; + $this->database->delete( 'x', [ 'field' => 3 ], __METHOD__ ); + $this->database->commit( __METHOD__ ); + } + + /** + * @covers \Wikimedia\Rdbms\Database::query + */ + public function testTransactionErrorState2() { + $wrapper = TestingAccessWrapper::newFromObject( $this->database ); + + $this->database->startAtomic( __METHOD__ ); + $wrapper->trxStatus = Database::STATUS_TRX_ERROR; + $this->database->rollback( __METHOD__ ); + $this->assertEquals( 0, $this->database->trxLevel() ); + $this->assertEquals( Database::STATUS_TRX_NONE, $wrapper->trxStatus() ); + $this->assertLastSql( 'BEGIN; ROLLBACK' ); + + $this->database->startAtomic( __METHOD__ ); + $this->assertEquals( Database::STATUS_TRX_OK, $wrapper->trxStatus() ); + $this->database->delete( 'x', [ 'field' => 1 ], __METHOD__ ); + $this->database->endAtomic( __METHOD__ ); + $this->assertEquals( Database::STATUS_TRX_NONE, $wrapper->trxStatus() ); + $this->assertLastSql( 'BEGIN; DELETE FROM x WHERE field = \'1\'; COMMIT' ); + $this->assertEquals( 0, $this->database->trxLevel(), 'Use after rollback()' ); + + $this->database->begin( __METHOD__ ); + $this->database->startAtomic( __METHOD__, Database::ATOMIC_CANCELABLE ); + $this->database->update( 'y', [ 'a' => 1 ], [ 'field' => 1 ], __METHOD__ ); + $wrapper->trxStatus = Database::STATUS_TRX_ERROR; + $this->database->cancelAtomic( __METHOD__ ); + $this->assertEquals( Database::STATUS_TRX_OK, $wrapper->trxStatus() ); + $this->database->startAtomic( __METHOD__ ); + $this->database->delete( 'y', [ 'field' => 1 ], __METHOD__ ); + $this->database->endAtomic( __METHOD__ ); + $this->database->commit( __METHOD__ ); + // phpcs:ignore Generic.Files.LineLength + $this->assertLastSql( 'BEGIN; SAVEPOINT wikimedia_rdbms_atomic1; UPDATE y SET a = \'1\' WHERE field = \'1\'; ROLLBACK TO SAVEPOINT wikimedia_rdbms_atomic1; DELETE FROM y WHERE field = \'1\'; COMMIT' ); + $this->assertEquals( 0, $this->database->trxLevel(), 'Use after rollback()' ); + + // Next transaction + $this->database->startAtomic( __METHOD__ ); + $this->assertEquals( Database::STATUS_TRX_OK, $wrapper->trxStatus() ); + $this->database->delete( 'x', [ 'field' => 3 ], __METHOD__ ); + $this->database->endAtomic( __METHOD__ ); + $this->assertEquals( Database::STATUS_TRX_NONE, $wrapper->trxStatus() ); + $this->assertLastSql( 'BEGIN; DELETE FROM x WHERE field = \'3\'; COMMIT' ); + $this->assertEquals( 0, $this->database->trxLevel() ); + } + + /** + * @covers \Wikimedia\Rdbms\Database::query + */ + public function testImplicitTransactionRollback() { + $doError = function () { + $this->database->forceNextQueryError( 666, 'Evilness' ); + try { + $this->database->delete( 'error', '1', __CLASS__ . '::SomeCaller' ); + $this->fail( 'Expected exception not thrown' ); + } catch ( DBError $e ) { + $this->assertSame( 666, $e->errno ); + } + }; + + $this->database->setFlag( Database::DBO_TRX ); + + // Implicit transaction gets silently rolled back + $this->database->begin( __METHOD__, Database::TRANSACTION_INTERNAL ); + call_user_func( $doError ); + $this->database->delete( 'x', [ 'field' => 1 ], __METHOD__ ); + $this->database->commit( __METHOD__, Database::FLUSHING_INTERNAL ); + // phpcs:ignore + $this->assertLastSql( 'BEGIN; DELETE FROM error WHERE 1; ROLLBACK; BEGIN; DELETE FROM x WHERE field = \'1\'; COMMIT' ); + + // ... unless there were prior writes + $this->database->begin( __METHOD__, Database::TRANSACTION_INTERNAL ); + $this->database->delete( 'x', [ 'field' => 1 ], __METHOD__ ); + call_user_func( $doError ); + try { + $this->database->delete( 'x', [ 'field' => 1 ], __METHOD__ ); + $this->fail( 'Expected exception not thrown' ); + } catch ( DBTransactionStateError $e ) { + } + $this->database->rollback( __METHOD__, Database::FLUSHING_INTERNAL ); + // phpcs:ignore + $this->assertLastSql( 'BEGIN; DELETE FROM x WHERE field = \'1\'; DELETE FROM error WHERE 1; ROLLBACK' ); + } + + /** + * @covers \Wikimedia\Rdbms\Database::query + */ + public function testTransactionStatementRollbackIgnoring() { + $wrapper = TestingAccessWrapper::newFromObject( $this->database ); + $warning = []; + $wrapper->deprecationLogger = function ( $msg ) use ( &$warning ) { + $warning[] = $msg; + }; + + $doError = function () { + $this->database->forceNextQueryError( 666, 'Evilness', [ + 'wasKnownStatementRollbackError' => true, + ] ); + try { + $this->database->delete( 'error', '1', __CLASS__ . '::SomeCaller' ); + $this->fail( 'Expected exception not thrown' ); + } catch ( DBError $e ) { + $this->assertSame( 666, $e->errno ); + } + }; + $expectWarning = 'Caller from ' . __METHOD__ . + ' ignored an error originally raised from ' . __CLASS__ . '::SomeCaller: [666] Evilness'; + + // Rollback doesn't raise a warning + $warning = []; + $this->database->startAtomic( __METHOD__ ); + call_user_func( $doError ); + $this->database->rollback( __METHOD__ ); + $this->database->delete( 'x', [ 'field' => 1 ], __METHOD__ ); + $this->assertSame( [], $warning ); + // phpcs:ignore + $this->assertLastSql( 'BEGIN; DELETE FROM error WHERE 1; ROLLBACK; DELETE FROM x WHERE field = \'1\'' ); + + // cancelAtomic() doesn't raise a warning + $warning = []; + $this->database->begin( __METHOD__ ); + $this->database->startAtomic( __METHOD__, Database::ATOMIC_CANCELABLE ); + call_user_func( $doError ); + $this->database->cancelAtomic( __METHOD__ ); + $this->database->delete( 'x', [ 'field' => 1 ], __METHOD__ ); + $this->database->commit( __METHOD__ ); + $this->assertSame( [], $warning ); + // phpcs:ignore + $this->assertLastSql( 'BEGIN; SAVEPOINT wikimedia_rdbms_atomic1; DELETE FROM error WHERE 1; ROLLBACK TO SAVEPOINT wikimedia_rdbms_atomic1; DELETE FROM x WHERE field = \'1\'; COMMIT' ); + + // Commit does raise a warning + $warning = []; + $this->database->begin( __METHOD__ ); + call_user_func( $doError ); + $this->database->commit( __METHOD__ ); + $this->assertSame( [ $expectWarning ], $warning ); + $this->assertLastSql( 'BEGIN; DELETE FROM error WHERE 1; COMMIT' ); + + // Deprecation only gets raised once + $warning = []; + $this->database->begin( __METHOD__ ); + call_user_func( $doError ); + $this->database->delete( 'x', [ 'field' => 1 ], __METHOD__ ); + $this->database->commit( __METHOD__ ); + $this->assertSame( [ $expectWarning ], $warning ); + // phpcs:ignore + $this->assertLastSql( 'BEGIN; DELETE FROM error WHERE 1; DELETE FROM x WHERE field = \'1\'; COMMIT' ); + } + + /** + * @covers \Wikimedia\Rdbms\Database::close + */ + public function testPrematureClose1() { + $fname = __METHOD__; + $this->database->begin( __METHOD__ ); + $this->database->onTransactionIdle( function () use ( $fname ) { + $this->database->query( 'SELECT 1', $fname ); + } ); + $this->database->delete( 'x', [ 'field' => 3 ], __METHOD__ ); + $this->database->close(); + + $this->assertFalse( $this->database->isOpen() ); + $this->assertLastSql( 'BEGIN; DELETE FROM x WHERE field = \'3\'; COMMIT; SELECT 1' ); + $this->assertEquals( 0, $this->database->trxLevel() ); + } + + /** + * @covers \Wikimedia\Rdbms\Database::close + */ + public function testPrematureClose2() { + try { + $fname = __METHOD__; + $this->database->startAtomic( __METHOD__ ); + $this->database->onTransactionIdle( function () use ( $fname ) { + $this->database->query( 'SELECT 1', $fname ); + } ); + $this->database->delete( 'x', [ 'field' => 3 ], __METHOD__ ); + $this->database->close(); + $this->fail( 'Expected exception not thrown' ); + } catch ( DBUnexpectedError $ex ) { + $this->assertSame( + 'Wikimedia\Rdbms\Database::close: atomic sections ' . + 'DatabaseSQLTest::testPrematureClose2 are still open.', + $ex->getMessage() + ); + } + + $this->assertFalse( $this->database->isOpen() ); + $this->assertLastSql( 'BEGIN; DELETE FROM x WHERE field = \'3\'; ROLLBACK' ); + $this->assertEquals( 0, $this->database->trxLevel() ); + } + + /** + * @covers \Wikimedia\Rdbms\Database::close + */ + public function testPrematureClose3() { + try { + $this->database->setFlag( IDatabase::DBO_TRX ); + $this->database->delete( 'x', [ 'field' => 3 ], __METHOD__ ); + $this->assertEquals( 1, $this->database->trxLevel() ); + $this->database->close(); + $this->fail( 'Expected exception not thrown' ); + } catch ( DBUnexpectedError $ex ) { + $this->assertSame( + 'Wikimedia\Rdbms\Database::close: ' . + 'mass commit/rollback of peer transaction required (DBO_TRX set).', + $ex->getMessage() + ); + } + + $this->assertFalse( $this->database->isOpen() ); + $this->assertLastSql( 'BEGIN; DELETE FROM x WHERE field = \'3\'; ROLLBACK' ); + $this->assertEquals( 0, $this->database->trxLevel() ); + } + + /** + * @covers \Wikimedia\Rdbms\Database::close + */ + public function testPrematureClose4() { + $this->database->setFlag( IDatabase::DBO_TRX ); + $this->database->query( 'SELECT 1', __METHOD__ ); + $this->assertEquals( 1, $this->database->trxLevel() ); + $this->database->close(); + $this->database->clearFlag( IDatabase::DBO_TRX ); + + $this->assertFalse( $this->database->isOpen() ); + $this->assertLastSql( 'BEGIN; SELECT 1; COMMIT' ); + $this->assertEquals( 0, $this->database->trxLevel() ); + } + + /** + * @covers Wikimedia\Rdbms\Database::selectFieldValues() + */ + public function testSelectFieldValues() { + $this->database->forceNextResult( [ + (object)[ 'value' => 'row1' ], + (object)[ 'value' => 'row2' ], + (object)[ 'value' => 'row3' ], + ] ); + + $this->assertSame( + [ 'row1', 'row2', 'row3' ], + $this->database->selectFieldValues( 'table', 'table.field', 'conds', __METHOD__ ) + ); + $this->assertLastSql( 'SELECT table.field AS value FROM table WHERE conds' ); + } } diff --git a/www/wiki/tests/phpunit/includes/libs/rdbms/database/DatabaseSqliteRdbmsTest.php b/www/wiki/tests/phpunit/includes/libs/rdbms/database/DatabaseSqliteRdbmsTest.php new file mode 100644 index 00000000..a886d6bf --- /dev/null +++ b/www/wiki/tests/phpunit/includes/libs/rdbms/database/DatabaseSqliteRdbmsTest.php @@ -0,0 +1,60 @@ +<?php + +use Wikimedia\Rdbms\DatabaseSqlite; + +/** + * DatabaseSqliteTest is already defined in mediawiki core hence the 'Rdbms' included in this + * class name. + * The test in core should have mediawiki specific stuff removed and the tests moved to this + * rdbms libs test. + */ +class DatabaseSqliteRdbmsTest extends PHPUnit\Framework\TestCase { + + use MediaWikiCoversValidator; + use PHPUnit4And6Compat; + + /** + * @return PHPUnit_Framework_MockObject_MockObject|DatabaseSqlite + */ + private function getMockDb() { + return $this->getMockBuilder( DatabaseSqlite::class ) + ->disableOriginalConstructor() + ->setMethods( null ) + ->getMock(); + } + + public function provideBuildSubstring() { + yield [ 'someField', 1, 2, 'SUBSTR(someField,1,2)' ]; + yield [ 'someField', 1, null, 'SUBSTR(someField,1)' ]; + } + + /** + * @covers Wikimedia\Rdbms\DatabaseSqlite::buildSubstring + * @dataProvider provideBuildSubstring + */ + public function testBuildSubstring( $input, $start, $length, $expected ) { + $dbMock = $this->getMockDb(); + $output = $dbMock->buildSubstring( $input, $start, $length ); + $this->assertSame( $expected, $output ); + } + + public function provideBuildSubstring_invalidParams() { + yield [ -1, 1 ]; + yield [ 1, -1 ]; + yield [ 1, 'foo' ]; + yield [ 'foo', 1 ]; + yield [ null, 1 ]; + yield [ 0, 1 ]; + } + + /** + * @covers Wikimedia\Rdbms\DatabaseSqlite::buildSubstring + * @dataProvider provideBuildSubstring_invalidParams + */ + public function testBuildSubstring_invalidParams( $start, $length ) { + $dbMock = $this->getMockDb(); + $this->setExpectedException( InvalidArgumentException::class ); + $dbMock->buildSubstring( 'foo', $start, $length ); + } + +} diff --git a/www/wiki/tests/phpunit/includes/libs/rdbms/database/DatabaseTest.php b/www/wiki/tests/phpunit/includes/libs/rdbms/database/DatabaseTest.php index 70b6c360..444a946e 100644 --- a/www/wiki/tests/phpunit/includes/libs/rdbms/database/DatabaseTest.php +++ b/www/wiki/tests/phpunit/includes/libs/rdbms/database/DatabaseTest.php @@ -1,16 +1,46 @@ <?php +use Wikimedia\Rdbms\Database; use Wikimedia\Rdbms\IDatabase; +use Wikimedia\Rdbms\DatabaseMysqli; use Wikimedia\Rdbms\LBFactorySingle; use Wikimedia\Rdbms\TransactionProfiler; use Wikimedia\TestingAccessWrapper; +use Wikimedia\Rdbms\DatabaseSqlite; +use Wikimedia\Rdbms\DatabasePostgres; +use Wikimedia\Rdbms\DatabaseMssql; -class DatabaseTest extends PHPUnit_Framework_TestCase { +class DatabaseTest extends PHPUnit\Framework\TestCase { + + use MediaWikiCoversValidator; protected function setUp() { $this->db = new DatabaseTestHelper( __CLASS__ . '::' . $this->getName() ); } + /** + * @dataProvider provideAddQuotes + * @covers Wikimedia\Rdbms\Database::factory + */ + public function testFactory() { + $m = Database::NEW_UNCONNECTED; // no-connect mode + $p = [ 'host' => 'localhost', 'user' => 'me', 'password' => 'myself', 'dbname' => 'i' ]; + + $this->assertInstanceOf( DatabaseMysqli::class, Database::factory( 'mysqli', $p, $m ) ); + $this->assertInstanceOf( DatabaseMysqli::class, Database::factory( 'MySqli', $p, $m ) ); + $this->assertInstanceOf( DatabaseMysqli::class, Database::factory( 'MySQLi', $p, $m ) ); + $this->assertInstanceOf( DatabasePostgres::class, Database::factory( 'postgres', $p, $m ) ); + $this->assertInstanceOf( DatabasePostgres::class, Database::factory( 'Postgres', $p, $m ) ); + + $x = $p + [ 'port' => 10000, 'UseWindowsAuth' => false ]; + $this->assertInstanceOf( DatabaseMssql::class, Database::factory( 'mssql', $x, $m ) ); + + $x = $p + [ 'dbFilePath' => 'some/file.sqlite' ]; + $this->assertInstanceOf( DatabaseSqlite::class, Database::factory( 'sqlite', $x, $m ) ); + $x = $p + [ 'dbDirectory' => 'some/file' ]; + $this->assertInstanceOf( DatabaseSqlite::class, Database::factory( 'sqlite', $x, $m ) ); + } + public static function provideAddQuotes() { return [ [ null, 'NULL' ], @@ -94,6 +124,52 @@ class DatabaseTest extends PHPUnit_Framework_TestCase { ); } + public function provideTableNamesWithIndexClauseOrJOIN() { + return [ + 'one-element array' => [ + [ 'table' ], [], 'table ' + ], + 'comma join' => [ + [ 'table1', 'table2' ], [], 'table1,table2 ' + ], + 'real join' => [ + [ 'table1', 'table2' ], + [ 'table2' => [ 'LEFT JOIN', 't1_id = t2_id' ] ], + 'table1 LEFT JOIN table2 ON ((t1_id = t2_id))' + ], + 'real join with multiple conditionals' => [ + [ 'table1', 'table2' ], + [ 'table2' => [ 'LEFT JOIN', [ 't1_id = t2_id', 't2_x = \'X\'' ] ] ], + 'table1 LEFT JOIN table2 ON ((t1_id = t2_id) AND (t2_x = \'X\'))' + ], + 'join with parenthesized group' => [ + [ 'table1', 'n' => [ 'table2', 'table3' ] ], + [ + 'table3' => [ 'JOIN', 't2_id = t3_id' ], + 'n' => [ 'LEFT JOIN', 't1_id = t2_id' ], + ], + 'table1 LEFT JOIN (table2 JOIN table3 ON ((t2_id = t3_id))) ON ((t1_id = t2_id))' + ], + 'join with degenerate parenthesized group' => [ + [ 'table1', 'n' => [ 't2' => 'table2' ] ], + [ + 'n' => [ 'LEFT JOIN', 't1_id = t2_id' ], + ], + 'table1 LEFT JOIN table2 t2 ON ((t1_id = t2_id))' + ], + ]; + } + + /** + * @dataProvider provideTableNamesWithIndexClauseOrJOIN + * @covers Wikimedia\Rdbms\Database::tableNamesWithIndexClauseOrJOIN + */ + public function testTableNamesWithIndexClauseOrJOIN( $tables, $join_conds, $expect ) { + $clause = TestingAccessWrapper::newFromObject( $this->db ) + ->tableNamesWithIndexClauseOrJOIN( $tables, [], [], $join_conds ); + $this->assertSame( $expect, $clause ); + } + /** * @covers Wikimedia\Rdbms\Database::onTransactionIdle * @covers Wikimedia\Rdbms\Database::runOnTransactionIdleCallbacks @@ -101,28 +177,27 @@ class DatabaseTest extends PHPUnit_Framework_TestCase { public function testTransactionIdle() { $db = $this->db; - $db->setFlag( DBO_TRX ); + $db->clearFlag( DBO_TRX ); $called = false; $flagSet = null; - $db->onTransactionIdle( - function () use ( $db, &$flagSet, &$called ) { - $called = true; - $flagSet = $db->getFlag( DBO_TRX ); - }, - __METHOD__ - ); - $this->assertFalse( $flagSet, 'DBO_TRX off in callback' ); - $this->assertTrue( $db->getFlag( DBO_TRX ), 'DBO_TRX restored to default' ); + $callback = function () use ( $db, &$flagSet, &$called ) { + $called = true; + $flagSet = $db->getFlag( DBO_TRX ); + }; + + $db->onTransactionIdle( $callback, __METHOD__ ); $this->assertTrue( $called, 'Callback reached' ); + $this->assertFalse( $flagSet, 'DBO_TRX off in callback' ); + $this->assertFalse( $db->getFlag( DBO_TRX ), 'DBO_TRX still default' ); - $db->clearFlag( DBO_TRX ); $flagSet = null; - $db->onTransactionIdle( - function () use ( $db, &$flagSet ) { - $flagSet = $db->getFlag( DBO_TRX ); - }, - __METHOD__ - ); + $called = false; + $db->startAtomic( __METHOD__ ); + $db->onTransactionIdle( $callback, __METHOD__ ); + $this->assertFalse( $called, 'Callback not reached during TRX' ); + $db->endAtomic( __METHOD__ ); + + $this->assertTrue( $called, 'Callback reached after COMMIT' ); $this->assertFalse( $flagSet, 'DBO_TRX off in callback' ); $this->assertFalse( $db->getFlag( DBO_TRX ), 'DBO_TRX restored to default' ); @@ -137,6 +212,56 @@ class DatabaseTest extends PHPUnit_Framework_TestCase { } /** + * @covers Wikimedia\Rdbms\Database::onTransactionIdle + * @covers Wikimedia\Rdbms\Database::runOnTransactionIdleCallbacks + */ + public function testTransactionIdle_TRX() { + $db = $this->getMockDB( [ 'isOpen', 'ping' ] ); + $db->method( 'isOpen' )->willReturn( true ); + $db->method( 'ping' )->willReturn( true ); + $db->setFlag( DBO_TRX ); + + $lbFactory = LBFactorySingle::newFromConnection( $db ); + // Ask for the connection so that LB sets internal state + // about this connection being the master connection + $lb = $lbFactory->getMainLB(); + $conn = $lb->openConnection( $lb->getWriterIndex() ); + $this->assertSame( $db, $conn, 'Same DB instance' ); + $this->assertTrue( $db->getFlag( DBO_TRX ), 'DBO_TRX is set' ); + + $called = false; + $flagSet = null; + $callback = function () use ( $db, &$flagSet, &$called ) { + $called = true; + $flagSet = $db->getFlag( DBO_TRX ); + }; + + $db->onTransactionIdle( $callback, __METHOD__ ); + $this->assertTrue( $called, 'Called when idle if DBO_TRX is set' ); + $this->assertFalse( $flagSet, 'DBO_TRX off in callback' ); + $this->assertTrue( $db->getFlag( DBO_TRX ), 'DBO_TRX still default' ); + + $called = false; + $lbFactory->beginMasterChanges( __METHOD__ ); + $db->onTransactionIdle( $callback, __METHOD__ ); + $this->assertFalse( $called, 'Not called when lb-transaction is active' ); + + $lbFactory->commitMasterChanges( __METHOD__ ); + $this->assertTrue( $called, 'Called when lb-transaction is committed' ); + + $called = false; + $lbFactory->beginMasterChanges( __METHOD__ ); + $db->onTransactionIdle( $callback, __METHOD__ ); + $this->assertFalse( $called, 'Not called when lb-transaction is active' ); + + $lbFactory->rollbackMasterChanges( __METHOD__ ); + $this->assertFalse( $called, 'Not called when lb-transaction is rolled back' ); + + $lbFactory->commitMasterChanges( __METHOD__ ); + $this->assertFalse( $called, 'Not called in next round commit' ); + } + + /** * @covers Wikimedia\Rdbms\Database::onTransactionPreCommitOrIdle * @covers Wikimedia\Rdbms\Database::runOnTransactionPreCommitCallbacks */ @@ -174,31 +299,47 @@ class DatabaseTest extends PHPUnit_Framework_TestCase { * @covers Wikimedia\Rdbms\Database::runOnTransactionPreCommitCallbacks */ public function testTransactionPreCommitOrIdle_TRX() { - $db = $this->getMockDB( [ 'isOpen' ] ); + $db = $this->getMockDB( [ 'isOpen', 'ping' ] ); $db->method( 'isOpen' )->willReturn( true ); + $db->method( 'ping' )->willReturn( true ); $db->setFlag( DBO_TRX ); $lbFactory = LBFactorySingle::newFromConnection( $db ); - // Ask for the connectin so that LB sets internal state + // Ask for the connection so that LB sets internal state // about this connection being the master connection $lb = $lbFactory->getMainLB(); $conn = $lb->openConnection( $lb->getWriterIndex() ); $this->assertSame( $db, $conn, 'Same DB instance' ); + + $this->assertFalse( $lb->hasMasterChanges() ); $this->assertTrue( $db->getFlag( DBO_TRX ), 'DBO_TRX is set' ); + $called = false; + $callback = function () use ( &$called ) { + $called = true; + }; + $db->onTransactionPreCommitOrIdle( $callback, __METHOD__ ); + $this->assertTrue( $called, 'Called when idle if DBO_TRX is set' ); + $called = false; + $lbFactory->commitMasterChanges(); + $this->assertFalse( $called ); $called = false; - $db->onTransactionPreCommitOrIdle( - function () use ( &$called ) { - $called = true; - } - ); - $this->assertFalse( $called, 'Not called when idle if DBO_TRX is set' ); + $lbFactory->beginMasterChanges( __METHOD__ ); + $db->onTransactionPreCommitOrIdle( $callback, __METHOD__ ); + $this->assertFalse( $called, 'Not called when lb-transaction is active' ); + $lbFactory->commitMasterChanges( __METHOD__ ); + $this->assertTrue( $called, 'Called when lb-transaction is committed' ); + $called = false; $lbFactory->beginMasterChanges( __METHOD__ ); + $db->onTransactionPreCommitOrIdle( $callback, __METHOD__ ); $this->assertFalse( $called, 'Not called when lb-transaction is active' ); + $lbFactory->rollbackMasterChanges( __METHOD__ ); + $this->assertFalse( $called, 'Not called when lb-transaction is rolled back' ); + $lbFactory->commitMasterChanges( __METHOD__ ); - $this->assertTrue( $called, 'Called when lb-transaction is committed' ); + $this->assertFalse( $called, 'Not called in next round commit' ); } /** @@ -274,7 +415,7 @@ class DatabaseTest extends PHPUnit_Framework_TestCase { */ private function getMockDB( $methods = [] ) { static $abstractMethods = [ - 'affectedRows', + 'fetchAffectedRowCount', 'closeConnection', 'dataSeek', 'doQuery', @@ -322,10 +463,34 @@ class DatabaseTest extends PHPUnit_Framework_TestCase { $this->assertFalse( (bool)$db->trxLevel(), "Transaction cleared." ); } + /** + * @covers Wikimedia\Rdbms\Database::getScopedLockAndFlush + * @covers Wikimedia\Rdbms\Database::lock + * @covers Wikimedia\Rdbms\Database::unlock + * @covers Wikimedia\Rdbms\Database::lockIsFree + */ public function testGetScopedLock() { $db = $this->getMockDB( [ 'isOpen' ] ); $db->method( 'isOpen' )->willReturn( true ); + $this->assertEquals( 0, $db->trxLevel() ); + $this->assertEquals( true, $db->lockIsFree( 'x', __METHOD__ ) ); + $this->assertEquals( true, $db->lock( 'x', __METHOD__ ) ); + $this->assertEquals( false, $db->lockIsFree( 'x', __METHOD__ ) ); + $this->assertEquals( true, $db->unlock( 'x', __METHOD__ ) ); + $this->assertEquals( true, $db->lockIsFree( 'x', __METHOD__ ) ); + $this->assertEquals( 0, $db->trxLevel() ); + + $db->setFlag( DBO_TRX ); + $this->assertEquals( true, $db->lockIsFree( 'x', __METHOD__ ) ); + $this->assertEquals( true, $db->lock( 'x', __METHOD__ ) ); + $this->assertEquals( false, $db->lockIsFree( 'x', __METHOD__ ) ); + $this->assertEquals( true, $db->unlock( 'x', __METHOD__ ) ); + $this->assertEquals( true, $db->lockIsFree( 'x', __METHOD__ ) ); + $db->clearFlag( DBO_TRX ); + + $this->assertEquals( 0, $db->trxLevel() ); + $db->setFlag( DBO_TRX ); try { $this->badLockingMethodImplicit( $db ); @@ -398,6 +563,32 @@ class DatabaseTest extends PHPUnit_Framework_TestCase { } /** + * @expectedException UnexpectedValueException + * @covers Wikimedia\Rdbms\Database::setFlag + */ + public function testDBOIgnoreSet() { + $db = $this->getMockBuilder( DatabaseMysqli::class ) + ->disableOriginalConstructor() + ->setMethods( null ) + ->getMock(); + + $db->setFlag( Database::DBO_IGNORE ); + } + + /** + * @expectedException UnexpectedValueException + * @covers Wikimedia\Rdbms\Database::clearFlag + */ + public function testDBOIgnoreClear() { + $db = $this->getMockBuilder( DatabaseMysqli::class ) + ->disableOriginalConstructor() + ->setMethods( null ) + ->getMock(); + + $db->clearFlag( Database::DBO_IGNORE ); + } + + /** * @covers Wikimedia\Rdbms\Database::tablePrefix * @covers Wikimedia\Rdbms\Database::dbSchema */ @@ -418,4 +609,5 @@ class DatabaseTest extends PHPUnit_Framework_TestCase { $this->db->dbSchema( $old ); $this->assertNotEquals( 'xxx', $this->db->dbSchema() ); } + } diff --git a/www/wiki/tests/phpunit/includes/libs/xmp/XMPTest.php b/www/wiki/tests/phpunit/includes/libs/xmp/XMPTest.php index 514e6cdd..73fd4716 100644 --- a/www/wiki/tests/phpunit/includes/libs/xmp/XMPTest.php +++ b/www/wiki/tests/phpunit/includes/libs/xmp/XMPTest.php @@ -4,7 +4,9 @@ * @group Media * @covers XMPReader */ -class XMPTest extends PHPUnit_Framework_TestCase { +class XMPTest extends PHPUnit\Framework\TestCase { + + use MediaWikiCoversValidator; protected function setUp() { parent::setUp(); diff --git a/www/wiki/tests/phpunit/includes/libs/xmp/XMPValidateTest.php b/www/wiki/tests/phpunit/includes/libs/xmp/XMPValidateTest.php index 7f7ea930..746f68ac 100644 --- a/www/wiki/tests/phpunit/includes/libs/xmp/XMPValidateTest.php +++ b/www/wiki/tests/phpunit/includes/libs/xmp/XMPValidateTest.php @@ -5,7 +5,9 @@ use Psr\Log\NullLogger; /** * @group Media */ -class XMPValidateTest extends PHPUnit_Framework_TestCase { +class XMPValidateTest extends PHPUnit\Framework\TestCase { + + use MediaWikiCoversValidator; /** * @dataProvider provideDates diff --git a/www/wiki/tests/phpunit/includes/linkeddata/PageDataRequestHandlerTest.php b/www/wiki/tests/phpunit/includes/linkeddata/PageDataRequestHandlerTest.php index 2b18b08b..ad0c3d1e 100644 --- a/www/wiki/tests/phpunit/includes/linkeddata/PageDataRequestHandlerTest.php +++ b/www/wiki/tests/phpunit/includes/linkeddata/PageDataRequestHandlerTest.php @@ -2,10 +2,7 @@ /** * @covers PageDataRequestHandler - * * @group PageData - * - * @license GPL-2.0+ */ class PageDataRequestHandlerTest extends \MediaWikiTestCase { diff --git a/www/wiki/tests/phpunit/includes/logging/BlockLogFormatterTest.php b/www/wiki/tests/phpunit/includes/logging/BlockLogFormatterTest.php index 4158ea23..03671ac8 100644 --- a/www/wiki/tests/phpunit/includes/logging/BlockLogFormatterTest.php +++ b/www/wiki/tests/phpunit/includes/logging/BlockLogFormatterTest.php @@ -1,5 +1,8 @@ <?php +/** + * @covers BlockLogFormatter + */ class BlockLogFormatterTest extends LogFormatterTestCase { /** diff --git a/www/wiki/tests/phpunit/includes/logging/ContentModelLogFormatterTest.php b/www/wiki/tests/phpunit/includes/logging/ContentModelLogFormatterTest.php new file mode 100644 index 00000000..17e54115 --- /dev/null +++ b/www/wiki/tests/phpunit/includes/logging/ContentModelLogFormatterTest.php @@ -0,0 +1,60 @@ +<?php + +/** + * @covers ContentModelLogFormatter + */ +class ContentModelLogFormatterTest extends LogFormatterTestCase { + public static function provideContentModelLogDatabaseRows() { + return [ + [ + [ + 'type' => 'contentmodel', + 'action' => 'new', + 'comment' => 'new content model comment', + 'namespace' => NS_MAIN, + 'title' => 'ContentModelPage', + 'params' => [ + '5::newModel' => 'testcontentmodel', + ], + ], + [ + 'text' => 'User created the page ContentModelPage ' . + 'using a non-default content model ' . + '"testcontentmodel"', + 'api' => [ + 'newModel' => 'testcontentmodel', + ], + ], + ], + [ + [ + 'type' => 'contentmodel', + 'action' => 'change', + 'comment' => 'change content model comment', + 'namespace' => NS_MAIN, + 'title' => 'ContentModelPage', + 'params' => [ + '4::oldmodel' => 'wikitext', + '5::newModel' => 'testcontentmodel', + ], + ], + [ + 'text' => 'User changed the content model of the page ' . + 'ContentModelPage from "wikitext" to ' . + '"testcontentmodel"', + 'api' => [ + 'oldmodel' => 'wikitext', + 'newModel' => 'testcontentmodel', + ], + ], + ], + ]; + } + + /** + * @dataProvider provideContentModelLogDatabaseRows + */ + public function testContentModelLogDatabaseRows( $row, $extra ) { + $this->doTestLogFormatter( $row, $extra ); + } +} diff --git a/www/wiki/tests/phpunit/includes/logging/DatabaseLogEntryTest.php b/www/wiki/tests/phpunit/includes/logging/DatabaseLogEntryTest.php new file mode 100644 index 00000000..4af1742e --- /dev/null +++ b/www/wiki/tests/phpunit/includes/logging/DatabaseLogEntryTest.php @@ -0,0 +1,162 @@ +<?php + +use MediaWiki\MediaWikiServices; +use Wikimedia\Rdbms\IDatabase; + +class DatabaseLogEntryTest extends MediaWikiTestCase { + public function setUp() { + parent::setUp(); + + // These services cache their joins + MediaWikiServices::getInstance()->resetServiceForTesting( 'CommentStore' ); + MediaWikiServices::getInstance()->resetServiceForTesting( 'ActorMigration' ); + } + + public function tearDown() { + parent::tearDown(); + + MediaWikiServices::getInstance()->resetServiceForTesting( 'CommentStore' ); + MediaWikiServices::getInstance()->resetServiceForTesting( 'ActorMigration' ); + } + + /** + * @covers DatabaseLogEntry::newFromId + * @covers DatabaseLogEntry::getSelectQueryData + * + * @dataProvider provideNewFromId + * + * @param int $id + * @param array $selectFields + * @param string[]|null $row + * @param string[]|null $expectedFields + * @param string $migration + */ + public function testNewFromId( $id, + array $selectFields, + array $row = null, + array $expectedFields = null, + $migration + ) { + $this->setMwGlobals( [ + 'wgCommentTableSchemaMigrationStage' => $migration, + 'wgActorTableSchemaMigrationStage' => $migration, + ] ); + + $row = $row ? (object)$row : null; + $db = $this->getMock( IDatabase::class ); + $db->expects( self::once() ) + ->method( 'selectRow' ) + ->with( $selectFields['tables'], + $selectFields['fields'], + $selectFields['conds'], + 'DatabaseLogEntry::newFromId', + $selectFields['options'], + $selectFields['join_conds'] + ) + ->will( self::returnValue( $row ) ); + + /** @var IDatabase $db */ + $logEntry = DatabaseLogEntry::newFromId( $id, $db ); + + if ( !$expectedFields ) { + self::assertNull( $logEntry, "Expected no log entry returned for id=$id" ); + } else { + self::assertEquals( $id, $logEntry->getId() ); + self::assertEquals( $expectedFields['type'], $logEntry->getType() ); + self::assertEquals( $expectedFields['comment'], $logEntry->getComment() ); + } + } + + public function provideNewFromId() { + $oldTables = [ + 'tables' => [ 'logging', 'user' ], + 'fields' => [ + 'log_id', + 'log_type', + 'log_action', + 'log_timestamp', + 'log_namespace', + 'log_title', + 'log_params', + 'log_deleted', + 'user_id', + 'user_name', + 'user_editcount', + 'log_comment_text' => 'log_comment', + 'log_comment_data' => 'NULL', + 'log_comment_cid' => 'NULL', + 'log_user' => 'log_user', + 'log_user_text' => 'log_user_text', + 'log_actor' => 'NULL', + ], + 'options' => [], + 'join_conds' => [ 'user' => [ 'LEFT JOIN', 'user_id=log_user' ] ], + ]; + $newTables = [ + 'tables' => [ + 'logging', + 'user', + 'comment_log_comment' => 'comment', + 'actor_log_user' => 'actor' + ], + 'fields' => [ + 'log_id', + 'log_type', + 'log_action', + 'log_timestamp', + 'log_namespace', + 'log_title', + 'log_params', + 'log_deleted', + 'user_id', + 'user_name', + 'user_editcount', + 'log_comment_text' => 'comment_log_comment.comment_text', + 'log_comment_data' => 'comment_log_comment.comment_data', + 'log_comment_cid' => 'comment_log_comment.comment_id', + 'log_user' => 'actor_log_user.actor_user', + 'log_user_text' => 'actor_log_user.actor_name', + 'log_actor' => 'log_actor', + ], + 'options' => [], + 'join_conds' => [ + 'user' => [ 'LEFT JOIN', 'user_id=actor_log_user.actor_user' ], + 'comment_log_comment' => [ 'JOIN', 'comment_log_comment.comment_id = log_comment_id' ], + 'actor_log_user' => [ 'JOIN', 'actor_log_user.actor_id = log_actor' ], + ], + ]; + return [ + [ + 0, + $oldTables + [ 'conds' => [ 'log_id' => 0 ] ], + null, + null, + MIGRATION_OLD, + ], + [ + 123, + $oldTables + [ 'conds' => [ 'log_id' => 123 ] ], + [ + 'log_id' => 123, + 'log_type' => 'foobarize', + 'log_comment_text' => 'test!', + 'log_comment_data' => null, + ], + [ 'type' => 'foobarize', 'comment' => 'test!' ], + MIGRATION_OLD, + ], + [ + 567, + $newTables + [ 'conds' => [ 'log_id' => 567 ] ], + [ + 'log_id' => 567, + 'log_type' => 'foobarize', + 'log_comment_text' => 'test!', + 'log_comment_data' => null, + ], + [ 'type' => 'foobarize', 'comment' => 'test!' ], + MIGRATION_NEW, + ], + ]; + } +} diff --git a/www/wiki/tests/phpunit/includes/logging/DeleteLogFormatterTest.php b/www/wiki/tests/phpunit/includes/logging/DeleteLogFormatterTest.php index 23378999..0e6855d9 100644 --- a/www/wiki/tests/phpunit/includes/logging/DeleteLogFormatterTest.php +++ b/www/wiki/tests/phpunit/includes/logging/DeleteLogFormatterTest.php @@ -1,5 +1,8 @@ <?php +/** + * @covers DeleteLogFormatter + */ class DeleteLogFormatterTest extends LogFormatterTestCase { /** diff --git a/www/wiki/tests/phpunit/includes/logging/ImportLogFormatterTest.php b/www/wiki/tests/phpunit/includes/logging/ImportLogFormatterTest.php index ec120780..80e4c0bc 100644 --- a/www/wiki/tests/phpunit/includes/logging/ImportLogFormatterTest.php +++ b/www/wiki/tests/phpunit/includes/logging/ImportLogFormatterTest.php @@ -1,5 +1,8 @@ <?php +/** + * @covers ImportLogFormatter + */ class ImportLogFormatterTest extends LogFormatterTestCase { /** diff --git a/www/wiki/tests/phpunit/includes/logging/LogFormatterTest.php b/www/wiki/tests/phpunit/includes/logging/LogFormatterTest.php index 1ef3df6c..e523a31e 100644 --- a/www/wiki/tests/phpunit/includes/logging/LogFormatterTest.php +++ b/www/wiki/tests/phpunit/includes/logging/LogFormatterTest.php @@ -53,8 +53,8 @@ class LogFormatterTest extends MediaWikiLangTestCase { $this->setMwGlobals( [ 'wgLogTypes' => [ 'phpunit' ], - 'wgLogActionsHandlers' => [ 'phpunit/test' => 'LogFormatter', - 'phpunit/param' => 'LogFormatter' ], + 'wgLogActionsHandlers' => [ 'phpunit/test' => LogFormatter::class, + 'phpunit/param' => LogFormatter::class ], 'wgUser' => User::newFromName( 'Testuser' ), ] ); @@ -308,6 +308,10 @@ class LogFormatterTest extends MediaWikiLangTestCase { 'key_ns' => NS_PROJECT, 'key_title' => Title::newFromText( 'project:foo' )->getFullText(), ] ], + [ '4:title-link:key', '<invalid>', [ + 'key_ns' => NS_SPECIAL, + 'key_title' => SpecialPage::getTitleFor( 'Badtitle', '<invalid>' )->getFullText(), + ] ], [ '4:user:key', 'foo', [ 'key' => 'Foo' ] ], [ '4:user-link:key', 'foo', [ 'key' => 'Foo' ] ], ]; diff --git a/www/wiki/tests/phpunit/includes/logging/LogFormatterTestCase.php b/www/wiki/tests/phpunit/includes/logging/LogFormatterTestCase.php index 2dc9a2cf..786d7619 100644 --- a/www/wiki/tests/phpunit/includes/logging/LogFormatterTestCase.php +++ b/www/wiki/tests/phpunit/includes/logging/LogFormatterTestCase.php @@ -36,6 +36,7 @@ abstract class LogFormatterTestCase extends MediaWikiLangTestCase { 'log_timestamp' => isset( $data['timestamp'] ) ? $data['timestamp'] : wfTimestampNow(), 'log_user' => isset( $data['user'] ) ? $data['user'] : 0, 'log_user_text' => isset( $data['user_text'] ) ? $data['user_text'] : 'User', + 'log_actor' => isset( $data['actor'] ) ? $data['actor'] : 0, 'log_namespace' => isset( $data['namespace'] ) ? $data['namespace'] : NS_MAIN, 'log_title' => isset( $data['title'] ) ? $data['title'] : 'Main_Page', 'log_page' => isset( $data['page'] ) ? $data['page'] : 0, diff --git a/www/wiki/tests/phpunit/includes/logging/MergeLogFormatterTest.php b/www/wiki/tests/phpunit/includes/logging/MergeLogFormatterTest.php index 8b9abe42..1978f1b5 100644 --- a/www/wiki/tests/phpunit/includes/logging/MergeLogFormatterTest.php +++ b/www/wiki/tests/phpunit/includes/logging/MergeLogFormatterTest.php @@ -1,5 +1,8 @@ <?php +/** + * @covers MergeLogFormatter + */ class MergeLogFormatterTest extends LogFormatterTestCase { /** diff --git a/www/wiki/tests/phpunit/includes/logging/MoveLogFormatterTest.php b/www/wiki/tests/phpunit/includes/logging/MoveLogFormatterTest.php index 3433a6a4..ebda46b2 100644 --- a/www/wiki/tests/phpunit/includes/logging/MoveLogFormatterTest.php +++ b/www/wiki/tests/phpunit/includes/logging/MoveLogFormatterTest.php @@ -1,5 +1,8 @@ <?php +/** + * @covers MoveLogFormatter + */ class MoveLogFormatterTest extends LogFormatterTestCase { /** diff --git a/www/wiki/tests/phpunit/includes/logging/NewUsersLogFormatterTest.php b/www/wiki/tests/phpunit/includes/logging/NewUsersLogFormatterTest.php index 333fd88d..eee2981c 100644 --- a/www/wiki/tests/phpunit/includes/logging/NewUsersLogFormatterTest.php +++ b/www/wiki/tests/phpunit/includes/logging/NewUsersLogFormatterTest.php @@ -1,6 +1,7 @@ <?php /** + * @covers NewUsersLogFormatter * @group Database */ class NewUsersLogFormatterTest extends LogFormatterTestCase { @@ -10,11 +11,11 @@ class NewUsersLogFormatterTest extends LogFormatterTestCase { // Register LogHandler, see $wgNewUserLog in Setup.php $this->mergeMwGlobalArrayValue( 'wgLogActionsHandlers', [ - 'newusers/newusers' => 'NewUsersLogFormatter', - 'newusers/create' => 'NewUsersLogFormatter', - 'newusers/create2' => 'NewUsersLogFormatter', - 'newusers/byemail' => 'NewUsersLogFormatter', - 'newusers/autocreate' => 'NewUsersLogFormatter', + 'newusers/newusers' => NewUsersLogFormatter::class, + 'newusers/create' => NewUsersLogFormatter::class, + 'newusers/create2' => NewUsersLogFormatter::class, + 'newusers/byemail' => NewUsersLogFormatter::class, + 'newusers/autocreate' => NewUsersLogFormatter::class, ] ); } diff --git a/www/wiki/tests/phpunit/includes/logging/PageLangLogFormatterTest.php b/www/wiki/tests/phpunit/includes/logging/PageLangLogFormatterTest.php index 2156bdb4..33fd68f6 100644 --- a/www/wiki/tests/phpunit/includes/logging/PageLangLogFormatterTest.php +++ b/www/wiki/tests/phpunit/includes/logging/PageLangLogFormatterTest.php @@ -1,5 +1,8 @@ <?php +/** + * @covers PageLangLogFormatter + */ class PageLangLogFormatterTest extends LogFormatterTestCase { protected function setUp() { @@ -9,7 +12,7 @@ class PageLangLogFormatterTest extends LogFormatterTestCase { $this->setMwGlobals( 'wgHooks', [] ); // Register LogHandler, see $wgPageLanguageUseDB in Setup.php $this->mergeMwGlobalArrayValue( 'wgLogActionsHandlers', [ - 'pagelang/pagelang' => 'PageLangLogFormatter', + 'pagelang/pagelang' => PageLangLogFormatter::class, ] ); } diff --git a/www/wiki/tests/phpunit/includes/logging/PatrolLogFormatterTest.php b/www/wiki/tests/phpunit/includes/logging/PatrolLogFormatterTest.php index b6804543..0d78ed9c 100644 --- a/www/wiki/tests/phpunit/includes/logging/PatrolLogFormatterTest.php +++ b/www/wiki/tests/phpunit/includes/logging/PatrolLogFormatterTest.php @@ -1,5 +1,8 @@ <?php +/** + * @covers PatrolLogFormatter + */ class PatrolLogFormatterTest extends LogFormatterTestCase { /** diff --git a/www/wiki/tests/phpunit/includes/logging/ProtectLogFormatterTest.php b/www/wiki/tests/phpunit/includes/logging/ProtectLogFormatterTest.php index 1fa7fc24..1c076cab 100644 --- a/www/wiki/tests/phpunit/includes/logging/ProtectLogFormatterTest.php +++ b/www/wiki/tests/phpunit/includes/logging/ProtectLogFormatterTest.php @@ -1,5 +1,8 @@ <?php +/** + * @covers ProtectLogFormatter + */ class ProtectLogFormatterTest extends LogFormatterTestCase { /** diff --git a/www/wiki/tests/phpunit/includes/logging/RightsLogFormatterTest.php b/www/wiki/tests/phpunit/includes/logging/RightsLogFormatterTest.php index f48507d8..d081c61b 100644 --- a/www/wiki/tests/phpunit/includes/logging/RightsLogFormatterTest.php +++ b/www/wiki/tests/phpunit/includes/logging/RightsLogFormatterTest.php @@ -1,5 +1,8 @@ <?php +/** + * @covers RightsLogFormatter + */ class RightsLogFormatterTest extends LogFormatterTestCase { /** diff --git a/www/wiki/tests/phpunit/includes/logging/UploadLogFormatterTest.php b/www/wiki/tests/phpunit/includes/logging/UploadLogFormatterTest.php index 00d93d14..2b4067f1 100644 --- a/www/wiki/tests/phpunit/includes/logging/UploadLogFormatterTest.php +++ b/www/wiki/tests/phpunit/includes/logging/UploadLogFormatterTest.php @@ -1,5 +1,8 @@ <?php +/** + * @covers UploadLogFormatter + */ class UploadLogFormatterTest extends LogFormatterTestCase { /** diff --git a/www/wiki/tests/phpunit/includes/mail/MailAddressTest.php b/www/wiki/tests/phpunit/includes/mail/MailAddressTest.php index c837d26f..459f5cc4 100644 --- a/www/wiki/tests/phpunit/includes/mail/MailAddressTest.php +++ b/www/wiki/tests/phpunit/includes/mail/MailAddressTest.php @@ -7,7 +7,7 @@ class MailAddressTest extends MediaWikiTestCase { */ public function testConstructor() { $ma = new MailAddress( 'foo@bar.baz', 'UserName', 'Real name' ); - $this->assertInstanceOf( 'MailAddress', $ma ); + $this->assertInstanceOf( MailAddress::class, $ma ); } /** @@ -17,7 +17,7 @@ class MailAddressTest extends MediaWikiTestCase { if ( wfIsWindows() ) { $this->markTestSkipped( 'This test only works on non-Windows platforms' ); } - $user = $this->createMock( 'User' ); + $user = $this->createMock( User::class ); $user->expects( $this->any() )->method( 'getName' )->will( $this->returnValue( 'UserName' ) ); @@ -29,11 +29,11 @@ class MailAddressTest extends MediaWikiTestCase { ); $ma = MailAddress::newFromUser( $user ); - $this->assertInstanceOf( 'MailAddress', $ma ); + $this->assertInstanceOf( MailAddress::class, $ma ); $this->setMwGlobals( 'wgEnotifUseRealName', true ); - $this->assertEquals( 'Real name <foo@bar.baz>', $ma->toString() ); + $this->assertEquals( '"Real name" <foo@bar.baz>', $ma->toString() ); $this->setMwGlobals( 'wgEnotifUseRealName', false ); - $this->assertEquals( 'UserName <foo@bar.baz>', $ma->toString() ); + $this->assertEquals( '"UserName" <foo@bar.baz>', $ma->toString() ); } /** @@ -51,11 +51,16 @@ class MailAddressTest extends MediaWikiTestCase { public static function provideToString() { return [ - [ true, 'foo@bar.baz', 'FooBar', 'Foo Bar', 'Foo Bar <foo@bar.baz>' ], - [ true, 'foo@bar.baz', 'UserName', null, 'UserName <foo@bar.baz>' ], - [ true, 'foo@bar.baz', 'AUser', 'My real name', 'My real name <foo@bar.baz>' ], + [ true, 'foo@bar.baz', 'FooBar', 'Foo Bar', '"Foo Bar" <foo@bar.baz>' ], + [ true, 'foo@bar.baz', 'UserName', null, '"UserName" <foo@bar.baz>' ], + [ true, 'foo@bar.baz', 'AUser', 'My real name', '"My real name" <foo@bar.baz>' ], + [ true, 'foo@bar.baz', 'AUser', 'My "real" name', '"My \"real\" name" <foo@bar.baz>' ], + [ true, 'foo@bar.baz', 'AUser', 'My "A/B" test', '"My \"A/B\" test" <foo@bar.baz>' ], + [ true, 'foo@bar.baz', 'AUser', 'E=MC2', '=?UTF-8?Q?E=3DMC2?= <foo@bar.baz>' ], + // A backslash (\) should be escaped (\\). In a string literal that is \\\\ (4x). + [ true, 'foo@bar.baz', 'AUser', 'My "B\C" test', '"My \"B\\\\C\" test" <foo@bar.baz>' ], [ true, 'foo@bar.baz', 'A.user.name', 'my@real.name', '"my@real.name" <foo@bar.baz>' ], - [ false, 'foo@bar.baz', 'AUserName', 'Some real name', 'AUserName <foo@bar.baz>' ], + [ false, 'foo@bar.baz', 'AUserName', 'Some real name', '"AUserName" <foo@bar.baz>' ], [ false, 'foo@bar.baz', '', '', 'foo@bar.baz' ], [ true, 'foo@bar.baz', '', '', 'foo@bar.baz' ], [ true, '', '', '', '' ], diff --git a/www/wiki/tests/phpunit/includes/media/BitmapScalingTest.php b/www/wiki/tests/phpunit/includes/media/BitmapScalingTest.php index 92a927f8..fb96f7db 100644 --- a/www/wiki/tests/phpunit/includes/media/BitmapScalingTest.php +++ b/www/wiki/tests/phpunit/includes/media/BitmapScalingTest.php @@ -124,7 +124,7 @@ class BitmapScalingTest extends MediaWikiTestCase { $file = new FakeDimensionFile( [ 4000, 4000 ] ); $handler = new BitmapHandler; $params = [ 'width' => '3700' ]; // Still bigger than max size. - $this->assertEquals( 'TransformTooBigImageAreaError', + $this->assertEquals( TransformTooBigImageAreaError::class, get_class( $handler->doTransform( $file, 'dummy path', '', $params ) ) ); } @@ -136,7 +136,7 @@ class BitmapScalingTest extends MediaWikiTestCase { $file->mustRender = true; $handler = new BitmapHandler; $params = [ 'width' => '5000' ]; // Still bigger than max size. - $this->assertEquals( 'TransformTooBigImageAreaError', + $this->assertEquals( TransformTooBigImageAreaError::class, get_class( $handler->doTransform( $file, 'dummy path', '', $params ) ) ); } diff --git a/www/wiki/tests/phpunit/includes/media/ExifBitmapTest.php b/www/wiki/tests/phpunit/includes/media/ExifBitmapTest.php index 3dd7e4c6..eb02e7ed 100644 --- a/www/wiki/tests/phpunit/includes/media/ExifBitmapTest.php +++ b/www/wiki/tests/phpunit/includes/media/ExifBitmapTest.php @@ -47,9 +47,8 @@ class ExifBitmapTest extends MediaWikiMediaTestCase { * @covers ExifBitmapHandler::isMetadataValid */ public function testGoodMetadata() { - // @codingStandardsIgnoreStart Ignore Generic.Files.LineLength.TooLong + // phpcs:ignore Generic.Files.LineLength $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;}'; - // @codingStandardsIgnoreEnd $res = $this->handler->isMetadataValid( null, $meta ); $this->assertEquals( ExifBitmapHandler::METADATA_GOOD, $res ); } @@ -58,9 +57,8 @@ class ExifBitmapTest extends MediaWikiMediaTestCase { * @covers ExifBitmapHandler::isMetadataValid */ public function testIsOldGood() { - // @codingStandardsIgnoreStart Ignore Generic.Files.LineLength.TooLong + // phpcs:ignore Generic.Files.LineLength $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;}'; - // @codingStandardsIgnoreEnd $res = $this->handler->isMetadataValid( null, $meta ); $this->assertEquals( ExifBitmapHandler::METADATA_COMPATIBLE, $res ); } @@ -70,9 +68,8 @@ class ExifBitmapTest extends MediaWikiMediaTestCase { * @covers ExifBitmapHandler::isMetadataValid */ public function testPagedTiffHandledGracefully() { - // @codingStandardsIgnoreStart Ignore Generic.Files.LineLength.TooLong + // phpcs:ignore Generic.Files.LineLength $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";}'; - // @codingStandardsIgnoreEnd $res = $this->handler->isMetadataValid( null, $meta ); $this->assertEquals( ExifBitmapHandler::METADATA_BAD, $res ); } diff --git a/www/wiki/tests/phpunit/includes/media/ExifRotationTest.php b/www/wiki/tests/phpunit/includes/media/ExifRotationTest.php index 5ae17635..fff101f3 100644 --- a/www/wiki/tests/phpunit/includes/media/ExifRotationTest.php +++ b/www/wiki/tests/phpunit/includes/media/ExifRotationTest.php @@ -5,10 +5,13 @@ * @group Media * @group medium * - * @todo covers tags + * @covers BitmapHandler */ class ExifRotationTest extends MediaWikiMediaTestCase { + /** @var BitmapHandler */ + private $handler; + protected function setUp() { parent::setUp(); $this->checkPHPExtension( 'exif' ); diff --git a/www/wiki/tests/phpunit/includes/media/FakeDimensionFile.php b/www/wiki/tests/phpunit/includes/media/FakeDimensionFile.php index 81e820e0..5e3a3cba 100644 --- a/www/wiki/tests/phpunit/includes/media/FakeDimensionFile.php +++ b/www/wiki/tests/phpunit/includes/media/FakeDimensionFile.php @@ -1,8 +1,5 @@ <?php -/** - * @group Media - */ class FakeDimensionFile extends File { public $mustRender = false; public $mime; diff --git a/www/wiki/tests/phpunit/includes/media/FormatMetadataTest.php b/www/wiki/tests/phpunit/includes/media/FormatMetadataTest.php index e9fc84e7..0987bd0a 100644 --- a/www/wiki/tests/phpunit/includes/media/FormatMetadataTest.php +++ b/www/wiki/tests/phpunit/includes/media/FormatMetadataTest.php @@ -44,7 +44,7 @@ class FormatMetadataTest extends MediaWikiMediaTestCase { */ public function testResolveMultivalueValue( $input, $output ) { $formatMetadata = new FormatMetadata(); - $class = new ReflectionClass( 'FormatMetadata' ); + $class = new ReflectionClass( FormatMetadata::class ); $method = $class->getMethod( 'resolveMultivalueValue' ); $method->setAccessible( true ); $actualInput = $method->invoke( $formatMetadata, $input ); @@ -95,4 +95,50 @@ class FormatMetadataTest extends MediaWikiMediaTestCase { ], ]; } + + /** + * @param mixed $input + * @param mixed $output + * @dataProvider provideGetFormattedData + * @covers FormatMetadata::getFormattedData + */ + public function testGetFormattedData( $input, $output ) { + $this->assertEquals( $output, FormatMetadata::getFormattedData( $input ) ); + } + + public function provideGetFormattedData() { + return [ + [ + [ 'Software' => 'Adobe Photoshop CS6 (Macintosh)' ], + [ 'Software' => 'Adobe Photoshop CS6 (Macintosh)' ], + ], + [ + [ 'Software' => [ 'FotoWare FotoStation' ] ], + [ 'Software' => 'FotoWare FotoStation' ], + ], + [ + [ 'Software' => [ [ 'Capture One PRO', '3.7.7' ] ] ], + [ 'Software' => 'Capture One PRO (Version 3.7.7)' ], + ], + [ + [ 'Software' => [ [ 'FotoWare ColorFactory', '' ] ] ], + [ 'Software' => 'FotoWare ColorFactory (Version )' ], + ], + [ + [ 'Software' => [ 'x-default' => 'paint.net 4.0.12', '_type' => 'lang' ] ], + [ 'Software' => '<ul class="metadata-langlist">'. + '<li class="mw-metadata-lang-default">'. + '<span class="mw-metadata-lang-value">paint.net 4.0.12</span>'. + "</li>\n". + '</ul>' + ], + ], + [ + // https://phabricator.wikimedia.org/T178130 + // WebMHandler.php turns both 'muxingapp' & 'writingapp' to 'Software' + [ 'Software' => [ [ 'Lavf57.25.100' ], [ 'Lavf57.25.100' ] ] ], + [ 'Software' => "<ul><li>Lavf57.25.100</li>\n<li>Lavf57.25.100</li></ul>" ], + ], + ]; + } } diff --git a/www/wiki/tests/phpunit/includes/media/GIFTest.php b/www/wiki/tests/phpunit/includes/media/GIFTest.php index aaa3ac4d..4dd7443e 100644 --- a/www/wiki/tests/phpunit/includes/media/GIFTest.php +++ b/www/wiki/tests/phpunit/includes/media/GIFTest.php @@ -72,18 +72,18 @@ class GIFHandlerTest extends MediaWikiMediaTestCase { } public static function provideIsMetadataValid() { + // phpcs:disable Generic.Files.LineLength return [ [ GIFHandler::BROKEN_FILE, GIFHandler::METADATA_GOOD ], [ '', GIFHandler::METADATA_BAD ], [ null, GIFHandler::METADATA_BAD ], [ 'Something invalid!', GIFHandler::METADATA_BAD ], - // @codingStandardsIgnoreStart Ignore Generic.Files.LineLength.TooLong [ '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 ], - // @codingStandardsIgnoreEnd ]; + // phpcs:enable } /** @@ -99,8 +99,8 @@ class GIFHandlerTest extends MediaWikiMediaTestCase { } public static function provideGetMetadata() { + // phpcs:disable Generic.Files.LineLength return [ - // @codingStandardsIgnoreStart Ignore Generic.Files.LineLength.TooLong [ '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;}}' @@ -109,8 +109,8 @@ class GIFHandlerTest extends MediaWikiMediaTestCase { '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;}}' ], - // @codingStandardsIgnoreEnd ]; + // phpcs:enable } /** @@ -153,6 +153,7 @@ class GIFHandlerTest extends MediaWikiMediaTestCase { * @param string $filename * @param float $expectedLength * @dataProvider provideGetLength + * @covers GIFHandler::getLength */ public function testGetLength( $filename, $expectedLength ) { $file = $this->dataFile( $filename, 'image/gif' ); diff --git a/www/wiki/tests/phpunit/includes/media/IPTCTest.php b/www/wiki/tests/phpunit/includes/media/IPTCTest.php index 826957ec..4b3ba075 100644 --- a/www/wiki/tests/phpunit/includes/media/IPTCTest.php +++ b/www/wiki/tests/phpunit/includes/media/IPTCTest.php @@ -44,13 +44,6 @@ class IPTCTest extends MediaWikiTestCase { * @covers IPTC::parse */ public function testIPTCParseForcedUTFButInvalid() { - if ( version_compare( PHP_VERSION, '5.5.26', '<' ) - || ( version_compare( PHP_VERSION, '5.6.0', '>' ) - && version_compare( PHP_VERSION, '5.6.10', '<' ) - ) - ) { - $this->markTestSkipped( 'Test fails on pre-PHP 5.5.25. See T124574/T39665 for details.' ); - } $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 ); diff --git a/www/wiki/tests/phpunit/includes/media/JpegMetadataExtractorTest.php b/www/wiki/tests/phpunit/includes/media/JpegMetadataExtractorTest.php index 09912541..c943cef9 100644 --- a/www/wiki/tests/phpunit/includes/media/JpegMetadataExtractorTest.php +++ b/www/wiki/tests/phpunit/includes/media/JpegMetadataExtractorTest.php @@ -108,4 +108,21 @@ class JpegMetadataExtractorTest extends MediaWikiTestCase { $expected = 'BE'; $this->assertEquals( $expected, $res['byteOrder'] ); } + + public function testInfiniteRead() { + // test file truncated right after a segment, which previously + // caused an infinite loop looking for the next segment byte. + // Should get past infinite loop and throw in wfUnpack() + $this->setExpectedException( 'MWException' ); + $res = JpegMetadataExtractor::segmentSplitter( $this->filePath . 'jpeg-segment-loop1.jpg' ); + } + + public function testInfiniteRead2() { + // test file truncated after a segment's marker and size, which + // would cause a seek past end of file. Seek past end of file + // doesn't actually fail, but prevents further reading and was + // devolving into the previous case (testInfiniteRead). + $this->setExpectedException( 'MWException' ); + $res = JpegMetadataExtractor::segmentSplitter( $this->filePath . 'jpeg-segment-loop2.jpg' ); + } } diff --git a/www/wiki/tests/phpunit/includes/media/JpegTest.php b/www/wiki/tests/phpunit/includes/media/JpegTest.php index abe02808..13de7ff9 100644 --- a/www/wiki/tests/phpunit/includes/media/JpegTest.php +++ b/www/wiki/tests/phpunit/includes/media/JpegTest.php @@ -24,9 +24,8 @@ class JpegTest extends MediaWikiMediaTestCase { public function testJpegMetadataExtraction() { $file = $this->dataFile( 'test.jpg', 'image/jpeg' ); $res = $this->handler->getMetadata( $file, $this->filePath . 'test.jpg' ); - // @codingStandardsIgnoreStart Ignore Generic.Files.LineLength.TooLong + // phpcs:ignore Generic.Files.LineLength $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;}'; - // @codingStandardsIgnoreEnd // Unserialize in case serialization format ever changes. $this->assertEquals( unserialize( $expected ), unserialize( $res ) ); diff --git a/www/wiki/tests/phpunit/includes/media/MediaWikiMediaTestCase.php b/www/wiki/tests/phpunit/includes/media/MediaWikiMediaTestCase.php index 196f6882..a4e8056a 100644 --- a/www/wiki/tests/phpunit/includes/media/MediaWikiMediaTestCase.php +++ b/www/wiki/tests/phpunit/includes/media/MediaWikiMediaTestCase.php @@ -76,7 +76,7 @@ abstract class MediaWikiMediaTestCase extends MediaWikiTestCase { protected function dataFile( $name, $type = null ) { if ( !$type ) { // Autodetect by file extension for the lazy. - $magic = MimeMagic::singleton(); + $magic = MediaWiki\MediaWikiServices::getInstance()->getMimeAnalyzer(); $parts = explode( $name, '.' ); $type = $magic->guessTypesForExtension( $parts[count( $parts ) - 1] ); } diff --git a/www/wiki/tests/phpunit/includes/media/PNGMetadataExtractorTest.php b/www/wiki/tests/phpunit/includes/media/PNGMetadataExtractorTest.php index a9eaa9e7..22de9357 100644 --- a/www/wiki/tests/phpunit/includes/media/PNGMetadataExtractorTest.php +++ b/www/wiki/tests/phpunit/includes/media/PNGMetadataExtractorTest.php @@ -66,24 +66,6 @@ class PNGMetadataExtractorTest extends MediaWikiTestCase { } /** - * 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() { diff --git a/www/wiki/tests/phpunit/includes/media/PNGTest.php b/www/wiki/tests/phpunit/includes/media/PNGTest.php index 32d54df7..5a66586e 100644 --- a/www/wiki/tests/phpunit/includes/media/PNGTest.php +++ b/www/wiki/tests/phpunit/includes/media/PNGTest.php @@ -73,18 +73,18 @@ class PNGHandlerTest extends MediaWikiMediaTestCase { } public static function provideIsMetadataValid() { + // phpcs:disable Generic.Files.LineLength return [ [ PNGHandler::BROKEN_FILE, PNGHandler::METADATA_GOOD ], [ '', PNGHandler::METADATA_BAD ], [ null, PNGHandler::METADATA_BAD ], [ 'Something invalid!', PNGHandler::METADATA_BAD ], - // @codingStandardsIgnoreStart Ignore Generic.Files.LineLength.TooLong [ '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 ], - // @codingStandardsIgnoreEnd ]; + // phpcs:enable } /** @@ -101,8 +101,8 @@ class PNGHandlerTest extends MediaWikiMediaTestCase { } public static function provideGetMetadata() { + // phpcs:disable Generic.Files.LineLength return [ - // @codingStandardsIgnoreStart Ignore Generic.Files.LineLength.TooLong [ '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;}}' @@ -111,8 +111,8 @@ class PNGHandlerTest extends MediaWikiMediaTestCase { '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;}}' ], - // @codingStandardsIgnoreEnd ]; + // phpcs:enable } /** @@ -142,6 +142,7 @@ class PNGHandlerTest extends MediaWikiMediaTestCase { * @param string $filename * @param float $expectedLength * @dataProvider provideGetLength + * @covers PNGHandler::getLength */ public function testGetLength( $filename, $expectedLength ) { $file = $this->dataFile( $filename, 'image/png' ); diff --git a/www/wiki/tests/phpunit/includes/media/SVGMetadataExtractorTest.php b/www/wiki/tests/phpunit/includes/media/SVGMetadataExtractorTest.php index 9bfd5f61..6fbb4740 100644 --- a/www/wiki/tests/phpunit/includes/media/SVGMetadataExtractorTest.php +++ b/www/wiki/tests/phpunit/includes/media/SVGMetadataExtractorTest.php @@ -128,14 +128,14 @@ class SVGMetadataExtractorTest extends MediaWikiTestCase { public static function provideSvgFilesWithXMLMetadata() { $base = __DIR__ . '/../../data/media'; - // @codingStandardsIgnoreStart Ignore Generic.Files.LineLength.TooLong + // phpcs:disable Generic.Files.LineLength $metadata = '<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"> <ns4:Work xmlns:ns4="http://creativecommons.org/ns#" rdf:about=""> <ns5:format xmlns:ns5="http://purl.org/dc/elements/1.1/">image/svg+xml</ns5:format> <ns5:type xmlns:ns5="http://purl.org/dc/elements/1.1/" rdf:resource="http://purl.org/dc/dcmitype/StillImage"/> </ns4:Work> </rdf:RDF>'; - // @codingStandardsIgnoreEnd + // phpcs:enable $metadata = str_replace( "\r", '', $metadata ); // Windows compat return [ diff --git a/www/wiki/tests/phpunit/includes/media/SVGTest.php b/www/wiki/tests/phpunit/includes/media/SVGTest.php index 4a986b4c..b68dd0ee 100644 --- a/www/wiki/tests/phpunit/includes/media/SVGTest.php +++ b/www/wiki/tests/phpunit/includes/media/SVGTest.php @@ -3,7 +3,12 @@ /** * @group Media */ -class SvgTest extends MediaWikiMediaTestCase { +class SVGTest extends MediaWikiMediaTestCase { + + /** + * @var SvgHandler + */ + private $handler; protected function setUp() { parent::setUp(); @@ -38,4 +43,71 @@ class SvgTest extends MediaWikiMediaTestCase { [ 'Wikimedia-logo.svg', [] ] ]; } + + /** + * @param string $userPreferredLanguage + * @param array $svgLanguages + * @param string $expectedMatch + * @dataProvider providerGetMatchedLanguage + * @covers SvgHandler::getMatchedLanguage + */ + public function testGetMatchedLanguage( $userPreferredLanguage, $svgLanguages, $expectedMatch ) { + $match = $this->handler->getMatchedLanguage( $userPreferredLanguage, $svgLanguages ); + $this->assertEquals( $expectedMatch, $match ); + } + + public function providerGetMatchedLanguage() { + return [ + 'no match' => [ + 'userPreferredLanguage' => 'en', + 'svgLanguages' => [ 'de-DE', 'zh', 'ga', 'fr', 'sr-Latn-ME' ], + 'expectedMatch' => null, + ], + 'no subtags' => [ + 'userPreferredLanguage' => 'en', + 'svgLanguages' => [ 'de', 'zh', 'en', 'fr' ], + 'expectedMatch' => 'en', + ], + 'user no subtags, svg 1 subtag' => [ + 'userPreferredLanguage' => 'en', + 'svgLanguages' => [ 'de-DE', 'en-GB', 'en-US', 'fr' ], + 'expectedMatch' => 'en-GB', + ], + 'user no subtags, svg >1 subtag' => [ + 'userPreferredLanguage' => 'sr', + 'svgLanguages' => [ 'de-DE', 'sr-Cyrl-BA', 'sr-Latn-ME', 'en-US', 'fr' ], + 'expectedMatch' => 'sr-Cyrl-BA', + ], + 'user 1 subtag, svg no subtags' => [ + 'userPreferredLanguage' => 'en-US', + 'svgLanguages' => [ 'de', 'en', 'en', 'fr' ], + 'expectedMatch' => null, + ], + 'user 1 subtag, svg 1 subtag' => [ + 'userPreferredLanguage' => 'en-US', + 'svgLanguages' => [ 'de-DE', 'en-GB', 'en-US', 'fr' ], + 'expectedMatch' => 'en-US', + ], + 'user 1 subtag, svg >1 subtag' => [ + 'userPreferredLanguage' => 'sr-Latn', + 'svgLanguages' => [ 'de-DE', 'sr-Cyrl-BA', 'sr-Latn-ME', 'fr' ], + 'expectedMatch' => 'sr-Latn-ME', + ], + 'user >1 subtag, svg >1 subtag' => [ + 'userPreferredLanguage' => 'sr-Latn-ME', + 'svgLanguages' => [ 'de-DE', 'sr-Cyrl-BA', 'sr-Latn-ME', 'en-US', 'fr' ], + 'expectedMatch' => 'sr-Latn-ME', + ], + 'user >1 subtag, svg <=1 subtag' => [ + 'userPreferredLanguage' => 'sr-Latn-ME', + 'svgLanguages' => [ 'de-DE', 'sr-Cyrl', 'sr-Latn', 'en-US', 'fr' ], + 'expectedMatch' => null, + ], + 'ensure case-insensitive' => [ + 'userPreferredLanguage' => 'sr-latn', + 'svgLanguages' => [ 'de-DE', 'sr-Cyrl', 'sr-Latn-ME', 'en-US', 'fr' ], + 'expectedMatch' => 'sr-Latn-ME', + ], + ]; + } } diff --git a/www/wiki/tests/phpunit/includes/media/TiffTest.php b/www/wiki/tests/phpunit/includes/media/TiffTest.php index d1148202..8a69ec5b 100644 --- a/www/wiki/tests/phpunit/includes/media/TiffTest.php +++ b/www/wiki/tests/phpunit/includes/media/TiffTest.php @@ -34,9 +34,8 @@ class TiffTest extends MediaWikiTestCase { public function testTiffMetadataExtraction() { $res = $this->handler->getMetadata( null, $this->filePath . 'test.tiff' ); - // @codingStandardsIgnoreStart Ignore Generic.Files.LineLength.TooLong + // phpcs:ignore Generic.Files.LineLength $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;}'; - // @codingStandardsIgnoreEnd // Re-unserialize in case there are subtle differences between how versions // of php serialize stuff. diff --git a/www/wiki/tests/phpunit/includes/media/WebPTest.php b/www/wiki/tests/phpunit/includes/media/WebPTest.php index f51693d4..a0a99cc2 100644 --- a/www/wiki/tests/phpunit/includes/media/WebPTest.php +++ b/www/wiki/tests/phpunit/includes/media/WebPTest.php @@ -1,4 +1,8 @@ <?php + +/** + * @covers WebPHandler + */ class WebPHandlerTest extends MediaWikiTestCase { public function setUp() { parent::setUp(); @@ -19,7 +23,7 @@ class WebPHandlerTest extends MediaWikiTestCase { $this->assertEquals( $expectedResult, WebPHandler::extractMetadata( $this->tempFileName ) ); } public function provideTestExtractMetaData() { - // @codingStandardsIgnoreStart Generic.Files.LineLength + // phpcs:disable Generic.Files.LineLength return [ // Files from https://developers.google.com/speed/webp/gallery2 [ "\x52\x49\x46\x46\x90\x68\x01\x00\x57\x45\x42\x50\x56\x50\x38\x4C\x83\x68\x01\x00\x2F\x8F\x01\x4B\x10\x8D\x38\x6C\xDB\x46\x92\xE0\xE0\x82\x7B\x6C", @@ -67,7 +71,7 @@ class WebPHandlerTest extends MediaWikiTestCase { [ 'RIFF1234WEBPVP8 ', false ], [ 'RIFF1234WEBPVP8L ', false ], ]; - // @codingStandardsIgnoreEnd + // phpcs:enable } /** @@ -123,7 +127,7 @@ class WebPHandlerTest extends MediaWikiTestCase { * @dataProvider provideTestGetMimeType */ public function testGuessMimeType( $path ) { - $mime = MimeMagic::singleton(); + $mime = MediaWiki\MediaWikiServices::getInstance()->getMimeAnalyzer(); $this->assertEquals( 'image/webp', $mime->guessMimeType( $path, false ) ); } public function provideTestGetMimeType() { diff --git a/www/wiki/tests/phpunit/includes/media/XMPTest.php b/www/wiki/tests/phpunit/includes/media/XMPTest.php deleted file mode 100644 index bffe415c..00000000 --- a/www/wiki/tests/phpunit/includes/media/XMPTest.php +++ /dev/null @@ -1,223 +0,0 @@ -<?php - -/** - * @group Media - * @covers XMPReader - */ -class XMPTest extends MediaWikiTestCase { - - protected function setUp() { - parent::setUp(); - $this->checkPHPExtension( 'exif' ); # Requires libxml to do XMP parsing - } - - /** - * Put XMP in, compare what comes out... - * - * @param string $xmp The actual xml data. - * @param array $expected Expected result of parsing the xmp. - * @param string $info Short sentence on what's being tested. - * - * @throws Exception - * @dataProvider provideXMPParse - * - * @covers XMPReader::parse - */ - 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 = []; - - // $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 = [ - [ '1', 'parseType=Resource test' ], - [ '2', 'Structure with mixed attribute and element props' ], - [ '3', 'Extra qualifiers (that should be ignored)' ], - [ '3-invalid', 'Test ignoring qualifiers that look like normal props' ], - [ '4', 'Flash as qualifier' ], - [ '5', 'Flash as qualifier 2' ], - [ '6', 'Multiple rdf:Description' ], - [ '7', 'Generic test of several property types' ], - [ 'flash', 'Test of Flash property' ], - [ 'invalid-child-not-struct', 'Test child props not in struct or ignored' ], - [ 'no-recognized-props', 'Test namespace and no recognized props' ], - [ 'no-namespace', 'Test non-namespaced attributes are ignored' ], - [ 'bag-for-seq', "Allow bag's instead of seq's. (bug 27105)" ], - [ 'utf16BE', 'UTF-16BE encoding' ], - [ 'utf16LE', 'UTF-16LE encoding' ], - [ 'utf32BE', 'UTF-32BE encoding' ], - [ 'utf32LE', 'UTF-32LE encoding' ], - [ 'xmpExt', 'Extended XMP missing second part' ], - [ 'gps', 'Handling of exif GPS parameters in XMP' ], - ]; - - $xmpFiles[] = [ 'doctype-included', 'XMP includes doctype' ]; - - 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[] = [ $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. - * - * @covers XMPReader::parseExtended - */ - 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 = [ - 'xmp-exif' => [ - '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. - * - * @covers XMPReader::parseExtended - */ - 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 = [ - 'xmp-exif' => [ - '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. - * - * @covers XMPReader::parseExtended - */ - 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 = [ - 'xmp-exif' => [ - 'DigitalZoomRatio' => '0/10', - 'Flash' => 9, - ] - ]; - - $this->assertEquals( $expected, $actual ); - } - - /** - * Test for multi-section, hostile XML - * @covers XMPReader::checkParseSafety - */ - public function testCheckParseSafety() { - - // Test for detection - $xmpPath = __DIR__ . '/../../data/xmp/'; - $file = fopen( $xmpPath . 'doctype-included.xmp', 'rb' ); - $valid = false; - $reader = new XMPReader(); - do { - $chunk = fread( $file, 10 ); - $valid = $reader->parse( $chunk, feof( $file ) ); - } while ( !feof( $file ) ); - $this->assertFalse( $valid, 'Check that doctype is detected in fragmented XML' ); - $this->assertEquals( - [], - $reader->getResults(), - 'Check that doctype is detected in fragmented XML' - ); - fclose( $file ); - unset( $reader ); - - // Test for false positives - $file = fopen( $xmpPath . 'doctype-not-included.xmp', 'rb' ); - $valid = false; - $reader = new XMPReader(); - do { - $chunk = fread( $file, 10 ); - $valid = $reader->parse( $chunk, feof( $file ) ); - } while ( !feof( $file ) ); - $this->assertTrue( - $valid, - 'Check for false-positive detecting doctype in fragmented XML' - ); - $this->assertEquals( - [ - 'xmp-exif' => [ - 'DigitalZoomRatio' => '0/10', - 'Flash' => '9' - ] - ], - $reader->getResults(), - 'Check that doctype is detected in fragmented XML' - ); - } -} diff --git a/www/wiki/tests/phpunit/includes/media/XMPValidateTest.php b/www/wiki/tests/phpunit/includes/media/XMPValidateTest.php deleted file mode 100644 index 6a006295..00000000 --- a/www/wiki/tests/phpunit/includes/media/XMPValidateTest.php +++ /dev/null @@ -1,53 +0,0 @@ -<?php - -use Psr\Log\NullLogger; - -/** - * @group Media - */ -class XMPValidateTest extends MediaWikiTestCase { - - /** - * @dataProvider provideDates - * @covers XMPValidate::validateDate - */ - public function testValidateDate( $value, $expected ) { - // The method should modify $value. - $validate = new XMPValidate( new NullLogger() ); - $validate->validateDate( [], $value, true ); - $this->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 [ - [ '1992', '1992' ], - [ '1992-04', '1992:04' ], - [ '1992-02-01', '1992:02:01' ], - [ '2011-09-29', '2011:09:29' ], - [ '1982-12-15T20:12', '1982:12:15 20:12' ], - [ '1982-12-15T20:12Z', '1982:12:15 20:12' ], - [ '1982-12-15T20:12+02:30', '1982:12:15 22:42' ], - [ '1982-12-15T01:12-02:30', '1982:12:14 22:42' ], - [ '1982-12-15T20:12:11', '1982:12:15 20:12:11' ], - [ '1982-12-15T20:12:11Z', '1982:12:15 20:12:11' ], - [ '1982-12-15T20:12:11+01:10', '1982:12:15 21:22:11' ], - [ '2045-12-15T20:12:11', '2045:12:15 20:12:11' ], - [ '1867-06-01T15:00:00', '1867:06:01 15:00:00' ], - /* some invalid ones */ - [ '2001--12', null ], - [ '2001-5-12', null ], - [ '2001-5-12TZ', null ], - [ '2001-05-12T15', null ], - [ '2001-12T15:13', null ], - ]; - } -} diff --git a/www/wiki/tests/phpunit/includes/objectcache/MemcachedBagOStuffTest.php b/www/wiki/tests/phpunit/includes/objectcache/MemcachedBagOStuffTest.php index 7814b830..432754b6 100644 --- a/www/wiki/tests/phpunit/includes/objectcache/MemcachedBagOStuffTest.php +++ b/www/wiki/tests/phpunit/includes/objectcache/MemcachedBagOStuffTest.php @@ -42,7 +42,7 @@ class MemcachedBagOStuffTest extends MediaWikiTestCase { ); $this->assertEquals( - 'test:##dc89dcb43b28614da27660240af478b5', + 'test:BagOStuff-long-key:##dc89dcb43b28614da27660240af478b5', $this->cache->makeKey( '𝕖𝕧𝕖𝕟', '𝕚𝕗', '𝕨𝕖', '𝕄𝔻𝟝', '𝕖𝕒𝕔𝕙', '𝕒𝕣𝕘𝕦𝕞𝕖𝕟𝕥', '𝕥𝕙𝕚𝕤', '𝕜𝕖𝕪', '𝕨𝕠𝕦𝕝𝕕', '𝕤𝕥𝕚𝕝𝕝', '𝕓𝕖', '𝕥𝕠𝕠', '𝕝𝕠𝕟𝕘' ) ); @@ -70,6 +70,7 @@ class MemcachedBagOStuffTest extends MediaWikiTestCase { /** * @dataProvider validKeyProvider + * @covers MemcachedBagOStuff::validateKeyEncoding */ public function testValidateKeyEncoding( $key ) { $this->assertSame( $key, $this->cache->validateKeyEncoding( $key ) ); @@ -86,9 +87,10 @@ class MemcachedBagOStuffTest extends MediaWikiTestCase { /** * @dataProvider invalidKeyProvider + * @covers MemcachedBagOStuff::validateKeyEncoding */ public function testValidateKeyEncodingThrowsException( $key ) { - $this->setExpectedException( 'Exception' ); + $this->setExpectedException( Exception::class ); $this->cache->validateKeyEncoding( $key ); } diff --git a/www/wiki/tests/phpunit/includes/objectcache/ObjectCacheTest.php b/www/wiki/tests/phpunit/includes/objectcache/ObjectCacheTest.php index f8ce24ec..43188528 100644 --- a/www/wiki/tests/phpunit/includes/objectcache/ObjectCacheTest.php +++ b/www/wiki/tests/phpunit/includes/objectcache/ObjectCacheTest.php @@ -16,13 +16,13 @@ class ObjectCacheTest extends MediaWikiTestCase { private function setCacheConfig( $arr = [] ) { $defaults = [ - CACHE_NONE => [ 'class' => 'EmptyBagOStuff' ], - CACHE_DB => [ 'class' => 'SqlBagOStuff' ], + CACHE_NONE => [ 'class' => EmptyBagOStuff::class ], + CACHE_DB => [ 'class' => SqlBagOStuff::class ], CACHE_ANYTHING => [ 'factory' => 'ObjectCache::newAnything' ], // Mock ACCEL with 'hash' as being installed. // This makes tests deterministic regardless of APC. - CACHE_ACCEL => [ 'class' => 'HashBagOStuff' ], - 'hash' => [ 'class' => 'HashBagOStuff' ], + CACHE_ACCEL => [ 'class' => HashBagOStuff::class ], + 'hash' => [ 'class' => HashBagOStuff::class ], ]; $this->setMwGlobals( 'wgObjectCaches', $arr + $defaults ); } @@ -70,7 +70,7 @@ class ObjectCacheTest extends MediaWikiTestCase { $this->setCacheConfig( [ // Mock APC not being installed (T160519, T147161) - CACHE_ACCEL => [ 'class' => 'EmptyBagOStuff' ] + CACHE_ACCEL => [ 'class' => EmptyBagOStuff::class ] ] ); $this->assertInstanceOf( @@ -91,7 +91,7 @@ class ObjectCacheTest extends MediaWikiTestCase { $this->setCacheConfig( [ // Mock APC not being installed (T160519, T147161) - CACHE_ACCEL => [ 'class' => 'EmptyBagOStuff' ] + CACHE_ACCEL => [ 'class' => EmptyBagOStuff::class ] ] ); $this->assertInstanceOf( diff --git a/www/wiki/tests/phpunit/includes/objectcache/RESTBagOStuffTest.php b/www/wiki/tests/phpunit/includes/objectcache/RESTBagOStuffTest.php index f722fe13..66754fc9 100644 --- a/www/wiki/tests/phpunit/includes/objectcache/RESTBagOStuffTest.php +++ b/www/wiki/tests/phpunit/includes/objectcache/RESTBagOStuffTest.php @@ -1,6 +1,8 @@ <?php /** * @group BagOStuff + * + * @covers RESTBagOStuff */ class RESTBagOStuffTest extends MediaWikiTestCase { @@ -16,7 +18,7 @@ class RESTBagOStuffTest extends MediaWikiTestCase { public function setUp() { parent::setUp(); $this->client = - $this->getMockBuilder( 'MultiHttpClient' ) + $this->getMockBuilder( MultiHttpClient::class ) ->setConstructorArgs( [ [] ] ) ->setMethods( [ 'run' ] ) ->getMock(); diff --git a/www/wiki/tests/phpunit/includes/objectcache/RedisBagOStuffTest.php b/www/wiki/tests/phpunit/includes/objectcache/RedisBagOStuffTest.php index 34a72cec..df5614d8 100644 --- a/www/wiki/tests/phpunit/includes/objectcache/RedisBagOStuffTest.php +++ b/www/wiki/tests/phpunit/includes/objectcache/RedisBagOStuffTest.php @@ -5,13 +5,16 @@ use Wikimedia\TestingAccessWrapper; /** * @group BagOStuff */ -class RedisBagOStuffTest extends PHPUnit_Framework_TestCase { +class RedisBagOStuffTest extends PHPUnit\Framework\TestCase { + + use MediaWikiCoversValidator; + /** @var RedisBagOStuff */ private $cache; protected function setUp() { parent::setUp(); - $cache = $this->getMockBuilder( 'RedisBagOStuff' ) + $cache = $this->getMockBuilder( RedisBagOStuff::class ) ->disableOriginalConstructor() ->getMock(); $this->cache = TestingAccessWrapper::newFromObject( $cache ); diff --git a/www/wiki/tests/phpunit/includes/page/ArticleTest.php b/www/wiki/tests/phpunit/includes/page/ArticleTest.php index 7d0813d1..df4a2817 100644 --- a/www/wiki/tests/phpunit/includes/page/ArticleTest.php +++ b/www/wiki/tests/phpunit/includes/page/ArticleTest.php @@ -54,26 +54,4 @@ class ArticleTest extends MediaWikiTestCase { $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) - * - * @covers Article::selectFields - * @covers Article::onArticleCreate - * @covers Article::onArticleDelete - * @covers Article::onArticleEdit - */ - public function testStaticFunctions() { - $this->hideDeprecated( 'Article::selectFields' ); - - $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" ); - } } diff --git a/www/wiki/tests/phpunit/includes/page/ImagePage404Test.php b/www/wiki/tests/phpunit/includes/page/ImagePage404Test.php index 48c4392d..4faace21 100644 --- a/www/wiki/tests/phpunit/includes/page/ImagePage404Test.php +++ b/www/wiki/tests/phpunit/includes/page/ImagePage404Test.php @@ -28,6 +28,7 @@ class ImagePage404Test extends MediaWikiMediaTestCase { } /** + * @covers ImagePage::getThumbSizes * @dataProvider providerGetThumbSizes * @param string $filename * @param int $expectedNumberThumbs How many thumbnails to show diff --git a/www/wiki/tests/phpunit/includes/page/ImagePageTest.php b/www/wiki/tests/phpunit/includes/page/ImagePageTest.php index 2b30cfa5..8e49bf98 100644 --- a/www/wiki/tests/phpunit/includes/page/ImagePageTest.php +++ b/www/wiki/tests/phpunit/includes/page/ImagePageTest.php @@ -21,6 +21,7 @@ class ImagePageTest extends MediaWikiMediaTestCase { } /** + * @covers ImagePage::getDisplayWidthHeight * @dataProvider providerGetDisplayWidthHeight * @param array $dim Array [maxWidth, maxHeight, width, height] * @param array $expected Array [width, height] The width and height we expect to display at @@ -65,6 +66,7 @@ class ImagePageTest extends MediaWikiMediaTestCase { } /** + * @covers ImagePage::getThumbSizes * @dataProvider providerGetThumbSizes * @param string $filename * @param int $expectedNumberThumbs How many thumbnails to show diff --git a/www/wiki/tests/phpunit/includes/page/WikiPageContentHandlerDbTest.php b/www/wiki/tests/phpunit/includes/page/WikiPageContentHandlerDbTest.php new file mode 100644 index 00000000..2d7d6cc3 --- /dev/null +++ b/www/wiki/tests/phpunit/includes/page/WikiPageContentHandlerDbTest.php @@ -0,0 +1,42 @@ +<?php + +/** + * @group ContentHandler + * @group Database + * @group medium + */ +class WikiPageContentHandlerDbTest extends WikiPageDbTestBase { + + protected function getContentHandlerUseDB() { + return true; + } + + /** + * @covers WikiPage::getContentModel + */ + public function testGetContentModel() { + $page = $this->createPage( + __METHOD__, + "some text", + CONTENT_MODEL_JAVASCRIPT + ); + + $page = new WikiPage( $page->getTitle() ); + $this->assertEquals( CONTENT_MODEL_JAVASCRIPT, $page->getContentModel() ); + } + + /** + * @covers WikiPage::getContentHandler + */ + public function testGetContentHandler() { + $page = $this->createPage( + __METHOD__, + "some text", + CONTENT_MODEL_JAVASCRIPT + ); + + $page = new WikiPage( $page->getTitle() ); + $this->assertEquals( JavaScriptContentHandler::class, get_class( $page->getContentHandler() ) ); + } + +} diff --git a/www/wiki/tests/phpunit/includes/page/WikiPageDbTestBase.php b/www/wiki/tests/phpunit/includes/page/WikiPageDbTestBase.php new file mode 100644 index 00000000..53b659f2 --- /dev/null +++ b/www/wiki/tests/phpunit/includes/page/WikiPageDbTestBase.php @@ -0,0 +1,1903 @@ +<?php + +abstract class WikiPageDbTestBase extends MediaWikiLangTestCase { + + private $pagesToDelete; + + public function __construct( $name = null, array $data = [], $dataName = '' ) { + parent::__construct( $name, $data, $dataName ); + + $this->tablesUsed = array_merge( + $this->tablesUsed, + [ 'page', + 'revision', + 'redirect', + 'archive', + 'category', + 'ip_changes', + 'text', + + 'recentchanges', + 'logging', + + 'page_props', + 'pagelinks', + 'categorylinks', + 'langlinks', + 'externallinks', + 'imagelinks', + 'templatelinks', + 'iwlinks' ] ); + } + + protected function setUp() { + parent::setUp(); + $this->setMwGlobals( 'wgContentHandlerUseDB', $this->getContentHandlerUseDB() ); + $this->pagesToDelete = []; + } + + protected function tearDown() { + foreach ( $this->pagesToDelete as $p ) { + /* @var $p WikiPage */ + + try { + if ( $p->exists() ) { + $p->doDeleteArticle( "testing done." ); + } + } catch ( MWException $ex ) { + // fail silently + } + } + parent::tearDown(); + } + + abstract protected function getContentHandlerUseDB(); + + /** + * @param Title|string $title + * @param string|null $model + * @return WikiPage + */ + private function newPage( $title, $model = null ) { + if ( is_string( $title ) ) { + $ns = $this->getDefaultWikitextNS(); + $title = Title::newFromText( $title, $ns ); + } + + $p = new WikiPage( $title ); + + $this->pagesToDelete[] = $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 + * @covers WikiPage::doModify + * @covers WikiPage::doCreate + * @covers WikiPage::doEditUpdates + */ + public function testDoEditContent() { + $page = $this->newPage( __METHOD__ ); + $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_REPLICA ); + $res = $dbr->select( 'pagelinks', '*', [ '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_REPLICA ); + $res = $dbr->select( 'pagelinks', '*', [ 'pl_from' => $id ] ); + $n = $res->numRows(); + $res->free(); + + $this->assertEquals( 2, $n, 'pagelinks should contain two links from the page' ); + } + + /** + * @covers WikiPage::doDeleteArticle + * @covers WikiPage::doDeleteArticleReal + */ + public function testDoDeleteArticle() { + $page = $this->createPage( + __METHOD__, + "[[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" + ); + + $t = Title::newFromText( $page->getTitle()->getPrefixedText() ); + $this->assertFalse( + $t->exists(), + "Title::exists should return false after page was deleted" + ); + + // Run the job queue + JobQueueGroup::destroySingletons(); + $jobs = new RunJobs; + $jobs->loadParamsAndArgs( null, [ 'quiet' => true ], null ); + $jobs->execute(); + + # ------------------------ + $dbr = wfGetDB( DB_REPLICA ); + $res = $dbr->select( 'pagelinks', '*', [ '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( + __METHOD__, + "[[original text]] foo", + CONTENT_MODEL_WIKITEXT + ); + $id = $page->getId(); + + // Similar to MovePage logic + wfGetDB( DB_MASTER )->delete( 'page', [ 'page_id' => $id ], __METHOD__ ); + $page->doDeleteUpdates( $id ); + + // Run the job queue + JobQueueGroup::destroySingletons(); + $jobs = new RunJobs; + $jobs->loadParamsAndArgs( null, [ 'quiet' => true ], null ); + $jobs->execute(); + + # ------------------------ + $dbr = wfGetDB( DB_REPLICA ); + $res = $dbr->select( 'pagelinks', '*', [ '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( __METHOD__ ); + + $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( __METHOD__ ); + + $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::exists + */ + public function testExists() { + $page = $this->newPage( __METHOD__ ); + $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 function provideHasViewableContent() { + return [ + [ 'WikiPageTest_testHasViewableContent', false, true ], + [ 'Special:WikiPageTest_testHasViewableContent', false ], + [ 'MediaWiki:WikiPageTest_testHasViewableContent', false ], + [ 'Special:Userlogin', true ], + [ '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 function provideGetRedirectTarget() { + return [ + [ 'WikiPageTest_testGetRedirectTarget_1', CONTENT_MODEL_WIKITEXT, "hello world", null ], + [ + 'WikiPageTest_testGetRedirectTarget_2', + CONTENT_MODEL_WIKITEXT, + "#REDIRECT [[hello world]]", + "Hello world" + ], + ]; + } + + /** + * @dataProvider provideGetRedirectTarget + * @covers WikiPage::getRedirectTarget + */ + public function testGetRedirectTarget( $title, $model, $text, $target ) { + $this->setMwGlobals( [ + 'wgCapitalLinks' => true, + ] ); + + $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::class, 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 function provideIsCountable() { + return [ + + // any + [ 'WikiPageTest_testIsCountable', + CONTENT_MODEL_WIKITEXT, + '', + 'any', + true + ], + [ 'WikiPageTest_testIsCountable', + CONTENT_MODEL_WIKITEXT, + 'Foo', + 'any', + true + ], + + // link + [ 'WikiPageTest_testIsCountable', + CONTENT_MODEL_WIKITEXT, + 'Foo', + 'link', + false + ], + [ 'WikiPageTest_testIsCountable', + CONTENT_MODEL_WIKITEXT, + 'Foo [[bar]]', + 'link', + true + ], + + // redirects + [ 'WikiPageTest_testIsCountable', + CONTENT_MODEL_WIKITEXT, + '#REDIRECT [[bar]]', + 'any', + false + ], + [ 'WikiPageTest_testIsCountable', + CONTENT_MODEL_WIKITEXT, + '#REDIRECT [[bar]]', + 'link', + false + ], + + // not a content namespace + [ 'Talk:WikiPageTest_testIsCountable', + CONTENT_MODEL_WIKITEXT, + 'Foo', + 'any', + false + ], + [ 'Talk:WikiPageTest_testIsCountable', + CONTENT_MODEL_WIKITEXT, + 'Foo [[bar]]', + 'link', + false + ], + + // not a content namespace, different model + [ 'MediaWiki:WikiPageTest_testIsCountable.js', + null, + 'Foo', + 'any', + false + ], + [ '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 ); + + $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 function provideGetParserOutput() { + return [ + [ + CONTENT_MODEL_WIKITEXT, + "hello ''world''\n", + "<div class=\"mw-parser-output\"><p>hello <i>world</i></p></div>" + ], + // @todo more...? + ]; + } + + /** + * @dataProvider provideGetParserOutput + * @covers WikiPage::getParserOutput + */ + public function testGetParserOutput( $model, $text, $expectedHtml ) { + $page = $this->createPage( __METHOD__, $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*(</p>|</div>)!sm', '\1', $text ); # don't let tidy confuse us + + $this->assertEquals( $expectedHtml, $text ); + + return $po; + } + + /** + * @covers WikiPage::getParserOutput + */ + public function testGetParserOutput_nonexisting() { + $page = new WikiPage( Title::newFromText( __METHOD__ ) ); + + $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( __METHOD__, '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." ); + } + + public 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 [ + [ 'Help:WikiPageTest_testReplaceSection', + CONTENT_MODEL_WIKITEXT, + self::$sections, + "0", + "No more", + null, + trim( preg_replace( '/^Intro/sm', 'No more', self::$sections ) ) + ], + [ 'Help:WikiPageTest_testReplaceSection', + CONTENT_MODEL_WIKITEXT, + self::$sections, + "", + "No more", + null, + "No more" + ], + [ 'Help:WikiPageTest_testReplaceSection', + CONTENT_MODEL_WIKITEXT, + self::$sections, + "2", + "== TEST ==\nmore fun", + null, + trim( preg_replace( '/^== test ==.*== foo ==/sm', + "== TEST ==\nmore fun\n\n== foo ==", + self::$sections ) ) + ], + [ 'Help:WikiPageTest_testReplaceSection', + CONTENT_MODEL_WIKITEXT, + self::$sections, + "8", + "No more", + null, + trim( self::$sections ) + ], + [ 'Help:WikiPageTest_testReplaceSection', + CONTENT_MODEL_WIKITEXT, + self::$sections, + "new", + "No more", + "New", + trim( self::$sections ) . "\n\n== New ==\n\nNo more" + ], + ]; + } + + /** + * @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() ) ); + } + + /** + * @dataProvider dataReplaceSection + * @covers WikiPage::replaceSectionAtRev + */ + public function testReplaceSectionAtRev( $title, $model, $text, $section, + $with, $sectionTitle, $expected + ) { + $page = $this->createPage( $title, $text, $model ); + $baseRevId = $page->getLatest(); + + $content = ContentHandler::makeContent( $with, $page->getTitle(), $page->getContentModel() ); + $c = $page->replaceSectionAtRev( $section, $content, $sectionTitle, $baseRevId ); + + $this->assertEquals( $expected, is_null( $c ) ? null : trim( $c->getNativeData() ) ); + } + + /** + * @covers WikiPage::getOldestRevision + */ + public function testGetOldestRevision() { + $page = $this->newPage( __METHOD__ ); + $page->doEditContent( + new WikitextContent( 'one' ), + "first edit", + EDIT_NEW + ); + $rev1 = $page->getRevision(); + + $page = new WikiPage( $page->getTitle() ); + $page->doEditContent( + new WikitextContent( 'two' ), + "second edit", + EDIT_UPDATE + ); + + $page = new WikiPage( $page->getTitle() ); + $page->doEditContent( + new WikitextContent( 'three' ), + "third edit", + EDIT_UPDATE + ); + + // sanity check + $this->assertNotEquals( + $rev1->getId(), + $page->getRevision()->getId(), + '$page->getRevision()->getId()' + ); + + // actual test + $this->assertEquals( + $rev1->getId(), + $page->getOldestRevision()->getId(), + '$page->getOldestRevision()->getId()' + ); + } + + /** + * @covers WikiPage::doRollback + * @covers WikiPage::commitRollback + */ + public function testDoRollback() { + $admin = $this->getTestSysop()->getUser(); + $user1 = $this->getTestUser()->getUser(); + // Use the confirmed group for user2 to make sure the user is different + $user2 = $this->getTestUser( [ 'confirmed' ] )->getUser(); + + $page = $this->newPage( __METHOD__ ); + + // Make some edits + $text = "one"; + $status1 = $page->doEditContent( ContentHandler::makeContent( $text, $page->getTitle() ), + "section one", EDIT_NEW, false, $admin ); + + $text .= "\n\ntwo"; + $status2 = $page->doEditContent( ContentHandler::makeContent( $text, $page->getTitle() ), + "adding section two", 0, false, $user1 ); + + $text .= "\n\nthree"; + $status3 = $page->doEditContent( ContentHandler::makeContent( $text, $page->getTitle() ), + "adding section three", 0, false, $user2 ); + + /** @var Revision $rev1 */ + /** @var Revision $rev2 */ + /** @var Revision $rev3 */ + $rev1 = $status1->getValue()['revision']; + $rev2 = $status2->getValue()['revision']; + $rev3 = $status3->getValue()['revision']; + + /** + * 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 revisions have the correct usernames. + */ + $this->assertEquals( 3, Revision::countByPageId( wfGetDB( DB_REPLICA ), $page->getId() ) ); + $this->assertEquals( $admin->getName(), $rev1->getUserText() ); + $this->assertEquals( $user1->getName(), $rev2->getUserText() ); + $this->assertEquals( $user2->getName(), $rev3->getUserText() ); + + // Now, try the actual rollback + $token = $admin->getEditToken( 'rollback' ); + $rollbackErrors = $page->doRollback( + $user2->getName(), + "testing rollback", + $token, + false, + $resultDetails, + $admin + ); + + if ( $rollbackErrors ) { + $this->fail( + "Rollback failed:\n" . + print_r( $rollbackErrors, true ) . ";\n" . + print_r( $resultDetails, 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() ); + } + + /** + * @covers WikiPage::doRollback + * @covers WikiPage::commitRollback + */ + public function testDoRollback_simple() { + $admin = $this->getTestSysop()->getUser(); + + $text = "one"; + $page = $this->newPage( __METHOD__ ); + $page->doEditContent( + ContentHandler::makeContent( $text, $page->getTitle(), CONTENT_MODEL_WIKITEXT ), + "section one", + EDIT_NEW, + false, + $admin + ); + $rev1 = $page->getRevision(); + + $user1 = $this->getTestUser()->getUser(); + $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 + $token = $admin->getEditToken( 'rollback' ); + $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() ); + } + + /** + * @covers WikiPage::doRollback + * @covers WikiPage::commitRollback + */ + public function testDoRollbackFailureSameContent() { + $admin = $this->getTestSysop()->getUser(); + + $text = "one"; + $page = $this->newPage( __METHOD__ ); + $page->doEditContent( + ContentHandler::makeContent( $text, $page->getTitle(), CONTENT_MODEL_WIKITEXT ), + "section one", + EDIT_NEW, + false, + $admin + ); + $rev1 = $page->getRevision(); + + $user1 = $this->getTestUser( [ 'sysop' ] )->getUser(); + $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, do a the rollback from the same user was doing the edit before + $resultDetails = []; + $token = $user1->getEditToken( 'rollback' ); + $errors = $page->doRollback( + $user1->getName(), + "testing revert same user", + $token, + false, + $resultDetails, + $admin + ); + + $this->assertEquals( [], $errors, "Rollback failed same user" ); + + # now, try the rollback + $resultDetails = []; + $token = $admin->getEditToken( 'rollback' ); + $errors = $page->doRollback( + $user1->getName(), + "testing revert", + $token, + false, + $resultDetails, + $admin + ); + + $this->assertEquals( + [ + [ + 'alreadyrolled', + __METHOD__, + $user1->getName(), + $admin->getName(), + ], + ], + $errors, + "Rollback not failed" + ); + + $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() ); + } + + /** + * Tests tagging for edits that do rollback action + * @covers WikiPage::doRollback + */ + public function testDoRollbackTagging() { + if ( !in_array( 'mw-rollback', ChangeTags::getSoftwareTags() ) ) { + $this->markTestSkipped( 'Rollback tag deactivated, skipped the test.' ); + } + + $admin = new User(); + $admin->setName( 'Administrator' ); + $admin->addToDatabase(); + + $text = 'First line'; + $page = $this->newPage( 'WikiPageTest_testDoRollbackTagging' ); + $page->doEditContent( + ContentHandler::makeContent( $text, $page->getTitle(), CONTENT_MODEL_WIKITEXT ), + 'Added first line', + EDIT_NEW, + false, + $admin + ); + + $secondUser = new User(); + $secondUser->setName( '92.65.217.32' ); + $text .= '\n\nSecond line'; + $page = new WikiPage( $page->getTitle() ); + $page->doEditContent( + ContentHandler::makeContent( $text, $page->getTitle(), CONTENT_MODEL_WIKITEXT ), + 'Adding second line', + 0, + false, + $secondUser + ); + + // Now, try the rollback + $admin->addGroup( 'sysop' ); // Make the test user a sysop + $token = $admin->getEditToken( 'rollback' ); + $errors = $page->doRollback( + $secondUser->getName(), + 'testing rollback', + $token, + false, + $resultDetails, + $admin + ); + + // If doRollback completed without errors + if ( $errors === [] ) { + $tags = $resultDetails[ 'tags' ]; + $this->assertContains( 'mw-rollback', $tags ); + } + } + + public function provideGetAutoDeleteReason() { + return [ + [ + [], + false, + false + ], + + [ + [ + [ "first edit", null ], + ], + "/first edit.*only contributor/", + false + ], + + [ + [ + [ "first edit", null ], + [ "second edit", null ], + ], + "/second edit.*only contributor/", + true + ], + + [ + [ + [ "first edit", "127.0.2.22" ], + [ "second edit", "127.0.3.33" ], + ], + "/second edit/", + true + ], + + [ + [ + [ + "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 + ], + + [ + [ + [ "first edit", "127.0.2.22" ], + [ "", "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 function providePreSaveTransform() { + return [ + [ 'hello this is ~~~', + "hello this is [[Special:Contributions/127.0.0.1|127.0.0.1]]", + ], + [ 'hello \'\'this\'\' is <nowiki>~~~</nowiki>', + 'hello \'\'this\'\' is <nowiki>~~~</nowiki>', + ], + ]; + } + + /** + * @covers WikiPage::factory + */ + public function testWikiPageFactory() { + $title = Title::makeTitle( NS_FILE, 'Someimage.png' ); + $page = WikiPage::factory( $title ); + $this->assertEquals( WikiFilePage::class, get_class( $page ) ); + + $title = Title::makeTitle( NS_CATEGORY, 'SomeCategory' ); + $page = WikiPage::factory( $title ); + $this->assertEquals( WikiCategoryPage::class, get_class( $page ) ); + + $title = Title::makeTitle( NS_MAIN, 'SomePage' ); + $page = WikiPage::factory( $title ); + $this->assertEquals( WikiPage::class, get_class( $page ) ); + } + + /** + * @dataProvider provideCommentMigrationOnDeletion + * + * @param int $writeStage + * @param int $readStage + */ + public function testCommentMigrationOnDeletion( $writeStage, $readStage ) { + $this->setMwGlobals( 'wgCommentTableSchemaMigrationStage', $writeStage ); + $this->overrideMwServices(); + + $dbr = wfGetDB( DB_REPLICA ); + + $page = $this->createPage( + __METHOD__, + "foo", + CONTENT_MODEL_WIKITEXT + ); + $revid = $page->getLatest(); + if ( $writeStage > MIGRATION_OLD ) { + $comment_id = $dbr->selectField( + 'revision_comment_temp', + 'revcomment_comment_id', + [ 'revcomment_rev' => $revid ], + __METHOD__ + ); + } + + $this->setMwGlobals( 'wgCommentTableSchemaMigrationStage', $readStage ); + $this->overrideMwServices(); + + $page->doDeleteArticle( "testing deletion" ); + + if ( $readStage > MIGRATION_OLD ) { + // Didn't leave behind any 'revision_comment_temp' rows + $n = $dbr->selectField( + 'revision_comment_temp', 'COUNT(*)', [ 'revcomment_rev' => $revid ], __METHOD__ + ); + $this->assertEquals( 0, $n, 'no entry in revision_comment_temp after deletion' ); + + // Copied or upgraded the comment_id, as applicable + $ar_comment_id = $dbr->selectField( + 'archive', + 'ar_comment_id', + [ 'ar_rev_id' => $revid ], + __METHOD__ + ); + if ( $writeStage > MIGRATION_OLD ) { + $this->assertSame( $comment_id, $ar_comment_id ); + } else { + $this->assertNotEquals( 0, $ar_comment_id ); + } + } + + // Copied rev_comment, if applicable + if ( $readStage <= MIGRATION_WRITE_BOTH && $writeStage <= MIGRATION_WRITE_BOTH ) { + $ar_comment = $dbr->selectField( + 'archive', + 'ar_comment', + [ 'ar_rev_id' => $revid ], + __METHOD__ + ); + $this->assertSame( 'testing', $ar_comment ); + } + } + + public function provideCommentMigrationOnDeletion() { + return [ + [ MIGRATION_OLD, MIGRATION_OLD ], + [ MIGRATION_OLD, MIGRATION_WRITE_BOTH ], + [ MIGRATION_OLD, MIGRATION_WRITE_NEW ], + [ MIGRATION_WRITE_BOTH, MIGRATION_OLD ], + [ MIGRATION_WRITE_BOTH, MIGRATION_WRITE_BOTH ], + [ MIGRATION_WRITE_BOTH, MIGRATION_WRITE_NEW ], + [ MIGRATION_WRITE_BOTH, MIGRATION_NEW ], + [ MIGRATION_WRITE_NEW, MIGRATION_WRITE_BOTH ], + [ MIGRATION_WRITE_NEW, MIGRATION_WRITE_NEW ], + [ MIGRATION_WRITE_NEW, MIGRATION_NEW ], + [ MIGRATION_NEW, MIGRATION_WRITE_BOTH ], + [ MIGRATION_NEW, MIGRATION_WRITE_NEW ], + [ MIGRATION_NEW, MIGRATION_NEW ], + ]; + } + + /** + * @covers WikiPage::updateCategoryCounts + */ + public function testUpdateCategoryCounts() { + $page = new WikiPage( Title::newFromText( __METHOD__ ) ); + + // Add an initial category + $page->updateCategoryCounts( [ 'A' ], [], 0 ); + + $this->assertEquals( 1, Category::newFromName( 'A' )->getPageCount() ); + $this->assertEquals( 0, Category::newFromName( 'B' )->getPageCount() ); + $this->assertEquals( 0, Category::newFromName( 'C' )->getPageCount() ); + + // Add a new category + $page->updateCategoryCounts( [ 'B' ], [], 0 ); + + $this->assertEquals( 1, Category::newFromName( 'A' )->getPageCount() ); + $this->assertEquals( 1, Category::newFromName( 'B' )->getPageCount() ); + $this->assertEquals( 0, Category::newFromName( 'C' )->getPageCount() ); + + // Add and remove a category + $page->updateCategoryCounts( [ 'C' ], [ 'A' ], 0 ); + + $this->assertEquals( 0, Category::newFromName( 'A' )->getPageCount() ); + $this->assertEquals( 1, Category::newFromName( 'B' )->getPageCount() ); + $this->assertEquals( 1, Category::newFromName( 'C' )->getPageCount() ); + } + + public function provideUpdateRedirectOn() { + yield [ '#REDIRECT [[Foo]]', true, null, true, true, 0 ]; + yield [ '#REDIRECT [[Foo]]', true, 'Foo', true, false, 1 ]; + yield [ 'SomeText', false, null, false, true, 0 ]; + yield [ 'SomeText', false, 'Foo', false, false, 1 ]; + } + + /** + * @dataProvider provideUpdateRedirectOn + * @covers WikiPage::updateRedirectOn + * + * @param string $initialText + * @param bool $initialRedirectState + * @param string|null $redirectTitle + * @param bool|null $lastRevIsRedirect + * @param bool $expectedSuccess + * @param int $expectedRowCount + */ + public function testUpdateRedirectOn( + $initialText, + $initialRedirectState, + $redirectTitle, + $lastRevIsRedirect, + $expectedSuccess, + $expectedRowCount + ) { + static $pageCounter = 0; + $pageCounter++; + + $page = $this->createPage( Title::newFromText( __METHOD__ . $pageCounter ), $initialText ); + $this->assertSame( $initialRedirectState, $page->isRedirect() ); + + $redirectTitle = is_string( $redirectTitle ) + ? Title::newFromText( $redirectTitle ) + : $redirectTitle; + + $success = $page->updateRedirectOn( $this->db, $redirectTitle, $lastRevIsRedirect ); + $this->assertSame( $expectedSuccess, $success, 'Success assertion' ); + /** + * updateRedirectOn explicitly updates the redirect table (and not the page table). + * Most of core checks the page table for redirect status, so we have to be ugly and + * assert a select from the table here. + */ + $this->assertRedirectTableCountForPageId( $page->getId(), $expectedRowCount ); + } + + private function assertRedirectTableCountForPageId( $pageId, $expected ) { + $this->assertSelect( + 'redirect', + 'COUNT(*)', + [ 'rd_from' => $pageId ], + [ [ strval( $expected ) ] ] + ); + } + + /** + * @covers WikiPage::insertRedirectEntry + */ + public function testInsertRedirectEntry_insertsRedirectEntry() { + $page = $this->createPage( Title::newFromText( __METHOD__ ), 'A' ); + $this->assertRedirectTableCountForPageId( $page->getId(), 0 ); + + $targetTitle = Title::newFromText( 'SomeTarget#Frag' ); + $targetTitle->mInterwiki = 'eninter'; + $page->insertRedirectEntry( $targetTitle, null ); + + $this->assertSelect( + 'redirect', + [ 'rd_from', 'rd_namespace', 'rd_title', 'rd_fragment', 'rd_interwiki' ], + [ 'rd_from' => $page->getId() ], + [ [ + strval( $page->getId() ), + strval( $targetTitle->getNamespace() ), + strval( $targetTitle->getDBkey() ), + strval( $targetTitle->getFragment() ), + strval( $targetTitle->getInterwiki() ), + ] ] + ); + } + + /** + * @covers WikiPage::insertRedirectEntry + */ + public function testInsertRedirectEntry_insertsRedirectEntryWithPageLatest() { + $page = $this->createPage( Title::newFromText( __METHOD__ ), 'A' ); + $this->assertRedirectTableCountForPageId( $page->getId(), 0 ); + + $targetTitle = Title::newFromText( 'SomeTarget#Frag' ); + $targetTitle->mInterwiki = 'eninter'; + $page->insertRedirectEntry( $targetTitle, $page->getLatest() ); + + $this->assertSelect( + 'redirect', + [ 'rd_from', 'rd_namespace', 'rd_title', 'rd_fragment', 'rd_interwiki' ], + [ 'rd_from' => $page->getId() ], + [ [ + strval( $page->getId() ), + strval( $targetTitle->getNamespace() ), + strval( $targetTitle->getDBkey() ), + strval( $targetTitle->getFragment() ), + strval( $targetTitle->getInterwiki() ), + ] ] + ); + } + + /** + * @covers WikiPage::insertRedirectEntry + */ + public function testInsertRedirectEntry_doesNotInsertIfPageLatestIncorrect() { + $page = $this->createPage( Title::newFromText( __METHOD__ ), 'A' ); + $this->assertRedirectTableCountForPageId( $page->getId(), 0 ); + + $targetTitle = Title::newFromText( 'SomeTarget#Frag' ); + $targetTitle->mInterwiki = 'eninter'; + $page->insertRedirectEntry( $targetTitle, 215251 ); + + $this->assertRedirectTableCountForPageId( $page->getId(), 0 ); + } + + private function getRow( array $overrides = [] ) { + $row = [ + 'page_id' => '44', + 'page_len' => '76', + 'page_is_redirect' => '1', + 'page_latest' => '99', + 'page_namespace' => '3', + 'page_title' => 'JaJaTitle', + 'page_restrictions' => 'edit=autoconfirmed,sysop:move=sysop', + 'page_touched' => '20120101020202', + 'page_links_updated' => '20140101020202', + ]; + foreach ( $overrides as $key => $value ) { + $row[$key] = $value; + } + return (object)$row; + } + + public function provideNewFromRowSuccess() { + yield 'basic row' => [ + $this->getRow(), + function ( WikiPage $wikiPage, self $test ) { + $test->assertSame( 44, $wikiPage->getId() ); + $test->assertSame( 76, $wikiPage->getTitle()->getLength() ); + $test->assertTrue( $wikiPage->isRedirect() ); + $test->assertSame( 99, $wikiPage->getLatest() ); + $test->assertSame( 3, $wikiPage->getTitle()->getNamespace() ); + $test->assertSame( 'JaJaTitle', $wikiPage->getTitle()->getDBkey() ); + $test->assertSame( + [ + 'edit' => [ 'autoconfirmed', 'sysop' ], + 'move' => [ 'sysop' ], + ], + $wikiPage->getTitle()->getAllRestrictions() + ); + $test->assertSame( '20120101020202', $wikiPage->getTouched() ); + $test->assertSame( '20140101020202', $wikiPage->getLinksTimestamp() ); + } + ]; + yield 'different timestamp formats' => [ + $this->getRow( [ + 'page_touched' => '2012-01-01 02:02:02', + 'page_links_updated' => '2014-01-01 02:02:02', + ] ), + function ( WikiPage $wikiPage, self $test ) { + $test->assertSame( '20120101020202', $wikiPage->getTouched() ); + $test->assertSame( '20140101020202', $wikiPage->getLinksTimestamp() ); + } + ]; + yield 'no restrictions' => [ + $this->getRow( [ + 'page_restrictions' => '', + ] ), + function ( WikiPage $wikiPage, self $test ) { + $test->assertSame( + [ + 'edit' => [], + 'move' => [], + ], + $wikiPage->getTitle()->getAllRestrictions() + ); + } + ]; + yield 'not redirect' => [ + $this->getRow( [ + 'page_is_redirect' => '0', + ] ), + function ( WikiPage $wikiPage, self $test ) { + $test->assertFalse( $wikiPage->isRedirect() ); + } + ]; + } + + /** + * @covers WikiPage::newFromRow + * @covers WikiPage::loadFromRow + * @dataProvider provideNewFromRowSuccess + * + * @param object $row + * @param callable $assertions + */ + public function testNewFromRow( $row, $assertions ) { + $page = WikiPage::newFromRow( $row, 'fromdb' ); + $assertions( $page, $this ); + } + + public function provideTestNewFromId_returnsNullOnBadPageId() { + yield[ 0 ]; + yield[ -11 ]; + } + + /** + * @covers WikiPage::newFromID + * @dataProvider provideTestNewFromId_returnsNullOnBadPageId + */ + public function testNewFromId_returnsNullOnBadPageId( $pageId ) { + $this->assertNull( WikiPage::newFromID( $pageId ) ); + } + + /** + * @covers WikiPage::newFromID + */ + public function testNewFromId_appearsToFetchCorrectRow() { + $createdPage = $this->createPage( __METHOD__, 'Xsfaij09' ); + $fetchedPage = WikiPage::newFromID( $createdPage->getId() ); + $this->assertSame( $createdPage->getId(), $fetchedPage->getId() ); + $this->assertEquals( + $createdPage->getContent()->getNativeData(), + $fetchedPage->getContent()->getNativeData() + ); + } + + /** + * @covers WikiPage::newFromID + */ + public function testNewFromId_returnsNullOnNonExistingId() { + $this->assertNull( WikiPage::newFromID( 2147483647 ) ); + } + + public function provideTestInsertProtectNullRevision() { + // phpcs:disable Generic.Files.LineLength + yield [ + 'goat-message-key', + [ 'edit' => 'sysop' ], + [ 'edit' => '20200101040404' ], + false, + 'Goat Reason', + true, + '(goat-message-key: WikiPageDbTestBase::testInsertProtectNullRevision, UTSysop)(colon-separator)Goat Reason(word-separator)(parentheses: (protect-summary-desc: (restriction-edit), (protect-level-sysop), (protect-expiring: 04:04, 1 (january) 2020, 1 (january) 2020, 04:04)))' + ]; + yield [ + 'goat-key', + [ 'edit' => 'sysop', 'move' => 'something' ], + [ 'edit' => '20200101040404', 'move' => '20210101050505' ], + false, + 'Goat Goat', + true, + '(goat-key: WikiPageDbTestBase::testInsertProtectNullRevision, UTSysop)(colon-separator)Goat Goat(word-separator)(parentheses: (protect-summary-desc: (restriction-edit), (protect-level-sysop), (protect-expiring: 04:04, 1 (january) 2020, 1 (january) 2020, 04:04))(word-separator)(protect-summary-desc: (restriction-move), (protect-level-something), (protect-expiring: 05:05, 1 (january) 2021, 1 (january) 2021, 05:05)))' + ]; + // phpcs:enable + } + + /** + * @dataProvider provideTestInsertProtectNullRevision + * @covers WikiPage::insertProtectNullRevision + * @covers WikiPage::protectDescription + * + * @param string $revCommentMsg + * @param array $limit + * @param array $expiry + * @param bool $cascade + * @param string $reason + * @param bool|null $user true if the test sysop should be used, or null + * @param string $expectedComment + */ + public function testInsertProtectNullRevision( + $revCommentMsg, + array $limit, + array $expiry, + $cascade, + $reason, + $user, + $expectedComment + ) { + $this->setContentLang( 'qqx' ); + + $page = $this->createPage( __METHOD__, 'Goat' ); + + $user = $user === null ? $user : $this->getTestSysop()->getUser(); + + $result = $page->insertProtectNullRevision( + $revCommentMsg, + $limit, + $expiry, + $cascade, + $reason, + $user + ); + + $this->assertTrue( $result instanceof Revision ); + $this->assertSame( $expectedComment, $result->getComment( Revision::RAW ) ); + } + + /** + * @covers WikiPage::updateRevisionOn + */ + public function testUpdateRevisionOn_existingPage() { + $user = $this->getTestSysop()->getUser(); + $page = $this->createPage( __METHOD__, 'StartText' ); + + $revision = new Revision( + [ + 'id' => 9989, + 'page' => $page->getId(), + 'title' => $page->getTitle(), + 'comment' => __METHOD__, + 'minor_edit' => true, + 'text' => __METHOD__ . '-text', + 'len' => strlen( __METHOD__ . '-text' ), + 'user' => $user->getId(), + 'user_text' => $user->getName(), + 'timestamp' => '20170707040404', + 'content_model' => CONTENT_MODEL_WIKITEXT, + 'content_format' => CONTENT_FORMAT_WIKITEXT, + ] + ); + + $result = $page->updateRevisionOn( $this->db, $revision ); + $this->assertTrue( $result ); + $this->assertSame( 9989, $page->getLatest() ); + $this->assertEquals( $revision, $page->getRevision() ); + } + + /** + * @covers WikiPage::updateRevisionOn + */ + public function testUpdateRevisionOn_NonExistingPage() { + $user = $this->getTestSysop()->getUser(); + $page = $this->createPage( __METHOD__, 'StartText' ); + $page->doDeleteArticle( 'reason' ); + + $revision = new Revision( + [ + 'id' => 9989, + 'page' => $page->getId(), + 'title' => $page->getTitle(), + 'comment' => __METHOD__, + 'minor_edit' => true, + 'text' => __METHOD__ . '-text', + 'len' => strlen( __METHOD__ . '-text' ), + 'user' => $user->getId(), + 'user_text' => $user->getName(), + 'timestamp' => '20170707040404', + 'content_model' => CONTENT_MODEL_WIKITEXT, + 'content_format' => CONTENT_FORMAT_WIKITEXT, + ] + ); + + $result = $page->updateRevisionOn( $this->db, $revision ); + $this->assertFalse( $result ); + } + + /** + * @covers WikiPage::updateIfNewerOn + */ + public function testUpdateIfNewerOn_olderRevision() { + $user = $this->getTestSysop()->getUser(); + $page = $this->createPage( __METHOD__, 'StartText' ); + $initialRevision = $page->getRevision(); + + $olderTimeStamp = wfTimestamp( + TS_MW, + wfTimestamp( TS_UNIX, $initialRevision->getTimestamp() ) - 1 + ); + + $olderRevison = new Revision( + [ + 'id' => 9989, + 'page' => $page->getId(), + 'title' => $page->getTitle(), + 'comment' => __METHOD__, + 'minor_edit' => true, + 'text' => __METHOD__ . '-text', + 'len' => strlen( __METHOD__ . '-text' ), + 'user' => $user->getId(), + 'user_text' => $user->getName(), + 'timestamp' => $olderTimeStamp, + 'content_model' => CONTENT_MODEL_WIKITEXT, + 'content_format' => CONTENT_FORMAT_WIKITEXT, + ] + ); + + $result = $page->updateIfNewerOn( $this->db, $olderRevison ); + $this->assertFalse( $result ); + } + + /** + * @covers WikiPage::updateIfNewerOn + */ + public function testUpdateIfNewerOn_newerRevision() { + $user = $this->getTestSysop()->getUser(); + $page = $this->createPage( __METHOD__, 'StartText' ); + $initialRevision = $page->getRevision(); + + $newerTimeStamp = wfTimestamp( + TS_MW, + wfTimestamp( TS_UNIX, $initialRevision->getTimestamp() ) + 1 + ); + + $newerRevision = new Revision( + [ + 'id' => 9989, + 'page' => $page->getId(), + 'title' => $page->getTitle(), + 'comment' => __METHOD__, + 'minor_edit' => true, + 'text' => __METHOD__ . '-text', + 'len' => strlen( __METHOD__ . '-text' ), + 'user' => $user->getId(), + 'user_text' => $user->getName(), + 'timestamp' => $newerTimeStamp, + 'content_model' => CONTENT_MODEL_WIKITEXT, + 'content_format' => CONTENT_FORMAT_WIKITEXT, + ] + ); + $result = $page->updateIfNewerOn( $this->db, $newerRevision ); + $this->assertTrue( $result ); + } + + /** + * @covers WikiPage::insertOn + */ + public function testInsertOn() { + $title = Title::newFromText( __METHOD__ ); + $page = new WikiPage( $title ); + + $startTimeStamp = wfTimestampNow(); + $result = $page->insertOn( $this->db ); + $endTimeStamp = wfTimestampNow(); + + $this->assertInternalType( 'int', $result ); + $this->assertTrue( $result > 0 ); + + $condition = [ 'page_id' => $result ]; + + // Check the default fields have been filled + $this->assertSelect( + 'page', + [ + 'page_namespace', + 'page_title', + 'page_restrictions', + 'page_is_redirect', + 'page_is_new', + 'page_latest', + 'page_len', + ], + $condition, + [ [ + '0', + __METHOD__, + '', + '0', + '1', + '0', + '0', + ] ] + ); + + // Check the page_random field has been filled + $pageRandom = $this->db->selectField( 'page', 'page_random', $condition ); + $this->assertTrue( (float)$pageRandom < 1 && (float)$pageRandom > 0 ); + + // Assert the touched timestamp in the DB is roughly when we inserted the page + $pageTouched = $this->db->selectField( 'page', 'page_touched', $condition ); + $this->assertTrue( + wfTimestamp( TS_UNIX, $startTimeStamp ) + <= wfTimestamp( TS_UNIX, $pageTouched ) + ); + $this->assertTrue( + wfTimestamp( TS_UNIX, $endTimeStamp ) + >= wfTimestamp( TS_UNIX, $pageTouched ) + ); + + // Try inserting the same page again and checking the result is false (no change) + $result = $page->insertOn( $this->db ); + $this->assertFalse( $result ); + } + + /** + * @covers WikiPage::insertOn + */ + public function testInsertOn_idSpecified() { + $title = Title::newFromText( __METHOD__ ); + $page = new WikiPage( $title ); + $id = 1478952189; + + $result = $page->insertOn( $this->db, $id ); + + $this->assertSame( $id, $result ); + + $condition = [ 'page_id' => $result ]; + + // Check there is actually a row in the db + $this->assertSelect( + 'page', + [ 'page_title' ], + $condition, + [ [ __METHOD__ ] ] + ); + } + + public function provideTestDoUpdateRestrictions_setBasicRestrictions() { + // Note: Once the current dates passes the date in these tests they will fail. + yield 'move something' => [ + true, + [ 'move' => 'something' ], + [], + [ 'edit' => [], 'move' => [ 'something' ] ], + [], + ]; + yield 'move something, edit blank' => [ + true, + [ 'move' => 'something', 'edit' => '' ], + [], + [ 'edit' => [], 'move' => [ 'something' ] ], + [], + ]; + yield 'edit sysop, with expiry' => [ + true, + [ 'edit' => 'sysop' ], + [ 'edit' => '21330101020202' ], + [ 'edit' => [ 'sysop' ], 'move' => [] ], + [ 'edit' => '21330101020202' ], + ]; + yield 'move and edit, move with expiry' => [ + true, + [ 'move' => 'something', 'edit' => 'another' ], + [ 'move' => '22220202010101' ], + [ 'edit' => [ 'another' ], 'move' => [ 'something' ] ], + [ 'move' => '22220202010101' ], + ]; + yield 'move and edit, edit with infinity expiry' => [ + true, + [ 'move' => 'something', 'edit' => 'another' ], + [ 'edit' => 'infinity' ], + [ 'edit' => [ 'another' ], 'move' => [ 'something' ] ], + [ 'edit' => 'infinity' ], + ]; + yield 'non existing, create something' => [ + false, + [ 'create' => 'something' ], + [], + [ 'create' => [ 'something' ] ], + [], + ]; + yield 'non existing, create something with expiry' => [ + false, + [ 'create' => 'something' ], + [ 'create' => '23451212112233' ], + [ 'create' => [ 'something' ] ], + [ 'create' => '23451212112233' ], + ]; + } + + /** + * @dataProvider provideTestDoUpdateRestrictions_setBasicRestrictions + * @covers WikiPage::doUpdateRestrictions + */ + public function testDoUpdateRestrictions_setBasicRestrictions( + $pageExists, + array $limit, + array $expiry, + array $expectedRestrictions, + array $expectedRestrictionExpiries + ) { + if ( $pageExists ) { + $page = $this->createPage( __METHOD__, 'ABC' ); + } else { + $page = new WikiPage( Title::newFromText( __METHOD__ . '-nonexist' ) ); + } + $user = $this->getTestSysop()->getUser(); + $cascade = false; + + $status = $page->doUpdateRestrictions( $limit, $expiry, $cascade, 'aReason', $user, [] ); + + $logId = $status->getValue(); + $allRestrictions = $page->getTitle()->getAllRestrictions(); + + $this->assertTrue( $status->isGood() ); + $this->assertInternalType( 'int', $logId ); + $this->assertSame( $expectedRestrictions, $allRestrictions ); + foreach ( $expectedRestrictionExpiries as $key => $value ) { + $this->assertSame( $value, $page->getTitle()->getRestrictionExpiry( $key ) ); + } + + // Make sure the log entry looks good + // log_params is not checked here + $actorQuery = ActorMigration::newMigration()->getJoin( 'log_user' ); + $this->assertSelect( + [ 'logging' ] + $actorQuery['tables'], + [ + 'log_comment', + 'log_user' => $actorQuery['fields']['log_user'], + 'log_user_text' => $actorQuery['fields']['log_user_text'], + 'log_namespace', + 'log_title', + ], + [ 'log_id' => $logId ], + [ [ + 'aReason', + (string)$user->getId(), + $user->getName(), + (string)$page->getTitle()->getNamespace(), + $page->getTitle()->getDBkey(), + ] ], + [], + $actorQuery['joins'] + ); + } + + /** + * @covers WikiPage::doUpdateRestrictions + */ + public function testDoUpdateRestrictions_failsOnReadOnly() { + $page = $this->createPage( __METHOD__, 'ABC' ); + $user = $this->getTestSysop()->getUser(); + $cascade = false; + + // Set read only + $readOnly = $this->getMockBuilder( ReadOnlyMode::class ) + ->disableOriginalConstructor() + ->setMethods( [ 'isReadOnly', 'getReason' ] ) + ->getMock(); + $readOnly->expects( $this->once() ) + ->method( 'isReadOnly' ) + ->will( $this->returnValue( true ) ); + $readOnly->expects( $this->once() ) + ->method( 'getReason' ) + ->will( $this->returnValue( 'Some Read Only Reason' ) ); + $this->setService( 'ReadOnlyMode', $readOnly ); + + $status = $page->doUpdateRestrictions( [], [], $cascade, 'aReason', $user, [] ); + $this->assertFalse( $status->isOK() ); + $this->assertSame( 'readonlytext', $status->getMessage()->getKey() ); + } + + /** + * @covers WikiPage::doUpdateRestrictions + */ + public function testDoUpdateRestrictions_returnsGoodIfNothingChanged() { + $page = $this->createPage( __METHOD__, 'ABC' ); + $user = $this->getTestSysop()->getUser(); + $cascade = false; + $limit = [ 'edit' => 'sysop' ]; + + $status = $page->doUpdateRestrictions( + $limit, + [], + $cascade, + 'aReason', + $user, + [] + ); + + // The first entry should have a logId as it did something + $this->assertTrue( $status->isGood() ); + $this->assertInternalType( 'int', $status->getValue() ); + + $status = $page->doUpdateRestrictions( + $limit, + [], + $cascade, + 'aReason', + $user, + [] + ); + + // The second entry should not have a logId as nothing changed + $this->assertTrue( $status->isGood() ); + $this->assertNull( $status->getValue() ); + } + + /** + * @covers WikiPage::doUpdateRestrictions + */ + public function testDoUpdateRestrictions_logEntryTypeAndAction() { + $page = $this->createPage( __METHOD__, 'ABC' ); + $user = $this->getTestSysop()->getUser(); + $cascade = false; + + // Protect the page + $status = $page->doUpdateRestrictions( + [ 'edit' => 'sysop' ], + [], + $cascade, + 'aReason', + $user, + [] + ); + $this->assertTrue( $status->isGood() ); + $this->assertInternalType( 'int', $status->getValue() ); + $this->assertSelect( + 'logging', + [ 'log_type', 'log_action' ], + [ 'log_id' => $status->getValue() ], + [ [ 'protect', 'protect' ] ] + ); + + // Modify the protection + $status = $page->doUpdateRestrictions( + [ 'edit' => 'somethingElse' ], + [], + $cascade, + 'aReason', + $user, + [] + ); + $this->assertTrue( $status->isGood() ); + $this->assertInternalType( 'int', $status->getValue() ); + $this->assertSelect( + 'logging', + [ 'log_type', 'log_action' ], + [ 'log_id' => $status->getValue() ], + [ [ 'protect', 'modify' ] ] + ); + + // Remove the protection + $status = $page->doUpdateRestrictions( + [], + [], + $cascade, + 'aReason', + $user, + [] + ); + $this->assertTrue( $status->isGood() ); + $this->assertInternalType( 'int', $status->getValue() ); + $this->assertSelect( + 'logging', + [ 'log_type', 'log_action' ], + [ 'log_id' => $status->getValue() ], + [ [ 'protect', 'unprotect' ] ] + ); + } + +} diff --git a/www/wiki/tests/phpunit/includes/page/WikiPageNoContentHandlerDbTest.php b/www/wiki/tests/phpunit/includes/page/WikiPageNoContentHandlerDbTest.php new file mode 100644 index 00000000..a6ce185a --- /dev/null +++ b/www/wiki/tests/phpunit/includes/page/WikiPageNoContentHandlerDbTest.php @@ -0,0 +1,14 @@ +<?php + +/** + * @group ContentHandler + * @group Database + * @group medium + */ +class WikiPageNoContentHandlerDbTest extends WikiPageDbTestBase { + + protected function getContentHandlerUseDB() { + return false; + } + +} diff --git a/www/wiki/tests/phpunit/includes/page/WikiPageTest.php b/www/wiki/tests/phpunit/includes/page/WikiPageTest.php deleted file mode 100644 index 386f142d..00000000 --- a/www/wiki/tests/phpunit/includes/page/WikiPageTest.php +++ /dev/null @@ -1,1208 +0,0 @@ -<?php - -/** - * @group ContentHandler - * @group Database - * ^--- important, causes temporary tables to be used instead of the real database - * @group medium - */ -class WikiPageTest extends MediaWikiLangTestCase { - - protected $pages_to_delete; - - function __construct( $name = null, array $data = [], $dataName = '' ) { - parent::__construct( $name, $data, $dataName ); - - $this->tablesUsed = array_merge( - $this->tablesUsed, - [ 'page', - 'revision', - 'archive', - 'ip_changes', - 'text', - - 'recentchanges', - 'logging', - - 'page_props', - 'pagelinks', - 'categorylinks', - 'langlinks', - 'externallinks', - 'imagelinks', - 'templatelinks', - 'iwlinks' ] ); - } - - protected function setUp() { - parent::setUp(); - $this->pages_to_delete = []; - - 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|string $title - * @param string|null $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 - * @covers WikiPage::doModify - * @covers WikiPage::doCreate - * @covers WikiPage::doEditUpdates - */ - 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_REPLICA ); - $res = $dbr->select( 'pagelinks', '*', [ '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_REPLICA ); - $res = $dbr->select( 'pagelinks', '*', [ 'pl_from' => $id ] ); - $n = $res->numRows(); - $res->free(); - - $this->assertEquals( 2, $n, 'pagelinks should contain two links from the page' ); - } - - /** - * @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" - ); - - $t = Title::newFromText( $page->getTitle()->getPrefixedText() ); - $this->assertFalse( - $t->exists(), - "Title::exists should return false after page was deleted" - ); - - // Run the job queue - JobQueueGroup::destroySingletons(); - $jobs = new RunJobs; - $jobs->loadParamsAndArgs( null, [ 'quiet' => true ], null ); - $jobs->execute(); - - # ------------------------ - $dbr = wfGetDB( DB_REPLICA ); - $res = $dbr->select( 'pagelinks', '*', [ '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(); - - // Similar to MovePage logic - wfGetDB( DB_MASTER )->delete( 'page', [ 'page_id' => $id ], __METHOD__ ); - $page->doDeleteUpdates( $id ); - - // Run the job queue - JobQueueGroup::destroySingletons(); - $jobs = new RunJobs; - $jobs->loadParamsAndArgs( null, [ 'quiet' => true ], null ); - $jobs->execute(); - - # ------------------------ - $dbr = wfGetDB( DB_REPLICA ); - $res = $dbr->select( 'pagelinks', '*', [ '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::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 [ - [ 'WikiPageTest_testHasViewableContent', false, true ], - [ 'Special:WikiPageTest_testHasViewableContent', false ], - [ 'MediaWiki:WikiPageTest_testHasViewableContent', false ], - [ 'Special:Userlogin', true ], - [ '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 [ - [ 'WikiPageTest_testGetRedirectTarget_1', CONTENT_MODEL_WIKITEXT, "hello world", null ], - [ - 'WikiPageTest_testGetRedirectTarget_2', - CONTENT_MODEL_WIKITEXT, - "#REDIRECT [[hello world]]", - "Hello world" - ], - ]; - } - - /** - * @dataProvider provideGetRedirectTarget - * @covers WikiPage::getRedirectTarget - */ - public function testGetRedirectTarget( $title, $model, $text, $target ) { - $this->setMwGlobals( [ - 'wgCapitalLinks' => true, - ] ); - - $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 [ - - // any - [ 'WikiPageTest_testIsCountable', - CONTENT_MODEL_WIKITEXT, - '', - 'any', - true - ], - [ 'WikiPageTest_testIsCountable', - CONTENT_MODEL_WIKITEXT, - 'Foo', - 'any', - true - ], - - // comma - [ 'WikiPageTest_testIsCountable', - CONTENT_MODEL_WIKITEXT, - 'Foo', - 'comma', - false - ], - [ 'WikiPageTest_testIsCountable', - CONTENT_MODEL_WIKITEXT, - 'Foo, bar', - 'comma', - true - ], - - // link - [ 'WikiPageTest_testIsCountable', - CONTENT_MODEL_WIKITEXT, - 'Foo', - 'link', - false - ], - [ 'WikiPageTest_testIsCountable', - CONTENT_MODEL_WIKITEXT, - 'Foo [[bar]]', - 'link', - true - ], - - // redirects - [ 'WikiPageTest_testIsCountable', - CONTENT_MODEL_WIKITEXT, - '#REDIRECT [[bar]]', - 'any', - false - ], - [ 'WikiPageTest_testIsCountable', - CONTENT_MODEL_WIKITEXT, - '#REDIRECT [[bar]]', - 'comma', - false - ], - [ 'WikiPageTest_testIsCountable', - CONTENT_MODEL_WIKITEXT, - '#REDIRECT [[bar]]', - 'link', - false - ], - - // not a content namespace - [ 'Talk:WikiPageTest_testIsCountable', - CONTENT_MODEL_WIKITEXT, - 'Foo', - 'any', - false - ], - [ 'Talk:WikiPageTest_testIsCountable', - CONTENT_MODEL_WIKITEXT, - 'Foo, bar', - 'comma', - false - ], - [ 'Talk:WikiPageTest_testIsCountable', - CONTENT_MODEL_WIKITEXT, - 'Foo [[bar]]', - 'link', - false - ], - - // not a content namespace, different model - [ 'MediaWiki:WikiPageTest_testIsCountable.js', - null, - 'Foo', - 'any', - false - ], - [ 'MediaWiki:WikiPageTest_testIsCountable.js', - null, - 'Foo, bar', - 'comma', - false - ], - [ '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 ); - - $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 [ - [ - CONTENT_MODEL_WIKITEXT, - "hello ''world''\n", - "<div class=\"mw-parser-output\"><p>hello <i>world</i></p></div>" - ], - // @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*(</p>|</div>)!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." ); - } - - public 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 [ - [ 'Help:WikiPageTest_testReplaceSection', - CONTENT_MODEL_WIKITEXT, - self::$sections, - "0", - "No more", - null, - trim( preg_replace( '/^Intro/sm', 'No more', self::$sections ) ) - ], - [ 'Help:WikiPageTest_testReplaceSection', - CONTENT_MODEL_WIKITEXT, - self::$sections, - "", - "No more", - null, - "No more" - ], - [ 'Help:WikiPageTest_testReplaceSection', - CONTENT_MODEL_WIKITEXT, - self::$sections, - "2", - "== TEST ==\nmore fun", - null, - trim( preg_replace( '/^== test ==.*== foo ==/sm', - "== TEST ==\nmore fun\n\n== foo ==", - self::$sections ) ) - ], - [ 'Help:WikiPageTest_testReplaceSection', - CONTENT_MODEL_WIKITEXT, - self::$sections, - "8", - "No more", - null, - trim( self::$sections ) - ], - [ 'Help:WikiPageTest_testReplaceSection', - CONTENT_MODEL_WIKITEXT, - self::$sections, - "new", - "No more", - "New", - trim( self::$sections ) . "\n\n== New ==\n\nNo more" - ], - ]; - } - - /** - * @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() ) ); - } - - /** - * @dataProvider dataReplaceSection - * @covers WikiPage::replaceSectionAtRev - */ - public function testReplaceSectionAtRev( $title, $model, $text, $section, - $with, $sectionTitle, $expected - ) { - $page = $this->createPage( $title, $text, $model ); - $baseRevId = $page->getLatest(); - - $content = ContentHandler::makeContent( $with, $page->getTitle(), $page->getContentModel() ); - $c = $page->replaceSectionAtRev( $section, $content, $sectionTitle, $baseRevId ); - - $this->assertEquals( $expected, is_null( $c ) ? null : trim( $c->getNativeData() ) ); - } - - /* @todo FIXME: fix this! - public function testGetUndoText() { - $this->markTestSkippedIfNoDiff3(); - - $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 ); - } - */ - - /** - * @covers WikiPage::getOldestRevision - */ - public function testGetOldestRevision() { - $page = $this->newPage( "WikiPageTest_testGetOldestRevision" ); - $page->doEditContent( - new WikitextContent( 'one' ), - "first edit", - EDIT_NEW - ); - $rev1 = $page->getRevision(); - - $page = new WikiPage( $page->getTitle() ); - $page->doEditContent( - new WikitextContent( 'two' ), - "second edit", - EDIT_UPDATE - ); - - $page = new WikiPage( $page->getTitle() ); - $page->doEditContent( - new WikitextContent( 'three' ), - "third edit", - EDIT_UPDATE - ); - - // sanity check - $this->assertNotEquals( - $rev1->getId(), - $page->getRevision()->getId(), - '$page->getRevision()->getId()' - ); - - // actual test - $this->assertEquals( - $rev1->getId(), - $page->getOldestRevision()->getId(), - '$page->getOldestRevision()->getId()' - ); - } - - /** - * @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_REPLICA ); - $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->addToDatabase(); - $admin->addGroup( "sysop" ); # XXX: make the test user a sysop... - $token = $admin->getEditToken( - [ $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" ); - $admin->addToDatabase(); - - $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( 'rollback' ); - $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() ); - } - - /** - * @covers WikiPage::doRollback - */ - public function testDoRollbackFailureSameContent() { - $admin = new User(); - $admin->setName( "Admin" ); - $admin->addToDatabase(); - $admin->addGroup( "sysop" ); # XXX: make the test user a sysop... - - $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" ); - $user1->addToDatabase(); - $user1->addGroup( "sysop" ); # XXX: make the test user a sysop... - $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, do a the rollback from the same user was doing the edit before - $resultDetails = []; - $token = $user1->getEditToken( 'rollback' ); - $errors = $page->doRollback( - $user1->getName(), - "testing revert same user", - $token, - false, - $resultDetails, - $admin - ); - - $this->assertEquals( [], $errors, "Rollback failed same user" ); - - # now, try the rollback - $resultDetails = []; - $token = $admin->getEditToken( 'rollback' ); - $errors = $page->doRollback( - $user1->getName(), - "testing revert", - $token, - false, - $resultDetails, - $admin - ); - - $this->assertEquals( [ [ 'alreadyrolled', 'WikiPageTest testDoRollback', - '127.0.1.11', 'Admin' ] ], $errors, "Rollback not failed" ); - - $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 provideGetAutoDeleteReason() { - return [ - [ - [], - false, - false - ], - - [ - [ - [ "first edit", null ], - ], - "/first edit.*only contributor/", - false - ], - - [ - [ - [ "first edit", null ], - [ "second edit", null ], - ], - "/second edit.*only contributor/", - true - ], - - [ - [ - [ "first edit", "127.0.2.22" ], - [ "second edit", "127.0.3.33" ], - ], - "/second edit/", - true - ], - - [ - [ - [ - "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 - ], - - [ - [ - [ "first edit", "127.0.2.22" ], - [ "", "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 [ - [ 'hello this is ~~~', - "hello this is [[Special:Contributions/127.0.0.1|127.0.0.1]]", - ], - [ 'hello \'\'this\'\' is <nowiki>~~~</nowiki>', - 'hello \'\'this\'\' is <nowiki>~~~</nowiki>', - ], - ]; - } - - /** - * @covers WikiPage::factory - */ - 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 ) ); - } - - /** - * @dataProvider provideCommentMigrationOnDeletion - * @param int $wstage - * @param int $rstage - */ - public function testCommentMigrationOnDeletion( $wstage, $rstage ) { - $this->setMwGlobals( 'wgCommentTableSchemaMigrationStage', $wstage ); - $dbr = wfGetDB( DB_REPLICA ); - - $page = $this->createPage( - "WikiPageTest_testCommentMigrationOnDeletion", - "foo", - CONTENT_MODEL_WIKITEXT - ); - $revid = $page->getLatest(); - if ( $wstage > MIGRATION_OLD ) { - $comment_id = $dbr->selectField( - 'revision_comment_temp', - 'revcomment_comment_id', - [ 'revcomment_rev' => $revid ], - __METHOD__ - ); - } - - $this->setMwGlobals( 'wgCommentTableSchemaMigrationStage', $rstage ); - - $page->doDeleteArticle( "testing deletion" ); - - if ( $rstage > MIGRATION_OLD ) { - // Didn't leave behind any 'revision_comment_temp' rows - $n = $dbr->selectField( - 'revision_comment_temp', 'COUNT(*)', [ 'revcomment_rev' => $revid ], __METHOD__ - ); - $this->assertEquals( 0, $n, 'no entry in revision_comment_temp after deletion' ); - - // Copied or upgraded the comment_id, as applicable - $ar_comment_id = $dbr->selectField( - 'archive', - 'ar_comment_id', - [ 'ar_rev_id' => $revid ], - __METHOD__ - ); - if ( $wstage > MIGRATION_OLD ) { - $this->assertSame( $comment_id, $ar_comment_id ); - } else { - $this->assertNotEquals( 0, $ar_comment_id ); - } - } - - // Copied rev_comment, if applicable - if ( $rstage <= MIGRATION_WRITE_BOTH && $wstage <= MIGRATION_WRITE_BOTH ) { - $ar_comment = $dbr->selectField( - 'archive', - 'ar_comment', - [ 'ar_rev_id' => $revid ], - __METHOD__ - ); - $this->assertSame( 'testing', $ar_comment ); - } - } - - public static function provideCommentMigrationOnDeletion() { - return [ - [ MIGRATION_OLD, MIGRATION_OLD ], - [ MIGRATION_OLD, MIGRATION_WRITE_BOTH ], - [ MIGRATION_OLD, MIGRATION_WRITE_NEW ], - [ MIGRATION_WRITE_BOTH, MIGRATION_OLD ], - [ MIGRATION_WRITE_BOTH, MIGRATION_WRITE_BOTH ], - [ MIGRATION_WRITE_BOTH, MIGRATION_WRITE_NEW ], - [ MIGRATION_WRITE_BOTH, MIGRATION_NEW ], - [ MIGRATION_WRITE_NEW, MIGRATION_WRITE_BOTH ], - [ MIGRATION_WRITE_NEW, MIGRATION_WRITE_NEW ], - [ MIGRATION_WRITE_NEW, MIGRATION_NEW ], - [ MIGRATION_NEW, MIGRATION_WRITE_BOTH ], - [ MIGRATION_NEW, MIGRATION_WRITE_NEW ], - [ MIGRATION_NEW, MIGRATION_NEW ], - ]; - } - -} diff --git a/www/wiki/tests/phpunit/includes/page/WikiPageTestContentHandlerUseDB.php b/www/wiki/tests/phpunit/includes/page/WikiPageTestContentHandlerUseDB.php deleted file mode 100644 index 3db76280..00000000 --- a/www/wiki/tests/phpunit/includes/page/WikiPageTestContentHandlerUseDB.php +++ /dev/null @@ -1,61 +0,0 @@ -<?php - -/** - * @group ContentHandler - * @group Database - * ^--- important, causes temporary tables to be used instead of the real database - */ -class WikiPageTestContentHandlerUseDB extends WikiPageTest { - - protected function setUp() { - parent::setUp(); - $this->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/www/wiki/tests/phpunit/includes/pager/RangeChronologicalPagerTest.php b/www/wiki/tests/phpunit/includes/pager/RangeChronologicalPagerTest.php index 4721ce6f..72390ac8 100644 --- a/www/wiki/tests/phpunit/includes/pager/RangeChronologicalPagerTest.php +++ b/www/wiki/tests/phpunit/includes/pager/RangeChronologicalPagerTest.php @@ -14,7 +14,7 @@ class RangeChronologicalPagerTest extends MediaWikiLangTestCase { * @dataProvider getDateCondProvider */ public function testGetDateCond( $inputYear, $inputMonth, $inputDay, $expected ) { - $pager = $this->getMockForAbstractClass( 'RangeChronologicalPager' ); + $pager = $this->getMockForAbstractClass( RangeChronologicalPager::class ); $this->assertEquals( $expected, wfTimestamp( TS_MW, $pager->getDateCond( $inputYear, $inputMonth, $inputDay ) ) @@ -42,7 +42,7 @@ class RangeChronologicalPagerTest extends MediaWikiLangTestCase { * @dataProvider getDateRangeCondProvider */ public function testGetDateRangeCond( $start, $end, $expected ) { - $pager = $this->getMockForAbstractClass( 'RangeChronologicalPager' ); + $pager = $this->getMockForAbstractClass( RangeChronologicalPager::class ); $this->assertArrayEquals( $expected, $pager->getDateRangeCond( $start, $end ) ); } @@ -84,7 +84,7 @@ class RangeChronologicalPagerTest extends MediaWikiLangTestCase { * @dataProvider getDateRangeCondInvalidProvider */ public function testGetDateRangeCondInvalid( $start, $end ) { - $pager = $this->getMockForAbstractClass( 'RangeChronologicalPager' ); + $pager = $this->getMockForAbstractClass( RangeChronologicalPager::class ); $this->assertEquals( null, $pager->getDateRangeCond( $start, $end ) ); } diff --git a/www/wiki/tests/phpunit/includes/pager/ReverseChronologicalPagerTest.php b/www/wiki/tests/phpunit/includes/pager/ReverseChronologicalPagerTest.php index eb163d38..3910ab64 100644 --- a/www/wiki/tests/phpunit/includes/pager/ReverseChronologicalPagerTest.php +++ b/www/wiki/tests/phpunit/includes/pager/ReverseChronologicalPagerTest.php @@ -13,7 +13,7 @@ class ReverseChronologicalPagerTest extends MediaWikiLangTestCase { * @covers ReverseChronologicalPager::getDateCond */ public function testGetDateCond() { - $pager = $this->getMockForAbstractClass( 'ReverseChronologicalPager' ); + $pager = $this->getMockForAbstractClass( ReverseChronologicalPager::class ); $timestamp = MWTimestamp::getInstance(); $db = wfGetDB( DB_MASTER ); diff --git a/www/wiki/tests/phpunit/includes/parser/CoreParserFunctionsTest.php b/www/wiki/tests/phpunit/includes/parser/CoreParserFunctionsTest.php new file mode 100644 index 00000000..c6304477 --- /dev/null +++ b/www/wiki/tests/phpunit/includes/parser/CoreParserFunctionsTest.php @@ -0,0 +1,21 @@ +<?php +/** + * @group Database + * @covers CoreParserFunctions + */ +class CoreParserFunctionsTest extends MediaWikiTestCase { + + public function testGender() { + $user = User::createNew( '*Female' ); + $user->setOption( 'gender', 'female' ); + $user->saveSettings(); + + $msg = ( new RawMessage( '{{GENDER:*Female|m|f|o}}' ) )->parse(); + $this->assertEquals( $msg, 'f', 'Works unescaped' ); + $escapedName = wfEscapeWikiText( '*Female' ); + $msg2 = ( new RawMessage( '{{GENDER:' . $escapedName . '|m|f|o}}' ) ) + ->parse(); + $this->assertEquals( $msg, 'f', 'Works escaped' ); + } + +} diff --git a/www/wiki/tests/phpunit/includes/parser/MagicVariableTest.php b/www/wiki/tests/phpunit/includes/parser/MagicVariableTest.php index 6a2afad6..86b496e2 100644 --- a/www/wiki/tests/phpunit/includes/parser/MagicVariableTest.php +++ b/www/wiki/tests/phpunit/includes/parser/MagicVariableTest.php @@ -9,11 +9,12 @@ * @author Antoine Musso * @copyright Copyright © 2011, Antoine Musso * @file - * @todo covers tags - * - * @group Database */ +/** + * @group Database + * @covers Parser::getVariableValue + */ class MagicVariableTest extends MediaWikiTestCase { /** * @var Parser diff --git a/www/wiki/tests/phpunit/includes/parser/MediaWikiParserTest.php b/www/wiki/tests/phpunit/includes/parser/MediaWikiParserTest.php deleted file mode 100644 index 173447fc..00000000 --- a/www/wiki/tests/phpunit/includes/parser/MediaWikiParserTest.php +++ /dev/null @@ -1,138 +0,0 @@ -<?php -require_once __DIR__ . '/NewParserTest.php'; - -/** - * The UnitTest must be either a class that inherits from MediaWikiTestCase - * or a class that provides a public static suite() method which returns - * an PHPUnit_Framework_Test object - * - * @group Parser - * @group ParserTests - * @group Database - */ -class MediaWikiParserTest { - - /** - * @defgroup filtering_constants Filtering constants - * - * Limit inclusion of parser tests files coming from MediaWiki core - * @{ - */ - - /** Include files shipped with MediaWiki core */ - const CORE_ONLY = 1; - /** Include non core files as set in $wgParserTestFiles */ - const NO_CORE = 2; - /** Include anything set via $wgParserTestFiles */ - const WITH_ALL = 3; # CORE_ONLY | NO_CORE - - /** @} */ - - /** - * Get a PHPUnit test suite of parser tests. Optionally filtered with - * $flags. - * - * @par Examples: - * Get a suite of parser tests shipped by MediaWiki core: - * @code - * MediaWikiParserTest::suite( MediaWikiParserTest::CORE_ONLY ); - * @endcode - * Get a suite of various parser tests, like extensions: - * @code - * MediaWikiParserTest::suite( MediaWikiParserTest::NO_CORE ); - * @endcode - * Get any test defined via $wgParserTestFiles: - * @code - * MediaWikiParserTest::suite( MediaWikiParserTest::WITH_ALL ); - * @endcode - * - * @param int $flags Bitwise flag to filter out the $wgParserTestFiles that - * will be included. Default: MediaWikiParserTest::CORE_ONLY - * - * @return PHPUnit_Framework_TestSuite - */ - public static function suite( $flags = self::CORE_ONLY ) { - if ( is_string( $flags ) ) { - $flags = self::CORE_ONLY; - } - global $wgParserTestFiles, $IP; - - $mwTestDir = $IP . '/tests/'; - - # Human friendly helpers - $wantsCore = ( $flags & self::CORE_ONLY ); - $wantsRest = ( $flags & self::NO_CORE ); - - # Will hold the .txt parser test files we will include - $filesToTest = []; - - # Filter out .txt files - foreach ( $wgParserTestFiles as $parserTestFile ) { - $isCore = ( 0 === strpos( $parserTestFile, $mwTestDir ) ); - - if ( $isCore && $wantsCore ) { - self::debug( "included core parser tests: $parserTestFile" ); - $filesToTest[] = $parserTestFile; - } elseif ( !$isCore && $wantsRest ) { - self::debug( "included non core parser tests: $parserTestFile" ); - $filesToTest[] = $parserTestFile; - } else { - self::debug( "skipped parser tests: $parserTestFile" ); - } - } - self::debug( 'parser tests files: ' - . implode( ' ', $filesToTest ) ); - - $suite = new PHPUnit_Framework_TestSuite; - $testList = []; - $counter = 0; - foreach ( $filesToTest as $fileName ) { - // Call the highest level directory the extension name. - // It may or may not actually be, but it should be close - // enough to cause there to be separate names for different - // things, which is good enough for our purposes. - $extensionName = basename( dirname( $fileName ) ); - $testsName = $extensionName . '__' . basename( $fileName, '.txt' ); - $escapedFileName = strtr( $fileName, [ "'" => "\\'", '\\' => '\\\\' ] ); - $parserTestClassName = ucfirst( $testsName ); - - // Official spec for class names: http://php.net/manual/en/language.oop5.basic.php - // Prepend 'ParserTest_' to be paranoid about it not starting with a number - $parserTestClassName = 'ParserTest_' . - preg_replace( '/[^a-zA-Z0-9_\x7f-\xff]/', '_', $parserTestClassName ); - - if ( isset( $testList[$parserTestClassName] ) ) { - // If a conflict happens, gives a very unclear fatal. - // So as a last ditch effort to prevent that eventuality, if there - // is a conflict, append a number. - $counter++; - $parserTestClassName .= $counter; - } - $testList[$parserTestClassName] = true; - $parserTestClassDefinition = <<<EOT -/** - * @group Database - * @group Parser - * @group ParserTests - * @group ParserTests_$parserTestClassName - */ -class $parserTestClassName extends NewParserTest { - protected \$file = '$escapedFileName'; -} -EOT; - - eval( $parserTestClassDefinition ); - self::debug( "Adding test class $parserTestClassName" ); - $suite->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/www/wiki/tests/phpunit/includes/parser/NewParserTest.php b/www/wiki/tests/phpunit/includes/parser/NewParserTest.php deleted file mode 100644 index 8be35a5d..00000000 --- a/www/wiki/tests/phpunit/includes/parser/NewParserTest.php +++ /dev/null @@ -1,1123 +0,0 @@ -<?php - -/** - * Although marked as a stub, can work independently. - * - * @group Database - * @group Parser - * @group Stub - * - * @todo covers tags - */ -class NewParserTest extends MediaWikiTestCase { - static protected $articles = []; // Array of test articles defined by the tests - /* The data provider is run on a different instance than the test, so it must be static - * When running tests from several files, all tests will see all articles. - */ - static protected $backendToUse; - - public $keepUploads = false; - public $runDisabled = false; - public $runParsoid = false; - public $regex = ''; - public $showProgress = true; - public $savedWeirdGlobals = []; - public $savedGlobals = []; - public $hooks = []; - public $functionHooks = []; - public $transparentHooks = []; - - // Fuzz test - public $maxFuzzTestLength = 300; - public $fuzzSeed = 0; - public $memoryLimit = 50; - - /** - * @var DjVuSupport - */ - private $djVuSupport; - /** - * @var TidySupport - */ - private $tidySupport; - - protected $file = false; - - public static function setUpBeforeClass() { - // Inject ParserTest well-known interwikis - ParserTest::setupInterwikis(); - } - - protected function setUp() { - global $wgNamespaceAliases, $wgContLang; - global $wgHooks, $IP; - - parent::setUp(); - - // Setup CLI arguments - if ( $this->getCliArg( 'regex' ) ) { - $this->regex = $this->getCliArg( 'regex' ); - } else { - # Matches anything - $this->regex = ''; - } - - $this->keepUploads = $this->getCliArg( 'keep-uploads' ); - - $tmpGlobals = []; - - $tmpGlobals['wgLanguageCode'] = 'en'; - $tmpGlobals['wgContLang'] = Language::factory( 'en' ); - $tmpGlobals['wgSitename'] = 'MediaWiki'; - $tmpGlobals['wgServer'] = 'http://example.org'; - $tmpGlobals['wgServerName'] = 'example.org'; - $tmpGlobals['wgScriptPath'] = ''; - $tmpGlobals['wgScript'] = '/index.php'; - $tmpGlobals['wgResourceBasePath'] = ''; - $tmpGlobals['wgStylePath'] = '/skins'; - $tmpGlobals['wgExtensionAssetsPath'] = '/extensions'; - $tmpGlobals['wgArticlePath'] = '/wiki/$1'; - $tmpGlobals['wgActionPaths'] = []; - $tmpGlobals['wgVariantArticlePath'] = false; - $tmpGlobals['wgEnableUploads'] = true; - $tmpGlobals['wgUploadNavigationUrl'] = false; - $tmpGlobals['wgThumbnailScriptPath'] = false; - $tmpGlobals['wgLocalFileRepo'] = [ - 'class' => 'LocalRepo', - 'name' => 'local', - 'url' => 'http://example.com/images', - 'hashLevels' => 2, - 'transformVia404' => false, - 'backend' => 'local-backend' - ]; - $tmpGlobals['wgForeignFileRepos'] = []; - $tmpGlobals['wgDefaultExternalStore'] = []; - $tmpGlobals['wgParserCacheType'] = CACHE_NONE; - $tmpGlobals['wgCapitalLinks'] = true; - $tmpGlobals['wgNoFollowLinks'] = true; - $tmpGlobals['wgNoFollowDomainExceptions'] = []; - $tmpGlobals['wgExternalLinkTarget'] = false; - $tmpGlobals['wgThumbnailScriptPath'] = false; - $tmpGlobals['wgUseImageResize'] = true; - $tmpGlobals['wgAllowExternalImages'] = true; - $tmpGlobals['wgRawHtml'] = false; - $tmpGlobals['wgExperimentalHtmlIds'] = false; - $tmpGlobals['wgAdaptiveMessageCache'] = true; - $tmpGlobals['wgUseDatabaseMessages'] = true; - $tmpGlobals['wgLocaltimezone'] = 'UTC'; - $tmpGlobals['wgGroupPermissions'] = [ - '*' => [ - 'createaccount' => true, - 'read' => true, - 'edit' => true, - 'createpage' => true, - 'createtalk' => true, - ] ]; - $tmpGlobals['wgNamespaceProtection'] = [ NS_MEDIAWIKI => 'editinterface' ]; - - $tmpGlobals['wgParser'] = new StubObject( - 'wgParser', $GLOBALS['wgParserConf']['class'], - [ $GLOBALS['wgParserConf'] ] ); - - $tmpGlobals['wgFileExtensions'][] = 'svg'; - $tmpGlobals['wgSVGConverter'] = 'rsvg'; - $tmpGlobals['wgSVGConverters']['rsvg'] = - '$path/rsvg-convert -w $width -h $height -o $output $input'; - - 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'; - - // DjVu images have to be handled slightly differently - $tmpGlobals['wgMediaHandlers']['image/vnd.djvu'] = 'MockDjVuHandler'; - - // Ogg video/audio increasingly more differently - $tmpGlobals['wgMediaHandlers']['application/ogg'] = 'MockOggHandler'; - - $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'] = [ 100 => 'MemoryAlpha' ]; - - $tmpGlobals['wgLocalInterwikis'] = [ 'local', 'mi' ]; - # "extra language links" - # see https://gerrit.wikimedia.org/r/111390 - $tmpGlobals['wgExtraInterlanguageLinkPrefixes'] = [ 'mul' ]; - - // DjVu support - $this->djVuSupport = new DjVuSupport(); - // Tidy support - $this->tidySupport = new TidySupport(); - $tmpGlobals['wgTidyConfig'] = null; - $tmpGlobals['wgUseTidy'] = false; - $tmpGlobals['wgDebugTidy'] = false; - $tmpGlobals['wgTidyConf'] = $IP . '/includes/tidy/tidy.conf'; - $tmpGlobals['wgTidyOpts'] = ''; - $tmpGlobals['wgTidyInternal'] = $this->tidySupport->isInternal(); - - $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']; - - MWTidy::destroySingleton(); - - // 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 addDBDataOnce() { - # disabled for performance - # $this->tablesUsed[] = 'image'; - - # Update certain things in site_stats - $this->db->insert( 'site_stats', - [ 'ss_row_id' => 1, 'ss_images' => 2, 'ss_good_articles' => 1 ], - __METHOD__, - [ 'IGNORE' ] - ); - - $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', [ 'img_name' => $image->getName() ] ) ) { - $image->recordUpload2( - '', // archive name - 'Upload of some lame file', - 'Some lame file', - [ - 'size' => 7881, - 'width' => 1941, - 'height' => 220, - 'bits' => 8, - 'media_type' => MEDIATYPE_BITMAP, - 'mime' => 'image/jpeg', - 'metadata' => serialize( [] ), - 'sha1' => Wikimedia\base_convert( '1', 16, 36, 31 ), - 'fileExists' => true ], - $this->db->timestamp( '20010115123500' ), $user - ); - } - - $image = wfLocalFile( Title::makeTitle( NS_FILE, 'Thumb.png' ) ); - if ( !$this->db->selectField( 'image', '1', [ 'img_name' => $image->getName() ] ) ) { - $image->recordUpload2( - '', // archive name - 'Upload of some lame thumbnail', - 'Some lame thumbnail', - [ - 'size' => 22589, - 'width' => 135, - 'height' => 135, - 'bits' => 8, - 'media_type' => MEDIATYPE_BITMAP, - 'mime' => 'image/png', - 'metadata' => serialize( [] ), - 'sha1' => Wikimedia\base_convert( '2', 16, 36, 31 ), - 'fileExists' => true ], - $this->db->timestamp( '20130225203040' ), $user - ); - } - - # This image will be blacklisted in [[MediaWiki:Bad image list]] - $image = wfLocalFile( Title::makeTitle( NS_FILE, 'Bad.jpg' ) ); - if ( !$this->db->selectField( 'image', '1', [ 'img_name' => $image->getName() ] ) ) { - $image->recordUpload2( - '', // archive name - 'zomgnotcensored', - 'Borderline image', - [ - 'size' => 12345, - 'width' => 320, - 'height' => 240, - 'bits' => 24, - 'media_type' => MEDIATYPE_BITMAP, - 'mime' => 'image/jpeg', - 'metadata' => serialize( [] ), - 'sha1' => Wikimedia\base_convert( '3', 16, 36, 31 ), - 'fileExists' => true ], - $this->db->timestamp( '20010115123500' ), $user - ); - } - $image = wfLocalFile( Title::makeTitle( NS_FILE, 'Foobar.svg' ) ); - if ( !$this->db->selectField( 'image', '1', [ 'img_name' => $image->getName() ] ) ) { - $image->recordUpload2( '', 'Upload of some lame SVG', 'Some lame SVG', [ - 'size' => 12345, - 'width' => 240, - 'height' => 180, - 'bits' => 0, - 'media_type' => MEDIATYPE_DRAWING, - 'mime' => 'image/svg+xml', - 'metadata' => serialize( [] ), - 'sha1' => Wikimedia\base_convert( '', 16, 36, 31 ), - 'fileExists' => true - ], $this->db->timestamp( '20010115123500' ), $user ); - } - - $image = wfLocalFile( Title::makeTitle( NS_FILE, 'Video.ogv' ) ); - if ( !$this->db->selectField( 'image', '1', [ 'img_name' => $image->getName() ] ) ) { - $image->recordUpload2( '', 'A pretty movie', 'Will it play', [ - 'size' => 12345, - 'width' => 320, - 'height' => 240, - 'bits' => 0, - 'media_type' => MEDIATYPE_VIDEO, - 'mime' => 'application/ogg', - 'metadata' => serialize( [] ), - 'sha1' => Wikimedia\base_convert( '', 16, 36, 32 ), - 'fileExists' => true - ], $this->db->timestamp( '20010115123500' ), $user ); - } - - # A DjVu file - # A DjVu file - $image = wfLocalFile( Title::makeTitle( NS_FILE, 'LoremIpsum.djvu' ) ); - if ( !$this->db->selectField( 'image', '1', [ 'img_name' => $image->getName() ] ) ) { - $image->recordUpload2( '', 'Upload a DjVu', 'A DjVu', [ - 'size' => 3249, - 'width' => 2480, - 'height' => 3508, - 'bits' => 0, - 'media_type' => MEDIATYPE_BITMAP, - 'mime' => 'image/vnd.djvu', - 'metadata' => '<?xml version="1.0" ?> -<!DOCTYPE DjVuXML PUBLIC "-//W3C//DTD DjVuXML 1.1//EN" "pubtext/DjVuXML-s.dtd"> -<DjVuXML> -<HEAD></HEAD> -<BODY><OBJECT height="3508" width="2480"> -<PARAM name="DPI" value="300" /> -<PARAM name="GAMMA" value="2.2" /> -</OBJECT> -<OBJECT height="3508" width="2480"> -<PARAM name="DPI" value="300" /> -<PARAM name="GAMMA" value="2.2" /> -</OBJECT> -<OBJECT height="3508" width="2480"> -<PARAM name="DPI" value="300" /> -<PARAM name="GAMMA" value="2.2" /> -</OBJECT> -<OBJECT height="3508" width="2480"> -<PARAM name="DPI" value="300" /> -<PARAM name="GAMMA" value="2.2" /> -</OBJECT> -<OBJECT height="3508" width="2480"> -<PARAM name="DPI" value="300" /> -<PARAM name="GAMMA" value="2.2" /> -</OBJECT> -</BODY> -</DjVuXML>', - 'sha1' => Wikimedia\base_convert( '', 16, 36, 31 ), - 'fileExists' => true - ], $this->db->timestamp( '20140115123600' ), $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. - * @param array $opts - * @param string $config - * @return RequestContext - */ - protected function setupGlobals( $opts = [], $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 = []; - foreach ( $wgFileBackends as $conf ) { - if ( $conf['name'] == $name ) { - $useConfig = $conf; - } - } - $useConfig['name'] = 'local-backend'; // swap name - unset( $useConfig['lockManager'] ); - unset( $useConfig['fileJournal'] ); - $class = $useConfig['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( [ - 'name' => 'local-backend', - 'wikiId' => wfWikiID() - ] ); - } - - $settings = [ - 'wgLocalFileRepo' => [ - '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' => [ NS_MAIN => isset( $opts['subpage'] ) ], - 'wgAllowExternalImages' => self::getOptionValue( 'wgAllowExternalImages', $opts, true ), - 'wgThumbLimits' => [ self::getOptionValue( 'thumbsize', $opts, 180 ) ], - 'wgMaxTocLevel' => $maxtoclevel, - 'wgUseTeX' => isset( $opts['math'] ) || isset( $opts['texvc'] ), - 'wgMathDirectory' => $uploadDir . '/math', - 'wgDefaultLanguageVariant' => $variant, - 'wgLinkHolderBatchSize' => $linkHolderBatchSize, - 'wgUseTidy' => isset( $opts['tidy'] ), - ]; - - if ( $config ) { - $configLines = explode( "\n", $config ); - - foreach ( $configLines as $line ) { - list( $var, $value ) = explode( '=', $line, 2 ); - - $settings[$var] = eval( "return $value;" ); // ??? - } - } - - $this->savedGlobals = []; - - /** @since 1.20 */ - Hooks::run( 'ParserTestGlobals', [ &$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(); - - // We (re)set $wgThumbLimits to a single-element array above. - $context->getUser()->setOption( 'thumbsize', 0 ); - - foreach ( $settings as $var => $val ) { - if ( array_key_exists( $var, $GLOBALS ) ) { - $this->savedGlobals[$var] = $GLOBALS[$var]; - } - - $GLOBALS[$var] = $val; - } - - MWTidy::destroySingleton(); - 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 ) { - // Don't use getNewTempDirectory() as this is meant to persist - $dir = wfTempDir() . '/mwParser-images'; - - if ( is_dir( $dir ) ) { - return $dir; - } - } else { - $dir = $this->getNewTempDirectory(); - } - - 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( [ 'dir' => "$base/local-public/3/3a" ] ); - $backend->store( [ - 'src' => "$IP/tests/phpunit/data/parser/headbg.jpg", - 'dst' => "$base/local-public/3/3a/Foobar.jpg" - ] ); - $backend->prepare( [ 'dir' => "$base/local-public/e/ea" ] ); - $backend->store( [ - 'src' => "$IP/tests/phpunit/data/parser/wiki.png", - 'dst' => "$base/local-public/e/ea/Thumb.png" - ] ); - $backend->prepare( [ 'dir' => "$base/local-public/0/09" ] ); - $backend->store( [ - 'src' => "$IP/tests/phpunit/data/parser/headbg.jpg", - 'dst' => "$base/local-public/0/09/Bad.jpg" - ] ); - $backend->prepare( [ 'dir' => "$base/local-public/5/5f" ] ); - $backend->store( [ - 'src' => "$IP/tests/phpunit/data/parser/LoremIpsum.djvu", - 'dst' => "$base/local-public/5/5f/LoremIpsum.djvu" - ] ); - - // No helpful SVG file to copy, so make one ourselves - $data = '<?xml version="1.0" encoding="utf-8"?>' . - '<svg xmlns="http://www.w3.org/2000/svg"' . - ' version="1.1" width="240" height="180"/>'; - - $backend->prepare( [ 'dir' => "$base/local-public/f/ff" ] ); - $backend->quickCreate( [ - 'content' => $data, '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( - [ - "$base/local-public/3/3a/Foobar.jpg", - "$base/local-thumb/3/3a/Foobar.jpg/1000px-Foobar.jpg", - "$base/local-thumb/3/3a/Foobar.jpg/100px-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/137px-Foobar.jpg", - "$base/local-thumb/3/3a/Foobar.jpg/1500px-Foobar.jpg", - "$base/local-thumb/3/3a/Foobar.jpg/177px-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/206px-Foobar.jpg", - "$base/local-thumb/3/3a/Foobar.jpg/20px-Foobar.jpg", - "$base/local-thumb/3/3a/Foobar.jpg/220px-Foobar.jpg", - "$base/local-thumb/3/3a/Foobar.jpg/265px-Foobar.jpg", - "$base/local-thumb/3/3a/Foobar.jpg/270px-Foobar.jpg", - "$base/local-thumb/3/3a/Foobar.jpg/274px-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/330px-Foobar.jpg", - "$base/local-thumb/3/3a/Foobar.jpg/353px-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/440px-Foobar.jpg", - "$base/local-thumb/3/3a/Foobar.jpg/442px-Foobar.jpg", - "$base/local-thumb/3/3a/Foobar.jpg/450px-Foobar.jpg", - "$base/local-thumb/3/3a/Foobar.jpg/50px-Foobar.jpg", - "$base/local-thumb/3/3a/Foobar.jpg/600px-Foobar.jpg", - "$base/local-thumb/3/3a/Foobar.jpg/640px-Foobar.jpg", - "$base/local-thumb/3/3a/Foobar.jpg/70px-Foobar.jpg", - "$base/local-thumb/3/3a/Foobar.jpg/75px-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/5/5f/LoremIpsum.djvu", - "$base/local-thumb/5/5f/LoremIpsum.djvu/page2-2480px-LoremIpsum.djvu.jpg", - "$base/local-thumb/5/5f/LoremIpsum.djvu/page2-3720px-LoremIpsum.djvu.jpg", - "$base/local-thumb/5/5f/LoremIpsum.djvu/page2-4960px-LoremIpsum.djvu.jpg", - - "$base/local-public/f/ff/Foobar.svg", - "$base/local-thumb/f/ff/Foobar.svg/180px-Foobar.svg.png", - "$base/local-thumb/f/ff/Foobar.svg/2000px-Foobar.svg.png", - "$base/local-thumb/f/ff/Foobar.svg/270px-Foobar.svg.png", - "$base/local-thumb/f/ff/Foobar.svg/3000px-Foobar.svg.png", - "$base/local-thumb/f/ff/Foobar.svg/360px-Foobar.svg.png", - "$base/local-thumb/f/ff/Foobar.svg/4000px-Foobar.svg.png", - "$base/local-thumb/f/ff/Foobar.svg/langde-180px-Foobar.svg.png", - "$base/local-thumb/f/ff/Foobar.svg/langde-270px-Foobar.svg.png", - "$base/local-thumb/f/ff/Foobar.svg/langde-360px-Foobar.svg.png", - - "$base/local-public/math/f/a/5/fa50b8b616463173474302ca3e63586b.png", - ] - ); - } - - /** - * Delete the specified files, if they exist. - * @param array $files Full paths to files to delete. - */ - private static function deleteFiles( $files ) { - $backend = RepoGroup::singleton()->getLocalRepo()->getBackend(); - foreach ( $files as $file ) { - $backend->delete( [ 'src' => $file ], [ 'force' => 1 ] ); - } - foreach ( $files as $file ) { - $tmp = FileBackend::parentStoragePath( $file ); - while ( $tmp ) { - if ( !$backend->clean( [ 'dir' => $tmp ] )->isOK() ) { - break; - } - $tmp = FileBackend::parentStoragePath( $tmp ); - } - } - } - - protected function getBaseDir() { - return 'mwstore://local-backend'; - } - - public function parserTestProvider() { - if ( $this->file === false ) { - global $wgParserTestFiles; - $this->file = $wgParserTestFiles[0]; - } - - return new TestFileDataProvider( $this->file, $this ); - } - - /** - * Set the file from whose tests will be run by this instance - * @param string $filename - */ - public function setParserTestFile( $filename ) { - $this->file = $filename; - } - - /** - * @group medium - * @group ParserTests - * @dataProvider parserTestProvider - * @param string $desc - * @param string $input - * @param string $result - * @param array $opts - * @param array $config - */ - 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['djvu'] ) ) { - if ( !$this->djVuSupport->isEnabled() ) { - $this->markTestSkipped( "SKIPPED: djvu binaries do not exist or are not executable.\n" ); - } - } - - if ( isset( $opts['tidy'] ) ) { - if ( !$this->tidySupport->isEnabled() ) { - $this->markTestSkipped( "SKIPPED: tidy extension is not installed.\n" ); - } else { - $options->setTidy( true ); - } - } - - 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['tidy'] ) ) { - $out = preg_replace( '/\s+$/', '', $out ); - } - - if ( isset( $opts['showtitle'] ) ) { - if ( $output->getTitleText() ) { - $title = $output->getTitleText(); - } - - $out = "$title\n$out"; - } - - if ( isset( $opts['showindicators'] ) ) { - $indicators = ''; - foreach ( $output->getIndicators() as $id => $content ) { - $indicators .= "$id=$content\n"; - } - $out = $indicators . $out; - } - - if ( isset( $opts['ill'] ) ) { - $out = implode( ' ', $output->getLanguageLinks() ); - } elseif ( isset( $opts['cat'] ) ) { - $outputPage = $context->getOutput(); - $outputPage->addCategoryLinks( $output->getCategories() ); - $cats = $outputPage->getCategoryLinks(); - - if ( isset( $cats['normal'] ) ) { - $out = implode( ' ', $cats['normal'] ); - } else { - $out = ''; - } - } - $parser->mPreprocessor = null; - } - - $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 = [ $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 - * @param array $filenames - * @return string - */ - 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 - * @return array - */ - function getMemoryBreakdown() { - $memStats = []; - - 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 - * @param Preprocessor $preprocessor - * @return Parser - */ - function getParser( $preprocessor = null ) { - global $wgParserConf; - - $class = $wgParserConf['class']; - $parser = new $class( [ 'preprocessorClass' => $preprocessor ] + $wgParserConf ); - - Hooks::run( 'ParserTestParser', [ &$parser ] ); - - return $parser; - } - - // Various action functions - - public function addArticle( $name, $text, $line ) { - self::$articles[$name] = [ $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 string $name - * @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] ); - } - - public function requireTransparentHook( $name ) { - global $wgParser; - $wgParser->firstCallInit(); // make sure hooks are loaded. - return isset( $wgParser->mTransparentTagHooks[$name] ); - } - - // Various "cleanup" functions - - /** - * Remove last character if it is a newline - * @param string $s - * @return string - */ - 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 = []; - // 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( [ $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 string $key Name of option val to retrieve - * @param array $opts Options array to look in - * @param mixed $default Default value returned if not found - * @return mixed - */ - protected static function getOptionValue( $key, $opts, $default ) { - $key = strtolower( $key ); - - if ( isset( $opts[$key] ) ) { - return $opts[$key]; - } else { - return $default; - } - } -} diff --git a/www/wiki/tests/phpunit/includes/parser/ParserIntegrationTest.php b/www/wiki/tests/phpunit/includes/parser/ParserIntegrationTest.php index c9209824..91653b5d 100644 --- a/www/wiki/tests/phpunit/includes/parser/ParserIntegrationTest.php +++ b/www/wiki/tests/phpunit/includes/parser/ParserIntegrationTest.php @@ -8,13 +8,29 @@ use Wikimedia\ScopedCallback; * Note: the following groups are not used by PHPUnit. * The list in ParserTestFileSuite::__construct() is used instead. * + * @group large * @group Database * @group Parser * @group ParserTests * - * @todo covers tags + * @covers Parser + * @covers BlockLevelPass + * @covers CoreParserFunctions + * @covers CoreTagHooks + * @covers Sanitizer + * @covers Preprocessor + * @covers Preprocessor_DOM + * @covers Preprocessor_Hash + * @covers DateFormatter + * @covers LinkHolderArray + * @covers StripState + * @covers ParserOptions + * @covers ParserOutput */ -class ParserIntegrationTest extends PHPUnit_Framework_TestCase { +class ParserIntegrationTest extends PHPUnit\Framework\TestCase { + + use MediaWikiCoversValidator; + /** @var array */ private $ptTest; diff --git a/www/wiki/tests/phpunit/includes/parser/ParserMethodsTest.php b/www/wiki/tests/phpunit/includes/parser/ParserMethodsTest.php index ae58d1ce..d2ed4415 100644 --- a/www/wiki/tests/phpunit/includes/parser/ParserMethodsTest.php +++ b/www/wiki/tests/phpunit/includes/parser/ParserMethodsTest.php @@ -3,6 +3,7 @@ /** * @group Database * @covers Parser + * @covers BlockLevelPass */ class ParserMethodsTest extends MediaWikiLangTestCase { diff --git a/www/wiki/tests/phpunit/includes/parser/ParserOptionsTest.php b/www/wiki/tests/phpunit/includes/parser/ParserOptionsTest.php index ad899bd7..e2ed1d57 100644 --- a/www/wiki/tests/phpunit/includes/parser/ParserOptionsTest.php +++ b/www/wiki/tests/phpunit/includes/parser/ParserOptionsTest.php @@ -3,6 +3,9 @@ use Wikimedia\TestingAccessWrapper; use Wikimedia\ScopedCallback; +/** + * @covers ParserOptions + */ class ParserOptionsTest extends MediaWikiTestCase { private static function clearCache() { @@ -18,7 +21,6 @@ class ParserOptionsTest extends MediaWikiTestCase { 'stubthreshold' => true, 'printable' => true, 'userlang' => true, - 'wrapclass' => true, ]; } @@ -59,11 +61,14 @@ class ParserOptionsTest extends MediaWikiTestCase { 'No overrides' => [ true, [] ], 'In-key options are ok' => [ true, [ 'thumbsize' => 1e100, - 'wrapclass' => false, + 'printable' => false, ] ], 'Non-in-key options are not ok' => [ false, [ 'removeComments' => false, ] ], + 'Non-in-key options are not ok (2)' => [ false, [ + 'wrapclass' => 'foobar', + ] ], 'Canonical override, not default (1)' => [ true, [ 'tidy' => true, ] ], @@ -99,7 +104,7 @@ class ParserOptionsTest extends MediaWikiTestCase { } public static function provideOptionsHash() { - $used = [ 'wrapclass', 'printable' ]; + $used = [ 'thumbsize', 'printable' ]; $classWrapper = TestingAccessWrapper::newFromClass( ParserOptions::class ); $classWrapper->getDefaults(); @@ -113,9 +118,9 @@ class ParserOptionsTest extends MediaWikiTestCase { 'Canonical options, used some options' => [ $used, 'canonical', [] ], 'Used some options, non-default values' => [ $used, - 'printable=1!wrapclass=foobar', + 'printable=1!thumbsize=200', [ - 'wrapclass' => 'foobar', + 'thumbsize' => 200, 'printable' => true, ] ], @@ -136,23 +141,6 @@ class ParserOptionsTest extends MediaWikiTestCase { $confstr .= '!onPageRenderingHash'; } - // Test weird historical behavior is still weird - public function testOptionsHashEditSection() { - $popt = ParserOptions::newCanonical(); - $popt->registerWatcher( function ( $name ) { - $this->assertNotEquals( 'editsection', $name ); - } ); - - $this->assertTrue( $popt->getEditSection() ); - $this->assertSame( 'canonical', $popt->optionsHash( [] ) ); - $this->assertSame( 'canonical', $popt->optionsHash( [ 'editsection' ] ) ); - - $popt->setEditSection( false ); - $this->assertFalse( $popt->getEditSection() ); - $this->assertSame( 'canonical', $popt->optionsHash( [] ) ); - $this->assertSame( 'editsection=0', $popt->optionsHash( [ 'editsection' ] ) ); - } - /** * @expectedException InvalidArgumentException * @expectedExceptionMessage Unknown parser option bogus @@ -210,7 +198,7 @@ class ParserOptionsTest extends MediaWikiTestCase { $wgHooks['ParserOptionsRegister'] = []; $this->assertSame( [ 'dateformat', 'numberheadings', 'printable', 'stubthreshold', - 'thumbsize', 'userlang', 'wrapclass', + 'thumbsize', 'userlang' ], ParserOptions::allCacheVaryingOptions() ); self::clearCache(); @@ -228,7 +216,7 @@ class ParserOptionsTest extends MediaWikiTestCase { }; $this->assertSame( [ 'dateformat', 'foo', 'numberheadings', 'printable', 'stubthreshold', - 'thumbsize', 'userlang', 'wrapclass', + 'thumbsize', 'userlang' ], ParserOptions::allCacheVaryingOptions() ); } diff --git a/www/wiki/tests/phpunit/includes/parser/ParserOutputTest.php b/www/wiki/tests/phpunit/includes/parser/ParserOutputTest.php index ec8f0d07..b08ba6c4 100644 --- a/www/wiki/tests/phpunit/includes/parser/ParserOutputTest.php +++ b/www/wiki/tests/phpunit/includes/parser/ParserOutputTest.php @@ -89,4 +89,206 @@ class ParserOutputTest extends MediaWikiTestCase { $this->assertArrayNotHasKey( 'foo', $properties ); } + /** + * @covers ParserOutput::getText + * @dataProvider provideGetText + * @param array $options Options to getText() + * @param string $text Parser text + * @param string $expect Expected output + */ + public function testGetText( $options, $text, $expect ) { + $this->setMwGlobals( [ + 'wgArticlePath' => '/wiki/$1', + 'wgScriptPath' => '/w', + 'wgScript' => '/w/index.php', + ] ); + + $po = new ParserOutput( $text ); + $actual = $po->getText( $options ); + $this->assertSame( $expect, $actual ); + } + + public static function provideGetText() { + // phpcs:disable Generic.Files.LineLength + $text = <<<EOF +<div class="mw-parser-output"><p>Test document. +</p> +<mw:toc><div id="toc" class="toc"><div class="toctitle"><h2>Contents</h2></div> +<ul> +<li class="toclevel-1 tocsection-1"><a href="#Section_1"><span class="tocnumber">1</span> <span class="toctext">Section 1</span></a></li> +<li class="toclevel-1 tocsection-2"><a href="#Section_2"><span class="tocnumber">2</span> <span class="toctext">Section 2</span></a> +<ul> +<li class="toclevel-2 tocsection-3"><a href="#Section_2.1"><span class="tocnumber">2.1</span> <span class="toctext">Section 2.1</span></a></li> +</ul> +</li> +<li class="toclevel-1 tocsection-4"><a href="#Section_3"><span class="tocnumber">3</span> <span class="toctext">Section 3</span></a></li> +</ul> +</div> +</mw:toc> +<h2><span class="mw-headline" id="Section_1">Section 1</span><mw:editsection page="Test Page" section="1">Section 1</mw:editsection></h2> +<p>One +</p> +<h2><span class="mw-headline" id="Section_2">Section 2</span><mw:editsection page="Test Page" section="2">Section 2</mw:editsection></h2> +<p>Two +</p> +<h3><span class="mw-headline" id="Section_2.1">Section 2.1</span><mw:editsection page="Test Page" section="3">Section 2.1</mw:editsection></h3> +<p>Two point one +</p> +<h2><span class="mw-headline" id="Section_3">Section 3</span><mw:editsection page="Test Page" section="4">Section 3</mw:editsection></h2> +<p>Three +</p></div> +EOF; + + $dedupText = <<<EOF +<p>This is a test document.</p> +<style data-mw-deduplicate="duplicate1">.Duplicate1 {}</style> +<style data-mw-deduplicate="duplicate1">.Duplicate1 {}</style> +<style data-mw-deduplicate="duplicate2">.Duplicate2 {}</style> +<style data-mw-deduplicate="duplicate1">.Duplicate1 {}</style> +<style data-mw-deduplicate="duplicate2">.Duplicate2 {}</style> +<style data-mw-not-deduplicate="duplicate1">.Duplicate1 {}</style> +<style data-mw-deduplicate="duplicate1">.Same-attribute-different-content {}</style> +<style data-mw-deduplicate="duplicate3">.Duplicate1 {}</style> +<style>.Duplicate1 {}</style> +EOF; + + return [ + 'No options' => [ + [], $text, <<<EOF +<div class="mw-parser-output"><p>Test document. +</p> +<div id="toc" class="toc"><div class="toctitle"><h2>Contents</h2></div> +<ul> +<li class="toclevel-1 tocsection-1"><a href="#Section_1"><span class="tocnumber">1</span> <span class="toctext">Section 1</span></a></li> +<li class="toclevel-1 tocsection-2"><a href="#Section_2"><span class="tocnumber">2</span> <span class="toctext">Section 2</span></a> +<ul> +<li class="toclevel-2 tocsection-3"><a href="#Section_2.1"><span class="tocnumber">2.1</span> <span class="toctext">Section 2.1</span></a></li> +</ul> +</li> +<li class="toclevel-1 tocsection-4"><a href="#Section_3"><span class="tocnumber">3</span> <span class="toctext">Section 3</span></a></li> +</ul> +</div> + +<h2><span class="mw-headline" id="Section_1">Section 1</span><span class="mw-editsection"><span class="mw-editsection-bracket">[</span><a href="/w/index.php?title=Test_Page&action=edit&section=1" title="Edit section: Section 1">edit</a><span class="mw-editsection-bracket">]</span></span></h2> +<p>One +</p> +<h2><span class="mw-headline" id="Section_2">Section 2</span><span class="mw-editsection"><span class="mw-editsection-bracket">[</span><a href="/w/index.php?title=Test_Page&action=edit&section=2" title="Edit section: Section 2">edit</a><span class="mw-editsection-bracket">]</span></span></h2> +<p>Two +</p> +<h3><span class="mw-headline" id="Section_2.1">Section 2.1</span><span class="mw-editsection"><span class="mw-editsection-bracket">[</span><a href="/w/index.php?title=Test_Page&action=edit&section=3" title="Edit section: Section 2.1">edit</a><span class="mw-editsection-bracket">]</span></span></h3> +<p>Two point one +</p> +<h2><span class="mw-headline" id="Section_3">Section 3</span><span class="mw-editsection"><span class="mw-editsection-bracket">[</span><a href="/w/index.php?title=Test_Page&action=edit&section=4" title="Edit section: Section 3">edit</a><span class="mw-editsection-bracket">]</span></span></h2> +<p>Three +</p></div> +EOF + ], + 'Disable section edit links' => [ + [ 'enableSectionEditLinks' => false ], $text, <<<EOF +<div class="mw-parser-output"><p>Test document. +</p> +<div id="toc" class="toc"><div class="toctitle"><h2>Contents</h2></div> +<ul> +<li class="toclevel-1 tocsection-1"><a href="#Section_1"><span class="tocnumber">1</span> <span class="toctext">Section 1</span></a></li> +<li class="toclevel-1 tocsection-2"><a href="#Section_2"><span class="tocnumber">2</span> <span class="toctext">Section 2</span></a> +<ul> +<li class="toclevel-2 tocsection-3"><a href="#Section_2.1"><span class="tocnumber">2.1</span> <span class="toctext">Section 2.1</span></a></li> +</ul> +</li> +<li class="toclevel-1 tocsection-4"><a href="#Section_3"><span class="tocnumber">3</span> <span class="toctext">Section 3</span></a></li> +</ul> +</div> + +<h2><span class="mw-headline" id="Section_1">Section 1</span></h2> +<p>One +</p> +<h2><span class="mw-headline" id="Section_2">Section 2</span></h2> +<p>Two +</p> +<h3><span class="mw-headline" id="Section_2.1">Section 2.1</span></h3> +<p>Two point one +</p> +<h2><span class="mw-headline" id="Section_3">Section 3</span></h2> +<p>Three +</p></div> +EOF + ], + 'Disable TOC' => [ + [ 'allowTOC' => false ], $text, <<<EOF +<div class="mw-parser-output"><p>Test document. +</p> + +<h2><span class="mw-headline" id="Section_1">Section 1</span><span class="mw-editsection"><span class="mw-editsection-bracket">[</span><a href="/w/index.php?title=Test_Page&action=edit&section=1" title="Edit section: Section 1">edit</a><span class="mw-editsection-bracket">]</span></span></h2> +<p>One +</p> +<h2><span class="mw-headline" id="Section_2">Section 2</span><span class="mw-editsection"><span class="mw-editsection-bracket">[</span><a href="/w/index.php?title=Test_Page&action=edit&section=2" title="Edit section: Section 2">edit</a><span class="mw-editsection-bracket">]</span></span></h2> +<p>Two +</p> +<h3><span class="mw-headline" id="Section_2.1">Section 2.1</span><span class="mw-editsection"><span class="mw-editsection-bracket">[</span><a href="/w/index.php?title=Test_Page&action=edit&section=3" title="Edit section: Section 2.1">edit</a><span class="mw-editsection-bracket">]</span></span></h3> +<p>Two point one +</p> +<h2><span class="mw-headline" id="Section_3">Section 3</span><span class="mw-editsection"><span class="mw-editsection-bracket">[</span><a href="/w/index.php?title=Test_Page&action=edit&section=4" title="Edit section: Section 3">edit</a><span class="mw-editsection-bracket">]</span></span></h2> +<p>Three +</p></div> +EOF + ], + 'Unwrap text' => [ + [ 'unwrap' => true ], $text, <<<EOF +<p>Test document. +</p> +<div id="toc" class="toc"><div class="toctitle"><h2>Contents</h2></div> +<ul> +<li class="toclevel-1 tocsection-1"><a href="#Section_1"><span class="tocnumber">1</span> <span class="toctext">Section 1</span></a></li> +<li class="toclevel-1 tocsection-2"><a href="#Section_2"><span class="tocnumber">2</span> <span class="toctext">Section 2</span></a> +<ul> +<li class="toclevel-2 tocsection-3"><a href="#Section_2.1"><span class="tocnumber">2.1</span> <span class="toctext">Section 2.1</span></a></li> +</ul> +</li> +<li class="toclevel-1 tocsection-4"><a href="#Section_3"><span class="tocnumber">3</span> <span class="toctext">Section 3</span></a></li> +</ul> +</div> + +<h2><span class="mw-headline" id="Section_1">Section 1</span><span class="mw-editsection"><span class="mw-editsection-bracket">[</span><a href="/w/index.php?title=Test_Page&action=edit&section=1" title="Edit section: Section 1">edit</a><span class="mw-editsection-bracket">]</span></span></h2> +<p>One +</p> +<h2><span class="mw-headline" id="Section_2">Section 2</span><span class="mw-editsection"><span class="mw-editsection-bracket">[</span><a href="/w/index.php?title=Test_Page&action=edit&section=2" title="Edit section: Section 2">edit</a><span class="mw-editsection-bracket">]</span></span></h2> +<p>Two +</p> +<h3><span class="mw-headline" id="Section_2.1">Section 2.1</span><span class="mw-editsection"><span class="mw-editsection-bracket">[</span><a href="/w/index.php?title=Test_Page&action=edit&section=3" title="Edit section: Section 2.1">edit</a><span class="mw-editsection-bracket">]</span></span></h3> +<p>Two point one +</p> +<h2><span class="mw-headline" id="Section_3">Section 3</span><span class="mw-editsection"><span class="mw-editsection-bracket">[</span><a href="/w/index.php?title=Test_Page&action=edit&section=4" title="Edit section: Section 3">edit</a><span class="mw-editsection-bracket">]</span></span></h2> +<p>Three +</p> +EOF + ], + 'Unwrap without a mw-parser-output wrapper' => [ + [ 'unwrap' => true ], '<div class="foobar">Content</div>', '<div class="foobar">Content</div>' + ], + 'Unwrap with extra comment at end' => [ + [ 'unwrap' => true ], '<div class="mw-parser-output"><p>Test document.</p></div> +<!-- Saved in parser cache... -->', '<p>Test document.</p> +<!-- Saved in parser cache... -->' + ], + 'Style deduplication' => [ + [], $dedupText, <<<EOF +<p>This is a test document.</p> +<style data-mw-deduplicate="duplicate1">.Duplicate1 {}</style> +<link rel="mw-deduplicated-inline-style" href="mw-data:duplicate1"/> +<style data-mw-deduplicate="duplicate2">.Duplicate2 {}</style> +<link rel="mw-deduplicated-inline-style" href="mw-data:duplicate1"/> +<link rel="mw-deduplicated-inline-style" href="mw-data:duplicate2"/> +<style data-mw-not-deduplicate="duplicate1">.Duplicate1 {}</style> +<link rel="mw-deduplicated-inline-style" href="mw-data:duplicate1"/> +<style data-mw-deduplicate="duplicate3">.Duplicate1 {}</style> +<style>.Duplicate1 {}</style> +EOF + ], + 'Style deduplication disabled' => [ + [ 'deduplicateStyles' => false ], $dedupText, $dedupText + ], + ]; + // phpcs:enable + } + } diff --git a/www/wiki/tests/phpunit/includes/parser/PreprocessorTest.php b/www/wiki/tests/phpunit/includes/parser/PreprocessorTest.php index 11a21976..c415b586 100644 --- a/www/wiki/tests/phpunit/includes/parser/PreprocessorTest.php +++ b/www/wiki/tests/phpunit/includes/parser/PreprocessorTest.php @@ -37,8 +37,8 @@ class PreprocessorTest extends MediaWikiTestCase { protected $mPreprocessors; protected static $classNames = [ - 'Preprocessor_DOM', - 'Preprocessor_Hash' + Preprocessor_DOM::class, + Preprocessor_Hash::class ]; protected function setUp() { @@ -68,7 +68,7 @@ class PreprocessorTest extends MediaWikiTestCase { } public static function provideCases() { - // @codingStandardsIgnoreStart Ignore Generic.Files.LineLength.TooLong + // phpcs:disable Generic.Files.LineLength return self::addClassArg( [ [ "Foo", "<root>Foo</root>" ], [ "<!-- Foo -->", "<root><comment><!-- Foo --></comment></root>" ], @@ -156,7 +156,7 @@ class PreprocessorTest extends MediaWikiTestCase { [ "{{Foo|} Bar=}}", "<root><template><title>Foo</title><part><name>} Bar</name>=<value></value></part></template></root>" ], /* [ file_get_contents( __DIR__ . '/QuoteQuran.txt' ], file_get_contents( __DIR__ . '/QuoteQuranExpanded.txt' ) ], */ ] ); - // @codingStandardsIgnoreEnd + // phpcs:enable } /** @@ -208,7 +208,7 @@ class PreprocessorTest extends MediaWikiTestCase { * These are more complex test cases taken out of wiki articles. */ public static function provideFiles() { - // @codingStandardsIgnoreStart Ignore Generic.Files.LineLength.TooLong + // phpcs:disable Generic.Files.LineLength return self::addClassArg( [ [ "QuoteQuran" ], # https://en.wikipedia.org/w/index.php?title=Template:QuoteQuran/sandbox&oldid=237348988 GFDL + CC BY-SA by Striver [ "Factorial" ], # https://en.wikipedia.org/w/index.php?title=Template:Factorial&oldid=98548758 GFDL + CC BY-SA by Polonium @@ -216,7 +216,7 @@ class PreprocessorTest extends MediaWikiTestCase { [ "Fundraising" ], # https://tl.wiktionary.org/w/index.php?title=MediaWiki:Sitenotice&oldid=5716 GFDL + CC BY-SA, copied there by Sky Harbor. [ "NestedTemplates" ], # T29936 ] ); - // @codingStandardsIgnoreEnd + // phpcs:enable } /** @@ -242,7 +242,7 @@ class PreprocessorTest extends MediaWikiTestCase { * Tests from T30642 · https://phabricator.wikimedia.org/T30642 */ public static function provideHeadings() { - // @codingStandardsIgnoreStart Ignore Generic.Files.LineLength.TooLong + // phpcs:disable Generic.Files.LineLength return self::addClassArg( [ /* These should become headings: */ [ "== h ==<!--c1-->", "<root><h level=\"2\" i=\"1\">== h ==<comment><!--c1--></comment></h></root>" ], @@ -281,7 +281,7 @@ class PreprocessorTest extends MediaWikiTestCase { [ "== h ==<!--c1--> x <!--c2--><!--c3--> ", "<root>== h ==<comment><!--c1--></comment> x <comment><!--c2--></comment><comment><!--c3--></comment> </root>" ], [ "== h ==<!--c1--><!--c2--><!--c3--> x ", "<root>== h ==<comment><!--c1--></comment><comment><!--c2--></comment><comment><!--c3--></comment> x </root>" ], ] ); - // @codingStandardsIgnoreEnd + // phpcs:enable } /** diff --git a/www/wiki/tests/phpunit/includes/SanitizerTest.php b/www/wiki/tests/phpunit/includes/parser/SanitizerTest.php index 7256694f..35b81fb9 100644 --- a/www/wiki/tests/phpunit/includes/SanitizerTest.php +++ b/www/wiki/tests/phpunit/includes/parser/SanitizerTest.php @@ -322,6 +322,7 @@ class SanitizerTest extends MediaWikiTestCase { ], [ '/* insecure input */', 'foo: attr( title, url );' ], [ '/* insecure input */', 'foo: attr( title url );' ], + [ '/* insecure input */', 'foo: var(--evil-attribute)' ], ]; } @@ -388,7 +389,7 @@ class SanitizerTest extends MediaWikiTestCase { */ public function testEscapeIdReferenceList( $referenceList, $id1, $id2 ) { $this->assertEquals( - Sanitizer::escapeIdReferenceList( $referenceList, 'noninitial' ), + Sanitizer::escapeIdReferenceList( $referenceList ), Sanitizer::escapeIdForAttribute( $id1 ) . ' ' . Sanitizer::escapeIdForAttribute( $id2 ) @@ -406,6 +407,7 @@ class SanitizerTest extends MediaWikiTestCase { /** * @dataProvider provideIsReservedDataAttribute + * @covers Sanitizer::isReservedDataAttribute */ public function testIsReservedDataAttribute( $attr, $expected ) { $this->assertSame( $expected, Sanitizer::isReservedDataAttribute( $attr ) ); @@ -513,6 +515,34 @@ class SanitizerTest extends MediaWikiTestCase { } /** + * @dataProvider provideStripAllTags + * + * @covers Sanitizer::stripAllTags() + * @covers RemexStripTagHandler + * + * @param string $input + * @param string $expected + */ + public function testStripAllTags( $input, $expected ) { + $this->assertEquals( $expected, Sanitizer::stripAllTags( $input ) ); + } + + public function provideStripAllTags() { + return [ + [ '<p>Foo</p>', 'Foo' ], + [ '<p id="one">Foo</p><p id="two">Bar</p>', 'FooBar' ], + [ "<p>Foo</p>\n<p>Bar</p>", 'Foo Bar' ], + [ '<p>Hello <strong> world café</p>', 'Hello <strong> world café' ], + [ + '<p><small data-foo=\'bar"<baz>quux\'><a href="./Foo">Bar</a></small> Whee!</p>', + 'Bar Whee!' + ], + [ '1<span class="<?php">2</span>3', '123' ], + [ '1<span class="<?">2</span>3', '123' ], + ]; + } + + /** * @expectedException InvalidArgumentException * @covers Sanitizer::escapeIdInternal() */ diff --git a/www/wiki/tests/phpunit/includes/parser/StripStateTest.php b/www/wiki/tests/phpunit/includes/parser/StripStateTest.php new file mode 100644 index 00000000..0f4f6e0f --- /dev/null +++ b/www/wiki/tests/phpunit/includes/parser/StripStateTest.php @@ -0,0 +1,136 @@ +<?php + +/** + * @covers StripState + */ +class StripStateTest extends MediaWikiTestCase { + public function setUp() { + parent::setUp(); + $this->setContentLang( 'qqx' ); + } + + private function getMarker() { + static $i; + return Parser::MARKER_PREFIX . '-blah-' . sprintf( '%08X', $i++ ) . Parser::MARKER_SUFFIX; + } + + private static function getWarning( $message, $max = '' ) { + return "<span class=\"error\">($message: $max)</span>"; + } + + public function testAddNoWiki() { + $ss = new StripState; + $marker = $this->getMarker(); + $ss->addNoWiki( $marker, '<>' ); + $text = "x{$marker}y"; + $text = $ss->unstripGeneral( $text ); + $text = str_replace( '<', '', $text ); + $text = $ss->unstripNoWiki( $text ); + $this->assertSame( 'x<>y', $text ); + } + + public function testAddGeneral() { + $ss = new StripState; + $marker = $this->getMarker(); + $ss->addGeneral( $marker, '<>' ); + $text = "x{$marker}y"; + $text = $ss->unstripNoWiki( $text ); + $text = str_replace( '<', '', $text ); + $text = $ss->unstripGeneral( $text ); + $this->assertSame( 'x<>y', $text ); + } + + public function testUnstripBoth() { + $ss = new StripState; + $mk1 = $this->getMarker(); + $mk2 = $this->getMarker(); + $ss->addNoWiki( $mk1, '<1>' ); + $ss->addGeneral( $mk2, '<2>' ); + $text = "x{$mk1}{$mk2}y"; + $text = str_replace( '<', '', $text ); + $text = $ss->unstripBoth( $text ); + $this->assertSame( 'x<1><2>y', $text ); + } + + public static function provideUnstripRecursive() { + return [ + [ 0, 'text' ], + [ 1, '=text=' ], + [ 2, '==text==' ], + [ 3, '==' . self::getWarning( 'unstrip-depth-warning', 2 ) . '==' ], + ]; + } + + /** @dataProvider provideUnstripRecursive */ + public function testUnstripRecursive( $depth, $expected ) { + $ss = new StripState( null, [ 'depthLimit' => 2 ] ); + $text = 'text'; + for ( $i = 0; $i < $depth; $i++ ) { + $mk = $this->getMarker(); + $ss->addNoWiki( $mk, "={$text}=" ); + $text = $mk; + } + $text = $ss->unstripNoWiki( $text ); + $this->assertSame( $expected, $text ); + } + + public function testUnstripLoop() { + $ss = new StripState( null, [ 'depthLimit' => 2 ] ); + $mk = $this->getMarker(); + $ss->addNoWiki( $mk, $mk ); + $text = $ss->unstripNoWiki( $mk ); + $this->assertSame( self::getWarning( 'parser-unstrip-loop-warning' ), $text ); + } + + public static function provideUnstripSize() { + return [ + [ 0, 'x' ], + [ 1, 'xx' ], + [ 2, str_repeat( self::getWarning( 'unstrip-size-warning', 5 ), 2 ) ] + ]; + } + + /** @dataProvider provideUnstripSize */ + public function testUnstripSize( $depth, $expected ) { + $ss = new StripState( null, [ 'sizeLimit' => 5 ] ); + $text = 'x'; + for ( $i = 0; $i < $depth; $i++ ) { + $mk = $this->getMarker(); + $ss->addNoWiki( $mk, $text ); + $text = "$mk$mk"; + } + $text = $ss->unstripNoWiki( $text ); + $this->assertSame( $expected, $text ); + } + + public function provideGetLimitReport() { + for ( $i = 1; $i < 4; $i++ ) { + yield [ $i ]; + } + } + + /** @dataProvider provideGetLimitReport */ + public function testGetLimitReport( $depth ) { + $sizeLimit = 100000; + $ss = new StripState( null, [ 'depthLimit' => 5, 'sizeLimit' => $sizeLimit ] ); + $text = 'x'; + for ( $i = 0; $i < $depth; $i++ ) { + $mk = $this->getMarker(); + $ss->addNoWiki( $mk, $text ); + $text = "$mk$mk"; + } + $text = $ss->unstripNoWiki( $text ); + $report = $ss->getLimitReport(); + $messages = []; + foreach ( $report as list( $msg, $params ) ) { + $messages[$msg] = $params; + } + $this->assertSame( [ $depth - 1, 5 ], $messages['limitreport-unstrip-depth'] ); + $this->assertSame( + [ + strlen( $this->getMarker() ) * 2 * ( pow( 2, $depth ) - 2 ) + pow( 2, $depth ), + $sizeLimit + ], + $messages['limitreport-unstrip-size' ] ); + } +} diff --git a/www/wiki/tests/phpunit/includes/parser/TagHooksTest.php b/www/wiki/tests/phpunit/includes/parser/TagHooksTest.php index 06fe272b..bc09adc8 100644 --- a/www/wiki/tests/phpunit/includes/parser/TagHooksTest.php +++ b/www/wiki/tests/phpunit/includes/parser/TagHooksTest.php @@ -5,6 +5,7 @@ * @group Parser * * @covers Parser + * @covers BlockLevelPass * @covers StripState * * @covers Preprocessor_DOM @@ -28,7 +29,7 @@ * @covers PPNode_Hash_Array * @covers PPNode_Hash_Attr */ -class TagHookTest extends MediaWikiTestCase { +class TagHooksTest extends MediaWikiTestCase { public static function provideValidNames() { return [ [ 'foo' ], @@ -46,7 +47,6 @@ class TagHookTest extends MediaWikiTestCase { private function getParserOptions() { global $wgContLang; $popt = ParserOptions::newFromUserAndLang( new User, $wgContLang ); - $popt->setWrapOutputClass( false ); return $popt; } @@ -63,7 +63,7 @@ class TagHookTest extends MediaWikiTestCase { Title::newFromText( 'Test' ), $this->getParserOptions() ); - $this->assertEquals( "<p>FooOneBaz\n</p>", $parserOutput->getText() ); + $this->assertEquals( "<p>FooOneBaz\n</p>", $parserOutput->getText( [ 'unwrap' => true ] ) ); $parser->mPreprocessor = null; # Break the Parser <-> Preprocessor cycle } @@ -98,7 +98,7 @@ class TagHookTest extends MediaWikiTestCase { Title::newFromText( 'Test' ), $this->getParserOptions() ); - $this->assertEquals( "<p>FooOneBaz\n</p>", $parserOutput->getText() ); + $this->assertEquals( "<p>FooOneBaz\n</p>", $parserOutput->getText( [ 'unwrap' => true ] ) ); $parser->mPreprocessor = null; # Break the Parser <-> Preprocessor cycle } diff --git a/www/wiki/tests/phpunit/includes/parser/TidyTest.php b/www/wiki/tests/phpunit/includes/parser/TidyTest.php index 62b84aa1..be5125c7 100644 --- a/www/wiki/tests/phpunit/includes/parser/TidyTest.php +++ b/www/wiki/tests/phpunit/includes/parser/TidyTest.php @@ -55,8 +55,8 @@ MathML; '<editsection> should survive tidy' ], [ '<mw:toc>foo</mw:toc>', '<mw:toc>foo</mw:toc>', '<mw:toc> should survive tidy' ], - [ "<link foo=\"bar\" />\nfoo", '<link foo="bar"/>foo', '<link> should survive tidy' ], - [ "<meta foo=\"bar\" />\nfoo", '<meta foo="bar"/>foo', '<meta> should survive tidy' ], + [ "<link foo=\"bar\" />foo", '<link foo="bar"/>foo', '<link> should survive tidy' ], + [ "<meta foo=\"bar\" />foo", '<meta foo="bar"/>foo', '<meta> should survive tidy' ], [ $testMathML, $testMathML, '<math> should survive tidy' ], ]; } diff --git a/www/wiki/tests/phpunit/includes/password/BcryptPasswordTest.php b/www/wiki/tests/phpunit/includes/password/BcryptPasswordTest.php index 9b8e01e7..952f5417 100644 --- a/www/wiki/tests/phpunit/includes/password/BcryptPasswordTest.php +++ b/www/wiki/tests/phpunit/includes/password/BcryptPasswordTest.php @@ -10,13 +10,13 @@ class BcryptPasswordTest extends PasswordTestCase { protected function getTypeConfigs() { return [ 'bcrypt' => [ - 'class' => 'BcryptPassword', + 'class' => BcryptPassword::class, 'cost' => 9, ] ]; } public static function providePasswordTests() { - // @codingStandardsIgnoreStart Generic.Files.LineLength + // phpcs:disable Generic.Files.LineLength return [ // Tests from glibc bcrypt implementation [ true, ':bcrypt:5$CCCCCCCCCCCCCCCCCCCCC.$E5YPO9kmyuRGyh0XouQYb4YMJKvyOeW', "U*U" ], @@ -39,6 +39,6 @@ class BcryptPasswordTest extends PasswordTestCase { [ false, ':bcrypt:5$CCCCCCCCCCCCCCCCCCCCC.$E5YPO9kmyuRGyh0XouQYb4YMJKvyOeW', "UXU" ], [ false, ':bcrypt:5$CCCCCCCCCCCCCCCCCCCCC.$E5YPO9kmyuRGyh0XouQYb4YMJKvyOeW', "" ], ]; - // @codingStandardsIgnoreEnd + // phpcs:enable } } diff --git a/www/wiki/tests/phpunit/includes/password/EncryptedPasswordTest.php b/www/wiki/tests/phpunit/includes/password/EncryptedPasswordTest.php index 0c856537..6dfdea69 100644 --- a/www/wiki/tests/phpunit/includes/password/EncryptedPasswordTest.php +++ b/www/wiki/tests/phpunit/includes/password/EncryptedPasswordTest.php @@ -4,13 +4,12 @@ * @covers EncryptedPassword * @covers ParameterizedPassword * @covers Password - * @codingStandardsIgnoreStart Generic.Files.LineLength */ class EncryptedPasswordTest extends PasswordTestCase { protected function getTypeConfigs() { return [ 'both' => [ - 'class' => 'EncryptedPassword', + 'class' => EncryptedPassword::class, 'underlying' => 'pbkdf2', 'secrets' => [ md5( 'secret1' ), @@ -19,7 +18,7 @@ class EncryptedPasswordTest extends PasswordTestCase { 'cipher' => 'aes-256-cbc', ], 'secret1' => [ - 'class' => 'EncryptedPassword', + 'class' => EncryptedPassword::class, 'underlying' => 'pbkdf2', 'secrets' => [ md5( 'secret1' ), @@ -27,7 +26,7 @@ class EncryptedPasswordTest extends PasswordTestCase { 'cipher' => 'aes-256-cbc', ], 'secret2' => [ - 'class' => 'EncryptedPassword', + 'class' => EncryptedPassword::class, 'underlying' => 'pbkdf2', 'secrets' => [ md5( 'secret2' ), @@ -35,7 +34,7 @@ class EncryptedPasswordTest extends PasswordTestCase { 'cipher' => 'aes-256-cbc', ], 'pbkdf2' => [ - 'class' => 'Pbkdf2Password', + 'class' => Pbkdf2Password::class, 'algo' => 'sha256', 'cost' => '10', 'length' => '64', @@ -44,6 +43,7 @@ class EncryptedPasswordTest extends PasswordTestCase { } public static function providePasswordTests() { + // phpcs:disable Generic.Files.LineLength return [ // Encrypted with secret1 [ true, ':both:aes-256-cbc:0:izBpxujqC1YbzpCB3qAzgg==:ZqHnitT1pL4YJqKqFES2KEevZYSy2LtlibW5+IMi4XKOGKGy6sE638BXyBbLQQsBtTSrt+JyzwOayKtwIfRbaQsBridx/O1JwBSai1TkGkOsYMBXnlu2Bu/EquCBj5QpjYh7p3Uq4rpiop1KQlin1BJMwnAa1PovhxjpxnYhlhkM4X5ALoGi3XM0bapN48vt', 'password' ], @@ -54,6 +54,7 @@ class EncryptedPasswordTest extends PasswordTestCase { [ true, ':both:aes-256-cbc:1:m1LCnQVIakfYBNlr9KEgQg==:5yPTctqrzsybdgaMEag18AZYbnL37pAtXVBqmWxkjXbnNmiDH+1bHoL8lsEVTH/sJntC82kNVgE7zeiD8xUVLYF2VUnvB5+sU+aysE45/zwsCu7a22TaischMAOWrsHZ/tIgS/TnZY2d+HNyxgsEeeYf/QoL+FhmqHquK02+4SRbA5lLuj9niYy1r5CoM9cQ', 'password' ], [ true, ':secret2:aes-256-cbc:0:m1LCnQVIakfYBNlr9KEgQg==:5yPTctqrzsybdgaMEag18AZYbnL37pAtXVBqmWxkjXbnNmiDH+1bHoL8lsEVTH/sJntC82kNVgE7zeiD8xUVLYF2VUnvB5+sU+aysE45/zwsCu7a22TaischMAOWrsHZ/tIgS/TnZY2d+HNyxgsEeeYf/QoL+FhmqHquK02+4SRbA5lLuj9niYy1r5CoM9cQ', 'password' ], ]; + // phpcs:enable } /** @@ -61,12 +62,14 @@ class EncryptedPasswordTest extends PasswordTestCase { * @expectedException PasswordError */ public function testDecryptionError() { + // phpcs:ignore Generic.Files.LineLength $hash = ':secret1:aes-256-cbc:0:m1LCnQVIakfYBNlr9KEgQg==:5yPTctqrzsybdgaMEag18AZYbnL37pAtXVBqmWxkjXbnNmiDH+1bHoL8lsEVTH/sJntC82kNVgE7zeiD8xUVLYF2VUnvB5+sU+aysE45/zwsCu7a22TaischMAOWrsHZ/tIgS/TnZY2d+HNyxgsEeeYf/QoL+FhmqHquK02+4SRbA5lLuj9niYy1r5CoM9cQ'; $password = $this->passwordFactory->newFromCiphertext( $hash ); $password->crypt( 'password' ); } public function testUpdate() { + // phpcs:ignore Generic.Files.LineLength $hash = ':both:aes-256-cbc:0:izBpxujqC1YbzpCB3qAzgg==:ZqHnitT1pL4YJqKqFES2KEevZYSy2LtlibW5+IMi4XKOGKGy6sE638BXyBbLQQsBtTSrt+JyzwOayKtwIfRbaQsBridx/O1JwBSai1TkGkOsYMBXnlu2Bu/EquCBj5QpjYh7p3Uq4rpiop1KQlin1BJMwnAa1PovhxjpxnYhlhkM4X5ALoGi3XM0bapN48vt'; $fromHash = $this->passwordFactory->newFromCiphertext( $hash ); $fromPlaintext = $this->passwordFactory->newFromPlaintext( 'password', $fromHash ); diff --git a/www/wiki/tests/phpunit/includes/password/LayeredParameterizedPasswordTest.php b/www/wiki/tests/phpunit/includes/password/LayeredParameterizedPasswordTest.php index cf96d067..6a965a03 100644 --- a/www/wiki/tests/phpunit/includes/password/LayeredParameterizedPasswordTest.php +++ b/www/wiki/tests/phpunit/includes/password/LayeredParameterizedPasswordTest.php @@ -8,7 +8,7 @@ class LayeredParameterizedPasswordTest extends PasswordTestCase { protected function getTypeConfigs() { return [ 'testLargeLayeredTop' => [ - 'class' => 'LayeredParameterizedPassword', + 'class' => LayeredParameterizedPassword::class, 'types' => [ 'testLargeLayeredBottom', 'testLargeLayeredBottom', @@ -18,13 +18,13 @@ class LayeredParameterizedPasswordTest extends PasswordTestCase { ], ], 'testLargeLayeredBottom' => [ - 'class' => 'Pbkdf2Password', + 'class' => Pbkdf2Password::class, 'algo' => 'sha512', 'cost' => 1024, 'length' => 512, ], 'testLargeLayeredFinal' => [ - 'class' => 'BcryptPassword', + 'class' => BcryptPassword::class, 'cost' => 5, ] ]; @@ -35,15 +35,15 @@ class LayeredParameterizedPasswordTest extends PasswordTestCase { } public static function providePasswordTests() { - // @codingStandardsIgnoreStart Generic.Files.LineLength.TooLong + // phpcs:disable Generic.Files.LineLength return [ [ true, ':testLargeLayeredTop:sha512:1024:512!sha512:1024:512!sha512:1024:512!sha512:1024:512!5!vnRy+2SrSA0fHt3dwhTP5g==!AVnwfZsAQjn+gULv7FSGjA==!xvHUX3WcpkeSn1lvjWcvBg==!It+OC/N9tu+d3ByHhuB0BQ==!Tb.gqUOiD.aWktVwHM.Q/O!7CcyMfXUPky5ptyATJsR2nq3vUqtnBC', - 'testPassword123' + 'testPassword123' ], ]; - // @codingStandardsIgnoreEnd + // phpcs:enable } /** diff --git a/www/wiki/tests/phpunit/includes/password/MWOldPasswordTest.php b/www/wiki/tests/phpunit/includes/password/MWOldPasswordTest.php index 51e739ca..50100826 100644 --- a/www/wiki/tests/phpunit/includes/password/MWOldPasswordTest.php +++ b/www/wiki/tests/phpunit/includes/password/MWOldPasswordTest.php @@ -8,7 +8,7 @@ class MWOldPasswordTest extends PasswordTestCase { protected function getTypeConfigs() { return [ 'A' => [ - 'class' => 'MWOldPassword', + 'class' => MWOldPassword::class, ] ]; } diff --git a/www/wiki/tests/phpunit/includes/password/MWSaltedPasswordTest.php b/www/wiki/tests/phpunit/includes/password/MWSaltedPasswordTest.php index 53a6ad13..5616868d 100644 --- a/www/wiki/tests/phpunit/includes/password/MWSaltedPasswordTest.php +++ b/www/wiki/tests/phpunit/includes/password/MWSaltedPasswordTest.php @@ -8,7 +8,7 @@ class MWSaltedPasswordTest extends PasswordTestCase { protected function getTypeConfigs() { return [ 'B' => [ - 'class' => 'MWSaltedPassword', + 'class' => MWSaltedPassword::class, ] ]; } diff --git a/www/wiki/tests/phpunit/includes/password/PasswordFactoryTest.php b/www/wiki/tests/phpunit/includes/password/PasswordFactoryTest.php index 5d585f32..01b0de2c 100644 --- a/www/wiki/tests/phpunit/includes/password/PasswordFactoryTest.php +++ b/www/wiki/tests/phpunit/includes/password/PasswordFactoryTest.php @@ -6,14 +6,14 @@ class PasswordFactoryTest extends MediaWikiTestCase { public function testRegister() { $pf = new PasswordFactory; - $pf->register( 'foo', [ 'class' => 'InvalidPassword' ] ); + $pf->register( 'foo', [ 'class' => InvalidPassword::class ] ); $this->assertArrayHasKey( 'foo', $pf->getTypes() ); } public function testSetDefaultType() { $pf = new PasswordFactory; - $pf->register( '1', [ 'class' => 'InvalidPassword' ] ); - $pf->register( '2', [ 'class' => 'InvalidPassword' ] ); + $pf->register( '1', [ 'class' => InvalidPassword::class ] ); + $pf->register( '2', [ 'class' => InvalidPassword::class ] ); $pf->setDefaultType( '1' ); $this->assertSame( '1', $pf->getDefaultType() ); $pf->setDefaultType( '2' ); @@ -31,7 +31,7 @@ class PasswordFactoryTest extends MediaWikiTestCase { public function testInit() { $config = new HashConfig( [ 'PasswordConfig' => [ - 'foo' => [ 'class' => 'InvalidPassword' ], + 'foo' => [ 'class' => InvalidPassword::class ], ], 'PasswordDefault' => 'foo' ] ); @@ -43,7 +43,7 @@ class PasswordFactoryTest extends MediaWikiTestCase { public function testNewFromCiphertext() { $pf = new PasswordFactory; - $pf->register( 'B', [ 'class' => 'MWSaltedPassword' ] ); + $pf->register( 'B', [ 'class' => MWSaltedPassword::class ] ); $pw = $pf->newFromCiphertext( ':B:salt:d529e941509eb9e9b9cfaeae1fe7ca23' ); $this->assertInstanceOf( MWSaltedPassword::class, $pw ); } @@ -58,13 +58,13 @@ class PasswordFactoryTest extends MediaWikiTestCase { */ public function testNewFromCiphertextErrors( $hash ) { $pf = new PasswordFactory; - $pf->register( 'B', [ 'class' => 'MWSaltedPassword' ] ); + $pf->register( 'B', [ 'class' => MWSaltedPassword::class ] ); $pf->newFromCiphertext( $hash ); } public function testNewFromType() { $pf = new PasswordFactory; - $pf->register( 'B', [ 'class' => 'MWSaltedPassword' ] ); + $pf->register( 'B', [ 'class' => MWSaltedPassword::class ] ); $pw = $pf->newFromType( 'B' ); $this->assertInstanceOf( MWSaltedPassword::class, $pw ); } @@ -74,26 +74,26 @@ class PasswordFactoryTest extends MediaWikiTestCase { */ public function testNewFromTypeError() { $pf = new PasswordFactory; - $pf->register( 'B', [ 'class' => 'MWSaltedPassword' ] ); + $pf->register( 'B', [ 'class' => MWSaltedPassword::class ] ); $pf->newFromType( 'bogus' ); } public function testNewFromPlaintext() { $pf = new PasswordFactory; - $pf->register( 'A', [ 'class' => 'MWOldPassword' ] ); - $pf->register( 'B', [ 'class' => 'MWSaltedPassword' ] ); + $pf->register( 'A', [ 'class' => MWOldPassword::class ] ); + $pf->register( 'B', [ 'class' => MWSaltedPassword::class ] ); $pf->setDefaultType( 'A' ); - $this->assertInstanceOf( 'InvalidPassword', $pf->newFromPlaintext( null ) ); - $this->assertInstanceOf( 'MWOldPassword', $pf->newFromPlaintext( 'password' ) ); - $this->assertInstanceOf( 'MWSaltedPassword', + $this->assertInstanceOf( InvalidPassword::class, $pf->newFromPlaintext( null ) ); + $this->assertInstanceOf( MWOldPassword::class, $pf->newFromPlaintext( 'password' ) ); + $this->assertInstanceOf( MWSaltedPassword::class, $pf->newFromPlaintext( 'password', $pf->newFromType( 'B' ) ) ); } public function testNeedsUpdate() { $pf = new PasswordFactory; - $pf->register( 'A', [ 'class' => 'MWOldPassword' ] ); - $pf->register( 'B', [ 'class' => 'MWSaltedPassword' ] ); + $pf->register( 'A', [ 'class' => MWOldPassword::class ] ); + $pf->register( 'B', [ 'class' => MWSaltedPassword::class ] ); $pf->setDefaultType( 'A' ); $this->assertFalse( $pf->needsUpdate( $pf->newFromType( 'A' ) ) ); @@ -105,6 +105,6 @@ class PasswordFactoryTest extends MediaWikiTestCase { } public function testNewInvalidPassword() { - $this->assertInstanceOf( 'InvalidPassword', PasswordFactory::newInvalidPassword() ); + $this->assertInstanceOf( InvalidPassword::class, PasswordFactory::newInvalidPassword() ); } } diff --git a/www/wiki/tests/phpunit/includes/password/PasswordTest.php b/www/wiki/tests/phpunit/includes/password/PasswordTest.php index d0177d0d..65c91993 100644 --- a/www/wiki/tests/phpunit/includes/password/PasswordTest.php +++ b/www/wiki/tests/phpunit/includes/password/PasswordTest.php @@ -36,6 +36,6 @@ class PasswordTest extends MediaWikiTestCase { $passwordFactory = new PasswordFactory(); $invalid = $passwordFactory->newFromPlaintext( null ); - $this->assertInstanceOf( 'InvalidPassword', $invalid ); + $this->assertInstanceOf( InvalidPassword::class, $invalid ); } } diff --git a/www/wiki/tests/phpunit/includes/password/Pbkdf2PasswordFallbackTest.php b/www/wiki/tests/phpunit/includes/password/Pbkdf2PasswordFallbackTest.php index 605d1905..cf851c81 100644 --- a/www/wiki/tests/phpunit/includes/password/Pbkdf2PasswordFallbackTest.php +++ b/www/wiki/tests/phpunit/includes/password/Pbkdf2PasswordFallbackTest.php @@ -9,7 +9,7 @@ class Pbkdf2PasswordFallbackTest extends PasswordTestCase { protected function getTypeConfigs() { return [ 'pbkdf2' => [ - 'class' => 'Pbkdf2Password', + 'class' => Pbkdf2Password::class, 'algo' => 'sha256', 'cost' => '10000', 'length' => '128', diff --git a/www/wiki/tests/phpunit/includes/password/Pbkdf2PasswordTest.php b/www/wiki/tests/phpunit/includes/password/Pbkdf2PasswordTest.php index ff069cd9..7e97ab1a 100644 --- a/www/wiki/tests/phpunit/includes/password/Pbkdf2PasswordTest.php +++ b/www/wiki/tests/phpunit/includes/password/Pbkdf2PasswordTest.php @@ -10,7 +10,7 @@ class Pbkdf2PasswordTest extends PasswordTestCase { protected function getTypeConfigs() { return [ 'pbkdf2' => [ - 'class' => 'Pbkdf2Password', + 'class' => Pbkdf2Password::class, 'algo' => 'sha256', 'cost' => '10000', 'length' => '128', diff --git a/www/wiki/tests/phpunit/includes/password/UserPasswordPolicyTest.php b/www/wiki/tests/phpunit/includes/password/UserPasswordPolicyTest.php index 0839cfbb..78175fac 100644 --- a/www/wiki/tests/phpunit/includes/password/UserPasswordPolicyTest.php +++ b/www/wiki/tests/phpunit/includes/password/UserPasswordPolicyTest.php @@ -26,6 +26,8 @@ */ class UserPasswordPolicyTest extends MediaWikiTestCase { + protected $tablesUsed = [ 'user', 'user_groups' ]; + protected $policies = [ 'checkuser' => [ 'MinimalPasswordLength' => 10, diff --git a/www/wiki/tests/phpunit/includes/poolcounter/PoolCounterTest.php b/www/wiki/tests/phpunit/includes/poolcounter/PoolCounterTest.php index d57ad041..f7f2013c 100644 --- a/www/wiki/tests/phpunit/includes/poolcounter/PoolCounterTest.php +++ b/www/wiki/tests/phpunit/includes/poolcounter/PoolCounterTest.php @@ -9,6 +9,9 @@ abstract class PoolCounterAbstractMock extends PoolCounter { } } +/** + * @covers PoolCounter + */ class PoolCounterTest extends MediaWikiTestCase { public function testConstruct() { $poolCounterConfig = [ @@ -18,13 +21,13 @@ class PoolCounterTest extends MediaWikiTestCase { 'maxqueue' => 100, ]; - $poolCounter = $this->getMockBuilder( 'PoolCounterAbstractMock' ) + $poolCounter = $this->getMockBuilder( PoolCounterAbstractMock::class ) ->setConstructorArgs( [ $poolCounterConfig, 'testCounter', 'someKey' ] ) // don't mock anything - the proper syntax would be setMethods(null), but due // to a PHPUnit bug that does not work with getMockForAbstractClass() ->setMethods( [ 'idontexist' ] ) ->getMockForAbstractClass(); - $this->assertInstanceOf( 'PoolCounter', $poolCounter ); + $this->assertInstanceOf( PoolCounter::class, $poolCounter ); } public function testConstructWithSlots() { @@ -36,15 +39,15 @@ class PoolCounterTest extends MediaWikiTestCase { 'maxqueue' => 100, ]; - $poolCounter = $this->getMockBuilder( 'PoolCounterAbstractMock' ) + $poolCounter = $this->getMockBuilder( PoolCounterAbstractMock::class ) ->setConstructorArgs( [ $poolCounterConfig, 'testCounter', 'key' ] ) ->setMethods( [ 'idontexist' ] ) // don't mock anything ->getMockForAbstractClass(); - $this->assertInstanceOf( 'PoolCounter', $poolCounter ); + $this->assertInstanceOf( PoolCounter::class, $poolCounter ); } public function testHashKeyIntoSlots() { - $poolCounter = $this->getMockBuilder( 'PoolCounterAbstractMock' ) + $poolCounter = $this->getMockBuilder( PoolCounterAbstractMock::class ) // don't mock anything - the proper syntax would be setMethods(null), but due // to a PHPUnit bug that does not work with getMockForAbstractClass() ->setMethods( [ 'idontexist' ] ) diff --git a/www/wiki/tests/phpunit/includes/preferences/DefaultPreferencesFactoryTest.php b/www/wiki/tests/phpunit/includes/preferences/DefaultPreferencesFactoryTest.php new file mode 100644 index 00000000..c1015234 --- /dev/null +++ b/www/wiki/tests/phpunit/includes/preferences/DefaultPreferencesFactoryTest.php @@ -0,0 +1,183 @@ +<?php + +use MediaWiki\Auth\AuthManager; +use MediaWiki\MediaWikiServices; +use MediaWiki\Preferences\DefaultPreferencesFactory; +use Wikimedia\ObjectFactory; +use Wikimedia\TestingAccessWrapper; + +/** + * 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 + */ + +/** + * @group Preferences + */ +class DefaultPreferencesFactoryTest extends MediaWikiTestCase { + + /** @var IContextSource */ + protected $context; + + /** @var Config */ + protected $config; + + public function setUp() { + parent::setUp(); + global $wgParserConf; + $this->context = new RequestContext(); + $this->context->setTitle( Title::newFromText( self::class ) ); + $this->setMwGlobals( 'wgParser', + ObjectFactory::constructClassInstance( $wgParserConf['class'], [ $wgParserConf ] ) + ); + $this->config = MediaWikiServices::getInstance()->getMainConfig(); + } + + /** + * Get a basic PreferencesFactory for testing with. + * @return DefaultPreferencesFactory + */ + protected function getPreferencesFactory() { + return new DefaultPreferencesFactory( + $this->config, + new Language(), + AuthManager::singleton(), + MediaWikiServices::getInstance()->getLinkRenderer() + ); + } + + /** + * @covers MediaWiki\Preferences\DefaultPreferencesFactory::getForm() + */ + public function testGetForm() { + $testUser = $this->getTestUser(); + $form = $this->getPreferencesFactory()->getForm( $testUser->getUser(), $this->context ); + $this->assertInstanceOf( PreferencesForm::class, $form ); + $this->assertCount( 5, $form->getPreferenceSections() ); + } + + /** + * CSS classes for emailauthentication preference field when there's no email. + * @see https://phabricator.wikimedia.org/T36302 + * @covers MediaWiki\Preferences\DefaultPreferencesFactory::profilePreferences() + * @dataProvider emailAuthenticationProvider + */ + public function testEmailAuthentication( $user, $cssClass ) { + $prefs = $this->getPreferencesFactory()->getFormDescriptor( $user, $this->context ); + $this->assertArrayHasKey( 'cssclass', $prefs['emailauthentication'] ); + $this->assertEquals( $cssClass, $prefs['emailauthentication']['cssclass'] ); + } + + public function emailAuthenticationProvider() { + $userNoEmail = new User; + $userEmailUnauthed = new User; + $userEmailUnauthed->setEmail( 'noauth@example.org' ); + $userEmailAuthed = new User; + $userEmailAuthed->setEmail( 'noauth@example.org' ); + $userEmailAuthed->setEmailAuthenticationTimestamp( wfTimestamp() ); + return [ + [ $userNoEmail, 'mw-email-none' ], + [ $userEmailUnauthed, 'mw-email-not-authenticated' ], + [ $userEmailAuthed, 'mw-email-authenticated' ], + ]; + } + + /** + * Test that PreferencesFormPreSave hook has correct data: + * - user Object is passed + * - oldUserOptions contains previous user options (before save) + * - formData and User object have set up new properties + * + * @see https://phabricator.wikimedia.org/T169365 + * @covers MediaWiki\Preferences\DefaultPreferencesFactory::submitForm() + */ + public function testPreferencesFormPreSaveHookHasCorrectData() { + $oldOptions = [ + 'test' => 'abc', + 'option' => 'old' + ]; + $newOptions = [ + 'test' => 'abc', + 'option' => 'new' + ]; + $configMock = new HashConfig( [ + 'HiddenPrefs' => [] + ] ); + $form = $this->getMockBuilder( PreferencesForm::class ) + ->disableOriginalConstructor() + ->getMock(); + + $userMock = $this->getMockBuilder( User::class ) + ->disableOriginalConstructor() + ->getMock(); + $userMock->method( 'getOptions' ) + ->willReturn( $oldOptions ); + $userMock->method( 'isAllowedAny' ) + ->willReturn( true ); + $userMock->method( 'isAllowed' ) + ->willReturn( true ); + + $userMock->expects( $this->exactly( 2 ) ) + ->method( 'setOption' ) + ->withConsecutive( + [ $this->equalTo( 'test' ), $this->equalTo( $newOptions[ 'test' ] ) ], + [ $this->equalTo( 'option' ), $this->equalTo( $newOptions[ 'option' ] ) ] + ); + + $form->expects( $this->any() ) + ->method( 'getModifiedUser' ) + ->willReturn( $userMock ); + + $form->expects( $this->any() ) + ->method( 'getContext' ) + ->willReturn( $this->context ); + + $form->expects( $this->any() ) + ->method( 'getConfig' ) + ->willReturn( $configMock ); + + $this->setTemporaryHook( 'PreferencesFormPreSave', + function ( $formData, $form, $user, &$result, $oldUserOptions ) + use ( $newOptions, $oldOptions, $userMock ) { + $this->assertSame( $userMock, $user ); + foreach ( $newOptions as $option => $value ) { + $this->assertSame( $value, $formData[ $option ] ); + } + foreach ( $oldOptions as $option => $value ) { + $this->assertSame( $value, $oldUserOptions[ $option ] ); + } + $this->assertEquals( true, $result ); + } + ); + + $factory = TestingAccessWrapper::newFromObject( $this->getPreferencesFactory() ); + $factory->saveFormData( $newOptions, $form ); + } + + /** + * The rclimit preference should accept non-integer input and filter it to become an integer. + */ + public function testIntvalFilter() { + // Test a string with leading zeros (i.e. not octal) and spaces. + $this->context->getRequest()->setVal( 'wprclimit', ' 0012 ' ); + $user = new User; + $form = $this->getPreferencesFactory()->getForm( $user, $this->context ); + $form->show(); + $form->trySubmit(); + $this->assertEquals( 12, $user->getOption( 'rclimit' ) ); + } +} diff --git a/www/wiki/tests/phpunit/includes/rcfeed/RCFeedIntegrationTest.php b/www/wiki/tests/phpunit/includes/rcfeed/RCFeedIntegrationTest.php index 3e9c567e..871ea911 100644 --- a/www/wiki/tests/phpunit/includes/rcfeed/RCFeedIntegrationTest.php +++ b/www/wiki/tests/phpunit/includes/rcfeed/RCFeedIntegrationTest.php @@ -1,7 +1,13 @@ <?php /** + * @group medium * @group Database + * @covers FormattedRCFeed + * @covers RecentChange + * @covers JSONRCFeedFormatter + * @covers MachineReadableRCFeedFormatter + * @covers RCFeed */ class RCFeedIntegrationTest extends MediaWikiTestCase { protected function setUp() { @@ -17,18 +23,9 @@ class RCFeedIntegrationTest extends MediaWikiTestCase { ] ); } - /** - * @covers RecentChange::notifyRCFeeds - * @covers RecentChange::getEngine - * @covers RCFeed::factory - * @covers FormattedRCFeed::__construct - * @covers FormattedRCFeed::notify - * @covers JSONRCFeedFormatter::formatArray - * @covers MachineReadableRCFeedFormatter::getLine - */ public function testNotify() { - $feed = $this->getMockBuilder( 'RCFeedEngine' ) - ->setConstructorArgs( [ [ 'formatter' => 'JSONRCFeedFormatter' ] ] ) + $feed = $this->getMockBuilder( RCFeedEngine::class ) + ->setConstructorArgs( [ [ 'formatter' => JSONRCFeedFormatter::class ] ] ) ->setMethods( [ 'send' ] ) ->getMock(); @@ -71,7 +68,7 @@ class RCFeedIntegrationTest extends MediaWikiTestCase { 'wgRCFeeds' => [ 'myfeed' => [ 'uri' => 'test://localhost:1234', - 'formatter' => 'JSONRCFeedFormatter', + 'formatter' => JSONRCFeedFormatter::class, ], ], 'wgRCEngines' => [ diff --git a/www/wiki/tests/phpunit/includes/registration/CoreVersionCheckerTest.php b/www/wiki/tests/phpunit/includes/registration/CoreVersionCheckerTest.php deleted file mode 100644 index 4aa4f415..00000000 --- a/www/wiki/tests/phpunit/includes/registration/CoreVersionCheckerTest.php +++ /dev/null @@ -1,38 +0,0 @@ -<?php - -/** - * @covers CoreVersionChecker - */ -class CoreVersionCheckerTest extends PHPUnit_Framework_TestCase { - /** - * @dataProvider provideCheck - */ - public function testCheck( $coreVersion, $constraint, $expected ) { - $checker = new CoreVersionChecker( $coreVersion ); - $this->assertEquals( $expected, $checker->check( $constraint ) ); - } - - public static function provideCheck() { - return [ - // array( $wgVersion, constraint, expected ) - [ '1.25alpha', '>= 1.26', false ], - [ '1.25.0', '>= 1.26', false ], - [ '1.26alpha', '>= 1.26', true ], - [ '1.26alpha', '>= 1.26.0', true ], - [ '1.26alpha', '>= 1.26.0-stable', false ], - [ '1.26.0', '>= 1.26.0-stable', true ], - [ '1.26.1', '>= 1.26.0-stable', true ], - [ '1.27.1', '>= 1.26.0-stable', true ], - [ '1.26alpha', '>= 1.26.1', false ], - [ '1.26alpha', '>= 1.26alpha', true ], - [ '1.26alpha', '>= 1.25', true ], - [ '1.26.0-alpha.14', '>= 1.26.0-alpha.15', false ], - [ '1.26.0-alpha.14', '>= 1.26.0-alpha.10', true ], - [ '1.26.1', '>= 1.26.2, <=1.26.0', false ], - [ '1.26.1', '^1.26.2', false ], - // Accept anything for un-parsable version strings - [ '1.26mwf14', '== 1.25alpha', true ], - [ 'totallyinvalid', '== 1.0', true ], - ]; - } -} diff --git a/www/wiki/tests/phpunit/includes/registration/ExtensionJsonValidatorTest.php b/www/wiki/tests/phpunit/includes/registration/ExtensionJsonValidatorTest.php new file mode 100644 index 00000000..d69ad597 --- /dev/null +++ b/www/wiki/tests/phpunit/includes/registration/ExtensionJsonValidatorTest.php @@ -0,0 +1,84 @@ +<?php +/** + * Copyright (C) 2018 Kunal Mehta <legoktm@member.fsf.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. + * + */ + +/** + * @covers ExtensionJsonValidator + */ +class ExtensionJsonValidatorTest extends MediaWikiTestCase { + + /** + * @dataProvider provideValidate + */ + public function testValidate( $file, $expected ) { + // If a dependency is missing, skip this test. + $validator = new ExtensionJsonValidator( function ( $msg ) { + $this->markTestSkipped( $msg ); + } ); + + if ( is_string( $expected ) ) { + $this->setExpectedException( + ExtensionJsonValidationError::class, + $expected + ); + } + + $dir = __DIR__ . '/../../data/registration/'; + $this->assertSame( + $expected, + $validator->validate( $dir . $file ) + ); + } + + public function provideValidate() { + return [ + [ + 'notjson.txt', + 'notjson.txt is not valid JSON' + ], + [ + 'no_manifest_version.json', + 'no_manifest_version.json does not have manifest_version set.' + ], + [ + 'old_manifest_version.json', + 'old_manifest_version.json is using a non-supported schema version' + ], + [ + 'newer_manifest_version.json', + 'newer_manifest_version.json is using a non-supported schema version' + ], + [ + 'bad_spdx.json', + "bad_spdx.json did not pass validation. +[license-name] Invalid SPDX license identifier, see <https://spdx.org/licenses/>" + ], + [ + 'invalid.json', + "invalid.json did not pass validation. +[license-name] Array value found, but a string is required" + ], + [ + 'good.json', + true + ], + ]; + } + +} diff --git a/www/wiki/tests/phpunit/includes/registration/ExtensionProcessorTest.php b/www/wiki/tests/phpunit/includes/registration/ExtensionProcessorTest.php index 7b56def1..d9e091dc 100644 --- a/www/wiki/tests/phpunit/includes/registration/ExtensionProcessorTest.php +++ b/www/wiki/tests/phpunit/includes/registration/ExtensionProcessorTest.php @@ -2,6 +2,9 @@ use Wikimedia\TestingAccessWrapper; +/** + * @covers ExtensionProcessor + */ class ExtensionProcessorTest extends MediaWikiTestCase { private $dir, $dirname; @@ -21,9 +24,6 @@ class ExtensionProcessorTest extends MediaWikiTestCase { 'name' => 'FooBar', ]; - /** - * @covers ExtensionProcessor::extractInfo - */ public function testExtractInfo() { // Test that attributes that begin with @ are ignored $processor = new ExtensionProcessor(); @@ -31,6 +31,8 @@ class ExtensionProcessorTest extends MediaWikiTestCase { '@metadata' => [ 'foobarbaz' ], 'AnAttribute' => [ 'omg' ], 'AutoloadClasses' => [ 'FooBar' => 'includes/FooBar.php' ], + 'SpecialPages' => [ 'Foo' => 'SpecialFoo' ], + 'callback' => 'FooBar::onRegistration', ], 1 ); $extracted = $processor->getExtractedInfo(); @@ -38,12 +40,17 @@ class ExtensionProcessorTest extends MediaWikiTestCase { $this->assertArrayHasKey( 'AnAttribute', $attributes ); $this->assertArrayNotHasKey( '@metadata', $attributes ); $this->assertArrayNotHasKey( 'AutoloadClasses', $attributes ); + $this->assertSame( + [ 'FooBar' => 'FooBar::onRegistration' ], + $extracted['callbacks'] + ); + $this->assertSame( + [ 'Foo' => 'SpecialFoo' ], + $extracted['globals']['wgSpecialPages'] + ); } - /** - * @covers ExtensionProcessor::extractInfo - */ - public function testExtractInfo_namespaces() { + public function testExtractNamespaces() { // Test that namespace IDs can be overwritten if ( !defined( 'MW_EXTENSION_PROCESSOR_TEST_EXTRACT_INFO_X' ) ) { define( 'MW_EXTENSION_PROCESSOR_TEST_EXTRACT_INFO_X', 123456 ); @@ -56,13 +63,20 @@ class ExtensionProcessorTest extends MediaWikiTestCase { 'id' => 332200, 'constant' => 'MW_EXTENSION_PROCESSOR_TEST_EXTRACT_INFO_A', 'name' => 'Test_A', - 'content' => 'TestModel' + 'defaultcontentmodel' => 'TestModel', + 'gender' => [ + 'male' => 'Male test', + 'female' => 'Female test', + ], + 'subpages' => true, + 'content' => true, + 'protection' => 'userright', ], [ // Test_X will use ID 123456 not 334400 'id' => 334400, 'constant' => 'MW_EXTENSION_PROCESSOR_TEST_EXTRACT_INFO_X', 'name' => 'Test_X', - 'content' => 'TestModel' + 'defaultcontentmodel' => 'TestModel' ], ] ], 1 ); @@ -90,6 +104,13 @@ class ExtensionProcessorTest extends MediaWikiTestCase { $this->assertSame( 'Test_X', $extracted['attributes']['ExtensionNamespaces'][123456] ); $this->assertSame( 'Test_A', $extracted['attributes']['ExtensionNamespaces'][332200] ); + $this->assertSame( + [ 'male' => 'Male test', 'female' => 'Female test' ], + $extracted['globals']['wgExtraGenderNamespaces'][332200] + ); + // A has subpages, X does not + $this->assertTrue( $extracted['globals']['wgNamespacesWithSubpages'][332200] ); + $this->assertArrayNotHasKey( 123456, $extracted['globals']['wgNamespacesWithSubpages'] ); } public static function provideRegisterHooks() { @@ -152,7 +173,6 @@ class ExtensionProcessorTest extends MediaWikiTestCase { } /** - * @covers ExtensionProcessor::extractHooks * @dataProvider provideRegisterHooks */ public function testRegisterHooks( $pre, $info, $expected ) { @@ -162,9 +182,6 @@ class ExtensionProcessorTest extends MediaWikiTestCase { $this->assertEquals( $expected, $extracted['globals']['wgHooks'] ); } - /** - * @covers ExtensionProcessor::extractConfig1 - */ public function testExtractConfig1() { $processor = new ExtensionProcessor; $info = [ @@ -191,9 +208,6 @@ class ExtensionProcessorTest extends MediaWikiTestCase { $this->assertEquals( 'somevalue', $extracted['globals']['egBar'] ); } - /** - * @covers ExtensionProcessor::extractConfig2 - */ public function testExtractConfig2() { $processor = new ExtensionProcessor; $info = [ @@ -201,6 +215,13 @@ class ExtensionProcessorTest extends MediaWikiTestCase { 'Bar' => [ 'value' => 'somevalue' ], 'Foo' => [ 'value' => 10 ], 'Path' => [ 'value' => 'foo.txt', 'path' => true ], + 'Namespaces' => [ + 'value' => [ + '10' => true, + '12' => false, + ], + 'merge_strategy' => 'array_plus', + ], ], ] + self::$default; $info2 = [ @@ -218,6 +239,50 @@ class ExtensionProcessorTest extends MediaWikiTestCase { $this->assertEquals( "{$this->dirname}/foo.txt", $extracted['globals']['wgPath'] ); // Custom prefix: $this->assertEquals( 'somevalue', $extracted['globals']['egBar'] ); + $this->assertSame( + [ 10 => true, 12 => false, ExtensionRegistry::MERGE_STRATEGY => 'array_plus' ], + $extracted['globals']['wgNamespaces'] + ); + } + + /** + * @expectedException RuntimeException + */ + public function testDuplicateConfigKey1() { + $processor = new ExtensionProcessor; + $info = [ + 'config' => [ + 'Bar' => '', + ] + ] + self::$default; + $info2 = [ + 'config' => [ + 'Bar' => 'g', + ], + 'name' => 'FooBar2', + ]; + $processor->extractInfo( $this->dir, $info, 1 ); + $processor->extractInfo( $this->dir, $info2, 1 ); + } + + /** + * @expectedException RuntimeException + */ + public function testDuplicateConfigKey2() { + $processor = new ExtensionProcessor; + $info = [ + 'config' => [ + 'Bar' => [ 'value' => 'somevalue' ], + ] + ] + self::$default; + $info2 = [ + 'config' => [ + 'Bar' => [ 'value' => 'somevalue' ], + ], + 'name' => 'FooBar2', + ]; + $processor->extractInfo( $this->dir, $info, 2 ); + $processor->extractInfo( $this->dir, $info2, 2 ); } public static function provideExtractExtensionMessagesFiles() { @@ -245,7 +310,6 @@ class ExtensionProcessorTest extends MediaWikiTestCase { } /** - * @covers ExtensionProcessor::extractExtensionMessagesFiles * @dataProvider provideExtractExtensionMessagesFiles */ public function testExtractExtensionMessagesFiles( $input, $expected ) { @@ -272,7 +336,6 @@ class ExtensionProcessorTest extends MediaWikiTestCase { } /** - * @covers ExtensionProcessor::extractMessagesDirs * @dataProvider provideExtractMessagesDirs */ public function testExtractMessagesDirs( $input, $expected ) { @@ -284,18 +347,14 @@ class ExtensionProcessorTest extends MediaWikiTestCase { } } - /** - * @covers ExtensionProcessor::extractCredits - */ public function testExtractCredits() { $processor = new ExtensionProcessor(); $processor->extractInfo( $this->dir, self::$default, 1 ); - $this->setExpectedException( 'Exception' ); + $this->setExpectedException( Exception::class ); $processor->extractInfo( $this->dir, self::$default, 1 ); } /** - * @covers ExtensionProcessor::extractResourceLoaderModules * @dataProvider provideExtractResourceLoaderModules */ public function testExtractResourceLoaderModules( $input, $expected ) { @@ -338,8 +397,8 @@ class ExtensionProcessorTest extends MediaWikiTestCase { // Input [ 'ResourceFileModulePaths' => [ - 'localBasePath' => '', - 'remoteExtPath' => 'FooBar', + 'localBasePath' => 'modules', + 'remoteExtPath' => 'FooBar/modules', ], 'ResourceModules' => [ // No paths @@ -370,8 +429,8 @@ class ExtensionProcessorTest extends MediaWikiTestCase { 'wgResourceModules' => [ 'test.foo' => [ 'styles' => 'foo.js', - 'localBasePath' => $dir, - 'remoteExtPath' => 'FooBar', + 'localBasePath' => "$dir/modules", + 'remoteExtPath' => 'FooBar/modules', ], 'test.bar' => [ 'styles' => 'bar.js', @@ -381,14 +440,14 @@ class ExtensionProcessorTest extends MediaWikiTestCase { 'test.class' => [ 'class' => 'FooBarModule', 'extra' => 'argument', - 'localBasePath' => $dir, - 'remoteExtPath' => 'FooBar', + 'localBasePath' => "$dir/modules", + 'remoteExtPath' => 'FooBar/modules', ], 'test.class.with.path' => [ 'class' => 'FooBarPathModule', 'extra' => 'argument', 'localBasePath' => $dir, - 'remoteExtPath' => 'FooBar', + 'remoteExtPath' => 'FooBar/modules', ] ], ], @@ -501,9 +560,6 @@ class ExtensionProcessorTest extends MediaWikiTestCase { /** * Attributes under manifest_version 2 - * - * @covers ExtensionProcessor::extractAttributes - * @covers ExtensionProcessor::getExtractedInfo */ public function testExtractAttributes() { $processor = new ExtensionProcessor(); @@ -539,8 +595,6 @@ class ExtensionProcessorTest extends MediaWikiTestCase { /** * Attributes under manifest_version 1 - * - * @covers ExtensionProcessor::extractInfo */ public function testAttributes1() { $processor = new ExtensionProcessor(); @@ -557,14 +611,104 @@ class ExtensionProcessorTest extends MediaWikiTestCase { ], 1 ); + $processor->extractInfo( + $this->dir, + [ + 'name' => 'FooBar2', + 'FizzBuzzMorePlugins' => [ + 'ext.bar.fizzbuzz', + ] + ], + 1 + ); $info = $processor->getExtractedInfo(); $this->assertArrayHasKey( 'FooBarPlugins', $info['attributes'] ); $this->assertSame( [ 'ext.baz.foobar' ], $info['attributes']['FooBarPlugins'] ); $this->assertArrayHasKey( 'FizzBuzzMorePlugins', $info['attributes'] ); - $this->assertSame( [ 'ext.baz.fizzbuzz' ], $info['attributes']['FizzBuzzMorePlugins'] ); + $this->assertSame( + [ 'ext.baz.fizzbuzz', 'ext.bar.fizzbuzz' ], + $info['attributes']['FizzBuzzMorePlugins'] + ); + } + + public function testAttributes1_notarray() { + $processor = new ExtensionProcessor(); + $this->setExpectedException( + InvalidArgumentException::class, + "The value for 'FooBarPlugins' should be an array (from {$this->dir})" + ); + $processor->extractInfo( + $this->dir, + [ + 'FooBarPlugins' => 'ext.baz.foobar', + ] + self::$default, + 1 + ); } + public function testExtractPathBasedGlobal() { + $processor = new ExtensionProcessor(); + $processor->extractInfo( + $this->dir, + [ + 'ParserTestFiles' => [ + 'tests/parserTests.txt', + 'tests/extraParserTests.txt', + ], + 'ServiceWiringFiles' => [ + 'includes/ServiceWiring.php' + ], + ] + self::$default, + 1 + ); + $globals = $processor->getExtractedInfo()['globals']; + $this->assertArrayHasKey( 'wgParserTestFiles', $globals ); + $this->assertSame( [ + "{$this->dirname}/tests/parserTests.txt", + "{$this->dirname}/tests/extraParserTests.txt" + ], $globals['wgParserTestFiles'] ); + $this->assertArrayHasKey( 'wgServiceWiringFiles', $globals ); + $this->assertSame( [ + "{$this->dirname}/includes/ServiceWiring.php" + ], $globals['wgServiceWiringFiles'] ); + } + + public function testGetRequirements() { + $info = self::$default + [ + 'requires' => [ + 'MediaWiki' => '>= 1.25.0', + 'extensions' => [ + 'Bar' => '*' + ] + ] + ]; + $processor = new ExtensionProcessor(); + $this->assertSame( + $info['requires'], + $processor->getRequirements( $info ) + ); + $this->assertSame( + [], + $processor->getRequirements( [] ) + ); + } + + public function testGetExtraAutoloaderPaths() { + $processor = new ExtensionProcessor(); + $this->assertSame( + [ "{$this->dirname}/vendor/autoload.php" ], + $processor->getExtraAutoloaderPaths( $this->dirname, [ + 'load_composer_autoloader' => true, + ] ) + ); + } + + /** + * Verify that extension.schema.json is in sync with ExtensionProcessor + * + * @coversNothing + */ public function testGlobalSettingsDocumentedInSchema() { global $IP; $globalSettings = TestingAccessWrapper::newFromClass( diff --git a/www/wiki/tests/phpunit/includes/registration/ExtensionRegistryTest.php b/www/wiki/tests/phpunit/includes/registration/ExtensionRegistryTest.php index 9b57e1c3..67bc088d 100644 --- a/www/wiki/tests/phpunit/includes/registration/ExtensionRegistryTest.php +++ b/www/wiki/tests/phpunit/includes/registration/ExtensionRegistryTest.php @@ -1,9 +1,57 @@ <?php +/** + * @covers ExtensionRegistry + */ class ExtensionRegistryTest extends MediaWikiTestCase { + private $dataDir; + + public function setUp() { + parent::setUp(); + $this->dataDir = __DIR__ . '/../../data/registration'; + } + + public function testQueue_invalid() { + $registry = new ExtensionRegistry(); + $path = __DIR__ . '/doesnotexist.json'; + $this->setExpectedException( + Exception::class, + "$path does not exist!" + ); + $registry->queue( $path ); + } + + public function testQueue() { + $registry = new ExtensionRegistry(); + $path = "{$this->dataDir}/good.json"; + $registry->queue( $path ); + $this->assertArrayHasKey( + $path, + $registry->getQueue() + ); + $registry->clearQueue(); + $this->assertEmpty( $registry->getQueue() ); + } + + public function testLoadFromQueue_empty() { + $registry = new ExtensionRegistry(); + $registry->loadFromQueue(); + $this->assertEmpty( $registry->getAllThings() ); + } + + public function testLoadFromQueue_late() { + $registry = new ExtensionRegistry(); + $registry->finish(); + $registry->queue( "{$this->dataDir}/good.json" ); + $this->setExpectedException( + MWException::class, + "The following paths tried to load late: {$this->dataDir}/good.json" + ); + $registry->loadFromQueue(); + } + /** - * @covers ExtensionRegistry::exportExtractedData * @dataProvider provideExportExtractedDataGlobals */ public function testExportExtractedDataGlobals( $desc, $before, $globals, $expected ) { @@ -28,7 +76,7 @@ class ExtensionRegistryTest extends MediaWikiTestCase { 'autoloaderPaths' => [] ]; $registry = new ExtensionRegistry(); - $class = new ReflectionClass( 'ExtensionRegistry' ); + $class = new ReflectionClass( ExtensionRegistry::class ); $method = $class->getMethod( 'exportExtractedData' ); $method->setAccessible( true ); $method->invokeArgs( $registry, [ $info ] ); @@ -287,6 +335,18 @@ class ExtensionRegistryTest extends MediaWikiTestCase { ], ], ], + [ + 'global is null before', + [ + 'NullGlobal' => null, + ], + [ + 'NullGlobal' => 'not-null' + ], + [ + 'NullGlobal' => null + ], + ], ]; } } diff --git a/www/wiki/tests/phpunit/includes/registration/VersionCheckerTest.php b/www/wiki/tests/phpunit/includes/registration/VersionCheckerTest.php index 9ee58816..b668a9ad 100644 --- a/www/wiki/tests/phpunit/includes/registration/VersionCheckerTest.php +++ b/www/wiki/tests/phpunit/includes/registration/VersionCheckerTest.php @@ -3,7 +3,11 @@ /** * @covers VersionChecker */ -class VersionCheckerTest extends PHPUnit_Framework_TestCase { +class VersionCheckerTest extends PHPUnit\Framework\TestCase { + + use MediaWikiCoversValidator; + use PHPUnit4And6Compat; + /** * @dataProvider provideCheck */ @@ -13,8 +17,7 @@ class VersionCheckerTest extends PHPUnit_Framework_TestCase { 'FakeExtension' => [ 'MediaWiki' => $constraint, ], - ] ) - ); + ] ) ); } public static function provideCheck() { @@ -46,16 +49,15 @@ class VersionCheckerTest extends PHPUnit_Framework_TestCase { */ public function testType( $given, $expected ) { $checker = new VersionChecker( '1.0.0' ); - $checker - ->setLoadedExtensionsAndSkins( [ + $checker->setLoadedExtensionsAndSkins( [ 'FakeDependency' => [ 'version' => '1.0.0', ], + 'NoVersionGiven' => [], ] ); $this->assertEquals( $expected, $checker->checkArray( [ 'FakeExtension' => $given, - ] ) - ); + ] ) ); } public static function provideType() { @@ -64,16 +66,81 @@ class VersionCheckerTest extends PHPUnit_Framework_TestCase { [ [ 'extensions' => [ - 'FakeDependency' => '1.0.0' - ] + 'FakeDependency' => '1.0.0', + ], + ], + [], + ], + [ + [ + 'MediaWiki' => '1.0.0', ], - [] + [], ], [ [ - 'MediaWiki' => '1.0.0' + 'extensions' => [ + 'NoVersionGiven' => '*', + ], + ], + [], + ], + [ + [ + 'extensions' => [ + 'NoVersionGiven' => '1.0', + ], + ], + [ + [ + 'incompatible' => 'FakeExtension', + 'type' => 'incompatible-extensions', + 'msg' => 'NoVersionGiven does not expose its version, but FakeExtension requires: 1.0.', + ], + ], + ], + [ + [ + 'extensions' => [ + 'Missing' => '*', + ], + ], + [ + [ + 'missing' => 'Missing', + 'type' => 'missing-extensions', + 'msg' => 'FakeExtension requires Missing to be installed.', + ], + ], + ], + [ + [ + 'extensions' => [ + 'FakeDependency' => '2.0.0', + ], + ], + [ + [ + 'incompatible' => 'FakeExtension', + 'type' => 'incompatible-extensions', + // phpcs:ignore Generic.Files.LineLength.TooLong + 'msg' => 'FakeExtension is not compatible with the current installed version of FakeDependency (1.0.0), it requires: 2.0.0.', + ], + ], + ], + [ + [ + 'skins' => [ + 'FakeSkin' => '*', + ], + ], + [ + [ + 'missing' => 'FakeSkin', + 'type' => 'missing-skins', + 'msg' => 'FakeExtension requires FakeSkin to be installed.', + ], ], - [] ], ]; } @@ -84,35 +151,57 @@ class VersionCheckerTest extends PHPUnit_Framework_TestCase { */ public function testInvalidConstraint() { $checker = new VersionChecker( '1.0.0' ); - $checker - ->setLoadedExtensionsAndSkins( [ + $checker->setLoadedExtensionsAndSkins( [ 'FakeDependency' => [ 'version' => 'not really valid', ], ] ); - $this->assertEquals( [ "FakeDependency does not have a valid version string." ], - $checker->checkArray( [ - 'FakeExtension' => [ - 'extensions' => [ - 'FakeDependency' => '1.24.3', - ], + $this->assertEquals( [ + [ + 'type' => 'invalid-version', + 'msg' => "FakeDependency does not have a valid version string.", + ], + ], $checker->checkArray( [ + 'FakeExtension' => [ + 'extensions' => [ + 'FakeDependency' => '1.24.3', ], - ] ) - ); + ], + ] ) ); $checker = new VersionChecker( '1.0.0' ); - $checker - ->setLoadedExtensionsAndSkins( [ + $checker->setLoadedExtensionsAndSkins( [ 'FakeDependency' => [ 'version' => '1.24.3', ], ] ); - $this->setExpectedException( 'UnexpectedValueException' ); + $this->setExpectedException( UnexpectedValueException::class ); $checker->checkArray( [ 'FakeExtension' => [ 'FakeDependency' => 'not really valid', - ] + ], ] ); } + + /** + * T197478 + */ + public function testInvalidDependency() { + $checker = new VersionChecker( '1.0.0' ); + $this->setExpectedException( UnexpectedValueException::class, + 'Dependency type skin unknown in FakeExtension' ); + $this->assertEquals( [ + [ + 'type' => 'invalid-version', + 'msg' => 'FakeDependency does not have a valid version string.', + ], + ], $checker->checkArray( [ + 'FakeExtension' => [ + 'skin' => [ + 'FakeSkin' => '*', + ], + ], + ] ) ); + } } diff --git a/www/wiki/tests/phpunit/includes/resourceloader/DerivativeResourceLoaderContextTest.php b/www/wiki/tests/phpunit/includes/resourceloader/DerivativeResourceLoaderContextTest.php index 0be04efd..e4f58eb1 100644 --- a/www/wiki/tests/phpunit/includes/resourceloader/DerivativeResourceLoaderContextTest.php +++ b/www/wiki/tests/phpunit/includes/resourceloader/DerivativeResourceLoaderContextTest.php @@ -4,7 +4,9 @@ * @group ResourceLoader * @covers DerivativeResourceLoaderContext */ -class DerivativeResourceLoaderContextTest extends PHPUnit_Framework_TestCase { +class DerivativeResourceLoaderContextTest extends PHPUnit\Framework\TestCase { + + use MediaWikiCoversValidator; protected static function getContext() { $request = new FauxRequest( [ diff --git a/www/wiki/tests/phpunit/includes/resourceloader/MessageBlobStoreTest.php b/www/wiki/tests/phpunit/includes/resourceloader/MessageBlobStoreTest.php index cea1a560..7eb09441 100644 --- a/www/wiki/tests/phpunit/includes/resourceloader/MessageBlobStoreTest.php +++ b/www/wiki/tests/phpunit/includes/resourceloader/MessageBlobStoreTest.php @@ -6,13 +6,15 @@ use Wikimedia\TestingAccessWrapper; * @group Cache * @covers MessageBlobStore */ -class MessageBlobStoreTest extends PHPUnit_Framework_TestCase { +class MessageBlobStoreTest extends PHPUnit\Framework\TestCase { + + use MediaWikiCoversValidator; protected function setUp() { parent::setUp(); // MediaWiki tests defaults $wgMainWANCache to CACHE_NONE. // Use hash instead so that caching is observed - $this->wanCache = $this->getMockBuilder( 'WANObjectCache' ) + $this->wanCache = $this->getMockBuilder( WANObjectCache::class ) ->setConstructorArgs( [ [ 'cache' => new HashBagOStuff(), 'pool' => 'test', @@ -24,13 +26,17 @@ class MessageBlobStoreTest extends PHPUnit_Framework_TestCase { $this->wanCache->expects( $this->any() ) ->method( 'makePurgeValue' ) ->will( $this->returnCallback( function ( $timestamp, $holdoff ) { - // Disable holdoff as it messes with testing - return WANObjectCache::PURGE_VAL_PREFIX . (float)$timestamp . ':0'; + // Disable holdoff as it messes with testing. Aside from a 0-second holdoff, + // make sure that "time" passes between getMulti() check init and the set() + // in recacheMessageBlob(). This especially matters for Windows clocks. + $ts = (float)$timestamp - 0.0001; + + return WANObjectCache::PURGE_VAL_PREFIX . $ts . ':0'; } ) ); } protected function makeBlobStore( $methods = null, $rl = null ) { - $blobStore = $this->getMockBuilder( 'MessageBlobStore' ) + $blobStore = $this->getMockBuilder( MessageBlobStore::class ) ->setConstructorArgs( [ $rl ] ) ->setMethods( $methods ) ->getMock(); @@ -200,12 +206,16 @@ class MessageBlobStoreTest extends PHPUnit_Framework_TestCase { ->method( 'fetchMessage' ) ->will( $this->onConsecutiveCalls( 'First', 'Second' ) ); + $now = microtime( true ); + $this->wanCache->setMockTime( $now ); + $blob = $blobStore->getBlob( $module, 'en' ); $this->assertEquals( '{"example":"First"}', $blob, 'Generated blob' ); $blob = $blobStore->getBlob( $module, 'en' ); $this->assertEquals( '{"example":"First"}', $blob, 'Cache-hit' ); + $now += 1; $blobStore->clear(); $blob = $blobStore->getBlob( $module, 'en' ); diff --git a/www/wiki/tests/phpunit/includes/resourceloader/ResourceLoaderClientHtmlTest.php b/www/wiki/tests/phpunit/includes/resourceloader/ResourceLoaderClientHtmlTest.php index 3530d3c1..07956f1d 100644 --- a/www/wiki/tests/phpunit/includes/resourceloader/ResourceLoaderClientHtmlTest.php +++ b/www/wiki/tests/phpunit/includes/resourceloader/ResourceLoaderClientHtmlTest.php @@ -5,7 +5,9 @@ use Wikimedia\TestingAccessWrapper; /** * @group ResourceLoader */ -class ResourceLoaderClientHtmlTest extends PHPUnit_Framework_TestCase { +class ResourceLoaderClientHtmlTest extends PHPUnit\Framework\TestCase { + + use MediaWikiCoversValidator; protected static function expandVariables( $text ) { return strtr( $text, [ @@ -40,9 +42,8 @@ class ResourceLoaderClientHtmlTest extends PHPUnit_Framework_TestCase { protected static function makeSampleModules() { $modules = [ 'test' => [], - 'test.top' => [ 'position' => 'top' ], - 'test.private.top' => [ 'group' => 'private', 'position' => 'top' ], - 'test.private.bottom' => [ 'group' => 'private', 'position' => 'bottom' ], + 'test.private' => [ 'group' => 'private' ], + 'test.shouldembed.empty' => [ 'shouldEmbed' => true, 'isKnownEmpty' => true ], 'test.shouldembed' => [ 'shouldEmbed' => true ], 'test.styles.pure' => [ 'type' => ResourceLoaderModule::LOAD_STYLES ], @@ -72,7 +73,6 @@ class ResourceLoaderClientHtmlTest extends PHPUnit_Framework_TestCase { ], 'test.scripts' => [], - 'test.scripts.top' => [ 'position' => 'top' ], 'test.scripts.user' => [ 'group' => 'user' ], 'test.scripts.user.empty' => [ 'group' => 'user', 'isKnownEmpty' => true ], 'test.scripts.raw' => [ 'isRaw' => true ], @@ -112,9 +112,8 @@ class ResourceLoaderClientHtmlTest extends PHPUnit_Framework_TestCase { $client = new ResourceLoaderClientHtml( $context ); $client->setModules( [ 'test', - 'test.private.bottom', - 'test.private.top', - 'test.top', + 'test.private', + 'test.shouldembed.empty', 'test.shouldembed', 'test.unregistered', ] ); @@ -128,43 +127,41 @@ class ResourceLoaderClientHtmlTest extends PHPUnit_Framework_TestCase { ] ); $client->setModuleScripts( [ 'test.scripts', + 'test.scripts.user', 'test.scripts.user.empty', - 'test.scripts.top', 'test.scripts.shouldembed', 'test.unregistered.scripts', ] ); $expected = [ 'states' => [ - 'test.private.top' => 'loading', - 'test.private.bottom' => 'loading', + 'test.private' => 'loading', + 'test.shouldembed.empty' => 'ready', 'test.shouldembed' => 'loading', 'test.styles.pure' => 'ready', 'test.styles.user.empty' => 'ready', 'test.styles.private' => 'ready', 'test.styles.shouldembed' => 'ready', 'test.scripts' => 'loading', - 'test.scripts.top' => 'loading', + 'test.scripts.user' => 'loading', 'test.scripts.user.empty' => 'ready', 'test.scripts.shouldembed' => 'loading', ], 'general' => [ 'test', - 'test.top', ], 'styles' => [ 'test.styles.pure', ], 'scripts' => [ 'test.scripts', - 'test.scripts.top', + 'test.scripts.user', 'test.scripts.shouldembed', ], 'embed' => [ 'styles' => [ 'test.styles.private', 'test.styles.shouldembed' ], 'general' => [ - 'test.private.bottom', - 'test.private.top', + 'test.private', 'test.shouldembed', ], ], @@ -188,39 +185,77 @@ class ResourceLoaderClientHtmlTest extends PHPUnit_Framework_TestCase { $client = new ResourceLoaderClientHtml( $context ); $client->setConfig( [ 'key' => 'value' ] ); $client->setModules( [ - 'test.top', - 'test.private.top', + 'test', + 'test.private', ] ); $client->setModuleStyles( [ 'test.styles.pure', 'test.styles.private', ] ); $client->setModuleScripts( [ - 'test.scripts.top', + 'test.scripts', ] ); $client->setExemptStates( [ 'test.exempt' => 'ready', ] ); - // @codingStandardsIgnoreStart Generic.Files.LineLength + // phpcs:disable Generic.Files.LineLength $expected = '<script>document.documentElement.className = document.documentElement.className.replace( /(^|\s)client-nojs(\s|$)/, "$1client-js$2" );</script>' . "\n" . '<script>(window.RLQ=window.RLQ||[]).push(function(){' . 'mw.config.set({"key":"value"});' - . 'mw.loader.state({"test.exempt":"ready","test.private.top":"loading","test.styles.pure":"ready","test.styles.private":"ready","test.scripts.top":"loading"});' - . 'mw.loader.implement("test.private.top@{blankVer}",function($,jQuery,require,module){},{"css":[]});' - . 'mw.loader.load(["test.top"]);' - . 'mw.loader.load("/w/load.php?debug=false\u0026lang=nl\u0026modules=test.scripts.top\u0026only=scripts\u0026skin=fallback");' + . 'mw.loader.state({"test.exempt":"ready","test.private":"loading","test.styles.pure":"ready","test.styles.private":"ready","test.scripts":"loading"});' + . 'mw.loader.implement("test.private@{blankVer}",function($,jQuery,require,module){},{"css":[]});' + . 'mw.loader.load(["test"]);' + . 'mw.loader.load("/w/load.php?debug=false\u0026lang=nl\u0026modules=test.scripts\u0026only=scripts\u0026skin=fallback");' . '});</script>' . "\n" . '<link rel="stylesheet" href="/w/load.php?debug=false&lang=nl&modules=test.styles.pure&only=styles&skin=fallback"/>' . "\n" . '<style>.private{}</style>' . "\n" . '<script async="" src="/w/load.php?debug=false&lang=nl&modules=startup&only=scripts&skin=fallback"></script>'; - // @codingStandardsIgnoreEnd + // phpcs:enable $expected = self::expandVariables( $expected ); $this->assertEquals( $expected, $client->getHeadHtml() ); } /** + * Confirm that 'target' is passed down to the startup module's load url. + * + * @covers ResourceLoaderClientHtml::getHeadHtml + */ + public function testGetHeadHtmlWithTarget() { + $client = new ResourceLoaderClientHtml( + self::makeContext(), + [ 'target' => 'example' ] + ); + + // phpcs:disable Generic.Files.LineLength + $expected = '<script>document.documentElement.className = document.documentElement.className.replace( /(^|\s)client-nojs(\s|$)/, "$1client-js$2" );</script>' . "\n" + . '<script async="" src="/w/load.php?debug=false&lang=nl&modules=startup&only=scripts&skin=fallback&target=example"></script>'; + // phpcs:enable + + $this->assertEquals( $expected, $client->getHeadHtml() ); + } + + /** + * Confirm that a null 'target' is the same as no target. + * + * @covers ResourceLoaderClientHtml::getHeadHtml + */ + public function testGetHeadHtmlWithNullTarget() { + $client = new ResourceLoaderClientHtml( + self::makeContext(), + [ 'target' => null ] + ); + + // phpcs:disable Generic.Files.LineLength + $expected = '<script>document.documentElement.className = document.documentElement.className.replace( /(^|\s)client-nojs(\s|$)/, "$1client-js$2" );</script>' . "\n" + . '<script async="" src="/w/load.php?debug=false&lang=nl&modules=startup&only=scripts&skin=fallback"></script>'; + // phpcs:enable + + $this->assertEquals( $expected, $client->getHeadHtml() ); + } + + /** * @covers ResourceLoaderClientHtml::getBodyHtml * @covers ResourceLoaderClientHtml::getLoad */ @@ -245,8 +280,8 @@ class ResourceLoaderClientHtmlTest extends PHPUnit_Framework_TestCase { } public static function provideMakeLoad() { + // phpcs:disable Generic.Files.LineLength return [ - // @codingStandardsIgnoreStart Generic.Files.LineLength [ 'context' => [], 'modules' => [ 'test.unknown' ], @@ -261,9 +296,9 @@ class ResourceLoaderClientHtmlTest extends PHPUnit_Framework_TestCase { ], [ 'context' => [], - 'modules' => [ 'test.private.top' ], + 'modules' => [ 'test.private' ], 'only' => ResourceLoaderModule::TYPE_COMBINED, - 'output' => '<script>(window.RLQ=window.RLQ||[]).push(function(){mw.loader.implement("test.private.top@{blankVer}",function($,jQuery,require,module){},{"css":[]});});</script>', + 'output' => '<script>(window.RLQ=window.RLQ||[]).push(function(){mw.loader.implement("test.private@{blankVer}",function($,jQuery,require,module){},{"css":[]});});</script>', ], [ 'context' => [], @@ -273,6 +308,12 @@ class ResourceLoaderClientHtmlTest extends PHPUnit_Framework_TestCase { 'output' => '<script async="" src="/w/load.php?debug=false&lang=nl&modules=test.scripts.raw&only=scripts&skin=fallback"></script>', ], [ + 'context' => [ 'sync' => true ], + 'modules' => [ 'test.scripts.raw' ], + 'only' => ResourceLoaderModule::TYPE_SCRIPTS, + 'output' => '<script src="/w/load.php?debug=false&lang=nl&modules=test.scripts.raw&only=scripts&skin=fallback&sync=1"></script>', + ], + [ 'context' => [], 'modules' => [ 'test.scripts.user' ], 'only' => ResourceLoaderModule::TYPE_SCRIPTS, @@ -338,8 +379,8 @@ class ResourceLoaderClientHtmlTest extends PHPUnit_Framework_TestCase { . '<style>.orderingC{}.orderingD{}</style>' . "\n" . '<link rel="stylesheet" href="/w/load.php?debug=false&lang=nl&modules=test.ordering.e&only=styles&skin=fallback"/>' ], - // @codingStandardsIgnoreEnd ]; + // phpcs:enable } /** @@ -357,7 +398,7 @@ class ResourceLoaderClientHtmlTest extends PHPUnit_Framework_TestCase { public function testMakeLoad( array $extraQuery, array $modules, $type, $expected ) { $context = self::makeContext( $extraQuery ); $context->getResourceLoader()->register( self::makeSampleModules() ); - $actual = ResourceLoaderClientHtml::makeLoad( $context, $modules, $type ); + $actual = ResourceLoaderClientHtml::makeLoad( $context, $modules, $type, $extraQuery ); $expected = self::expandVariables( $expected ); $this->assertEquals( $expected, (string)$actual ); } diff --git a/www/wiki/tests/phpunit/includes/resourceloader/ResourceLoaderContextTest.php b/www/wiki/tests/phpunit/includes/resourceloader/ResourceLoaderContextTest.php index b658efba..b226ee1c 100644 --- a/www/wiki/tests/phpunit/includes/resourceloader/ResourceLoaderContextTest.php +++ b/www/wiki/tests/phpunit/includes/resourceloader/ResourceLoaderContextTest.php @@ -8,7 +8,10 @@ * @group Cache * @covers ResourceLoaderContext */ -class ResourceLoaderContextTest extends PHPUnit_Framework_TestCase { +class ResourceLoaderContextTest extends PHPUnit\Framework\TestCase { + + use MediaWikiCoversValidator; + protected static function getResourceLoader() { return new EmptyResourceLoader( new HashConfig( [ 'ResourceLoaderDebug' => false, diff --git a/www/wiki/tests/phpunit/includes/resourceloader/ResourceLoaderImageModuleTest.php b/www/wiki/tests/phpunit/includes/resourceloader/ResourceLoaderImageModuleTest.php index f53cd069..3f5704d6 100644 --- a/www/wiki/tests/phpunit/includes/resourceloader/ResourceLoaderImageModuleTest.php +++ b/www/wiki/tests/phpunit/includes/resourceloader/ResourceLoaderImageModuleTest.php @@ -59,7 +59,7 @@ class ResourceLoaderImageModuleTest extends ResourceLoaderTestCase { return [ [ [ - 'class' => 'ResourceLoaderImageModule', + 'class' => ResourceLoaderImageModule::class, 'prefix' => 'oo-ui-icon', 'variants' => self::$commonImageVariants, 'images' => self::$commonImageData, @@ -100,7 +100,7 @@ class ResourceLoaderImageModuleTest extends ResourceLoaderTestCase { ], [ [ - 'class' => 'ResourceLoaderImageModule', + 'class' => ResourceLoaderImageModule::class, 'selectorWithoutVariant' => '.mw-ui-icon-{name}:after, .mw-ui-icon-{name}:before', 'selectorWithVariant' => '.mw-ui-icon-{name}-{variant}:after, .mw-ui-icon-{name}-{variant}:before', @@ -207,7 +207,6 @@ class ResourceLoaderImageModuleTest extends ResourceLoaderTestCase { <<<TEXT background-image: url(rasterized.png); background-image: linear-gradient(transparent, transparent), url(original.svg); - background-image: -o-linear-gradient(transparent, transparent), url(rasterized.png); TEXT ], [ @@ -215,7 +214,6 @@ TEXT <<<TEXT background-image: url(rasterized.png); background-image: linear-gradient(transparent, transparent), url(data:image/svg+xml); - background-image: -o-linear-gradient(transparent, transparent), url(rasterized.png); TEXT ], @@ -241,7 +239,7 @@ TEXT } private function getImageMock( ResourceLoaderContext $context, $dataUriReturnValue ) { - $image = $this->getMockBuilder( 'ResourceLoaderImage' ) + $image = $this->getMockBuilder( ResourceLoaderImage::class ) ->disableOriginalConstructor() ->getMock(); $image->method( 'getDataUri' ) diff --git a/www/wiki/tests/phpunit/includes/resourceloader/ResourceLoaderImageTest.php b/www/wiki/tests/phpunit/includes/resourceloader/ResourceLoaderImageTest.php index aea27764..35c3ef64 100644 --- a/www/wiki/tests/phpunit/includes/resourceloader/ResourceLoaderImageTest.php +++ b/www/wiki/tests/phpunit/includes/resourceloader/ResourceLoaderImageTest.php @@ -15,8 +15,8 @@ class ResourceLoaderImageTest extends ResourceLoaderTestCase { protected function getTestImage( $name ) { $options = ResourceLoaderImageModuleTest::$commonImageData[$name]; $fileDescriptor = is_string( $options ) ? $options : $options['file']; - $allowedVariants = is_array( $options ) && - isset( $options['variants'] ) ? $options['variants'] : []; + $allowedVariants = ( is_array( $options ) && isset( $options['variants'] ) ) ? + $options['variants'] : []; $variants = array_fill_keys( $allowedVariants, [ 'color' => 'red' ] ); return new ResourceLoaderImageTestable( $name, @@ -39,7 +39,9 @@ class ResourceLoaderImageTest extends ResourceLoaderTestCase { [ 'mno', 'ar', 'mno-rtl.svg' ], [ 'mno', 'he', 'mno-ltr.svg' ], [ 'pqr', 'en', 'pqr-b.svg' ], + [ 'pqr', 'en-gb', 'pqr-b.svg' ], [ 'pqr', 'de', 'pqr-f.svg' ], + [ 'pqr', 'de-formal', 'pqr-f.svg' ], [ 'pqr', 'ar', 'pqr-f.svg' ], [ 'pqr', 'fr', 'pqr-a.svg' ], [ 'pqr', 'he', 'pqr-a.svg' ], @@ -53,7 +55,9 @@ class ResourceLoaderImageTest extends ResourceLoaderTestCase { public function testGetPath( $imageName, $languageCode, $path ) { static $dirMap = [ 'en' => 'ltr', + 'en-gb' => 'ltr', 'de' => 'ltr', + 'de-formal' => 'ltr', 'fr' => 'ltr', 'he' => 'rtl', 'ar' => 'rtl', diff --git a/www/wiki/tests/phpunit/includes/resourceloader/ResourceLoaderModuleTest.php b/www/wiki/tests/phpunit/includes/resourceloader/ResourceLoaderModuleTest.php index 7c7f1cf5..c917882a 100644 --- a/www/wiki/tests/phpunit/includes/resourceloader/ResourceLoaderModuleTest.php +++ b/www/wiki/tests/phpunit/includes/resourceloader/ResourceLoaderModuleTest.php @@ -4,6 +4,8 @@ class ResourceLoaderModuleTest extends ResourceLoaderTestCase { /** * @covers ResourceLoaderModule::getVersionHash + * @covers ResourceLoaderModule::getModifiedTime + * @covers ResourceLoaderModule::getModifiedHash */ public function testGetVersionHash() { $context = $this->getResourceLoaderContext(); @@ -149,9 +151,9 @@ class ResourceLoaderModuleTest extends ResourceLoaderTestCase { * @covers ResourceLoaderModule::expandRelativePaths */ public function testPlaceholderize() { - $getRelativePaths = new ReflectionMethod( 'ResourceLoaderModule', 'getRelativePaths' ); + $getRelativePaths = new ReflectionMethod( ResourceLoaderModule::class, 'getRelativePaths' ); $getRelativePaths->setAccessible( true ); - $expandRelativePaths = new ReflectionMethod( 'ResourceLoaderModule', 'expandRelativePaths' ); + $expandRelativePaths = new ReflectionMethod( ResourceLoaderModule::class, 'expandRelativePaths' ); $expandRelativePaths->setAccessible( true ); $this->setMwGlobals( [ diff --git a/www/wiki/tests/phpunit/includes/resourceloader/ResourceLoaderOOUIImageModuleTest.php b/www/wiki/tests/phpunit/includes/resourceloader/ResourceLoaderOOUIImageModuleTest.php index 491fff6b..ea220f11 100644 --- a/www/wiki/tests/phpunit/includes/resourceloader/ResourceLoaderOOUIImageModuleTest.php +++ b/www/wiki/tests/phpunit/includes/resourceloader/ResourceLoaderOOUIImageModuleTest.php @@ -10,7 +10,7 @@ class ResourceLoaderOOUIImageModuleTest extends ResourceLoaderTestCase { */ public function testNonDefaultSkin() { $module = new ResourceLoaderOOUIImageModule( [ - 'class' => 'ResourceLoaderOOUIImageModule', + 'class' => ResourceLoaderOOUIImageModule::class, 'name' => 'icons', 'rootPath' => 'tests/phpunit/data/resourceloader/oouiimagemodule', ] ); @@ -22,7 +22,7 @@ class ResourceLoaderOOUIImageModuleTest extends ResourceLoaderTestCase { function () { } ); - $r = new ReflectionMethod( 'ExtensionRegistry', 'exportExtractedData' ); + $r = new ReflectionMethod( ExtensionRegistry::class, 'exportExtractedData' ); $r->setAccessible( true ); $r->invoke( ExtensionRegistry::getInstance(), [ 'globals' => [], diff --git a/www/wiki/tests/phpunit/includes/resourceloader/ResourceLoaderSkinModuleTest.php b/www/wiki/tests/phpunit/includes/resourceloader/ResourceLoaderSkinModuleTest.php index c5676982..a1b14220 100644 --- a/www/wiki/tests/phpunit/includes/resourceloader/ResourceLoaderSkinModuleTest.php +++ b/www/wiki/tests/phpunit/includes/resourceloader/ResourceLoaderSkinModuleTest.php @@ -1,15 +1,18 @@ <?php /** - * @group Database * @group ResourceLoader */ -class ResourceLoaderSkinModuleTest extends PHPUnit_Framework_TestCase { +class ResourceLoaderSkinModuleTest extends PHPUnit\Framework\TestCase { + + use MediaWikiCoversValidator; public static function provideGetStyles() { + // phpcs:disable Generic.Files.LineLength return [ [ 'parent' => [], + 'logo' => '/logo.png', 'expected' => [ 'all' => [ '.mw-wiki-logo { background-image: url(/logo.png); }' ], ], @@ -18,12 +21,52 @@ class ResourceLoaderSkinModuleTest extends PHPUnit_Framework_TestCase { 'parent' => [ 'screen' => '.example {}', ], + 'logo' => '/logo.png', 'expected' => [ 'screen' => [ '.example {}' ], 'all' => [ '.mw-wiki-logo { background-image: url(/logo.png); }' ], ], ], + [ + 'parent' => [], + 'logo' => [ + '1x' => '/logo.png', + '1.5x' => '/logo@1.5x.png', + '2x' => '/logo@2x.png', + ], + 'expected' => [ + 'all' => [ <<<CSS +.mw-wiki-logo { background-image: url(/logo.png); } +CSS + ], + '(-webkit-min-device-pixel-ratio: 1.5), (min--moz-device-pixel-ratio: 1.5), (min-resolution: 1.5dppx), (min-resolution: 144dpi)' => [ <<<CSS +.mw-wiki-logo { background-image: url(/logo@1.5x.png);background-size: 135px auto; } +CSS + ], + '(-webkit-min-device-pixel-ratio: 2), (min--moz-device-pixel-ratio: 2), (min-resolution: 2dppx), (min-resolution: 192dpi)' => [ <<<CSS +.mw-wiki-logo { background-image: url(/logo@2x.png);background-size: 135px auto; } +CSS + ], + ], + ], + [ + 'parent' => [], + 'logo' => [ + '1x' => '/logo.png', + 'svg' => '/logo.svg', + ], + 'expected' => [ + 'all' => [ <<<CSS +.mw-wiki-logo { background-image: url(/logo.png); } +CSS + , <<<CSS +.mw-wiki-logo { background-image: -webkit-linear-gradient(transparent, transparent), url(/logo.svg); background-image: linear-gradient(transparent, transparent), url(/logo.svg);background-size: 135px auto; } +CSS + ], + ], + ], ]; + // phpcs:enable } /** @@ -31,25 +74,24 @@ class ResourceLoaderSkinModuleTest extends PHPUnit_Framework_TestCase { * @covers ResourceLoaderSkinModule::normalizeStyles * @covers ResourceLoaderSkinModule::getStyles */ - public function testGetStyles( $parent, $expected ) { + public function testGetStyles( $parent, $logo, $expected ) { $module = $this->getMockBuilder( ResourceLoaderSkinModule::class ) ->disableOriginalConstructor() - ->setMethods( [ 'readStyleFiles' ] ) + ->setMethods( [ 'readStyleFiles', 'getConfig', 'getLogoData' ] ) ->getMock(); $module->expects( $this->once() )->method( 'readStyleFiles' ) ->willReturn( $parent ); - $module->setConfig( new HashConfig( [ - 'ResourceBasePath' => '/w', - 'Logo' => '/logo.png', - 'LogoHD' => false, - ] ) ); + $module->expects( $this->once() )->method( 'getConfig' ) + ->willReturn( new HashConfig() ); + $module->expects( $this->once() )->method( 'getLogoData' ) + ->willReturn( $logo ); $ctx = $this->getMockBuilder( ResourceLoaderContext::class ) ->disableOriginalConstructor()->getMock(); $this->assertEquals( - $module->getStyles( $ctx ), - $expected + $expected, + $module->getStyles( $ctx ) ); } @@ -64,4 +106,102 @@ class ResourceLoaderSkinModuleTest extends PHPUnit_Framework_TestCase { $this->assertFalse( $module->isKnownEmpty( $ctx ) ); } + + /** + * @dataProvider provideGetLogo + * @covers ResourceLoaderSkinModule::getLogo + */ + public function testGetLogo( $config, $expected, $baseDir = null ) { + if ( $baseDir ) { + $oldIP = $GLOBALS['IP']; + $GLOBALS['IP'] = $baseDir; + $teardown = new Wikimedia\ScopedCallback( function () use ( $oldIP ) { + $GLOBALS['IP'] = $oldIP; + } ); + } + + $this->assertEquals( + $expected, + ResourceLoaderSkinModule::getLogo( new HashConfig( $config ) ) + ); + } + + public function provideGetLogo() { + return [ + 'simple' => [ + 'config' => [ + 'ResourceBasePath' => '/w', + 'Logo' => '/img/default.png', + 'LogoHD' => false, + ], + 'expected' => '/img/default.png', + ], + 'default and 2x' => [ + 'config' => [ + 'ResourceBasePath' => '/w', + 'Logo' => '/img/default.png', + 'LogoHD' => [ + '2x' => '/img/two-x.png', + ], + ], + 'expected' => [ + '1x' => '/img/default.png', + '2x' => '/img/two-x.png', + ], + ], + 'default and all HiDPIs' => [ + 'config' => [ + 'ResourceBasePath' => '/w', + 'Logo' => '/img/default.png', + 'LogoHD' => [ + '1.5x' => '/img/one-point-five.png', + '2x' => '/img/two-x.png', + ], + ], + 'expected' => [ + '1x' => '/img/default.png', + '1.5x' => '/img/one-point-five.png', + '2x' => '/img/two-x.png', + ], + ], + 'default and SVG' => [ + 'config' => [ + 'ResourceBasePath' => '/w', + 'Logo' => '/img/default.png', + 'LogoHD' => [ + 'svg' => '/img/vector.svg', + ], + ], + 'expected' => [ + '1x' => '/img/default.png', + 'svg' => '/img/vector.svg', + ], + ], + 'everything' => [ + 'config' => [ + 'ResourceBasePath' => '/w', + 'Logo' => '/img/default.png', + 'LogoHD' => [ + '1.5x' => '/img/one-point-five.png', + '2x' => '/img/two-x.png', + 'svg' => '/img/vector.svg', + ], + ], + 'expected' => [ + '1x' => '/img/default.png', + 'svg' => '/img/vector.svg', + ], + ], + 'versioned url' => [ + 'config' => [ + 'ResourceBasePath' => '/w', + 'Logo' => '/w/test.jpg', + 'LogoHD' => false, + 'UploadPath' => '/w/images', + ], + 'expected' => '/w/test.jpg?edcf2', + 'baseDir' => dirname( dirname( __DIR__ ) ) . '/data/media', + ], + ]; + } } diff --git a/www/wiki/tests/phpunit/includes/resourceloader/ResourceLoaderStartUpModuleTest.php b/www/wiki/tests/phpunit/includes/resourceloader/ResourceLoaderStartUpModuleTest.php index 03a609b6..564f50bc 100644 --- a/www/wiki/tests/phpunit/includes/resourceloader/ResourceLoaderStartUpModuleTest.php +++ b/www/wiki/tests/phpunit/includes/resourceloader/ResourceLoaderStartUpModuleTest.php @@ -56,7 +56,7 @@ mw.loader.register( [ 'msg' => 'Version falls back gracefully if getVersionHash throws', 'modules' => [ 'test.fail' => ( - ( $mock = $this->getMockBuilder( 'ResourceLoaderTestModule' ) + ( $mock = $this->getMockBuilder( ResourceLoaderTestModule::class ) ->setMethods( [ 'getVersionHash' ] )->getMock() ) && $mock->method( 'getVersionHash' )->will( $this->throwException( new Exception ) @@ -81,7 +81,7 @@ mw.loader.state( { 'msg' => 'Use version from getVersionHash', 'modules' => [ 'test.version' => ( - ( $mock = $this->getMockBuilder( 'ResourceLoaderTestModule' ) + ( $mock = $this->getMockBuilder( ResourceLoaderTestModule::class ) ->setMethods( [ 'getVersionHash' ] )->getMock() ) && $mock->method( 'getVersionHash' )->willReturn( '1234567' ) ) ? $mock : $mock @@ -101,7 +101,7 @@ mw.loader.register( [ 'msg' => 'Re-hash version from getVersionHash if too long', 'modules' => [ 'test.version' => ( - ( $mock = $this->getMockBuilder( 'ResourceLoaderTestModule' ) + ( $mock = $this->getMockBuilder( ResourceLoaderTestModule::class ) ->setMethods( [ 'getVersionHash' ] )->getMock() ) && $mock->method( 'getVersionHash' )->willReturn( '12345678' ) ) ? $mock : $mock diff --git a/www/wiki/tests/phpunit/includes/resourceloader/ResourceLoaderTest.php b/www/wiki/tests/phpunit/includes/resourceloader/ResourceLoaderTest.php index e9d022f6..4e9f5399 100644 --- a/www/wiki/tests/phpunit/includes/resourceloader/ResourceLoaderTest.php +++ b/www/wiki/tests/phpunit/includes/resourceloader/ResourceLoaderTest.php @@ -86,7 +86,7 @@ class ResourceLoaderTest extends ResourceLoaderTestCase { */ public function testRegisterInvalidName() { $resourceLoader = new EmptyResourceLoader(); - $this->setExpectedException( 'MWException', "name 'test!invalid' is invalid" ); + $this->setExpectedException( MWException::class, "name 'test!invalid' is invalid" ); $resourceLoader->register( 'test!invalid', new ResourceLoaderTestModule() ); } @@ -95,7 +95,7 @@ class ResourceLoaderTest extends ResourceLoaderTestCase { */ public function testRegisterInvalidType() { $resourceLoader = new EmptyResourceLoader(); - $this->setExpectedException( 'MWException', 'ResourceLoader module info type error' ); + $this->setExpectedException( MWException::class, 'ResourceLoader module info type error' ); $resourceLoader->register( 'test', new stdClass() ); } @@ -261,7 +261,7 @@ class ResourceLoaderTest extends ResourceLoaderTestCase { 'jquery.foo,bar|jquery.ui.baz,quux', ], [ - 'Regression fixed in r88706 with dotless names', + 'Regression fixed in r87497 (7fee86c38e) with dotless names', [ 'foo', 'bar', 'baz' ], 'foo,bar,baz', ], @@ -336,7 +336,9 @@ class ResourceLoaderTest extends ResourceLoaderTestCase { */ public function testAddSourceDupe() { $rl = new ResourceLoader; - $this->setExpectedException( 'MWException', 'ResourceLoader duplicate source addition error' ); + $this->setExpectedException( + MWException::class, 'ResourceLoader duplicate source addition error' + ); $rl->addSource( 'foo', 'https://example.org/w/load.php' ); $rl->addSource( 'foo', 'https://example.com/w/load.php' ); } @@ -346,7 +348,7 @@ class ResourceLoaderTest extends ResourceLoaderTestCase { */ public function testAddSourceInvalid() { $rl = new ResourceLoader; - $this->setExpectedException( 'MWException', 'with no "loadScript" key' ); + $this->setExpectedException( MWException::class, 'with no "loadScript" key' ); $rl->addSource( 'foo', [ 'x' => 'https://example.org/w/load.php' ] ); } @@ -446,7 +448,7 @@ mw.example(); ResourceLoader::clearCache(); $this->setMwGlobals( 'wgResourceLoaderDebug', true ); - $rl = TestingAccessWrapper::newFromClass( 'ResourceLoader' ); + $rl = TestingAccessWrapper::newFromClass( ResourceLoader::class ); $this->assertEquals( $case['expected'], $rl->makeLoaderImplementScript( @@ -465,8 +467,8 @@ mw.example(); * @covers ResourceLoader::makeLoaderImplementScript */ public function testMakeLoaderImplementScriptInvalid() { - $this->setExpectedException( 'MWException', 'Invalid scripts error' ); - $rl = TestingAccessWrapper::newFromClass( 'ResourceLoader' ); + $this->setExpectedException( MWException::class, 'Invalid scripts error' ); + $rl = TestingAccessWrapper::newFromClass( ResourceLoader::class ); $rl->makeLoaderImplementScript( 'test', // name 123, // scripts @@ -753,7 +755,7 @@ mw.example(); 'foo' => self::getSimpleModuleMock( 'foo();' ), 'ferry' => self::getFailFerryMock(), 'bar' => self::getSimpleModuleMock( 'bar();' ), - 'startup' => [ 'class' => 'ResourceLoaderStartUpModule' ], + 'startup' => [ 'class' => ResourceLoaderStartUpModule::class ], ] ); $context = $this->getResourceLoaderContext( [ @@ -869,4 +871,41 @@ mw.example(); 'Extra headers' ); } + + /** + * @covers ResourceLoader::respond + */ + public function testRespond() { + $rl = $this->getMockBuilder( EmptyResourceLoader::class ) + ->setMethods( [ + 'tryRespondNotModified', + 'sendResponseHeaders', + 'measureResponseTime', + ] ) + ->getMock(); + $context = $this->getResourceLoaderContext( [ 'modules' => '' ], $rl ); + + $rl->expects( $this->once() )->method( 'measureResponseTime' ); + $this->expectOutputRegex( '/no modules were requested/' ); + + $rl->respond( $context ); + } + + /** + * @covers ResourceLoader::measureResponseTime + */ + public function testMeasureResponseTime() { + $stats = $this->getMockBuilder( NullStatsdDataFactory::class ) + ->setMethods( [ 'timing' ] )->getMock(); + $this->setService( 'StatsdDataFactory', $stats ); + + $stats->expects( $this->once() )->method( 'timing' ) + ->with( 'resourceloader.responseTime', $this->anything() ); + + $timing = new Timing(); + $timing->mark( 'requestShutdown' ); + $rl = TestingAccessWrapper::newFromObject( new EmptyResourceLoader ); + $rl->measureResponseTime( $timing ); + DeferredUpdates::doUpdates(); + } } diff --git a/www/wiki/tests/phpunit/includes/resourceloader/ResourceLoaderWikiModuleTest.php b/www/wiki/tests/phpunit/includes/resourceloader/ResourceLoaderWikiModuleTest.php index 78eec6a6..0aa37d23 100644 --- a/www/wiki/tests/phpunit/includes/resourceloader/ResourceLoaderWikiModuleTest.php +++ b/www/wiki/tests/phpunit/includes/resourceloader/ResourceLoaderWikiModuleTest.php @@ -12,7 +12,7 @@ class ResourceLoaderWikiModuleTest extends ResourceLoaderTestCase { */ public function testConstructor( $params ) { $module = new ResourceLoaderWikiModule( $params ); - $this->assertInstanceOf( 'ResourceLoaderWikiModule', $module ); + $this->assertInstanceOf( ResourceLoaderWikiModule::class, $module ); } public static function provideConstructor() { @@ -97,7 +97,7 @@ class ResourceLoaderWikiModuleTest extends ResourceLoaderTestCase { * @dataProvider provideIsKnownEmpty */ public function testIsKnownEmpty( $titleInfo, $group, $expected ) { - $module = $this->getMockBuilder( 'ResourceLoaderWikiModule' ) + $module = $this->getMockBuilder( ResourceLoaderWikiModule::class ) ->setMethods( [ 'getTitleInfo', 'getGroup' ] ) ->getMock(); $module->expects( $this->any() ) @@ -106,7 +106,7 @@ class ResourceLoaderWikiModuleTest extends ResourceLoaderTestCase { $module->expects( $this->any() ) ->method( 'getGroup' ) ->will( $this->returnValue( $group ) ); - $context = $this->getMockBuilder( 'ResourceLoaderContext' ) + $context = $this->getMockBuilder( ResourceLoaderContext::class ) ->disableOriginalConstructor() ->getMock(); $this->assertEquals( $expected, $module->isKnownEmpty( $context ) ); @@ -157,14 +157,14 @@ class ResourceLoaderWikiModuleTest extends ResourceLoaderTestCase { ]; $expected = $titleInfo; - $module = $this->getMockBuilder( 'TestResourceLoaderWikiModule' ) + $module = $this->getMockBuilder( TestResourceLoaderWikiModule::class ) ->setMethods( [ 'getPages' ] ) ->getMock(); $module->method( 'getPages' )->willReturn( $pages ); // Can't mock static methods $module::$returnFetchTitleInfo = $titleInfo; - $context = $this->getMockBuilder( 'ResourceLoaderContext' ) + $context = $this->getMockBuilder( ResourceLoaderContext::class ) ->disableOriginalConstructor() ->getMock(); @@ -192,7 +192,7 @@ class ResourceLoaderWikiModuleTest extends ResourceLoaderTestCase { ]; $expected = $titleInfo; - $module = $this->getMockBuilder( 'TestResourceLoaderWikiModule' ) + $module = $this->getMockBuilder( TestResourceLoaderWikiModule::class ) ->setMethods( [ 'getPages' ] ) ->getMock(); $module->method( 'getPages' )->willReturn( $pages ); @@ -231,7 +231,7 @@ class ResourceLoaderWikiModuleTest extends ResourceLoaderTestCase { $titleInfo = []; // Set up objects - $module = $this->getMockBuilder( 'TestResourceLoaderWikiModule' ) + $module = $this->getMockBuilder( TestResourceLoaderWikiModule::class ) ->setMethods( [ 'getPages' ] ) ->getMock(); $module->method( 'getPages' )->willReturn( $pages ); $module::$returnFetchTitleInfo = $titleInfo; @@ -299,7 +299,7 @@ class ResourceLoaderWikiModuleTest extends ResourceLoaderTestCase { */ public function testGetContent( $expected, $title ) { $context = $this->getResourceLoaderContext( [], new EmptyResourceLoader ); - $module = $this->getMockBuilder( 'ResourceLoaderWikiModule' ) + $module = $this->getMockBuilder( ResourceLoaderWikiModule::class ) ->setMethods( [ 'getContentObj' ] ) ->getMock(); $module->expects( $this->any() ) ->method( 'getContentObj' )->willReturn( null ); @@ -331,7 +331,7 @@ class ResourceLoaderWikiModuleTest extends ResourceLoaderTestCase { public function testGetContentForRedirects() { // Set up context and module object $context = $this->getResourceLoaderContext( [], new EmptyResourceLoader ); - $module = $this->getMockBuilder( 'ResourceLoaderWikiModule' ) + $module = $this->getMockBuilder( ResourceLoaderWikiModule::class ) ->setMethods( [ 'getPages', 'getContentObj' ] ) ->getMock(); $module->expects( $this->any() ) diff --git a/www/wiki/tests/phpunit/includes/search/SearchEnginePrefixTest.php b/www/wiki/tests/phpunit/includes/search/SearchEnginePrefixTest.php index 4c5bab3f..3f59295a 100644 --- a/www/wiki/tests/phpunit/includes/search/SearchEnginePrefixTest.php +++ b/www/wiki/tests/phpunit/includes/search/SearchEnginePrefixTest.php @@ -63,8 +63,8 @@ class SearchEnginePrefixTest extends MediaWikiLangTestCase { $this->search = MediaWikiServices::getInstance()->newSearchEngine(); $this->search->setNamespaces( [] ); - $this->originalHandlers = TestingAccessWrapper::newFromClass( 'Hooks' )->handlers; - TestingAccessWrapper::newFromClass( 'Hooks' )->handlers = []; + $this->originalHandlers = TestingAccessWrapper::newFromClass( Hooks::class )->handlers; + TestingAccessWrapper::newFromClass( Hooks::class )->handlers = []; SpecialPageFactory::resetList(); } @@ -72,7 +72,7 @@ class SearchEnginePrefixTest extends MediaWikiLangTestCase { public function tearDown() { parent::tearDown(); - TestingAccessWrapper::newFromClass( 'Hooks' )->handlers = $this->originalHandlers; + TestingAccessWrapper::newFromClass( Hooks::class )->handlers = $this->originalHandlers; SpecialPageFactory::resetList(); } @@ -337,7 +337,7 @@ class SearchEnginePrefixTest extends MediaWikiLangTestCase { * @covers PrefixSearch::searchBackend */ public function testSearchBackend( array $case ) { - $search = $stub = $this->getMockBuilder( 'SearchEngine' ) + $search = $stub = $this->getMockBuilder( SearchEngine::class ) ->setMethods( [ 'completionSearchBackend' ] )->getMock(); $return = SearchSuggestionSet::fromStrings( $case['provision'] ); diff --git a/www/wiki/tests/phpunit/includes/search/SearchEngineTest.php b/www/wiki/tests/phpunit/includes/search/SearchEngineTest.php index 9711eabb..b7bc1530 100644 --- a/www/wiki/tests/phpunit/includes/search/SearchEngineTest.php +++ b/www/wiki/tests/phpunit/includes/search/SearchEngineTest.php @@ -32,7 +32,11 @@ class SearchEngineTest extends MediaWikiLangTestCase { $searchType = SearchEngineFactory::getSearchEngineClass( $this->db ); $this->setMwGlobals( [ - 'wgSearchType' => $searchType + 'wgSearchType' => $searchType, + 'wgCapitalLinks' => true, + 'wgCapitalLinkOverrides' => [ + NS_CATEGORY => false // for testCompletionSearchMustRespectCapitalLinkOverrides + ] ] ); $this->search = new $searchType( $this->db ); @@ -52,7 +56,13 @@ class SearchEngineTest extends MediaWikiLangTestCase { // Reset the search type back to default - some extensions may have // overridden it. - $this->setMwGlobals( [ 'wgSearchType' => null ] ); + $this->setMwGlobals( [ + 'wgSearchType' => null, + 'wgCapitalLinks' => true, + 'wgCapitalLinkOverrides' => [ + NS_CATEGORY => false // for testCompletionSearchMustRespectCapitalLinkOverrides + ] + ] ); $this->insertPage( 'Not_Main_Page', 'This is not a main page' ); $this->insertPage( @@ -74,6 +84,9 @@ class SearchEngineTest extends MediaWikiLangTestCase { $this->insertPage( 'HalfNumbers', '1234567890' ); $this->insertPage( 'FullNumbers', '1234567890' ); $this->insertPage( 'DomainName', 'example.com' ); + $this->insertPage( 'DomainName', 'example.com' ); + $this->insertPage( 'Category:search is not Search', '' ); + $this->insertPage( 'Category:Search is not search', '' ); } protected function fetchIds( $results ) { @@ -213,6 +226,48 @@ class SearchEngineTest extends MediaWikiLangTestCase { "Title power search" ); } + public function provideCompletionSearchMustRespectCapitalLinkOverrides() { + return [ + 'Searching for "smithee" finds Smithee on NS_MAIN' => [ + 'smithee', + 'Smithee', + [ NS_MAIN ], + ], + 'Searching for "search is" will finds "search is not Search" on NS_CATEGORY' => [ + 'search is', + 'Category:search is not Search', + [ NS_CATEGORY ], + ], + 'Searching for "Search is" will finds "search is not Search" on NS_CATEGORY' => [ + 'Search is', + 'Category:Search is not search', + [ NS_CATEGORY ], + ], + ]; + } + + /** + * Test that the search query is not munged using wrong CapitalLinks setup + * (in other test that the default search backend can benefit from wgCapitalLinksOverride) + * Guard against regressions like T208255 + * @dataProvider provideCompletionSearchMustRespectCapitalLinkOverrides + * @covers SearchEngine::completionSearch + * @covers PrefixSearch::defaultSearchBackend + * @param string $search + * @param string $expectedSuggestion + * @param int[] $namespaces + */ + public function testCompletionSearchMustRespectCapitalLinkOverrides( + $search, + $expectedSuggestion, + array $namespaces + ) { + $this->search->setNamespaces( $namespaces ); + $results = $this->search->completionSearch( $search ); + $this->assertEquals( 1, $results->getSize() ); + $this->assertEquals( $expectedSuggestion, $results->getSuggestions()[0]->getText() ); + } + /** * @covers SearchEngine::getSearchIndexFields */ @@ -220,12 +275,12 @@ class SearchEngineTest extends MediaWikiLangTestCase { /** * @var $mockEngine SearchEngine */ - $mockEngine = $this->getMockBuilder( 'SearchEngine' ) + $mockEngine = $this->getMockBuilder( SearchEngine::class ) ->setMethods( [ 'makeSearchFieldMapping' ] )->getMock(); $mockFieldBuilder = function ( $name, $type ) { $mockField = - $this->getMockBuilder( 'SearchIndexFieldDefinition' )->setConstructorArgs( [ + $this->getMockBuilder( SearchIndexFieldDefinition::class )->setConstructorArgs( [ $name, $type ] )->getMock(); @@ -258,7 +313,7 @@ class SearchEngineTest extends MediaWikiLangTestCase { $fields = $mockEngine->getSearchIndexFields(); $this->assertArrayHasKey( 'language', $fields ); $this->assertArrayHasKey( 'category', $fields ); - $this->assertInstanceOf( 'SearchIndexField', $fields['testField'] ); + $this->assertInstanceOf( SearchIndexField::class, $fields['testField'] ); $mapping = $fields['testField']->getMapping( $mockEngine ); $this->assertArrayHasKey( 'testData', $mapping ); @@ -287,7 +342,7 @@ class SearchEngineTest extends MediaWikiLangTestCase { } public function addAugmentors( &$setAugmentors, &$rowAugmentors ) { - $setAugmentor = $this->createMock( 'ResultSetAugmentor' ); + $setAugmentor = $this->createMock( ResultSetAugmentor::class ); $setAugmentor->expects( $this->once() ) ->method( 'augmentAll' ) ->willReturnCallback( function ( SearchResultSet $resultSet ) { @@ -301,7 +356,7 @@ class SearchEngineTest extends MediaWikiLangTestCase { } ); $setAugmentors['testSet'] = $setAugmentor; - $rowAugmentor = $this->createMock( 'ResultAugmentor' ); + $rowAugmentor = $this->createMock( ResultAugmentor::class ); $rowAugmentor->expects( $this->exactly( 2 ) ) ->method( 'augment' ) ->willReturnCallback( function ( SearchResult $result ) { diff --git a/www/wiki/tests/phpunit/includes/search/SearchSuggestionSetTest.php b/www/wiki/tests/phpunit/includes/search/SearchSuggestionSetTest.php index 28c69fa4..54533a73 100644 --- a/www/wiki/tests/phpunit/includes/search/SearchSuggestionSetTest.php +++ b/www/wiki/tests/phpunit/includes/search/SearchSuggestionSetTest.php @@ -19,7 +19,7 @@ * http://www.gnu.org/copyleft/gpl.html */ -class SearchSuggestionSetTest extends \PHPUnit_Framework_TestCase { +class SearchSuggestionSetTest extends \PHPUnit\Framework\TestCase { /** * Test that adding a new suggestion at the end * will keep proper score ordering diff --git a/www/wiki/tests/phpunit/includes/Services/ServiceContainerTest.php b/www/wiki/tests/phpunit/includes/services/ServiceContainerTest.php index b68ee48b..a760908f 100644 --- a/www/wiki/tests/phpunit/includes/Services/ServiceContainerTest.php +++ b/www/wiki/tests/phpunit/includes/services/ServiceContainerTest.php @@ -6,7 +6,10 @@ use MediaWiki\Services\ServiceContainer; * * @group MediaWiki */ -class ServiceContainerTest extends PHPUnit_Framework_TestCase { +class ServiceContainerTest extends PHPUnit\Framework\TestCase { + + use MediaWikiCoversValidator; + use PHPUnit4And6Compat; private function newServiceContainer( $extraArgs = [] ) { return new ServiceContainer( $extraArgs ); @@ -69,7 +72,7 @@ class ServiceContainerTest extends PHPUnit_Framework_TestCase { $name = 'TestService92834576'; - $this->setExpectedException( 'MediaWiki\Services\NoSuchServiceException' ); + $this->setExpectedException( MediaWiki\Services\NoSuchServiceException::class ); $services->getService( $name ); } @@ -111,7 +114,7 @@ class ServiceContainerTest extends PHPUnit_Framework_TestCase { $name = 'TestService92834576'; - $this->setExpectedException( 'MediaWiki\Services\NoSuchServiceException' ); + $this->setExpectedException( MediaWiki\Services\NoSuchServiceException::class ); $services->peekService( $name ); } @@ -141,7 +144,7 @@ class ServiceContainerTest extends PHPUnit_Framework_TestCase { return $theService; } ); - $this->setExpectedException( 'MediaWiki\Services\ServiceAlreadyDefinedException' ); + $this->setExpectedException( MediaWiki\Services\ServiceAlreadyDefinedException::class ); $services->defineService( $name, function () use ( $theService ) { return $theService; @@ -238,7 +241,7 @@ class ServiceContainerTest extends PHPUnit_Framework_TestCase { ]; // loading the same file twice should fail, because - $this->setExpectedException( 'MediaWiki\Services\ServiceAlreadyDefinedException' ); + $this->setExpectedException( MediaWiki\Services\ServiceAlreadyDefinedException::class ); $services->loadWiringFiles( $wiringFiles ); } @@ -296,7 +299,7 @@ class ServiceContainerTest extends PHPUnit_Framework_TestCase { $theService = new stdClass(); $name = 'TestService92834576'; - $this->setExpectedException( 'MediaWiki\Services\NoSuchServiceException' ); + $this->setExpectedException( MediaWiki\Services\NoSuchServiceException::class ); $services->redefineService( $name, function () use ( $theService ) { return $theService; @@ -316,7 +319,7 @@ class ServiceContainerTest extends PHPUnit_Framework_TestCase { // create the service, so it can no longer be redefined $services->getService( $name ); - $this->setExpectedException( 'MediaWiki\Services\CannotReplaceActiveServiceException' ); + $this->setExpectedException( MediaWiki\Services\CannotReplaceActiveServiceException::class ); $services->redefineService( $name, function () use ( $theService ) { return $theService; @@ -326,7 +329,7 @@ class ServiceContainerTest extends PHPUnit_Framework_TestCase { public function testDisableService() { $services = $this->newServiceContainer( [ 'Foo' ] ); - $destructible = $this->getMockBuilder( 'MediaWiki\Services\DestructibleService' ) + $destructible = $this->getMockBuilder( MediaWiki\Services\DestructibleService::class ) ->getMock(); $destructible->expects( $this->once() ) ->method( 'destroy' ); @@ -365,7 +368,7 @@ class ServiceContainerTest extends PHPUnit_Framework_TestCase { $this->assertContains( 'Bar', $services->getServiceNames() ); $this->assertContains( 'Qux', $services->getServiceNames() ); - $this->setExpectedException( 'MediaWiki\Services\ServiceDisabledException' ); + $this->setExpectedException( MediaWiki\Services\ServiceDisabledException::class ); $services->getService( 'Qux' ); } @@ -375,7 +378,7 @@ class ServiceContainerTest extends PHPUnit_Framework_TestCase { $theService = new stdClass(); $name = 'TestService92834576'; - $this->setExpectedException( 'MediaWiki\Services\NoSuchServiceException' ); + $this->setExpectedException( MediaWiki\Services\NoSuchServiceException::class ); $services->redefineService( $name, function () use ( $theService ) { return $theService; @@ -385,7 +388,7 @@ class ServiceContainerTest extends PHPUnit_Framework_TestCase { public function testDestroy() { $services = $this->newServiceContainer(); - $destructible = $this->getMockBuilder( 'MediaWiki\Services\DestructibleService' ) + $destructible = $this->getMockBuilder( MediaWiki\Services\DestructibleService::class ) ->getMock(); $destructible->expects( $this->once() ) ->method( 'destroy' ); @@ -404,7 +407,7 @@ class ServiceContainerTest extends PHPUnit_Framework_TestCase { // destroy the container $services->destroy(); - $this->setExpectedException( 'MediaWiki\Services\ContainerDisabledException' ); + $this->setExpectedException( MediaWiki\Services\ContainerDisabledException::class ); $services->getService( 'Bar' ); } diff --git a/www/wiki/tests/phpunit/includes/Services/TestWiring1.php b/www/wiki/tests/phpunit/includes/services/TestWiring1.php index b6ff4eb3..b6ff4eb3 100644 --- a/www/wiki/tests/phpunit/includes/Services/TestWiring1.php +++ b/www/wiki/tests/phpunit/includes/services/TestWiring1.php diff --git a/www/wiki/tests/phpunit/includes/Services/TestWiring2.php b/www/wiki/tests/phpunit/includes/services/TestWiring2.php index dfff64f0..dfff64f0 100644 --- a/www/wiki/tests/phpunit/includes/Services/TestWiring2.php +++ b/www/wiki/tests/phpunit/includes/services/TestWiring2.php diff --git a/www/wiki/tests/phpunit/includes/session/BotPasswordSessionProviderTest.php b/www/wiki/tests/phpunit/includes/session/BotPasswordSessionProviderTest.php index 90550d2b..47679940 100644 --- a/www/wiki/tests/phpunit/includes/session/BotPasswordSessionProviderTest.php +++ b/www/wiki/tests/phpunit/includes/session/BotPasswordSessionProviderTest.php @@ -184,7 +184,7 @@ class BotPasswordSessionProviderTest extends MediaWikiTestCase { public function testNewSessionInfoForRequest() { $provider = $this->getProvider(); $user = static::getTestSysop()->getUser(); - $request = $this->getMockBuilder( 'FauxRequest' ) + $request = $this->getMockBuilder( \FauxRequest::class ) ->setMethods( [ 'getIP' ] )->getMock(); $request->expects( $this->any() )->method( 'getIP' ) ->will( $this->returnValue( '127.0.0.1' ) ); @@ -212,7 +212,7 @@ class BotPasswordSessionProviderTest extends MediaWikiTestCase { $provider->setLogger( $logger ); $user = static::getTestSysop()->getUser(); - $request = $this->getMockBuilder( 'FauxRequest' ) + $request = $this->getMockBuilder( \FauxRequest::class ) ->setMethods( [ 'getIP' ] )->getMock(); $request->expects( $this->any() )->method( 'getIP' ) ->will( $this->returnValue( '127.0.0.1' ) ); @@ -264,7 +264,7 @@ class BotPasswordSessionProviderTest extends MediaWikiTestCase { ], $logger->getBuffer() ); $logger->clearBuffer(); - $request2 = $this->getMockBuilder( 'FauxRequest' ) + $request2 = $this->getMockBuilder( \FauxRequest::class ) ->setMethods( [ 'getIP' ] )->getMock(); $request2->expects( $this->any() )->method( 'getIP' ) ->will( $this->returnValue( '10.0.0.1' ) ); diff --git a/www/wiki/tests/phpunit/includes/session/CookieSessionProviderTest.php b/www/wiki/tests/phpunit/includes/session/CookieSessionProviderTest.php index a47fd9a1..c1df365a 100644 --- a/www/wiki/tests/phpunit/includes/session/CookieSessionProviderTest.php +++ b/www/wiki/tests/phpunit/includes/session/CookieSessionProviderTest.php @@ -157,7 +157,7 @@ class CookieSessionProviderTest extends MediaWikiTestCase { ); $msg = $provider->whyNoSession(); - $this->assertInstanceOf( 'Message', $msg ); + $this->assertInstanceOf( \Message::class, $msg ); $this->assertSame( 'sessionprovider-nocookies', $msg->getKey() ); } @@ -415,7 +415,7 @@ class CookieSessionProviderTest extends MediaWikiTestCase { ); TestingAccessWrapper::newFromObject( $backend )->usePhpSessionHandling = false; - $mock = $this->getMockBuilder( 'stdClass' ) + $mock = $this->getMockBuilder( stdClass::class ) ->setMethods( [ 'onUserSetCookies' ] ) ->getMock(); $mock->expects( $this->never() )->method( 'onUserSetCookies' ); @@ -563,14 +563,14 @@ class CookieSessionProviderTest extends MediaWikiTestCase { } protected function getSentRequest() { - $sentResponse = $this->getMockBuilder( 'FauxResponse' ) + $sentResponse = $this->getMockBuilder( \FauxResponse::class ) ->setMethods( [ 'headersSent', 'setCookie', 'header' ] )->getMock(); $sentResponse->expects( $this->any() )->method( 'headersSent' ) ->will( $this->returnValue( true ) ); $sentResponse->expects( $this->never() )->method( 'setCookie' ); $sentResponse->expects( $this->never() )->method( 'header' ); - $sentRequest = $this->getMockBuilder( 'FauxRequest' ) + $sentRequest = $this->getMockBuilder( \FauxRequest::class ) ->setMethods( [ 'response' ] )->getMock(); $sentRequest->expects( $this->any() )->method( 'response' ) ->will( $this->returnValue( $sentResponse ) ); @@ -608,7 +608,7 @@ class CookieSessionProviderTest extends MediaWikiTestCase { TestingAccessWrapper::newFromObject( $backend )->usePhpSessionHandling = false; // Anonymous user - $mock = $this->getMockBuilder( 'stdClass' ) + $mock = $this->getMockBuilder( stdClass::class ) ->setMethods( [ 'onUserSetCookies' ] )->getMock(); $mock->expects( $this->never() )->method( 'onUserSetCookies' ); $this->mergeMwGlobalArrayValue( 'wgHooks', [ 'UserSetCookies' => [ $mock ] ] ); diff --git a/www/wiki/tests/phpunit/includes/session/ImmutableSessionProviderWithCookieTest.php b/www/wiki/tests/phpunit/includes/session/ImmutableSessionProviderWithCookieTest.php index 086fa28b..6dd32fcd 100644 --- a/www/wiki/tests/phpunit/includes/session/ImmutableSessionProviderWithCookieTest.php +++ b/www/wiki/tests/phpunit/includes/session/ImmutableSessionProviderWithCookieTest.php @@ -91,7 +91,7 @@ class ImmutableSessionProviderWithCookieTest extends MediaWikiTestCase { $this->assertFalse( $provider->canChangeUser() ); $msg = $provider->whyNoSession(); - $this->assertInstanceOf( 'Message', $msg ); + $this->assertInstanceOf( \Message::class, $msg ); $this->assertSame( 'sessionprovider-nocookies', $msg->getKey() ); } @@ -158,7 +158,7 @@ class ImmutableSessionProviderWithCookieTest extends MediaWikiTestCase { } protected function getSentRequest() { - $sentResponse = $this->getMockBuilder( 'FauxResponse' ) + $sentResponse = $this->getMockBuilder( \FauxResponse::class ) ->setMethods( [ 'headersSent', 'setCookie', 'header' ] ) ->getMock(); $sentResponse->expects( $this->any() )->method( 'headersSent' ) @@ -166,7 +166,7 @@ class ImmutableSessionProviderWithCookieTest extends MediaWikiTestCase { $sentResponse->expects( $this->never() )->method( 'setCookie' ); $sentResponse->expects( $this->never() )->method( 'header' ); - $sentRequest = $this->getMockBuilder( 'FauxRequest' ) + $sentRequest = $this->getMockBuilder( \FauxRequest::class ) ->setMethods( [ 'response' ] )->getMock(); $sentRequest->expects( $this->any() )->method( 'response' ) ->will( $this->returnValue( $sentResponse ) ); diff --git a/www/wiki/tests/phpunit/includes/session/MetadataMergeExceptionTest.php b/www/wiki/tests/phpunit/includes/session/MetadataMergeExceptionTest.php index 0981f026..8cb4302a 100644 --- a/www/wiki/tests/phpunit/includes/session/MetadataMergeExceptionTest.php +++ b/www/wiki/tests/phpunit/includes/session/MetadataMergeExceptionTest.php @@ -14,7 +14,7 @@ class MetadataMergeExceptionTest extends MediaWikiTestCase { $data = [ 'foo' => 'bar' ]; $ex = new MetadataMergeException(); - $this->assertInstanceOf( 'UnexpectedValueException', $ex ); + $this->assertInstanceOf( \UnexpectedValueException::class, $ex ); $this->assertSame( [], $ex->getContext() ); $ex2 = new MetadataMergeException( 'Message', 42, $ex, $data ); diff --git a/www/wiki/tests/phpunit/includes/session/PHPSessionHandlerTest.php b/www/wiki/tests/phpunit/includes/session/PHPSessionHandlerTest.php index 0a2e84e1..045ba2f0 100644 --- a/www/wiki/tests/phpunit/includes/session/PHPSessionHandlerTest.php +++ b/www/wiki/tests/phpunit/includes/session/PHPSessionHandlerTest.php @@ -15,15 +15,6 @@ class PHPSessionHandlerTest extends MediaWikiTestCase { private function getResetter( &$rProp = null ) { $reset = []; - // Ignore "headers already sent" warnings during this test - set_error_handler( function ( $errno, $errstr ) use ( &$warnings ) { - if ( preg_match( '/headers already sent/', $errstr ) ) { - return true; - } - return false; - } ); - $reset[] = new \Wikimedia\ScopedCallback( 'restore_error_handler' ); - $rProp = new \ReflectionProperty( PHPSessionHandler::class, 'instance' ); $rProp->setAccessible( true ); if ( $rProp->getValue() ) { @@ -109,7 +100,7 @@ class PHPSessionHandlerTest extends MediaWikiTestCase { $reset[] = $this->getResetter( $rProp ); $this->setMwGlobals( [ - 'wgSessionProviders' => [ [ 'class' => 'DummySessionProvider' ] ], + 'wgSessionProviders' => [ [ 'class' => \DummySessionProvider::class ] ], 'wgObjectCacheSessionExpiry' => 2, ] ); @@ -130,9 +121,9 @@ class PHPSessionHandlerTest extends MediaWikiTestCase { ); $wrap->setEnableFlags( 'warn' ); - \MediaWiki\suppressWarnings(); + \Wikimedia\suppressWarnings(); ini_set( 'session.serialize_handler', $handler ); - \MediaWiki\restoreWarnings(); + \Wikimedia\restoreWarnings(); if ( ini_get( 'session.serialize_handler' ) !== $handler ) { $this->markTestSkipped( "Cannot set session.serialize_handler to \"$handler\"" ); } diff --git a/www/wiki/tests/phpunit/includes/session/SessionBackendTest.php b/www/wiki/tests/phpunit/includes/session/SessionBackendTest.php index e0d1c307..48c3d179 100644 --- a/www/wiki/tests/phpunit/includes/session/SessionBackendTest.php +++ b/www/wiki/tests/phpunit/includes/session/SessionBackendTest.php @@ -2,6 +2,7 @@ namespace MediaWiki\Session; +use Config; use MediaWikiTestCase; use User; use Wikimedia\TestingAccessWrapper; @@ -14,9 +15,16 @@ use Wikimedia\TestingAccessWrapper; class SessionBackendTest extends MediaWikiTestCase { const SESSIONID = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'; + /** @var SessionManager */ protected $manager; + + /** @var Config */ protected $config; + + /** @var SessionProvider */ protected $provider; + + /** @var TestBagOStuff */ protected $store; protected $onSessionMetadataCalled = false; @@ -25,6 +33,7 @@ class SessionBackendTest extends MediaWikiTestCase { * Returns a non-persistent backend that thinks it has at least one session active * @param User|null $user * @param string $id + * @return SessionBackend */ protected function getBackend( User $user = null, $id = null ) { if ( !$this->config ) { @@ -142,14 +151,14 @@ class SessionBackendTest extends MediaWikiTestCase { $this->assertSame( self::SESSIONID, $backend->getId() ); $this->assertSame( $id, $backend->getSessionId() ); $this->assertSame( $this->provider, $backend->getProvider() ); - $this->assertInstanceOf( 'User', $backend->getUser() ); + $this->assertInstanceOf( User::class, $backend->getUser() ); $this->assertSame( 'UTSysop', $backend->getUser()->getName() ); $this->assertSame( $info->wasPersisted(), $backend->isPersistent() ); $this->assertSame( $info->wasRemembered(), $backend->shouldRememberUser() ); $this->assertSame( $info->forceHTTPS(), $backend->shouldForceHTTPS() ); $expire = time() + 100; - $this->store->setSessionMeta( self::SESSIONID, [ 'expires' => $expire ], 2 ); + $this->store->setSessionMeta( self::SESSIONID, [ 'expires' => $expire ] ); $info = new SessionInfo( SessionInfo::MIN_PRIORITY, [ 'provider' => $this->provider, @@ -164,7 +173,7 @@ class SessionBackendTest extends MediaWikiTestCase { $this->assertSame( self::SESSIONID, $backend->getId() ); $this->assertSame( $id, $backend->getSessionId() ); $this->assertSame( $this->provider, $backend->getProvider() ); - $this->assertInstanceOf( 'User', $backend->getUser() ); + $this->assertInstanceOf( User::class, $backend->getUser() ); $this->assertTrue( $backend->getUser()->isAnon() ); $this->assertSame( $info->wasPersisted(), $backend->isPersistent() ); $this->assertSame( $info->wasRemembered(), $backend->shouldRememberUser() ); @@ -258,7 +267,7 @@ class SessionBackendTest extends MediaWikiTestCase { public function testResetId() { $id = session_id(); - $builder = $this->getMockBuilder( 'DummySessionProvider' ) + $builder = $this->getMockBuilder( \DummySessionProvider::class ) ->setMethods( [ 'persistsSessionId', 'sessionIdWasReset' ] ); $this->provider = $builder->getMock(); @@ -294,7 +303,7 @@ class SessionBackendTest extends MediaWikiTestCase { } public function testPersist() { - $this->provider = $this->getMockBuilder( 'DummySessionProvider' ) + $this->provider = $this->getMockBuilder( \DummySessionProvider::class ) ->setMethods( [ 'persistSession' ] )->getMock(); $this->provider->expects( $this->once() )->method( 'persistSession' ); $backend = $this->getBackend(); @@ -314,7 +323,7 @@ class SessionBackendTest extends MediaWikiTestCase { } public function testUnpersist() { - $this->provider = $this->getMockBuilder( 'DummySessionProvider' ) + $this->provider = $this->getMockBuilder( \DummySessionProvider::class ) ->setMethods( [ 'unpersistSession' ] )->getMock(); $this->provider->expects( $this->once() )->method( 'unpersistSession' ); $backend = $this->getBackend(); @@ -367,7 +376,7 @@ class SessionBackendTest extends MediaWikiTestCase { public function testSetUser() { $user = static::getTestSysop()->getUser(); - $this->provider = $this->getMockBuilder( 'DummySessionProvider' ) + $this->provider = $this->getMockBuilder( \DummySessionProvider::class ) ->setMethods( [ 'canChangeUser' ] )->getMock(); $this->provider->expects( $this->any() )->method( 'canChangeUser' ) ->will( $this->returnValue( false ) ); @@ -498,7 +507,7 @@ class SessionBackendTest extends MediaWikiTestCase { ->setMethods( [ 'onSessionMetadata' ] )->getMock(); $neverHook->expects( $this->never() )->method( 'onSessionMetadata' ); - $builder = $this->getMockBuilder( 'DummySessionProvider' ) + $builder = $this->getMockBuilder( \DummySessionProvider::class ) ->setMethods( [ 'persistSession', 'unpersistSession' ] ); $neverProvider = $builder->getMock(); @@ -746,7 +755,7 @@ class SessionBackendTest extends MediaWikiTestCase { $testData = [ 'foo' => 'foo!', 'bar', [ 'baz', null ] ]; // Not persistent - $this->provider = $this->getMockBuilder( 'DummySessionProvider' ) + $this->provider = $this->getMockBuilder( \DummySessionProvider::class ) ->setMethods( [ 'persistSession' ] )->getMock(); $this->provider->expects( $this->never() )->method( 'persistSession' ); $this->onSessionMetadataCalled = false; @@ -772,7 +781,7 @@ class SessionBackendTest extends MediaWikiTestCase { $this->assertNotEquals( 0, $wrap->expires ); // Persistent - $this->provider = $this->getMockBuilder( 'DummySessionProvider' ) + $this->provider = $this->getMockBuilder( \DummySessionProvider::class ) ->setMethods( [ 'persistSession' ] )->getMock(); $this->provider->expects( $this->atLeastOnce() )->method( 'persistSession' ); $this->onSessionMetadataCalled = false; @@ -799,7 +808,7 @@ class SessionBackendTest extends MediaWikiTestCase { $this->assertNotEquals( 0, $wrap->expires ); // Not persistent, not expiring - $this->provider = $this->getMockBuilder( 'DummySessionProvider' ) + $this->provider = $this->getMockBuilder( \DummySessionProvider::class ) ->setMethods( [ 'persistSession' ] )->getMock(); $this->provider->expects( $this->never() )->method( 'persistSession' ); $this->onSessionMetadataCalled = false; @@ -891,7 +900,7 @@ class SessionBackendTest extends MediaWikiTestCase { $manager->globalSessionRequest = $request; session_id( self::SESSIONID ); - \MediaWiki\quietCall( 'session_start' ); + \Wikimedia\quietCall( 'session_start' ); $_SESSION['foo'] = __METHOD__; $backend->resetId(); $this->assertNotEquals( self::SESSIONID, $backend->getId() ); @@ -929,9 +938,10 @@ class SessionBackendTest extends MediaWikiTestCase { $manager->globalSessionRequest = $request; session_id( self::SESSIONID . 'x' ); - \MediaWiki\quietCall( 'session_start' ); + \Wikimedia\quietCall( 'session_start' ); $backend->unpersist(); $this->assertSame( self::SESSIONID . 'x', session_id() ); + session_write_close(); session_id( self::SESSIONID ); $wrap->persist = true; @@ -940,7 +950,7 @@ class SessionBackendTest extends MediaWikiTestCase { } public function testGetAllowedUserRights() { - $this->provider = $this->getMockBuilder( 'DummySessionProvider' ) + $this->provider = $this->getMockBuilder( \DummySessionProvider::class ) ->setMethods( [ 'getAllowedUserRights' ] ) ->getMock(); $this->provider->expects( $this->any() )->method( 'getAllowedUserRights' ) diff --git a/www/wiki/tests/phpunit/includes/session/SessionManagerTest.php b/www/wiki/tests/phpunit/includes/session/SessionManagerTest.php index 9eb46bc3..b33cd24a 100644 --- a/www/wiki/tests/phpunit/includes/session/SessionManagerTest.php +++ b/www/wiki/tests/phpunit/includes/session/SessionManagerTest.php @@ -14,7 +14,14 @@ use Wikimedia\TestingAccessWrapper; */ class SessionManagerTest extends MediaWikiTestCase { - protected $config, $logger, $store; + /** @var \HashConfig */ + private $config; + + /** @var \TestLogger */ + private $logger; + + /** @var TestBagOStuff */ + private $store; protected function getManager() { \ObjectCache::$instances['testSessionStore'] = new TestBagOStuff(); @@ -23,7 +30,7 @@ class SessionManagerTest extends MediaWikiTestCase { 'SessionCacheType' => 'testSessionStore', 'ObjectCacheSessionExpiry' => 100, 'SessionProviders' => [ - [ 'class' => 'DummySessionProvider' ], + [ 'class' => \DummySessionProvider::class ], ] ] ); $this->logger = new \TestLogger( false, function ( $m ) { @@ -75,6 +82,7 @@ class SessionManagerTest extends MediaWikiTestCase { $context->setRequest( $request ); $id = $request->getSession()->getId(); + session_write_close(); session_id( '' ); $session = SessionManager::getGlobalSession(); $this->assertSame( $id, $session->getId() ); @@ -138,7 +146,7 @@ class SessionManagerTest extends MediaWikiTestCase { $id2 = ''; $idEmpty = 'empty-session-------------------'; - $providerBuilder = $this->getMockBuilder( 'DummySessionProvider' ) + $providerBuilder = $this->getMockBuilder( \DummySessionProvider::class ) ->setMethods( [ 'provideSessionInfo', 'newSessionInfo', '__toString', 'describe', 'unpersistSession' ] ); @@ -397,7 +405,7 @@ class SessionManagerTest extends MediaWikiTestCase { // Failure to create an empty session $manager = $this->getManager(); - $provider = $this->getMockBuilder( 'DummySessionProvider' ) + $provider = $this->getMockBuilder( \DummySessionProvider::class ) ->setMethods( [ 'provideSessionInfo', 'newSessionInfo', '__toString' ] ) ->getMock(); $provider->expects( $this->any() )->method( 'provideSessionInfo' ) @@ -422,7 +430,7 @@ class SessionManagerTest extends MediaWikiTestCase { $pmanager = TestingAccessWrapper::newFromObject( $manager ); $request = new \FauxRequest(); - $providerBuilder = $this->getMockBuilder( 'DummySessionProvider' ) + $providerBuilder = $this->getMockBuilder( \DummySessionProvider::class ) ->setMethods( [ 'provideSessionInfo', 'newSessionInfo', '__toString' ] ); $expectId = null; @@ -646,7 +654,7 @@ class SessionManagerTest extends MediaWikiTestCase { $user = User::newFromName( 'UTSysop' ); $manager = $this->getManager(); - $providerBuilder = $this->getMockBuilder( 'DummySessionProvider' ) + $providerBuilder = $this->getMockBuilder( \DummySessionProvider::class ) ->setMethods( [ 'invalidateSessionsForUser', '__toString' ] ); $provider1 = $providerBuilder->getMock(); @@ -674,7 +682,7 @@ class SessionManagerTest extends MediaWikiTestCase { public function testGetVaryHeaders() { $manager = $this->getManager(); - $providerBuilder = $this->getMockBuilder( 'DummySessionProvider' ) + $providerBuilder = $this->getMockBuilder( \DummySessionProvider::class ) ->setMethods( [ 'getVaryHeaders', '__toString' ] ); $provider1 = $providerBuilder->getMock(); @@ -718,7 +726,7 @@ class SessionManagerTest extends MediaWikiTestCase { public function testGetVaryCookies() { $manager = $this->getManager(); - $providerBuilder = $this->getMockBuilder( 'DummySessionProvider' ) + $providerBuilder = $this->getMockBuilder( \DummySessionProvider::class ) ->setMethods( [ 'getVaryCookies', '__toString' ] ); $provider1 = $providerBuilder->getMock(); @@ -751,7 +759,7 @@ class SessionManagerTest extends MediaWikiTestCase { $manager = TestingAccessWrapper::newFromObject( $realManager ); $this->config->set( 'SessionProviders', [ - [ 'class' => 'DummySessionProvider' ], + [ 'class' => \DummySessionProvider::class ], ] ); $providers = $manager->getProviders(); $this->assertArrayHasKey( 'DummySessionProvider', $providers ); @@ -761,8 +769,8 @@ class SessionManagerTest extends MediaWikiTestCase { $this->assertSame( $realManager, $provider->getManager() ); $this->config->set( 'SessionProviders', [ - [ 'class' => 'DummySessionProvider' ], - [ 'class' => 'DummySessionProvider' ], + [ 'class' => \DummySessionProvider::class ], + [ 'class' => \DummySessionProvider::class ], ] ); $manager->sessionProviders = null; try { @@ -780,7 +788,7 @@ class SessionManagerTest extends MediaWikiTestCase { $manager = TestingAccessWrapper::newFromObject( $this->getManager() ); $manager->setLogger( new \Psr\Log\NullLogger() ); - $mock = $this->getMockBuilder( 'stdClass' ) + $mock = $this->getMockBuilder( stdClass::class ) ->setMethods( [ 'shutdown' ] )->getMock(); $mock->expects( $this->once() )->method( 'shutdown' ); @@ -871,7 +879,7 @@ class SessionManagerTest extends MediaWikiTestCase { public function testPreventSessionsForUser() { $manager = $this->getManager(); - $providerBuilder = $this->getMockBuilder( 'DummySessionProvider' ) + $providerBuilder = $this->getMockBuilder( \DummySessionProvider::class ) ->setMethods( [ 'preventSessionsForUser', '__toString' ] ); $provider1 = $providerBuilder->getMock(); diff --git a/www/wiki/tests/phpunit/includes/session/SessionTest.php b/www/wiki/tests/phpunit/includes/session/SessionTest.php index adf0f5db..f84d435f 100644 --- a/www/wiki/tests/phpunit/includes/session/SessionTest.php +++ b/www/wiki/tests/phpunit/includes/session/SessionTest.php @@ -365,9 +365,9 @@ class SessionTest extends MediaWikiTestCase { $hmac = hash_hmac( 'sha256', $sealed, $hmacKey, true ); $encrypted = base64_encode( $hmac ) . '.' . $sealed; $session->set( 'test', $encrypted ); - \MediaWiki\suppressWarnings(); + \Wikimedia\suppressWarnings(); $this->assertEquals( 'defaulted', $session->getSecret( 'test', 'defaulted' ) ); - \MediaWiki\restoreWarnings(); + \Wikimedia\restoreWarnings(); } } diff --git a/www/wiki/tests/phpunit/includes/session/TestBagOStuff.php b/www/wiki/tests/phpunit/includes/session/TestBagOStuff.php index fd02a2e9..f9e30f06 100644 --- a/www/wiki/tests/phpunit/includes/session/TestBagOStuff.php +++ b/www/wiki/tests/phpunit/includes/session/TestBagOStuff.php @@ -14,53 +14,44 @@ class TestBagOStuff extends \CachedBagOStuff { /** * @param string $id Session ID * @param array $data Session data - * @param int $expiry Expiry - * @param User $user User for metadata */ - public function setSessionData( $id, array $data, $expiry = 0, User $user = null ) { - $this->setSession( $id, [ 'data' => $data ], $expiry, $user ); + public function setSessionData( $id, array $data ) { + $this->setSession( $id, [ 'data' => $data ] ); } /** * @param string $id Session ID * @param array $metadata Session metadata - * @param int $expiry Expiry */ - public function setSessionMeta( $id, array $metadata, $expiry = 0 ) { - $this->setSession( $id, [ 'metadata' => $metadata ], $expiry ); + public function setSessionMeta( $id, array $metadata ) { + $this->setSession( $id, [ 'metadata' => $metadata ] ); } /** * @param string $id Session ID * @param array $blob Session metadata and data - * @param int $expiry Expiry - * @param User $user User for metadata */ - public function setSession( $id, array $blob, $expiry = 0, User $user = null ) { + public function setSession( $id, array $blob ) { $blob += [ 'data' => [], 'metadata' => [], ]; $blob['metadata'] += [ - 'userId' => $user ? $user->getId() : 0, - 'userName' => $user ? $user->getName() : null, - 'userToken' => $user ? $user->getToken( true ) : null, + 'userId' => 0, + 'userName' => null, + 'userToken' => null, 'provider' => 'DummySessionProvider', ]; - $this->setRawSession( $id, $blob, $expiry, $user ); + $this->setRawSession( $id, $blob ); } /** * @param string $id Session ID * @param array|mixed $blob Session metadata and data - * @param int $expiry Expiry */ - public function setRawSession( $id, $blob, $expiry = 0 ) { - if ( $expiry <= 0 ) { - $expiry = \RequestContext::getMain()->getConfig()->get( 'ObjectCacheSessionExpiry' ); - } - + public function setRawSession( $id, $blob ) { + $expiry = \RequestContext::getMain()->getConfig()->get( 'ObjectCacheSessionExpiry' ); $this->set( $this->makeKey( 'MWSession', $id ), $blob, $expiry ); } diff --git a/www/wiki/tests/phpunit/includes/session/TestUtils.php b/www/wiki/tests/phpunit/includes/session/TestUtils.php index af29d6bd..5db1ad0e 100644 --- a/www/wiki/tests/phpunit/includes/session/TestUtils.php +++ b/www/wiki/tests/phpunit/includes/session/TestUtils.php @@ -79,7 +79,7 @@ class TestUtils { * If you need a Session for testing but don't want to create a backend to * construct one, use this. * @param object $backend Object to serve as the SessionBackend - * @param int $index Index + * @param int $index * @param LoggerInterface $logger * @return Session */ diff --git a/www/wiki/tests/phpunit/includes/session/UserInfoTest.php b/www/wiki/tests/phpunit/includes/session/UserInfoTest.php index c38edd69..4d79a956 100644 --- a/www/wiki/tests/phpunit/includes/session/UserInfoTest.php +++ b/www/wiki/tests/phpunit/includes/session/UserInfoTest.php @@ -41,7 +41,7 @@ class UserInfoTest extends MediaWikiTestCase { $this->assertSame( $user->getId(), $userinfo->getId() ); $this->assertSame( $user->getName(), $userinfo->getName() ); $this->assertSame( $user->getToken( true ), $userinfo->getToken() ); - $this->assertInstanceOf( 'User', $userinfo->getUser() ); + $this->assertInstanceOf( User::class, $userinfo->getUser() ); $userinfo2 = $userinfo->verified(); $this->assertNotSame( $userinfo2, $userinfo ); $this->assertSame( "<-:{$user->getId()}:{$user->getName()}>", (string)$userinfo ); @@ -51,7 +51,7 @@ class UserInfoTest extends MediaWikiTestCase { $this->assertSame( $user->getId(), $userinfo2->getId() ); $this->assertSame( $user->getName(), $userinfo2->getName() ); $this->assertSame( $user->getToken( true ), $userinfo2->getToken() ); - $this->assertInstanceOf( 'User', $userinfo2->getUser() ); + $this->assertInstanceOf( User::class, $userinfo2->getUser() ); $this->assertSame( $userinfo2, $userinfo2->verified() ); $this->assertSame( "<+:{$user->getId()}:{$user->getName()}>", (string)$userinfo2 ); @@ -76,7 +76,7 @@ class UserInfoTest extends MediaWikiTestCase { $this->assertSame( $user->getId(), $userinfo->getId() ); $this->assertSame( $user->getName(), $userinfo->getName() ); $this->assertSame( $user->getToken( true ), $userinfo->getToken() ); - $this->assertInstanceOf( 'User', $userinfo->getUser() ); + $this->assertInstanceOf( User::class, $userinfo->getUser() ); $userinfo2 = $userinfo->verified(); $this->assertNotSame( $userinfo2, $userinfo ); $this->assertSame( "<-:{$user->getId()}:{$user->getName()}>", (string)$userinfo ); @@ -86,7 +86,7 @@ class UserInfoTest extends MediaWikiTestCase { $this->assertSame( $user->getId(), $userinfo2->getId() ); $this->assertSame( $user->getName(), $userinfo2->getName() ); $this->assertSame( $user->getToken( true ), $userinfo2->getToken() ); - $this->assertInstanceOf( 'User', $userinfo2->getUser() ); + $this->assertInstanceOf( User::class, $userinfo2->getUser() ); $this->assertSame( $userinfo2, $userinfo2->verified() ); $this->assertSame( "<+:{$user->getId()}:{$user->getName()}>", (string)$userinfo2 ); @@ -103,7 +103,7 @@ class UserInfoTest extends MediaWikiTestCase { $this->assertSame( $user->getId(), $userinfo->getId() ); $this->assertSame( $user->getName(), $userinfo->getName() ); $this->assertSame( '', $userinfo->getToken() ); - $this->assertInstanceOf( 'User', $userinfo->getUser() ); + $this->assertInstanceOf( User::class, $userinfo->getUser() ); $userinfo2 = $userinfo->verified(); $this->assertNotSame( $userinfo2, $userinfo ); $this->assertSame( "<-:{$user->getId()}:{$user->getName()}>", (string)$userinfo ); @@ -113,7 +113,7 @@ class UserInfoTest extends MediaWikiTestCase { $this->assertSame( $user->getId(), $userinfo2->getId() ); $this->assertSame( $user->getName(), $userinfo2->getName() ); $this->assertSame( '', $userinfo2->getToken() ); - $this->assertInstanceOf( 'User', $userinfo2->getUser() ); + $this->assertInstanceOf( User::class, $userinfo2->getUser() ); $this->assertSame( $userinfo2, $userinfo2->verified() ); $this->assertSame( "<+:{$user->getId()}:{$user->getName()}>", (string)$userinfo2 ); diff --git a/www/wiki/tests/phpunit/includes/shell/CommandFactoryTest.php b/www/wiki/tests/phpunit/includes/shell/CommandFactoryTest.php index aacfd43c..b031431a 100644 --- a/www/wiki/tests/phpunit/includes/shell/CommandFactoryTest.php +++ b/www/wiki/tests/phpunit/includes/shell/CommandFactoryTest.php @@ -1,13 +1,18 @@ <?php +use MediaWiki\Shell\Command; use MediaWiki\Shell\CommandFactory; +use MediaWiki\Shell\FirejailCommand; use Psr\Log\NullLogger; use Wikimedia\TestingAccessWrapper; /** * @group Shell */ -class CommandFactoryTest extends PHPUnit_Framework_TestCase { +class CommandFactoryTest extends PHPUnit\Framework\TestCase { + + use MediaWikiCoversValidator; + /** * @covers MediaWiki\Shell\CommandFactory::create */ @@ -21,13 +26,25 @@ class CommandFactoryTest extends PHPUnit_Framework_TestCase { 'walltime' => 40, ]; - $factory = new CommandFactory( $limits, $cgroup ); + $factory = new CommandFactory( $limits, $cgroup, false ); $factory->setLogger( $logger ); + $factory->logStderr(); $command = $factory->create(); + $this->assertInstanceOf( Command::class, $command ); $wrapper = TestingAccessWrapper::newFromObject( $command ); $this->assertSame( $logger, $wrapper->logger ); $this->assertSame( $cgroup, $wrapper->cgroup ); $this->assertSame( $limits, $wrapper->limits ); + $this->assertTrue( $wrapper->doLogStderr ); + } + + /** + * @covers MediaWiki\Shell\CommandFactory::create + */ + public function testFirejailCreate() { + $factory = new CommandFactory( [], false, 'firejail' ); + $factory->setLogger( new NullLogger() ); + $this->assertInstanceOf( FirejailCommand::class, $factory->create() ); } } diff --git a/www/wiki/tests/phpunit/includes/shell/CommandTest.php b/www/wiki/tests/phpunit/includes/shell/CommandTest.php index d57b1b1c..2e031638 100644 --- a/www/wiki/tests/phpunit/includes/shell/CommandTest.php +++ b/www/wiki/tests/phpunit/includes/shell/CommandTest.php @@ -4,9 +4,13 @@ use MediaWiki\Shell\Command; use Wikimedia\TestingAccessWrapper; /** + * @covers \MediaWiki\Shell\Command * @group Shell */ -class CommandTest extends PHPUnit_Framework_TestCase { +class CommandTest extends PHPUnit\Framework\TestCase { + + use MediaWikiCoversValidator; + private function requirePosix() { if ( wfIsWindows() ) { $this->markTestSkipped( 'This test requires a POSIX environment.' ); @@ -47,23 +51,61 @@ class CommandTest extends PHPUnit_Framework_TestCase { $this->assertSame( "bar\n", $result->getStdout() ); } + public function testStdout() { + $this->requirePosix(); + + $command = new Command(); + + $result = $command + ->params( 'bash', '-c', 'echo ThisIsStderr 1>&2' ) + ->execute(); + + $this->assertNotContains( 'ThisIsStderr', $result->getStdout() ); + $this->assertEquals( "ThisIsStderr\n", $result->getStderr() ); + } + + public function testStdoutRedirection() { + $this->requirePosix(); + + $command = new Command(); + + $result = $command + ->params( 'bash', '-c', 'echo ThisIsStderr 1>&2' ) + ->includeStderr( true ) + ->execute(); + + $this->assertEquals( "ThisIsStderr\n", $result->getStdout() ); + $this->assertNull( $result->getStderr() ); + } + public function testOutput() { global $IP; $this->requirePosix(); + chdir( $IP ); $command = new Command(); $result = $command - ->params( [ 'ls', "$IP/index.php" ] ) + ->params( [ 'ls', 'index.php' ] ) ->execute(); - $this->assertSame( "$IP/index.php", trim( $result->getStdout() ) ); + $this->assertRegExp( '/^index.php$/m', $result->getStdout() ); + $this->assertSame( null, $result->getStderr() ); $command = new Command(); $result = $command ->params( [ 'ls', 'index.php', 'no-such-file' ] ) ->includeStderr() ->execute(); + $this->assertRegExp( '/^index.php$/m', $result->getStdout() ); $this->assertRegExp( '/^.+no-such-file.*$/m', $result->getStdout() ); + $this->assertSame( null, $result->getStderr() ); + + $command = new Command(); + $result = $command + ->params( [ 'ls', 'index.php', 'no-such-file' ] ) + ->execute(); + $this->assertRegExp( '/^index.php$/m', $result->getStdout() ); + $this->assertRegExp( '/^.+no-such-file.*$/m', $result->getStderr() ); } /** @@ -91,4 +133,49 @@ class CommandTest extends PHPUnit_Framework_TestCase { $this->assertEquals( 333333, strlen( $output ) ); } } + + public function testLogStderr() { + $this->requirePosix(); + + $logger = new TestLogger( true, function ( $message, $level, $context ) { + return $level === Psr\Log\LogLevel::ERROR ? '1' : null; + }, true ); + $command = new Command(); + $command->setLogger( $logger ); + $command->params( 'bash', '-c', 'echo ThisIsStderr 1>&2' ); + $command->execute(); + $this->assertEmpty( $logger->getBuffer() ); + + $command = new Command(); + $command->setLogger( $logger ); + $command->logStderr(); + $command->params( 'bash', '-c', 'echo ThisIsStderr 1>&2' ); + $command->execute(); + $this->assertSame( 1, count( $logger->getBuffer() ) ); + $this->assertSame( trim( $logger->getBuffer()[0][2]['error'] ), 'ThisIsStderr' ); + } + + public function testInput() { + $this->requirePosix(); + + $command = new Command(); + $command->params( 'cat' ); + $command->input( 'abc' ); + $result = $command->execute(); + $this->assertSame( 'abc', $result->getStdout() ); + + // now try it with something that does not fit into a single block + $command = new Command(); + $command->params( 'cat' ); + $command->input( str_repeat( '!', 1000000 ) ); + $result = $command->execute(); + $this->assertSame( 1000000, strlen( $result->getStdout() ) ); + + // And try it with empty input + $command = new Command(); + $command->params( 'cat' ); + $command->input( '' ); + $result = $command->execute(); + $this->assertSame( '', $result->getStdout() ); + } } diff --git a/www/wiki/tests/phpunit/includes/shell/FirejailCommandTest.php b/www/wiki/tests/phpunit/includes/shell/FirejailCommandTest.php new file mode 100644 index 00000000..681c3dcd --- /dev/null +++ b/www/wiki/tests/phpunit/includes/shell/FirejailCommandTest.php @@ -0,0 +1,85 @@ +<?php + +/** + * Copyright (C) 2017 Kunal Mehta <legoktm@member.fsf.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. + * + */ + +use MediaWiki\Shell\FirejailCommand; +use MediaWiki\Shell\Shell; +use Wikimedia\TestingAccessWrapper; + +class FirejailCommandTest extends PHPUnit\Framework\TestCase { + + use MediaWikiCoversValidator; + + public function provideBuildFinalCommand() { + global $IP; + // phpcs:ignore Generic.Files.LineLength + $env = "'MW_INCLUDE_STDERR=;MW_CPU_LIMIT=180; MW_CGROUP='\'''\''; MW_MEM_LIMIT=307200; MW_FILE_SIZE_LIMIT=102400; MW_WALL_CLOCK_LIMIT=180; MW_USE_LOG_PIPE=yes'"; + $limit = "/bin/bash '$IP/includes/shell/limit.sh'"; + $profile = "--profile=$IP/includes/shell/firejail.profile"; + $blacklist = '--blacklist=' . realpath( MW_CONFIG_FILE ); + $default = "$blacklist --noroot --seccomp --private-dev"; + return [ + [ + 'No restrictions', + 'ls', 0, "$limit ''\''ls'\''' $env" + ], + [ + 'default restriction', + 'ls', Shell::RESTRICT_DEFAULT, + "$limit 'firejail --quiet $profile $default -- '\''ls'\''' $env" + ], + [ + 'no network', + 'ls', Shell::NO_NETWORK, + "$limit 'firejail --quiet $profile --net=none -- '\''ls'\''' $env" + ], + [ + 'default restriction & no network', + 'ls', Shell::RESTRICT_DEFAULT | Shell::NO_NETWORK, + "$limit 'firejail --quiet $profile $default --net=none -- '\''ls'\''' $env" + ], + [ + 'seccomp', + 'ls', Shell::SECCOMP, + "$limit 'firejail --quiet $profile --seccomp -- '\''ls'\''' $env" + ], + [ + 'seccomp & no execve', + 'ls', Shell::SECCOMP | Shell::NO_EXECVE, + "$limit 'firejail --quiet $profile --shell=none --seccomp=execve -- '\''ls'\''' $env" + ], + ]; + } + + /** + * @covers \MediaWiki\Shell\FirejailCommand::buildFinalCommand() + * @dataProvider provideBuildFinalCommand + */ + public function testBuildFinalCommand( $desc, $params, $flags, $expected ) { + $command = new FirejailCommand( 'firejail' ); + $command + ->params( $params ) + ->restrict( $flags ); + $wrapper = TestingAccessWrapper::newFromObject( $command ); + $output = $wrapper->buildFinalCommand( $wrapper->command ); + $this->assertEquals( $expected, $output[0], $desc ); + } + +} diff --git a/www/wiki/tests/phpunit/includes/shell/ShellTest.php b/www/wiki/tests/phpunit/includes/shell/ShellTest.php index 7c96c3c8..bf46f44b 100644 --- a/www/wiki/tests/phpunit/includes/shell/ShellTest.php +++ b/www/wiki/tests/phpunit/includes/shell/ShellTest.php @@ -1,11 +1,17 @@ <?php +use MediaWiki\Shell\Command; use MediaWiki\Shell\Shell; +use Wikimedia\TestingAccessWrapper; /** + * @covers \MediaWiki\Shell\Shell * @group Shell */ -class ShellTest extends PHPUnit_Framework_TestCase { +class ShellTest extends MediaWikiTestCase { + + use MediaWikiCoversValidator; + public function testIsDisabled() { $this->assertInternalType( 'bool', Shell::isDisabled() ); // sanity } @@ -28,4 +34,72 @@ class ShellTest extends PHPUnit_Framework_TestCase { 'skip nulls' => [ [ 'ls', null ], "'ls'" ], ]; } + + /** + * @covers \MediaWiki\Shell\Shell::makeScriptCommand + * @dataProvider provideMakeScriptCommand + * + * @param string $expected + * @param string $script + * @param string[] $parameters + * @param string[] $options + * @param callable|null $hook + */ + public function testMakeScriptCommand( $expected, + $script, + $parameters, + $options = [], + $hook = null + ) { + // Running tests under Vagrant involves MWMultiVersion that uses the below hook + $this->setMwGlobals( 'wgHooks', [] ); + + if ( $hook ) { + $this->setTemporaryHook( 'wfShellWikiCmd', $hook ); + } + + $command = Shell::makeScriptCommand( $script, $parameters, $options ); + $command->params( 'safe' ) + ->unsafeParams( 'unsafe' ); + + $this->assertType( Command::class, $command ); + + $wrapper = TestingAccessWrapper::newFromObject( $command ); + $this->assertEquals( $expected, $wrapper->command ); + $this->assertEquals( 0, $wrapper->restrictions & Shell::NO_LOCALSETTINGS ); + } + + public function provideMakeScriptCommand() { + global $wgPhpCli; + + return [ + [ + "'$wgPhpCli' 'maintenance/foobar.php' 'bar'\\''\"baz' 'safe' unsafe", + 'maintenance/foobar.php', + [ 'bar\'"baz' ], + ], + [ + "'$wgPhpCli' 'changed.php' '--wiki=somewiki' 'bar'\\''\"baz' 'safe' unsafe", + 'maintenance/foobar.php', + [ 'bar\'"baz' ], + [], + function ( &$script, array &$parameters ) { + $script = 'changed.php'; + array_unshift( $parameters, '--wiki=somewiki' ); + } + ], + [ + "'/bin/perl' 'maintenance/foobar.php' 'bar'\\''\"baz' 'safe' unsafe", + 'maintenance/foobar.php', + [ 'bar\'"baz' ], + [ 'php' => '/bin/perl' ], + ], + [ + "'$wgPhpCli' 'foobinize' 'maintenance/foobar.php' 'bar'\\''\"baz' 'safe' unsafe", + 'maintenance/foobar.php', + [ 'bar\'"baz' ], + [ 'wrapper' => 'foobinize' ], + ], + ]; + } } diff --git a/www/wiki/tests/phpunit/includes/site/CachingSiteStoreTest.php b/www/wiki/tests/phpunit/includes/site/CachingSiteStoreTest.php index adf95e6c..0fdcf6da 100644 --- a/www/wiki/tests/phpunit/includes/site/CachingSiteStoreTest.php +++ b/www/wiki/tests/phpunit/includes/site/CachingSiteStoreTest.php @@ -42,13 +42,13 @@ class CachingSiteStoreTest extends MediaWikiTestCase { $sites = $store->getSites(); - $this->assertInstanceOf( 'SiteList', $sites ); + $this->assertInstanceOf( SiteList::class, $sites ); /** * @var Site $site */ foreach ( $sites as $site ) { - $this->assertInstanceOf( 'Site', $site ); + $this->assertInstanceOf( Site::class, $site ); } foreach ( $testSites as $site ) { @@ -79,11 +79,11 @@ class CachingSiteStoreTest extends MediaWikiTestCase { $this->assertTrue( $store->saveSites( $sites ) ); $site = $store->getSite( 'ertrywuutr' ); - $this->assertInstanceOf( 'Site', $site ); + $this->assertInstanceOf( Site::class, $site ); $this->assertEquals( 'en', $site->getLanguageCode() ); $site = $store->getSite( 'sdfhxujgkfpth' ); - $this->assertInstanceOf( 'Site', $site ); + $this->assertInstanceOf( Site::class, $site ); $this->assertEquals( 'nl', $site->getLanguageCode() ); } @@ -91,7 +91,7 @@ class CachingSiteStoreTest extends MediaWikiTestCase { * @covers CachingSiteStore::reset */ public function testReset() { - $dbSiteStore = $this->getMockBuilder( 'SiteStore' ) + $dbSiteStore = $this->getMockBuilder( SiteStore::class ) ->disableOriginalConstructor() ->getMock(); diff --git a/www/wiki/tests/phpunit/includes/site/DBSiteStoreTest.php b/www/wiki/tests/phpunit/includes/site/DBSiteStoreTest.php index 32dd7f28..7c16f6c5 100644 --- a/www/wiki/tests/phpunit/includes/site/DBSiteStoreTest.php +++ b/www/wiki/tests/phpunit/includes/site/DBSiteStoreTest.php @@ -51,13 +51,13 @@ class DBSiteStoreTest extends MediaWikiTestCase { $sites = $store->getSites(); - $this->assertInstanceOf( 'SiteList', $sites ); + $this->assertInstanceOf( SiteList::class, $sites ); /** * @var Site $site */ foreach ( $sites as $site ) { - $this->assertInstanceOf( 'Site', $site ); + $this->assertInstanceOf( Site::class, $site ); } foreach ( $expectedSites as $site ) { @@ -88,15 +88,15 @@ class DBSiteStoreTest extends MediaWikiTestCase { $this->assertTrue( $store->saveSites( $sites ) ); $site = $store->getSite( 'ertrywuutr' ); - $this->assertInstanceOf( 'Site', $site ); + $this->assertInstanceOf( Site::class, $site ); $this->assertEquals( 'en', $site->getLanguageCode() ); - $this->assertTrue( is_integer( $site->getInternalId() ) ); + $this->assertTrue( is_int( $site->getInternalId() ) ); $this->assertTrue( $site->getInternalId() >= 0 ); $site = $store->getSite( 'sdfhxujgkfpth' ); - $this->assertInstanceOf( 'Site', $site ); + $this->assertInstanceOf( Site::class, $site ); $this->assertEquals( 'nl', $site->getLanguageCode() ); - $this->assertTrue( is_integer( $site->getInternalId() ) ); + $this->assertTrue( is_int( $site->getInternalId() ) ); $this->assertTrue( $site->getInternalId() >= 0 ); } diff --git a/www/wiki/tests/phpunit/includes/site/FileBasedSiteLookupTest.php b/www/wiki/tests/phpunit/includes/site/FileBasedSiteLookupTest.php index 7984795b..69e0e389 100644 --- a/www/wiki/tests/phpunit/includes/site/FileBasedSiteLookupTest.php +++ b/www/wiki/tests/phpunit/includes/site/FileBasedSiteLookupTest.php @@ -27,7 +27,9 @@ * * @author Katie Filbert < aude.wiki@gmail.com > */ -class FileBasedSiteLookupTest extends PHPUnit_Framework_TestCase { +class FileBasedSiteLookupTest extends PHPUnit\Framework\TestCase { + + use MediaWikiCoversValidator; protected function setUp() { $this->cacheFile = $this->getCacheFile(); @@ -64,7 +66,7 @@ class FileBasedSiteLookupTest extends PHPUnit_Framework_TestCase { } private function getSiteLookup( SiteList $sites ) { - $siteLookup = $this->getMockBuilder( 'SiteLookup' ) + $siteLookup = $this->getMockBuilder( SiteLookup::class ) ->disableOriginalConstructor() ->getMock(); diff --git a/www/wiki/tests/phpunit/includes/site/MediaWikiPageNameNormalizerTest.php b/www/wiki/tests/phpunit/includes/site/MediaWikiPageNameNormalizerTest.php index 64cdbaa9..2ac27146 100644 --- a/www/wiki/tests/phpunit/includes/site/MediaWikiPageNameNormalizerTest.php +++ b/www/wiki/tests/phpunit/includes/site/MediaWikiPageNameNormalizerTest.php @@ -27,7 +27,9 @@ use MediaWiki\Site\MediaWikiPageNameNormalizer; * * @author Marius Hoch */ -class MediaWikiPageNameNormalizerTest extends PHPUnit_Framework_TestCase { +class MediaWikiPageNameNormalizerTest extends PHPUnit\Framework\TestCase { + + use MediaWikiCoversValidator; /** * @dataProvider normalizePageTitleProvider diff --git a/www/wiki/tests/phpunit/includes/site/SiteExporterTest.php b/www/wiki/tests/phpunit/includes/site/SiteExporterTest.php index c0d8c001..db900da9 100644 --- a/www/wiki/tests/phpunit/includes/site/SiteExporterTest.php +++ b/www/wiki/tests/phpunit/includes/site/SiteExporterTest.php @@ -29,10 +29,13 @@ * * @author Daniel Kinzler */ -class SiteExporterTest extends PHPUnit_Framework_TestCase { +class SiteExporterTest extends PHPUnit\Framework\TestCase { + + use MediaWikiCoversValidator; + use PHPUnit4And6Compat; public function testConstructor_InvalidArgument() { - $this->setExpectedException( 'InvalidArgumentException' ); + $this->setExpectedException( InvalidArgumentException::class ); new SiteExporter( 'Foo' ); } @@ -75,7 +78,7 @@ class SiteExporterTest extends PHPUnit_Framework_TestCase { } private function newSiteStore( SiteList $sites ) { - $store = $this->getMockBuilder( 'SiteStore' )->getMock(); + $store = $this->getMockBuilder( SiteStore::class )->getMock(); $store->expects( $this->once() ) ->method( 'saveSites' ) diff --git a/www/wiki/tests/phpunit/includes/site/SiteImporterTest.php b/www/wiki/tests/phpunit/includes/site/SiteImporterTest.php index ea49429c..bd95a501 100644 --- a/www/wiki/tests/phpunit/includes/site/SiteImporterTest.php +++ b/www/wiki/tests/phpunit/includes/site/SiteImporterTest.php @@ -29,10 +29,13 @@ * * @author Daniel Kinzler */ -class SiteImporterTest extends PHPUnit_Framework_TestCase { +class SiteImporterTest extends PHPUnit\Framework\TestCase { + + use MediaWikiCoversValidator; + use PHPUnit4And6Compat; private function newSiteImporter( array $expectedSites, $errorCount ) { - $store = $this->getMockBuilder( 'SiteStore' )->getMock(); + $store = $this->getMockBuilder( SiteStore::class )->getMock(); $store->expects( $this->once() ) ->method( 'saveSites' ) @@ -44,7 +47,7 @@ class SiteImporterTest extends PHPUnit_Framework_TestCase { ->method( 'getSites' ) ->will( $this->returnValue( new SiteList() ) ); - $errorHandler = $this->getMockBuilder( 'Psr\Log\LoggerInterface' )->getMock(); + $errorHandler = $this->getMockBuilder( Psr\Log\LoggerInterface::class )->getMock(); $errorHandler->expects( $this->exactly( $errorCount ) ) ->method( 'error' ); @@ -146,9 +149,9 @@ class SiteImporterTest extends PHPUnit_Framework_TestCase { } public function testImportFromXML_malformed() { - $this->setExpectedException( 'Exception' ); + $this->setExpectedException( Exception::class ); - $store = $this->getMockBuilder( 'SiteStore' )->getMock(); + $store = $this->getMockBuilder( SiteStore::class )->getMock(); $importer = new SiteImporter( $store ); $importer->importFromXML( 'THIS IS NOT XML' ); } diff --git a/www/wiki/tests/phpunit/includes/site/SiteListTest.php b/www/wiki/tests/phpunit/includes/site/SiteListTest.php index f7d01171..a4a171c9 100644 --- a/www/wiki/tests/phpunit/includes/site/SiteListTest.php +++ b/www/wiki/tests/phpunit/includes/site/SiteListTest.php @@ -99,7 +99,7 @@ class SiteListTest extends MediaWikiTestCase { * @var Site $site */ foreach ( $sites as $site ) { - if ( is_integer( $site->getInternalId() ) ) { + if ( is_int( $site->getInternalId() ) ) { $this->assertEquals( $site, $sites->getSiteByInternalId( $site->getInternalId() ) ); } } @@ -155,7 +155,7 @@ class SiteListTest extends MediaWikiTestCase { * @var Site $site */ foreach ( $sites as $site ) { - if ( is_integer( $site->getInternalId() ) ) { + if ( is_int( $site->getInternalId() ) ) { $this->assertTrue( $site, $sites->hasInternalId( $site->getInternalId() ) ); } } diff --git a/www/wiki/tests/phpunit/includes/site/SiteTest.php b/www/wiki/tests/phpunit/includes/site/SiteTest.php index 59f046b2..ac5f956e 100644 --- a/www/wiki/tests/phpunit/includes/site/SiteTest.php +++ b/www/wiki/tests/phpunit/includes/site/SiteTest.php @@ -283,12 +283,12 @@ class SiteTest extends MediaWikiTestCase { * @covers Site::unserialize */ public function testSerialization( Site $site ) { - $this->assertInstanceOf( 'Serializable', $site ); + $this->assertInstanceOf( Serializable::class, $site ); $serialization = serialize( $site ); $newInstance = unserialize( $serialization ); - $this->assertInstanceOf( 'Site', $newInstance ); + $this->assertInstanceOf( Site::class, $newInstance ); $this->assertEquals( $serialization, serialize( $newInstance ) ); } diff --git a/www/wiki/tests/phpunit/includes/site/SitesCacheFileBuilderTest.php b/www/wiki/tests/phpunit/includes/site/SitesCacheFileBuilderTest.php index af94a9d2..8c84ce57 100644 --- a/www/wiki/tests/phpunit/includes/site/SitesCacheFileBuilderTest.php +++ b/www/wiki/tests/phpunit/includes/site/SitesCacheFileBuilderTest.php @@ -27,7 +27,9 @@ * * @author Katie Filbert < aude.wiki@gmail.com > */ -class SitesCacheFileBuilderTest extends PHPUnit_Framework_TestCase { +class SitesCacheFileBuilderTest extends PHPUnit\Framework\TestCase { + + use MediaWikiCoversValidator; protected function setUp() { $this->cacheFile = $this->getCacheFile(); @@ -98,7 +100,7 @@ class SitesCacheFileBuilderTest extends PHPUnit_Framework_TestCase { } private function getSiteLookup( SiteList $sites ) { - $siteLookup = $this->getMockBuilder( 'SiteLookup' ) + $siteLookup = $this->getMockBuilder( SiteLookup::class ) ->disableOriginalConstructor() ->getMock(); diff --git a/www/wiki/tests/phpunit/includes/site/TestSites.php b/www/wiki/tests/phpunit/includes/site/TestSites.php index 39c34217..a66fce29 100644 --- a/www/wiki/tests/phpunit/includes/site/TestSites.php +++ b/www/wiki/tests/phpunit/includes/site/TestSites.php @@ -24,8 +24,6 @@ * @ingroup Site * @ingroup Test * - * @group Site - * * @author Jeroen De Dauw < jeroendedauw@gmail.com > */ class TestSites { diff --git a/www/wiki/tests/phpunit/includes/skins/SkinFactoryTest.php b/www/wiki/tests/phpunit/includes/skins/SkinFactoryTest.php index d3663c84..4289fd91 100644 --- a/www/wiki/tests/phpunit/includes/skins/SkinFactoryTest.php +++ b/www/wiki/tests/phpunit/includes/skins/SkinFactoryTest.php @@ -11,7 +11,7 @@ class SkinFactoryTest extends MediaWikiTestCase { return new SkinFallback(); } ); $this->assertTrue( true ); // No exception thrown - $this->setExpectedException( 'InvalidArgumentException' ); + $this->setExpectedException( InvalidArgumentException::class ); $factory->register( 'invalid', 'Invalid', 'Invalid callback' ); } @@ -20,7 +20,7 @@ class SkinFactoryTest extends MediaWikiTestCase { */ public function testMakeSkinWithNoBuilders() { $factory = new SkinFactory(); - $this->setExpectedException( 'SkinException' ); + $this->setExpectedException( SkinException::class ); $factory->makeSkin( 'nobuilderregistered' ); } @@ -32,7 +32,7 @@ class SkinFactoryTest extends MediaWikiTestCase { $factory->register( 'unittest', 'Unittest', function () { return true; // Not a Skin object } ); - $this->setExpectedException( 'UnexpectedValueException' ); + $this->setExpectedException( UnexpectedValueException::class ); $factory->makeSkin( 'unittest' ); } @@ -46,8 +46,20 @@ class SkinFactoryTest extends MediaWikiTestCase { } ); $skin = $factory->makeSkin( 'testfallback' ); - $this->assertInstanceOf( 'Skin', $skin ); - $this->assertInstanceOf( 'SkinFallback', $skin ); + $this->assertInstanceOf( Skin::class, $skin ); + $this->assertInstanceOf( SkinFallback::class, $skin ); + $this->assertEquals( 'fallback', $skin->getSkinName() ); + } + + /** + * @covers Skin::__construct + * @covers Skin::getSkinName + */ + public function testGetSkinName() { + $skin = new SkinFallback(); + $this->assertEquals( 'fallback', $skin->getSkinName(), 'Default' ); + $skin = new SkinFallback( 'testname' ); + $this->assertEquals( 'testname', $skin->getSkinName(), 'Constructor argument' ); } /** diff --git a/www/wiki/tests/phpunit/includes/skins/SkinTemplateTest.php b/www/wiki/tests/phpunit/includes/skins/SkinTemplateTest.php index e8260ac2..06b06677 100644 --- a/www/wiki/tests/phpunit/includes/skins/SkinTemplateTest.php +++ b/www/wiki/tests/phpunit/includes/skins/SkinTemplateTest.php @@ -7,13 +7,12 @@ * * @author Bene* < benestar.wikimedia@gmail.com > */ - class SkinTemplateTest extends MediaWikiTestCase { /** * @dataProvider makeListItemProvider */ public function testMakeListItem( $expected, $key, $item, $options, $message ) { - $template = $this->getMockForAbstractClass( 'BaseTemplate' ); + $template = $this->getMockForAbstractClass( BaseTemplate::class ); $this->assertEquals( $expected, diff --git a/www/wiki/tests/phpunit/includes/sparql/SparqlClientTest.php b/www/wiki/tests/phpunit/includes/sparql/SparqlClientTest.php new file mode 100644 index 00000000..b217af15 --- /dev/null +++ b/www/wiki/tests/phpunit/includes/sparql/SparqlClientTest.php @@ -0,0 +1,190 @@ +<?php +namespace MediaWiki\Sparql; + +use Http; +use MediaWiki\Http\HttpRequestFactory; +use MWHttpRequest; +use PHPUnit4And6Compat; + +/** + * @covers \MediaWiki\Sparql\SparqlClient + */ +class SparqlClientTest extends \PHPUnit\Framework\TestCase { + + use PHPUnit4And6Compat; + + private function getRequestFactory( $request ) { + $requestFactory = $this->getMock( HttpRequestFactory::class ); + $requestFactory->method( 'create' )->willReturn( $request ); + return $requestFactory; + } + + private function getRequestMock( $content ) { + $request = $this->getMockBuilder( MWHttpRequest::class )->disableOriginalConstructor()->getMock(); + $request->method( 'execute' )->willReturn( \Status::newGood( 200 ) ); + $request->method( 'getContent' )->willReturn( $content ); + return $request; + } + + public function testQuery() { + $json = <<<JSON +{ + "head" : { + "vars" : [ "x", "y", "z" ] + }, + "results" : { + "bindings" : [ { + "x" : { + "type" : "uri", + "value" : "http://wikiba.se/ontology#Dump" + }, + "y" : { + "type" : "uri", + "value" : "http://creativecommons.org/ns#license" + }, + "z" : { + "type" : "uri", + "value" : "http://creativecommons.org/publicdomain/zero/1.0/" + } + }, { + "x" : { + "type" : "uri", + "value" : "http://wikiba.se/ontology#Dump" + }, + "z" : { + "type" : "literal", + "value" : "0.1.0" + } + } ] + } +} +JSON; + + $request = $this->getRequestMock( $json ); + $client = new SparqlClient( 'http://acme.test/', $this->getRequestFactory( $request ) ); + + // values only + $result = $client->query( "TEST SPARQL" ); + $this->assertCount( 2, $result ); + $this->assertEquals( 'http://wikiba.se/ontology#Dump', $result[0]['x'] ); + $this->assertEquals( 'http://creativecommons.org/ns#license', $result[0]['y'] ); + $this->assertEquals( '0.1.0', $result[1]['z'] ); + $this->assertNull( $result[1]['y'] ); + // raw data format + $result = $client->query( "TEST SPARQL 2", true ); + $this->assertCount( 2, $result ); + $this->assertEquals( 'uri', $result[0]['x']['type'] ); + $this->assertEquals( 'http://wikiba.se/ontology#Dump', $result[0]['x']['value'] ); + $this->assertEquals( 'literal', $result[1]['z']['type'] ); + $this->assertEquals( '0.1.0', $result[1]['z']['value'] ); + $this->assertNull( $result[1]['y'] ); + } + + /** + * @expectedException \Mediawiki\Sparql\SparqlException + */ + public function testBadQuery() { + $request = $this->getMockBuilder( MWHttpRequest::class )->disableOriginalConstructor()->getMock(); + $client = new SparqlClient( 'http://acme.test/', $this->getRequestFactory( $request ) ); + + $request->method( 'execute' )->willReturn( \Status::newFatal( "Bad query" ) ); + $result = $client->query( "TEST SPARQL 3" ); + } + + public function optionsProvider() { + return [ + 'defaults' => [ + 'TEST тест SPARQL 4 ', + null, + null, + [ + 'http://acme.test/', + 'query=TEST+%D1%82%D0%B5%D1%81%D1%82+SPARQL+4+', + 'format=json', + 'maxQueryTimeMillis=30000', + ], + [ + 'method' => 'GET', + 'userAgent' => Http::userAgent() ." SparqlClient", + 'timeout' => 30 + ] + ], + 'big query' => [ + str_repeat( 'ZZ', SparqlClient::MAX_GET_SIZE ), + null, + null, + [ + 'format=json', + 'maxQueryTimeMillis=30000', + ], + [ + 'method' => 'POST', + 'postData' => 'query=' . str_repeat( 'ZZ', SparqlClient::MAX_GET_SIZE ), + ] + ], + 'timeout 1s' => [ + 'TEST SPARQL 4', + null, + 1, + [ + 'maxQueryTimeMillis=1000', + ], + [ + 'timeout' => 1 + ] + ], + 'more options' => [ + 'TEST SPARQL 5', + [ + 'userAgent' => 'My Test', + 'randomOption' => 'duck', + ], + null, + [], + [ + 'userAgent' => 'My Test', + 'randomOption' => 'duck', + ] + ], + + ]; + } + + /** + * @dataProvider optionsProvider + * @param string $sparql + * @param array|null $options + * @param int|null $timeout + * @param array $expectedUrl + * @param array $expectedOptions + */ + public function testOptions( $sparql, $options, $timeout, $expectedUrl, $expectedOptions ) { + $requestFactory = $this->getMock( HttpRequestFactory::class ); + $client = new SparqlClient( 'http://acme.test/', $requestFactory ); + + $request = $this->getRequestMock( '{}' ); + + $requestFactory->method( 'create' )->willReturnCallback( + function ( $url, $options ) use ( $request, $expectedUrl, $expectedOptions ) { + foreach ( $expectedUrl as $eurl ) { + $this->assertContains( $eurl, $url ); + } + foreach ( $expectedOptions as $ekey => $evalue ) { + $this->assertArrayHasKey( $ekey, $options ); + $this->assertEquals( $options[$ekey], $evalue ); + } + return $request; + } + ); + + if ( !is_null( $options ) ) { + $client->setClientOptions( $options ); + } + if ( !is_null( $timeout ) ) { + $client->setTimeout( $timeout ); + } + + $result = $client->query( $sparql ); + } + +} diff --git a/www/wiki/tests/phpunit/includes/specialpage/AbstractChangesListSpecialPageTestCase.php b/www/wiki/tests/phpunit/includes/specialpage/AbstractChangesListSpecialPageTestCase.php index b101857e..8b8ba0c0 100644 --- a/www/wiki/tests/phpunit/includes/specialpage/AbstractChangesListSpecialPageTestCase.php +++ b/www/wiki/tests/phpunit/includes/specialpage/AbstractChangesListSpecialPageTestCase.php @@ -19,7 +19,10 @@ abstract class AbstractChangesListSpecialPageTestCase extends MediaWikiTestCase global $wgGroupPermissions; parent::setUp(); - $this->setMwGlobals( 'wgRCWatchCategoryMembership', true ); + $this->setMwGlobals( [ + 'wgRCWatchCategoryMembership' => true, + 'wgUseRCPatrol' => true, + ] ); if ( isset( $wgGroupPermissions['patrollers'] ) ) { $this->oldPatrollersGroup = $wgGroupPermissions['patrollers']; @@ -44,6 +47,8 @@ abstract class AbstractChangesListSpecialPageTestCase extends MediaWikiTestCase $this->changesListSpecialPage->registerFilters(); } + abstract protected function getPage(); + protected function tearDown() { global $wgGroupPermissions; @@ -54,6 +59,8 @@ abstract class AbstractChangesListSpecialPageTestCase extends MediaWikiTestCase } } + abstract public function provideParseParameters(); + /** * @dataProvider provideParseParameters */ diff --git a/www/wiki/tests/phpunit/includes/specialpage/ChangesListSpecialPageTest.php b/www/wiki/tests/phpunit/includes/specialpage/ChangesListSpecialPageTest.php index f494785b..aeaa1aee 100644 --- a/www/wiki/tests/phpunit/includes/specialpage/ChangesListSpecialPageTest.php +++ b/www/wiki/tests/phpunit/includes/specialpage/ChangesListSpecialPageTest.php @@ -15,6 +15,13 @@ use Wikimedia\TestingAccessWrapper; * @covers ChangesListSpecialPage */ class ChangesListSpecialPageTest extends AbstractChangesListSpecialPageTestCase { + public function setUp() { + parent::setUp(); + $this->setMwGlobals( [ + 'wgStructuredChangeFiltersShowPreference' => true, + ] ); + } + protected function getPage() { $mock = $this->getMockBuilder( ChangesListSpecialPage::class ) ->setConstructorArgs( @@ -98,9 +105,14 @@ class ChangesListSpecialPageTest extends AbstractChangesListSpecialPageTestCase } private static function normalizeCondition( $conds ) { + $dbr = wfGetDB( DB_REPLICA ); $normalized = array_map( - function ( $k, $v ) { - return is_numeric( $k ) ? $v : "$k = $v"; + function ( $k, $v ) use ( $dbr ) { + if ( is_array( $v ) ) { + sort( $v ); + } + // (Ab)use makeList() to format only this entry + return $dbr->makeList( [ $k => $v ], Database::LIST_AND ); }, array_keys( $conds ), $conds @@ -109,9 +121,9 @@ class ChangesListSpecialPageTest extends AbstractChangesListSpecialPageTestCase return $normalized; } - /** return false if condition begin with 'rc_timestamp ' */ + /** return false if condition begins with 'rc_timestamp ' */ private static function filterOutRcTimestampCondition( $var ) { - return ( false === strpos( $var, 'rc_timestamp ' ) ); + return ( is_array( $var ) || false === strpos( $var, 'rc_timestamp ' ) ); } public function testRcNsFilter() { @@ -192,10 +204,15 @@ class ChangesListSpecialPageTest extends AbstractChangesListSpecialPageTestCase } public function testRcHidemyselfFilter() { + $this->setMwGlobals( 'wgActorTableSchemaMigrationStage', MIGRATION_WRITE_BOTH ); + $this->overrideMwServices(); + $user = $this->getTestUser()->getUser(); + $user->getActorId( wfGetDB( DB_MASTER ) ); $this->assertConditions( [ # expected - "rc_user_text != '{$user->getName()}'", + "NOT((rc_actor = '{$user->getActorId()}') OR " + . "(rc_actor = '0' AND rc_user = '{$user->getId()}'))", ], [ 'hidemyself' => 1, @@ -205,9 +222,10 @@ class ChangesListSpecialPageTest extends AbstractChangesListSpecialPageTestCase ); $user = User::newFromName( '10.11.12.13', false ); + $id = $user->getActorId( wfGetDB( DB_MASTER ) ); $this->assertConditions( [ # expected - "rc_user_text != '10.11.12.13'", + "NOT((rc_actor = '$id') OR (rc_actor = '0' AND rc_user_text = '10.11.12.13'))", ], [ 'hidemyself' => 1, @@ -218,10 +236,15 @@ class ChangesListSpecialPageTest extends AbstractChangesListSpecialPageTestCase } public function testRcHidebyothersFilter() { + $this->setMwGlobals( 'wgActorTableSchemaMigrationStage', MIGRATION_WRITE_BOTH ); + $this->overrideMwServices(); + $user = $this->getTestUser()->getUser(); + $user->getActorId( wfGetDB( DB_MASTER ) ); $this->assertConditions( [ # expected - "rc_user_text = '{$user->getName()}'", + "(rc_actor = '{$user->getActorId()}') OR " + . "(rc_actor = '0' AND rc_user_text = '{$user->getName()}')", ], [ 'hidebyothers' => 1, @@ -231,9 +254,10 @@ class ChangesListSpecialPageTest extends AbstractChangesListSpecialPageTestCase ); $user = User::newFromName( '10.11.12.13', false ); + $id = $user->getActorId( wfGetDB( DB_MASTER ) ); $this->assertConditions( [ # expected - "rc_user_text = '10.11.12.13'", + "(rc_actor = '$id') OR (rc_actor = '0' AND rc_user_text = '10.11.12.13')", ], [ 'hidebyothers' => 1, @@ -293,6 +317,7 @@ class ChangesListSpecialPageTest extends AbstractChangesListSpecialPageTestCase } public function testRcHidepatrolledDisabledFilter() { + $this->setMwGlobals( 'wgUseRCPatrol', false ); $user = $this->getTestUser()->getUser(); $this->assertConditions( [ # expected @@ -306,6 +331,7 @@ class ChangesListSpecialPageTest extends AbstractChangesListSpecialPageTestCase } public function testRcHideunpatrolledDisabledFilter() { + $this->setMwGlobals( 'wgUseRCPatrol', false ); $user = $this->getTestUser()->getUser(); $this->assertConditions( [ # expected @@ -321,7 +347,7 @@ class ChangesListSpecialPageTest extends AbstractChangesListSpecialPageTestCase $user = $this->getTestSysop()->getUser(); $this->assertConditions( [ # expected - "rc_patrolled = 0", + 'rc_patrolled' => 0, ], [ 'hidepatrolled' => 1, @@ -335,7 +361,7 @@ class ChangesListSpecialPageTest extends AbstractChangesListSpecialPageTestCase $user = $this->getTestSysop()->getUser(); $this->assertConditions( [ # expected - "rc_patrolled = 1", + 'rc_patrolled' => [ 1, 2 ], ], [ 'hideunpatrolled' => 1, @@ -345,6 +371,30 @@ class ChangesListSpecialPageTest extends AbstractChangesListSpecialPageTestCase ); } + public function testRcReviewStatusFilter() { + $user = $this->getTestSysop()->getUser(); + $this->assertConditions( + [ #expected + 'rc_patrolled' => 1, + ], + [ + 'reviewStatus' => 'manual' + ], + "rc conditions: reviewStatus=manual", + $user + ); + $this->assertConditions( + [ #expected + 'rc_patrolled' => [ 0, 2 ], + ], + [ + 'reviewStatus' => 'unpatrolled;auto' + ], + "rc conditions: reviewStatus=unpatrolled;auto", + $user + ); + } + public function testRcHideminorFilter() { $this->assertConditions( [ # expected @@ -419,10 +469,13 @@ class ChangesListSpecialPageTest extends AbstractChangesListSpecialPageTestCase } public function testFilterUserExpLevelAllExperienceLevels() { + $this->setMwGlobals( 'wgActorTableSchemaMigrationStage', MIGRATION_WRITE_BOTH ); + $this->overrideMwServices(); + $this->assertConditions( [ # expected - 'rc_user != 0', + 'COALESCE( actor_rc_user.actor_user, rc_user ) != 0', ], [ 'userExpLevel' => 'newcomer;learner;experienced', @@ -432,10 +485,13 @@ class ChangesListSpecialPageTest extends AbstractChangesListSpecialPageTestCase } public function testFilterUserExpLevelRegistrered() { + $this->setMwGlobals( 'wgActorTableSchemaMigrationStage', MIGRATION_WRITE_BOTH ); + $this->overrideMwServices(); + $this->assertConditions( [ # expected - 'rc_user != 0', + 'COALESCE( actor_rc_user.actor_user, rc_user ) != 0', ], [ 'userExpLevel' => 'registered', @@ -445,10 +501,13 @@ class ChangesListSpecialPageTest extends AbstractChangesListSpecialPageTestCase } public function testFilterUserExpLevelUnregistrered() { + $this->setMwGlobals( 'wgActorTableSchemaMigrationStage', MIGRATION_WRITE_BOTH ); + $this->overrideMwServices(); + $this->assertConditions( [ # expected - 'rc_user' => 0, + 'COALESCE( actor_rc_user.actor_user, rc_user ) = 0', ], [ 'userExpLevel' => 'unregistered', @@ -458,10 +517,13 @@ class ChangesListSpecialPageTest extends AbstractChangesListSpecialPageTestCase } public function testFilterUserExpLevelRegistreredOrLearner() { + $this->setMwGlobals( 'wgActorTableSchemaMigrationStage', MIGRATION_WRITE_BOTH ); + $this->overrideMwServices(); + $this->assertConditions( [ # expected - 'rc_user != 0', + 'COALESCE( actor_rc_user.actor_user, rc_user ) != 0', ], [ 'userExpLevel' => 'registered;learner', @@ -471,10 +533,14 @@ class ChangesListSpecialPageTest extends AbstractChangesListSpecialPageTestCase } public function testFilterUserExpLevelUnregistreredOrExperienced() { + $this->setMwGlobals( 'wgActorTableSchemaMigrationStage', MIGRATION_WRITE_BOTH ); + $this->overrideMwServices(); + $conds = $this->buildQuery( [ 'userExpLevel' => 'unregistered;experienced' ] ); $this->assertRegExp( - '/\(rc_user = 0\) OR \(\(user_editcount >= 500\) AND \(user_registration <= \'[^\']+\'\)\)/', + '/\(COALESCE\( actor_rc_user.actor_user, rc_user \) = 0\) OR ' + . '\(\(user_editcount >= 500\) AND \(user_registration <= \'[^\']+\'\)\)/', reset( $conds ), "rc conditions: userExpLevel=unregistered;experienced" ); @@ -586,8 +652,10 @@ class ChangesListSpecialPageTest extends AbstractChangesListSpecialPageTestCase ] ); + // @todo: This is not at all safe or sane. It just blindly assumes + // nothing in $conds depends on any other tables. $result = wfGetDB( DB_MASTER )->select( - $tables, + 'user', 'user_name', array_filter( $conds ) + [ 'user_email' => 'ut' ] ); @@ -734,6 +802,7 @@ class ChangesListSpecialPageTest extends AbstractChangesListSpecialPageTestCase 'cssClass' => null, 'conflicts' => [], 'subset' => [], + 'defaultHighlightColor' => null ], [ 'name' => 'hidefoo', @@ -744,6 +813,7 @@ class ChangesListSpecialPageTest extends AbstractChangesListSpecialPageTestCase 'cssClass' => null, 'conflicts' => [], 'subset' => [], + 'defaultHighlightColor' => null ], ], 'fullCoverage' => true, @@ -765,6 +835,7 @@ class ChangesListSpecialPageTest extends AbstractChangesListSpecialPageTestCase 'priority' => -2, 'conflicts' => [], 'subset' => [], + 'defaultHighlightColor' => null ], [ 'name' => 'garply', @@ -774,6 +845,7 @@ class ChangesListSpecialPageTest extends AbstractChangesListSpecialPageTestCase 'priority' => -3, 'conflicts' => [], 'subset' => [], + 'defaultHighlightColor' => null ], ], 'conflicts' => [], @@ -968,15 +1040,33 @@ class ChangesListSpecialPageTest extends AbstractChangesListSpecialPageTestCase [ [ 'hideanons' => 1, 'hideliu' => 1, 'hidebots' => 1 ], true, - [ 'hideliu' => 1, 'hidebots' => 1, ], + [ 'userExpLevel' => 'unregistered', 'hidebots' => 1, ], ], - [ [ 'hideanons' => 1, 'hideliu' => 1, 'hidebots' => 0 ], true, [ 'hidebots' => 0, 'hidehumans' => 1 ], ], - + [ + [ 'hideanons' => 1 ], + true, + [ 'userExpLevel' => 'registered' ] + ], + [ + [ 'hideliu' => 1 ], + true, + [ 'userExpLevel' => 'unregistered' ] + ], + [ + [ 'hideanons' => 1, 'hidebots' => 1 ], + true, + [ 'userExpLevel' => 'registered', 'hidebots' => 1 ] + ], + [ + [ 'hideliu' => 1, 'hidebots' => 0 ], + true, + [ 'userExpLevel' => 'unregistered', 'hidebots' => 0 ] + ], [ [ 'hidemyself' => 1, 'hidebyothers' => 1 ], true, diff --git a/www/wiki/tests/phpunit/includes/specialpage/SpecialPageFactoryTest.php b/www/wiki/tests/phpunit/includes/specialpage/SpecialPageFactoryTest.php index f79f6e48..9ac546dc 100644 --- a/www/wiki/tests/phpunit/includes/specialpage/SpecialPageFactoryTest.php +++ b/www/wiki/tests/phpunit/includes/specialpage/SpecialPageFactoryTest.php @@ -83,7 +83,7 @@ class SpecialPageFactoryTest extends MediaWikiTestCase { SpecialPageFactory::resetList(); $page = SpecialPageFactory::getPage( 'testdummy' ); - $this->assertInstanceOf( 'SpecialPage', $page ); + $this->assertInstanceOf( SpecialPage::class, $page ); $page2 = SpecialPageFactory::getPage( 'testdummy' ); $this->assertEquals( $shouldReuseInstance, $page2 === $page, "Should re-use instance:" ); @@ -93,7 +93,7 @@ class SpecialPageFactoryTest extends MediaWikiTestCase { * @covers SpecialPageFactory::getNames */ public function testGetNames() { - $this->mergeMwGlobalArrayValue( 'wgSpecialPages', [ 'testdummy' => 'SpecialAllPages' ] ); + $this->mergeMwGlobalArrayValue( 'wgSpecialPages', [ 'testdummy' => SpecialAllPages::class ] ); SpecialPageFactory::resetList(); $names = SpecialPageFactory::getNames(); diff --git a/www/wiki/tests/phpunit/includes/specialpage/SpecialPageTest.php b/www/wiki/tests/phpunit/includes/specialpage/SpecialPageTest.php index c665f3cf..2ad39729 100644 --- a/www/wiki/tests/phpunit/includes/specialpage/SpecialPageTest.php +++ b/www/wiki/tests/phpunit/includes/specialpage/SpecialPageTest.php @@ -67,7 +67,7 @@ class SpecialPageTest extends MediaWikiTestCase { $specialPage->getContext()->setUser( $user ); $specialPage->getContext()->setLanguage( Language::factory( 'en' ) ); - $this->setExpectedException( 'UserNotLoggedIn', $expected ); + $this->setExpectedException( UserNotLoggedIn::class, $expected ); // $specialPage->requireLogin( [ $reason [, $title ] ] ) call_user_func_array( diff --git a/www/wiki/tests/phpunit/includes/specials/ContribsPagerTest.php b/www/wiki/tests/phpunit/includes/specials/ContribsPagerTest.php index 9366282f..1147805c 100644 --- a/www/wiki/tests/phpunit/includes/specials/ContribsPagerTest.php +++ b/www/wiki/tests/phpunit/includes/specials/ContribsPagerTest.php @@ -18,6 +18,7 @@ class ContribsPagerTest extends MediaWikiTestCase { } /** + * @covers ContribsPager::processDateFilter * @dataProvider dateFilterOptionProcessingProvider * @param array $inputOpts Input options * @param array $expectedOpts Expected options diff --git a/www/wiki/tests/phpunit/includes/specials/ImageListPagerTest.php b/www/wiki/tests/phpunit/includes/specials/ImageListPagerTest.php index 22bdefdf..10c6d04c 100644 --- a/www/wiki/tests/phpunit/includes/specials/ImageListPagerTest.php +++ b/www/wiki/tests/phpunit/includes/specials/ImageListPagerTest.php @@ -8,7 +8,6 @@ * * @group Database */ - class ImageListPagerTest extends MediaWikiTestCase { /** * @expectedException MWException diff --git a/www/wiki/tests/phpunit/includes/specials/QueryAllSpecialPagesTest.php b/www/wiki/tests/phpunit/includes/specials/QueryAllSpecialPagesTest.php index 1208a20c..d53a9b8f 100644 --- a/www/wiki/tests/phpunit/includes/specials/QueryAllSpecialPagesTest.php +++ b/www/wiki/tests/phpunit/includes/specials/QueryAllSpecialPagesTest.php @@ -5,10 +5,10 @@ * Copyright © 2011, Antoine Musso * * @author Antoine Musso - * @group Database */ /** + * @group Database * @covers QueryPage<extended> */ class QueryAllSpecialPagesTest extends MediaWikiTestCase { @@ -20,7 +20,7 @@ class QueryAllSpecialPagesTest extends MediaWikiTestCase { /** List query pages that can not be tested automatically */ protected $manualTest = [ - 'LinkSearchPage' + LinkSearchPage::class ]; /** @@ -30,7 +30,7 @@ class QueryAllSpecialPagesTest extends MediaWikiTestCase { * https://bugs.mysql.com/bug.php?id=10327 */ protected $reopensTempTable = [ - 'BrokenRedirects', + BrokenRedirects::class, ]; /** diff --git a/www/wiki/tests/phpunit/includes/specials/SpecialBlankPageTest.php b/www/wiki/tests/phpunit/includes/specials/SpecialBlankPageTest.php index 7bfb8618..e0d059fb 100644 --- a/www/wiki/tests/phpunit/includes/specials/SpecialBlankPageTest.php +++ b/www/wiki/tests/phpunit/includes/specials/SpecialBlankPageTest.php @@ -1,7 +1,7 @@ <?php /** - * @licence GNU GPL v2+ + * @license GNU GPL v2+ * @author Addshore * * @covers SpecialBlankpage diff --git a/www/wiki/tests/phpunit/includes/specials/SpecialEditWatchlistTest.php b/www/wiki/tests/phpunit/includes/specials/SpecialEditWatchlistTest.php index ab3ac553..05a63dbc 100644 --- a/www/wiki/tests/phpunit/includes/specials/SpecialEditWatchlistTest.php +++ b/www/wiki/tests/phpunit/includes/specials/SpecialEditWatchlistTest.php @@ -19,7 +19,7 @@ class SpecialEditWatchlistTest extends SpecialPageTestBase { } public function testNotLoggedIn_throwsException() { - $this->setExpectedException( 'UserNotLoggedIn' ); + $this->setExpectedException( UserNotLoggedIn::class ); $this->executeSpecialPage(); } diff --git a/www/wiki/tests/phpunit/includes/specials/SpecialMIMESearchTest.php b/www/wiki/tests/phpunit/includes/specials/SpecialMIMESearchTest.php index ede27917..4ecb813f 100644 --- a/www/wiki/tests/phpunit/includes/specials/SpecialMIMESearchTest.php +++ b/www/wiki/tests/phpunit/includes/specials/SpecialMIMESearchTest.php @@ -1,8 +1,9 @@ <?php + /** * @group Database + * @covers MIMEsearchPage */ - class SpecialMIMESearchTest extends MediaWikiTestCase { /** @var MIMEsearchPage */ diff --git a/www/wiki/tests/phpunit/includes/specials/SpecialMyLanguageTest.php b/www/wiki/tests/phpunit/includes/specials/SpecialMyLanguageTest.php index 89fd1b06..84fa71a2 100644 --- a/www/wiki/tests/phpunit/includes/specials/SpecialMyLanguageTest.php +++ b/www/wiki/tests/phpunit/includes/specials/SpecialMyLanguageTest.php @@ -8,7 +8,10 @@ class SpecialMyLanguageTest extends MediaWikiTestCase { public function addDBDataOnce() { $titles = [ 'Page/Another', + 'Page/Another/ar', + 'Page/Another/en', 'Page/Another/ru', + 'Page/Another/zh-hans', ]; foreach ( $titles as $title ) { $page = WikiPage::factory( Title::newFromText( $title ) ); @@ -54,12 +57,22 @@ class SpecialMyLanguageTest extends MediaWikiTestCase { } public static function provideFindTitle() { + // See addDBDataOnce() for page declarations return [ + // [ $expected, $subpage, $langCode, $userLang ] [ null, '::Fail', 'en', 'en' ], [ 'Page/Another', 'Page/Another/en', 'en', 'en' ], [ 'Page/Another', 'Page/Another', 'en', 'en' ], [ 'Page/Another/ru', 'Page/Another', 'en', 'ru' ], [ 'Page/Another', 'Page/Another', 'en', 'es' ], + [ 'Page/Another/zh-hans', 'Page/Another', 'en', 'zh-hans' ], + [ 'Page/Another/zh-hans', 'Page/Another', 'en', 'zh-mo' ], + [ 'Page/Another/en', 'Page/Another', 'de', 'es' ], + [ 'Page/Another/ar', 'Page/Another', 'en', 'ar' ], + [ 'Page/Another/ar', 'Page/Another', 'en', 'arz' ], + [ 'Page/Another/ar', 'Page/Another/de', 'en', 'arz' ], + [ 'Page/Another/ru', 'Page/Another/ru', 'en', 'arz' ], + [ 'Page/Another/ar', 'Page/Another/ru', 'en', 'ar' ], ]; } } diff --git a/www/wiki/tests/phpunit/includes/specials/SpecialPageDataTest.php b/www/wiki/tests/phpunit/includes/specials/SpecialPageDataTest.php index 3d0d3441..40754063 100644 --- a/www/wiki/tests/phpunit/includes/specials/SpecialPageDataTest.php +++ b/www/wiki/tests/phpunit/includes/specials/SpecialPageDataTest.php @@ -2,12 +2,9 @@ /** * @covers SpecialPageData - * * @group Database - * * @group SpecialPage * - * @license GPL-2.0+ * @author Daniel Kinzler */ class SpecialPageDataTest extends SpecialPageTestBase { diff --git a/www/wiki/tests/phpunit/includes/specials/SpecialPageTestBase.php b/www/wiki/tests/phpunit/includes/specials/SpecialPageTestBase.php index 930bbe4d..274a23c4 100644 --- a/www/wiki/tests/phpunit/includes/specials/SpecialPageTestBase.php +++ b/www/wiki/tests/phpunit/includes/specials/SpecialPageTestBase.php @@ -5,11 +5,11 @@ * * @since 1.26 * - * @licence GNU GPL v2+ + * @license GNU GPL v2+ * @author Jeroen De Dauw < jeroendedauw@gmail.com > * @author Daniel Kinzler * @author Addshore - * @author Thiemo Mättig + * @author Thiemo Kreuz */ abstract class SpecialPageTestBase extends MediaWikiTestCase { diff --git a/www/wiki/tests/phpunit/includes/specials/SpecialPreferencesTest.php b/www/wiki/tests/phpunit/includes/specials/SpecialPreferencesTest.php index ac58d68c..bdfbb62e 100644 --- a/www/wiki/tests/phpunit/includes/specials/SpecialPreferencesTest.php +++ b/www/wiki/tests/phpunit/includes/specials/SpecialPreferencesTest.php @@ -7,6 +7,7 @@ */ /** + * @group Preferences * @group Database * * @covers SpecialPreferences @@ -24,7 +25,7 @@ class SpecialPreferencesTest extends MediaWikiTestCase { // Set a low limit $this->setMwGlobals( 'wgMaxSigChars', 2 ); - $user = $this->createMock( 'User' ); + $user = $this->createMock( User::class ); $user->expects( $this->any() ) ->method( 'isAnon' ) ->will( $this->returnValue( false ) ); diff --git a/www/wiki/tests/phpunit/includes/specials/SpecialSearchTest.php b/www/wiki/tests/phpunit/includes/specials/SpecialSearchTest.php index 94924ee1..f0a57266 100644 --- a/www/wiki/tests/phpunit/includes/specials/SpecialSearchTest.php +++ b/www/wiki/tests/phpunit/includes/specials/SpecialSearchTest.php @@ -194,7 +194,7 @@ class SpecialSearchTest extends MediaWikiTestCase { ); $mockSearchEngine = $this->mockSearchEngine( $searchResults ); - $search = $this->getMockBuilder( 'SpecialSearch' ) + $search = $this->getMockBuilder( SpecialSearch::class ) ->setMethods( [ 'getSearchEngine' ] ) ->getMock(); $search->expects( $this->any() ) @@ -213,7 +213,7 @@ class SpecialSearchTest extends MediaWikiTestCase { } protected function mockSearchEngine( $results ) { - $mock = $this->getMockBuilder( 'SearchEngine' ) + $mock = $this->getMockBuilder( SearchEngine::class ) ->setMethods( [ 'searchText', 'searchTitle' ] ) ->getMock(); diff --git a/www/wiki/tests/phpunit/includes/specials/SpecialShortpagesTest.php b/www/wiki/tests/phpunit/includes/specials/SpecialShortpagesTest.php index a5fb50e0..f799b115 100644 --- a/www/wiki/tests/phpunit/includes/specials/SpecialShortpagesTest.php +++ b/www/wiki/tests/phpunit/includes/specials/SpecialShortpagesTest.php @@ -5,7 +5,7 @@ * * @since 1.30 * - * @licence GNU GPL v2+ + * @license GNU GPL v2+ */ class SpecialShortpagesTest extends MediaWikiTestCase { diff --git a/www/wiki/tests/phpunit/includes/specials/SpecialUncategorizedcategoriesTest.php b/www/wiki/tests/phpunit/includes/specials/SpecialUncategorizedcategoriesTest.php index 64e78f28..80bd365f 100644 --- a/www/wiki/tests/phpunit/includes/specials/SpecialUncategorizedcategoriesTest.php +++ b/www/wiki/tests/phpunit/includes/specials/SpecialUncategorizedcategoriesTest.php @@ -5,10 +5,11 @@ class UncategorizedCategoriesPageTest extends MediaWikiTestCase { /** * @dataProvider provideTestGetQueryInfoData + * @covers UncategorizedCategoriesPage::getQueryInfo */ public function testGetQueryInfo( $msgContent, $expected ) { $msg = new RawMessage( $msgContent ); - $mockContext = $this->getMockBuilder( 'RequestContext' )->getMock(); + $mockContext = $this->getMockBuilder( RequestContext::class )->getMock(); $mockContext->method( 'msg' )->willReturn( $msg ); $special = new UncategorizedCategoriesPage(); $special->setContext( $mockContext ); diff --git a/www/wiki/tests/phpunit/includes/specials/SpecialUploadTest.php b/www/wiki/tests/phpunit/includes/specials/SpecialUploadTest.php new file mode 100644 index 00000000..95026c18 --- /dev/null +++ b/www/wiki/tests/phpunit/includes/specials/SpecialUploadTest.php @@ -0,0 +1,29 @@ +<?php + +class SpecialUploadTest extends MediaWikiTestCase { + /** + * @covers SpecialUpload::getInitialPageText + * @dataProvider provideGetInitialPageText + */ + public function testGetInitialPageText( $expected, $inputParams ) { + $result = call_user_func_array( [ 'SpecialUpload', 'getInitialPageText' ], $inputParams ); + $this->assertEquals( $expected, $result ); + } + + public function provideGetInitialPageText() { + return [ + [ + 'expect' => "== Summary ==\nthis is a test\n", + 'params' => [ + 'this is a test' + ], + ], + [ + 'expect' => "== Summary ==\nthis is a test\n", + 'params' => [ + "== Summary ==\nthis is a test", + ], + ], + ]; + } +} diff --git a/www/wiki/tests/phpunit/includes/specials/SpecialWatchlistTest.php b/www/wiki/tests/phpunit/includes/specials/SpecialWatchlistTest.php index 1c439199..5adbed81 100644 --- a/www/wiki/tests/phpunit/includes/specials/SpecialWatchlistTest.php +++ b/www/wiki/tests/phpunit/includes/specials/SpecialWatchlistTest.php @@ -56,7 +56,7 @@ class SpecialWatchlistTest extends SpecialPageTestBase { } public function testNotLoggedIn_throwsException() { - $this->setExpectedException( 'UserNotLoggedIn' ); + $this->setExpectedException( UserNotLoggedIn::class ); $this->executeSpecialPage(); } @@ -149,6 +149,7 @@ class SpecialWatchlistTest extends SpecialPageTestBase { // Second two overriden 'hideanons' => false, 'hideliu' => true, + 'userExpLevel' => 'registered' ] + $wikiDefaults, [ 'watchlisthideminor' => 1, @@ -171,12 +172,14 @@ class SpecialWatchlistTest extends SpecialPageTestBase { 'hidebots' => true, 'hideanons' => false, 'hideliu' => true, + 'userExpLevel' => 'unregistered' ] + $allFalse, [ 'watchlisthideminor' => 0, 'watchlisthidebots' => 1, - 'watchlisthideanons' => 1, - 'watchlisthideliu' => 0, + + 'watchlisthideanons' => 0, + 'watchlisthideliu' => 1, ], [ 'hidebots' => 1, diff --git a/www/wiki/tests/phpunit/includes/tidy/RemexDriverTest.php b/www/wiki/tests/phpunit/includes/tidy/RemexDriverTest.php index 6b16cbf6..a5ebaa5d 100644 --- a/www/wiki/tests/phpunit/includes/tidy/RemexDriverTest.php +++ b/www/wiki/tests/phpunit/includes/tidy/RemexDriverTest.php @@ -200,14 +200,14 @@ class RemexDriverTest extends MediaWikiTestCase { 'a<small><i><div>d</div></i>e</small>', '<p>a</p><small><i><div>d</div></i></small><p><small>e</small></p>' ], + // phpcs:disable Generic.Files.LineLength [ 'Complex pwrap test 6', '<i>a<div>b</div>c<b>d<div>e</div>f</b>g</i>', - // @codingStandardsIgnoreStart Generic.Files.LineLength.TooLong // PHP 5 does not allow concatenation in initialisation of a class static variable '<p><i>a</i></p><i><div>b</div></i><p><i>c<b>d</b></i></p><i><b><div>e</div></b></i><p><i><b>f</b>g</i></p>' - // @codingStandardsIgnoreEnd ], + // phpcs:enable /* FIXME the second <b> causes a stack split which clones the <i> even * though no <p> is actually generated [ @@ -252,6 +252,16 @@ class RemexDriverTest extends MediaWikiTestCase { '<table><b>1<p>2</b>3</p>', '<b>1</b><p><b>2</b>3</p><table></table>' ], + [ + 'AAA causes reparent of p-wrapped text node (T178632)', + '<i><blockquote>x</i></blockquote>', + '<i></i><blockquote><p><i>x</i></p></blockquote>', + ], + [ + 'p-wrap ended by reparenting (T200827)', + '<i><blockquote><p></i>', + '<i></i><blockquote><p><i></i></p><p><i></i></p></blockquote>', + ], ]; public function provider() { diff --git a/www/wiki/tests/phpunit/includes/title/ForeignTitleTest.php b/www/wiki/tests/phpunit/includes/title/ForeignTitleTest.php index 25ff186b..f2fccc75 100644 --- a/www/wiki/tests/phpunit/includes/title/ForeignTitleTest.php +++ b/www/wiki/tests/phpunit/includes/title/ForeignTitleTest.php @@ -68,7 +68,7 @@ class ForeignTitleTest extends MediaWikiTestCase { } public function testUnknownNamespaceError() { - $this->setExpectedException( 'MWException' ); + $this->setExpectedException( MWException::class ); $title = new ForeignTitle( null, 'this', 'that' ); $title->getNamespaceId(); } diff --git a/www/wiki/tests/phpunit/includes/title/MediaWikiPageLinkRendererTest.php b/www/wiki/tests/phpunit/includes/title/MediaWikiPageLinkRendererTest.php deleted file mode 100644 index c79471d5..00000000 --- a/www/wiki/tests/phpunit/includes/title/MediaWikiPageLinkRendererTest.php +++ /dev/null @@ -1,168 +0,0 @@ -<?php -/** - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along - * with this program; if not, write to the Free Software Foundation, Inc., - * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - * http://www.gnu.org/copyleft/gpl.html - * - * @file - * @author Daniel Kinzler - */ - -/** - * @covers MediaWikiPageLinkRenderer - * - * @group Title - * @group Database - */ -class MediaWikiPageLinkRendererTest extends MediaWikiTestCase { - - protected function setUp() { - parent::setUp(); - - $this->setMwGlobals( [ - 'wgContLang' => Language::factory( 'en' ), - ] ); - } - - /** - * Returns a mock GenderCache that will return "female" always. - * - * @return GenderCache - */ - private function getGenderCache() { - $genderCache = $this->getMockBuilder( 'GenderCache' ) - ->disableOriginalConstructor() - ->getMock(); - - $genderCache->expects( $this->any() ) - ->method( 'getGenderOf' ) - ->will( $this->returnValue( 'female' ) ); - - return $genderCache; - } - - public static function provideGetPageUrl() { - return [ - [ - new TitleValue( NS_MAIN, 'Foo_Bar' ), - [], - '/Foo_Bar' - ], - [ - new TitleValue( NS_USER, 'Hansi_Maier', 'stuff' ), - [ 'foo' => 'bar' ], - '/User:Hansi_Maier?foo=bar#stuff' - ], - ]; - } - - /** - * @dataProvider provideGetPageUrl - */ - public function testGetPageUrl( TitleValue $title, $params, $url ) { - // NOTE: was of Feb 2014, MediaWikiPageLinkRenderer *ignores* the - // WikitextTitleFormatter we pass here, and relies on the Linker - // class for generating the link! This may break the test e.g. - // of Linker uses a different language for the namespace names. - - $lang = Language::factory( 'en' ); - - $formatter = new MediaWikiTitleCodec( $lang, $this->getGenderCache() ); - $renderer = new MediaWikiPageLinkRenderer( $formatter, '/' ); - $actual = $renderer->getPageUrl( $title, $params ); - - $this->assertEquals( $url, $actual ); - } - - public static function provideRenderHtmlLink() { - return [ - [ - new TitleValue( NS_MAIN, 'Foo_Bar' ), - 'Foo Bar', - '!<a .*href=".*?Foo_Bar.*?".*?>Foo Bar</a>!' - ], - [ - // NOTE: Linker doesn't include fragments in "broken" links - // NOTE: once this no longer uses Linker, we will get "2" instead of "User" for the namespace. - new TitleValue( NS_USER, 'Hansi_Maier', 'stuff' ), - 'Hansi Maier\'s Stuff', - '!<a .*href=".*?User:Hansi_Maier.*?>Hansi Maier\'s Stuff</a>!' - ], - [ - // NOTE: Linker doesn't include fragments in "broken" links - // NOTE: once this no longer uses Linker, we will get "2" instead of "User" for the namespace. - new TitleValue( NS_USER, 'Hansi_Maier', 'stuff' ), - null, - '!<a .*href=".*?User:Hansi_Maier.*?>User:Hansi Maier#stuff</a>!' - ], - ]; - } - - /** - * @dataProvider provideRenderHtmlLink - */ - public function testRenderHtmlLink( TitleValue $title, $text, $pattern ) { - // NOTE: was of Feb 2014, MediaWikiPageLinkRenderer *ignores* the - // WikitextTitleFormatter we pass here, and relies on the Linker - // class for generating the link! This may break the test e.g. - // of Linker uses a different language for the namespace names. - - $lang = Language::factory( 'en' ); - - $formatter = new MediaWikiTitleCodec( $lang, $this->getGenderCache() ); - $renderer = new MediaWikiPageLinkRenderer( $formatter ); - $actual = $renderer->renderHtmlLink( $title, $text ); - - $this->assertRegExp( $pattern, $actual ); - } - - public static function provideRenderWikitextLink() { - return [ - [ - new TitleValue( NS_MAIN, 'Foo_Bar' ), - 'Foo Bar', - '[[:0:Foo Bar|Foo Bar]]' - ], - [ - new TitleValue( NS_USER, 'Hansi_Maier', 'stuff' ), - 'Hansi Maier\'s Stuff', - '[[:2:Hansi Maier#stuff|Hansi Maier's Stuff]]' - ], - [ - new TitleValue( NS_USER, 'Hansi_Maier', 'stuff' ), - null, - '[[:2:Hansi Maier#stuff|2:Hansi Maier#stuff]]' - ], - ]; - } - - /** - * @dataProvider provideRenderWikitextLink - */ - public function testRenderWikitextLink( TitleValue $title, $text, $expected ) { - $formatter = $this->getMock( 'TitleFormatter' ); - $formatter->expects( $this->any() ) - ->method( 'getFullText' ) - ->will( $this->returnCallback( - function ( TitleValue $title ) { - return str_replace( '_', ' ', "$title" ); - } - ) ); - - $renderer = new MediaWikiPageLinkRenderer( $formatter, '/' ); - $actual = $renderer->renderWikitextLink( $title, $text ); - - $this->assertEquals( $expected, $actual ); - } -} diff --git a/www/wiki/tests/phpunit/includes/title/MediaWikiTitleCodecTest.php b/www/wiki/tests/phpunit/includes/title/MediaWikiTitleCodecTest.php index e7ac940b..e1b98ec3 100644 --- a/www/wiki/tests/phpunit/includes/title/MediaWikiTitleCodecTest.php +++ b/www/wiki/tests/phpunit/includes/title/MediaWikiTitleCodecTest.php @@ -64,7 +64,7 @@ class MediaWikiTitleCodecTest extends MediaWikiTestCase { * @return GenderCache */ private function getGenderCache() { - $genderCache = $this->getMockBuilder( 'GenderCache' ) + $genderCache = $this->getMockBuilder( GenderCache::class ) ->disableOriginalConstructor() ->getMock(); @@ -385,7 +385,7 @@ class MediaWikiTitleCodecTest extends MediaWikiTestCase { * @dataProvider provideParseTitle_invalid */ public function testParseTitle_invalid( $text ) { - $this->setExpectedException( 'MalformedTitleException' ); + $this->setExpectedException( MalformedTitleException::class ); $codec = $this->makeCodec( 'en' ); $codec->parseTitle( $text, NS_MAIN ); diff --git a/www/wiki/tests/phpunit/includes/title/SubpageImportTitleFactoryTest.php b/www/wiki/tests/phpunit/includes/title/SubpageImportTitleFactoryTest.php index 93ce0800..008cf5d9 100644 --- a/www/wiki/tests/phpunit/includes/title/SubpageImportTitleFactoryTest.php +++ b/www/wiki/tests/phpunit/includes/title/SubpageImportTitleFactoryTest.php @@ -80,7 +80,7 @@ class SubpageImportTitleFactoryTest extends MediaWikiTestCase { * @dataProvider failureProvider */ public function testFailures( Title $rootPage ) { - $this->setExpectedException( 'MWException' ); + $this->setExpectedException( MWException::class ); new SubpageImportTitleFactory( $rootPage ); } } diff --git a/www/wiki/tests/phpunit/includes/title/TitleValueTest.php b/www/wiki/tests/phpunit/includes/title/TitleValueTest.php index 4dbda74a..d221b431 100644 --- a/www/wiki/tests/phpunit/includes/title/TitleValueTest.php +++ b/www/wiki/tests/phpunit/includes/title/TitleValueTest.php @@ -78,7 +78,7 @@ class TitleValueTest extends MediaWikiTestCase { * @dataProvider badConstructorProvider */ public function testConstructionErrors( $ns, $text, $fragment, $interwiki ) { - $this->setExpectedException( 'InvalidArgumentException' ); + $this->setExpectedException( InvalidArgumentException::class ); new TitleValue( $ns, $text, $fragment, $interwiki ); } @@ -116,4 +116,33 @@ class TitleValueTest extends MediaWikiTestCase { $this->assertEquals( $text, $title->getText() ); } + + public function provideTestToString() { + yield [ + new TitleValue( 0, 'Foo' ), + '0:Foo' + ]; + yield [ + new TitleValue( 1, 'Bar_Baz' ), + '1:Bar_Baz' + ]; + yield [ + new TitleValue( 9, 'JoJo', 'Frag' ), + '9:JoJo#Frag' + ]; + yield [ + new TitleValue( 200, 'tea', 'Fragment', 'wikicode' ), + 'wikicode:200:tea#Fragment' + ]; + } + + /** + * @dataProvider provideTestToString + */ + public function testToString( TitleValue $value, $expected ) { + $this->assertSame( + $expected, + $value->__toString() + ); + } } diff --git a/www/wiki/tests/phpunit/includes/upload/UploadBaseTest.php b/www/wiki/tests/phpunit/includes/upload/UploadBaseTest.php index dd68cdca..a80262e9 100644 --- a/www/wiki/tests/phpunit/includes/upload/UploadBaseTest.php +++ b/www/wiki/tests/phpunit/includes/upload/UploadBaseTest.php @@ -103,6 +103,8 @@ class UploadBaseTest extends MediaWikiTestCase { } /** + * @covers UploadBase::verifyUpload + * * test uploading a 100 bytes file with $wgMaxUploadSize = 100 * * This method should be abstracted so we can test different settings. @@ -126,6 +128,7 @@ class UploadBaseTest extends MediaWikiTestCase { } /** + * @covers UploadBase::checkSvgScriptCallback * @dataProvider provideCheckSvgScriptCallback */ public function testCheckSvgScriptCallback( $svg, $wellFormed, $filterMatch, $message ) { @@ -135,7 +138,7 @@ class UploadBaseTest extends MediaWikiTestCase { } public static function provideCheckSvgScriptCallback() { - // @codingStandardsIgnoreStart Generic.Files.LineLength + // phpcs:disable Generic.Files.LineLength return [ // html5sec SVG vectors [ @@ -508,10 +511,11 @@ class UploadBaseTest extends MediaWikiTestCase { 'DTD with aliased entities apos (Should be allowed)' ] ]; - // @codingStandardsIgnoreEnd + // phpcs:enable } /** + * @covers UploadBase::detectScriptInSvg * @dataProvider provideDetectScriptInSvg */ public function testDetectScriptInSvg( $svg, $expected, $message ) { @@ -552,12 +556,13 @@ class UploadBaseTest extends MediaWikiTestCase { } /** + * @covers UploadBase::checkXMLEncodingMissmatch * @dataProvider provideCheckXMLEncodingMissmatch */ public function testCheckXMLEncodingMissmatch( $fileContents, $evil ) { $filename = $this->getNewTempFile(); file_put_contents( $filename, $fileContents ); - $this->assertSame( UploadBase::checkXMLEncodingMissmatch( $filename ), $evil ); + $this->assertSame( $evil, UploadBase::checkXMLEncodingMissmatch( $filename ) ); } public function provideCheckXMLEncodingMissmatch() { diff --git a/www/wiki/tests/phpunit/includes/upload/UploadFromUrlTest.php b/www/wiki/tests/phpunit/includes/upload/UploadFromUrlTest.php index 62081aa3..a69a137b 100644 --- a/www/wiki/tests/phpunit/includes/upload/UploadFromUrlTest.php +++ b/www/wiki/tests/phpunit/includes/upload/UploadFromUrlTest.php @@ -22,7 +22,7 @@ class UploadFromUrlTest extends ApiTestCase { } protected function doApiRequest( array $params, array $unused = null, - $appendModule = false, User $user = null + $appendModule = false, User $user = null, $tokenType = null ) { global $wgRequest; diff --git a/www/wiki/tests/phpunit/includes/upload/UploadStashTest.php b/www/wiki/tests/phpunit/includes/upload/UploadStashTest.php index 7c40a2df..39acbb07 100644 --- a/www/wiki/tests/phpunit/includes/upload/UploadStashTest.php +++ b/www/wiki/tests/phpunit/includes/upload/UploadStashTest.php @@ -14,14 +14,13 @@ class UploadStashTest extends MediaWikiTestCase { /** * @var string */ - private $bug29408File; + private $tmpFile; protected function setUp() { parent::setUp(); - // Setup a file for T31408 - $this->bug29408File = wfTempDir() . '/bug29408'; - file_put_contents( $this->bug29408File, "\x00" ); + $this->tmpFile = $this->getNewTempFile(); + file_put_contents( $this->tmpFile, "\x00" ); self::$users = [ 'sysop' => new TestUser( @@ -39,18 +38,6 @@ class UploadStashTest extends MediaWikiTestCase { ]; } - protected function tearDown() { - if ( file_exists( $this->bug29408File . "." ) ) { - unlink( $this->bug29408File . "." ); - } - - if ( file_exists( $this->bug29408File ) ) { - unlink( $this->bug29408File ); - } - - parent::tearDown(); - } - /** * @todo give this test a real name explaining what is being tested here */ @@ -61,7 +48,7 @@ class UploadStashTest extends MediaWikiTestCase { $stash = new UploadStash( $repo ); // Throws exception caught by PHPUnit on failure - $file = $stash->stashFile( $this->bug29408File ); + $file = $stash->stashFile( $this->tmpFile ); // We'll never reach this point if we hit T31408 $this->assertTrue( true, 'Unrecognized file without extension' ); @@ -104,4 +91,23 @@ class UploadStashTest extends MediaWikiTestCase { $this->assertTrue( UploadFromStash::isValidRequest( $request ) ); } + public function testExceptionWhenStoreTempFails() { + $mockRepoStoreStatusResult = Status::newFatal( 'TEST_ERROR' ); + $mockRepo = $this->getMockBuilder( FileRepo::class ) + ->disableOriginalConstructor() + ->getMock(); + $mockRepo->expects( $this->once() ) + ->method( 'storeTemp' ) + ->willReturn( $mockRepoStoreStatusResult ); + + $stash = new UploadStash( $mockRepo ); + try { + $stash->stashFile( $this->tmpFile ); + $this->fail( 'Expected UploadStashFileException not thrown' ); + } catch ( UploadStashFileException $e ) { + $this->assertInstanceOf( ILocalizedException::class, $e ); + } catch ( Exception $e ) { + $this->fail( 'Unexpected exception class ' . get_class( $e ) ); + } + } } diff --git a/www/wiki/tests/phpunit/includes/user/BotPasswordTest.php b/www/wiki/tests/phpunit/includes/user/BotPasswordTest.php index 09cf3507..3bbc2dfa 100644 --- a/www/wiki/tests/phpunit/includes/user/BotPasswordTest.php +++ b/www/wiki/tests/phpunit/includes/user/BotPasswordTest.php @@ -32,14 +32,14 @@ class BotPasswordTest extends MediaWikiTestCase { $this->testUser = $this->getMutableTestUser(); $this->testUserName = $this->testUser->getUser()->getName(); - $mock1 = $this->getMockForAbstractClass( 'CentralIdLookup' ); + $mock1 = $this->getMockForAbstractClass( CentralIdLookup::class ); $mock1->expects( $this->any() )->method( 'isAttached' ) ->will( $this->returnValue( true ) ); $mock1->expects( $this->any() )->method( 'lookupUserNames' ) ->will( $this->returnValue( [ $this->testUserName => 42, 'UTDummy' => 43, 'UTInvalid' => 0 ] ) ); $mock1->expects( $this->never() )->method( 'lookupCentralIds' ); - $mock2 = $this->getMockForAbstractClass( 'CentralIdLookup' ); + $mock2 = $this->getMockForAbstractClass( CentralIdLookup::class ); $mock2->expects( $this->any() )->method( 'isAttached' ) ->will( $this->returnValue( false ) ); $mock2->expects( $this->any() )->method( 'lookupUserNames' ) @@ -96,7 +96,7 @@ class BotPasswordTest extends MediaWikiTestCase { public function testBasics() { $user = $this->testUser->getUser(); $bp = BotPassword::newFromUser( $user, 'BotPassword' ); - $this->assertInstanceOf( 'BotPassword', $bp ); + $this->assertInstanceOf( BotPassword::class, $bp ); $this->assertTrue( $bp->isSaved() ); $this->assertSame( 42, $bp->getUserCentralId() ); $this->assertSame( 'BotPassword', $bp->getAppId() ); @@ -124,7 +124,7 @@ class BotPasswordTest extends MediaWikiTestCase { 'user' => $user, 'appId' => 'DoesNotExist' ] ); - $this->assertInstanceOf( 'BotPassword', $bp ); + $this->assertInstanceOf( BotPassword::class, $bp ); $this->assertFalse( $bp->isSaved() ); $this->assertSame( 42, $bp->getUserCentralId() ); $this->assertSame( 'DoesNotExist', $bp->getAppId() ); @@ -137,7 +137,7 @@ class BotPasswordTest extends MediaWikiTestCase { 'restrictions' => MWRestrictions::newFromJson( '{"IPAddresses":["127.0.0.0/8"]}' ), 'grants' => [ 'test' ], ] ); - $this->assertInstanceOf( 'BotPassword', $bp ); + $this->assertInstanceOf( BotPassword::class, $bp ); $this->assertFalse( $bp->isSaved() ); $this->assertSame( 43, $bp->getUserCentralId() ); $this->assertSame( 'DoesNotExist2', $bp->getAppId() ); @@ -149,7 +149,7 @@ class BotPasswordTest extends MediaWikiTestCase { 'centralId' => 45, 'appId' => 'DoesNotExist' ] ); - $this->assertInstanceOf( 'BotPassword', $bp ); + $this->assertInstanceOf( BotPassword::class, $bp ); $this->assertFalse( $bp->isSaved() ); $this->assertSame( 45, $bp->getUserCentralId() ); $this->assertSame( 'DoesNotExist', $bp->getAppId() ); @@ -159,7 +159,7 @@ class BotPasswordTest extends MediaWikiTestCase { 'user' => $user, 'appId' => 'BotPassword' ] ); - $this->assertInstanceOf( 'BotPassword', $bp ); + $this->assertInstanceOf( BotPassword::class, $bp ); $this->assertFalse( $bp->isSaved() ); $this->assertNull( BotPassword::newUnsaved( [ @@ -187,12 +187,12 @@ class BotPasswordTest extends MediaWikiTestCase { $bp = TestingAccessWrapper::newFromObject( BotPassword::newFromCentralId( 42, 'BotPassword' ) ); $password = $bp->getPassword(); - $this->assertInstanceOf( 'Password', $password ); + $this->assertInstanceOf( Password::class, $password ); $this->assertTrue( $password->equals( 'foobaz' ) ); $bp->centralId = 44; $password = $bp->getPassword(); - $this->assertInstanceOf( 'InvalidPassword', $password ); + $this->assertInstanceOf( InvalidPassword::class, $password ); $bp = TestingAccessWrapper::newFromObject( BotPassword::newFromCentralId( 42, 'BotPassword' ) ); $dbw = wfGetDB( DB_MASTER ); @@ -203,21 +203,21 @@ class BotPasswordTest extends MediaWikiTestCase { __METHOD__ ); $password = $bp->getPassword(); - $this->assertInstanceOf( 'InvalidPassword', $password ); + $this->assertInstanceOf( InvalidPassword::class, $password ); } public function testInvalidateAllPasswordsForUser() { $bp1 = TestingAccessWrapper::newFromObject( BotPassword::newFromCentralId( 42, 'BotPassword' ) ); $bp2 = TestingAccessWrapper::newFromObject( BotPassword::newFromCentralId( 43, 'BotPassword' ) ); - $this->assertNotInstanceOf( 'InvalidPassword', $bp1->getPassword(), 'sanity check' ); - $this->assertNotInstanceOf( 'InvalidPassword', $bp2->getPassword(), 'sanity check' ); + $this->assertNotInstanceOf( InvalidPassword::class, $bp1->getPassword(), 'sanity check' ); + $this->assertNotInstanceOf( InvalidPassword::class, $bp2->getPassword(), 'sanity check' ); BotPassword::invalidateAllPasswordsForUser( $this->testUserName ); - $this->assertInstanceOf( 'InvalidPassword', $bp1->getPassword() ); - $this->assertNotInstanceOf( 'InvalidPassword', $bp2->getPassword() ); + $this->assertInstanceOf( InvalidPassword::class, $bp1->getPassword() ); + $this->assertNotInstanceOf( InvalidPassword::class, $bp2->getPassword() ); $bp = TestingAccessWrapper::newFromObject( BotPassword::newFromCentralId( 42, 'BotPassword' ) ); - $this->assertInstanceOf( 'InvalidPassword', $bp->getPassword() ); + $this->assertInstanceOf( InvalidPassword::class, $bp->getPassword() ); } public function testRemoveAllPasswordsForUser() { @@ -312,7 +312,7 @@ class BotPasswordTest extends MediaWikiTestCase { ); // Failed restriction - $request = $this->getMockBuilder( 'FauxRequest' ) + $request = $this->getMockBuilder( FauxRequest::class ) ->setMethods( [ 'getIP' ] ) ->getMock(); $request->expects( $this->any() )->method( 'getIP' ) @@ -333,7 +333,7 @@ class BotPasswordTest extends MediaWikiTestCase { 'sanity check' ); $status = BotPassword::login( "{$this->testUserName}@BotPassword", 'foobaz', $request ); - $this->assertInstanceOf( 'Status', $status ); + $this->assertInstanceOf( Status::class, $status ); $this->assertTrue( $status->isGood() ); $session = $status->getValue(); $this->assertInstanceOf( MediaWiki\Session\Session::class, $session ); @@ -368,7 +368,7 @@ class BotPasswordTest extends MediaWikiTestCase { $this->assertFalse( $bp->save( 'update', $passwordHash ) ); $this->assertTrue( $bp->save( 'insert', $passwordHash ) ); $bp2 = BotPassword::newFromCentralId( 42, 'TestSave', BotPassword::READ_LATEST ); - $this->assertInstanceOf( 'BotPassword', $bp2 ); + $this->assertInstanceOf( BotPassword::class, $bp2 ); $this->assertEquals( $bp->getUserCentralId(), $bp2->getUserCentralId() ); $this->assertEquals( $bp->getAppId(), $bp2->getAppId() ); $this->assertEquals( $bp->getToken(), $bp2->getToken() ); @@ -376,7 +376,7 @@ class BotPasswordTest extends MediaWikiTestCase { $this->assertEquals( $bp->getGrants(), $bp2->getGrants() ); $pw = TestingAccessWrapper::newFromObject( $bp )->getPassword(); if ( $password === null ) { - $this->assertInstanceOf( 'InvalidPassword', $pw ); + $this->assertInstanceOf( InvalidPassword::class, $pw ); } else { $this->assertTrue( $pw->equals( $password ) ); } @@ -388,11 +388,11 @@ class BotPasswordTest extends MediaWikiTestCase { $this->assertTrue( $bp->save( 'update' ) ); $this->assertNotEquals( $token, $bp->getToken() ); $bp2 = BotPassword::newFromCentralId( 42, 'TestSave', BotPassword::READ_LATEST ); - $this->assertInstanceOf( 'BotPassword', $bp2 ); + $this->assertInstanceOf( BotPassword::class, $bp2 ); $this->assertEquals( $bp->getToken(), $bp2->getToken() ); $pw = TestingAccessWrapper::newFromObject( $bp )->getPassword(); if ( $password === null ) { - $this->assertInstanceOf( 'InvalidPassword', $pw ); + $this->assertInstanceOf( InvalidPassword::class, $pw ); } else { $this->assertTrue( $pw->equals( $password ) ); } diff --git a/www/wiki/tests/phpunit/includes/user/CentralIdLookupTest.php b/www/wiki/tests/phpunit/includes/user/CentralIdLookupTest.php index 789cf08f..dc9fe2ad 100644 --- a/www/wiki/tests/phpunit/includes/user/CentralIdLookupTest.php +++ b/www/wiki/tests/phpunit/includes/user/CentralIdLookupTest.php @@ -9,16 +9,16 @@ use Wikimedia\TestingAccessWrapper; class CentralIdLookupTest extends MediaWikiTestCase { public function testFactory() { - $mock = $this->getMockForAbstractClass( 'CentralIdLookup' ); + $mock = $this->getMockForAbstractClass( CentralIdLookup::class ); $this->setMwGlobals( [ 'wgCentralIdLookupProviders' => [ - 'local' => [ 'class' => 'LocalIdLookup' ], - 'local2' => [ 'class' => 'LocalIdLookup' ], + 'local' => [ 'class' => LocalIdLookup::class ], + 'local2' => [ 'class' => LocalIdLookup::class ], 'mock' => [ 'factory' => function () use ( $mock ) { return $mock; } ], - 'bad' => [ 'class' => 'stdClass' ], + 'bad' => [ 'class' => stdClass::class ], ], 'wgCentralIdLookupProvider' => 'mock', ] ); @@ -29,13 +29,13 @@ class CentralIdLookupTest extends MediaWikiTestCase { $local = CentralIdLookup::factory( 'local' ); $this->assertNotSame( $mock, $local ); - $this->assertInstanceOf( 'LocalIdLookup', $local ); + $this->assertInstanceOf( LocalIdLookup::class, $local ); $this->assertSame( $local, CentralIdLookup::factory( 'local' ) ); $this->assertSame( 'local', $local->getProviderId() ); $local2 = CentralIdLookup::factory( 'local2' ); $this->assertNotSame( $local, $local2 ); - $this->assertInstanceOf( 'LocalIdLookup', $local2 ); + $this->assertInstanceOf( LocalIdLookup::class, $local2 ); $this->assertSame( 'local2', $local2->getProviderId() ); $this->assertNull( CentralIdLookup::factory( 'unconfigured' ) ); @@ -44,14 +44,14 @@ class CentralIdLookupTest extends MediaWikiTestCase { public function testCheckAudience() { $mock = TestingAccessWrapper::newFromObject( - $this->getMockForAbstractClass( 'CentralIdLookup' ) + $this->getMockForAbstractClass( CentralIdLookup::class ) ); $user = static::getTestSysop()->getUser(); $this->assertSame( $user, $mock->checkAudience( $user ) ); $user = $mock->checkAudience( CentralIdLookup::AUDIENCE_PUBLIC ); - $this->assertInstanceOf( 'User', $user ); + $this->assertInstanceOf( User::class, $user ); $this->assertSame( 0, $user->getId() ); $this->assertNull( $mock->checkAudience( CentralIdLookup::AUDIENCE_RAW ) ); @@ -65,7 +65,7 @@ class CentralIdLookupTest extends MediaWikiTestCase { } public function testNameFromCentralId() { - $mock = $this->getMockForAbstractClass( 'CentralIdLookup' ); + $mock = $this->getMockForAbstractClass( CentralIdLookup::class ); $mock->expects( $this->once() )->method( 'lookupCentralIds' ) ->with( $this->equalTo( [ 15 => null ] ), @@ -86,7 +86,7 @@ class CentralIdLookupTest extends MediaWikiTestCase { * @param bool $succeeds */ public function testLocalUserFromCentralId( $name, $succeeds ) { - $mock = $this->getMockForAbstractClass( 'CentralIdLookup' ); + $mock = $this->getMockForAbstractClass( CentralIdLookup::class ); $mock->expects( $this->any() )->method( 'isAttached' ) ->will( $this->returnValue( true ) ); $mock->expects( $this->once() )->method( 'lookupCentralIds' ) @@ -101,13 +101,13 @@ class CentralIdLookupTest extends MediaWikiTestCase { 42, CentralIdLookup::AUDIENCE_RAW, CentralIdLookup::READ_LATEST ); if ( $succeeds ) { - $this->assertInstanceOf( 'User', $user ); + $this->assertInstanceOf( User::class, $user ); $this->assertSame( $name, $user->getName() ); } else { $this->assertNull( $user ); } - $mock = $this->getMockForAbstractClass( 'CentralIdLookup' ); + $mock = $this->getMockForAbstractClass( CentralIdLookup::class ); $mock->expects( $this->any() )->method( 'isAttached' ) ->will( $this->returnValue( false ) ); $mock->expects( $this->once() )->method( 'lookupCentralIds' ) @@ -133,7 +133,7 @@ class CentralIdLookupTest extends MediaWikiTestCase { } public function testCentralIdFromName() { - $mock = $this->getMockForAbstractClass( 'CentralIdLookup' ); + $mock = $this->getMockForAbstractClass( CentralIdLookup::class ); $mock->expects( $this->once() )->method( 'lookupUserNames' ) ->with( $this->equalTo( [ 'FooBar' => 0 ] ), @@ -149,7 +149,7 @@ class CentralIdLookupTest extends MediaWikiTestCase { } public function testCentralIdFromLocalUser() { - $mock = $this->getMockForAbstractClass( 'CentralIdLookup' ); + $mock = $this->getMockForAbstractClass( CentralIdLookup::class ); $mock->expects( $this->any() )->method( 'isAttached' ) ->will( $this->returnValue( true ) ); $mock->expects( $this->once() )->method( 'lookupUserNames' ) @@ -167,7 +167,7 @@ class CentralIdLookupTest extends MediaWikiTestCase { ) ); - $mock = $this->getMockForAbstractClass( 'CentralIdLookup' ); + $mock = $this->getMockForAbstractClass( CentralIdLookup::class ); $mock->expects( $this->any() )->method( 'isAttached' ) ->will( $this->returnValue( false ) ); $mock->expects( $this->never() )->method( 'lookupUserNames' ); diff --git a/www/wiki/tests/phpunit/includes/user/ExternalUserNamesTest.php b/www/wiki/tests/phpunit/includes/user/ExternalUserNamesTest.php new file mode 100644 index 00000000..429bda46 --- /dev/null +++ b/www/wiki/tests/phpunit/includes/user/ExternalUserNamesTest.php @@ -0,0 +1,131 @@ +<?php + +use MediaWiki\Interwiki\InterwikiLookup; + +/** + * @covers ExternalUserNames + */ +class ExternalUserNamesTest extends MediaWikiTestCase { + + public function provideGetUserLinkTitle() { + return [ + [ 'valid:>User1', Title::makeTitle( NS_MAIN, ':User:User1', '', 'valid' ) ], + [ + 'valid:valid:>User1', + Title::makeTitle( NS_MAIN, 'valid::User:User1', '', 'valid' ) + ], + [ + '127.0.0.1', + Title::makeTitle( NS_SPECIAL, 'Contributions/127.0.0.1', '', '' ) + ], + [ 'invalid:>User1', null ] + ]; + } + + /** + * @covers ExternalUserNames::getUserLinkTitle + * @dataProvider provideGetUserLinkTitle + */ + public function testGetUserLinkTitle( $username, $expected ) { + $interwikiLookupMock = $this->getMockBuilder( InterwikiLookup::class ) + ->getMock(); + + $interwikiValueMap = [ + [ 'valid', true ], + [ 'invalid', false ] + ]; + $interwikiLookupMock->expects( $this->any() ) + ->method( 'isValidInterwiki' ) + ->will( $this->returnValueMap( $interwikiValueMap ) ); + + $this->setService( 'InterwikiLookup', $interwikiLookupMock ); + + $this->assertEquals( + $expected, + ExternalUserNames::getUserLinkTitle( $username ) + ); + } + + public function provideApplyPrefix() { + return [ + [ 'User1', 'prefix', 'prefix>User1' ], + [ 'User1', 'prefix:>', 'prefix>User1' ], + [ 'User1', 'prefix:', 'prefix>User1' ], + ]; + } + + /** + * @covers ExternalUserNames::applyPrefix + * @dataProvider provideApplyPrefix + */ + public function testApplyPrefix( $username, $prefix, $expected ) { + $externalUserNames = new ExternalUserNames( $prefix, true ); + + $this->assertSame( + $expected, + $externalUserNames->applyPrefix( $username ) + ); + } + + public function provideAddPrefix() { + return [ + [ 'User1', 'prefix', 'prefix>User1' ], + [ 'User2', 'prefix2', 'prefix2>User2' ], + [ 'User3', 'prefix3', 'prefix3>User3' ], + ]; + } + + /** + * @covers ExternalUserNames::addPrefix + * @dataProvider provideAddPrefix + */ + public function testAddPrefix( $username, $prefix, $expected ) { + $externalUserNames = new ExternalUserNames( $prefix, true ); + + $this->assertSame( + $expected, + $externalUserNames->addPrefix( $username ) + ); + } + + public function provideIsExternal() { + return [ + [ 'User1', false ], + [ '>User1', true ], + [ 'prefix>User1', true ], + [ 'prefix:>User1', true ], + ]; + } + + /** + * @covers ExternalUserNames::isExternal + * @dataProvider provideIsExternal + */ + public function testIsExternal( $username, $expected ) { + $this->assertSame( + $expected, + ExternalUserNames::isExternal( $username ) + ); + } + + public function provideGetLocal() { + return [ + [ 'User1', 'User1' ], + [ '>User2', 'User2' ], + [ 'prefix>User3', 'User3' ], + [ 'prefix:>User4', 'User4' ], + ]; + } + + /** + * @covers ExternalUserNames::getLocal + * @dataProvider provideGetLocal + */ + public function testGetLocal( $username, $expected ) { + $this->assertSame( + $expected, + ExternalUserNames::getLocal( $username ) + ); + } + +} diff --git a/www/wiki/tests/phpunit/includes/user/PasswordResetTest.php b/www/wiki/tests/phpunit/includes/user/PasswordResetTest.php index 53f02df6..1f578ab0 100644 --- a/www/wiki/tests/phpunit/includes/user/PasswordResetTest.php +++ b/www/wiki/tests/phpunit/includes/user/PasswordResetTest.php @@ -3,9 +3,10 @@ use MediaWiki\Auth\AuthManager; /** + * @covers PasswordReset * @group Database */ -class PasswordResetTest extends PHPUnit_Framework_TestCase { +class PasswordResetTest extends MediaWikiTestCase { /** * @dataProvider provideIsAllowed */ @@ -150,6 +151,12 @@ class PasswordResetTest extends PHPUnit_Framework_TestCase { 'EnableEmail' => true, ] ); + // Unregister the hooks for proper unit testing + $this->mergeMwGlobalArrayValue( 'wgHooks', [ + 'User::mailPasswordInternal' => [], + 'SpecialPasswordResetOnSubmit' => [], + ] ); + $authManager = $this->getMockBuilder( AuthManager::class )->disableOriginalConstructor() ->getMock(); $authManager->expects( $this->any() )->method( 'allowsAuthenticationDataChange' ) diff --git a/www/wiki/tests/phpunit/includes/user/UserArrayFromResultTest.php b/www/wiki/tests/phpunit/includes/user/UserArrayFromResultTest.php index cf980b12..beaacec8 100644 --- a/www/wiki/tests/phpunit/includes/user/UserArrayFromResultTest.php +++ b/www/wiki/tests/phpunit/includes/user/UserArrayFromResultTest.php @@ -7,7 +7,7 @@ class UserArrayFromResultTest extends MediaWikiTestCase { private function getMockResultWrapper( $row = null, $numRows = 1 ) { - $resultWrapper = $this->getMockBuilder( 'ResultWrapper' ) + $resultWrapper = $this->getMockBuilder( Wikimedia\Rdbms\ResultWrapper::class ) ->disableOriginalConstructor(); $resultWrapper = $resultWrapper->getMock(); @@ -57,7 +57,7 @@ class UserArrayFromResultTest extends MediaWikiTestCase { $this->assertEquals( $resultWrapper, $object->res ); $this->assertSame( 0, $object->key ); - $this->assertInstanceOf( 'User', $object->current ); + $this->assertInstanceOf( User::class, $object->current ); $this->assertEquals( $username, $object->current->mName ); } @@ -88,7 +88,7 @@ class UserArrayFromResultTest extends MediaWikiTestCase { $username = 'addshore'; $userRow = $this->getRowWithUsername( $username ); $object = $this->getUserArrayFromResult( $this->getMockResultWrapper( $userRow ) ); - $this->assertInstanceOf( 'User', $object->current() ); + $this->assertInstanceOf( User::class, $object->current() ); $this->assertEquals( $username, $object->current()->mName ); } diff --git a/www/wiki/tests/phpunit/includes/user/UserGroupMembershipTest.php b/www/wiki/tests/phpunit/includes/user/UserGroupMembershipTest.php index f95e3871..4862747b 100644 --- a/www/wiki/tests/phpunit/includes/user/UserGroupMembershipTest.php +++ b/www/wiki/tests/phpunit/includes/user/UserGroupMembershipTest.php @@ -4,6 +4,9 @@ * @group Database */ class UserGroupMembershipTest extends MediaWikiTestCase { + + protected $tablesUsed = [ 'user', 'user_groups' ]; + /** * @var User Belongs to no groups */ diff --git a/www/wiki/tests/phpunit/includes/user/UserTest.php b/www/wiki/tests/phpunit/includes/user/UserTest.php index aa368de7..ebfecbca 100644 --- a/www/wiki/tests/phpunit/includes/user/UserTest.php +++ b/www/wiki/tests/phpunit/includes/user/UserTest.php @@ -21,7 +21,9 @@ class UserTest extends MediaWikiTestCase { $this->setMwGlobals( [ 'wgGroupPermissions' => [], 'wgRevokePermissions' => [], + 'wgActorTableSchemaMigrationStage' => MIGRATION_WRITE_BOTH, ] ); + $this->overrideMwServices(); $this->setUpPermissionGlobals(); @@ -121,7 +123,7 @@ class UserTest extends MediaWikiTestCase { $this->assertContains( 'nukeworld', $rights ); // Add a Session that limits rights - $mock = $this->getMockBuilder( stdclass::class ) + $mock = $this->getMockBuilder( stdClass::class ) ->setMethods( [ 'getAllowedUserRights', 'deregisterSession', 'getSessionId' ] ) ->getMock(); $mock->method( 'getAllowedUserRights' )->willReturn( [ 'test', 'writetest' ] ); @@ -236,6 +238,8 @@ class UserTest extends MediaWikiTestCase { * Test, if for all rights a right- message exist, * which is used on Special:ListGroupRights as help text * Extensions and core + * + * @coversNothing */ public function testAllRightsWithMessage() { // Getting all user rights, for core: User::$mCoreRights, for extensions: $wgAvailableRights @@ -346,6 +350,7 @@ class UserTest extends MediaWikiTestCase { $user->setOption( 'userjs-someoption', 'test' ); $user->setOption( 'rclimit', 200 ); + $user->setOption( 'wpwatchlistdays', '0' ); $user->saveSettings(); $user = User::newFromName( $user->getName() ); @@ -357,6 +362,11 @@ class UserTest extends MediaWikiTestCase { MediaWikiServices::getInstance()->getMainWANObjectCache()->clearProcessCache(); $this->assertEquals( 'test', $user->getOption( 'userjs-someoption' ) ); $this->assertEquals( 200, $user->getOption( 'rclimit' ) ); + + // Check that an option saved as a string '0' is returned as an integer. + $user = User::newFromName( $user->getName() ); + $user->load( User::READ_LATEST ); + $this->assertSame( 0, $user->getOption( 'wpwatchlistdays' ) ); } /** @@ -600,7 +610,13 @@ class UserTest extends MediaWikiTestCase { 'wgSecretKey' => MWCryptRand::generateHex( 64, true ), ] ); + // Unregister the hooks for proper unit testing + $this->mergeMwGlobalArrayValue( 'wgHooks', [ + 'PerformRetroactiveAutoblock' => [] + ] ); + // 1. Log in a test user, and block them. + $userBlocker = $this->getTestSysop()->getUser(); $user1tmp = $this->getTestUser()->getUser(); $request1 = new FauxRequest(); $request1->getSession()->setUser( $user1tmp ); @@ -609,8 +625,11 @@ class UserTest extends MediaWikiTestCase { 'enableAutoblock' => true, 'expiry' => wfTimestamp( TS_MW, $expiryFiveHours ), ] ); + $block->setBlocker( $this->getTestSysop()->getUser() ); $block->setTarget( $user1tmp ); - $block->insert(); + $block->setBlocker( $userBlocker ); + $res = $block->insert(); + $this->assertTrue( (bool)$res['id'], 'Failed to insert block' ); $user1 = User::newFromSession( $request1 ); $user1->mBlock = $block; $user1->load(); @@ -639,7 +658,8 @@ class UserTest extends MediaWikiTestCase { $this->assertTrue( $user2->isAnon() ); $this->assertFalse( $user2->isLoggedIn() ); $this->assertTrue( $user2->isBlocked() ); - $this->assertEquals( true, $user2->getBlock()->isAutoblocking() ); // Non-strict type-check. + // Non-strict type-check. + $this->assertEquals( true, $user2->getBlock()->isAutoblocking(), 'Autoblock does not work' ); // Can't directly compare the objects becuase of member type differences. // One day this will work: $this->assertEquals( $block, $user2->getBlock() ); $this->assertEquals( $block->getId(), $user2->getBlock()->getId() ); @@ -672,13 +692,22 @@ class UserTest extends MediaWikiTestCase { 'wgSecretKey' => MWCryptRand::generateHex( 64, true ), ] ); + // Unregister the hooks for proper unit testing + $this->mergeMwGlobalArrayValue( 'wgHooks', [ + 'PerformRetroactiveAutoblock' => [] + ] ); + // 1. Log in a test user, and block them. + $userBlocker = $this->getTestSysop()->getUser(); $testUser = $this->getTestUser()->getUser(); $request1 = new FauxRequest(); $request1->getSession()->setUser( $testUser ); $block = new Block( [ 'enableAutoblock' => true ] ); + $block->setBlocker( $this->getTestSysop()->getUser() ); $block->setTarget( $testUser ); - $block->insert(); + $block->setBlocker( $userBlocker ); + $res = $block->insert(); + $this->assertTrue( (bool)$res['id'], 'Failed to insert block' ); $user = User::newFromSession( $request1 ); $user->mBlock = $block; $user->load(); @@ -708,13 +737,23 @@ class UserTest extends MediaWikiTestCase { 'wgCookiePrefix' => 'wm_infinite_block', 'wgSecretKey' => MWCryptRand::generateHex( 64, true ), ] ); + + // Unregister the hooks for proper unit testing + $this->mergeMwGlobalArrayValue( 'wgHooks', [ + 'PerformRetroactiveAutoblock' => [] + ] ); + // 1. Log in a test user, and block them indefinitely. + $userBlocker = $this->getTestSysop()->getUser(); $user1Tmp = $this->getTestUser()->getUser(); $request1 = new FauxRequest(); $request1->getSession()->setUser( $user1Tmp ); $block = new Block( [ 'enableAutoblock' => true, 'expiry' => 'infinity' ] ); + $block->setBlocker( $this->getTestSysop()->getUser() ); $block->setTarget( $user1Tmp ); - $block->insert(); + $block->setBlocker( $userBlocker ); + $res = $block->insert(); + $this->assertTrue( (bool)$res['id'], 'Failed to insert block' ); $user1 = User::newFromSession( $request1 ); $user1->mBlock = $block; $user1->load(); @@ -756,30 +795,36 @@ class UserTest extends MediaWikiTestCase { } public function testSoftBlockRanges() { - global $wgUser; - - $this->setMwGlobals( [ - 'wgSoftBlockRanges' => [ '10.0.0.0/8' ], - 'wgUser' => null, - ] ); + $setSessionUser = function ( User $user, WebRequest $request ) { + $this->setMwGlobals( 'wgUser', $user ); + RequestContext::getMain()->setUser( $user ); + RequestContext::getMain()->setRequest( $request ); + TestingAccessWrapper::newFromObject( $user )->mRequest = $request; + $request->getSession()->setUser( $user ); + }; + $this->setMwGlobals( 'wgSoftBlockRanges', [ '10.0.0.0/8' ] ); // IP isn't in $wgSoftBlockRanges + $wgUser = new User(); $request = new FauxRequest(); $request->setIP( '192.168.0.1' ); - $wgUser = User::newFromSession( $request ); + $setSessionUser( $wgUser, $request ); $this->assertNull( $wgUser->getBlock() ); // IP is in $wgSoftBlockRanges + $wgUser = new User(); $request = new FauxRequest(); $request->setIP( '10.20.30.40' ); - $wgUser = User::newFromSession( $request ); + $setSessionUser( $wgUser, $request ); $block = $wgUser->getBlock(); $this->assertInstanceOf( Block::class, $block ); $this->assertSame( 'wgSoftBlockRanges', $block->getSystemBlockType() ); // Make sure the block is really soft - $request->getSession()->setUser( $this->getTestUser()->getUser() ); - $wgUser = User::newFromSession( $request ); + $wgUser = $this->getTestUser()->getUser(); + $request = new FauxRequest(); + $request->setIP( '10.20.30.40' ); + $setSessionUser( $wgUser, $request ); $this->assertFalse( $wgUser->isAnon(), 'sanity check' ); $this->assertNull( $wgUser->getBlock() ); } @@ -795,13 +840,22 @@ class UserTest extends MediaWikiTestCase { 'wgSecretKey' => MWCryptRand::generateHex( 64, true ), ] ); + // Unregister the hooks for proper unit testing + $this->mergeMwGlobalArrayValue( 'wgHooks', [ + 'PerformRetroactiveAutoblock' => [] + ] ); + // 1. Log in a blocked test user. + $userBlocker = $this->getTestSysop()->getUser(); $user1tmp = $this->getTestUser()->getUser(); $request1 = new FauxRequest(); $request1->getSession()->setUser( $user1tmp ); $block = new Block( [ 'enableAutoblock' => true ] ); + $block->setBlocker( $this->getTestSysop()->getUser() ); $block->setTarget( $user1tmp ); - $block->insert(); + $block->setBlocker( $userBlocker ); + $res = $block->insert(); + $this->assertTrue( (bool)$res['id'], 'Failed to insert block' ); $user1 = User::newFromSession( $request1 ); $user1->mBlock = $block; $user1->load(); @@ -832,13 +886,22 @@ class UserTest extends MediaWikiTestCase { 'wgSecretKey' => null, ] ); + // Unregister the hooks for proper unit testing + $this->mergeMwGlobalArrayValue( 'wgHooks', [ + 'PerformRetroactiveAutoblock' => [] + ] ); + // 1. Log in a blocked test user. + $userBlocker = $this->getTestSysop()->getUser(); $user1tmp = $this->getTestUser()->getUser(); $request1 = new FauxRequest(); $request1->getSession()->setUser( $user1tmp ); $block = new Block( [ 'enableAutoblock' => true ] ); + $block->setBlocker( $this->getTestSysop()->getUser() ); $block->setTarget( $user1tmp ); - $block->insert(); + $block->setBlocker( $userBlocker ); + $res = $block->insert(); + $this->assertTrue( (bool)$res['id'], 'Failed to insert block' ); $user1 = User::newFromSession( $request1 ); $user1->mBlock = $block; $user1->load(); @@ -861,6 +924,9 @@ class UserTest extends MediaWikiTestCase { $block->delete(); } + /** + * @covers User::isPingLimitable + */ public function testIsPingLimitable() { $request = new FauxRequest(); $request->setIP( '1.2.3.4' ); @@ -897,6 +963,7 @@ class UserTest extends MediaWikiTestCase { } /** + * @covers User::getExperienceLevel * @dataProvider provideExperienceLevel */ public function testExperienceLevel( $editCount, $memberSince, $expLevel ) { @@ -908,24 +975,25 @@ class UserTest extends MediaWikiTestCase { ] ); $db = wfGetDB( DB_MASTER ); - - $data = new stdClass(); - $data->user_id = 1; - $data->user_name = 'name'; - $data->user_real_name = 'Real Name'; - $data->user_touched = 1; - $data->user_token = 'token'; - $data->user_email = 'a@a.a'; - $data->user_email_authenticated = null; - $data->user_email_token = 'token'; - $data->user_email_token_expires = null; - $data->user_editcount = $editCount; - $data->user_registration = $db->timestamp( time() - $memberSince * 86400 ); - $user = User::newFromRow( $data ); + $userQuery = User::getQueryInfo(); + $row = $db->selectRow( + $userQuery['tables'], + $userQuery['fields'], + [ 'user_id' => $this->getTestUser()->getUser()->getId() ], + __METHOD__, + [], + $userQuery['joins'] + ); + $row->user_editcount = $editCount; + $row->user_registration = $db->timestamp( time() - $memberSince * 86400 ); + $user = User::newFromRow( $row ); $this->assertEquals( $expLevel, $user->getExperienceLevel() ); } + /** + * @covers User::getExperienceLevel + */ public function testExperienceLevelAnon() { $user = User::newFromName( '10.11.12.13', false ); @@ -977,4 +1045,164 @@ class UserTest extends MediaWikiTestCase { ); $this->assertTrue( User::isLocallyBlockedProxy( $ip ) ); } + + public function testActorId() { + $this->hideDeprecated( 'User::selectFields' ); + + // Newly-created user has an actor ID + $user = User::createNew( 'UserTestActorId1' ); + $id = $user->getId(); + $this->assertTrue( $user->getActorId() > 0, 'User::createNew sets an actor ID' ); + + $user = User::newFromName( 'UserTestActorId2' ); + $user->addToDatabase(); + $this->assertTrue( $user->getActorId() > 0, 'User::addToDatabase sets an actor ID' ); + + $user = User::newFromName( 'UserTestActorId1' ); + $this->assertTrue( $user->getActorId() > 0, 'Actor ID can be retrieved for user loaded by name' ); + + $user = User::newFromId( $id ); + $this->assertTrue( $user->getActorId() > 0, 'Actor ID can be retrieved for user loaded by ID' ); + + $user2 = User::newFromActorId( $user->getActorId() ); + $this->assertEquals( $user->getId(), $user2->getId(), + 'User::newFromActorId works for an existing user' ); + + $row = $this->db->selectRow( 'user', User::selectFields(), [ 'user_id' => $id ], __METHOD__ ); + $user = User::newFromRow( $row ); + $this->assertTrue( $user->getActorId() > 0, + 'Actor ID can be retrieved for user loaded with User::selectFields()' ); + + $this->db->delete( 'actor', [ 'actor_user' => $id ], __METHOD__ ); + User::purge( wfWikiId(), $id ); + // Because WANObjectCache->delete() stupidly doesn't delete from the process cache. + ObjectCache::getMainWANInstance()->clearProcessCache(); + + $user = User::newFromId( $id ); + $this->assertFalse( $user->getActorId() > 0, 'No Actor ID by default if none in database' ); + $this->assertTrue( $user->getActorId( $this->db ) > 0, 'Actor ID can be created if none in db' ); + + $user->setName( 'UserTestActorId4-renamed' ); + $user->saveSettings(); + $this->assertEquals( + $user->getName(), + $this->db->selectField( + 'actor', 'actor_name', [ 'actor_id' => $user->getActorId() ], __METHOD__ + ), + 'User::saveSettings updates actor table for name change' + ); + + // For sanity + $ip = '192.168.12.34'; + $this->db->delete( 'actor', [ 'actor_name' => $ip ], __METHOD__ ); + + $user = User::newFromName( $ip, false ); + $this->assertFalse( $user->getActorId() > 0, 'Anonymous user has no actor ID by default' ); + $this->assertTrue( $user->getActorId( $this->db ) > 0, + 'Actor ID can be created for an anonymous user' ); + + $user = User::newFromName( $ip, false ); + $this->assertTrue( $user->getActorId() > 0, 'Actor ID can be loaded for an anonymous user' ); + $user2 = User::newFromActorId( $user->getActorId() ); + $this->assertEquals( $user->getName(), $user2->getName(), + 'User::newFromActorId works for an anonymous user' ); + } + + public function testNewFromAnyId() { + // Registered user + $user = $this->getTestUser()->getUser(); + for ( $i = 1; $i <= 7; $i++ ) { + $test = User::newFromAnyId( + ( $i & 1 ) ? $user->getId() : null, + ( $i & 2 ) ? $user->getName() : null, + ( $i & 4 ) ? $user->getActorId() : null + ); + $this->assertSame( $user->getId(), $test->getId() ); + $this->assertSame( $user->getName(), $test->getName() ); + $this->assertSame( $user->getActorId(), $test->getActorId() ); + } + + // Anon user. Can't load by only user ID when that's 0. + $user = User::newFromName( '192.168.12.34', false ); + $user->getActorId( $this->db ); // Make sure an actor ID exists + + $test = User::newFromAnyId( null, '192.168.12.34', null ); + $this->assertSame( $user->getId(), $test->getId() ); + $this->assertSame( $user->getName(), $test->getName() ); + $this->assertSame( $user->getActorId(), $test->getActorId() ); + $test = User::newFromAnyId( null, null, $user->getActorId() ); + $this->assertSame( $user->getId(), $test->getId() ); + $this->assertSame( $user->getName(), $test->getName() ); + $this->assertSame( $user->getActorId(), $test->getActorId() ); + + // Bogus data should still "work" as long as nothing triggers a ->load(), + // and accessing the specified data shouldn't do that. + $test = User::newFromAnyId( 123456, 'Bogus', 654321 ); + $this->assertSame( 123456, $test->getId() ); + $this->assertSame( 'Bogus', $test->getName() ); + $this->assertSame( 654321, $test->getActorId() ); + + // Exceptional cases + try { + User::newFromAnyId( null, null, null ); + $this->fail( 'Expected exception not thrown' ); + } catch ( InvalidArgumentException $ex ) { + } + try { + User::newFromAnyId( 0, null, 0 ); + $this->fail( 'Expected exception not thrown' ); + } catch ( InvalidArgumentException $ex ) { + } + } + + /** + * @covers User::getBlockedStatus + * @covers User::getBlock + * @covers User::blockedBy + * @covers User::blockedFor + * @covers User::isHidden + * @covers User::isBlockedFrom + */ + public function testBlockInstanceCache() { + // First, check the user isn't blocked + $user = $this->getMutableTestUser()->getUser(); + $ut = Title::makeTitle( NS_USER_TALK, $user->getName() ); + $this->assertNull( $user->getBlock( false ), 'sanity check' ); + $this->assertSame( '', $user->blockedBy(), 'sanity check' ); + $this->assertSame( '', $user->blockedFor(), 'sanity check' ); + $this->assertFalse( (bool)$user->isHidden(), 'sanity check' ); + $this->assertFalse( $user->isBlockedFrom( $ut ), 'sanity check' ); + + // Block the user + $blocker = $this->getTestSysop()->getUser(); + $block = new Block( [ + 'hideName' => true, + 'allowUsertalk' => false, + 'reason' => 'Because', + ] ); + $block->setTarget( $user ); + $block->setBlocker( $blocker ); + $res = $block->insert(); + $this->assertTrue( (bool)$res['id'], 'sanity check: Failed to insert block' ); + + // Clear cache and confirm it loaded the block properly + $user->clearInstanceCache(); + $this->assertInstanceOf( Block::class, $user->getBlock( false ) ); + $this->assertSame( $blocker->getName(), $user->blockedBy() ); + $this->assertSame( 'Because', $user->blockedFor() ); + $this->assertTrue( (bool)$user->isHidden() ); + $this->assertTrue( $user->isBlockedFrom( $ut ) ); + + // Unblock + $block->delete(); + + // Clear cache and confirm it loaded the not-blocked properly + $user->clearInstanceCache(); + $this->assertNull( $user->getBlock( false ) ); + $this->assertSame( '', $user->blockedBy() ); + $this->assertSame( '', $user->blockedFor() ); + $this->assertFalse( (bool)$user->isHidden() ); + $this->assertFalse( $user->isBlockedFrom( $ut ) ); + } + } diff --git a/www/wiki/tests/phpunit/includes/utils/AvroValidatorTest.php b/www/wiki/tests/phpunit/includes/utils/AvroValidatorTest.php index 7559e224..cf45f9fd 100644 --- a/www/wiki/tests/phpunit/includes/utils/AvroValidatorTest.php +++ b/www/wiki/tests/phpunit/includes/utils/AvroValidatorTest.php @@ -4,12 +4,18 @@ * * Ported from /t/inc/IP.t by avar. * - * @group IP * @todo Test methods in this call should be split into a method and a * dataprovider. */ -class AvroValidatorTest extends PHPUnit_Framework_TestCase { +/** + * @group IP + * @covers AvroValidator + */ +class AvroValidatorTest extends PHPUnit\Framework\TestCase { + + use MediaWikiCoversValidator; + public function setUp() { if ( !class_exists( 'AvroSchema' ) ) { $this->markTestSkipped( 'Avro is required to run the AvroValidatorTest' ); diff --git a/www/wiki/tests/phpunit/includes/utils/BatchRowUpdateTest.php b/www/wiki/tests/phpunit/includes/utils/BatchRowUpdateTest.php index 017e97d3..52b14339 100644 --- a/www/wiki/tests/phpunit/includes/utils/BatchRowUpdateTest.php +++ b/www/wiki/tests/phpunit/includes/utils/BatchRowUpdateTest.php @@ -4,11 +4,15 @@ * Tests for BatchRowUpdate and its components * * @group db + * + * @covers BatchRowUpdate + * @covers BatchRowIterator + * @covers BatchRowWriter */ class BatchRowUpdateTest extends MediaWikiTestCase { public function testWriterBasicFunctionality() { - $db = $this->mockDb(); + $db = $this->mockDb( [ 'update' ] ); $writer = new BatchRowWriter( $db, 'echo_event' ); $updates = [ @@ -32,17 +36,13 @@ class BatchRowUpdateTest extends MediaWikiTestCase { } public function testReaderBasicIterate() { - $db = $this->mockDb(); $batchSize = 2; - $reader = new BatchRowIterator( $db, 'some_table', 'id_field', $batchSize ); - $response = $this->genSelectResult( $batchSize, /*numRows*/ 5, function () { static $i = 0; return [ 'id_field' => ++$i ]; } ); - $db->expects( $this->exactly( count( $response ) ) ) - ->method( 'select' ) - ->will( $this->consecutivelyReturnFromSelect( $response ) ); + $db = $this->mockDbConsecutiveSelect( $response ); + $reader = new BatchRowIterator( $db, 'some_table', 'id_field', $batchSize ); $pos = 0; foreach ( $reader as $rows ) { @@ -126,7 +126,7 @@ class BatchRowUpdateTest extends MediaWikiTestCase { public function testReaderSetFetchColumns( $message, array $columns, array $primaryKeys, array $fetchColumns ) { - $db = $this->mockDb(); + $db = $this->mockDb( [ 'select' ] ); $db->expects( $this->once() ) ->method( 'select' ) // only testing second parameter of Database::select @@ -198,7 +198,7 @@ class BatchRowUpdateTest extends MediaWikiTestCase { } protected function mockDbConsecutiveSelect( array $retvals ) { - $db = $this->mockDb(); + $db = $this->mockDb( [ 'select', 'addQuotes' ] ); $db->expects( $this->any() ) ->method( 'select' ) ->will( $this->consecutivelyReturnFromSelect( $retvals ) ); @@ -234,11 +234,12 @@ class BatchRowUpdateTest extends MediaWikiTestCase { return $res; } - protected function mockDb() { + protected function mockDb( $methods = [] ) { // @TODO: mock from Database // FIXME: the constructor normally sets mAtomicLevels and mSrvCache - $databaseMysql = $this->getMockBuilder( 'DatabaseMysqli' ) + $databaseMysql = $this->getMockBuilder( Wikimedia\Rdbms\DatabaseMysqli::class ) ->disableOriginalConstructor() + ->setMethods( array_merge( [ 'isOpen', 'getApproximateLagStatus' ], $methods ) ) ->getMock(); $databaseMysql->expects( $this->any() ) ->method( 'isOpen' ) diff --git a/www/wiki/tests/phpunit/includes/utils/ClassCollectorTest.php b/www/wiki/tests/phpunit/includes/utils/ClassCollectorTest.php index e8a228e3..9e5163f9 100644 --- a/www/wiki/tests/phpunit/includes/utils/ClassCollectorTest.php +++ b/www/wiki/tests/phpunit/includes/utils/ClassCollectorTest.php @@ -3,7 +3,9 @@ /** * @covers ClassCollector */ -class ClassCollectorTest extends PHPUnit_Framework_TestCase { +class ClassCollectorTest extends PHPUnit\Framework\TestCase { + + use MediaWikiCoversValidator; public static function provideCases() { return [ @@ -32,6 +34,12 @@ class ClassCollectorTest extends PHPUnit_Framework_TestCase { [ 'Bar' ], ], [ + // Namespaced class is not currently supported. Must use namespace declaration + // earlier in the file. + "class_alias( Example\Foo::class, 'Bar' );", + [], + ], + [ "namespace Example;\nclass Foo {}\nclass_alias( Foo::class, 'Bar' );", [ 'Example\Foo', 'Bar' ], ], diff --git a/www/wiki/tests/phpunit/includes/utils/FileContentsHasherTest.php b/www/wiki/tests/phpunit/includes/utils/FileContentsHasherTest.php index 0ee4c134..316d9f42 100644 --- a/www/wiki/tests/phpunit/includes/utils/FileContentsHasherTest.php +++ b/www/wiki/tests/phpunit/includes/utils/FileContentsHasherTest.php @@ -3,7 +3,9 @@ /** * @covers FileContentsHasherTest */ -class FileContentsHasherTest extends PHPUnit_Framework_TestCase { +class FileContentsHasherTest extends PHPUnit\Framework\TestCase { + + use MediaWikiCoversValidator; public function provideSingleFile() { return array_map( function ( $file ) { diff --git a/www/wiki/tests/phpunit/includes/utils/IPTest.php b/www/wiki/tests/phpunit/includes/utils/IPTest.php deleted file mode 100644 index 5e0626b6..00000000 --- a/www/wiki/tests/phpunit/includes/utils/IPTest.php +++ /dev/null @@ -1,670 +0,0 @@ -<?php -/** - * Tests for IP validity functions. - * - * Ported from /t/inc/IP.t by avar. - * - * @group IP - * @todo Test methods in this call should be split into a method and a - * dataprovider. - */ - -class IPTest extends PHPUnit_Framework_TestCase { - /** - * @covers IP::isIPAddress - * @dataProvider provideInvalidIPs - */ - public function isNotIPAddress( $val, $desc ) { - $this->assertFalse( IP::isIPAddress( $val ), $desc ); - } - - /** - * Provide a list of things that aren't IP addresses - */ - public function provideInvalidIPs() { - return [ - [ false, 'Boolean false is not an IP' ], - [ true, 'Boolean true is not an IP' ], - [ '', 'Empty string is not an IP' ], - [ 'abc', 'Garbage IP string' ], - [ ':', 'Single ":" is not an IP' ], - [ '2001:0DB8::A:1::1', 'IPv6 with a double :: occurrence' ], - [ '2001:0DB8::A:1::', 'IPv6 with a double :: occurrence, last at end' ], - [ '::2001:0DB8::5:1', 'IPv6 with a double :: occurrence, firt at beginning' ], - [ '124.24.52', 'IPv4 not enough quads' ], - [ '24.324.52.13', 'IPv4 out of range' ], - [ '.24.52.13', 'IPv4 starts with period' ], - [ 'fc:100:300', 'IPv6 with only 3 words' ], - ]; - } - - /** - * @covers IP::isIPAddress - */ - public function testisIPAddress() { - $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 = [ '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 - * @dataProvider provideInvalidIPv4Addresses - */ - public function testisNotIPv4( $bogusIP, $desc ) { - $this->assertFalse( IP::isIPv4( $bogusIP ), $desc ); - } - - public function provideInvalidIPv4Addresses() { - return [ - [ false, 'Boolean false is not an IP' ], - [ true, 'Boolean true is not an IP' ], - [ '', 'Empty string is not an IP' ], - [ 'abc', 'Letters are not an IP' ], - [ ':', 'A colon is not an IP' ], - [ '124.24.52', 'IPv4 not enough quads' ], - [ '24.324.52.13', 'IPv4 out of range' ], - [ '.24.52.13', 'IPv4 starts with period' ], - ]; - } - - /** - * @covers IP::isIPv4 - * @dataProvider provideValidIPv4Address - */ - public function testIsIPv4( $ip, $desc ) { - $this->assertTrue( IP::isIPv4( $ip ), $desc ); - } - - /** - * Provide some IPv4 addresses and ranges - */ - public function provideValidIPv4Address() { - return [ - [ '124.24.52.13', 'Valid IPv4 address' ], - [ '1.24.52.13', 'Another valid IPv4 address' ], - [ '74.24.52.13/20', 'An 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( [ $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( [ $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( [ $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( [ $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 = [ - '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 = [ - '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" ); - } - } - - /** - * Provide some valid IP blocks - */ - public function provideValidBlocks() { - return [ - [ '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' ], - ]; - } - - /** - * @covers IP::isValidBlock - * @dataProvider provideValidBlocks - */ - public function testValidBlocks( $block ) { - $this->assertTrue( IP::isValidBlock( $block ), "$block is a valid IP block" ); - } - - /** - * @covers IP::isValidBlock - * @dataProvider provideInvalidBlocks - */ - public function testInvalidBlocks( $invalid ) { - $this->assertFalse( IP::isValidBlock( $invalid ), "$invalid is not a valid IP block" ); - } - - public function provideInvalidBlocks() { - return [ - [ '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/' ], - ]; - } - - /** - * @covers IP::sanitizeIP - * @dataProvider provideSanitizeIP - */ - public function testSanitizeIP( $expected, $input ) { - $result = IP::sanitizeIP( $input ); - $this->assertEquals( $expected, $result ); - } - - /** - * Provider for IP::testSanitizeIP() - */ - public static function provideSanitizeIP() { - return [ - [ '0.0.0.0', '0.0.0.0' ], - [ '0.0.0.0', '00.00.00.00' ], - [ '0.0.0.0', '000.000.000.000' ], - [ '141.0.11.253', '141.000.011.253' ], - [ '1.2.4.5', '1.2.4.5' ], - [ '1.2.4.5', '01.02.04.05' ], - [ '1.2.4.5', '001.002.004.005' ], - [ '10.0.0.1', '010.0.000.1' ], - [ '80.72.250.4', '080.072.250.04' ], - [ 'Foo.1000.00', 'Foo.1000.00' ], - [ 'Bar.01', 'Bar.01' ], - [ 'Bar.010', 'Bar.010' ], - [ null, '' ], - [ null, ' ' ] - ]; - } - - /** - * @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 [ - [ '00000001', '0.0.0.1' ], - [ '01020304', '1.2.3.4' ], - [ '7F000001', '127.0.0.1' ], - [ '80000000', '128.0.0.0' ], - [ 'DEADCAFE', '222.173.202.254' ], - [ 'FFFFFFFF', '255.255.255.255' ], - [ '8D000BFD', '141.000.11.253' ], - [ false, 'IN.VA.LI.D' ], - [ 'v6-00000000000000000000000000000001', '::1' ], - [ 'v6-20010DB885A3000000008A2E03707334', '2001:0db8:85a3:0000:0000:8a2e:0370:7334' ], - [ 'v6-20010DB885A3000000008A2E03707334', '2001:db8:85a3::8a2e:0370:7334' ], - [ false, 'IN:VA::LI:D' ], - [ false, ':::1' ] - ]; - } - - /** - * @covers IP::isPublic - * @dataProvider provideIsPublic - */ - public function testIsPublic( $expected, $input ) { - $result = IP::isPublic( $input ); - $this->assertEquals( $expected, $result ); - } - - /** - * Provider for IP::testIsPublic() - */ - public static function provideIsPublic() { - return [ - [ false, 'fc00::3' ], # RFC 4193 (local) - [ false, 'fc00::ff' ], # RFC 4193 (local) - [ false, '127.1.2.3' ], # loopback - [ false, '::1' ], # loopback - [ false, 'fe80::1' ], # link-local - [ false, '169.254.1.1' ], # link-local - [ false, '10.0.0.1' ], # RFC 1918 (private) - [ false, '172.16.0.1' ], # RFC 1918 (private) - [ false, '192.168.0.1' ], # RFC 1918 (private) - [ true, '2001:5c0:1000:a::133' ], # public - [ true, 'fc::3' ], # public - [ true, '00FC::' ] # public - ]; - } - - // Private wrapper used to test CIDR Parsing. - private function assertFalseCIDR( $CIDR, $msg = '' ) { - $ff = [ 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 - * @dataProvider provideIPsAndHexes - */ - public function testHexToQuad( $ip, $hex ) { - $this->assertEquals( $ip, IP::hexToQuad( $hex ) ); - } - - /** - * Provide some IP addresses and their equivalent hex representations - */ - public function provideIPsandHexes() { - return [ - [ '0.0.0.1', '00000001' ], - [ '255.0.0.0', 'FF000000' ], - [ '255.255.255.255', 'FFFFFFFF' ], - [ '10.188.222.255', '0ABCDEFF' ], - // hex not left-padded... - [ '0.0.0.0', '0' ], - [ '0.0.0.1', '1' ], - [ '0.0.0.255', 'FF' ], - [ '0.0.255.0', 'FF00' ], - ]; - } - - /** - * @covers IP::hexToOctet - * @dataProvider provideOctetsAndHexes - */ - public function testHexToOctet( $octet, $hex ) { - $this->assertEquals( $octet, IP::hexToOctet( $hex ) ); - } - - /** - * Provide some hex and octet representations of the same IPs - */ - public function provideOctetsAndHexes() { - return [ - [ '0:0:0:0:0:0:0:1', '00000000000000000000000000000001' ], - [ '0:0:0:0:0:0:FF:3', '00000000000000000000000000FF0003' ], - [ '0:0:0:0:0:0:FF00:6', '000000000000000000000000FF000006' ], - [ '0:0:0:0:0:0:FCCF:FAFF', '000000000000000000000000FCCFFAFF' ], - [ 'FFFF:FFFF:FFFF:FFFF:FFFF:FFFF:FFFF:FFFF', 'FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF' ], - // hex not left-padded... - [ '0:0:0:0:0:0:0:0', '0' ], - [ '0:0:0:0:0:0:0:1', '1' ], - [ '0:0:0:0:0:0:0:FF', 'FF' ], - [ '0:0:0:0:0:0:0:FFD0', 'FFD0' ], - [ '0:0:0:0:0:0:FA00:0', 'FA000000' ], - [ '0:0:0:0:0:0:FCCF:FAFF', '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( [ 0, 0 ], IP::parseCIDR( '192.0.0.2/0' ) ); - $this->assertEquals( [ 0, 0 ], IP::parseCIDR( '0.0.0.0/0' ) ); - $this->assertEquals( [ 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 [ - # IPv4 - [ true, '192.0.2.0', '192.0.2.0/24', 'Network address' ], - [ true, '192.0.2.77', '192.0.2.0/24', 'Simple address' ], - [ true, '192.0.2.255', '192.0.2.0/24', 'Broadcast address' ], - - [ false, '0.0.0.0', '192.0.2.0/24' ], - [ false, '255.255.255', '192.0.2.0/24' ], - - # IPv6 - [ false, '::1', '2001:DB8::/32' ], - [ false, '::', '2001:DB8::/32' ], - [ false, 'FE80::1', '2001:DB8::/32' ], - - [ true, '2001:DB8::', '2001:DB8::/32' ], - [ true, '2001:0DB8::', '2001:DB8::/32' ], - [ true, '2001:DB8::1', '2001:DB8::/32' ], - [ true, '2001:0DB8::1', '2001:DB8::/32' ], - [ true, '2001:0DB8:FFFF:FFFF:FFFF:FFFF:FFFF:FFFF', - '2001:DB8::/32' ], - - [ 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 [ - [ false, '[', 'Unclosed square bracket' ], - [ false, '[::', 'Unclosed square bracket 2' ], - [ [ '::', false ], '::', 'Bare IPv6 0' ], - [ [ '::1', false ], '::1', 'Bare IPv6 1' ], - [ [ '::', false ], '[::]', 'Bracketed IPv6 0' ], - [ [ '::1', false ], '[::1]', 'Bracketed IPv6 1' ], - [ [ '::1', 80 ], '[::1]:80', 'Bracketed IPv6 with port' ], - [ false, '::x', 'Double colon but no IPv6' ], - [ [ 'x', 80 ], 'x:80', 'Hostname and port' ], - [ false, 'x:x', 'Hostname and invalid port' ], - [ [ '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 [ - [ '[::1]', [ '::1', 2, 2 ], 'IPv6 default port' ], - [ '[::1]:2', [ '::1', 2, 3 ], 'IPv6 non-default port' ], - [ 'x', [ 'x', 2, 2 ], 'Normal default port' ], - [ 'x:2', [ '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 [ - [ '35.56.31.252/16', '35.56.0.0/16', 'IPv4 range' ], - [ '135.16.21.252/24', '135.16.21.0/24', 'IPv4 range' ], - [ '5.36.71.252/32', '5.36.71.252/32', 'IPv4 silly range' ], - [ '5.36.71.252', '5.36.71.252', 'IPv4 non-range' ], - [ '0:1:2:3:4:c5:f6:7/96', '0:1:2:3:4:C5:0:0/96', 'IPv6 range' ], - [ '0:1:2:3:4:5:6:7/120', '0:1:2:3:4:5:6:0/120', 'IPv6 range' ], - [ '0:e1:2:3:4:5:e6:7/128', '0:E1:2:3:4:5:E6:7/128', 'IPv6 silly range' ], - [ '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 [ - [ '0:0:0:0:0:0:0:0', '::' ], - [ '0:0:0::0:0:0', '::' ], - [ '0:0:0:1:0:0:0:0', '0:0:0:1::' ], - [ '0:0::f', '::f' ], - [ '0::0:0:0:33:fef:b', '::33:fef:b' ], - [ '3f:535:0:0:0:0:e:fbb', '3f:535::e:fbb' ], - [ '0:0:fef:0:0:0:e:fbb', '0:0:fef::e:fbb' ], - [ 'abbc:2004::0:0:0:0', 'abbc:2004::' ], - [ 'cebc:2004:f:0:0:0:0:0', 'cebc:2004:f::' ], - [ '0:0:0:0:0:0:0:0/16', '::/16' ], - [ '0:0:0::0:0:0/64', '::/64' ], - [ '0:0::f/52', '::f/52' ], - [ '::0:0:33:fef:b/52', '::33:fef:b/52' ], - [ '3f:535:0:0:0:0:e:fbb/48', '3f:535::e:fbb/48' ], - [ '0:0:fef:0:0:0:e:fbb/96', '0:0:fef::e:fbb/96' ], - [ 'abbc:2004:0:0::0:0/40', 'abbc:2004::/40' ], - [ 'aebc:2004:f:0:0:0:0:0/80', 'aebc:2004:f::/80' ], - ]; - } -} diff --git a/www/wiki/tests/phpunit/includes/utils/MWCryptHKDFTest.php b/www/wiki/tests/phpunit/includes/utils/MWCryptHKDFTest.php index 86c19ae4..05a33c5a 100644 --- a/www/wiki/tests/phpunit/includes/utils/MWCryptHKDFTest.php +++ b/www/wiki/tests/phpunit/includes/utils/MWCryptHKDFTest.php @@ -1,9 +1,9 @@ <?php /** - * * @group HKDF + * @covers CryptHKDF + * @covers MWCryptHKDF */ - class MWCryptHKDFTest extends MediaWikiTestCase { protected function setUp() { @@ -40,7 +40,7 @@ class MWCryptHKDFTest extends MediaWikiTestCase { * Test vectors from Appendix A on https://tools.ietf.org/html/rfc5869 */ public static function providerRfc5869() { - // @codingStandardsIgnoreStart Generic.Files.LineLength + // phpcs:disable Generic.Files.LineLength return [ // A.1 [ @@ -93,6 +93,6 @@ class MWCryptHKDFTest extends MediaWikiTestCase { '0bd770a74d1160f7c9f12cd5912a06ebff6adcae899d92191fe4305673ba2ffe8fa3f1a4e5ad79f3f334b3b202b2173c486ea37ce3d397ed034c7f9dfeb15c5e927336d0441f4c4300e2cff0d0900b52d3b4' // okm ], ]; - // @codingStandardsIgnoreEnd + // phpcs:enable } } diff --git a/www/wiki/tests/phpunit/includes/utils/MWCryptHashTest.php b/www/wiki/tests/phpunit/includes/utils/MWCryptHashTest.php index 905d14ca..94705bff 100644 --- a/www/wiki/tests/phpunit/includes/utils/MWCryptHashTest.php +++ b/www/wiki/tests/phpunit/includes/utils/MWCryptHashTest.php @@ -1,10 +1,13 @@ <?php + /** - * * @group Hash + * + * @covers MWCryptHash */ +class MWCryptHashTest extends PHPUnit\Framework\TestCase { -class MWCryptHashTest extends PHPUnit_Framework_TestCase { + use MediaWikiCoversValidator; public function testHashLength() { if ( MWCryptHash::hashAlgo() !== 'whirlpool' ) { @@ -21,9 +24,8 @@ class MWCryptHashTest extends PHPUnit_Framework_TestCase { } $data = 'foobar'; - // @codingStandardsIgnoreStart Generic.Files.LineLength + // phpcs:ignore Generic.Files.LineLength $hash = '9923afaec3a86f865bb231a588f453f84e8151a2deb4109aebc6de4284be5bebcff4fab82a7e51d920237340a043736e9d13bab196006dcca0fe65314d68eab9'; - // @codingStandardsIgnoreEnd $this->assertEquals( hex2bin( $hash ), @@ -44,9 +46,8 @@ class MWCryptHashTest extends PHPUnit_Framework_TestCase { $data = 'foobar'; $key = 'secret'; - // @codingStandardsIgnoreStart Generic.Files.LineLength + // phpcs:ignore Generic.Files.LineLength $hash = 'ddc94177b2020e55ce2049199fd9cc6327f416ff6dc621cc34cb43d9bec61d73372b4790c0e24957f565ecaf2d42821e6303619093e99cbe14a3b9250bda5f81'; - // @codingStandardsIgnoreEnd $this->assertEquals( hex2bin( $hash ), diff --git a/www/wiki/tests/phpunit/includes/utils/MWRestrictionsTest.php b/www/wiki/tests/phpunit/includes/utils/MWRestrictionsTest.php index f2ea489b..abdfbb14 100644 --- a/www/wiki/tests/phpunit/includes/utils/MWRestrictionsTest.php +++ b/www/wiki/tests/phpunit/includes/utils/MWRestrictionsTest.php @@ -1,5 +1,7 @@ <?php -class MWRestrictionsTest extends PHPUnit_Framework_TestCase { +class MWRestrictionsTest extends PHPUnit\Framework\TestCase { + + use MediaWikiCoversValidator; protected static $restrictionsForChecks; @@ -19,7 +21,7 @@ class MWRestrictionsTest extends PHPUnit_Framework_TestCase { */ public function testNewDefault() { $ret = MWRestrictions::newDefault(); - $this->assertInstanceOf( 'MWRestrictions', $ret ); + $this->assertInstanceOf( MWRestrictions::class, $ret ); $this->assertSame( '{"IPAddresses":["0.0.0.0/0","::/0"]}', $ret->toJson() @@ -39,7 +41,7 @@ class MWRestrictionsTest extends PHPUnit_Framework_TestCase { public function testArray( $data, $expect ) { if ( $expect === true ) { $ret = MWRestrictions::newFromArray( $data ); - $this->assertInstanceOf( 'MWRestrictions', $ret ); + $this->assertInstanceOf( MWRestrictions::class, $ret ); $this->assertSame( $data, $ret->toArray() ); } else { try { @@ -87,7 +89,7 @@ class MWRestrictionsTest extends PHPUnit_Framework_TestCase { public function testJson( $json, $expect ) { if ( is_array( $expect ) ) { $ret = MWRestrictions::newFromJson( $json ); - $this->assertInstanceOf( 'MWRestrictions', $ret ); + $this->assertInstanceOf( MWRestrictions::class, $ret ); $this->assertSame( $expect, $ret->toArray() ); $this->assertSame( $json, $ret->toJson( false ) ); @@ -178,7 +180,7 @@ class MWRestrictionsTest extends PHPUnit_Framework_TestCase { public function provideCheck() { $ret = []; - $mockBuilder = $this->getMockBuilder( 'FauxRequest' ) + $mockBuilder = $this->getMockBuilder( FauxRequest::class ) ->setMethods( [ 'getIP' ] ); foreach ( self::provideCheckIP() as $checkIP ) { diff --git a/www/wiki/tests/phpunit/includes/utils/UIDGeneratorTest.php b/www/wiki/tests/phpunit/includes/utils/UIDGeneratorTest.php index 8b54b722..d335a93a 100644 --- a/www/wiki/tests/phpunit/includes/utils/UIDGeneratorTest.php +++ b/www/wiki/tests/phpunit/includes/utils/UIDGeneratorTest.php @@ -1,6 +1,8 @@ <?php -class UIDGeneratorTest extends PHPUnit_Framework_TestCase { +class UIDGeneratorTest extends PHPUnit\Framework\TestCase { + + use MediaWikiCoversValidator; protected function tearDown() { // Bug: 44850 @@ -16,7 +18,7 @@ class UIDGeneratorTest extends PHPUnit_Framework_TestCase { * @covers UIDGenerator::newTimestampedUID88 */ public function testTimestampedUID( $method, $digitlen, $bits, $tbits, $hostbits ) { - $id = call_user_func( [ 'UIDGenerator', $method ] ); + $id = call_user_func( [ UIDGenerator::class, $method ] ); $this->assertEquals( true, ctype_digit( $id ), "UID made of digit characters" ); $this->assertLessThanOrEqual( $digitlen, strlen( $id ), "UID has the right number of digits" ); @@ -25,7 +27,7 @@ class UIDGeneratorTest extends PHPUnit_Framework_TestCase { $ids = []; for ( $i = 0; $i < 300; $i++ ) { - $ids[] = call_user_func( [ 'UIDGenerator', $method ] ); + $ids[] = call_user_func( [ UIDGenerator::class, $method ] ); } $lastId = array_shift( $ids ); diff --git a/www/wiki/tests/phpunit/includes/utils/ZipDirectoryReaderTest.php b/www/wiki/tests/phpunit/includes/utils/ZipDirectoryReaderTest.php index 7e74d960..9f18e5af 100644 --- a/www/wiki/tests/phpunit/includes/utils/ZipDirectoryReaderTest.php +++ b/www/wiki/tests/phpunit/includes/utils/ZipDirectoryReaderTest.php @@ -4,7 +4,10 @@ * @covers ZipDirectoryReader * NOTE: this test is more like an integration test than a unit test */ -class ZipDirectoryReaderTest extends PHPUnit_Framework_TestCase { +class ZipDirectoryReaderTest extends PHPUnit\Framework\TestCase { + + use MediaWikiCoversValidator; + protected $zipDir; protected $entries; diff --git a/www/wiki/tests/phpunit/includes/watcheditem/NoWriteWatchedItemStoreUnitTest.php b/www/wiki/tests/phpunit/includes/watcheditem/NoWriteWatchedItemStoreUnitTest.php new file mode 100644 index 00000000..a8761e39 --- /dev/null +++ b/www/wiki/tests/phpunit/includes/watcheditem/NoWriteWatchedItemStoreUnitTest.php @@ -0,0 +1,246 @@ +<?php + +/** + * @author Addshore + * + * @covers NoWriteWatchedItemStore + */ +class NoWriteWatchedItemStoreUnitTest extends MediaWikiTestCase { + + public function testAddWatch() { + /** @var WatchedItemStoreInterface|PHPUnit_Framework_MockObject_MockObject $innerService */ + $innerService = $this->getMockForAbstractClass( WatchedItemStoreInterface::class ); + $innerService->expects( $this->never() )->method( 'addWatch' ); + $noWriteService = new NoWriteWatchedItemStore( $innerService ); + + $this->setExpectedException( DBReadOnlyError::class ); + $noWriteService->addWatch( $this->getTestSysop()->getUser(), new TitleValue( 0, 'Foo' ) ); + } + + public function testAddWatchBatchForUser() { + /** @var WatchedItemStoreInterface|PHPUnit_Framework_MockObject_MockObject $innerService */ + $innerService = $this->getMockForAbstractClass( WatchedItemStoreInterface::class ); + $innerService->expects( $this->never() )->method( 'addWatchBatchForUser' ); + $noWriteService = new NoWriteWatchedItemStore( $innerService ); + + $this->setExpectedException( DBReadOnlyError::class ); + $noWriteService->addWatchBatchForUser( $this->getTestSysop()->getUser(), [] ); + } + + public function testRemoveWatch() { + /** @var WatchedItemStoreInterface|PHPUnit_Framework_MockObject_MockObject $innerService */ + $innerService = $this->getMockForAbstractClass( WatchedItemStoreInterface::class ); + $innerService->expects( $this->never() )->method( 'removeWatch' ); + $noWriteService = new NoWriteWatchedItemStore( $innerService ); + + $this->setExpectedException( DBReadOnlyError::class ); + $noWriteService->removeWatch( $this->getTestSysop()->getUser(), new TitleValue( 0, 'Foo' ) ); + } + + public function testSetNotificationTimestampsForUser() { + /** @var WatchedItemStoreInterface|PHPUnit_Framework_MockObject_MockObject $innerService */ + $innerService = $this->getMockForAbstractClass( WatchedItemStoreInterface::class ); + $innerService->expects( $this->never() )->method( 'setNotificationTimestampsForUser' ); + $noWriteService = new NoWriteWatchedItemStore( $innerService ); + + $this->setExpectedException( DBReadOnlyError::class ); + $noWriteService->setNotificationTimestampsForUser( + $this->getTestSysop()->getUser(), + 'timestamp', + [] + ); + } + + public function testUpdateNotificationTimestamp() { + /** @var WatchedItemStoreInterface|PHPUnit_Framework_MockObject_MockObject $innerService */ + $innerService = $this->getMockForAbstractClass( WatchedItemStoreInterface::class ); + $innerService->expects( $this->never() )->method( 'updateNotificationTimestamp' ); + $noWriteService = new NoWriteWatchedItemStore( $innerService ); + + $this->setExpectedException( DBReadOnlyError::class ); + $noWriteService->updateNotificationTimestamp( + $this->getTestSysop()->getUser(), + new TitleValue( 0, 'Foo' ), + 'timestamp' + ); + } + + public function testResetNotificationTimestamp() { + /** @var WatchedItemStoreInterface|PHPUnit_Framework_MockObject_MockObject $innerService */ + $innerService = $this->getMockForAbstractClass( WatchedItemStoreInterface::class ); + $innerService->expects( $this->never() )->method( 'resetNotificationTimestamp' ); + $noWriteService = new NoWriteWatchedItemStore( $innerService ); + + $this->setExpectedException( DBReadOnlyError::class ); + $noWriteService->resetNotificationTimestamp( + $this->getTestSysop()->getUser(), + Title::newFromText( 'Foo' ) + ); + } + + public function testCountWatchedItems() { + /** @var WatchedItemStoreInterface|PHPUnit_Framework_MockObject_MockObject $innerService */ + $innerService = $this->getMockForAbstractClass( WatchedItemStoreInterface::class ); + $innerService->expects( $this->once() )->method( 'countWatchedItems' )->willReturn( __METHOD__ ); + $noWriteService = new NoWriteWatchedItemStore( $innerService ); + + $return = $noWriteService->countWatchedItems( + $this->getTestSysop()->getUser() + ); + $this->assertEquals( __METHOD__, $return ); + } + + public function testCountWatchers() { + /** @var WatchedItemStoreInterface|PHPUnit_Framework_MockObject_MockObject $innerService */ + $innerService = $this->getMockForAbstractClass( WatchedItemStoreInterface::class ); + $innerService->expects( $this->once() )->method( 'countWatchers' )->willReturn( __METHOD__ ); + $noWriteService = new NoWriteWatchedItemStore( $innerService ); + + $return = $noWriteService->countWatchers( + new TitleValue( 0, 'Foo' ) + ); + $this->assertEquals( __METHOD__, $return ); + } + + public function testCountVisitingWatchers() { + /** @var WatchedItemStoreInterface|PHPUnit_Framework_MockObject_MockObject $innerService */ + $innerService = $this->getMockForAbstractClass( WatchedItemStoreInterface::class ); + $innerService->expects( $this->once() ) + ->method( 'countVisitingWatchers' ) + ->willReturn( __METHOD__ ); + $noWriteService = new NoWriteWatchedItemStore( $innerService ); + + $return = $noWriteService->countVisitingWatchers( + new TitleValue( 0, 'Foo' ), + 9 + ); + $this->assertEquals( __METHOD__, $return ); + } + + public function testCountWatchersMultiple() { + /** @var WatchedItemStoreInterface|PHPUnit_Framework_MockObject_MockObject $innerService */ + $innerService = $this->getMockForAbstractClass( WatchedItemStoreInterface::class ); + $innerService->expects( $this->once() ) + ->method( 'countVisitingWatchersMultiple' ) + ->willReturn( __METHOD__ ); + $noWriteService = new NoWriteWatchedItemStore( $innerService ); + + $return = $noWriteService->countWatchersMultiple( + [ new TitleValue( 0, 'Foo' ) ], + [] + ); + $this->assertEquals( __METHOD__, $return ); + } + + public function testCountVisitingWatchersMultiple() { + /** @var WatchedItemStoreInterface|PHPUnit_Framework_MockObject_MockObject $innerService */ + $innerService = $this->getMockForAbstractClass( WatchedItemStoreInterface::class ); + $innerService->expects( $this->once() ) + ->method( 'countVisitingWatchersMultiple' ) + ->willReturn( __METHOD__ ); + $noWriteService = new NoWriteWatchedItemStore( $innerService ); + + $return = $noWriteService->countVisitingWatchersMultiple( + [ [ new TitleValue( 0, 'Foo' ), 99 ] ], + 11 + ); + $this->assertEquals( __METHOD__, $return ); + } + + public function testGetWatchedItem() { + /** @var WatchedItemStoreInterface|PHPUnit_Framework_MockObject_MockObject $innerService */ + $innerService = $this->getMockForAbstractClass( WatchedItemStoreInterface::class ); + $innerService->expects( $this->once() )->method( 'getWatchedItem' )->willReturn( __METHOD__ ); + $noWriteService = new NoWriteWatchedItemStore( $innerService ); + + $return = $noWriteService->getWatchedItem( + $this->getTestSysop()->getUser(), + new TitleValue( 0, 'Foo' ) + ); + $this->assertEquals( __METHOD__, $return ); + } + + public function testLoadWatchedItem() { + /** @var WatchedItemStoreInterface|PHPUnit_Framework_MockObject_MockObject $innerService */ + $innerService = $this->getMockForAbstractClass( WatchedItemStoreInterface::class ); + $innerService->expects( $this->once() )->method( 'loadWatchedItem' )->willReturn( __METHOD__ ); + $noWriteService = new NoWriteWatchedItemStore( $innerService ); + + $return = $noWriteService->loadWatchedItem( + $this->getTestSysop()->getUser(), + new TitleValue( 0, 'Foo' ) + ); + $this->assertEquals( __METHOD__, $return ); + } + + public function testGetWatchedItemsForUser() { + /** @var WatchedItemStoreInterface|PHPUnit_Framework_MockObject_MockObject $innerService */ + $innerService = $this->getMockForAbstractClass( WatchedItemStoreInterface::class ); + $innerService->expects( $this->once() ) + ->method( 'getWatchedItemsForUser' ) + ->willReturn( __METHOD__ ); + $noWriteService = new NoWriteWatchedItemStore( $innerService ); + + $return = $noWriteService->getWatchedItemsForUser( + $this->getTestSysop()->getUser(), + [] + ); + $this->assertEquals( __METHOD__, $return ); + } + + public function testIsWatched() { + /** @var WatchedItemStoreInterface|PHPUnit_Framework_MockObject_MockObject $innerService */ + $innerService = $this->getMockForAbstractClass( WatchedItemStoreInterface::class ); + $innerService->expects( $this->once() )->method( 'isWatched' )->willReturn( __METHOD__ ); + $noWriteService = new NoWriteWatchedItemStore( $innerService ); + + $return = $noWriteService->isWatched( + $this->getTestSysop()->getUser(), + new TitleValue( 0, 'Foo' ) + ); + $this->assertEquals( __METHOD__, $return ); + } + + public function testGetNotificationTimestampsBatch() { + /** @var WatchedItemStoreInterface|PHPUnit_Framework_MockObject_MockObject $innerService */ + $innerService = $this->getMockForAbstractClass( WatchedItemStoreInterface::class ); + $innerService->expects( $this->once() ) + ->method( 'getNotificationTimestampsBatch' ) + ->willReturn( __METHOD__ ); + $noWriteService = new NoWriteWatchedItemStore( $innerService ); + + $return = $noWriteService->getNotificationTimestampsBatch( + $this->getTestSysop()->getUser(), + [ new TitleValue( 0, 'Foo' ) ] + ); + $this->assertEquals( __METHOD__, $return ); + } + + public function testCountUnreadNotifications() { + /** @var WatchedItemStoreInterface|PHPUnit_Framework_MockObject_MockObject $innerService */ + $innerService = $this->getMockForAbstractClass( WatchedItemStoreInterface::class ); + $innerService->expects( $this->once() ) + ->method( 'countUnreadNotifications' ) + ->willReturn( __METHOD__ ); + $noWriteService = new NoWriteWatchedItemStore( $innerService ); + + $return = $noWriteService->countUnreadNotifications( + $this->getTestSysop()->getUser(), + 88 + ); + $this->assertEquals( __METHOD__, $return ); + } + + public function testDuplicateAllAssociatedEntries() { + /** @var WatchedItemStoreInterface|PHPUnit_Framework_MockObject_MockObject $innerService */ + $innerService = $this->getMockForAbstractClass( WatchedItemStoreInterface::class ); + $noWriteService = new NoWriteWatchedItemStore( $innerService ); + + $this->setExpectedException( DBReadOnlyError::class ); + $noWriteService->duplicateAllAssociatedEntries( + new TitleValue( 0, 'Foo' ), + new TitleValue( 0, 'Bar' ) + ); + } + +} diff --git a/www/wiki/tests/phpunit/includes/WatchedItemQueryServiceUnitTest.php b/www/wiki/tests/phpunit/includes/watcheditem/WatchedItemQueryServiceUnitTest.php index 62ba5f68..50e6c202 100644 --- a/www/wiki/tests/phpunit/includes/WatchedItemQueryServiceUnitTest.php +++ b/www/wiki/tests/phpunit/includes/watcheditem/WatchedItemQueryServiceUnitTest.php @@ -1,12 +1,80 @@ <?php -use Wikimedia\ScopedCallback; +use Wikimedia\Rdbms\LoadBalancer; use Wikimedia\TestingAccessWrapper; /** * @covers WatchedItemQueryService */ -class WatchedItemQueryServiceUnitTest extends PHPUnit_Framework_TestCase { +class WatchedItemQueryServiceUnitTest extends MediaWikiTestCase { + + use MediaWikiCoversValidator; + + /** + * @return PHPUnit_Framework_MockObject_MockObject|CommentStore + */ + private function getMockCommentStore() { + $mockStore = $this->getMockBuilder( CommentStore::class ) + ->disableOriginalConstructor() + ->getMock(); + $mockStore->expects( $this->any() ) + ->method( 'getFields' ) + ->willReturn( [ 'commentstore' => 'fields' ] ); + $mockStore->expects( $this->any() ) + ->method( 'getJoin' ) + ->willReturn( [ + 'tables' => [ 'commentstore' => 'table' ], + 'fields' => [ 'commentstore' => 'field' ], + 'joins' => [ 'commentstore' => 'join' ], + ] ); + return $mockStore; + } + + /** + * @return PHPUnit_Framework_MockObject_MockObject|ActorMigration + */ + private function getMockActorMigration() { + $mockStore = $this->getMockBuilder( ActorMigration::class ) + ->disableOriginalConstructor() + ->getMock(); + $mockStore->expects( $this->any() ) + ->method( 'getJoin' ) + ->willReturn( [ + 'tables' => [ 'actormigration' => 'table' ], + 'fields' => [ + 'rc_user' => 'actormigration_user', + 'rc_user_text' => 'actormigration_user_text', + 'rc_actor' => 'actormigration_actor', + ], + 'joins' => [ 'actormigration' => 'join' ], + ] ); + $mockStore->expects( $this->any() ) + ->method( 'getWhere' ) + ->willReturn( [ + 'tables' => [ 'actormigration' => 'table' ], + 'conds' => 'actormigration_conds', + 'joins' => [ 'actormigration' => 'join' ], + ] ); + $mockStore->expects( $this->any() ) + ->method( 'isAnon' ) + ->willReturn( 'actormigration is anon' ); + $mockStore->expects( $this->any() ) + ->method( 'isNotAnon' ) + ->willReturn( 'actormigration is not anon' ); + return $mockStore; + } + + /** + * @param PHPUnit_Framework_MockObject_MockObject|Database $mockDb + * @return WatchedItemQueryService + */ + private function newService( $mockDb ) { + return new WatchedItemQueryService( + $this->getMockLoadBalancer( $mockDb ), + $this->getMockCommentStore(), + $this->getMockActorMigration() + ); + } /** * @return PHPUnit_Framework_MockObject_MockObject|Database @@ -24,10 +92,17 @@ class WatchedItemQueryServiceUnitTest extends PHPUnit_Framework_TestCase { ) ->will( $this->returnCallback( function ( $a, $conj ) { $sqlConj = $conj === LIST_AND ? ' AND ' : ' OR '; - return join( $sqlConj, array_map( function ( $s ) { - return '(' . $s . ')'; - }, $a - ) ); + $conds = []; + foreach ( $a as $k => $v ) { + if ( is_int( $k ) ) { + $conds[] = "($v)"; + } elseif ( is_array( $v ) ) { + $conds[] = "($k IN ('" . implode( "','", $v ) . "'))"; + } else { + $conds[] = "($k = '$v')"; + } + } + return implode( $sqlConj, $conds ); } ) ); $mock->expects( $this->any() ) @@ -230,7 +305,7 @@ class WatchedItemQueryServiceUnitTest extends PHPUnit_Framework_TestCase { ] ), ] ) ); - $queryService = new WatchedItemQueryService( $this->getMockLoadBalancer( $mockDb ) ); + $queryService = $this->newService( $mockDb ); $user = $this->getMockUnrestrictedNonAnonUserWithId( 1 ); $startFrom = null; @@ -390,7 +465,7 @@ class WatchedItemQueryServiceUnitTest extends PHPUnit_Framework_TestCase { $startFrom = [ '20160203123456', 42 ]; } ) ); - $queryService = new WatchedItemQueryService( $this->getMockLoadBalancer( $mockDb ) ); + $queryService = $this->newService( $mockDb ); TestingAccessWrapper::newFromObject( $queryService )->extensions = [ $mockExtension ]; $startFrom = null; @@ -457,76 +532,29 @@ class WatchedItemQueryServiceUnitTest extends PHPUnit_Framework_TestCase { [ [ 'includeFields' => [ WatchedItemQueryService::INCLUDE_USER ] ], null, - [], - [ 'rc_user_text' ], - [], + [ 'actormigration' => 'table' ], + [ 'rc_user_text' => 'actormigration_user_text' ], [], [], + [ 'actormigration' => 'join' ], ], [ [ 'includeFields' => [ WatchedItemQueryService::INCLUDE_USER_ID ] ], null, - [], - [ 'rc_user' ], - [], - [], - [], - ], - [ - [ 'includeFields' => [ WatchedItemQueryService::INCLUDE_COMMENT ] ], - null, - [], - [ - 'rc_comment_text' => 'rc_comment', - 'rc_comment_data' => 'NULL', - 'rc_comment_cid' => 'NULL', - ], - [], - [], - [], - [ 'wgCommentTableSchemaMigrationStage' => MIGRATION_OLD ], - ], - [ - [ 'includeFields' => [ WatchedItemQueryService::INCLUDE_COMMENT ] ], - null, - [ 'comment_rc_comment' => 'comment' ], - [ - 'rc_comment_text' => 'COALESCE( comment_rc_comment.comment_text, rc_comment )', - 'rc_comment_data' => 'comment_rc_comment.comment_data', - 'rc_comment_cid' => 'comment_rc_comment.comment_id', - ], - [], - [], - [ 'comment_rc_comment' => [ 'LEFT JOIN', 'comment_rc_comment.comment_id = rc_comment_id' ] ], - [ 'wgCommentTableSchemaMigrationStage' => MIGRATION_WRITE_BOTH ], - ], - [ - [ 'includeFields' => [ WatchedItemQueryService::INCLUDE_COMMENT ] ], - null, - [ 'comment_rc_comment' => 'comment' ], - [ - 'rc_comment_text' => 'COALESCE( comment_rc_comment.comment_text, rc_comment )', - 'rc_comment_data' => 'comment_rc_comment.comment_data', - 'rc_comment_cid' => 'comment_rc_comment.comment_id', - ], + [ 'actormigration' => 'table' ], + [ 'rc_user' => 'actormigration_user' ], [], [], - [ 'comment_rc_comment' => [ 'LEFT JOIN', 'comment_rc_comment.comment_id = rc_comment_id' ] ], - [ 'wgCommentTableSchemaMigrationStage' => MIGRATION_WRITE_NEW ], + [ 'actormigration' => 'join' ], ], [ [ 'includeFields' => [ WatchedItemQueryService::INCLUDE_COMMENT ] ], null, - [ 'comment_rc_comment' => 'comment' ], - [ - 'rc_comment_text' => 'comment_rc_comment.comment_text', - 'rc_comment_data' => 'comment_rc_comment.comment_data', - 'rc_comment_cid' => 'comment_rc_comment.comment_id', - ], + [ 'commentstore' => 'table' ], + [ 'commentstore' => 'field' ], [], [], - [ 'comment_rc_comment' => [ 'JOIN', 'comment_rc_comment.comment_id = rc_comment_id' ] ], - [ 'wgCommentTableSchemaMigrationStage' => MIGRATION_NEW ], + [ 'commentstore' => 'join' ], ], [ [ 'includeFields' => [ WatchedItemQueryService::INCLUDE_PATROL_INFO ] ], @@ -719,20 +747,20 @@ class WatchedItemQueryServiceUnitTest extends PHPUnit_Framework_TestCase { [ [ 'filters' => [ WatchedItemQueryService::FILTER_ANON ] ], null, + [ 'actormigration' => 'table' ], [], + [ 'actormigration is anon' ], [], - [ 'rc_user = 0' ], - [], - [], + [ 'actormigration' => 'join' ], ], [ [ 'filters' => [ WatchedItemQueryService::FILTER_NOT_ANON ] ], null, + [ 'actormigration' => 'table' ], [], + [ 'actormigration is not anon' ], [], - [ 'rc_user != 0' ], - [], - [], + [ 'actormigration' => 'join' ], ], [ [ 'filters' => [ WatchedItemQueryService::FILTER_PATROLLED ] ], @@ -748,7 +776,7 @@ class WatchedItemQueryServiceUnitTest extends PHPUnit_Framework_TestCase { null, [], [], - [ 'rc_patrolled = 0' ], + [ 'rc_patrolled' => 0 ], [], [], ], @@ -773,20 +801,20 @@ class WatchedItemQueryServiceUnitTest extends PHPUnit_Framework_TestCase { [ [ 'onlyByUser' => 'SomeOtherUser' ], null, + [ 'actormigration' => 'table' ], [], + [ 'actormigration_conds' ], [], - [ 'rc_user_text' => 'SomeOtherUser' ], - [], - [], + [ 'actormigration' => 'join' ], ], [ [ 'notByUser' => 'SomeOtherUser' ], null, + [ 'actormigration' => 'table' ], [], + [ 'NOT(actormigration_conds)' ], [], - [ "rc_user_text != 'SomeOtherUser'" ], - [], - [], + [ 'actormigration' => 'join' ], ], [ [ 'dir' => WatchedItemQueryService::DIR_OLDER ], @@ -834,23 +862,8 @@ class WatchedItemQueryServiceUnitTest extends PHPUnit_Framework_TestCase { array $expectedExtraFields, array $expectedExtraConds, array $expectedDbOptions, - array $expectedExtraJoinConds, - array $globals = [] + array $expectedExtraJoinConds ) { - // Sigh. This test class doesn't extend MediaWikiTestCase, so we have to reinvent setMwGlobals(). - if ( $globals ) { - $resetGlobals = []; - foreach ( $globals as $k => $v ) { - $resetGlobals[$k] = $GLOBALS[$k]; - $GLOBALS[$k] = $v; - } - $reset = new ScopedCallback( function () use ( $resetGlobals ) { - foreach ( $resetGlobals as $k => $v ) { - $GLOBALS[$k] = $v; - } - } ); - } - $expectedTables = array_merge( [ 'recentchanges', 'watchlist', 'page' ], $expectedExtraTables ); $expectedFields = array_merge( [ @@ -902,7 +915,7 @@ class WatchedItemQueryServiceUnitTest extends PHPUnit_Framework_TestCase { ) ->will( $this->returnValue( [] ) ); - $queryService = new WatchedItemQueryService( $this->getMockLoadBalancer( $mockDb ) ); + $queryService = $this->newService( $mockDb ); $user = $this->getMockUnrestrictedNonAnonUserWithId( 1 ); $items = $queryService->getWatchedItemsWithRecentChangeInfo( $user, $options, $startFrom ); @@ -939,7 +952,7 @@ class WatchedItemQueryServiceUnitTest extends PHPUnit_Framework_TestCase { $user = $this->getMockNonAnonUserWithIdAndNoPatrolRights( 1 ); - $queryService = new WatchedItemQueryService( $this->getMockLoadBalancer( $mockDb ) ); + $queryService = $this->newService( $mockDb ); $items = $queryService->getWatchedItemsWithRecentChangeInfo( $user, [ 'filters' => [ $filtersOption ] ] @@ -1000,7 +1013,7 @@ class WatchedItemQueryServiceUnitTest extends PHPUnit_Framework_TestCase { ->method( 'getType' ) ->will( $this->returnValue( $dbType ) ); - $queryService = new WatchedItemQueryService( $this->getMockLoadBalancer( $mockDb ) ); + $queryService = $this->newService( $mockDb ); $user = $this->getMockUnrestrictedNonAnonUserWithId( 1 ); $items = $queryService->getWatchedItemsWithRecentChangeInfo( $user, $options ); @@ -1013,62 +1026,74 @@ class WatchedItemQueryServiceUnitTest extends PHPUnit_Framework_TestCase { [ [], 'deletedhistory', + [], [ '(rc_type != ' . RC_LOG . ') OR ((rc_deleted & ' . LogPage::DELETED_ACTION . ') != ' . LogPage::DELETED_ACTION . ')' ], + [], ], [ [], 'suppressrevision', + [], [ '(rc_type != ' . RC_LOG . ') OR (' . '(rc_deleted & ' . ( LogPage::DELETED_ACTION | LogPage::DELETED_RESTRICTED ) . ') != ' . ( LogPage::DELETED_ACTION | LogPage::DELETED_RESTRICTED ) . ')' ], + [], ], [ [], 'viewsuppressed', + [], [ '(rc_type != ' . RC_LOG . ') OR (' . '(rc_deleted & ' . ( LogPage::DELETED_ACTION | LogPage::DELETED_RESTRICTED ) . ') != ' . ( LogPage::DELETED_ACTION | LogPage::DELETED_RESTRICTED ) . ')' ], + [], ], [ [ 'onlyByUser' => 'SomeOtherUser' ], 'deletedhistory', + [ 'actormigration' => 'table' ], [ - 'rc_user_text' => 'SomeOtherUser', + 'actormigration_conds', '(rc_deleted & ' . Revision::DELETED_USER . ') != ' . Revision::DELETED_USER, '(rc_type != ' . RC_LOG . ') OR ((rc_deleted & ' . LogPage::DELETED_ACTION . ') != ' . LogPage::DELETED_ACTION . ')' ], + [ 'actormigration' => 'join' ], ], [ [ 'onlyByUser' => 'SomeOtherUser' ], 'suppressrevision', + [ 'actormigration' => 'table' ], [ - 'rc_user_text' => 'SomeOtherUser', + 'actormigration_conds', '(rc_deleted & ' . ( Revision::DELETED_USER | Revision::DELETED_RESTRICTED ) . ') != ' . ( Revision::DELETED_USER | Revision::DELETED_RESTRICTED ), '(rc_type != ' . RC_LOG . ') OR (' . '(rc_deleted & ' . ( LogPage::DELETED_ACTION | LogPage::DELETED_RESTRICTED ) . ') != ' . ( LogPage::DELETED_ACTION | LogPage::DELETED_RESTRICTED ) . ')' ], + [ 'actormigration' => 'join' ], ], [ [ 'onlyByUser' => 'SomeOtherUser' ], 'viewsuppressed', + [ 'actormigration' => 'table' ], [ - 'rc_user_text' => 'SomeOtherUser', + 'actormigration_conds', '(rc_deleted & ' . ( Revision::DELETED_USER | Revision::DELETED_RESTRICTED ) . ') != ' . ( Revision::DELETED_USER | Revision::DELETED_RESTRICTED ), '(rc_type != ' . RC_LOG . ') OR (' . '(rc_deleted & ' . ( LogPage::DELETED_ACTION | LogPage::DELETED_RESTRICTED ) . ') != ' . ( LogPage::DELETED_ACTION | LogPage::DELETED_RESTRICTED ) . ')' ], + [ 'actormigration' => 'join' ], ], ]; } @@ -1079,7 +1104,9 @@ class WatchedItemQueryServiceUnitTest extends PHPUnit_Framework_TestCase { public function testGetWatchedItemsWithRecentChangeInfo_userPermissionRelatedExtraChecks( array $options, $notAllowedAction, - array $expectedExtraConds + array $expectedExtraTables, + array $expectedExtraConds, + array $expectedExtraJoins ) { $commonConds = [ 'wl_user' => 1, '(rc_this_oldid=page_latest) OR (rc_type=3)' ]; $conds = array_merge( $commonConds, $expectedExtraConds ); @@ -1088,18 +1115,21 @@ class WatchedItemQueryServiceUnitTest extends PHPUnit_Framework_TestCase { $mockDb->expects( $this->once() ) ->method( 'select' ) ->with( - [ 'recentchanges', 'watchlist', 'page' ], + array_merge( [ 'recentchanges', 'watchlist', 'page' ], $expectedExtraTables ), $this->isType( 'array' ), $conds, $this->isType( 'string' ), $this->isType( 'array' ), - $this->isType( 'array' ) + array_merge( [ + 'watchlist' => [ 'INNER JOIN', [ 'wl_namespace=rc_namespace', 'wl_title=rc_title' ] ], + 'page' => [ 'LEFT JOIN', 'rc_cur_id=page_id' ], + ], $expectedExtraJoins ) ) ->will( $this->returnValue( [] ) ); $user = $this->getMockNonAnonUserWithIdAndRestrictedPermissions( 1, $notAllowedAction ); - $queryService = new WatchedItemQueryService( $this->getMockLoadBalancer( $mockDb ) ); + $queryService = $this->newService( $mockDb ); $items = $queryService->getWatchedItemsWithRecentChangeInfo( $user, $options ); $this->assertEmpty( $items ); @@ -1139,7 +1169,7 @@ class WatchedItemQueryServiceUnitTest extends PHPUnit_Framework_TestCase { ) ->will( $this->returnValue( [] ) ); - $queryService = new WatchedItemQueryService( $this->getMockLoadBalancer( $mockDb ) ); + $queryService = $this->newService( $mockDb ); $user = $this->getMockUnrestrictedNonAnonUserWithId( 1 ); $items = $queryService->getWatchedItemsWithRecentChangeInfo( $user, [ 'allRevisions' => true ] ); @@ -1224,7 +1254,7 @@ class WatchedItemQueryServiceUnitTest extends PHPUnit_Framework_TestCase { $mockDb->expects( $this->never() ) ->method( $this->anything() ); - $queryService = new WatchedItemQueryService( $this->getMockLoadBalancer( $mockDb ) ); + $queryService = $this->newService( $mockDb ); $user = $this->getMockUnrestrictedNonAnonUserWithId( 1 ); $this->setExpectedException( InvalidArgumentException::class, $expectedInExceptionMessage ); @@ -1266,7 +1296,7 @@ class WatchedItemQueryServiceUnitTest extends PHPUnit_Framework_TestCase { ) ->will( $this->returnValue( [] ) ); - $queryService = new WatchedItemQueryService( $this->getMockLoadBalancer( $mockDb ) ); + $queryService = $this->newService( $mockDb ); $user = $this->getMockUnrestrictedNonAnonUserWithId( 1 ); $items = $queryService->getWatchedItemsWithRecentChangeInfo( @@ -1308,7 +1338,7 @@ class WatchedItemQueryServiceUnitTest extends PHPUnit_Framework_TestCase { ) ->will( $this->returnValue( [] ) ); - $queryService = new WatchedItemQueryService( $this->getMockLoadBalancer( $mockDb ) ); + $queryService = $this->newService( $mockDb ); $user = $this->getMockUnrestrictedNonAnonUserWithId( 1 ); $items = $queryService->getWatchedItemsWithRecentChangeInfo( @@ -1336,7 +1366,7 @@ class WatchedItemQueryServiceUnitTest extends PHPUnit_Framework_TestCase { ) ->will( $this->returnValue( [] ) ); - $queryService = new WatchedItemQueryService( $this->getMockLoadBalancer( $mockDb ) ); + $queryService = $this->newService( $mockDb ); $user = $this->getMockUnrestrictedNonAnonUserWithId( 1 ); $otherUser = $this->getMockUnrestrictedNonAnonUserWithId( 2 ); $otherUser->expects( $this->once() ) @@ -1367,7 +1397,7 @@ class WatchedItemQueryServiceUnitTest extends PHPUnit_Framework_TestCase { $mockDb->expects( $this->never() ) ->method( $this->anything() ); - $queryService = new WatchedItemQueryService( $this->getMockLoadBalancer( $mockDb ) ); + $queryService = $this->newService( $mockDb ); $user = $this->getMockUnrestrictedNonAnonUserWithId( 1 ); $otherUser = $this->getMockUnrestrictedNonAnonUserWithId( 2 ); $otherUser->expects( $this->once() ) @@ -1404,7 +1434,7 @@ class WatchedItemQueryServiceUnitTest extends PHPUnit_Framework_TestCase { ] ), ] ) ); - $queryService = new WatchedItemQueryService( $this->getMockLoadBalancer( $mockDb ) ); + $queryService = $this->newService( $mockDb ); $user = $this->getMockNonAnonUserWithId( 1 ); $items = $queryService->getWatchedItemsForUser( $user ); @@ -1504,7 +1534,7 @@ class WatchedItemQueryServiceUnitTest extends PHPUnit_Framework_TestCase { ) ->will( $this->returnValue( [] ) ); - $queryService = new WatchedItemQueryService( $this->getMockLoadBalancer( $mockDb ) ); + $queryService = $this->newService( $mockDb ); $items = $queryService->getWatchedItemsForUser( $user, $options ); $this->assertEmpty( $items ); @@ -1601,7 +1631,7 @@ class WatchedItemQueryServiceUnitTest extends PHPUnit_Framework_TestCase { ) ->will( $this->returnCallback( function ( $a, $conj ) { $sqlConj = $conj === LIST_AND ? ' AND ' : ' OR '; - return join( $sqlConj, array_map( function ( $s ) { + return implode( $sqlConj, array_map( function ( $s ) { return '(' . $s . ')'; }, $a ) ); @@ -1617,7 +1647,7 @@ class WatchedItemQueryServiceUnitTest extends PHPUnit_Framework_TestCase { ) ->will( $this->returnValue( [] ) ); - $queryService = new WatchedItemQueryService( $this->getMockLoadBalancer( $mockDb ) ); + $queryService = $this->newService( $mockDb ); $items = $queryService->getWatchedItemsForUser( $user, $options ); $this->assertEmpty( $items ); @@ -1655,7 +1685,7 @@ class WatchedItemQueryServiceUnitTest extends PHPUnit_Framework_TestCase { array $options, $expectedInExceptionMessage ) { - $queryService = new WatchedItemQueryService( $this->getMockLoadBalancer( $this->getMockDb() ) ); + $queryService = $this->newService( $this->getMockDb() ); $this->setExpectedException( InvalidArgumentException::class, $expectedInExceptionMessage ); $queryService->getWatchedItemsForUser( $this->getMockNonAnonUserWithId( 1 ), $options ); @@ -1667,7 +1697,7 @@ class WatchedItemQueryServiceUnitTest extends PHPUnit_Framework_TestCase { $mockDb->expects( $this->never() ) ->method( $this->anything() ); - $queryService = new WatchedItemQueryService( $this->getMockLoadBalancer( $mockDb ) ); + $queryService = $this->newService( $mockDb ); $items = $queryService->getWatchedItemsForUser( $this->getMockAnonUser() ); $this->assertEmpty( $items ); diff --git a/www/wiki/tests/phpunit/includes/WatchedItemStoreIntegrationTest.php b/www/wiki/tests/phpunit/includes/watcheditem/WatchedItemStoreIntegrationTest.php index 61b62aa6..3102929e 100644 --- a/www/wiki/tests/phpunit/includes/WatchedItemStoreIntegrationTest.php +++ b/www/wiki/tests/phpunit/includes/watcheditem/WatchedItemStoreIntegrationTest.php @@ -106,6 +106,23 @@ class WatchedItemStoreIntegrationTest extends MediaWikiTestCase { ); } + public function testWatchBatchAndClearItems() { + $user = $this->getUser(); + $title1 = Title::newFromText( 'WatchedItemStoreIntegrationTestPage1' ); + $title2 = Title::newFromText( 'WatchedItemStoreIntegrationTestPage2' ); + $store = MediaWikiServices::getInstance()->getWatchedItemStore(); + + $store->addWatchBatchForUser( $user, [ $title1, $title2 ] ); + + $this->assertTrue( $store->isWatched( $user, $title1 ) ); + $this->assertTrue( $store->isWatched( $user, $title2 ) ); + + $store->clearUserWatchedItems( $user ); + + $this->assertFalse( $store->isWatched( $user, $title1 ) ); + $this->assertFalse( $store->isWatched( $user, $title2 ) ); + } + public function testUpdateResetAndSetNotificationTimestamp() { $user = $this->getUser(); $otherUser = ( new TestUser( 'WatchedItemStoreIntegrationTestUser_otherUser' ) )->getUser(); diff --git a/www/wiki/tests/phpunit/includes/WatchedItemStoreUnitTest.php b/www/wiki/tests/phpunit/includes/watcheditem/WatchedItemStoreUnitTest.php index 950e2208..26f69088 100644 --- a/www/wiki/tests/phpunit/includes/WatchedItemStoreUnitTest.php +++ b/www/wiki/tests/phpunit/includes/watcheditem/WatchedItemStoreUnitTest.php @@ -1,6 +1,8 @@ <?php use MediaWiki\Linker\LinkTarget; +use Wikimedia\Rdbms\LoadBalancer; use Wikimedia\ScopedCallback; +use Wikimedia\TestingAccessWrapper; /** * @author Addshore @@ -45,6 +47,7 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase { private function getMockCache() { $mock = $this->getMockBuilder( HashBagOStuff::class ) ->disableOriginalConstructor() + ->setMethods( [ 'get', 'set', 'delete', 'makeKey' ] ) ->getMock(); $mock->expects( $this->any() ) ->method( 'makeKey' ) @@ -103,10 +106,82 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase { return new WatchedItemStore( $loadBalancer, $cache, - $readOnlyMode + $readOnlyMode, + 1000 ); } + public function testClearWatchedItems() { + $user = $this->getMockNonAnonUserWithId( 7 ); + + $mockDb = $this->getMockDb(); + $mockDb->expects( $this->once() ) + ->method( 'selectField' ) + ->with( + 'watchlist', + 'COUNT(*)', + [ + 'wl_user' => $user->getId(), + ], + $this->isType( 'string' ) + ) + ->will( $this->returnValue( 12 ) ); + $mockDb->expects( $this->once() ) + ->method( 'delete' ) + ->with( + 'watchlist', + [ 'wl_user' => 7 ], + $this->isType( 'string' ) + ); + + $mockCache = $this->getMockCache(); + $mockCache->expects( $this->never() )->method( 'get' ); + $mockCache->expects( $this->never() )->method( 'set' ); + $mockCache->expects( $this->once() ) + ->method( 'delete' ) + ->with( 'RM-KEY' ); + + $store = $this->newWatchedItemStore( + $this->getMockLoadBalancer( $mockDb ), + $mockCache, + $this->getMockReadOnlyMode() + ); + TestingAccessWrapper::newFromObject( $store ) + ->cacheIndex = [ 0 => [ 'F' => [ 7 => 'RM-KEY', 9 => 'KEEP-KEY' ] ] ]; + + $this->assertTrue( $store->clearUserWatchedItems( $user ) ); + } + + public function testClearWatchedItems_tooManyItemsWatched() { + $user = $this->getMockNonAnonUserWithId( 7 ); + + $mockDb = $this->getMockDb(); + $mockDb->expects( $this->once() ) + ->method( 'selectField' ) + ->with( + 'watchlist', + 'COUNT(*)', + [ + 'wl_user' => $user->getId(), + ], + $this->isType( 'string' ) + ) + ->will( $this->returnValue( 99999 ) ); + + $mockCache = $this->getMockCache(); + $mockCache->expects( $this->never() )->method( 'get' ); + $mockCache->expects( $this->never() )->method( 'set' ); + $mockCache->expects( $this->never() )->method( 'delete' ); + + $store = $this->newWatchedItemStore( + $this->getMockLoadBalancer( $mockDb ), + $mockCache, + $this->getMockReadOnlyMode() + ); + + $this->assertFalse( $store->clearUserWatchedItems( $user ) ); + } + public function testCountWatchedItems() { $user = $this->getMockNonAnonUserWithId( 1 ); @@ -121,7 +196,7 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase { ], $this->isType( 'string' ) ) - ->will( $this->returnValue( 12 ) ); + ->will( $this->returnValue( '12' ) ); $mockCache = $this->getMockCache(); $mockCache->expects( $this->never() )->method( 'get' ); @@ -152,7 +227,7 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase { ], $this->isType( 'string' ) ) - ->will( $this->returnValue( 7 ) ); + ->will( $this->returnValue( '7' ) ); $mockCache = $this->getMockCache(); $mockCache->expects( $this->never() )->method( 'get' ); @@ -178,9 +253,9 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase { $mockDb = $this->getMockDb(); $dbResult = [ - $this->getFakeRow( [ 'wl_title' => 'SomeDbKey', 'wl_namespace' => 0, 'watchers' => 100 ] ), - $this->getFakeRow( [ 'wl_title' => 'OtherDbKey', 'wl_namespace' => 0, 'watchers' => 300 ] ), - $this->getFakeRow( [ 'wl_title' => 'AnotherDbKey', 'wl_namespace' => 1, 'watchers' => 500 ] + $this->getFakeRow( [ 'wl_title' => 'SomeDbKey', 'wl_namespace' => '0', 'watchers' => '100' ] ), + $this->getFakeRow( [ 'wl_title' => 'OtherDbKey', 'wl_namespace' => '0', 'watchers' => '300' ] ), + $this->getFakeRow( [ 'wl_title' => 'AnotherDbKey', 'wl_namespace' => '1', 'watchers' => '500' ] ), ]; $mockDb->expects( $this->once() ) @@ -244,9 +319,9 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase { $mockDb = $this->getMockDb(); $dbResult = [ - $this->getFakeRow( [ 'wl_title' => 'SomeDbKey', 'wl_namespace' => 0, 'watchers' => 100 ] ), - $this->getFakeRow( [ 'wl_title' => 'OtherDbKey', 'wl_namespace' => 0, 'watchers' => 300 ] ), - $this->getFakeRow( [ 'wl_title' => 'AnotherDbKey', 'wl_namespace' => 1, 'watchers' => 500 ] + $this->getFakeRow( [ 'wl_title' => 'SomeDbKey', 'wl_namespace' => '0', 'watchers' => '100' ] ), + $this->getFakeRow( [ 'wl_title' => 'OtherDbKey', 'wl_namespace' => '0', 'watchers' => '300' ] ), + $this->getFakeRow( [ 'wl_title' => 'AnotherDbKey', 'wl_namespace' => '1', 'watchers' => '500' ] ), ]; $mockDb->expects( $this->once() ) @@ -310,7 +385,7 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase { ], $this->isType( 'string' ) ) - ->will( $this->returnValue( 7 ) ); + ->will( $this->returnValue( '7' ) ); $mockDb->expects( $this->exactly( 1 ) ) ->method( 'addQuotes' ) ->will( $this->returnCallback( function ( $value ) { @@ -344,9 +419,11 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase { ]; $dbResult = [ - $this->getFakeRow( [ 'wl_title' => 'SomeDbKey', 'wl_namespace' => 0, 'watchers' => 100 ] ), - $this->getFakeRow( [ 'wl_title' => 'OtherDbKey', 'wl_namespace' => 0, 'watchers' => 300 ] ), - $this->getFakeRow( [ 'wl_title' => 'AnotherDbKey', 'wl_namespace' => 1, 'watchers' => 500 ] ), + $this->getFakeRow( [ 'wl_title' => 'SomeDbKey', 'wl_namespace' => '0', 'watchers' => '100' ] ), + $this->getFakeRow( [ 'wl_title' => 'OtherDbKey', 'wl_namespace' => '0', 'watchers' => '300' ] ), + $this->getFakeRow( + [ 'wl_title' => 'AnotherDbKey', 'wl_namespace' => '1', 'watchers' => '500' ] + ), ]; $mockDb = $this->getMockDb(); $mockDb->expects( $this->exactly( 2 * 3 ) ) @@ -367,7 +444,7 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase { ) ->will( $this->returnCallback( function ( $a, $conj ) { $sqlConj = $conj === LIST_AND ? ' AND ' : ' OR '; - return join( $sqlConj, array_map( function ( $s ) { + return implode( $sqlConj, array_map( function ( $s ) { return '(' . $s . ')'; }, $a ) ); @@ -433,14 +510,16 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase { ]; $dbResult = [ - $this->getFakeRow( [ 'wl_title' => 'SomeDbKey', 'wl_namespace' => 0, 'watchers' => 100 ] ), - $this->getFakeRow( [ 'wl_title' => 'OtherDbKey', 'wl_namespace' => 0, 'watchers' => 300 ] ), - $this->getFakeRow( [ 'wl_title' => 'AnotherDbKey', 'wl_namespace' => 1, 'watchers' => 500 ] ), + $this->getFakeRow( [ 'wl_title' => 'SomeDbKey', 'wl_namespace' => '0', 'watchers' => '100' ] ), + $this->getFakeRow( [ 'wl_title' => 'OtherDbKey', 'wl_namespace' => '0', 'watchers' => '300' ] ), $this->getFakeRow( - [ 'wl_title' => 'SomeNotExisitingDbKey', 'wl_namespace' => 0, 'watchers' => 100 ] + [ 'wl_title' => 'AnotherDbKey', 'wl_namespace' => '1', 'watchers' => '500' ] ), $this->getFakeRow( - [ 'wl_title' => 'OtherNotExisitingDbKey', 'wl_namespace' => 0, 'watchers' => 200 ] + [ 'wl_title' => 'SomeNotExisitingDbKey', 'wl_namespace' => '0', 'watchers' => '100' ] + ), + $this->getFakeRow( + [ 'wl_title' => 'OtherNotExisitingDbKey', 'wl_namespace' => '0', 'watchers' => '200' ] ), ]; $mockDb = $this->getMockDb(); @@ -462,7 +541,7 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase { ) ->will( $this->returnCallback( function ( $a, $conj ) { $sqlConj = $conj === LIST_AND ? ' AND ' : ' OR '; - return join( $sqlConj, array_map( function ( $s ) { + return implode( $sqlConj, array_map( function ( $s ) { return '(' . $s . ')'; }, $a ) ); @@ -595,7 +674,7 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase { ], $this->isType( 'string' ) ) - ->will( $this->returnValue( 9 ) ); + ->will( $this->returnValue( '9' ) ); $mockCache = $this->getMockCache(); $mockCache->expects( $this->never() )->method( 'set' ); @@ -630,7 +709,7 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase { $this->isType( 'string' ), [ 'LIMIT' => 50 ] ) - ->will( $this->returnValue( 50 ) ); + ->will( $this->returnValue( '50' ) ); $mockCache = $this->getMockCache(); $mockCache->expects( $this->never() )->method( 'set' ); @@ -668,7 +747,7 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase { $this->isType( 'string' ), [ 'LIMIT' => 50 ] ) - ->will( $this->returnValue( 9 ) ); + ->will( $this->returnValue( '9' ) ); $mockCache = $this->getMockCache(); $mockCache->expects( $this->never() )->method( 'set' ); @@ -720,8 +799,8 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase { public function testDuplicateEntry_somethingToDuplicate() { $fakeRows = [ - $this->getFakeRow( [ 'wl_user' => 1, 'wl_notificationtimestamp' => '20151212010101' ] ), - $this->getFakeRow( [ 'wl_user' => 2, 'wl_notificationtimestamp' => null ] ), + $this->getFakeRow( [ 'wl_user' => '1', 'wl_notificationtimestamp' => '20151212010101' ] ), + $this->getFakeRow( [ 'wl_user' => '2', 'wl_notificationtimestamp' => null ] ), ]; $mockDb = $this->getMockDb(); @@ -839,7 +918,7 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase { LinkTarget $newTarget ) { $fakeRows = [ - $this->getFakeRow( [ 'wl_user' => 1, 'wl_notificationtimestamp' => '20151212010101' ] ), + $this->getFakeRow( [ 'wl_user' => '1', 'wl_notificationtimestamp' => '20151212010101' ] ), ]; $mockDb = $this->getMockDb(); @@ -1113,7 +1192,7 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase { $this->getMockNonAnonUserWithId( 1 ), new TitleValue( 0, 'SomeDbKey' ) ); - $this->assertInstanceOf( 'WatchedItem', $watchedItem ); + $this->assertInstanceOf( WatchedItem::class, $watchedItem ); $this->assertEquals( 1, $watchedItem->getUser()->getId() ); $this->assertEquals( 'SomeDbKey', $watchedItem->getLinkTarget()->getDBkey() ); $this->assertEquals( 0, $watchedItem->getLinkTarget()->getNamespace() ); @@ -1312,7 +1391,7 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase { $this->getMockNonAnonUserWithId( 1 ), new TitleValue( 0, 'SomeDbKey' ) ); - $this->assertInstanceOf( 'WatchedItem', $watchedItem ); + $this->assertInstanceOf( WatchedItem::class, $watchedItem ); $this->assertEquals( 1, $watchedItem->getUser()->getId() ); $this->assertEquals( 'SomeDbKey', $watchedItem->getLinkTarget()->getDBkey() ); $this->assertEquals( 0, $watchedItem->getLinkTarget()->getNamespace() ); @@ -1452,7 +1531,7 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase { $this->assertInternalType( 'array', $watchedItems ); $this->assertCount( 2, $watchedItems ); foreach ( $watchedItems as $watchedItem ) { - $this->assertInstanceOf( 'WatchedItem', $watchedItem ); + $this->assertInstanceOf( WatchedItem::class, $watchedItem ); } $this->assertEquals( new WatchedItem( $user, new TitleValue( 0, 'Foo1' ), '20151212010101' ), @@ -1511,7 +1590,7 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase { $this->getMockReadOnlyMode() ); - $this->setExpectedException( 'InvalidArgumentException' ); + $this->setExpectedException( InvalidArgumentException::class ); $store->getWatchedItemsForUser( $this->getMockNonAnonUserWithId( 1 ), [ 'sort' => 'foo' ] @@ -1631,13 +1710,13 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase { $mockDb = $this->getMockDb(); $dbResult = [ $this->getFakeRow( [ - 'wl_namespace' => 0, + 'wl_namespace' => '0', 'wl_title' => 'SomeDbKey', 'wl_notificationtimestamp' => '20151212010101', ] ), $this->getFakeRow( [ - 'wl_namespace' => 1, + 'wl_namespace' => '1', 'wl_title' => 'AnotherDbKey', 'wl_notificationtimestamp' => null, ] @@ -1773,7 +1852,7 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase { ) ->will( $this->returnValue( [ $this->getFakeRow( - [ 'wl_namespace' => 1, 'wl_title' => 'AnotherDbKey', 'wl_notificationtimestamp' => null, ] + [ 'wl_namespace' => '1', 'wl_title' => 'AnotherDbKey', 'wl_notificationtimestamp' => null, ] ) ] ) ); @@ -1996,12 +2075,11 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase { ->method( 'selectRow' ); $mockCache = $this->getMockCache(); - $mockDb->expects( $this->never() ) - ->method( 'get' ); - $mockDb->expects( $this->never() ) - ->method( 'set' ); - $mockDb->expects( $this->never() ) - ->method( 'delete' ); + $mockCache->expects( $this->never() )->method( 'get' ); + $mockCache->expects( $this->never() )->method( 'set' ); + $mockCache->expects( $this->once() ) + ->method( 'delete' ) + ->with( '0:SomeDbKey:1' ); $store = $this->newWatchedItemStore( $this->getMockLoadBalancer( $mockDb ), @@ -2090,12 +2168,11 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase { ->method( 'selectRow' ); $mockCache = $this->getMockCache(); - $mockDb->expects( $this->never() ) - ->method( 'get' ); - $mockDb->expects( $this->never() ) - ->method( 'set' ); - $mockDb->expects( $this->never() ) - ->method( 'delete' ); + $mockCache->expects( $this->never() )->method( 'get' ); + $mockCache->expects( $this->never() )->method( 'set' ); + $mockCache->expects( $this->once() ) + ->method( 'delete' ) + ->with( '0:SomeTitle:1' ); $store = $this->newWatchedItemStore( $this->getMockLoadBalancer( $mockDb ), @@ -2157,12 +2234,13 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase { ) ); $mockCache = $this->getMockCache(); - $mockDb->expects( $this->never() ) - ->method( 'get' ); - $mockDb->expects( $this->never() ) - ->method( 'set' ); - $mockDb->expects( $this->never() ) - ->method( 'delete' ); + $mockCache->expects( $this->never() )->method( 'get' ); + $mockCache->expects( $this->once() ) + ->method( 'set' ) + ->with( '0:SomeDbKey:1', $this->isType( 'object' ) ); + $mockCache->expects( $this->once() ) + ->method( 'delete' ) + ->with( '0:SomeDbKey:1' ); $store = $this->newWatchedItemStore( $this->getMockLoadBalancer( $mockDb ), @@ -2233,12 +2311,11 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase { ->will( $this->returnValue( false ) ); $mockCache = $this->getMockCache(); - $mockDb->expects( $this->never() ) - ->method( 'get' ); - $mockDb->expects( $this->never() ) - ->method( 'set' ); - $mockDb->expects( $this->never() ) - ->method( 'delete' ); + $mockCache->expects( $this->never() )->method( 'get' ); + $mockCache->expects( $this->never() )->method( 'set' ); + $mockCache->expects( $this->once() ) + ->method( 'delete' ) + ->with( '0:SomeDbKey:1' ); $store = $this->newWatchedItemStore( $this->getMockLoadBalancer( $mockDb ), @@ -2300,12 +2377,13 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase { ) ); $mockCache = $this->getMockCache(); - $mockDb->expects( $this->never() ) - ->method( 'get' ); - $mockDb->expects( $this->never() ) - ->method( 'set' ); - $mockDb->expects( $this->never() ) - ->method( 'delete' ); + $mockCache->expects( $this->never() )->method( 'get' ); + $mockCache->expects( $this->once() ) + ->method( 'set' ) + ->with( '0:SomeDbKey:1', $this->isType( 'object' ) ); + $mockCache->expects( $this->once() ) + ->method( 'delete' ) + ->with( '0:SomeDbKey:1' ); $store = $this->newWatchedItemStore( $this->getMockLoadBalancer( $mockDb ), @@ -2378,12 +2456,13 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase { ) ); $mockCache = $this->getMockCache(); - $mockDb->expects( $this->never() ) - ->method( 'get' ); - $mockDb->expects( $this->never() ) - ->method( 'set' ); - $mockDb->expects( $this->never() ) - ->method( 'delete' ); + $mockCache->expects( $this->never() )->method( 'get' ); + $mockCache->expects( $this->once() ) + ->method( 'set' ) + ->with( '0:SomeDbKey:1', $this->isType( 'object' ) ); + $mockCache->expects( $this->once() ) + ->method( 'delete' ) + ->with( '0:SomeDbKey:1' ); $store = $this->newWatchedItemStore( $this->getMockLoadBalancer( $mockDb ), diff --git a/www/wiki/tests/phpunit/languages/LanguageCodeTest.php b/www/wiki/tests/phpunit/languages/LanguageCodeTest.php index 7689ef1d..544a0635 100644 --- a/www/wiki/tests/phpunit/languages/LanguageCodeTest.php +++ b/www/wiki/tests/phpunit/languages/LanguageCodeTest.php @@ -2,13 +2,13 @@ /** * @covers LanguageCode - * * @group Language * - * @license GPL-2.0+ - * @author Thiemo Mättig + * @author Thiemo Kreuz */ -class LanguageCodeTest extends PHPUnit_Framework_TestCase { +class LanguageCodeTest extends PHPUnit\Framework\TestCase { + + use MediaWikiCoversValidator; public function testConstructor() { $instance = new LanguageCode(); @@ -43,4 +43,119 @@ class LanguageCodeTest extends PHPUnit_Framework_TestCase { $this->assertEquals( null, LanguageCode::replaceDeprecatedCodes( null ) ); } + /** + * test @see LanguageCode::bcp47(). + * Please note the BCP 47 explicitly state that language codes are case + * insensitive, there are some exceptions to the rule :) + * This test is used to verify our formatting against all lower and + * all upper cases language code. + * + * @see https://tools.ietf.org/html/bcp47 + * @dataProvider provideLanguageCodes() + */ + public function testBcp47( $code, $expected ) { + $code = strtolower( $code ); + $this->assertEquals( $expected, LanguageCode::bcp47( $code ), + "Applying BCP47 standard to lower case '$code'" + ); + + $code = strtoupper( $code ); + $this->assertEquals( $expected, LanguageCode::bcp47( $code ), + "Applying BCP47 standard to upper case '$code'" + ); + } + + /** + * Array format is ($code, $expected) + */ + public static function provideLanguageCodes() { + return [ + // Extracted from BCP 47 (list not exhaustive) + # 2.1.1 + [ 'en-ca-x-ca', 'en-CA-x-ca' ], + [ 'sgn-be-fr', 'sgn-BE-FR' ], + [ 'az-latn-x-latn', 'az-Latn-x-latn' ], + # 2.2 + [ 'sr-Latn-RS', 'sr-Latn-RS' ], + [ 'az-arab-ir', 'az-Arab-IR' ], + + # 2.2.5 + [ 'sl-nedis', 'sl-nedis' ], + [ 'de-ch-1996', 'de-CH-1996' ], + + # 2.2.6 + [ + 'en-latn-gb-boont-r-extended-sequence-x-private', + 'en-Latn-GB-boont-r-extended-sequence-x-private' + ], + + // Examples from BCP 47 Appendix A + # Simple language subtag: + [ 'DE', 'de' ], + [ 'fR', 'fr' ], + [ 'ja', 'ja' ], + + # Language subtag plus script subtag: + [ 'zh-hans', 'zh-Hans' ], + [ 'sr-cyrl', 'sr-Cyrl' ], + [ 'sr-latn', 'sr-Latn' ], + + # Extended language subtags and their primary language subtag + # counterparts: + [ 'zh-cmn-hans-cn', 'zh-cmn-Hans-CN' ], + [ 'cmn-hans-cn', 'cmn-Hans-CN' ], + [ 'zh-yue-hk', 'zh-yue-HK' ], + [ 'yue-hk', 'yue-HK' ], + + # Language-Script-Region: + [ 'zh-hans-cn', 'zh-Hans-CN' ], + [ 'sr-latn-RS', 'sr-Latn-RS' ], + + # Language-Variant: + [ 'sl-rozaj', 'sl-rozaj' ], + [ 'sl-rozaj-biske', 'sl-rozaj-biske' ], + [ 'sl-nedis', 'sl-nedis' ], + + # Language-Region-Variant: + [ 'de-ch-1901', 'de-CH-1901' ], + [ 'sl-it-nedis', 'sl-IT-nedis' ], + + # Language-Script-Region-Variant: + [ 'hy-latn-it-arevela', 'hy-Latn-IT-arevela' ], + + # Language-Region: + [ 'de-de', 'de-DE' ], + [ 'en-us', 'en-US' ], + [ 'es-419', 'es-419' ], + + # Private use subtags: + [ 'de-ch-x-phonebk', 'de-CH-x-phonebk' ], + [ 'az-arab-x-aze-derbend', 'az-Arab-x-aze-derbend' ], + /** + * Previous test does not reflect the BCP 47 which states: + * az-Arab-x-AZE-derbend + * AZE being private, it should be lower case, hence the test above + * should probably be: + * [ 'az-arab-x-aze-derbend', 'az-Arab-x-AZE-derbend' ], + */ + + # Private use registry values: + [ 'x-whatever', 'x-whatever' ], + [ 'qaa-qaaa-qm-x-southern', 'qaa-Qaaa-QM-x-southern' ], + [ 'de-qaaa', 'de-Qaaa' ], + [ 'sr-latn-qm', 'sr-Latn-QM' ], + [ 'sr-qaaa-rs', 'sr-Qaaa-RS' ], + + # Tags that use extensions + [ 'en-us-u-islamcal', 'en-US-u-islamcal' ], + [ 'zh-cn-a-myext-x-private', 'zh-CN-a-myext-x-private' ], + [ '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/www/wiki/tests/phpunit/languages/LanguageConverterTest.php b/www/wiki/tests/phpunit/languages/LanguageConverterTest.php index fc2ed33b..82ab7def 100644 --- a/www/wiki/tests/phpunit/languages/LanguageConverterTest.php +++ b/www/wiki/tests/phpunit/languages/LanguageConverterTest.php @@ -160,6 +160,8 @@ class LanguageConverterTest extends MediaWikiLangTestCase { /** * Test exhausting pcre.backtrack_limit + * + * @covers LanguageConverter::autoConvert */ public function testAutoConvertT124404() { $testString = ''; diff --git a/www/wiki/tests/phpunit/languages/LanguageTest.php b/www/wiki/tests/phpunit/languages/LanguageTest.php index cd52366f..66bd76df 100644 --- a/www/wiki/tests/phpunit/languages/LanguageTest.php +++ b/www/wiki/tests/phpunit/languages/LanguageTest.php @@ -209,71 +209,105 @@ class LanguageTest extends LanguageClassesTestCase { } /** - * @covers Language::truncate + * @covers Language::truncateForDatabase + * @covers Language::truncateInternal */ - public function testTruncate() { + public function testTruncateForDatabase() { $this->assertEquals( "XXX", - $this->getLang()->truncate( "1234567890", 0, 'XXX' ), + $this->getLang()->truncateForDatabase( "1234567890", 0, 'XXX' ), 'truncate prefix, len 0, small ellipsis' ); $this->assertEquals( "12345XXX", - $this->getLang()->truncate( "1234567890", 8, 'XXX' ), + $this->getLang()->truncateForDatabase( "1234567890", 8, 'XXX' ), 'truncate prefix, small ellipsis' ); $this->assertEquals( "123456789", - $this->getLang()->truncate( "123456789", 5, 'XXXXXXXXXXXXXXX' ), + $this->getLang()->truncateForDatabase( "123456789", 5, 'XXXXXXXXXXXXXXX' ), 'truncate prefix, large ellipsis' ); $this->assertEquals( "XXX67890", - $this->getLang()->truncate( "1234567890", -8, 'XXX' ), + $this->getLang()->truncateForDatabase( "1234567890", -8, 'XXX' ), 'truncate suffix, small ellipsis' ); $this->assertEquals( "123456789", - $this->getLang()->truncate( "123456789", -5, 'XXXXXXXXXXXXXXX' ), + $this->getLang()->truncateForDatabase( "123456789", -5, 'XXXXXXXXXXXXXXX' ), 'truncate suffix, large ellipsis' ); $this->assertEquals( "123XXX", - $this->getLang()->truncate( "123 ", 9, 'XXX' ), + $this->getLang()->truncateForDatabase( "123 ", 9, 'XXX' ), 'truncate prefix, with spaces' ); $this->assertEquals( "12345XXX", - $this->getLang()->truncate( "12345 8", 11, 'XXX' ), + $this->getLang()->truncateForDatabase( "12345 8", 11, 'XXX' ), 'truncate prefix, with spaces and non-space ending' ); $this->assertEquals( "XXX234", - $this->getLang()->truncate( "1 234", -8, 'XXX' ), + $this->getLang()->truncateForDatabase( "1 234", -8, 'XXX' ), 'truncate suffix, with spaces' ); $this->assertEquals( "12345XXX", - $this->getLang()->truncate( "1234567890", 5, 'XXX', false ), + $this->getLang()->truncateForDatabase( "1234567890", 5, 'XXX', false ), 'truncate without adjustment' ); $this->assertEquals( "泰乐菌...", - $this->getLang()->truncate( "泰乐菌素123456789", 11, '...', false ), + $this->getLang()->truncateForDatabase( "泰乐菌素123456789", 11, '...', false ), 'truncate does not chop Unicode characters in half' ); $this->assertEquals( "\n泰乐菌...", - $this->getLang()->truncate( "\n泰乐菌素123456789", 12, '...', false ), + $this->getLang()->truncateForDatabase( "\n泰乐菌素123456789", 12, '...', false ), 'truncate does not chop Unicode characters in half if there is a preceding newline' ); } /** + * @dataProvider provideTruncateData + * @covers Language::truncateForVisual + * @covers Language::truncateInternal + */ + public function testTruncateForVisual( + $expected, $string, $length, $ellipsis = '...', $adjustLength = true + ) { + $this->assertEquals( + $expected, + $this->getLang()->truncateForVisual( $string, $length, $ellipsis, $adjustLength ) + ); + } + + /** + * @return array Format is ($expected, $string, $length, $ellipsis, $adjustLength) + */ + public static function provideTruncateData() { + return [ + [ "XXX", "тестирам да ли ради", 0, "XXX" ], + [ "testnXXX", "testni scenarij", 8, "XXX" ], + [ "حالة اختبار", "حالة اختبار", 5, "XXXXXXXXXXXXXXX" ], + [ "XXXедент", "прецедент", -8, "XXX" ], + [ "XXപിൾ", "ആപ്പിൾ", -5, "XX" ], + [ "神秘XXX", "神秘 ", 9, "XXX" ], + [ "ΔημιουργXXX", "Δημιουργία Σύμπαντος", 11, "XXX" ], + [ "XXXの家です", "地球は私たちの唯 の家です", -8, "XXX" ], + [ "زندگیXXX", "زندگی زیباست", 6, "XXX", false ], + [ "ცხოვრება...", "ცხოვრება არის საოცარი", 8, "...", false ], + [ "\nທ່ານ...", "\nທ່ານບໍ່ຮູ້ຫນັງສື", 5, "...", false ], + ]; + } + + /** * @dataProvider provideHTMLTruncateData * @covers Language::truncateHTML */ @@ -1010,6 +1044,27 @@ class LanguageTest extends LanguageClassesTestCase { 'nengo' ], [ + 'xtY', + '20190430235959', + '平成31', + '平成31', + 'nengo - last day of heisei' + ], + [ + 'xtY', + '20190501000000', + '令和元', + '令和元', + 'nengo - first day of reiwa' + ], + [ + 'xtY', + '20200501000000', + '令和2', + '令和2', + 'nengo - second year of reiwa' + ], + [ 'xrxkYY', '20120102090705', 'MMDLV2012', @@ -1326,7 +1381,7 @@ class LanguageTest extends LanguageClassesTestCase { } public static function provideCheckTitleEncodingData() { - // @codingStandardsIgnoreStart Ignore Generic.Files.LineLength.TooLong + // phpcs:disable Generic.Files.LineLength return [ [ "" ], [ "United States of America" ], // 7bit ASCII @@ -1377,7 +1432,7 @@ class LanguageTest extends LanguageClassesTestCase { ) ] ]; - // @codingStandardsIgnoreEnd + // phpcs:enable } /** @@ -1630,6 +1685,34 @@ class LanguageTest extends LanguageClassesTestCase { } /** + * @dataProvider provideFormatNum + * @covers Language::formatNum + */ + public function testFormatNum( + $translateNumerals, $langCode, $number, $nocommafy, $expected + ) { + $this->setMwGlobals( [ 'wgTranslateNumerals' => $translateNumerals ] ); + $lang = Language::factory( $langCode ); + $formattedNum = $lang->formatNum( $number, $nocommafy ); + $this->assertType( 'string', $formattedNum ); + $this->assertEquals( $expected, $formattedNum ); + } + + public function provideFormatNum() { + return [ + [ true, 'en', 100, false, '100' ], + [ true, 'en', 101, true, '101' ], + [ false, 'en', 103, false, '103' ], + [ false, 'en', 104, true, '104' ], + [ true, 'en', '105', false, '105' ], + [ true, 'en', '106', true, '106' ], + [ false, 'en', '107', false, '107' ], + [ false, 'en', '108', true, '108' ], + ]; + } + + /** + * @covers Language::parseFormattedNumber * @dataProvider parseFormattedNumberProvider */ public function testParseFormattedNumber( $langCode, $number ) { @@ -1768,6 +1851,9 @@ class LanguageTest extends LanguageClassesTestCase { ]; } + /** + * @covers Language::equals + */ public function testEquals() { $en1 = new Language(); $en1->setCode( 'en' ); diff --git a/www/wiki/tests/phpunit/languages/SpecialPageAliasTest.php b/www/wiki/tests/phpunit/languages/SpecialPageAliasTest.php index 4a7fed2a..0bb6a4d2 100644 --- a/www/wiki/tests/phpunit/languages/SpecialPageAliasTest.php +++ b/www/wiki/tests/phpunit/languages/SpecialPageAliasTest.php @@ -25,7 +25,7 @@ class SpecialPageAliasTest extends MediaWikiTestCase { } public function validSpecialPageAliasesProvider() { - $codes = array_keys( Language::fetchLanguageNames( 'mwfile' ) ); + $codes = array_keys( Language::fetchLanguageNames( null, 'mwfile' ) ); $data = []; diff --git a/www/wiki/tests/phpunit/languages/classes/LanguageArTest.php b/www/wiki/tests/phpunit/languages/classes/LanguageArTest.php index 5a667598..f3f5a3f1 100644 --- a/www/wiki/tests/phpunit/languages/classes/LanguageArTest.php +++ b/www/wiki/tests/phpunit/languages/classes/LanguageArTest.php @@ -4,7 +4,9 @@ * @file */ -/** Tests for MediaWiki languages/LanguageAr.php */ +/** + * @covers LanguageAr + */ class LanguageArTest extends LanguageClassesTestCase { /** * @covers Language::formatNum diff --git a/www/wiki/tests/phpunit/languages/classes/LanguageBe_taraskTest.php b/www/wiki/tests/phpunit/languages/classes/LanguageBe_taraskTest.php index 26db1062..4f049cd1 100644 --- a/www/wiki/tests/phpunit/languages/classes/LanguageBe_taraskTest.php +++ b/www/wiki/tests/phpunit/languages/classes/LanguageBe_taraskTest.php @@ -1,8 +1,11 @@ <?php -// @codingStandardsIgnoreStart Ignore Squiz.Classes.ValidClassName.NotCamelCaps +// phpcs:disable Squiz.Classes.ValidClassName.NotCamelCaps +/** + * @covers LanguageBe_tarask + */ class LanguageBe_taraskTest extends LanguageClassesTestCase { - // @codingStandardsIgnoreEnd + // phpcs:enable /** * Make sure the language code we are given is indeed * be-tarask. This is to ensure LanguageClassesTestCase diff --git a/www/wiki/tests/phpunit/languages/classes/LanguageBsTest.php b/www/wiki/tests/phpunit/languages/classes/LanguageBsTest.php index 207f5054..29b2ccf3 100644 --- a/www/wiki/tests/phpunit/languages/classes/LanguageBsTest.php +++ b/www/wiki/tests/phpunit/languages/classes/LanguageBsTest.php @@ -5,7 +5,11 @@ * @file */ -/** Tests for Croatian (hrvatski) */ +/** + * Tests for Croatian (hrvatski) + * + * @covers LanguageBs + */ class LanguageBsTest extends LanguageClassesTestCase { /** * @dataProvider providePlural diff --git a/www/wiki/tests/phpunit/languages/classes/LanguageCrhTest.php b/www/wiki/tests/phpunit/languages/classes/LanguageCrhTest.php new file mode 100644 index 00000000..7c99614e --- /dev/null +++ b/www/wiki/tests/phpunit/languages/classes/LanguageCrhTest.php @@ -0,0 +1,92 @@ +<?php + +/** + * @covers LanguageCrh + * @covers CrhConverter + */ +class LanguageCrhTest extends LanguageClassesTestCase { + /** + * @dataProvider provideAutoConvertToAllVariants + * @covers Language::autoConvertToAllVariants + */ + public function testAutoConvertToAllVariants( $result, $value ) { + $this->assertEquals( $result, $this->getLang()->autoConvertToAllVariants( $value ) ); + } + + public static function provideAutoConvertToAllVariants() { + return [ + [ // general words, covering more of the alphabet + [ + 'crh' => 'рузгярнынъ ruzgârnıñ Париж Parij', + 'crh-cyrl' => 'рузгярнынъ рузгярнынъ Париж Париж', + 'crh-latn' => 'ruzgârnıñ ruzgârnıñ Parij Parij', + ], + 'рузгярнынъ ruzgârnıñ Париж Parij' + ], + [ // general words, covering more of the alphabet + [ + 'crh' => 'чёкюч çöküç элифбени elifbeni полициясы politsiyası', + 'crh-cyrl' => 'чёкюч чёкюч элифбени элифбени полициясы полициясы', + 'crh-latn' => 'çöküç çöküç elifbeni elifbeni politsiyası politsiyası', + ], + 'чёкюч çöküç элифбени elifbeni полициясы politsiyası' + ], + [ // general words, covering more of the alphabet + [ + 'crh' => 'хусусында hususında акъшамларны aqşamlarnı опькеленюв öpkelenüv', + 'crh-cyrl' => 'хусусында хусусында акъшамларны акъшамларны опькеленюв опькеленюв', + 'crh-latn' => 'hususında hususında aqşamlarnı aqşamlarnı öpkelenüv öpkelenüv', + ], + 'хусусында hususında акъшамларны aqşamlarnı опькеленюв öpkelenüv' + ], + [ // general words, covering more of the alphabet + [ + 'crh' => 'кулюмсиреди külümsiredi айтмайджагъым aytmaycağım козьяшсыз közyaşsız', + 'crh-cyrl' => 'кулюмсиреди кулюмсиреди айтмайджагъым айтмайджагъым козьяшсыз козьяшсыз', + 'crh-latn' => 'külümsiredi külümsiredi aytmaycağım aytmaycağım közyaşsız közyaşsız', + ], + 'кулюмсиреди külümsiredi айтмайджагъым aytmaycağım козьяшсыз közyaşsız' + ], + [ // exception words + [ + 'crh' => 'инструменталь instrumental гургуль gürgül тюшюнмемек tüşünmemek', + 'crh-cyrl' => 'инструменталь инструменталь гургуль гургуль тюшюнмемек тюшюнмемек', + 'crh-latn' => 'instrumental instrumental gürgül gürgül tüşünmemek tüşünmemek', + ], + 'инструменталь instrumental гургуль gürgül тюшюнмемек tüşünmemek' + ], + [ // recent problem words, part 1 + [ + 'crh' => 'künü куню sürgünligi сюргюнлиги özü озю etti этти', + 'crh-cyrl' => 'куню куню сюргюнлиги сюргюнлиги озю озю этти этти', + 'crh-latn' => 'künü künü sürgünligi sürgünligi özü özü etti etti', + ], + 'künü куню sürgünligi сюргюнлиги özü озю etti этти' + ], + [ // recent problem words, part 2 + [ + 'crh' => 'esas эсас dört дёрт keldi кельди', + 'crh-cyrl' => 'эсас эсас дёрт дёрт кельди кельди', + 'crh-latn' => 'esas esas dört dört keldi keldi', + ], + 'esas эсас dört дёрт keldi кельди' + ], + [ // multi part words + [ + 'crh' => 'эки юз eki yüz', + 'crh-cyrl' => 'эки юз эки юз', + 'crh-latn' => 'eki yüz eki yüz', + ], + 'эки юз eki yüz' + ], + [ // ALL CAPS, made up acronyms (not 100% sure these are correct) + [ + 'crh' => 'ÑAB QIC ĞUK COT НЪАБ КЪЫДж ГЪУК ДЖОТ CA ДЖА', + 'crh-cyrl' => 'НЪАБ КЪЫДж ГЪУК ДЖОТ НЪАБ КЪЫДж ГЪУК ДЖОТ ДЖА ДЖА', + 'crh-latn' => 'ÑAB QIC ĞUK COT ÑAB QIC ĞUK COT CA CA', + ], + 'ÑAB QIC ĞUK COT НЪАБ КЪЫДж ГЪУК ДЖОТ CA ДЖА' + ], + ]; + } +} diff --git a/www/wiki/tests/phpunit/languages/classes/LanguageCuTest.php b/www/wiki/tests/phpunit/languages/classes/LanguageCuTest.php index de65d162..565a8856 100644 --- a/www/wiki/tests/phpunit/languages/classes/LanguageCuTest.php +++ b/www/wiki/tests/phpunit/languages/classes/LanguageCuTest.php @@ -5,7 +5,9 @@ * @file */ -/** Tests for MediaWiki languages/LanguageCu.php */ +/** + * @covers LanguageCu + */ class LanguageCuTest extends LanguageClassesTestCase { /** * @dataProvider providePlural diff --git a/www/wiki/tests/phpunit/languages/classes/LanguageDsbTest.php b/www/wiki/tests/phpunit/languages/classes/LanguageDsbTest.php index 949d5dbe..877a70cd 100644 --- a/www/wiki/tests/phpunit/languages/classes/LanguageDsbTest.php +++ b/www/wiki/tests/phpunit/languages/classes/LanguageDsbTest.php @@ -5,7 +5,9 @@ * @file */ -/** Tests for MediaWiki languages/classes/LanguageDsb.php */ +/** + * @covers LanguageDsb + */ class LanguageDsbTest extends LanguageClassesTestCase { /** * @dataProvider providePlural diff --git a/www/wiki/tests/phpunit/languages/classes/LanguageGanTest.php b/www/wiki/tests/phpunit/languages/classes/LanguageGanTest.php index 43eb93e3..c5d9e5e6 100644 --- a/www/wiki/tests/phpunit/languages/classes/LanguageGanTest.php +++ b/www/wiki/tests/phpunit/languages/classes/LanguageGanTest.php @@ -1,5 +1,9 @@ <?php +/** + * @covers LanguageGan + * @covers GanConverter + */ class LanguageGanTest extends LanguageClassesTestCase { /** * @dataProvider provideAutoConvertToAllVariants diff --git a/www/wiki/tests/phpunit/languages/classes/LanguageHsbTest.php b/www/wiki/tests/phpunit/languages/classes/LanguageHsbTest.php index 133cc1c3..0841f6f9 100644 --- a/www/wiki/tests/phpunit/languages/classes/LanguageHsbTest.php +++ b/www/wiki/tests/phpunit/languages/classes/LanguageHsbTest.php @@ -5,7 +5,9 @@ * @file */ -/** Tests for MediaWiki languages/classes/LanguageHsb.php */ +/** + * @covers LanguageHsb + */ class LanguageHsbTest extends LanguageClassesTestCase { /** * @dataProvider providePlural diff --git a/www/wiki/tests/phpunit/languages/classes/LanguageHuTest.php b/www/wiki/tests/phpunit/languages/classes/LanguageHuTest.php index 7ea63e10..a1925bdf 100644 --- a/www/wiki/tests/phpunit/languages/classes/LanguageHuTest.php +++ b/www/wiki/tests/phpunit/languages/classes/LanguageHuTest.php @@ -5,7 +5,9 @@ * @file */ -/** Tests for MediaWiki languages/LanguageHu.php */ +/** + * @covers LanguageHu + */ class LanguageHuTest extends LanguageClassesTestCase { /** * @dataProvider providePlural diff --git a/www/wiki/tests/phpunit/languages/classes/LanguageHyTest.php b/www/wiki/tests/phpunit/languages/classes/LanguageHyTest.php index 64530912..b4936154 100644 --- a/www/wiki/tests/phpunit/languages/classes/LanguageHyTest.php +++ b/www/wiki/tests/phpunit/languages/classes/LanguageHyTest.php @@ -5,7 +5,11 @@ * @file */ -/** Tests for Armenian (Հայերեն) */ +/** + * Tests for Armenian (Հայերեն) + * + * @covers LanguageHy + */ class LanguageHyTest extends LanguageClassesTestCase { /** * @dataProvider providePlural diff --git a/www/wiki/tests/phpunit/languages/classes/LanguageIuTest.php b/www/wiki/tests/phpunit/languages/classes/LanguageIuTest.php index ff9c4d0d..01d97fc0 100644 --- a/www/wiki/tests/phpunit/languages/classes/LanguageIuTest.php +++ b/www/wiki/tests/phpunit/languages/classes/LanguageIuTest.php @@ -1,5 +1,9 @@ <?php +/** + * @covers LanguageIu + * @covers IuConverter + */ class LanguageIuTest extends LanguageClassesTestCase { /** * @dataProvider provideAutoConvertToAllVariants diff --git a/www/wiki/tests/phpunit/languages/classes/LanguageKkTest.php b/www/wiki/tests/phpunit/languages/classes/LanguageKkTest.php index a03eac22..f21950e0 100644 --- a/www/wiki/tests/phpunit/languages/classes/LanguageKkTest.php +++ b/www/wiki/tests/phpunit/languages/classes/LanguageKkTest.php @@ -1,5 +1,10 @@ <?php +/** + * @covers LanguageKk + * @covers LanguageKk_cyrl + * @covers KkConverter + */ class LanguageKkTest extends LanguageClassesTestCase { /** * @dataProvider provideAutoConvertToAllVariants diff --git a/www/wiki/tests/phpunit/languages/classes/LanguageKshTest.php b/www/wiki/tests/phpunit/languages/classes/LanguageKshTest.php index f77c5b62..6419e281 100644 --- a/www/wiki/tests/phpunit/languages/classes/LanguageKshTest.php +++ b/www/wiki/tests/phpunit/languages/classes/LanguageKshTest.php @@ -5,7 +5,9 @@ * @file */ -/** Tests for MediaWiki languages/classes/LanguageKsh.php */ +/** + * @covers LanguageKsh + */ class LanguageKshTest extends LanguageClassesTestCase { /** * @dataProvider providePlural diff --git a/www/wiki/tests/phpunit/languages/classes/LanguageKuTest.php b/www/wiki/tests/phpunit/languages/classes/LanguageKuTest.php index 797ab4a4..db693088 100644 --- a/www/wiki/tests/phpunit/languages/classes/LanguageKuTest.php +++ b/www/wiki/tests/phpunit/languages/classes/LanguageKuTest.php @@ -1,5 +1,9 @@ <?php +/** + * @covers LanguageKu + * @covers KuConverter + */ class LanguageKuTest extends LanguageClassesTestCase { /** * @dataProvider provideAutoConvertToAllVariants diff --git a/www/wiki/tests/phpunit/languages/classes/LanguageMlTest.php b/www/wiki/tests/phpunit/languages/classes/LanguageMlTest.php index 6bac031d..673b5c77 100644 --- a/www/wiki/tests/phpunit/languages/classes/LanguageMlTest.php +++ b/www/wiki/tests/phpunit/languages/classes/LanguageMlTest.php @@ -5,7 +5,9 @@ * @file */ -/** Tests for MediaWiki languages/LanguageMl.php */ +/** + * @covers LanguageMl + */ class LanguageMlTest extends LanguageClassesTestCase { /** diff --git a/www/wiki/tests/phpunit/languages/classes/LanguagePlTest.php b/www/wiki/tests/phpunit/languages/classes/LanguagePlTest.php index a6d0f2e1..14877290 100644 --- a/www/wiki/tests/phpunit/languages/classes/LanguagePlTest.php +++ b/www/wiki/tests/phpunit/languages/classes/LanguagePlTest.php @@ -74,4 +74,31 @@ class LanguagePlTest extends LanguageClassesTestCase { [ 'other', 201 ], ]; } + + /** + * @covers Language::commafy() + * @dataProvider provideCommafyData + */ + public function testCommafy( $number, $numbersWithCommas ) { + $this->assertEquals( + $numbersWithCommas, + $this->getLang()->commafy( $number ), + "commafy('$number')" + ); + } + + public static function provideCommafyData() { + // Note that commafy() always uses English separators (',' and '.') instead of + // Polish (' ' and ','). There is another function that converts them later. + return [ + [ 1000, '1000' ], + [ 10000, '10,000' ], + [ 1000.0001, '1000.0001' ], + [ 10000.0001, '10,000.0001' ], + [ -1000, '-1000' ], + [ -10000, '-10,000' ], + [ -1000.0001, '-1000.0001' ], + [ -10000.0001, '-10,000.0001' ], + ]; + } } diff --git a/www/wiki/tests/phpunit/languages/classes/LanguageRuTest.php b/www/wiki/tests/phpunit/languages/classes/LanguageRuTest.php index a76293c1..a34c03fd 100644 --- a/www/wiki/tests/phpunit/languages/classes/LanguageRuTest.php +++ b/www/wiki/tests/phpunit/languages/classes/LanguageRuTest.php @@ -6,7 +6,6 @@ * @file */ -/** Tests for MediaWiki languages/classes/LanguageRu.php */ class LanguageRuTest extends LanguageClassesTestCase { /** * @dataProvider providePlural @@ -101,6 +100,11 @@ class LanguageRuTest extends LanguageClassesTestCase { 'genitive', ], [ + 'Викиверситета', + 'Викиверситет', + 'genitive', + ], + [ 'Викискладе', 'Викисклад', 'prepositional', @@ -111,6 +115,11 @@ class LanguageRuTest extends LanguageClassesTestCase { 'prepositional', ], [ + 'Викиверситете', + 'Викиверситет', + 'prepositional', + ], + [ 'русского', 'русский', 'languagegen', diff --git a/www/wiki/tests/phpunit/languages/classes/LanguageShiTest.php b/www/wiki/tests/phpunit/languages/classes/LanguageShiTest.php index 207a1b0b..1d0f8635 100644 --- a/www/wiki/tests/phpunit/languages/classes/LanguageShiTest.php +++ b/www/wiki/tests/phpunit/languages/classes/LanguageShiTest.php @@ -1,5 +1,9 @@ <?php +/** + * @covers LanguageShi + * @covers ShiConverter + */ class LanguageShiTest extends LanguageClassesTestCase { /** * @dataProvider provideAutoConvertToAllVariants diff --git a/www/wiki/tests/phpunit/languages/classes/LanguageSlTest.php b/www/wiki/tests/phpunit/languages/classes/LanguageSlTest.php index ed138c50..50100ce7 100644 --- a/www/wiki/tests/phpunit/languages/classes/LanguageSlTest.php +++ b/www/wiki/tests/phpunit/languages/classes/LanguageSlTest.php @@ -6,7 +6,9 @@ * @file */ -/** Tests for MediaWiki languages/classes/LanguageSl.php */ +/** + * @covers LanguageSl + */ class LanguageSlTest extends LanguageClassesTestCase { /** * @dataProvider providerPlural diff --git a/www/wiki/tests/phpunit/languages/classes/LanguageSrTest.php b/www/wiki/tests/phpunit/languages/classes/LanguageSrTest.php index b64fd679..e81d5370 100644 --- a/www/wiki/tests/phpunit/languages/classes/LanguageSrTest.php +++ b/www/wiki/tests/phpunit/languages/classes/LanguageSrTest.php @@ -16,7 +16,10 @@ * - Tests for LanguageConverter and Language should probably be separate.. */ -/** Tests for MediaWiki languages/LanguageSr.php */ +/** + * @covers LanguageSr + * @covers SrConverter + */ class LanguageSrTest extends LanguageClassesTestCase { /** * @covers LanguageConverter::convertTo diff --git a/www/wiki/tests/phpunit/languages/classes/LanguageTgTest.php b/www/wiki/tests/phpunit/languages/classes/LanguageTgTest.php index 0ed24ff1..89697675 100644 --- a/www/wiki/tests/phpunit/languages/classes/LanguageTgTest.php +++ b/www/wiki/tests/phpunit/languages/classes/LanguageTgTest.php @@ -1,5 +1,9 @@ <?php +/** + * @covers LanguageTg + * @covers TgConverter + */ class LanguageTgTest extends LanguageClassesTestCase { /** * @dataProvider provideAutoConvertToAllVariants diff --git a/www/wiki/tests/phpunit/languages/classes/LanguageTrTest.php b/www/wiki/tests/phpunit/languages/classes/LanguageTrTest.php index 28d71df7..3ddf2d03 100644 --- a/www/wiki/tests/phpunit/languages/classes/LanguageTrTest.php +++ b/www/wiki/tests/phpunit/languages/classes/LanguageTrTest.php @@ -5,7 +5,9 @@ * @file */ -/** Tests for MediaWiki languages/LanguageTr.php */ +/** + * @covers LanguageTr + */ class LanguageTrTest extends LanguageClassesTestCase { /** diff --git a/www/wiki/tests/phpunit/languages/classes/LanguageUkTest.php b/www/wiki/tests/phpunit/languages/classes/LanguageUkTest.php index 6b259823..0ccebbe2 100644 --- a/www/wiki/tests/phpunit/languages/classes/LanguageUkTest.php +++ b/www/wiki/tests/phpunit/languages/classes/LanguageUkTest.php @@ -6,7 +6,6 @@ * @file */ -/** Tests for Ukrainian */ class LanguageUkTest extends LanguageClassesTestCase { /** * @dataProvider providePlural diff --git a/www/wiki/tests/phpunit/languages/classes/LanguageUzTest.php b/www/wiki/tests/phpunit/languages/classes/LanguageUzTest.php index 7ef87bf4..367226d7 100644 --- a/www/wiki/tests/phpunit/languages/classes/LanguageUzTest.php +++ b/www/wiki/tests/phpunit/languages/classes/LanguageUzTest.php @@ -16,7 +16,10 @@ * - Tests for LanguageConverter and Language should probably be separate.. */ -/** Tests for MediaWiki languages/LanguageUz.php */ +/** + * @covers LanguageUz + * @covers UzConverter + */ class LanguageUzTest extends LanguageClassesTestCase { /** diff --git a/www/wiki/tests/phpunit/languages/classes/LanguageWaTest.php b/www/wiki/tests/phpunit/languages/classes/LanguageWaTest.php index 27e57f64..80c98603 100644 --- a/www/wiki/tests/phpunit/languages/classes/LanguageWaTest.php +++ b/www/wiki/tests/phpunit/languages/classes/LanguageWaTest.php @@ -5,7 +5,9 @@ * @file */ -/** Tests for MediaWiki languages/classes/LanguageWa.php */ +/** + * @covers LanguageWa + */ class LanguageWaTest extends LanguageClassesTestCase { /** * @dataProvider providePlural diff --git a/www/wiki/tests/phpunit/languages/classes/LanguageZhTest.php b/www/wiki/tests/phpunit/languages/classes/LanguageZhTest.php index 26edc90a..2e73ac51 100644 --- a/www/wiki/tests/phpunit/languages/classes/LanguageZhTest.php +++ b/www/wiki/tests/phpunit/languages/classes/LanguageZhTest.php @@ -1,5 +1,10 @@ <?php +/** + * @covers LanguageZh + * @covers LanguageZh_hans + * @covers ZhConverter + */ class LanguageZhTest extends LanguageClassesTestCase { /** * @dataProvider provideAutoConvertToAllVariants diff --git a/www/wiki/tests/phpunit/maintenance/BenchmarkerTest.php b/www/wiki/tests/phpunit/maintenance/BenchmarkerTest.php new file mode 100644 index 00000000..c15d789d --- /dev/null +++ b/www/wiki/tests/phpunit/maintenance/BenchmarkerTest.php @@ -0,0 +1,142 @@ +<?php + +namespace MediaWiki\Tests\Maintenance; + +use Benchmarker; +use MediaWikiCoversValidator; +use Wikimedia\TestingAccessWrapper; + +/** + * @covers Benchmarker + */ +class BenchmarkerTest extends \PHPUnit\Framework\TestCase { + + use MediaWikiCoversValidator; + + public function testBenchSimple() { + $bench = $this->getMockBuilder( Benchmarker::class ) + ->setMethods( [ 'execute', 'output' ] ) + ->getMock(); + $benchProxy = TestingAccessWrapper::newFromObject( $bench ); + $benchProxy->defaultCount = 3; + + $count = 0; + $bench->bench( [ + 'test' => function () use ( &$count ) { + $count++; + } + ] ); + + $this->assertSame( 3, $count ); + } + + public function testBenchSetup() { + $bench = $this->getMockBuilder( Benchmarker::class ) + ->setMethods( [ 'execute', 'output' ] ) + ->getMock(); + $benchProxy = TestingAccessWrapper::newFromObject( $bench ); + $benchProxy->defaultCount = 2; + + $buffer = []; + $bench->bench( [ + 'test' => [ + 'setup' => function () use ( &$buffer ) { + $buffer[] = 'setup'; + }, + 'function' => function () use ( &$buffer ) { + $buffer[] = 'run'; + } + ] + ] ); + + $this->assertSame( [ 'setup', 'run', 'run' ], $buffer ); + } + + public function testBenchVerbose() { + $bench = $this->getMockBuilder( Benchmarker::class ) + ->setMethods( [ 'execute', 'output', 'hasOption', 'verboseRun' ] ) + ->getMock(); + $benchProxy = TestingAccessWrapper::newFromObject( $bench ); + $benchProxy->defaultCount = 1; + + $bench->expects( $this->exactly( 2 ) )->method( 'hasOption' ) + ->will( $this->returnValueMap( [ + [ 'verbose', true ], + [ 'count', false ], + ] ) ); + + $bench->expects( $this->once() )->method( 'verboseRun' ) + ->with( 0 ) + ->willReturn( null ); + + $bench->bench( [ + 'test' => function () { + } + ] ); + } + + public function noop() { + } + + public function testBenchName_method() { + $bench = $this->getMockBuilder( Benchmarker::class ) + ->setMethods( [ 'execute', 'output', 'addResult' ] ) + ->getMock(); + $benchProxy = TestingAccessWrapper::newFromObject( $bench ); + $benchProxy->defaultCount = 1; + + $bench->expects( $this->once() )->method( 'addResult' ) + ->with( $this->callback( function ( $res ) { + return isset( $res['name'] ) && $res['name'] === __CLASS__ . '::noop()'; + } ) ); + + $bench->bench( [ + [ 'function' => [ $this, 'noop' ] ] + ] ); + } + + public function testBenchName_string() { + $bench = $this->getMockBuilder( Benchmarker::class ) + ->setMethods( [ 'execute', 'output', 'addResult' ] ) + ->getMock(); + $benchProxy = TestingAccessWrapper::newFromObject( $bench ); + $benchProxy->defaultCount = 1; + + $bench->expects( $this->once() )->method( 'addResult' ) + ->with( $this->callback( function ( $res ) { + return 'strtolower(A)'; + } ) ); + + $bench->bench( [ [ + 'function' => 'strtolower', + 'args' => [ 'A' ], + ] ] ); + } + + /** + * @covers Benchmarker::verboseRun + */ + public function testVerboseRun() { + $bench = $this->getMockBuilder( Benchmarker::class ) + ->setMethods( [ 'execute', 'output', 'hasOption', 'startBench', 'addResult' ] ) + ->getMock(); + $benchProxy = TestingAccessWrapper::newFromObject( $bench ); + $benchProxy->defaultCount = 1; + + $bench->expects( $this->exactly( 2 ) )->method( 'hasOption' ) + ->will( $this->returnValueMap( [ + [ 'verbose', true ], + [ 'count', false ], + ] ) ); + + $bench->expects( $this->once() )->method( 'output' ) + ->with( $this->callback( function ( $out ) { + return preg_match( '/memory.+ peak/', $out ) === 1; + } ) ); + + $bench->bench( [ + 'test' => function () { + } + ] ); + } +} diff --git a/www/wiki/tests/phpunit/maintenance/DumpTestCase.php b/www/wiki/tests/phpunit/maintenance/DumpTestCase.php index 99bd4277..9b90bfe6 100644 --- a/www/wiki/tests/phpunit/maintenance/DumpTestCase.php +++ b/www/wiki/tests/phpunit/maintenance/DumpTestCase.php @@ -1,5 +1,15 @@ <?php +namespace MediaWiki\Tests\Maintenance; + +use ContentHandler; +use ExecutableFinder; +use MediaWikiLangTestCase; +use Page; +use User; +use XMLReader; +use MWException; + /** * Base TestCase for dumps */ @@ -35,7 +45,7 @@ abstract class DumpTestCase extends MediaWikiLangTestCase { */ protected function checkHasGzip() { if ( self::$hasGzip === null ) { - self::$hasGzip = ( Installer::locateExecutableInDefaultPaths( 'gzip' ) !== false ); + self::$hasGzip = ( ExecutableFinder::findInDefaultPaths( 'gzip' ) !== false ); } if ( !self::$hasGzip ) { diff --git a/www/wiki/tests/phpunit/maintenance/MaintenanceBaseTestCase.php b/www/wiki/tests/phpunit/maintenance/MaintenanceBaseTestCase.php new file mode 100644 index 00000000..bdcf7e5f --- /dev/null +++ b/www/wiki/tests/phpunit/maintenance/MaintenanceBaseTestCase.php @@ -0,0 +1,93 @@ +<?php + +namespace MediaWiki\Tests\Maintenance; + +use Maintenance; +use MediaWikiTestCase; +use Wikimedia\TestingAccessWrapper; + +abstract class MaintenanceBaseTestCase extends MediaWikiTestCase { + + /** + * The main Maintenance instance that is used for testing, wrapped and mockable. + * + * @var Maintenance + */ + protected $maintenance; + + protected function setUp() { + parent::setUp(); + + $this->maintenance = $this->createMaintenance(); + } + + /** + * Do a little stream cleanup to prevent output in case the child class + * hasn't tested the capture buffer. + */ + protected function tearDown() { + if ( $this->maintenance ) { + $this->maintenance->cleanupChanneled(); + } + + // This is smelly, but maintenance scripts usually produce output, so + // we anticipate and ignore with a regex that will catch everything. + // + // If you call $this->expectOutputRegex in your subclass, this guard + // won't be triggered, and your specific pattern will be respected. + if ( !$this->hasExpectationOnOutput() ) { + $this->expectOutputRegex( '/.*/' ); + } + + parent::tearDown(); + } + + /** + * @return string Class name + * + * Subclasses must implement this in order to use the $this->maintenance + * variable. Normally, it will be set like: + * return PopulateDatabaseMaintenance::class; + * + * If you need to change the way your maintenance class is constructed, + * override createMaintenance. + */ + abstract protected function getMaintenanceClass(); + + /** + * Called by setUp to initialize $this->maintenance. + * + * @return object The Maintenance instance to test. + */ + protected function createMaintenance() { + $className = $this->getMaintenanceClass(); + $obj = new $className(); + + // We use TestingAccessWrapper in order to access protected internals + // such as `output()`. + return TestingAccessWrapper::newFromObject( $obj ); + } + + /** + * Asserts the output before and after simulating shutdown + * + * This function simulates shutdown of self::maintenance. + * + * @param string $preShutdownOutput Expected output before simulating shutdown + * @param bool $expectNLAppending 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. + */ + protected function assertOutputPrePostShutdown( $preShutdownOutput, $expectNLAppending ) { + $this->assertEquals( $preShutdownOutput, $this->getActualOutput(), + "Output before shutdown simulation" ); + + $this->maintenance->cleanupChanneled(); + + $postShutdownOutput = $preShutdownOutput . ( $expectNLAppending ? "\n" : "" ); + $this->expectOutputString( $postShutdownOutput ); + } + +} diff --git a/www/wiki/tests/phpunit/maintenance/MaintenanceTest.php b/www/wiki/tests/phpunit/maintenance/MaintenanceTest.php index e21e7269..141561f0 100644 --- a/www/wiki/tests/phpunit/maintenance/MaintenanceTest.php +++ b/www/wiki/tests/phpunit/maintenance/MaintenanceTest.php @@ -1,187 +1,35 @@ <?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. -use MediaWiki\MediaWikiServices; - -/** - * 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" ); - } +namespace MediaWiki\Tests\Maintenance; - // 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; - } - - call_user_func_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( [ "parent", __FUNCTION__ ], func_get_args() ); - } - - public function addOption( $name, $description, $required = false, - $withArg = false, $shortName = false, $multiOccurance = false - ) { - return call_user_func_array( [ "parent", __FUNCTION__ ], func_get_args() ); - } - - public function getOption( $name, $default = null ) { - return call_user_func_array( [ "parent", __FUNCTION__ ], func_get_args() ); - } - - // --- Requirements for getting instance of abstract class - - public function execute() { - $this->testCase->fail( __METHOD__ . " called unexpectedly" ); - } -} +use Maintenance; +use MediaWiki\MediaWikiServices; +use Wikimedia\TestingAccessWrapper; /** * @covers Maintenance */ -class MaintenanceTest extends MediaWikiTestCase { +class MaintenanceTest extends MaintenanceBaseTestCase { /** - * The main Maintenance instance that is used for testing. - * - * @var MaintenanceFixup + * @see MaintenanceBaseTestCase::getMaintenanceClass */ - 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(); + protected function getMaintenanceClass() { + return Maintenance::class; } /** - * asserts the output before and after simulating shutdown + * @see MaintenanceBaseTestCase::createMaintenance * - * This function simulates shutdown of self::m. - * - * @param string $preShutdownOutput Expected output before simulating shutdown - * @param bool $expectNLAppending 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. + * Note to extension authors looking for a model to follow: This function + * is normally not needed in a maintenance test, it's only overridden here + * because Maintenance is abstract. */ - private function assertOutputPrePostShutdown( $preShutdownOutput, $expectNLAppending ) { - $this->assertEquals( $preShutdownOutput, $this->getActualOutput(), - "Output before shutdown simulation" ); - - $this->m->simulateShutdown(); - $this->m = null; + protected function createMaintenance() { + $className = $this->getMaintenanceClass(); + $obj = $this->getMockForAbstractClass( $className ); - $postShutdownOutput = $preShutdownOutput . ( $expectNLAppending ? "\n" : "" ); - $this->expectOutputString( $postShutdownOutput ); + return TestingAccessWrapper::newFromObject( $obj ); } // Although the following tests do not seem to be too consistent (compare for @@ -189,632 +37,445 @@ class MaintenanceTest extends MediaWikiTestCase { // 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 ); + /** + * @dataProvider provideOutputData + */ + function testOutput( $outputs, $expected, $extraNL ) { + foreach ( $outputs as $data ) { + if ( is_array( $data ) ) { + list( $msg, $channel ) = $data; + } else { + $msg = $data; + $channel = null; + } + $this->maintenance->output( $msg, $channel ); + } + $this->assertOutputPrePostShutdown( $expected, $extraNL ); + } + + public function provideOutputData() { + return [ + [ [ "" ], "", false ], + [ [ "foo" ], "foo", false ], + [ [ "foo", "bar" ], "foobar", false ], + [ [ "foo\n" ], "foo\n", false ], + [ [ "foo\n\n" ], "foo\n\n", false ], + [ [ "foo\nbar" ], "foo\nbar", false ], + [ [ "foo\nbar\n" ], "foo\nbar\n", false ], + [ [ "foo\n", "bar\n" ], "foo\nbar\n", false ], + [ [ "", "foo", "", "\n", "ba", "", "r\n" ], "foo\nbar\n", false ], + [ [ "", "foo", "", "\nb", "a", "", "r\n" ], "foo\nbar\n", false ], + [ [ [ "foo", "bazChannel" ] ], "foo", true ], + [ [ [ "foo\n", "bazChannel" ] ], "foo", true ], + + // 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) + [ [ [ "foo\n\n", "bazChannel" ] ], "foo\n", true ], + [ [ [ "foo\nbar", "bazChannel" ] ], "foo\nbar", true ], + [ [ [ "foo\nbar\n", "bazChannel" ] ], "foo\nbar", true ], + [ + [ + [ "foo\n", "bazChannel" ], + [ "bar\n", "bazChannel" ], + ], + "foobar", + true + ], + [ + [ + [ "", "bazChannel" ], + [ "foo", "bazChannel" ], + [ "", "bazChannel" ], + [ "\n", "bazChannel" ], + [ "ba", "bazChannel" ], + [ "", "bazChannel" ], + [ "r\n", "bazChannel" ], + ], + "foobar", + true + ], + [ + [ + [ "", "bazChannel" ], + [ "foo", "bazChannel" ], + [ "", "bazChannel" ], + [ "\nb", "bazChannel" ], + [ "a", "bazChannel" ], + [ "", "bazChannel" ], + [ "r\n", "bazChannel" ], + ], + "foo\nbar", + true + ], + [ + [ + [ "foo", "bazChannel" ], + [ "bar", "bazChannel" ], + [ "qux", "quuxChannel" ], + [ "corge", "bazChannel" ], + ], + "foobar\nqux\ncorge", + true + ], + [ + [ + [ "foo", "bazChannel" ], + [ "bar\n", "bazChannel" ], + [ "qux\n", "quuxChannel" ], + [ "corge", "bazChannel" ], + ], + "foobar\nqux\ncorge", + true + ], + [ + [ + [ "foo", null ], + [ "bar", "bazChannel" ], + [ "qux", null ], + [ "quux", "bazChannel" ], + ], + "foobar\nquxquux", + true + ], + [ + [ + [ "foo", "bazChannel" ], + [ "bar", null ], + [ "qux", "bazChannel" ], + [ "quux", null ], + ], + "foo\nbarqux\nquux", + false + ], + [ + [ + [ "foo", 1 ], + [ "bar", 1.0 ], + ], + "foo\nbar", + true + ], + [ [ "foo", "", "bar" ], "foobar", false ], + [ [ "foo", false, "bar" ], "foobar", false ], + [ + [ + [ "qux", "quuxChannel" ], + "foo", + false, + "bar" + ], + "qux\nfoobar", + false + ], + [ + [ + [ "foo", "bazChannel" ], + [ "", "bazChannel" ], + [ "bar", "bazChannel" ], + ], + "foobar", + true + ], + [ + [ + [ "foo", "bazChannel" ], + [ false, "bazChannel" ], + [ "bar", "bazChannel" ], + ], + "foobar", + true + ], + ]; } - function testOutputChanneledWChannelIntermittentFalse() { - $this->m->outputChanneled( "foo", "bazChannel" ); - $this->m->outputChanneled( false, "bazChannel" ); - $this->m->outputChanneled( "bar", "bazChannel" ); - $this->assertOutputPrePostShutdown( "foo\nbar", true ); + /** + * @dataProvider provideOutputChanneledData + */ + function testOutputChanneled( $outputs, $expected, $extraNL ) { + foreach ( $outputs as $data ) { + if ( is_array( $data ) ) { + list( $msg, $channel ) = $data; + } else { + $msg = $data; + $channel = null; + } + $this->maintenance->outputChanneled( $msg, $channel ); + } + $this->assertOutputPrePostShutdown( $expected, $extraNL ); + } + + public function provideOutputChanneledData() { + return [ + [ [ "" ], "\n", false ], + [ [ "foo" ], "foo\n", false ], + [ [ "foo", "bar" ], "foo\nbar\n", false ], + [ [ "foo\nbar" ], "foo\nbar\n", false ], + [ [ "", "foo", "", "\nb", "a", "", "r" ], "\nfoo\n\n\nb\na\n\nr\n", false ], + [ [ [ "foo", "bazChannel" ] ], "foo", true ], + [ + [ + [ "foo\nbar", "bazChannel" ] + ], + "foo\nbar", + true + ], + [ + [ + [ "foo", "bazChannel" ], + [ "bar", "bazChannel" ], + ], + "foobar", + true + ], + [ + [ + [ "", "bazChannel" ], + [ "foo", "bazChannel" ], + [ "", "bazChannel" ], + [ "\nb", "bazChannel" ], + [ "a", "bazChannel" ], + [ "", "bazChannel" ], + [ "r", "bazChannel" ], + ], + "foo\nbar", + true + ], + [ + [ + [ "foo", "bazChannel" ], + [ "bar", "bazChannel" ], + [ "qux", "quuxChannel" ], + [ "corge", "bazChannel" ], + ], + "foobar\nqux\ncorge", + true + ], + [ + [ + [ "foo", "bazChannel" ], + [ "bar", "bazChannel" ], + [ "qux", "quuxChannel" ], + [ "corge", "bazChannel" ], + ], + "foobar\nqux\ncorge", + true + ], + [ + [ + [ "foo", "bazChannel" ], + [ "bar", null ], + [ "qux", null ], + [ "corge", "bazChannel" ], + ], + "foo\nbar\nqux\ncorge", + true + ], + [ + [ + [ "foo", null ], + [ "bar", "bazChannel" ], + [ "qux", null ], + [ "quux", "bazChannel" ], + ], + "foo\nbar\nqux\nquux", + true + ], + [ + [ + [ "foo", "bazChannel" ], + [ "bar", null ], + [ "qux", "bazChannel" ], + [ "quux", null ], + ], + "foo\nbar\nqux\nquux\n", + false + ], + [ + [ + [ "foo", 1 ], + [ "bar", 1.0 ], + ], + "foo\nbar", + true + ], + [ [ "foo", "", "bar" ], "foo\n\nbar\n", false ], + [ [ "foo", false, "bar" ], "foo\nbar\n", false ], + ]; } function testCleanupChanneledClean() { - $this->m->cleanupChanneled(); + $this->maintenance->cleanupChanneled(); $this->assertOutputPrePostShutdown( "", false ); } function testCleanupChanneledAfterOutput() { - $this->m->output( "foo" ); - $this->m->cleanupChanneled(); + $this->maintenance->output( "foo" ); + $this->maintenance->cleanupChanneled(); $this->assertOutputPrePostShutdown( "foo", false ); } function testCleanupChanneledAfterOutputWNullChannel() { - $this->m->output( "foo", null ); - $this->m->cleanupChanneled(); + $this->maintenance->output( "foo", null ); + $this->maintenance->cleanupChanneled(); $this->assertOutputPrePostShutdown( "foo", false ); } function testCleanupChanneledAfterOutputWChannel() { - $this->m->output( "foo", "bazChannel" ); - $this->m->cleanupChanneled(); + $this->maintenance->output( "foo", "bazChannel" ); + $this->maintenance->cleanupChanneled(); $this->assertOutputPrePostShutdown( "foo\n", false ); } function testCleanupChanneledAfterNLOutput() { - $this->m->output( "foo\n" ); - $this->m->cleanupChanneled(); + $this->maintenance->output( "foo\n" ); + $this->maintenance->cleanupChanneled(); $this->assertOutputPrePostShutdown( "foo\n", false ); } function testCleanupChanneledAfterNLOutputWNullChannel() { - $this->m->output( "foo\n", null ); - $this->m->cleanupChanneled(); + $this->maintenance->output( "foo\n", null ); + $this->maintenance->cleanupChanneled(); $this->assertOutputPrePostShutdown( "foo\n", false ); } function testCleanupChanneledAfterNLOutputWChannel() { - $this->m->output( "foo\n", "bazChannel" ); - $this->m->cleanupChanneled(); + $this->maintenance->output( "foo\n", "bazChannel" ); + $this->maintenance->cleanupChanneled(); $this->assertOutputPrePostShutdown( "foo\n", false ); } function testCleanupChanneledAfterOutputChanneledWOChannel() { - $this->m->outputChanneled( "foo" ); - $this->m->cleanupChanneled(); + $this->maintenance->outputChanneled( "foo" ); + $this->maintenance->cleanupChanneled(); $this->assertOutputPrePostShutdown( "foo\n", false ); } function testCleanupChanneledAfterOutputChanneledWNullChannel() { - $this->m->outputChanneled( "foo", null ); - $this->m->cleanupChanneled(); + $this->maintenance->outputChanneled( "foo", null ); + $this->maintenance->cleanupChanneled(); $this->assertOutputPrePostShutdown( "foo\n", false ); } function testCleanupChanneledAfterOutputChanneledWChannel() { - $this->m->outputChanneled( "foo", "bazChannel" ); - $this->m->cleanupChanneled(); + $this->maintenance->outputChanneled( "foo", "bazChannel" ); + $this->maintenance->cleanupChanneled(); $this->assertOutputPrePostShutdown( "foo\n", false ); } function testMultipleMaintenanceObjectsInteractionOutput() { - $m2 = new MaintenanceFixup( $this ); + $m2 = $this->createMaintenance(); - $this->m->output( "foo" ); + $this->maintenance->output( "foo" ); $m2->output( "bar" ); $this->assertEquals( "foobar", $this->getActualOutput(), "Output before shutdown simulation (m2)" ); - $m2->simulateShutdown(); + $m2->cleanupChanneled(); $this->assertOutputPrePostShutdown( "foobar", false ); } function testMultipleMaintenanceObjectsInteractionOutputWNullChannel() { - $m2 = new MaintenanceFixup( $this ); + $m2 = $this->createMaintenance(); - $this->m->output( "foo", null ); + $this->maintenance->output( "foo", null ); $m2->output( "bar", null ); $this->assertEquals( "foobar", $this->getActualOutput(), "Output before shutdown simulation (m2)" ); - $m2->simulateShutdown(); + $m2->cleanupChanneled(); $this->assertOutputPrePostShutdown( "foobar", false ); } function testMultipleMaintenanceObjectsInteractionOutputWChannel() { - $m2 = new MaintenanceFixup( $this ); + $m2 = $this->createMaintenance(); - $this->m->output( "foo", "bazChannel" ); + $this->maintenance->output( "foo", "bazChannel" ); $m2->output( "bar", "bazChannel" ); $this->assertEquals( "foobar", $this->getActualOutput(), "Output before shutdown simulation (m2)" ); - $m2->simulateShutdown(); + $m2->cleanupChanneled(); $this->assertOutputPrePostShutdown( "foobar\n", true ); } function testMultipleMaintenanceObjectsInteractionOutputWNullChannelNL() { - $m2 = new MaintenanceFixup( $this ); + $m2 = $this->createMaintenance(); - $this->m->output( "foo\n", null ); + $this->maintenance->output( "foo\n", null ); $m2->output( "bar\n", null ); $this->assertEquals( "foo\nbar\n", $this->getActualOutput(), "Output before shutdown simulation (m2)" ); - $m2->simulateShutdown(); + $m2->cleanupChanneled(); $this->assertOutputPrePostShutdown( "foo\nbar\n", false ); } function testMultipleMaintenanceObjectsInteractionOutputWChannelNL() { - $m2 = new MaintenanceFixup( $this ); + $m2 = $this->createMaintenance(); - $this->m->output( "foo\n", "bazChannel" ); + $this->maintenance->output( "foo\n", "bazChannel" ); $m2->output( "bar\n", "bazChannel" ); $this->assertEquals( "foobar", $this->getActualOutput(), "Output before shutdown simulation (m2)" ); - $m2->simulateShutdown(); + $m2->cleanupChanneled(); $this->assertOutputPrePostShutdown( "foobar\n", true ); } function testMultipleMaintenanceObjectsInteractionOutputChanneled() { - $m2 = new MaintenanceFixup( $this ); + $m2 = $this->createMaintenance(); - $this->m->outputChanneled( "foo" ); + $this->maintenance->outputChanneled( "foo" ); $m2->outputChanneled( "bar" ); $this->assertEquals( "foo\nbar\n", $this->getActualOutput(), "Output before shutdown simulation (m2)" ); - $m2->simulateShutdown(); + $m2->cleanupChanneled(); $this->assertOutputPrePostShutdown( "foo\nbar\n", false ); } function testMultipleMaintenanceObjectsInteractionOutputChanneledWNullChannel() { - $m2 = new MaintenanceFixup( $this ); + $m2 = $this->createMaintenance(); - $this->m->outputChanneled( "foo", null ); + $this->maintenance->outputChanneled( "foo", null ); $m2->outputChanneled( "bar", null ); $this->assertEquals( "foo\nbar\n", $this->getActualOutput(), "Output before shutdown simulation (m2)" ); - $m2->simulateShutdown(); + $m2->cleanupChanneled(); $this->assertOutputPrePostShutdown( "foo\nbar\n", false ); } function testMultipleMaintenanceObjectsInteractionOutputChanneledWChannel() { - $m2 = new MaintenanceFixup( $this ); + $m2 = $this->createMaintenance(); - $this->m->outputChanneled( "foo", "bazChannel" ); + $this->maintenance->outputChanneled( "foo", "bazChannel" ); $m2->outputChanneled( "bar", "bazChannel" ); $this->assertEquals( "foobar", $this->getActualOutput(), "Output before shutdown simulation (m2)" ); - $m2->simulateShutdown(); + $m2->cleanupChanneled(); $this->assertOutputPrePostShutdown( "foobar\n", true ); } function testMultipleMaintenanceObjectsInteractionCleanupChanneledWChannel() { - $m2 = new MaintenanceFixup( $this ); + $m2 = $this->createMaintenance(); - $this->m->outputChanneled( "foo", "bazChannel" ); + $this->maintenance->outputChanneled( "foo", "bazChannel" ); $m2->outputChanneled( "bar", "bazChannel" ); $this->assertEquals( "foobar", $this->getActualOutput(), "Output before first cleanup" ); - $this->m->cleanupChanneled(); + $this->maintenance->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(); + $m2->cleanupChanneled(); $this->assertOutputPrePostShutdown( "foobar\n\n", false ); } @@ -822,10 +483,10 @@ class MaintenanceTest extends MediaWikiTestCase { * @covers Maintenance::getConfig */ public function testGetConfig() { - $this->assertInstanceOf( 'Config', $this->m->getConfig() ); + $this->assertInstanceOf( 'Config', $this->maintenance->getConfig() ); $this->assertSame( MediaWikiServices::getInstance()->getMainConfig(), - $this->m->getConfig() + $this->maintenance->getConfig() ); } @@ -834,12 +495,13 @@ class MaintenanceTest extends MediaWikiTestCase { */ public function testSetConfig() { $conf = $this->createMock( 'Config' ); - $this->m->setConfig( $conf ); - $this->assertSame( $conf, $this->m->getConfig() ); + $this->maintenance->setConfig( $conf ); + $this->assertSame( $conf, $this->maintenance->getConfig() ); } function testParseArgs() { - $m2 = new MaintenanceFixup( $this ); + $m2 = $this->createMaintenance(); + // Create an option with an argument allowed to be specified multiple times $m2->addOption( 'multi', 'This option does stuff', false, true, false, true ); $m2->loadWithArgv( [ '--multi', 'this1', '--multi', 'this2' ] ); @@ -848,9 +510,9 @@ class MaintenanceTest extends MediaWikiTestCase { $this->assertEquals( [ [ 'multi', 'this1' ], [ 'multi', 'this2' ] ], $m2->orderedOptions ); - $m2->simulateShutdown(); + $m2->cleanupChanneled(); - $m2 = new MaintenanceFixup( $this ); + $m2 = $this->createMaintenance(); $m2->addOption( 'multi', 'This option does stuff', false, false, false, true ); $m2->loadWithArgv( [ '--multi', '--multi' ] ); @@ -858,9 +520,10 @@ class MaintenanceTest extends MediaWikiTestCase { $this->assertEquals( [ 1, 1 ], $m2->getOption( 'multi' ) ); $this->assertEquals( [ [ 'multi', 1 ], [ 'multi', 1 ] ], $m2->orderedOptions ); - $m2->simulateShutdown(); + $m2->cleanupChanneled(); + + $m2 = $this->createMaintenance(); - $m2 = new MaintenanceFixup( $this ); // Create an option with an argument allowed to be specified multiple times $m2->addOption( 'multi', 'This option doesn\'t actually support multiple occurrences' ); $m2->loadWithArgv( [ '--multi=yo' ] ); @@ -868,6 +531,6 @@ class MaintenanceTest extends MediaWikiTestCase { $this->assertEquals( 'yo', $m2->getOption( 'multi' ) ); $this->assertEquals( [ [ 'multi', 'yo' ] ], $m2->orderedOptions ); - $m2->simulateShutdown(); + $m2->cleanupChanneled(); } } diff --git a/www/wiki/tests/phpunit/maintenance/backupPrefetchTest.php b/www/wiki/tests/phpunit/maintenance/backupPrefetchTest.php index 2c5931d2..8824c7af 100644 --- a/www/wiki/tests/phpunit/maintenance/backupPrefetchTest.php +++ b/www/wiki/tests/phpunit/maintenance/backupPrefetchTest.php @@ -1,6 +1,9 @@ <?php -require_once __DIR__ . "/../../../maintenance/backupPrefetch.inc"; +namespace MediaWiki\Tests\Maintenance; + +use BaseDump; +use MediaWikiTestCase; /** * Tests for BaseDump @@ -151,7 +154,7 @@ class BaseDumpTest extends MediaWikiTestCase { $fname = $this->getNewTempFile(); // The header of every prefetch file - // @codingStandardsIgnoreStart Ignore Generic.Files.LineLength.TooLong + // phpcs:ignore Generic.Files.LineLength $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> @@ -180,7 +183,6 @@ class BaseDumpTest extends MediaWikiTestCase { </namespaces> </siteinfo> '; - // @codingStandardsIgnoreEnd // An array holding the pages that are available for prefetch $available_pages = []; diff --git a/www/wiki/tests/phpunit/maintenance/backupTextPassTest.php b/www/wiki/tests/phpunit/maintenance/backupTextPassTest.php index 0a1f3b4e..ad9bf3ea 100644 --- a/www/wiki/tests/phpunit/maintenance/backupTextPassTest.php +++ b/www/wiki/tests/phpunit/maintenance/backupTextPassTest.php @@ -1,5 +1,14 @@ <?php +namespace MediaWiki\Tests\Maintenance; + +use MediaWikiLangTestCase; +use TextContentHandler; +use TextPassDumper; +use Title; +use WikiExporter; +use WikiPage; + require_once __DIR__ . "/../../../maintenance/dumpTextPass.php"; /** @@ -34,7 +43,7 @@ class TextPassDumperDatabaseTest extends DumpTestCase { $this->tablesUsed[] = 'text'; $this->mergeMwGlobalArrayValue( 'wgContentHandlers', [ - "BackupTextPassTestModel" => "BackupTextPassTestModelHandler" + "BackupTextPassTestModel" => BackupTextPassTestModelHandler::class, ] ); $ns = $this->getDefaultWikitextNS(); @@ -170,7 +179,7 @@ class TextPassDumperDatabaseTest extends DumpTestCase { ]; // The mock itself - $prefetchMock = $this->getMockBuilder( 'BaseDump' ) + $prefetchMock = $this->getMockBuilder( BaseDump::class ) ->setMethods( [ 'prefetch' ] ) ->disableOriginalConstructor() ->getMock(); diff --git a/www/wiki/tests/phpunit/maintenance/backup_LogTest.php b/www/wiki/tests/phpunit/maintenance/backup_LogTest.php index 91e1b1e7..c215b997 100644 --- a/www/wiki/tests/phpunit/maintenance/backup_LogTest.php +++ b/www/wiki/tests/phpunit/maintenance/backup_LogTest.php @@ -1,4 +1,13 @@ <?php + +namespace MediaWiki\Tests\Maintenance; + +use DumpBackup; +use ManualLogEntry; +use Title; +use User; +use WikiExporter; + /** * Tests for log dumps of BackupDumper * diff --git a/www/wiki/tests/phpunit/maintenance/backup_PageTest.php b/www/wiki/tests/phpunit/maintenance/backup_PageTest.php index 554d5f66..51a1ed69 100644 --- a/www/wiki/tests/phpunit/maintenance/backup_PageTest.php +++ b/www/wiki/tests/phpunit/maintenance/backup_PageTest.php @@ -1,4 +1,13 @@ <?php + +namespace MediaWiki\Tests\Maintenance; + +use DumpBackup; +use Language; +use Title; +use WikiExporter; +use WikiPage; + /** * Tests for page dumps of BackupDumper * @@ -6,13 +15,12 @@ * @group Dump * @covers BackupDumper */ - class BackupDumperPageTest extends DumpTestCase { // We'll add several pages, revision and texts. The following variables hold the // corresponding ids. - private $pageId1, $pageId2, $pageId3, $pageId4, $pageId5; - private $pageTitle1, $pageTitle2, $pageTitle3, $pageTitle4, $pageTitle5; + private $pageId1, $pageId2, $pageId3, $pageId4; + private $pageTitle1, $pageTitle2, $pageTitle3, $pageTitle4; private $revId1_1, $textId1_1; private $revId2_1, $textId2_1, $revId2_2, $textId2_2; private $revId2_3, $textId2_3, $revId2_4, $textId2_4; diff --git a/www/wiki/tests/phpunit/maintenance/categoriesRdfTest.php b/www/wiki/tests/phpunit/maintenance/categoriesRdfTest.php index ec2746e8..5068e701 100644 --- a/www/wiki/tests/phpunit/maintenance/categoriesRdfTest.php +++ b/www/wiki/tests/phpunit/maintenance/categoriesRdfTest.php @@ -1,16 +1,46 @@ <?php +namespace MediaWiki\Tests\Maintenance; + +use DumpCategoriesAsRdf; +use MediaWikiLangTestCase; + +/** + * @covers CategoriesRdf + * @covers DumpCategoriesAsRdf + */ class CategoriesRdfTest extends MediaWikiLangTestCase { public function getCategoryIterator() { return [ // batch 1 [ - (object)[ 'page_title' => 'Category One', 'page_id' => 1 ], - (object)[ 'page_title' => '2 Category Two', 'page_id' => 2 ], + (object)[ + 'page_title' => 'Category One', + 'page_id' => 1, + 'pp_propname' => null, + 'cat_pages' => '20', + 'cat_subcats' => '10', + 'cat_files' => '3' + ], + (object)[ + 'page_title' => '2 Category Two', + 'page_id' => 2, + 'pp_propname' => 'hiddencat', + 'cat_pages' => 20, + 'cat_subcats' => 0, + 'cat_files' => 3 + ], ], // batch 2 [ - (object)[ 'page_title' => 'Третья категория', 'page_id' => 3 ], + (object)[ + 'page_title' => 'Третья категория', + 'page_id' => 3, + 'pp_propname' => null, + 'cat_pages' => '0', + 'cat_subcats' => '0', + 'cat_files' => '0' + ], ] ]; } @@ -60,8 +90,8 @@ class CategoriesRdfTest extends MediaWikiLangTestCase { $dumpScript->execute(); $actualOut = file_get_contents( $outFileName ); $actualOut = preg_replace( - '|<http://acme.test/categoriesDump> <http://schema.org/dateModified> "[^"]+?"|', - '<http://acme.test/categoriesDump> <http://schema.org/dateModified> "{DATE}"', + '|<http://acme.test/wiki/Special:CategoryDump> <http://schema.org/dateModified> "[^"]+?"|', + '<http://acme.test/wiki/Special:CategoryDump> <http://schema.org/dateModified> "{DATE}"', $actualOut ); diff --git a/www/wiki/tests/phpunit/maintenance/deleteAutoPatrolLogsTest.php b/www/wiki/tests/phpunit/maintenance/deleteAutoPatrolLogsTest.php new file mode 100644 index 00000000..c1418174 --- /dev/null +++ b/www/wiki/tests/phpunit/maintenance/deleteAutoPatrolLogsTest.php @@ -0,0 +1,252 @@ +<?php + +namespace MediaWiki\Tests\Maintenance; + +use DeleteAutoPatrolLogs; + +/** + * @group Database + * @covers DeleteAutoPatrolLogs + */ +class DeleteAutoPatrolLogsTest extends MaintenanceBaseTestCase { + + public function getMaintenanceClass() { + return DeleteAutoPatrolLogs::class; + } + + public function setUp() { + parent::setUp(); + $this->tablesUsed = [ 'logging' ]; + + $this->cleanLoggingTable(); + $this->insertLoggingData(); + } + + private function cleanLoggingTable() { + wfGetDB( DB_MASTER )->delete( 'logging', '*' ); + } + + private function insertLoggingData() { + $logs = []; + + // Manual patrolling + $logs[] = [ + 'log_type' => 'patrol', + 'log_action' => 'patrol', + 'log_user' => 7251, + 'log_params' => '', + 'log_timestamp' => 20041223210426 + ]; + + // Autopatrol #1 + $logs[] = [ + 'log_type' => 'patrol', + 'log_action' => 'autopatrol', + 'log_user' => 7252, + 'log_params' => '', + 'log_timestamp' => 20051223210426 + ]; + + // Block + $logs[] = [ + 'log_type' => 'block', + 'log_action' => 'block', + 'log_user' => 7253, + 'log_params' => '', + 'log_timestamp' => 20061223210426 + ]; + + // Very old/ invalid patrol + $logs[] = [ + 'log_type' => 'patrol', + 'log_action' => 'patrol', + 'log_user' => 7253, + 'log_params' => 'nanana', + 'log_timestamp' => 20061223210426 + ]; + + // Autopatrol #2 + $logs[] = [ + 'log_type' => 'patrol', + 'log_action' => 'autopatrol', + 'log_user' => 7254, + 'log_params' => '', + 'log_timestamp' => 20071223210426 + ]; + + // Autopatrol #3 old way + $logs[] = [ + 'log_type' => 'patrol', + 'log_action' => 'patrol', + 'log_user' => 7255, + 'log_params' => serialize( [ '6::auto' => true ] ), + 'log_timestamp' => 20081223210426 + ]; + + // Manual patrol #2 old way + $logs[] = [ + 'log_type' => 'patrol', + 'log_action' => 'patrol', + 'log_user' => 7256, + 'log_params' => serialize( [ '6::auto' => false ] ), + 'log_timestamp' => 20091223210426 + ]; + + wfGetDB( DB_MASTER )->insert( 'logging', $logs ); + } + + public function runProvider() { + $allRows = [ + (object)[ + 'log_type' => 'patrol', + 'log_action' => 'patrol', + 'log_user' => '7251', + ], + (object)[ + 'log_type' => 'patrol', + 'log_action' => 'autopatrol', + 'log_user' => '7252', + ], + (object)[ + 'log_type' => 'block', + 'log_action' => 'block', + 'log_user' => '7253', + ], + (object)[ + 'log_type' => 'patrol', + 'log_action' => 'patrol', + 'log_user' => '7253', + ], + (object)[ + 'log_type' => 'patrol', + 'log_action' => 'autopatrol', + 'log_user' => '7254', + ], + (object)[ + 'log_type' => 'patrol', + 'log_action' => 'patrol', + 'log_user' => '7255', + ], + (object)[ + 'log_type' => 'patrol', + 'log_action' => 'patrol', + 'log_user' => '7256', + ], + ]; + + $cases = [ + 'dry run' => [ + $allRows, + [ '--sleep', '0', '--dry-run', '-q' ] + ], + 'basic run' => [ + [ + $allRows[0], + $allRows[2], + $allRows[3], + $allRows[5], + $allRows[6], + ], + [ '--sleep', '0', '-q' ] + ], + 'run with before' => [ + [ + $allRows[0], + $allRows[2], + $allRows[3], + $allRows[4], + $allRows[5], + $allRows[6], + ], + [ '--sleep', '0', '--before', '20060123210426', '-q' ] + ], + 'run with check-old' => [ + [ + $allRows[0], + $allRows[1], + $allRows[2], + $allRows[3], + $allRows[4], + $allRows[6], + ], + [ '--sleep', '0', '--check-old', '-q' ] + ], + ]; + + foreach ( $cases as $key => $case ) { + yield $key . '-batch-size-1' => [ + $case[0], + array_merge( $case[1], [ '--batch-size', '1' ] ) + ]; + yield $key . '-batch-size-5' => [ + $case[0], + array_merge( $case[1], [ '--batch-size', '5' ] ) + ]; + yield $key . '-batch-size-1000' => [ + $case[0], + array_merge( $case[1], [ '--batch-size', '1000' ] ) + ]; + } + } + + /** + * @dataProvider runProvider + */ + public function testRun( $expected, $args ) { + $this->maintenance->loadWithArgv( $args ); + + $this->maintenance->execute(); + + $remainingLogs = wfGetDB( DB_REPLICA )->select( + [ 'logging' ], + [ 'log_type', 'log_action', 'log_user' ], + [], + __METHOD__, + [ 'ORDER BY' => 'log_id' ] + ); + + $this->assertEquals( $expected, iterator_to_array( $remainingLogs, false ) ); + } + + public function testFromId() { + $fromId = wfGetDB( DB_REPLICA )->selectField( + 'logging', + 'log_id', + [ 'log_params' => 'nanana' ] + ); + + $this->maintenance->loadWithArgv( [ '--sleep', '0', '--from-id', strval( $fromId ), '-q' ] ); + + $this->maintenance->execute(); + + $remainingLogs = wfGetDB( DB_REPLICA )->select( + [ 'logging' ], + [ 'log_type', 'log_action', 'log_user' ], + [], + __METHOD__, + [ 'ORDER BY' => 'log_id' ] + ); + + $deleted = [ + 'log_type' => 'patrol', + 'log_action' => 'autopatrol', + 'log_user' => '7254', + ]; + $notDeleted = [ + 'log_type' => 'patrol', + 'log_action' => 'autopatrol', + 'log_user' => '7252', + ]; + + $remainingLogs = array_map( + function ( $val ) { + return (array)$val; + }, + iterator_to_array( $remainingLogs, false ) + ); + + $this->assertNotContains( $deleted, $remainingLogs ); + $this->assertContains( $notDeleted, $remainingLogs ); + } + +} diff --git a/www/wiki/tests/phpunit/maintenance/fetchTextTest.php b/www/wiki/tests/phpunit/maintenance/fetchTextTest.php index 5a28bfb5..97e0c88f 100644 --- a/www/wiki/tests/phpunit/maintenance/fetchTextTest.php +++ b/www/wiki/tests/phpunit/maintenance/fetchTextTest.php @@ -1,5 +1,15 @@ <?php +namespace MediaWiki\Tests\Maintenance; + +use ContentHandler; +use FetchText; +use MediaWikiTestCase; +use MWException; +use Title; +use PHPUnit_Framework_ExpectationFailedException; +use WikiPage; + require_once __DIR__ . "/../../../maintenance/fetchText.php"; /** @@ -205,7 +215,7 @@ class FetchTextTest extends MediaWikiTestCase { function testExistingSeveral() { $this->assertFilter( - join( "\n", [ + implode( "\n", [ self::$textId1, self::$textId5, self::$textId3, diff --git a/www/wiki/tests/phpunit/mocks/MockChangesListFilterGroup.php b/www/wiki/tests/phpunit/mocks/MockChangesListFilterGroup.php index f2891ce9..e50b9b4e 100644 --- a/www/wiki/tests/phpunit/mocks/MockChangesListFilterGroup.php +++ b/www/wiki/tests/phpunit/mocks/MockChangesListFilterGroup.php @@ -1,5 +1,7 @@ <?php +use Wikimedia\Rdbms\IDatabase; + class MockChangesListFilterGroup extends ChangesListFilterGroup { public function createFilter( array $filterDefinition ) { return new MockChangesListFilter( $filterDefinition ); @@ -9,11 +11,11 @@ class MockChangesListFilterGroup extends ChangesListFilterGroup { $this->filters[$filter->getName()] = $filter; } - public function isPerGroupRequestParameter() { - throw new MWException( - 'Not implemented: If the test relies on this, put it one of the ' . - 'subclasses\' tests (e.g. ChangesListBooleanFilterGroupTest) ' . - 'instead of testing the abstract class' - ); + public function modifyQuery( IDatabase $dbr, ChangesListSpecialPage $specialPage, + &$tables, &$fields, &$conds, &$query_options, &$join_conds, FormOptions $opts, + $isStructuredFiltersEnabled ) { + } + + public function addOptions( FormOptions $opts, $allowDefaults, $isStructuredFiltersEnabled ) { } } diff --git a/www/wiki/tests/phpunit/mocks/MockMessageLocalizer.php b/www/wiki/tests/phpunit/mocks/MockMessageLocalizer.php new file mode 100644 index 00000000..143a419f --- /dev/null +++ b/www/wiki/tests/phpunit/mocks/MockMessageLocalizer.php @@ -0,0 +1,49 @@ +<?php + +/** + * A simple {@link MessageLocalizer} implementation for use in tests. + * By default, it sets the message language to 'qqx', + * to make the tests independent of the wiki configuration. + * + * @author Lucas Werkmeister + * @license GPL-2.0-or-later + */ +class MockMessageLocalizer implements MessageLocalizer { + + /** + * @var string|null + */ + private $languageCode; + + /** + * @param string|null $languageCode The language code to use for messages by default. + * You can specify null to use the user language, + * but this is not recommended as it may make your tests depend on the wiki configuration. + */ + public function __construct( $languageCode = 'qqx' ) { + $this->languageCode = $languageCode; + } + + /** + * Get a Message object. + * Parameters are the same as {@link wfMessage()}. + * + * @param string|string[]|MessageSpecifier $key Message key, or array of keys, + * or a MessageSpecifier. + * @param mixed $args,... + * @return Message + */ + public function msg( $key ) { + $args = func_get_args(); + + /** @var Message $message */ + $message = call_user_func_array( 'wfMessage', $args ); + + if ( $this->languageCode !== null ) { + $message->inLanguage( $this->languageCode ); + } + + return $message; + } + +} diff --git a/www/wiki/tests/phpunit/mocks/content/DummyContentForTesting.php b/www/wiki/tests/phpunit/mocks/content/DummyContentForTesting.php index cdb3f78a..e8259d3a 100644 --- a/www/wiki/tests/phpunit/mocks/content/DummyContentForTesting.php +++ b/www/wiki/tests/phpunit/mocks/content/DummyContentForTesting.php @@ -2,8 +2,10 @@ class DummyContentForTesting extends AbstractContent { + const MODEL_ID = "testing"; + public function __construct( $data ) { - parent::__construct( "testing" ); + parent::__construct( self::MODEL_ID ); $this->data = $data; } @@ -110,7 +112,7 @@ class DummyContentForTesting extends AbstractContent { * * @param Title $title Context title for parsing * @param int|null $revId Revision ID (for {{REVISIONID}}) - * @param ParserOptions $options Parser options + * @param ParserOptions $options * @param bool $generateHtml Whether or not to generate HTML * @param ParserOutput &$output The output object to fill (reference). */ diff --git a/www/wiki/tests/phpunit/mocks/content/DummyContentHandlerForTesting.php b/www/wiki/tests/phpunit/mocks/content/DummyContentHandlerForTesting.php index 6b9b7823..b71577c7 100644 --- a/www/wiki/tests/phpunit/mocks/content/DummyContentHandlerForTesting.php +++ b/www/wiki/tests/phpunit/mocks/content/DummyContentHandlerForTesting.php @@ -2,8 +2,8 @@ class DummyContentHandlerForTesting extends ContentHandler { - public function __construct( $dataModel ) { - parent::__construct( $dataModel, [ "testing" ] ); + public function __construct( $dataModel, $formats = [ DummyContentForTesting::MODEL_ID ] ) { + parent::__construct( $dataModel, $formats ); } /** diff --git a/www/wiki/tests/phpunit/mocks/content/DummyNonTextContent.php b/www/wiki/tests/phpunit/mocks/content/DummyNonTextContent.php index afc1a4ae..91bb1866 100644 --- a/www/wiki/tests/phpunit/mocks/content/DummyNonTextContent.php +++ b/www/wiki/tests/phpunit/mocks/content/DummyNonTextContent.php @@ -110,7 +110,7 @@ class DummyNonTextContent extends AbstractContent { * * @param Title $title Context title for parsing * @param int|null $revId Revision ID (for {{REVISIONID}}) - * @param ParserOptions $options Parser options + * @param ParserOptions $options * @param bool $generateHtml Whether or not to generate HTML * @param ParserOutput &$output The output object to fill (reference). */ diff --git a/www/wiki/tests/phpunit/mocks/content/DummySerializeErrorContentHandler.php b/www/wiki/tests/phpunit/mocks/content/DummySerializeErrorContentHandler.php new file mode 100644 index 00000000..720547a5 --- /dev/null +++ b/www/wiki/tests/phpunit/mocks/content/DummySerializeErrorContentHandler.php @@ -0,0 +1,51 @@ +<?php +/** + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + */ + +/** + * A dummy content handler that will throw on an attempt to serialize content. + */ +class DummySerializeErrorContentHandler extends DummyContentHandlerForTesting { + + public function __construct( $dataModel ) { + parent::__construct( $dataModel, [ "testing-serialize-error" ] ); + } + + /** + * @see ContentHandler::unserializeContent + * + * @param string $blob + * @param string $format + * + * @return Content + */ + public function unserializeContent( $blob, $format = null ) { + throw new MWContentSerializationException( 'Could not unserialize content' ); + } + + /** + * @see ContentHandler::supportsDirectEditing + * + * @return bool + * + * @todo Should this be in the parent class? + */ + public function supportsDirectApiEditing() { + return true; + } + +} diff --git a/www/wiki/tests/phpunit/mocks/media/MockOggHandler.php b/www/wiki/tests/phpunit/mocks/media/MockOggHandler.php deleted file mode 100644 index b110e213..00000000 --- a/www/wiki/tests/phpunit/mocks/media/MockOggHandler.php +++ /dev/null @@ -1,93 +0,0 @@ -<?php -/** - * Fake handler for Ogg videos. - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along - * with this program; if not, write to the Free Software Foundation, Inc., - * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - * http://www.gnu.org/copyleft/gpl.html - * - * @file - * @ingroup Media - */ - -class MockOggHandler extends OggHandlerTMH { - function doTransform( $file, $dstPath, $dstUrl, $params, $flags = 0 ) { - # Important or height handling is wrong. - if ( !$this->normaliseParams( $file, $params ) ) { - return new TransformParameterError( $params ); - } - - $srcWidth = $file->getWidth(); - $srcHeight = $file->getHeight(); - - // Audio should not be transformed by size, give it a default width and height - if ( $this->isAudio( $file ) ) { - $srcWidth = 220; - $srcHeight = 23; - } - - $params['width'] = isset( $params['width'] ) ? $params['width'] : $srcWidth; - - // if height overtakes width use height as max: - $targetWidth = $params['width']; - $targetHeight = $srcWidth == 0 ? $srcHeight : round( $params['width'] * $srcHeight / $srcWidth ); - if ( isset( $params['height'] ) && $targetHeight > $params['height'] ) { - $targetHeight = $params['height']; - $targetWidth = round( $params['height'] * $srcWidth / $srcHeight ); - } - $options = [ - 'file' => $file, - 'length' => $this->getLength( $file ), - 'offset' => $this->getOffset( $file ), - 'width' => $targetWidth, - 'height' => $targetHeight, - 'isVideo' => !$this->isAudio( $file ), - 'thumbtime' => isset( - $params['thumbtime'] - ) ? $params['thumbtime'] : intval( $file->getLength() / 2 ), - 'start' => isset( $params['start'] ) ? $params['start'] : false, - 'end' => isset( $params['end'] ) ? $params['end'] : false, - 'fillwindow' => isset( $params['fillwindow'] ) ? $params['fillwindow'] : false, - 'disablecontrols' => isset ( $params['disablecontrols'] ) ? $params['disablecontrols'] : false - ]; - - // No thumbs for audio - if ( !$options['isVideo'] ) { - return new TimedMediaTransformOutput( $options ); - } - - // Setup pointer to thumb arguments - $options[ 'thumbUrl' ] = $dstUrl; - $options[ 'dstPath' ] = $dstPath; - $options[ 'path' ] = $dstPath; - - return new TimedMediaTransformOutput( $options ); - } - - function getLength( $file ) { - return 4.3666666666667; - } - - function getBitRate( $file ) { - return 590013; - } - - function getWebType( $file ) { - return "video/ogg; codecs=\"theora\""; - } - - function getFramerate( $file ) { - return 30; - } -} diff --git a/www/wiki/tests/phpunit/phpunit.php b/www/wiki/tests/phpunit/phpunit.php index 72037770..650cfcfa 100755 --- a/www/wiki/tests/phpunit/phpunit.php +++ b/www/wiki/tests/phpunit/phpunit.php @@ -21,6 +21,7 @@ class PHPUnitMaintClass extends Maintenance { 'use-bagostuff' => false, 'use-jobqueue' => false, 'use-normal-tables' => false, + 'mwdebug' => false, 'reuse-db' => false, 'wiki' => false, 'profiler' => false, @@ -79,7 +80,7 @@ class PHPUnitMaintClass extends Maintenance { [ '--configuration', $IP . '/tests/phpunit/suite.xml' ] ); } - $phpUnitClass = 'PHPUnit_TextUI_Command'; + $phpUnitClass = PHPUnit_TextUI_Command::class; if ( $this->hasOption( 'with-phpunitclass' ) ) { $phpUnitClass = $this->getOption( 'with-phpunitclass' ); @@ -112,7 +113,7 @@ class PHPUnitMaintClass extends Maintenance { } } - if ( !class_exists( 'PHPUnit_Framework_TestCase' ) ) { + if ( !class_exists( 'PHPUnit\\Framework\\TestCase' ) ) { echo "PHPUnit not found. Please install it and other dev dependencies by running `composer install` in MediaWiki root directory.\n"; exit( 1 ); @@ -123,9 +124,9 @@ class PHPUnitMaintClass extends Maintenance { exit( 1 ); } - echo defined( 'HHVM_VERSION' ) ? + fwrite( STDERR, defined( 'HHVM_VERSION' ) ? 'Using HHVM ' . HHVM_VERSION . ' (' . PHP_VERSION . ")\n" : - 'Using PHP ' . PHP_VERSION . "\n"; + 'Using PHP ' . PHP_VERSION . "\n" ); // Prepare global services for unit tests. MediaWikiTestCase::prepareServices( new GlobalVarConfig() ); diff --git a/www/wiki/tests/phpunit/skins/SideBarTest.php b/www/wiki/tests/phpunit/skins/SideBarTest.php index af03fe63..ec85bb03 100644 --- a/www/wiki/tests/phpunit/skins/SideBarTest.php +++ b/www/wiki/tests/phpunit/skins/SideBarTest.php @@ -104,10 +104,10 @@ class SideBarTest extends MediaWikiLangTestCase { ] ); $this->assertSideBar( [ 'Title' => [ - # ** http://www.mediawiki.org/| Home + # ** https://www.mediawiki.org/| Home [ 'text' => 'Home', - 'href' => 'http://www.mediawiki.org/', + 'href' => 'https://www.mediawiki.org/', 'id' => 'n-Home', 'active' => null, 'rel' => 'nofollow', @@ -116,7 +116,7 @@ class SideBarTest extends MediaWikiLangTestCase { # ... skipped since it is missing a pipe with a description ] ], '* Title -** http://www.mediawiki.org/| Home +** https://www.mediawiki.org/| Home ** http://valid.no.desc.org/ ' ); @@ -160,7 +160,7 @@ class SideBarTest extends MediaWikiLangTestCase { private function getAttribs() { # Sidebar text we will use everytime $text = '* Title -** http://www.mediawiki.org/| Home'; +** https://www.mediawiki.org/| Home'; $bar = []; $this->skin->addToSidebarPlain( $bar, $text ); @@ -188,6 +188,7 @@ class SideBarTest extends MediaWikiLangTestCase { /** * Test $wgNoFollowLinks in sidebar + * @covers Skin::addToSidebarPlain */ public function testRespectWgnofollowlinks() { $this->setMwGlobals( 'wgNoFollowLinks', false ); @@ -201,6 +202,7 @@ class SideBarTest extends MediaWikiLangTestCase { /** * Test $wgExternaLinkTarget in sidebar * @dataProvider dataRespectExternallinktarget + * @covers Skin::addToSidebarPlain */ public function testRespectExternallinktarget( $externalLinkTarget ) { $this->setMwGlobals( 'wgExternalLinkTarget', $externalLinkTarget ); diff --git a/www/wiki/tests/phpunit/structure/ApiDocumentationTest.php b/www/wiki/tests/phpunit/structure/ApiDocumentationTest.php deleted file mode 100644 index 2049e38b..00000000 --- a/www/wiki/tests/phpunit/structure/ApiDocumentationTest.php +++ /dev/null @@ -1,180 +0,0 @@ -<?php - -/** - * Checks that all API modules, core and extensions, have documentation i18n messages - * - * It won't catch everything since i18n messages can vary based on the wiki - * configuration, but it should catch many cases for forgotten i18n. - * - * @group API - */ -class ApiDocumentationTest extends MediaWikiTestCase { - - /** @var ApiMain */ - private static $main; - - /** @var array Sets of globals to test. Each array element is input to HashConfig */ - private static $testGlobals = [ - [ - 'MiserMode' => false, - 'AllowCategorizedRecentChanges' => false, - ], - [ - 'MiserMode' => true, - 'AllowCategorizedRecentChanges' => true, - ], - ]; - - /** - * Initialize/fetch the ApiMain instance for testing - * @return ApiMain - */ - private static function getMain() { - if ( !self::$main ) { - self::$main = new ApiMain( RequestContext::getMain() ); - self::$main->getContext()->setLanguage( 'en' ); - self::$main->getContext()->setTitle( - Title::makeTitle( NS_SPECIAL, 'Badtitle/dummy title for ApiDocumentationTest' ) - ); - } - return self::$main; - } - - /** - * Test a message - * @param Message $msg - * @param string $what Which message is being checked - */ - private function checkMessage( $msg, $what ) { - $msg = ApiBase::makeMessage( $msg, self::getMain()->getContext() ); - $this->assertInstanceOf( 'Message', $msg, "$what message" ); - $this->assertTrue( $msg->exists(), "$what message {$msg->getKey()} exists" ); - } - - /** - * @dataProvider provideDocumentationExists - * @param string $path Module path - * @param array $globals Globals to set - */ - public function testDocumentationExists( $path, array $globals ) { - $main = self::getMain(); - - // Set configuration variables - $main->getContext()->setConfig( new MultiConfig( [ - new HashConfig( $globals ), - RequestContext::getMain()->getConfig(), - ] ) ); - foreach ( $globals as $k => $v ) { - $this->setMwGlobals( "wg$k", $v ); - } - - // Fetch module. - $module = TestingAccessWrapper::newFromObject( $main->getModuleFromPath( $path ) ); - - // Test messages for flags. - foreach ( $module->getHelpFlags() as $flag ) { - $this->checkMessage( "api-help-flag-$flag", "Flag $flag" ); - } - - // Module description messages. - $this->checkMessage( $module->getDescriptionMessage(), 'Module description' ); - - // Parameters. Lots of messages in here. - $params = $module->getFinalParams( ApiBase::GET_VALUES_FOR_HELP ); - $tags = []; - foreach ( $params as $name => $settings ) { - if ( !is_array( $settings ) ) { - $settings = []; - } - - // Basic description message - if ( isset( $settings[ApiBase::PARAM_HELP_MSG] ) ) { - $msg = $settings[ApiBase::PARAM_HELP_MSG]; - } else { - $msg = "apihelp-{$path}-param-{$name}"; - } - $this->checkMessage( $msg, "Parameter $name description" ); - - // If param-per-value is in use, each value's message - if ( isset( $settings[ApiBase::PARAM_HELP_MSG_PER_VALUE] ) ) { - $this->assertInternalType( 'array', $settings[ApiBase::PARAM_HELP_MSG_PER_VALUE], - "Parameter $name PARAM_HELP_MSG_PER_VALUE is array" ); - $this->assertInternalType( 'array', $settings[ApiBase::PARAM_TYPE], - "Parameter $name PARAM_TYPE is array for msg-per-value mode" ); - $valueMsgs = $settings[ApiBase::PARAM_HELP_MSG_PER_VALUE]; - foreach ( $settings[ApiBase::PARAM_TYPE] as $value ) { - if ( isset( $valueMsgs[$value] ) ) { - $msg = $valueMsgs[$value]; - } else { - $msg = "apihelp-{$path}-paramvalue-{$name}-{$value}"; - } - $this->checkMessage( $msg, "Parameter $name value $value" ); - } - } - - // Appended messages (e.g. "disabled in miser mode") - if ( isset( $settings[ApiBase::PARAM_HELP_MSG_APPEND] ) ) { - $this->assertInternalType( 'array', $settings[ApiBase::PARAM_HELP_MSG_APPEND], - "Parameter $name PARAM_HELP_MSG_APPEND is array" ); - foreach ( $settings[ApiBase::PARAM_HELP_MSG_APPEND] as $i => $msg ) { - $this->checkMessage( $msg, "Parameter $name HELP_MSG_APPEND #$i" ); - } - } - - // Info tags (e.g. "only usable in mode 1") are typically shared by - // several parameters, so accumulate them and test them later. - if ( !empty( $settings[ApiBase::PARAM_HELP_MSG_INFO] ) ) { - foreach ( $settings[ApiBase::PARAM_HELP_MSG_INFO] as $i ) { - $tags[array_shift( $i )] = 1; - } - } - } - - // Info tags (e.g. "only usable in mode 1") accumulated above - foreach ( $tags as $tag => $dummy ) { - $this->checkMessage( "apihelp-{$path}-paraminfo-{$tag}", "HELP_MSG_INFO tag $tag" ); - } - - // Messages for examples. - foreach ( $module->getExamplesMessages() as $qs => $msg ) { - $this->checkMessage( $msg, "Example $qs" ); - } - } - - public static function provideDocumentationExists() { - $main = self::getMain(); - $paths = self::getSubModulePaths( $main->getModuleManager() ); - array_unshift( $paths, $main->getModulePath() ); - - $ret = []; - foreach ( $paths as $path ) { - foreach ( self::$testGlobals as $globals ) { - $g = []; - foreach ( $globals as $k => $v ) { - $g[] = "$k=" . var_export( $v, 1 ); - } - $k = "Module $path with " . implode( ', ', $g ); - $ret[$k] = [ $path, $globals ]; - } - } - return $ret; - } - - /** - * Return paths of all submodules in an ApiModuleManager, recursively - * @param ApiModuleManager $manager - * @return string[] - */ - protected static function getSubModulePaths( ApiModuleManager $manager ) { - $paths = []; - foreach ( $manager->getNames() as $name ) { - $module = $manager->getModule( $name ); - $paths[] = $module->getModulePath(); - $subManager = $module->getModuleManager(); - if ( $subManager ) { - $paths = array_merge( $paths, self::getSubModulePaths( $subManager ) ); - } - } - return $paths; - } -} diff --git a/www/wiki/tests/phpunit/structure/ApiStructureTest.php b/www/wiki/tests/phpunit/structure/ApiStructureTest.php index 7912f979..77d6e741 100644 --- a/www/wiki/tests/phpunit/structure/ApiStructureTest.php +++ b/www/wiki/tests/phpunit/structure/ApiStructureTest.php @@ -20,15 +20,88 @@ class ApiStructureTest extends MediaWikiTestCase { private static $testGlobals = [ [ 'MiserMode' => false, - 'AllowCategorizedRecentChanges' => false, ], [ 'MiserMode' => true, - 'AllowCategorizedRecentChanges' => true, ], ]; /** + * Values are an array, where each array value is a permitted type. A type + * can be a string, which is the name of an internal type or a + * class/interface. Or it can be an array, in which case the value must be + * an array whose elements are the types given in the array (e.g., [ + * 'string', integer' ] means an array whose entries are strings and/or + * integers). + */ + private static $paramTypes = [ + // ApiBase::PARAM_DFLT => as appropriate for PARAM_TYPE + ApiBase::PARAM_ISMULTI => [ 'boolean' ], + ApiBase::PARAM_TYPE => [ 'string', [ 'string' ] ], + ApiBase::PARAM_MAX => [ 'integer' ], + ApiBase::PARAM_MAX2 => [ 'integer' ], + ApiBase::PARAM_MIN => [ 'integer' ], + ApiBase::PARAM_ALLOW_DUPLICATES => [ 'boolean' ], + ApiBase::PARAM_DEPRECATED => [ 'boolean' ], + ApiBase::PARAM_REQUIRED => [ 'boolean' ], + ApiBase::PARAM_RANGE_ENFORCE => [ 'boolean' ], + ApiBase::PARAM_HELP_MSG => [ 'string', 'array', Message::class ], + ApiBase::PARAM_HELP_MSG_APPEND => [ [ 'string', 'array', Message::class ] ], + ApiBase::PARAM_HELP_MSG_INFO => [ [ 'array' ] ], + ApiBase::PARAM_VALUE_LINKS => [ [ 'string' ] ], + ApiBase::PARAM_HELP_MSG_PER_VALUE => [ [ 'string', 'array', Message::class ] ], + ApiBase::PARAM_SUBMODULE_MAP => [ [ 'string' ] ], + ApiBase::PARAM_SUBMODULE_PARAM_PREFIX => [ 'string' ], + ApiBase::PARAM_ALL => [ 'boolean', 'string' ], + ApiBase::PARAM_EXTRA_NAMESPACES => [ [ 'integer' ] ], + ApiBase::PARAM_SENSITIVE => [ 'boolean' ], + ApiBase::PARAM_DEPRECATED_VALUES => [ 'array' ], + ApiBase::PARAM_ISMULTI_LIMIT1 => [ 'integer' ], + ApiBase::PARAM_ISMULTI_LIMIT2 => [ 'integer' ], + ApiBase::PARAM_MAX_BYTES => [ 'integer' ], + ApiBase::PARAM_MAX_CHARS => [ 'integer' ], + ]; + + // param => [ other param that must be present => required value or null ] + private static $paramRequirements = [ + ApiBase::PARAM_ALLOW_DUPLICATES => [ ApiBase::PARAM_ISMULTI => true ], + ApiBase::PARAM_ALL => [ ApiBase::PARAM_ISMULTI => true ], + ApiBase::PARAM_ISMULTI_LIMIT1 => [ + ApiBase::PARAM_ISMULTI => true, + ApiBase::PARAM_ISMULTI_LIMIT2 => null, + ], + ApiBase::PARAM_ISMULTI_LIMIT2 => [ + ApiBase::PARAM_ISMULTI => true, + ApiBase::PARAM_ISMULTI_LIMIT1 => null, + ], + ]; + + // param => type(s) allowed for this param ('array' is any array) + private static $paramAllowedTypes = [ + ApiBase::PARAM_MAX => [ 'integer', 'limit' ], + ApiBase::PARAM_MAX2 => 'limit', + ApiBase::PARAM_MIN => [ 'integer', 'limit' ], + ApiBase::PARAM_RANGE_ENFORCE => 'integer', + ApiBase::PARAM_VALUE_LINKS => 'array', + ApiBase::PARAM_HELP_MSG_PER_VALUE => 'array', + ApiBase::PARAM_SUBMODULE_MAP => 'submodule', + ApiBase::PARAM_SUBMODULE_PARAM_PREFIX => 'submodule', + ApiBase::PARAM_ALL => 'array', + ApiBase::PARAM_EXTRA_NAMESPACES => 'namespace', + ApiBase::PARAM_DEPRECATED_VALUES => 'array', + ApiBase::PARAM_MAX_BYTES => [ 'NULL', 'string', 'text', 'password' ], + ApiBase::PARAM_MAX_CHARS => [ 'NULL', 'string', 'text', 'password' ], + ]; + + private static $paramProhibitedTypes = [ + ApiBase::PARAM_ISMULTI => [ 'boolean', 'limit', 'upload' ], + ApiBase::PARAM_ALL => 'namespace', + ApiBase::PARAM_SENSITIVE => 'password', + ]; + + private static $constantNames = null; + + /** * Initialize/fetch the ApiMain instance for testing * @return ApiMain */ @@ -50,7 +123,7 @@ class ApiStructureTest extends MediaWikiTestCase { */ private function checkMessage( $msg, $what ) { $msg = ApiBase::makeMessage( $msg, self::getMain()->getContext() ); - $this->assertInstanceOf( 'Message', $msg, "$what message" ); + $this->assertInstanceOf( Message::class, $msg, "$what message" ); $this->assertTrue( $msg->exists(), "$what message {$msg->getKey()} exists" ); } @@ -180,26 +253,327 @@ class ApiStructureTest extends MediaWikiTestCase { // avoid warnings about empty tests when no parameter needs to be checked $this->assertTrue( true ); + if ( self::$constantNames === null ) { + self::$constantNames = []; + + foreach ( ( new ReflectionClass( 'ApiBase' ) )->getConstants() as $key => $val ) { + if ( substr( $key, 0, 6 ) === 'PARAM_' ) { + self::$constantNames[$val] = $key; + } + } + } + foreach ( [ $paramsPlain, $paramsForHelp ] as $params ) { foreach ( $params as $param => $config ) { + if ( !is_array( $config ) ) { + $config = [ ApiBase::PARAM_DFLT => $config ]; + } + if ( !isset( $config[ApiBase::PARAM_TYPE] ) ) { + $config[ApiBase::PARAM_TYPE] = isset( $config[ApiBase::PARAM_DFLT] ) + ? gettype( $config[ApiBase::PARAM_DFLT] ) + : 'NULL'; + } + + foreach ( self::$paramTypes as $key => $types ) { + if ( !isset( $config[$key] ) ) { + continue; + } + $keyName = self::$constantNames[$key]; + $this->validateType( $types, $config[$key], $param, $keyName ); + } + + foreach ( self::$paramRequirements as $key => $required ) { + if ( !isset( $config[$key] ) ) { + continue; + } + foreach ( $required as $requireKey => $requireVal ) { + $this->assertArrayHasKey( $requireKey, $config, + "$param: When " . self::$constantNames[$key] . " is set, " . + self::$constantNames[$requireKey] . " must also be set" ); + if ( $requireVal !== null ) { + $this->assertSame( $requireVal, $config[$requireKey], + "$param: When " . self::$constantNames[$key] . " is set, " . + self::$constantNames[$requireKey] . " must equal " . + var_export( $requireVal, true ) ); + } + } + } + + foreach ( self::$paramAllowedTypes as $key => $allowedTypes ) { + if ( !isset( $config[$key] ) ) { + continue; + } + + $actualType = is_array( $config[ApiBase::PARAM_TYPE] ) + ? 'array' : $config[ApiBase::PARAM_TYPE]; + + $this->assertContains( + $actualType, + (array)$allowedTypes, + "$param: " . self::$constantNames[$key] . + " can only be used with PARAM_TYPE " . + implode( ', ', (array)$allowedTypes ) + ); + } + + foreach ( self::$paramProhibitedTypes as $key => $prohibitedTypes ) { + if ( !isset( $config[$key] ) ) { + continue; + } + + $actualType = is_array( $config[ApiBase::PARAM_TYPE] ) + ? 'array' : $config[ApiBase::PARAM_TYPE]; + + $this->assertNotContains( + $actualType, + (array)$prohibitedTypes, + "$param: " . self::$constantNames[$key] . + " cannot be used with PARAM_TYPE " . + implode( ', ', (array)$prohibitedTypes ) + ); + } + + if ( isset( $config[ApiBase::PARAM_DFLT] ) ) { + $this->assertFalse( + isset( $config[ApiBase::PARAM_REQUIRED] ) && + $config[ApiBase::PARAM_REQUIRED], + "$param: A required parameter cannot have a default" ); + + $this->validateDefault( $param, $config ); + } + + if ( $config[ApiBase::PARAM_TYPE] === 'limit' ) { + $this->assertTrue( + isset( $config[ApiBase::PARAM_MAX] ) && + isset( $config[ApiBase::PARAM_MAX2] ), + "$param: PARAM_MAX and PARAM_MAX2 are required for limits" + ); + $this->assertGreaterThanOrEqual( + $config[ApiBase::PARAM_MAX], + $config[ApiBase::PARAM_MAX2], + "$param: PARAM_MAX cannot be greater than PARAM_MAX2" + ); + } + if ( - isset( $config[ApiBase::PARAM_ISMULTI_LIMIT1] ) - || isset( $config[ApiBase::PARAM_ISMULTI_LIMIT2] ) + isset( $config[ApiBase::PARAM_MIN] ) && + isset( $config[ApiBase::PARAM_MAX] ) ) { - $this->assertTrue( !empty( $config[ApiBase::PARAM_ISMULTI] ), $param - . ': PARAM_ISMULTI_LIMIT* only makes sense when PARAM_ISMULTI is true' ); - $this->assertTrue( isset( $config[ApiBase::PARAM_ISMULTI_LIMIT1] ) - && isset( $config[ApiBase::PARAM_ISMULTI_LIMIT2] ), $param - . ': PARAM_ISMULTI_LIMIT1 and PARAM_ISMULTI_LIMIT2 must be used together' ); - $this->assertType( 'int', $config[ApiBase::PARAM_ISMULTI_LIMIT1], $param - . 'PARAM_ISMULTI_LIMIT1 must be an integer' ); - $this->assertType( 'int', $config[ApiBase::PARAM_ISMULTI_LIMIT2], $param - . 'PARAM_ISMULTI_LIMIT2 must be an integer' ); - $this->assertGreaterThanOrEqual( $config[ApiBase::PARAM_ISMULTI_LIMIT1], - $config[ApiBase::PARAM_ISMULTI_LIMIT2], $param - . 'PARAM_ISMULTI limit cannot be smaller for users with apihighlimits rights' ); + $this->assertGreaterThanOrEqual( + $config[ApiBase::PARAM_MIN], + $config[ApiBase::PARAM_MAX], + "$param: PARAM_MIN cannot be greater than PARAM_MAX" + ); + } + + if ( isset( $config[ApiBase::PARAM_RANGE_ENFORCE] ) ) { + $this->assertTrue( + isset( $config[ApiBase::PARAM_MIN] ) || + isset( $config[ApiBase::PARAM_MAX] ), + "$param: PARAM_RANGE_ENFORCE can only be set together with " . + "PARAM_MIN or PARAM_MAX" + ); + } + + if ( isset( $config[ApiBase::PARAM_DEPRECATED_VALUES] ) ) { + foreach ( $config[ApiBase::PARAM_DEPRECATED_VALUES] as $key => $unused ) { + $this->assertContains( $key, $config[ApiBase::PARAM_TYPE], + "$param: Deprecated value \"$key\" is not allowed, " . + "how can it be deprecated?" ); + } + } + + if ( + isset( $config[ApiBase::PARAM_ISMULTI_LIMIT1] ) || + isset( $config[ApiBase::PARAM_ISMULTI_LIMIT2] ) + ) { + $this->assertGreaterThanOrEqual( 0, $config[ApiBase::PARAM_ISMULTI_LIMIT1], + "$param: PARAM_ISMULTI_LIMIT1 cannot be negative" ); + // Zero for both doesn't make sense, but you could have + // zero for non-bots + $this->assertGreaterThanOrEqual( 1, $config[ApiBase::PARAM_ISMULTI_LIMIT2], + "$param: PARAM_ISMULTI_LIMIT2 cannot be negative or zero" ); + $this->assertGreaterThanOrEqual( + $config[ApiBase::PARAM_ISMULTI_LIMIT1], + $config[ApiBase::PARAM_ISMULTI_LIMIT2], + "$param: PARAM_ISMULTI limit cannot be smaller for users with " . + "apihighlimits rights" ); + } + + if ( isset( $config[ApiBase::PARAM_MAX_BYTES] ) ) { + $this->assertGreaterThanOrEqual( 1, $config[ApiBase::PARAM_MAX_BYTES], + "$param: PARAM_MAX_BYTES cannot be negative or zero" ); + } + + if ( isset( $config[ApiBase::PARAM_MAX_CHARS] ) ) { + $this->assertGreaterThanOrEqual( 1, $config[ApiBase::PARAM_MAX_CHARS], + "$param: PARAM_MAX_CHARS cannot be negative or zero" ); + } + + if ( + isset( $config[ApiBase::PARAM_MAX_BYTES] ) && + isset( $config[ApiBase::PARAM_MAX_CHARS] ) + ) { + // Length of a string in chars is always <= length in bytes, + // so PARAM_MAX_CHARS is pointless if > PARAM_MAX_BYTES + $this->assertGreaterThanOrEqual( + $config[ApiBase::PARAM_MAX_CHARS], + $config[ApiBase::PARAM_MAX_BYTES], + "$param: PARAM_MAX_BYTES cannot be less than PARAM_MAX_CHARS" + ); + } + } + } + } + + /** + * Throws if $value does not match one of the types specified in $types. + * + * @param array $types From self::$paramTypes array + * @param mixed $value Value to check + * @param string $param Name of param we're checking, for error messages + * @param string $desc Description for error messages + */ + private function validateType( $types, $value, $param, $desc ) { + if ( count( $types ) === 1 ) { + // Only one type allowed + if ( is_string( $types[0] ) ) { + $this->assertType( $types[0], $value, "$param: $desc type" ); + } else { + // Array whose values have specified types, recurse + $this->assertInternalType( 'array', $value, "$param: $desc type" ); + foreach ( $value as $subvalue ) { + $this->validateType( $types[0], $subvalue, $param, "$desc value" ); + } + } + } else { + // Multiple options + foreach ( $types as $type ) { + if ( is_string( $type ) ) { + if ( class_exists( $type ) || interface_exists( $type ) ) { + if ( $value instanceof $type ) { + return; + } + } else { + if ( gettype( $value ) === $type ) { + return; + } + } + } else { + // Array whose values have specified types, recurse + try { + $this->validateType( [ $type ], $value, $param, "$desc type" ); + // Didn't throw, so we're good + return; + } catch ( Exception $unused ) { + } } } + // Doesn't match any of them + $this->fail( "$param: $desc has incorrect type" ); + } + } + + /** + * Asserts that $default is a valid default for $type. + * + * @param string $param Name of param, for error messages + * @param array $config Array of configuration options for this parameter + */ + private function validateDefault( $param, $config ) { + $type = $config[ApiBase::PARAM_TYPE]; + $default = $config[ApiBase::PARAM_DFLT]; + + if ( !empty( $config[ApiBase::PARAM_ISMULTI] ) ) { + if ( $default === '' ) { + // The empty array is fine + return; + } + $defaults = explode( '|', $default ); + $config[ApiBase::PARAM_ISMULTI] = false; + foreach ( $defaults as $defaultValue ) { + // Only allow integers in their simplest form with no leading + // or trailing characters etc. + if ( $type === 'integer' && $defaultValue === (string)(int)$defaultValue ) { + $defaultValue = (int)$defaultValue; + } + $config[ApiBase::PARAM_DFLT] = $defaultValue; + $this->validateDefault( $param, $config ); + } + return; + } + switch ( $type ) { + case 'boolean': + $this->assertFalse( $default, + "$param: Boolean params may only default to false" ); + break; + + case 'integer': + $this->assertInternalType( 'integer', $default, + "$param: Default $default is not an integer" ); + break; + + case 'limit': + if ( $default === 'max' ) { + break; + } + $this->assertInternalType( 'integer', $default, + "$param: Default $default is neither an integer nor \"max\"" ); + break; + + case 'namespace': + $validValues = MWNamespace::getValidNamespaces(); + if ( + isset( $config[ApiBase::PARAM_EXTRA_NAMESPACES] ) && + is_array( $config[ApiBase::PARAM_EXTRA_NAMESPACES] ) + ) { + $validValues = array_merge( + $validValues, + $config[ApiBase::PARAM_EXTRA_NAMESPACES] + ); + } + $this->assertContains( $default, $validValues, + "$param: Default $default is not a valid namespace" ); + break; + + case 'NULL': + case 'password': + case 'string': + case 'submodule': + case 'tags': + case 'text': + $this->assertInternalType( 'string', $default, + "$param: Default $default is not a string" ); + break; + + case 'timestamp': + if ( $default === 'now' ) { + return; + } + $this->assertNotFalse( wfTimestamp( TS_MW, $default ), + "$param: Default $default is not a valid timestamp" ); + break; + + case 'user': + // @todo Should we make user validation a public static method + // in ApiBase() or something so we don't have to resort to + // this? Or in User for that matter. + $wrapper = TestingAccessWrapper::newFromObject( new ApiMain() ); + try { + $wrapper->validateUser( $default, '' ); + } catch ( ApiUsageException $e ) { + $this->fail( "$param: Default $default is not a valid username/IP address" ); + } + break; + + default: + if ( is_array( $type ) ) { + $this->assertContains( $default, $type, + "$param: Default $default is not any of " . + implode( ', ', $type ) ); + } else { + $this->fail( "Unrecognized type $type" ); + } } } diff --git a/www/wiki/tests/phpunit/structure/AutoLoaderTest.php b/www/wiki/tests/phpunit/structure/AutoLoaderTest.php index d81e8c66..217232e3 100644 --- a/www/wiki/tests/phpunit/structure/AutoLoaderTest.php +++ b/www/wiki/tests/phpunit/structure/AutoLoaderTest.php @@ -58,9 +58,9 @@ class AutoLoaderTest extends MediaWikiTestCase { continue; } - MediaWiki\suppressWarnings(); + Wikimedia\suppressWarnings(); $contents = file_get_contents( $filePath ); - MediaWiki\restoreWarnings(); + Wikimedia\restoreWarnings(); if ( $contents === false ) { $actual[$class] = "[couldn't read file '$filePath']"; @@ -161,6 +161,7 @@ class AutoLoaderTest extends MediaWikiTestCase { $path = realpath( __DIR__ . '/../../..' ); $oldAutoload = file_get_contents( $path . '/autoload.php' ); $generator = new AutoloadGenerator( $path, 'local' ); + $generator->setExcludePaths( array_values( AutoLoader::getAutoloadNamespaces() ) ); $generator->initMediaWikiDefault(); $newAutoload = $generator->getAutoload( 'maintenance/generateLocalAutoload.php' ); diff --git a/www/wiki/tests/phpunit/structure/AvailableRightsTest.php b/www/wiki/tests/phpunit/structure/AvailableRightsTest.php index 16a9a24d..6c2ff024 100644 --- a/www/wiki/tests/phpunit/structure/AvailableRightsTest.php +++ b/www/wiki/tests/phpunit/structure/AvailableRightsTest.php @@ -6,7 +6,9 @@ * * @author Marius Hoch < hoo@online.de > */ -class AvailableRightsTest extends PHPUnit_Framework_TestCase { +class AvailableRightsTest extends PHPUnit\Framework\TestCase { + + use MediaWikiCoversValidator; /** * Returns all rights that should be in $wgAvailableRights + all rights diff --git a/www/wiki/tests/phpunit/structure/ExtensionJsonValidationTest.php b/www/wiki/tests/phpunit/structure/ExtensionJsonValidationTest.php index b19376d3..60c97ccf 100644 --- a/www/wiki/tests/phpunit/structure/ExtensionJsonValidationTest.php +++ b/www/wiki/tests/phpunit/structure/ExtensionJsonValidationTest.php @@ -20,7 +20,9 @@ * Validates all loaded extensions and skins using the ExtensionRegistry * against the extension.json schema in the docs/ folder. */ -class ExtensionJsonValidationTest extends PHPUnit_Framework_TestCase { +class ExtensionJsonValidationTest extends PHPUnit\Framework\TestCase { + + use MediaWikiCoversValidator; /** * @var ExtensionJsonValidator diff --git a/www/wiki/tests/phpunit/structure/ResourcesTest.php b/www/wiki/tests/phpunit/structure/ResourcesTest.php index 2fba76b6..62ddaceb 100644 --- a/www/wiki/tests/phpunit/structure/ResourcesTest.php +++ b/www/wiki/tests/phpunit/structure/ResourcesTest.php @@ -44,15 +44,24 @@ class ResourcesTest extends MediaWikiTestCase { } /** - * Verify that nothing explicitly depends on the 'jquery' and 'mediawiki' modules. - * They are always loaded, depending on them is unsupported and leads to unexpected behaviour. + * Verify that nothing explicitly depends on base modules, or other raw modules. + * + * Depending on them is unsupported as they are not registered client-side by the startup module. + * * TODO Modules can dynamically choose dependencies based on context. This method does not * test such dependencies. The same goes for testMissingDependencies() and * testUnsatisfiableDependencies(). */ public function testIllegalDependencies() { $data = self::getAllModules(); - $illegalDeps = [ 'jquery', 'mediawiki' ]; + + $illegalDeps = ResourceLoaderStartUpModule::getStartupModules(); + foreach ( $data['modules'] as $moduleName => $module ) { + if ( $module->isRaw() ) { + $illegalDeps[] = $moduleName; + } + } + $illegalDeps = array_unique( $illegalDeps ); /** @var ResourceLoaderModule $module */ foreach ( $data['modules'] as $moduleName => $module ) { diff --git a/www/wiki/tests/phpunit/structure/StructureTest.php b/www/wiki/tests/phpunit/structure/StructureTest.php index 9016cb7b..4df791ec 100644 --- a/www/wiki/tests/phpunit/structure/StructureTest.php +++ b/www/wiki/tests/phpunit/structure/StructureTest.php @@ -25,6 +25,8 @@ class StructureTest extends MediaWikiTestCase { 'MediaWikiTestCase', 'ResourceLoaderTestCase', 'PHPUnit_Framework_TestCase', + '\\?PHPUnit\\Framework\\TestCase', + 'TestCase', // \PHPUnit\Framework\TestCase with appropriate use statement 'DumpTestCase', ] ); $testClassRegex = "^class .* extends ($testClassRegex)"; diff --git a/www/wiki/tests/phpunit/suite.xml b/www/wiki/tests/phpunit/suite.xml index e8256ef2..16c0c17c 100644 --- a/www/wiki/tests/phpunit/suite.xml +++ b/www/wiki/tests/phpunit/suite.xml @@ -16,6 +16,7 @@ beStrictAboutTestsThatDoNotTestAnything="true" beStrictAboutOutputDuringTests="true" beStrictAboutTestSize="true" + stderr="true" verbose="false"> <testsuites> <testsuite name="includes"> diff --git a/www/wiki/tests/phpunit/suites/ParserTestFileSuite.php b/www/wiki/tests/phpunit/suites/ParserTestFileSuite.php index dbee8947..b72d8b84 100644 --- a/www/wiki/tests/phpunit/suites/ParserTestFileSuite.php +++ b/www/wiki/tests/phpunit/suites/ParserTestFileSuite.php @@ -23,6 +23,10 @@ class ParserTestFileSuite extends PHPUnit_Framework_TestSuite { } function setUp() { - $this->ptRunner->addArticles( $this->ptFileInfo[ 'articles'] ); + if ( !$this->ptRunner->meetsRequirements( $this->ptFileInfo['requirements'] ) ) { + $this->markTestSuiteSkipped( 'required extension not enabled' ); + } else { + $this->ptRunner->addArticles( $this->ptFileInfo[ 'articles'] ); + } } } diff --git a/www/wiki/tests/phpunit/suites/UploadFromUrlTestSuite.php b/www/wiki/tests/phpunit/suites/UploadFromUrlTestSuite.php index f2e6858a..556c7541 100644 --- a/www/wiki/tests/phpunit/suites/UploadFromUrlTestSuite.php +++ b/www/wiki/tests/phpunit/suites/UploadFromUrlTestSuite.php @@ -18,8 +18,7 @@ class UploadFromUrlTestSuite extends PHPUnit_Framework_TestSuite { protected function setUp() { global $wgParser, $wgParserConf, $IP, $messageMemc, $wgMemc, $wgUser, $wgLang, $wgOut, $wgRequest, $wgStyleDirectory, - $wgParserCacheType, $wgNamespaceAliases, $wgNamespaceProtection, - $parserMemc; + $wgParserCacheType, $wgNamespaceAliases, $wgNamespaceProtection; $tmpDir = $this->getNewTempDirectory(); $tmpGlobals = []; @@ -30,7 +29,7 @@ class UploadFromUrlTestSuite extends PHPUnit_Framework_TestSuite { $tmpGlobals['wgStylePath'] = '/skins'; $tmpGlobals['wgThumbnailScriptPath'] = false; $tmpGlobals['wgLocalFileRepo'] = [ - 'class' => 'LocalRepo', + 'class' => LocalRepo::class, 'name' => 'local', 'url' => 'http://example.com/images', 'hashLevels' => 2, @@ -89,54 +88,6 @@ class UploadFromUrlTestSuite extends PHPUnit_Framework_TestSuite { parent::tearDown(); } - /** - * Delete the specified files, if they exist. - * - * @param array $files 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 array $dirs 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; - - $dir = $this->getNewTempDirectory(); - - wfDebug( "Creating upload directory $dir\n" ); - - wfMkdirParents( $dir . '/3/3a', null, __METHOD__ ); - copy( "$IP/tests/phpunit/data/upload/headbg.jpg", "$dir/3/3a/Foobar.jpg" ); - - wfMkdirParents( $dir . '/0/09', null, __METHOD__ ); - copy( "$IP/tests/phpunit/data/upload/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 diff --git a/www/wiki/tests/phpunit/tests/MediaWikiTestCaseSchema1Test.php b/www/wiki/tests/phpunit/tests/MediaWikiTestCaseSchema1Test.php new file mode 100644 index 00000000..d794d131 --- /dev/null +++ b/www/wiki/tests/phpunit/tests/MediaWikiTestCaseSchema1Test.php @@ -0,0 +1,51 @@ +<?php +use Wikimedia\Rdbms\IMaintainableDatabase; + +/** + * @covers MediaWikiTestCase + * + * @group Database + * @group MediaWikiTestCaseTest + */ +class MediaWikiTestCaseSchema1Test extends MediaWikiTestCase { + + public static $hasRun = false; + + public function getSchemaOverrides( IMaintainableDatabase $db ) { + return [ + 'create' => [ 'MediaWikiTestCaseTestTable', 'imagelinks' ], + 'drop' => [ 'oldimage' ], + 'alter' => [ 'pagelinks' ], + 'scripts' => [ __DIR__ . '/MediaWikiTestCaseSchemaTest.sql' ] + ]; + } + + public function testMediaWikiTestCaseSchemaTestOrder() { + // The test must be run before the second test + self::$hasRun = true; + $this->assertTrue( self::$hasRun ); + } + + public function testTableWasCreated() { + // Make sure MediaWikiTestCaseTestTable was created. + $this->assertTrue( $this->db->tableExists( 'MediaWikiTestCaseTestTable' ) ); + } + + public function testTableWasDropped() { + // Make sure oldimage was dropped + $this->assertFalse( $this->db->tableExists( 'oldimage' ) ); + } + + public function testTableWasOverriden() { + // Make sure imagelinks was overwritten + $this->assertTrue( $this->db->tableExists( 'imagelinks' ) ); + $this->assertTrue( $this->db->fieldExists( 'imagelinks', 'il_frobnitz' ) ); + } + + public function testTableWasAltered() { + // Make sure pagelinks was altered + $this->assertTrue( $this->db->tableExists( 'pagelinks' ) ); + $this->assertTrue( $this->db->fieldExists( 'pagelinks', 'pl_frobnitz' ) ); + } + +} diff --git a/www/wiki/tests/phpunit/tests/MediaWikiTestCaseSchema2Test.php b/www/wiki/tests/phpunit/tests/MediaWikiTestCaseSchema2Test.php new file mode 100644 index 00000000..5464dc43 --- /dev/null +++ b/www/wiki/tests/phpunit/tests/MediaWikiTestCaseSchema2Test.php @@ -0,0 +1,48 @@ +<?php + +/** + * @covers MediaWikiTestCase + * + * @group Database + * @group MediaWikiTestCaseTest + * + * This test is intended to be executed AFTER MediaWikiTestCaseSchema1Test to ensure + * that any schema modifications have been cleaned up between test cases. + * As there seems to be no way to force execution order, we currently rely on + * test classes getting run in anpha-numerical order. + * Order is checked by the testMediaWikiTestCaseSchemaTestOrder test in both classes. + */ +class MediaWikiTestCaseSchema2Test extends MediaWikiTestCase { + + public function testMediaWikiTestCaseSchemaTestOrder() { + // The first test must have run before this one + $this->assertTrue( MediaWikiTestCaseSchema1Test::$hasRun ); + } + + public function testCreatedTableWasRemoved() { + // Make sure MediaWikiTestCaseTestTable created by MediaWikiTestCaseSchema1Test + // was dropped before executing MediaWikiTestCaseSchema2Test. + $this->assertFalse( $this->db->tableExists( 'MediaWikiTestCaseTestTable' ) ); + } + + public function testDroppedTableWasRestored() { + // Make sure oldimage that was dropped by MediaWikiTestCaseSchema1Test + // was restored before executing MediaWikiTestCaseSchema2Test. + $this->assertTrue( $this->db->tableExists( 'oldimage' ) ); + } + + public function testOverridenTableWasRestored() { + // Make sure imagelinks overwritten by MediaWikiTestCaseSchema1Test + // was restored to the original schema before executing MediaWikiTestCaseSchema2Test. + $this->assertTrue( $this->db->tableExists( 'imagelinks' ) ); + $this->assertFalse( $this->db->fieldExists( 'imagelinks', 'il_frobnitz' ) ); + } + + public function testAlteredTableWasRestored() { + // Make sure pagelinks altered by MediaWikiTestCaseSchema1Test + // was restored to the original schema before executing MediaWikiTestCaseSchema2Test. + $this->assertTrue( $this->db->tableExists( 'pagelinks' ) ); + $this->assertFalse( $this->db->fieldExists( 'pagelinks', 'pl_frobnitz' ) ); + } + +} diff --git a/www/wiki/tests/phpunit/tests/MediaWikiTestCaseSchemaTest.sql b/www/wiki/tests/phpunit/tests/MediaWikiTestCaseSchemaTest.sql new file mode 100644 index 00000000..e2818b55 --- /dev/null +++ b/www/wiki/tests/phpunit/tests/MediaWikiTestCaseSchemaTest.sql @@ -0,0 +1,18 @@ +CREATE TABLE /*_*/MediaWikiTestCaseTestTable ( + id INT NOT NULL, + name VARCHAR(20) NOT NULL, + PRIMARY KEY (id) +) /*$wgDBTableOptions*/; + +CREATE TABLE /*_*/imagelinks ( + il_from int NOT NULL DEFAULT 0, + il_from_namespace int NOT NULL DEFAULT 0, + il_to varchar(127) NOT NULL DEFAULT '', + il_frobnitz varchar(127) NOT NULL DEFAULT 'FROB', + PRIMARY KEY (il_from,il_to) +) /*$wgDBTableOptions*/; + +ALTER TABLE /*_*/pagelinks +ADD pl_frobnitz varchar(127) NOT NULL DEFAULT 'FROB'; + +DROP TABLE /*_*/oldimage; diff --git a/www/wiki/tests/phpunit/tests/MediaWikiTestCaseTest.php b/www/wiki/tests/phpunit/tests/MediaWikiTestCaseTest.php index 7d75ffe6..1850f6fe 100644 --- a/www/wiki/tests/phpunit/tests/MediaWikiTestCaseTest.php +++ b/www/wiki/tests/phpunit/tests/MediaWikiTestCaseTest.php @@ -2,9 +2,12 @@ use MediaWiki\Logger\LoggerFactory; use MediaWiki\MediaWikiServices; use Psr\Log\LoggerInterface; +use Wikimedia\Rdbms\LoadBalancer; /** * @covers MediaWikiTestCase + * @group MediaWikiTestCaseTest + * * @author Addshore */ class MediaWikiTestCaseTest extends MediaWikiTestCase { @@ -162,7 +165,7 @@ class MediaWikiTestCaseTest extends MediaWikiTestCase { $logger2 = LoggerFactory::getInstance( 'foo' ); $this->assertNotSame( $logger1, $logger2 ); - $this->assertInstanceOf( '\Psr\Log\LoggerInterface', $logger2 ); + $this->assertInstanceOf( \Psr\Log\LoggerInterface::class, $logger2 ); } /** diff --git a/www/wiki/tests/qunit/QUnitTestResources.php b/www/wiki/tests/qunit/QUnitTestResources.php index 7367560e..785e1146 100644 --- a/www/wiki/tests/qunit/QUnitTestResources.php +++ b/www/wiki/tests/qunit/QUnitTestResources.php @@ -33,7 +33,6 @@ return [ 'mediawiki.page.startup', 'test.sinonjs', ], - 'position' => 'top', 'targets' => [ 'desktop', 'mobile' ], ], @@ -46,14 +45,12 @@ return [ 'scripts' => [ 'tests/qunit/suites/resources/startup.test.js', 'tests/qunit/suites/resources/jquery/jquery.accessKeyLabel.test.js', - 'tests/qunit/suites/resources/jquery/jquery.autoEllipsis.test.js', - 'tests/qunit/suites/resources/jquery/jquery.byteLength.test.js', - 'tests/qunit/suites/resources/jquery/jquery.byteLimit.test.js', 'tests/qunit/suites/resources/jquery/jquery.color.test.js', 'tests/qunit/suites/resources/jquery/jquery.colorUtil.test.js', 'tests/qunit/suites/resources/jquery/jquery.getAttrs.test.js', 'tests/qunit/suites/resources/jquery/jquery.hidpi.test.js', 'tests/qunit/suites/resources/jquery/jquery.highlightText.test.js', + 'tests/qunit/suites/resources/jquery/jquery.lengthLimit.test.js', 'tests/qunit/suites/resources/jquery/jquery.localize.test.js', 'tests/qunit/suites/resources/jquery/jquery.makeCollapsible.test.js', 'tests/qunit/suites/resources/jquery/jquery.tabIndex.test.js', @@ -67,6 +64,8 @@ return [ 'tests/qunit/suites/resources/mediawiki/mediawiki.jscompat.test.js', 'tests/qunit/suites/resources/mediawiki/mediawiki.messagePoster.factory.test.js', 'tests/qunit/suites/resources/mediawiki/mediawiki.RegExp.test.js', + 'tests/qunit/suites/resources/mediawiki/mediawiki.String.byteLength.test.js', + 'tests/qunit/suites/resources/mediawiki/mediawiki.String.trimByteLength.test.js', 'tests/qunit/suites/resources/mediawiki/mediawiki.storage.test.js', 'tests/qunit/suites/resources/mediawiki/mediawiki.template.test.js', 'tests/qunit/suites/resources/mediawiki/mediawiki.template.mustache.test.js', @@ -93,22 +92,23 @@ return [ 'tests/qunit/suites/resources/mediawiki.special/mediawiki.special.recentchanges.test.js', 'tests/qunit/suites/resources/mediawiki.rcfilters/dm.FiltersViewModel.test.js', 'tests/qunit/suites/resources/mediawiki.rcfilters/dm.FilterItem.test.js', + 'tests/qunit/suites/resources/mediawiki.rcfilters/dm.SavedQueryItemModel.test.js', + 'tests/qunit/suites/resources/mediawiki.rcfilters/dm.SavedQueriesModel.test.js', 'tests/qunit/suites/resources/mediawiki.rcfilters/UriProcessor.test.js', 'tests/qunit/suites/resources/mediawiki/mediawiki.language.test.js', 'tests/qunit/suites/resources/mediawiki/mediawiki.cldr.test.js', 'tests/qunit/suites/resources/mediawiki/mediawiki.cookie.test.js', 'tests/qunit/suites/resources/mediawiki/mediawiki.experiments.test.js', + 'tests/qunit/suites/resources/mediawiki/mediawiki.visibleTimeout.test.js', ], 'dependencies' => [ 'jquery.accessKeyLabel', - 'jquery.autoEllipsis', - 'jquery.byteLength', - 'jquery.byteLimit', 'jquery.color', 'jquery.colorUtil', 'jquery.getAttrs', 'jquery.hidpi', 'jquery.highlightText', + 'jquery.lengthLimit', 'jquery.localize', 'jquery.makeCollapsible', 'jquery.tabIndex', @@ -125,6 +125,7 @@ return [ 'mediawiki.jqueryMsg', 'mediawiki.messagePoster', 'mediawiki.RegExp', + 'mediawiki.String', 'mediawiki.storage', 'mediawiki.Title', 'mediawiki.toc', @@ -141,6 +142,7 @@ return [ 'mediawiki.cookie', 'mediawiki.experiments', 'mediawiki.inspect', + 'mediawiki.visibleTimeout', 'test.mediawiki.qunit.testrunner', ], ] diff --git a/www/wiki/tests/qunit/data/callMwLoaderTestCallback.js b/www/wiki/tests/qunit/data/callMwLoaderTestCallback.js deleted file mode 100644 index dd034115..00000000 --- a/www/wiki/tests/qunit/data/callMwLoaderTestCallback.js +++ /dev/null @@ -1 +0,0 @@ -mediaWiki.loader.testCallback(); diff --git a/www/wiki/tests/qunit/data/generateJqueryMsgData.php b/www/wiki/tests/qunit/data/generateJqueryMsgData.php index 1c79f6d1..e4f87f81 100644 --- a/www/wiki/tests/qunit/data/generateJqueryMsgData.php +++ b/www/wiki/tests/qunit/data/generateJqueryMsgData.php @@ -21,7 +21,7 @@ $.each( mw.libs.phpParserData.tests, function ( i, test ) { QUnit.stop(); getMwLanguage( test.lang, function ( langClass ) { - var parser = new mw.jqueryMsg.parser( { language: langClass } ); + var parser = new mw.jqueryMsg.Parser( { language: langClass } ); assert.equal( parser.parse( test.key, test.args ).html(), test.result, @@ -50,7 +50,7 @@ }, 'Language class should be loaded', 1000 ); runs( function () { console.log( test.lang, 'running tests' ); - var parser = new mw.jqueryMsg.parser( { language: langClass } ); + var parser = new mw.jqueryMsg.Parser( { language: langClass } ); expect( parser.parse( test.key, test.args ).html() ).toEqual( test.result ); diff --git a/www/wiki/tests/qunit/data/load.mock.php b/www/wiki/tests/qunit/data/load.mock.php index 671bdf1f..23009498 100644 --- a/www/wiki/tests/qunit/data/load.mock.php +++ b/www/wiki/tests/qunit/data/load.mock.php @@ -24,27 +24,22 @@ */ header( 'Content-Type: text/javascript; charset=utf-8' ); -require_once __DIR__ . '/../../../includes/json/FormatJson.php'; -require_once __DIR__ . '/../../../includes/Xml.php'; - $moduleImplementations = [ 'testUsesMissing' => " mw.loader.implement( 'testUsesMissing', function () { - QUnit.ok( false, 'Module usesMissing script should not run.' ); - QUnit.start(); + mw.loader.testFail( 'Module usesMissing script should not run.' ); }, {}, {}); ", 'testUsesNestedMissing' => " mw.loader.implement( 'testUsesNestedMissing', function () { - QUnit.ok( false, 'Module testUsesNestedMissing script should not run.' ); - QUnit.start(); + mw.loader.testFail('Module testUsesNestedMissing script should not run.' ); }, {}, {}); ", 'testSkipped' => " mw.loader.implement( 'testSkipped', function () { - QUnit.ok( false, 'Module testSkipped was supposed to be skipped.' ); + mw.loader.testFail( false, 'Module testSkipped was supposed to be skipped.' ); }, {}, {}); ", @@ -55,18 +50,56 @@ mw.loader.implement( 'testNotSkipped', function () {}, {}, {}); 'testUsesSkippable' => " mw.loader.implement( 'testUsesSkippable', function () {}, {}, {}); ", + + 'testUrlInc' => " +mw.loader.implement( 'testUrlInc', function () {} ); +", + 'testUrlInc.a' => " +mw.loader.implement( 'testUrlInc.a', function () {} ); +", + 'testUrlInc.b' => " +mw.loader.implement( 'testUrlInc.b', function () {} ); +", + 'testUrlOrder' => " +mw.loader.implement( 'testUrlOrder', function () {} ); +", + 'testUrlOrder.a' => " +mw.loader.implement( 'testUrlOrder.a', function () {} ); +", + 'testUrlOrder.b' => " +mw.loader.implement( 'testUrlOrder.b', function () {} ); +", ]; $response = ''; -// Only support for non-encoded module names, full module names expected +// Does not support the full behaviour of ResourceLoaderContext::expandModuleNames(), +// Only supports dotless module names joined by comma, +// with the exception of the hardcoded cases for testUrl*. if ( isset( $_GET['modules'] ) ) { - $modules = explode( ',', $_GET['modules'] ); + if ( $_GET['modules'] === 'testUrlInc,testUrlIncDump|testUrlInc.a,b' ) { + $modules = [ 'testUrlInc', 'testUrlIncDump', 'testUrlInc.a', 'testUrlInc.b' ]; + } elseif ( $_GET['modules'] === 'testUrlOrder,testUrlOrderDump|testUrlOrder.a,b' ) { + $modules = [ 'testUrlOrder', 'testUrlOrderDump', 'testUrlOrder.a', 'testUrlOrder.b' ]; + } else { + $modules = explode( ',', $_GET['modules'] ); + } foreach ( $modules as $module ) { if ( isset( $moduleImplementations[$module] ) ) { $response .= $moduleImplementations[$module]; + } elseif ( preg_match( '/^test.*Dump$/', $module ) === 1 ) { + $queryModules = $_GET['modules']; + $queryVersion = isset( $_GET['version'] ) ? strval( $_GET['version'] ) : null; + $response .= 'mw.loader.implement( ' . json_encode( $module ) + . ', function ( $, jQuery, require, module ) {' + . 'module.exports.query = { ' + . 'modules: ' . json_encode( $queryModules ) . ',' + . 'version: ' . json_encode( $queryVersion ) + . ' };' + . '} );'; } else { - $response .= Xml::encodeJsCall( 'mw.loader.state', [ $module, 'missing' ], true ); + // Default + $response .= 'mw.loader.state(' . json_encode( $module ) . ', "missing" );' . "\n"; } } } diff --git a/www/wiki/tests/qunit/data/qunitOkCall.js b/www/wiki/tests/qunit/data/qunitOkCall.js deleted file mode 100644 index 3ed5514e..00000000 --- a/www/wiki/tests/qunit/data/qunitOkCall.js +++ /dev/null @@ -1,2 +0,0 @@ -QUnit.start(); -QUnit.assert.ok( true, 'Successfully loaded!' ); diff --git a/www/wiki/tests/qunit/data/testrunner.js b/www/wiki/tests/qunit/data/testrunner.js index e944ef01..06c146c2 100644 --- a/www/wiki/tests/qunit/data/testrunner.js +++ b/www/wiki/tests/qunit/data/testrunner.js @@ -6,18 +6,21 @@ /** * Make a safe copy of localEnv: - * - Creates a copy so that when the same object reference to module hooks is - * used by multipe test hooks, our QUnit.module extension will not wrap the - * callbacks multiple times. Instead, they wrap using a new object. - * - Normalise setup/teardown to avoid having to repeat this in each extension + * - Creates a new object that inherits, instead of modifying the original. + * This prevents recursion in the event that a test suite stores inherits + * hooks object statically and passes it to multiple QUnit.module() calls. + * - Supporting QUnit 1.x 'setup' and 'teardown' hooks * (deprecated in QUnit 1.16, removed in QUnit 2). - * - Strip any other properties. */ function makeSafeEnv( localEnv ) { - return { - beforeEach: localEnv.setup || localEnv.beforeEach, - afterEach: localEnv.teardown || localEnv.afterEach - }; + var wrap = localEnv ? Object.create( localEnv ) : {}; + if ( wrap.setup ) { + wrap.beforeEach = wrap.beforeEach || wrap.setup; + } + if ( wrap.teardown ) { + wrap.afterEach = wrap.afterEach || wrap.teardown; + } + return wrap; } /** @@ -73,13 +76,16 @@ useFakeTimers: false, useFakeServer: false }; - // Extend QUnit.module to provide a Sinon sandbox. + // Extend QUnit.module with: + // - Add support for QUnit 1.x 'setup' and 'teardown' hooks + // - Add a Sinon sandbox to the test context. + // - Add a test fixture to the test context. ( function () { var orgModule = QUnit.module; QUnit.module = function ( name, localEnv, executeNow ) { - var orgBeforeEach, orgAfterEach, orgExecute; + var orgExecute, orgBeforeEach, orgAfterEach; if ( nested ) { - // In a nested module, don't re-run our handlers. + // In a nested module, don't re-add our hooks, QUnit does that already. return orgModule.apply( this, arguments ); } if ( arguments.length === 2 && typeof localEnv === 'function' ) { @@ -98,49 +104,17 @@ }; } - localEnv = localEnv || {}; + localEnv = makeSafeEnv( localEnv ); orgBeforeEach = localEnv.beforeEach; orgAfterEach = localEnv.afterEach; + localEnv.beforeEach = function () { + // Sinon sandbox var config = sinon.getConfig( sinon.config ); config.injectInto = this; sinon.sandbox.create( config ); - if ( orgBeforeEach ) { - return orgBeforeEach.apply( this, arguments ); - } - }; - localEnv.afterEach = function () { - var ret; - if ( orgAfterEach ) { - ret = orgAfterEach.apply( this, arguments ); - } - - this.sandbox.verifyAndRestore(); - return ret; - }; - return orgModule( name, localEnv, executeNow ); - }; - }() ); - - // Extend QUnit.module to provide a fixture element. - ( function () { - var orgModule = QUnit.module; - QUnit.module = function ( name, localEnv, executeNow ) { - var orgBeforeEach, orgAfterEach; - if ( nested ) { - // In a nested module, don't re-run our handlers. - return orgModule.apply( this, arguments ); - } - if ( arguments.length === 2 && typeof localEnv === 'function' ) { - executeNow = localEnv; - localEnv = undefined; - } - - localEnv = localEnv || {}; - orgBeforeEach = localEnv.beforeEach; - orgAfterEach = localEnv.afterEach; - localEnv.beforeEach = function () { + // Fixture element this.fixture = document.createElement( 'div' ); this.fixture.id = 'qunit-fixture'; document.body.appendChild( this.fixture ); @@ -154,23 +128,11 @@ if ( orgAfterEach ) { ret = orgAfterEach.apply( this, arguments ); } - + this.sandbox.verifyAndRestore(); this.fixture.parentNode.removeChild( this.fixture ); return ret; }; - return orgModule( name, localEnv, executeNow ); - }; - }() ); - // Extend QUnit.module to normalise localEnv. - // NOTE: This MUST be the last QUnit.module extension so that the above extensions - // may safely modify the object and assume beforeEach/afterEach. - ( function () { - var orgModule = QUnit.module; - QUnit.module = function ( name, localEnv, executeNow ) { - if ( typeof localEnv === 'object' ) { - localEnv = makeSafeEnv( localEnv ); - } return orgModule( name, localEnv, executeNow ); }; }() ); @@ -239,98 +201,101 @@ } return function ( orgEnv ) { - var localEnv = orgEnv ? makeSafeEnv( orgEnv ) : {}; - // MediaWiki env testing - localEnv.config = orgEnv && orgEnv.config || {}; - localEnv.messages = orgEnv && orgEnv.messages || {}; - - return { - beforeEach: function () { - // Greetings, mock environment! - mw.config = new MwMap(); - mw.config.set( freshConfigCopy( localEnv.config ) ); - mw.messages = new MwMap(); - mw.messages.set( freshMessagesCopy( localEnv.messages ) ); - // Update reference to mw.messages - mw.jqueryMsg.setParserDefaults( { - messages: mw.messages - } ); - - this.suppressWarnings = suppressWarnings; - this.restoreWarnings = restoreWarnings; + var localEnv, orgBeforeEach, orgAfterEach; - // Start tracking ajax requests - $( document ).on( 'ajaxSend', trackAjax ); - - if ( localEnv.beforeEach ) { - return localEnv.beforeEach.apply( this, arguments ); - } - }, + localEnv = makeSafeEnv( orgEnv ); + // MediaWiki env testing + localEnv.config = localEnv.config || {}; + localEnv.messages = localEnv.messages || {}; - afterEach: function () { - var timers, pending, $activeLen, ret; + orgBeforeEach = localEnv.beforeEach; + orgAfterEach = localEnv.afterEach; - if ( localEnv.afterEach ) { - ret = localEnv.afterEach.apply( this, arguments ); - } + localEnv.beforeEach = function () { + // Greetings, mock environment! + mw.config = new MwMap(); + mw.config.set( freshConfigCopy( localEnv.config ) ); + mw.messages = new MwMap(); + mw.messages.set( freshMessagesCopy( localEnv.messages ) ); + // Update reference to mw.messages + mw.jqueryMsg.setParserDefaults( { + messages: mw.messages + } ); + + this.suppressWarnings = suppressWarnings; + this.restoreWarnings = restoreWarnings; + + // Start tracking ajax requests + $( document ).on( 'ajaxSend', trackAjax ); - // Stop tracking ajax requests - $( document ).off( 'ajaxSend', trackAjax ); + if ( orgBeforeEach ) { + return orgBeforeEach.apply( this, arguments ); + } + }; + localEnv.afterEach = function () { + var timers, pending, $activeLen, ret; - // As a convenience feature, automatically restore warnings if they're - // still suppressed by the end of the test. - restoreWarnings(); + if ( orgAfterEach ) { + ret = orgAfterEach.apply( this, arguments ); + } - // Farewell, mock environment! - mw.config = liveConfig; - mw.messages = liveMessages; - // Restore reference to mw.messages - mw.jqueryMsg.setParserDefaults( { - messages: liveMessages + // Stop tracking ajax requests + $( document ).off( 'ajaxSend', trackAjax ); + + // As a convenience feature, automatically restore warnings if they're + // still suppressed by the end of the test. + restoreWarnings(); + + // Farewell, mock environment! + mw.config = liveConfig; + mw.messages = liveMessages; + // Restore reference to mw.messages + mw.jqueryMsg.setParserDefaults( { + messages: liveMessages + } ); + + // Tests should use fake timers or wait for animations to complete + // Check for incomplete animations/requests/etc and throw if there are any. + if ( $.timers && $.timers.length !== 0 ) { + timers = $.timers.length; + $.each( $.timers, function ( i, timer ) { + var node = timer.elem; + mw.log.warn( 'Unfinished animation #' + i + ' in ' + timer.queue + ' queue on ' + + mw.html.element( node.nodeName.toLowerCase(), $( node ).getAttrs() ) + ); } ); + // Force animations to stop to give the next test a clean start + $.timers = []; + $.fx.stop(); - // Tests should use fake timers or wait for animations to complete - // Check for incomplete animations/requests/etc and throw if there are any. - if ( $.timers && $.timers.length !== 0 ) { - timers = $.timers.length; - $.each( $.timers, function ( i, timer ) { - var node = timer.elem; - mw.log.warn( 'Unfinished animation #' + i + ' in ' + timer.queue + ' queue on ' + - mw.html.element( node.nodeName.toLowerCase(), $( node ).getAttrs() ) - ); - } ); - // Force animations to stop to give the next test a clean start - $.timers = []; - $.fx.stop(); - - throw new Error( 'Unfinished animations: ' + timers ); - } + throw new Error( 'Unfinished animations: ' + timers ); + } - // Test should use fake XHR, wait for requests, or call abort() - $activeLen = $.active; - if ( $activeLen !== undefined && $activeLen !== 0 ) { - pending = $.grep( ajaxRequests, function ( ajax ) { - return ajax.xhr.state() === 'pending'; - } ); - if ( pending.length !== $activeLen ) { - mw.log.warn( 'Pending requests does not match jQuery.active count' ); - } - // Force requests to stop to give the next test a clean start - $.each( ajaxRequests, function ( i, ajax ) { - mw.log.warn( - 'AJAX request #' + i + ' (state: ' + ajax.xhr.state() + ')', - ajax.options - ); - ajax.xhr.abort(); - } ); - ajaxRequests = []; - - throw new Error( 'Pending AJAX requests: ' + pending.length + ' (active: ' + $activeLen + ')' ); + // Test should use fake XHR, wait for requests, or call abort() + $activeLen = $.active; + if ( $activeLen !== undefined && $activeLen !== 0 ) { + pending = ajaxRequests.filter( function ( ajax ) { + return ajax.xhr.state() === 'pending'; + } ); + if ( pending.length !== $activeLen ) { + mw.log.warn( 'Pending requests does not match jQuery.active count' ); } + // Force requests to stop to give the next test a clean start + ajaxRequests.forEach( function ( ajax, i ) { + mw.log.warn( + 'AJAX request #' + i + ' (state: ' + ajax.xhr.state() + ')', + ajax.options + ); + ajax.xhr.abort(); + } ); + ajaxRequests = []; - return ret; + throw new Error( 'Pending AJAX requests: ' + pending.length + ' (active: ' + $activeLen + ')' ); } + + return ret; }; + return localEnv; }; }() ); @@ -657,4 +622,31 @@ } ); } ); + QUnit.module( 'testrunner-hooks-outer', function () { + var beforeHookWasExecuted = false, + afterHookWasExecuted = false; + QUnit.module( 'testrunner-hooks', { + before: function () { + beforeHookWasExecuted = true; + + // This way we can be sure that module `testrunner-hook-after` will always + // be executed after module `testrunner-hooks` + QUnit.module( 'testrunner-hooks-after' ); + QUnit.test( + '`after` hook for module `testrunner-hooks` was executed', + function ( assert ) { + assert.ok( afterHookWasExecuted ); + } + ); + }, + after: function () { + afterHookWasExecuted = true; + } + } ); + + QUnit.test( '`before` hook was executed', function ( assert ) { + assert.ok( beforeHookWasExecuted ); + } ); + } ); + }( jQuery, mediaWiki, QUnit ) ); diff --git a/www/wiki/tests/qunit/suites/resources/jquery/jquery.autoEllipsis.test.js b/www/wiki/tests/qunit/suites/resources/jquery/jquery.autoEllipsis.test.js deleted file mode 100644 index c3521ba8..00000000 --- a/www/wiki/tests/qunit/suites/resources/jquery/jquery.autoEllipsis.test.js +++ /dev/null @@ -1,53 +0,0 @@ -( function ( $ ) { - - QUnit.module( 'jquery.autoEllipsis', QUnit.newMwEnvironment() ); - - function createWrappedDiv( text, width ) { - var $wrapper = $( '<div>' ).css( 'width', width ), - $div = $( '<div>' ).text( text ); - $wrapper.append( $div ); - return $wrapper; - } - - function findDivergenceIndex( a, b ) { - var i = 0; - while ( i < a.length && i < b.length && a[ i ] === b[ i ] ) { - i++; - } - return i; - } - - QUnit.test( 'Position right', function ( assert ) { - // We need this thing to be visible, so append it to the DOM - var $span, spanText, d, spanTextNew, - origText = 'This is a really long random string and there is no way it fits in 100 pixels.', - $wrapper = createWrappedDiv( origText, '100px' ); - - $( '#qunit-fixture' ).append( $wrapper ); - $wrapper.autoEllipsis( { position: 'right' } ); - - // Verify that, and only one, span element was created - $span = $wrapper.find( '> span' ); - assert.strictEqual( $span.length, 1, 'autoEllipsis wrapped the contents in a span element' ); - - // Check that the text fits by turning on word wrapping - $span.css( 'whiteSpace', 'nowrap' ); - assert.ltOrEq( - $span.width(), - $span.parent().width(), - 'Text fits (making the span "white-space: nowrap" does not make it wider than its parent)' - ); - - // Add two characters using scary black magic - spanText = $span.text(); - d = findDivergenceIndex( origText, spanText ); - spanTextNew = spanText.slice( 0, d ) + origText[ d ] + origText[ d ] + '...'; - - assert.gt( spanTextNew.length, spanText.length, 'Verify that the new span-length is indeed greater' ); - - // Put this text in the span and verify it doesn't fit - $span.text( spanTextNew ); - assert.gt( $span.width(), $span.parent().width(), 'Fit is maximal (adding two characters makes it not fit any more)' ); - } ); - -}( jQuery ) ); diff --git a/www/wiki/tests/qunit/suites/resources/jquery/jquery.byteLength.test.js b/www/wiki/tests/qunit/suites/resources/jquery/jquery.byteLength.test.js deleted file mode 100644 index 558e6416..00000000 --- a/www/wiki/tests/qunit/suites/resources/jquery/jquery.byteLength.test.js +++ /dev/null @@ -1,37 +0,0 @@ -( function ( $ ) { - QUnit.module( 'jquery.byteLength', QUnit.newMwEnvironment() ); - - QUnit.test( 'Simple text', function ( assert ) { - var azLc = 'abcdefghijklmnopqrstuvwxyz', - azUc = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', - num = '0123456789', - x = '*', - space = ' '; - - assert.equal( $.byteLength( azLc ), 26, 'Lowercase a-z' ); - assert.equal( $.byteLength( azUc ), 26, 'Uppercase A-Z' ); - assert.equal( $.byteLength( num ), 10, 'Numbers 0-9' ); - assert.equal( $.byteLength( x ), 1, 'An asterisk' ); - assert.equal( $.byteLength( space ), 3, '3 spaces' ); - - } ); - - QUnit.test( 'Special text', function ( assert ) { - // https://en.wikipedia.org/wiki/UTF-8 - var u0024 = '$', - // Cent symbol - u00A2 = '\u00A2', - // Euro symbol - u20AC = '\u20AC', - // Character \U00024B62 (Han script) can't be represented in javascript as a single - // code point, instead it is composed as a surrogate pair of two separate code units. - // http://codepoints.net/U+24B62 - // http://www.fileformat.info/info/unicode/char/24B62/index.htm - u024B62 = '\uD852\uDF62'; - - assert.strictEqual( $.byteLength( u0024 ), 1, 'U+0024' ); - assert.strictEqual( $.byteLength( u00A2 ), 2, 'U+00A2' ); - assert.strictEqual( $.byteLength( u20AC ), 3, 'U+20AC' ); - assert.strictEqual( $.byteLength( u024B62 ), 4, 'U+024B62 (surrogate pair: \\uD852\\uDF62)' ); - } ); -}( jQuery ) ); diff --git a/www/wiki/tests/qunit/suites/resources/jquery/jquery.highlightText.test.js b/www/wiki/tests/qunit/suites/resources/jquery/jquery.highlightText.test.js index 0dac22e9..277ba3f2 100644 --- a/www/wiki/tests/qunit/suites/resources/jquery/jquery.highlightText.test.js +++ b/www/wiki/tests/qunit/suites/resources/jquery/jquery.highlightText.test.js @@ -222,7 +222,7 @@ } ]; - $.each( cases, function ( i, item ) { + cases.forEach( function ( item ) { $fixture = $( '<p>' ).text( item.text ).highlightText( item.highlight ); assert.equal( $fixture.html(), diff --git a/www/wiki/tests/qunit/suites/resources/jquery/jquery.byteLimit.test.js b/www/wiki/tests/qunit/suites/resources/jquery/jquery.lengthLimit.test.js index 8555a7e4..7117d1f4 100644 --- a/www/wiki/tests/qunit/suites/resources/jquery/jquery.byteLimit.test.js +++ b/www/wiki/tests/qunit/suites/resources/jquery/jquery.lengthLimit.test.js @@ -1,7 +1,7 @@ ( function ( $, mw ) { - var simpleSample, U_20AC, mbSample; + var simpleSample, U_20AC, poop, mbSample; - QUnit.module( 'jquery.byteLimit', QUnit.newMwEnvironment() ); + QUnit.module( 'jquery.lengthLimit', QUnit.newMwEnvironment() ); // Simple sample (20 chars, 20 bytes) simpleSample = '12345678901234567890'; @@ -9,6 +9,9 @@ // 3 bytes (euro-symbol) U_20AC = '\u20AC'; + // Outside of the BMP (pile of poo emoji) + poop = '\uD83D\uDCA9'; // "💩" + // Multi-byte sample (22 chars, 26 bytes) mbSample = '1234567890' + U_20AC + '1234567890' + U_20AC; @@ -110,6 +113,14 @@ } ); byteLimitTest( { + description: 'Limit using a custom value (multibyte, outside BMP)', + $input: $( '<input>' ).attr( 'type', 'text' ) + .byteLimit( 3 ), + sample: poop, + expected: '' + } ); + + byteLimitTest( { description: 'Limit using a custom value (multibyte) overlapping a byte', $input: $( '<input>' ).attr( 'type', 'text' ) .byteLimit( 12 ), @@ -176,8 +187,6 @@ return 'prefix' + text; } ), sample: simpleSample, - hasLimit: true, - limit: 6, // 'prefix' length expected: '' } ); @@ -245,4 +254,33 @@ assert.strictEqual( $el.val(), 'abc', 'Trim from the insertion point (at 1), not the end' ); } ); + + QUnit.test( 'Do not cut up false matching substrings in emoji insertions', function ( assert ) { + var $el, + oldVal = '\uD83D\uDCA9\uD83D\uDCA9', // "💩💩" + newVal = '\uD83D\uDCA9\uD83D\uDCB9\uD83E\uDCA9\uD83D\uDCA9', // "💩💹🢩💩" + expected = '\uD83D\uDCA9\uD83D\uDCB9\uD83D\uDCA9'; // "💩💹💩" + + // Possible bad results: + // * With no surrogate support: + // '\uD83D\uDCA9\uD83D\uDCB9\uD83E\uDCA9' "💩💹🢩" + // * With correct trimming but bad detection of inserted text: + // '\uD83D\uDCA9\uD83D\uDCB9\uDCA9' "💩💹�" + + $el = $( '<input>' ).attr( 'type', 'text' ) + .appendTo( '#qunit-fixture' ) + .byteLimit( 12 ) + .val( oldVal ).trigger( 'change' ) + .val( newVal ).trigger( 'change' ); + + assert.strictEqual( $el.val(), expected, 'Pasted emoji correctly trimmed at the end' ); + } ); + + byteLimitTest( { + description: 'Unpaired surrogates do not crash', + $input: $( '<input>' ).attr( 'type', 'text' ).byteLimit( 4 ), + sample: '\uD800\uD800\uDFFF', + expected: '\uD800' + } ); + }( jQuery, mediaWiki ) ); diff --git a/www/wiki/tests/qunit/suites/resources/jquery/jquery.makeCollapsible.test.js b/www/wiki/tests/qunit/suites/resources/jquery/jquery.makeCollapsible.test.js index 0c91e43f..d51dc373 100644 --- a/www/wiki/tests/qunit/suites/resources/jquery/jquery.makeCollapsible.test.js +++ b/www/wiki/tests/qunit/suites/resources/jquery/jquery.makeCollapsible.test.js @@ -1,4 +1,4 @@ -( function ( mw, $ ) { +( function ( $ ) { var loremIpsum = 'Lorem ipsum dolor sit amet, consectetuer adipiscing elit.'; QUnit.module( 'jquery.makeCollapsible', QUnit.newMwEnvironment() ); @@ -374,4 +374,4 @@ $clone.find( '.mw-collapsible-toggle a' ).trigger( 'click' ); } ); -}( mediaWiki, jQuery ) ); +}( jQuery ) ); diff --git a/www/wiki/tests/qunit/suites/resources/jquery/jquery.mwExtension.test.js b/www/wiki/tests/qunit/suites/resources/jquery/jquery.mwExtension.test.js deleted file mode 100644 index 029edd55..00000000 --- a/www/wiki/tests/qunit/suites/resources/jquery/jquery.mwExtension.test.js +++ /dev/null @@ -1,64 +0,0 @@ -( function ( $ ) { - QUnit.module( 'jquery.mwExtension', QUnit.newMwEnvironment( { - // This entire module is deprecated. - // Surpress deprecation warnings in test output. - setup: function () { - this.suppressWarnings(); - }, - teardown: function () { - this.restoreWarnings(); - } - } ) ); - - QUnit.test( 'String functions', 7, function ( assert ) { - assert.equal( $.trimLeft( ' foo bar ' ), 'foo bar ', 'trimLeft' ); - assert.equal( $.trimRight( ' foo bar ' ), ' foo bar', 'trimRight' ); - assert.equal( $.ucFirst( 'foo' ), 'Foo', 'ucFirst' ); - - assert.equal( $.escapeRE( '<!-- ([{+mW+}]) $^|?>' ), - '<!\\-\\- \\(\\[\\{\\+mW\\+\\}\\]\\) \\$\\^\\|\\?>', 'escapeRE - Escape specials' ); - assert.equal( $.escapeRE( 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' ), - 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'escapeRE - Leave uppercase alone' ); - assert.equal( $.escapeRE( 'abcdefghijklmnopqrstuvwxyz' ), - 'abcdefghijklmnopqrstuvwxyz', 'escapeRE - Leave lowercase alone' ); - assert.equal( $.escapeRE( '0123456789' ), '0123456789', 'escapeRE - Leave numbers alone' ); - } ); - - QUnit.test( 'isDomElement', 6, function ( assert ) { - assert.strictEqual( $.isDomElement( document.createElement( 'div' ) ), true, - 'isDomElement: HTMLElement' ); - assert.strictEqual( $.isDomElement( document.createTextNode( '' ) ), true, - 'isDomElement: TextNode' ); - assert.strictEqual( $.isDomElement( null ), false, - 'isDomElement: null' ); - assert.strictEqual( $.isDomElement( document.getElementsByTagName( 'div' ) ), false, - 'isDomElement: NodeList' ); - assert.strictEqual( $.isDomElement( $( 'div' ) ), false, - 'isDomElement: jQuery' ); - assert.strictEqual( $.isDomElement( { foo: 1 } ), false, - 'isDomElement: Plain Object' ); - } ); - - QUnit.test( 'isEmpty', 7, function ( assert ) { - assert.strictEqual( $.isEmpty( 'string' ), false, 'isEmpty: "string"' ); - assert.strictEqual( $.isEmpty( '0' ), true, 'isEmpty: "0"' ); - assert.strictEqual( $.isEmpty( '' ), true, 'isEmpty: ""' ); - assert.strictEqual( $.isEmpty( 1 ), false, 'isEmpty: 1' ); - assert.strictEqual( $.isEmpty( [] ), true, 'isEmpty: []' ); - assert.strictEqual( $.isEmpty( {} ), true, 'isEmpty: {}' ); - - // Documented behavior - assert.strictEqual( $.isEmpty( { length: 0 } ), true, 'isEmpty: { length: 0 }' ); - } ); - - QUnit.test( 'Comparison functions', 5, function ( assert ) { - assert.ok( $.compareArray( [ 0, 'a', [], [ 2, 'b' ] ], [ 0, 'a', [], [ 2, 'b' ] ] ), - 'compareArray: Two deep arrays that are excactly the same' ); - assert.ok( !$.compareArray( [ 1 ], [ 2 ] ), 'compareArray: Two different arrays (false)' ); - - assert.ok( $.compareObject( {}, {} ), 'compareObject: Two empty objects' ); - assert.ok( $.compareObject( { foo: 1 }, { foo: 1 } ), 'compareObject: Two the same objects' ); - assert.ok( !$.compareObject( { bar: true }, { baz: false } ), - 'compareObject: Two different objects (false)' ); - } ); -}( jQuery ) ); diff --git a/www/wiki/tests/qunit/suites/resources/jquery/jquery.placeholder.test.js b/www/wiki/tests/qunit/suites/resources/jquery/jquery.placeholder.test.js deleted file mode 100644 index 5d0ddebb..00000000 --- a/www/wiki/tests/qunit/suites/resources/jquery/jquery.placeholder.test.js +++ /dev/null @@ -1,145 +0,0 @@ -( function ( $ ) { - - QUnit.module( 'jquery.placeholder', QUnit.newMwEnvironment() ); - - QUnit.test( 'caches results of feature tests', 2, function ( assert ) { - assert.strictEqual( typeof $.fn.placeholder.input, 'boolean', '$.fn.placeholder.input' ); - assert.strictEqual( typeof $.fn.placeholder.textarea, 'boolean', '$.fn.placeholder.textarea' ); - } ); - - if ( $.fn.placeholder.input && $.fn.placeholder.textarea ) { - return; - } - - var html = '<form>' + - '<input id="input-type-search" type="search" placeholder="Search this site...">' + - '<input id="input-type-text" type="text" placeholder="e.g. John Doe">' + - '<input id="input-type-email" type="email" placeholder="e.g. address@example.ext">' + - '<input id="input-type-url" type="url" placeholder="e.g. http://mathiasbynens.be/">' + - '<input id="input-type-tel" type="tel" placeholder="e.g. +32 472 77 69 88">' + - '<input id="input-type-password" type="password" placeholder="e.g. hunter2">' + - '<textarea id="textarea" name="message" placeholder="Your message goes here"></textarea>' + - '</form>', - testElement = function ( $el, assert ) { - - var el = $el[ 0 ], - placeholder = el.getAttribute( 'placeholder' ); - - assert.strictEqual( $el.placeholder(), $el, 'should be chainable' ); - - assert.strictEqual( el.value, placeholder, 'should set `placeholder` text as `value`' ); - assert.strictEqual( $el.prop( 'value' ), '', 'propHooks works properly' ); - assert.strictEqual( $el.val(), '', 'valHooks works properly' ); - assert.ok( $el.hasClass( 'placeholder' ), 'should have `placeholder` class' ); - - // test on focus - $el.focus(); - assert.strictEqual( el.value, '', '`value` should be the empty string on focus' ); - assert.strictEqual( $el.prop( 'value' ), '', 'propHooks works properly' ); - assert.strictEqual( $el.val(), '', 'valHooks works properly' ); - assert.ok( !$el.hasClass( 'placeholder' ), 'should not have `placeholder` class on focus' ); - - // and unfocus (blur) again - $el.blur(); - - assert.strictEqual( el.value, placeholder, 'should set `placeholder` text as `value`' ); - assert.strictEqual( $el.prop( 'value' ), '', 'propHooks works properly' ); - assert.strictEqual( $el.val(), '', 'valHooks works properly' ); - assert.ok( $el.hasClass( 'placeholder' ), 'should have `placeholder` class' ); - - // change the value - $el.val( 'lorem ipsum' ); - assert.strictEqual( $el.prop( 'value' ), 'lorem ipsum', '`$el.val(string)` should change the `value` property' ); - assert.strictEqual( el.value, 'lorem ipsum', '`$el.val(string)` should change the `value` attribute' ); - assert.ok( !$el.hasClass( 'placeholder' ), '`$el.val(string)` should remove `placeholder` class' ); - - // and clear it again - $el.val( '' ); - assert.strictEqual( $el.prop( 'value' ), '', '`$el.val("")` should change the `value` property' ); - assert.strictEqual( el.value, placeholder, '`$el.val("")` should change the `value` attribute' ); - assert.ok( $el.hasClass( 'placeholder' ), '`$el.val("")` should re-enable `placeholder` class' ); - - // make sure the placeholder property works as expected. - assert.strictEqual( $el.prop( 'placeholder' ), placeholder, '$el.prop(`placeholder`) should return the placeholder value' ); - $el.placeholder( 'new placeholder' ); - assert.strictEqual( el.getAttribute( 'placeholder' ), 'new placeholder', '$el.placeholder(<string>) should set the placeholder value' ); - assert.strictEqual( el.value, 'new placeholder', '$el.placeholder(<string>) should update the displayed placeholder value' ); - $el.placeholder( placeholder ); - }; - - QUnit.test( 'emulates placeholder for <input type=text>', 22, function ( assert ) { - $( '<div>' ).html( html ).appendTo( $( '#qunit-fixture' ) ); - testElement( $( '#input-type-text' ), assert ); - } ); - - QUnit.test( 'emulates placeholder for <input type=search>', 22, function ( assert ) { - $( '<div>' ).html( html ).appendTo( $( '#qunit-fixture' ) ); - testElement( $( '#input-type-search' ), assert ); - } ); - - QUnit.test( 'emulates placeholder for <input type=email>', 22, function ( assert ) { - $( '<div>' ).html( html ).appendTo( $( '#qunit-fixture' ) ); - testElement( $( '#input-type-email' ), assert ); - } ); - - QUnit.test( 'emulates placeholder for <input type=url>', 22, function ( assert ) { - $( '<div>' ).html( html ).appendTo( $( '#qunit-fixture' ) ); - testElement( $( '#input-type-url' ), assert ); - } ); - - QUnit.test( 'emulates placeholder for <input type=tel>', 22, function ( assert ) { - $( '<div>' ).html( html ).appendTo( $( '#qunit-fixture' ) ); - testElement( $( '#input-type-tel' ), assert ); - } ); - - QUnit.test( 'emulates placeholder for <input type=password>', 13, function ( assert ) { - $( '<div>' ).html( html ).appendTo( $( '#qunit-fixture' ) ); - - var selector = '#input-type-password', - $el = $( selector ), - el = $el[ 0 ], - placeholder = el.getAttribute( 'placeholder' ); - - assert.strictEqual( $el.placeholder(), $el, 'should be chainable' ); - - // Re-select the element, as it gets replaced by another one in some browsers - $el = $( selector ); - el = $el[ 0 ]; - - assert.strictEqual( el.value, placeholder, 'should set `placeholder` text as `value`' ); - assert.strictEqual( $el.prop( 'value' ), '', 'propHooks works properly' ); - assert.strictEqual( $el.val(), '', 'valHooks works properly' ); - assert.ok( $el.hasClass( 'placeholder' ), 'should have `placeholder` class' ); - - // test on focus - $el.focus(); - - // Re-select the element, as it gets replaced by another one in some browsers - $el = $( selector ); - el = $el[ 0 ]; - - assert.strictEqual( el.value, '', '`value` should be the empty string on focus' ); - assert.strictEqual( $el.prop( 'value' ), '', 'propHooks works properly' ); - assert.strictEqual( $el.val(), '', 'valHooks works properly' ); - assert.ok( !$el.hasClass( 'placeholder' ), 'should not have `placeholder` class on focus' ); - - // and unfocus (blur) again - $el.blur(); - - // Re-select the element, as it gets replaced by another one in some browsers - $el = $( selector ); - el = $el[ 0 ]; - - assert.strictEqual( el.value, placeholder, 'should set `placeholder` text as `value`' ); - assert.strictEqual( $el.prop( 'value' ), '', 'propHooks works properly' ); - assert.strictEqual( $el.val(), '', 'valHooks works properly' ); - assert.ok( $el.hasClass( 'placeholder' ), 'should have `placeholder` class' ); - - } ); - - QUnit.test( 'emulates placeholder for <textarea></textarea>', 22, function ( assert ) { - $( '<div>' ).html( html ).appendTo( $( '#qunit-fixture' ) ); - testElement( $( '#textarea' ), assert ); - } ); - -}( jQuery ) ); diff --git a/www/wiki/tests/qunit/suites/resources/jquery/jquery.tablesorter.parsers.test.js b/www/wiki/tests/qunit/suites/resources/jquery/jquery.tablesorter.parsers.test.js index 01589c33..2865cbba 100644 --- a/www/wiki/tests/qunit/suites/resources/jquery/jquery.tablesorter.parsers.test.js +++ b/www/wiki/tests/qunit/suites/resources/jquery/jquery.tablesorter.parsers.test.js @@ -62,7 +62,7 @@ } parser = $.tablesorter.getParser( parserId ); - $.each( data, function ( index, testcase ) { + data.forEach( function ( testcase ) { extractedR = parser.is( testcase[ 0 ] ); extractedF = parser.format( testcase[ 0 ] ); diff --git a/www/wiki/tests/qunit/suites/resources/jquery/jquery.tablesorter.test.js b/www/wiki/tests/qunit/suites/resources/jquery/jquery.tablesorter.test.js index 27d7e8da..23ef26f6 100644 --- a/www/wiki/tests/qunit/suites/resources/jquery/jquery.tablesorter.test.js +++ b/www/wiki/tests/qunit/suites/resources/jquery/jquery.tablesorter.test.js @@ -130,7 +130,7 @@ [ '$ 1.50' ], [ '$ 3.00' ], [ '$3.50' ], - // Comma's sort after dots + // Commas sort after dots // Not intentional but test to detect changes [ '€ 2,99' ] ], @@ -238,7 +238,7 @@ $tbody = $table.find( 'tbody' ), $tr = $( '<tr>' ); - $.each( header, function ( i, str ) { + header.forEach( function ( str ) { var $th = $( '<th>' ); $th.text( str ).appendTo( $tr ); } ); @@ -247,7 +247,7 @@ for ( i = 0; i < data.length; i++ ) { $tr = $( '<tr>' ); // eslint-disable-next-line no-loop-func - $.each( data[ i ], function ( j, str ) { + data[ i ].forEach( function ( str ) { var $td = $( '<td>' ); $td.text( str ).appendTo( $tr ); } ); diff --git a/www/wiki/tests/qunit/suites/resources/jquery/jquery.textSelection.test.js b/www/wiki/tests/qunit/suites/resources/jquery/jquery.textSelection.test.js index 5b3c2ed0..32cda7eb 100644 --- a/www/wiki/tests/qunit/suites/resources/jquery/jquery.textSelection.test.js +++ b/www/wiki/tests/qunit/suites/resources/jquery/jquery.textSelection.test.js @@ -224,7 +224,7 @@ function among( actual, expected, message ) { if ( Array.isArray( expected ) ) { - assert.ok( $.inArray( actual, expected ) !== -1, message + ' (got ' + actual + '; expected one of ' + expected.join( ', ' ) + ')' ); + assert.ok( expected.indexOf( actual ) !== -1, message + ' (got ' + actual + '; expected one of ' + expected.join( ', ' ) + ')' ); } else { assert.equal( actual, expected, message ); } diff --git a/www/wiki/tests/qunit/suites/resources/mediawiki.api/mediawiki.api.category.test.js b/www/wiki/tests/qunit/suites/resources/mediawiki.api/mediawiki.api.category.test.js index 8ad12900..50fa6d15 100644 --- a/www/wiki/tests/qunit/suites/resources/mediawiki.api/mediawiki.api.category.test.js +++ b/www/wiki/tests/qunit/suites/resources/mediawiki.api/mediawiki.api.category.test.js @@ -22,4 +22,93 @@ ); } ); } ); + + QUnit.test( '.isCategory("")', function ( assert ) { + this.server.respondWith( /titles=$/, [ + 200, + { 'Content-Type': 'application/json' }, + '{"batchcomplete":true}' + ] ); + return new mw.Api().isCategory( '' ).then( function ( response ) { + assert.equal( response, false ); + } ); + } ); + + QUnit.test( '.isCategory("#")', function ( assert ) { + this.server.respondWith( /titles=%23$/, [ + 200, + { 'Content-Type': 'application/json' }, + '{"batchcomplete":true,"query":{"normalized":[{"fromencoded":false,"from":"#","to":""}]}}' + ] ); + return new mw.Api().isCategory( '#' ).then( function ( response ) { + assert.equal( response, false ); + } ); + } ); + + QUnit.test( '.isCategory("mw:")', function ( assert ) { + this.server.respondWith( /titles=mw%3A$/, [ + 200, + { 'Content-Type': 'application/json' }, + '{"batchcomplete":true,"query":{"interwiki":[{"title":"mw:","iw":"mw"}]}}' + ] ); + return new mw.Api().isCategory( 'mw:' ).then( function ( response ) { + assert.equal( response, false ); + } ); + } ); + + QUnit.test( '.isCategory("|")', function ( assert ) { + this.server.respondWith( /titles=%1F%7C$/, [ + 200, + { 'Content-Type': 'application/json' }, + '{"batchcomplete":true,"query":{"pages":[{"title":"|","invalidreason":"The requested page title contains invalid characters: \\"|\\".","invalid":true}]}}' + ] ); + return new mw.Api().isCategory( '|' ).then( function ( response ) { + assert.equal( response, false ); + } ); + } ); + + QUnit.test( '.getCategories("")', function ( assert ) { + this.server.respondWith( /titles=$/, [ + 200, + { 'Content-Type': 'application/json' }, + '{"batchcomplete":true}' + ] ); + return new mw.Api().getCategories( '' ).then( function ( response ) { + assert.equal( response, false ); + } ); + } ); + + QUnit.test( '.getCategories("#")', function ( assert ) { + this.server.respondWith( /titles=%23$/, [ + 200, + { 'Content-Type': 'application/json' }, + '{"batchcomplete":true,"query":{"normalized":[{"fromencoded":false,"from":"#","to":""}]}}' + ] ); + return new mw.Api().getCategories( '#' ).then( function ( response ) { + assert.equal( response, false ); + } ); + } ); + + QUnit.test( '.getCategories("mw:")', function ( assert ) { + this.server.respondWith( /titles=mw%3A$/, [ + 200, + { 'Content-Type': 'application/json' }, + '{"batchcomplete":true,"query":{"interwiki":[{"title":"mw:","iw":"mw"}]}}' + ] ); + return new mw.Api().getCategories( 'mw:' ).then( function ( response ) { + assert.equal( response, false ); + } ); + } ); + + QUnit.test( '.getCategories("|")', function ( assert ) { + this.server.respondWith( /titles=%1F%7C$/, [ + 200, + { 'Content-Type': 'application/json' }, + '{"batchcomplete":true,"query":{"pages":[{"title":"|","invalidreason":"The requested page title contains invalid characters: \\"|\\".","invalid":true}]}}' + ] ); + return new mw.Api().getCategories( '|' ).then( function ( response ) { + assert.equal( response, false ); + } ); + } ); + }( mediaWiki ) ); diff --git a/www/wiki/tests/qunit/suites/resources/mediawiki.api/mediawiki.api.edit.test.js b/www/wiki/tests/qunit/suites/resources/mediawiki.api/mediawiki.api.edit.test.js index 13d7dcc2..4ce7c5db 100644 --- a/www/wiki/tests/qunit/suites/resources/mediawiki.api/mediawiki.api.edit.test.js +++ b/www/wiki/tests/qunit/suites/resources/mediawiki.api/mediawiki.api.edit.test.js @@ -47,6 +47,47 @@ } ); } ); + QUnit.test( 'edit( mw.Title, transform String )', function ( assert ) { + this.server.respond( function ( req ) { + if ( /query.+titles=Sandbox/.test( req.url ) ) { + req.respond( 200, { 'Content-Type': 'application/json' }, JSON.stringify( { + curtimestamp: '2016-01-02T12:00:00Z', + query: { + pages: [ { + pageid: 1, + ns: 0, + title: 'Sandbox', + revisions: [ { + timestamp: '2016-01-01T12:00:00Z', + contentformat: 'text/x-wiki', + contentmodel: 'wikitext', + content: 'Sand.' + } ] + } ] + } + } ) ); + } + if ( /edit.+basetimestamp=2016-01-01.+starttimestamp=2016-01-02.+text=Box%2E/.test( req.requestBody ) ) { + req.respond( 200, { 'Content-Type': 'application/json' }, JSON.stringify( { + edit: { + result: 'Success', + oldrevid: 11, + newrevid: 13, + newtimestamp: '2016-01-03T12:00:00Z' + } + } ) ); + } + } ); + + return new mw.Api() + .edit( new mw.Title( 'Sandbox' ), function ( revision ) { + return revision.content.replace( 'Sand', 'Box' ); + } ) + .then( function ( edit ) { + assert.equal( edit.newrevid, 13 ); + } ); + } ); + QUnit.test( 'edit( title, transform Promise )', function ( assert ) { this.server.respond( function ( req ) { if ( /query.+titles=Async/.test( req.url ) ) { @@ -129,6 +170,32 @@ } ); } ); + QUnit.test( 'edit( invalid-title, transform String )', function ( assert ) { + this.server.respond( function ( req ) { + if ( /query.+titles=%1F%7C/.test( req.url ) ) { + req.respond( 200, { 'Content-Type': 'application/json' }, JSON.stringify( { + query: { + pages: [ { + title: '|', + invalidreason: 'The requested page title contains invalid characters: "|".', + invalid: true + } ] + } + } ) ); + } + } ); + + return new mw.Api() + .edit( '|', function ( revision ) { + return revision.content.replace( 'Sand', 'Box' ); + } ) + .then( function () { + return $.Deferred().reject( 'Unexpected success' ); + }, function ( reason ) { + assert.equal( reason, 'invalidtitle' ); + } ); + } ); + QUnit.test( 'create( title, content )', function ( assert ) { this.server.respond( function ( req ) { if ( /edit.+text=Sand/.test( req.requestBody ) ) { diff --git a/www/wiki/tests/qunit/suites/resources/mediawiki.api/mediawiki.api.options.test.js b/www/wiki/tests/qunit/suites/resources/mediawiki.api/mediawiki.api.options.test.js index 4ee8038e..997a42c8 100644 --- a/www/wiki/tests/qunit/suites/resources/mediawiki.api/mediawiki.api.options.test.js +++ b/www/wiki/tests/qunit/suites/resources/mediawiki.api/mediawiki.api.options.test.js @@ -30,7 +30,7 @@ // Requests are POST, match requestBody instead of url this.server.respond( function ( request ) { - if ( $.inArray( request.requestBody, [ + if ( [ // simple 'action=options&format=json&formatversion=2&change=foo%3Dbar&token=%2B%5C', // two options @@ -43,7 +43,7 @@ 'action=options&format=json&formatversion=2&change=foo&token=%2B%5C', // reset an option, not bundleable 'action=options&format=json&formatversion=2&optionname=foo%7Cbar%3Dquux&token=%2B%5C' - ] ) !== -1 ) { + ].indexOf( request.requestBody ) !== -1 ) { assert.ok( true, 'Repond to ' + request.requestBody ); request.respond( 200, { 'Content-Type': 'application/json' }, '{ "options": "success" }' ); @@ -88,7 +88,7 @@ // Requests are POST, match requestBody instead of url this.server.respond( function ( request ) { - if ( $.inArray( request.requestBody, [ + if ( [ // simple 'action=options&format=json&formatversion=2&change=foo%3Dbar&token=%2B%5C', // two options @@ -102,7 +102,7 @@ 'action=options&format=json&formatversion=2&change=foo&token=%2B%5C', // reset an option, not bundleable 'action=options&format=json&formatversion=2&optionname=foo%7Cbar%3Dquux&token=%2B%5C' - ] ) !== -1 ) { + ].indexOf( request.requestBody ) !== -1 ) { assert.ok( true, 'Repond to ' + request.requestBody ); request.respond( 200, diff --git a/www/wiki/tests/qunit/suites/resources/mediawiki.api/mediawiki.api.test.js b/www/wiki/tests/qunit/suites/resources/mediawiki.api/mediawiki.api.test.js index 2361f700..417ad3d8 100644 --- a/www/wiki/tests/qunit/suites/resources/mediawiki.api/mediawiki.api.test.js +++ b/www/wiki/tests/qunit/suites/resources/mediawiki.api/mediawiki.api.test.js @@ -18,7 +18,7 @@ } function sequenceBodies( status, headers, bodies ) { - jQuery.each( bodies, function ( i, body ) { + bodies.forEach( function ( body, i ) { bodies[ i ] = [ status, headers, body ]; } ); return sequence( bodies ); @@ -203,7 +203,7 @@ // Don't cache error (T67268) return api.getToken( 'testerror' ) - .then( null, function ( err ) { + .catch( function ( err ) { assert.equal( err, 'bite-me', 'Expected error' ); return api.getToken( 'testerror' ); @@ -449,7 +449,7 @@ } ); this.api.abort(); assert.ok( this.requests.length === 2, 'Check both requests triggered' ); - $.each( this.requests, function ( i, request ) { + this.requests.forEach( function ( request, i ) { assert.ok( request.abort.calledOnce, 'abort request number ' + i ); } ); } ); diff --git a/www/wiki/tests/qunit/suites/resources/mediawiki.api/mediawiki.api.upload.test.js b/www/wiki/tests/qunit/suites/resources/mediawiki.api/mediawiki.api.upload.test.js index bfaf7f26..788a427e 100644 --- a/www/wiki/tests/qunit/suites/resources/mediawiki.api/mediawiki.api.upload.test.js +++ b/www/wiki/tests/qunit/suites/resources/mediawiki.api/mediawiki.api.upload.test.js @@ -19,15 +19,15 @@ api.uploadWithIframe( $( '<input>' )[ 0 ], { filename: 'Testing API upload.jpg' } ); - $iframe = $( 'iframe' ); + $iframe = $( 'iframe:last-child' ); $form = $( 'form.mw-api-upload-form' ); $input = $form.find( 'input[name=filename]' ); - assert.ok( $form.length > 0 ); - assert.ok( $input.length > 0 ); - assert.ok( $iframe.length > 0 ); - assert.strictEqual( $form.prop( 'target' ), $iframe.prop( 'id' ) ); - assert.strictEqual( $input.val(), 'Testing API upload.jpg' ); + assert.ok( $form.length > 0, 'form' ); + assert.ok( $input.length > 0, 'input' ); + assert.ok( $iframe.length > 0, 'frame' ); + assert.strictEqual( $form.prop( 'target' ), $iframe.prop( 'id' ), 'form.target and frame.id ' ); + assert.strictEqual( $input.val(), 'Testing API upload.jpg', 'input value' ); } ); }( mediaWiki, jQuery ) ); diff --git a/www/wiki/tests/qunit/suites/resources/mediawiki.rcfilters/UriProcessor.test.js b/www/wiki/tests/qunit/suites/resources/mediawiki.rcfilters/UriProcessor.test.js index edaaa39f..872f4ddf 100644 --- a/www/wiki/tests/qunit/suites/resources/mediawiki.rcfilters/UriProcessor.test.js +++ b/www/wiki/tests/qunit/suites/resources/mediawiki.rcfilters/UriProcessor.test.js @@ -6,24 +6,33 @@ title: 'Group 1', type: 'send_unselected_if_any', filters: [ - { name: 'filter1', default: true }, - { name: 'filter2' } + { name: 'filter1', cssClass: 'filter1class', default: true }, + { name: 'filter2', cssClass: 'filter2class' } ] }, { name: 'group2', title: 'Group 2', type: 'send_unselected_if_any', filters: [ - { name: 'filter3' }, - { name: 'filter4', default: true } + { name: 'filter3', cssClass: 'filter3class' }, + { name: 'filter4', cssClass: 'filter4class', default: true } ] }, { name: 'group3', title: 'Group 3', type: 'string_options', filters: [ - { name: 'filter5' }, - { name: 'filter6' } + { name: 'filter5', cssClass: 'filter5class' }, + { name: 'filter6' } // Not supporting highlights + ] + }, { + name: 'group4', + title: 'Group 4', + type: 'boolean', + sticky: true, + filters: [ + { name: 'stickyFilter7', cssClass: 'filter7class' }, + { name: 'stickyFilter8', cssClass: 'filter8class' } ] } ], minimalDefaultParams = { @@ -49,66 +58,75 @@ ); } ); - QUnit.test( 'updateModelBasedOnQuery & getUriParametersFromModel', function ( assert ) { + QUnit.test( 'getUpdatedUri', function ( assert ) { var uriProcessor, filtersModel = new mw.rcfilters.dm.FiltersViewModel(), - baseParams = { - filter1: '0', - filter2: '0', - filter3: '0', - filter4: '0', - group3: '', - highlight: '0', - invert: '0', - group1__filter1_color: null, - group1__filter2_color: null, - group2__filter3_color: null, - group2__filter4_color: null, - group3__filter5_color: null, - group3__filter6_color: null + makeUri = function ( queryParams ) { + var uri = new mw.Uri( 'http://server/wiki/Special:RC' ); + uri.query = queryParams; + return uri; }; filtersModel.initializeFilters( mockFilterStructure ); uriProcessor = new mw.rcfilters.UriProcessor( filtersModel ); - uriProcessor.updateModelBasedOnQuery( {} ); assert.deepEqual( - uriProcessor.getUriParametersFromModel(), - $.extend( true, {}, baseParams, minimalDefaultParams ), - 'Version 1: Empty url query sets model to defaults' + ( uriProcessor.getUpdatedUri( makeUri( {} ) ) ).query, + { urlversion: '2' }, + 'Empty model state with empty uri state, assumes the given uri is already normalized, and adds urlversion=2' ); - uriProcessor.updateModelBasedOnQuery( { urlversion: '2' } ); assert.deepEqual( - uriProcessor.getUriParametersFromModel(), - baseParams, - 'Version 2: Empty url query sets model to all-false' + ( uriProcessor.getUpdatedUri( makeUri( { foo: 'bar' } ) ) ).query, + { urlversion: '2', foo: 'bar' }, + 'Empty model state with unrecognized params retains unrecognized params' ); - uriProcessor.updateModelBasedOnQuery( { filter1: '1', urlversion: '2' } ); + // Update the model + filtersModel.toggleFiltersSelected( { + group1__filter1: true, // Param: filter2: '1' + group3__filter5: true // Param: group3: 'filter5' + } ); + assert.deepEqual( - uriProcessor.getUriParametersFromModel(), - $.extend( true, {}, baseParams, { filter1: '1' } ), - 'Parameters in Uri query set parameter value in the model' + ( uriProcessor.getUpdatedUri( makeUri( {} ) ) ).query, + { urlversion: '2', filter2: '1', group3: 'filter5' }, + 'Model state is reflected in the updated URI' ); - uriProcessor.updateModelBasedOnQuery( { highlight: '1', group1__filter1_color: 'c1', urlversion: '2' } ); assert.deepEqual( - uriProcessor.getUriParametersFromModel(), - $.extend( true, {}, baseParams, { - highlight: '1', - group1__filter1_color: 'c1' - } ), - 'Highlight parameters in Uri query set highlight state in the model' + ( uriProcessor.getUpdatedUri( makeUri( { foo: 'bar' } ) ) ).query, + { urlversion: '2', filter2: '1', group3: 'filter5', foo: 'bar' }, + 'Model state is reflected in the updated URI with existing uri params' ); + } ); + + QUnit.test( 'updateModelBasedOnQuery', function ( assert ) { + var uriProcessor, + filtersModel = new mw.rcfilters.dm.FiltersViewModel(); + + filtersModel.initializeFilters( mockFilterStructure ); + uriProcessor = new mw.rcfilters.UriProcessor( filtersModel ); - uriProcessor.updateModelBasedOnQuery( { invert: '1', urlversion: '2' } ); + uriProcessor.updateModelBasedOnQuery( {} ); assert.deepEqual( - uriProcessor.getUriParametersFromModel(), - $.extend( true, {}, baseParams, { - invert: '1' - } ), - 'Invert parameter in Uri query set invert state in the model' + filtersModel.getCurrentParameterState(), + minimalDefaultParams, + 'Version 1: Empty url query sets model to defaults' + ); + + uriProcessor.updateModelBasedOnQuery( { urlversion: '2' } ); + assert.deepEqual( + filtersModel.getCurrentParameterState(), + {}, + 'Version 2: Empty url query sets model to all-false' + ); + + uriProcessor.updateModelBasedOnQuery( { filter1: '1', urlversion: '2' } ); + assert.deepEqual( + filtersModel.getCurrentParameterState(), + $.extend( true, {}, { filter1: '1' } ), + 'Parameters in Uri query set parameter value in the model' ); } ); @@ -259,4 +277,78 @@ } ); } ); + QUnit.test( '_normalizeTargetInUri', function ( assert ) { + var cases = [ + { + input: 'http://host/wiki/Special:RecentChangesLinked/Moai', + output: 'http://host/wiki/Special:RecentChangesLinked?target=Moai', + message: 'Target as subpage in path' + }, + { + input: 'http://host/wiki/Special:RecentChangesLinked/Château', + output: 'http://host/wiki/Special:RecentChangesLinked?target=Château', + message: 'Target as subpage in path with special characters' + }, + { + input: 'http://host/wiki/Special:RecentChangesLinked/Moai/Sub1', + output: 'http://host/wiki/Special:RecentChangesLinked?target=Moai/Sub1', + message: 'Target as subpage also has a subpage' + }, + { + input: 'http://host/wiki/Special:RecentChangesLinked/Category:Foo', + output: 'http://host/wiki/Special:RecentChangesLinked?target=Category:Foo', + message: 'Target as subpage in path (with namespace)' + }, + { + input: 'http://host/wiki/Special:RecentChangesLinked/Category:Foo/Bar', + output: 'http://host/wiki/Special:RecentChangesLinked?target=Category:Foo/Bar', + message: 'Target as subpage in path also has a subpage (with namespace)' + }, + { + input: 'http://host/w/index.php?title=Special:RecentChangesLinked/Moai', + output: 'http://host/w/index.php?title=Special:RecentChangesLinked&target=Moai', + message: 'Target as subpage in title param' + }, + { + input: 'http://host/w/index.php?title=Special:RecentChangesLinked/Moai/Sub1', + output: 'http://host/w/index.php?title=Special:RecentChangesLinked&target=Moai/Sub1', + message: 'Target as subpage in title param also has a subpage' + }, + { + input: 'http://host/w/index.php?title=Special:RecentChangesLinked/Category:Foo/Bar', + output: 'http://host/w/index.php?title=Special:RecentChangesLinked&target=Category:Foo/Bar', + message: 'Target as subpage in title param also has a subpage (with namespace)' + }, + { + input: 'http://host/wiki/Special:Watchlist', + output: 'http://host/wiki/Special:Watchlist', + message: 'No target specified' + }, + { + normalizeTarget: false, + input: 'http://host/wiki/Special:RecentChanges/Foo', + output: 'http://host/wiki/Special:RecentChanges/Foo', + message: 'Do not normalize if "normalizeTarget" is false.' + } + ]; + + cases.forEach( function ( testCase ) { + var uriProcessor = new mw.rcfilters.UriProcessor( + null, + { + normalizeTarget: testCase.normalizeTarget === undefined ? + true : testCase.normalizeTarget + } + ); + + assert.equal( + uriProcessor._normalizeTargetInUri( + new mw.Uri( testCase.input ) + ).toString(), + new mw.Uri( testCase.output ).toString(), + testCase.message + ); + } ); + } ); + }( mediaWiki, jQuery ) ); diff --git a/www/wiki/tests/qunit/suites/resources/mediawiki.rcfilters/dm.FilterItem.test.js b/www/wiki/tests/qunit/suites/resources/mediawiki.rcfilters/dm.FilterItem.test.js index 271648f5..18a2c9ce 100644 --- a/www/wiki/tests/qunit/suites/resources/mediawiki.rcfilters/dm.FilterItem.test.js +++ b/www/wiki/tests/qunit/suites/resources/mediawiki.rcfilters/dm.FilterItem.test.js @@ -184,4 +184,22 @@ 'Events emitted successfully.' ); } ); + + QUnit.test( 'get/set boolean value', function ( assert ) { + var group = new mw.rcfilters.dm.FilterGroup( 'group1', { type: 'boolean' } ), + item = new mw.rcfilters.dm.FilterItem( 'filter1', group ); + + item.setValue( '1' ); + + assert.equal( item.getValue(), true, 'Value is coerced to boolean' ); + } ); + + QUnit.test( 'get/set any value', function ( assert ) { + var group = new mw.rcfilters.dm.FilterGroup( 'group1', { type: 'any_value' } ), + item = new mw.rcfilters.dm.FilterItem( 'filter1', group ); + + item.setValue( '1' ); + + assert.equal( item.getValue(), '1', 'Value is kept as-is' ); + } ); }( mediaWiki ) ); diff --git a/www/wiki/tests/qunit/suites/resources/mediawiki.rcfilters/dm.FiltersViewModel.test.js b/www/wiki/tests/qunit/suites/resources/mediawiki.rcfilters/dm.FiltersViewModel.test.js index 58e4d29c..2b42b5ab 100644 --- a/www/wiki/tests/qunit/suites/resources/mediawiki.rcfilters/dm.FiltersViewModel.test.js +++ b/www/wiki/tests/qunit/suites/resources/mediawiki.rcfilters/dm.FiltersViewModel.test.js @@ -7,6 +7,7 @@ { name: 'filter1', label: 'group1filter1-label', description: 'group1filter1-desc', default: true, + cssClass: 'filter1class', conflicts: [ { group: 'group2' } ], subset: [ { @@ -22,6 +23,7 @@ { name: 'filter2', label: 'group1filter2-label', description: 'group1filter2-desc', conflicts: [ { group: 'group2', filter: 'filter6' } ], + cssClass: 'filter2class', subset: [ { group: 'group1', @@ -29,6 +31,7 @@ } ] }, + // NOTE: This filter has no highlight! { name: 'filter3', label: 'group1filter3-label', description: 'group1filter3-desc', default: true } ] }, { @@ -37,10 +40,10 @@ fullCoverage: true, conflicts: [ { group: 'group1', filter: 'filter1' } ], filters: [ - { name: 'filter4', label: 'group2filter4-label', description: 'group2filter4-desc' }, - { name: 'filter5', label: 'group2filter5-label', description: 'group2filter5-desc', default: true }, + { name: 'filter4', label: 'group2filter4-label', description: 'group2filter4-desc', cssClass: 'filter4class' }, + { name: 'filter5', label: 'group2filter5-label', description: 'group2filter5-desc', default: true, cssClass: 'filter5class' }, { - name: 'filter6', label: 'group2filter6-label', description: 'group2filter6-desc', + name: 'filter6', label: 'group2filter6-label', description: 'group2filter6-desc', cssClass: 'filter6class', conflicts: [ { group: 'group1', filter: 'filter2' } ] } ] @@ -50,15 +53,17 @@ separator: ',', default: 'filter8', filters: [ - { name: 'filter7', label: 'group3filter7-label', description: 'group3filter7-desc' }, - { name: 'filter8', label: 'group3filter8-label', description: 'group3filter8-desc' }, - { name: 'filter9', label: 'group3filter9-label', description: 'group3filter9-desc' } + { name: 'filter7', label: 'group3filter7-label', description: 'group3filter7-desc', cssClass: 'filter7class' }, + { name: 'filter8', label: 'group3filter8-label', description: 'group3filter8-desc', cssClass: 'filter8class' }, + { name: 'filter9', label: 'group3filter9-label', description: 'group3filter9-desc', cssClass: 'filter9class' } ] }, { name: 'group4', type: 'single_option', + hidden: true, default: 'option2', filters: [ + // NOTE: The entire group has no highlight supported { name: 'option1', label: 'group4option1-label', description: 'group4option1-desc' }, { name: 'option2', label: 'group4option2-label', description: 'group4option2-desc' }, { name: 'option3', label: 'group4option3-label', description: 'group4option3-desc' } @@ -67,30 +72,46 @@ name: 'group5', type: 'single_option', filters: [ - { name: 'option1', label: 'group5option1-label', description: 'group5option1-desc' }, - { name: 'option2', label: 'group5option2-label', description: 'group5option2-desc' }, - { name: 'option3', label: 'group5option3-label', description: 'group5option3-desc' } + { name: 'option1', label: 'group5option1-label', description: 'group5option1-desc', cssClass: 'group5opt1class' }, + { name: 'option2', label: 'group5option2-label', description: 'group5option2-desc', cssClass: 'group5opt2class' }, + { name: 'option3', label: 'group5option3-label', description: 'group5option3-desc', cssClass: 'group5opt3class' } ] }, { name: 'group6', type: 'boolean', - isSticky: true, + sticky: true, filters: [ - { name: 'group6option1', label: 'group6option1-label', description: 'group6option1-desc' }, - { name: 'group6option2', label: 'group6option2-label', description: 'group6option2-desc', default: true }, - { name: 'group6option3', label: 'group6option3-label', description: 'group6option3-desc', default: true } + { name: 'group6option1', label: 'group6option1-label', description: 'group6option1-desc', cssClass: 'group6opt1class' }, + { name: 'group6option2', label: 'group6option2-label', description: 'group6option2-desc', default: true, cssClass: 'group6opt2class' }, + { name: 'group6option3', label: 'group6option3-label', description: 'group6option3-desc', default: true, cssClass: 'group6opt3class' } ] }, { name: 'group7', type: 'single_option', - isSticky: true, + sticky: true, default: 'group7option2', filters: [ - { name: 'group7option1', label: 'group7option1-label', description: 'group7option1-desc' }, - { name: 'group7option2', label: 'group7option2-label', description: 'group7option2-desc' }, - { name: 'group7option3', label: 'group7option3-label', description: 'group7option3-desc' } + { name: 'group7option1', label: 'group7option1-label', description: 'group7option1-desc', cssClass: 'group7opt1class' }, + { name: 'group7option2', label: 'group7option2-label', description: 'group7option2-desc', cssClass: 'group7opt2class' }, + { name: 'group7option3', label: 'group7option3-label', description: 'group7option3-desc', cssClass: 'group7opt3class' } ] } ], + shortFilterDefinition = [ { + name: 'group1', + type: 'send_unselected_if_any', + filters: [ { name: 'filter1' }, { name: 'filter2' } ] + }, { + name: 'group2', + type: 'boolean', + hidden: true, + filters: [ { name: 'filter3' }, { name: 'filter4' } ] + }, { + name: 'group3', + type: 'string_options', + sticky: true, + default: 'filter6', + filters: [ { name: 'filter5' }, { name: 'filter6' }, { name: 'filter7' } ] + } ], viewsDefinition = { namespaces: { label: 'Namespaces', @@ -101,10 +122,10 @@ type: 'string_options', separator: ';', filters: [ - { name: 0, label: 'Main' }, - { name: 1, label: 'Talk' }, - { name: 2, label: 'User' }, - { name: 3, label: 'User talk' } + { name: 0, label: 'Main', cssClass: 'namespace-0' }, + { name: 1, label: 'Talk', cssClass: 'namespace-1' }, + { name: 2, label: 'User', cssClass: 'namespace-2' }, + { name: 3, label: 'User talk', cssClass: 'namespace-3' } ] } ] } @@ -119,10 +140,6 @@ group3: 'filter8', group4: 'option2', group5: 'option1', - group6option1: '0', - group6option2: '1', - group6option3: '1', - group7: 'group7option2', namespace: '' }, baseParamRepresentation = { @@ -141,6 +158,48 @@ group7: 'group7option2', namespace: '' }, + emptyParamRepresentation = { + filter1: '0', + filter2: '0', + filter3: '0', + filter4: '0', + filter5: '0', + filter6: '0', + group3: '', + group4: '', + group5: '', + group6option1: '0', + group6option2: '0', + group6option3: '0', + group7: '', + namespace: '', + // Null highlights + group1__filter1_color: null, + group1__filter2_color: null, + // group1__filter3_color: null, // Highlight isn't supported + group2__filter4_color: null, + group2__filter5_color: null, + group2__filter6_color: null, + group3__filter7_color: null, + group3__filter8_color: null, + group3__filter9_color: null, + // group4__option1_color: null, // Highlight isn't supported + // group4__option2_color: null, // Highlight isn't supported + // group4__option3_color: null, // Highlight isn't supported + group5__option1_color: null, + group5__option2_color: null, + group5__option3_color: null, + group6__group6option1_color: null, + group6__group6option2_color: null, + group6__group6option3_color: null, + group7__group7option1_color: null, + group7__group7option2_color: null, + group7__group7option3_color: null, + namespace__0_color: null, + namespace__1_color: null, + namespace__2_color: null, + namespace__3_color: null + }, baseFilterRepresentation = { group1__filter1: false, group1__filter2: false, @@ -213,9 +272,6 @@ 'group2filter5-desc': 'Description of Filter 5 in Group 2', 'group2filter6-label': 'xGroup 2: Filter 6', 'group2filter6-desc': 'Description of Filter 6 in Group 2' - }, - config: { - wgStructuredChangeFiltersEnableExperimentalViews: true } } ) ); @@ -263,22 +319,165 @@ assert.deepEqual( model.getDefaultParams(), defaultParameters, - 'Default parameters are stored properly per filter and group' + 'Default parameters are stored properly per filter and group (sticky groups are ignored)' + ); + } ); + + QUnit.test( 'Parameter minimal state', function ( assert ) { + var model = new mw.rcfilters.dm.FiltersViewModel(), + cases = [ + { + input: {}, + result: {}, + msg: 'Empty parameter representation produces an empty result' + }, + { + input: { + filter1: '1', + filter2: '0', + filter3: '0', + group3: '', + group4: 'option2' + }, + result: { + filter1: '1', + group4: 'option2' + }, + msg: 'Mixed input results in only non-falsey values as result' + }, + { + input: { + filter1: '0', + filter2: '0', + filter3: '0', + group3: '', + group4: '', + group1__filter1_color: null + }, + result: {}, + msg: 'An all-falsey input results in an empty result.' + }, + { + input: { + filter1: '0', + filter2: '0', + filter3: '0', + group3: '', + group4: '', + group1__filter1_color: 'c1' + }, + result: { + group1__filter1_color: 'c1' + }, + msg: 'An all-falsey input with highlight params result in only the highlight param.' + }, + { + input: { + group1__filter1_color: 'c1', + group1__filter3_color: 'c3' // Not supporting highlights + }, + result: { + group1__filter1_color: 'c1' + }, + msg: 'Unsupported highlights are removed.' + } + ]; + + model.initializeFilters( filterDefinition, viewsDefinition ); + + cases.forEach( function ( test ) { + assert.deepEqual( + model.getMinimizedParamRepresentation( test.input ), + test.result, + test.msg + ); + } ); + } ); + + QUnit.test( 'Parameter states', function ( assert ) { + // Some groups / params have their defaults immediately applied + // to their state. These include single_option which can never + // be empty, etc. These are these states: + var parametersWithoutExcluded, + appliedDefaultParameters = { + group4: 'option2', + group5: 'option1', + // Sticky, their defaults apply immediately + group6option2: '1', + group6option3: '1', + group7: 'group7option2' + }, + model = new mw.rcfilters.dm.FiltersViewModel(); + + model.initializeFilters( filterDefinition, viewsDefinition ); + assert.deepEqual( + model.getEmptyParameterState(), + emptyParamRepresentation, + 'Producing an empty parameter state' ); - // Change sticky filter model.toggleFiltersSelected( { - group7__group7option1: true + group1__filter1: true, + group3__filter7: true } ); - // Make sure defaults have changed assert.deepEqual( - model.getDefaultParams(), - $.extend( true, {}, defaultParameters, { - group7: 'group7option1' + model.getCurrentParameterState(), + // appliedDefaultParams applies the default value to parameters + // who must have an initial value to begin with, so we have to + // take it into account in the current state + $.extend( true, {}, appliedDefaultParameters, { + filter2: '1', + filter3: '1', + group3: 'filter7' } ), - 'Default parameters are stored properly per filter and group' + 'Producing a current parameter state' ); + + // Reset + model = new mw.rcfilters.dm.FiltersViewModel(); + model.initializeFilters( filterDefinition, viewsDefinition ); + + parametersWithoutExcluded = $.extend( true, {}, appliedDefaultParameters ); + delete parametersWithoutExcluded.group7; + delete parametersWithoutExcluded.group6option2; + delete parametersWithoutExcluded.group6option3; + + assert.deepEqual( + model.getCurrentParameterState( true ), + parametersWithoutExcluded, + 'Producing a current clean parameter state without excluded filters' + ); + } ); + + QUnit.test( 'Cleaning up parameter states', function ( assert ) { + var model = new mw.rcfilters.dm.FiltersViewModel(), + cases = [ + { + input: {}, + result: {}, + msg: 'Empty parameter representation produces an empty result' + }, + { + input: { + filter1: '1', // Regular (do not strip) + group6option1: '1' // Sticky + }, + result: { filter1: '1' }, + msg: 'Valid input strips all sticky params regardless of value' + } + ]; + + model.initializeFilters( filterDefinition, viewsDefinition ); + + cases.forEach( function ( test ) { + assert.deepEqual( + model.removeStickyParams( test.input ), + test.result, + test.msg + ); + } ); + } ); QUnit.test( 'Finding matching filters', function ( assert ) { @@ -1319,4 +1518,45 @@ 'Items without a specified class identifier are not highlighted.' ); } ); + + QUnit.test( 'emptyAllFilters', function ( assert ) { + var model = new mw.rcfilters.dm.FiltersViewModel(); + + model.initializeFilters( shortFilterDefinition, null ); + + model.toggleFiltersSelected( { + group1__filter1: true, + group2__filter4: true, // hidden + group3__filter5: true // sticky + } ); + + model.emptyAllFilters(); + + assert.deepEqual( + model.getSelectedState( true ), + { + group3__filter5: true, + group3__filter6: true + }, + 'Emptying filters does not affect sticky filters' + ); + } ); + + QUnit.test( 'areVisibleFiltersEmpty', function ( assert ) { + var model = new mw.rcfilters.dm.FiltersViewModel(); + model.initializeFilters( shortFilterDefinition, null ); + + model.emptyAllFilters(); + assert.ok( model.areVisibleFiltersEmpty() ); + + model.toggleFiltersSelected( { + group3__filter5: true // sticky + } ); + assert.ok( model.areVisibleFiltersEmpty() ); + + model.toggleFiltersSelected( { + group1__filter1: true + } ); + assert.notOk( model.areVisibleFiltersEmpty() ); + } ); }( mediaWiki, jQuery ) ); diff --git a/www/wiki/tests/qunit/suites/resources/mediawiki.rcfilters/dm.SavedQueriesModel.test.js b/www/wiki/tests/qunit/suites/resources/mediawiki.rcfilters/dm.SavedQueriesModel.test.js new file mode 100644 index 00000000..ed054bd7 --- /dev/null +++ b/www/wiki/tests/qunit/suites/resources/mediawiki.rcfilters/dm.SavedQueriesModel.test.js @@ -0,0 +1,520 @@ +/* eslint-disable camelcase */ +( function ( mw ) { + var filterDefinition = [ { + name: 'group1', + type: 'send_unselected_if_any', + filters: [ + // Note: The fact filter2 is default means that in the + // filter representation, filter1 and filter3 are 'true' + { name: 'filter1', cssClass: 'filter1class' }, + { name: 'filter2', cssClass: 'filter2class', default: true }, + { name: 'filter3', cssClass: 'filter3class' } + ] + }, { + name: 'group2', + type: 'string_options', + separator: ',', + filters: [ + { name: 'filter4', cssClass: 'filter4class' }, + { name: 'filter5' }, // NOTE: Not supporting highlights! + { name: 'filter6', cssClass: 'filter6class' } + ] + }, { + name: 'group3', + type: 'boolean', + sticky: true, + filters: [ + { name: 'group3option1', cssClass: 'filter1class' }, + { name: 'group3option2', cssClass: 'filter1class' }, + { name: 'group3option3', cssClass: 'filter1class' } + ] + }, { + // Copy of the way the controller defines invert + // to check whether the conversion works + name: 'invertGroup', + type: 'boolean', + hidden: true, + filters: [ { + name: 'invert', + default: '0' + } ] + } ], + queriesFilterRepresentation = { + queries: { + 1234: { + label: 'Item converted', + data: { + filters: { + // - This value is true, but the original filter-representation + // of the saved queries ran against defaults. Since filter1 was + // set as default in the definition, the value would actually + // not appear in the representation itself. + // It is considered 'true', though, and should appear in the + // converted result in its parameter representation. + // >> group1__filter1: true, + // - The reverse is true for filter3. Filter3 is set as default + // but we don't want it in this representation of the saved query. + // Since the filter representation ran against default values, + // it will appear as 'false' value in this representation explicitly + // and the resulting parameter representation should have that + // as the result as well + group1__filter3: false, + group2__filter4: true, + group3__group3option1: true + }, + highlights: { + highlight: true, + group1__filter1: 'c5', + group3__group3option1: 'c1' + }, + invert: true + } + } + } + }, + queriesParamRepresentation = { + version: '2', + queries: { + 1234: { + label: 'Item converted', + data: { + params: { + // filter1 is 'true' so filter2 and filter3 are both '1' + // in param representation + filter2: '1', filter3: '1', + // Group type string_options + group2: 'filter4' + // Note - Group3 is sticky, so it won't show in output + }, + highlights: { + group1__filter1_color: 'c5', + group3__group3option1_color: 'c1' + } + } + } + } + }, + removeHighlights = function ( data ) { + var copy = $.extend( true, {}, data ); + copy.queries[ 1234 ].data.highlights = {}; + return copy; + }; + + QUnit.module( 'mediawiki.rcfilters - SavedQueriesModel' ); + + QUnit.test( 'Initializing queries', function ( assert ) { + var filtersModel = new mw.rcfilters.dm.FiltersViewModel(), + queriesModel = new mw.rcfilters.dm.SavedQueriesModel( filtersModel ), + exampleQueryStructure = { + version: '2', + default: '1234', + queries: { + 1234: { + label: 'Query 1234', + data: { + params: { + filter2: '1' + }, + highlights: { + group1__filter3_color: 'c2' + } + } + } + } + }, + cases = [ + { + input: {}, + finalState: { version: '2', queries: {} }, + msg: 'Empty initial query structure results in base saved queries structure.' + }, + { + input: $.extend( true, {}, exampleQueryStructure ), + finalState: $.extend( true, {}, exampleQueryStructure ), + msg: 'Initialization of given query structure does not corrupt the structure.' + }, + { + // Converting from old structure + input: $.extend( true, {}, queriesFilterRepresentation ), + finalState: $.extend( true, {}, queriesParamRepresentation ), + msg: 'Conversion from filter representation to parameters retains data.' + }, + { + // Converting from old structure + input: $.extend( true, {}, queriesFilterRepresentation, { queries: { 1234: { data: { + filters: { + // Entire group true: normalize params + filter1: true, + filter2: true, + filter3: true + }, + highlights: { + filter3: null // Get rid of empty highlight + } + } } } } ), + finalState: $.extend( true, {}, queriesParamRepresentation ), + msg: 'Conversion from filter representation to parameters normalizes params and highlights.' + }, + { + // Converting from old structure with default + input: $.extend( true, { default: '1234' }, queriesFilterRepresentation ), + finalState: $.extend( true, { default: '1234' }, queriesParamRepresentation ), + msg: 'Conversion from filter representation to parameters, with default set up, retains data.' + }, + { + // Converting from old structure and cleaning up highlights + input: $.extend( true, queriesFilterRepresentation, { queries: { 1234: { data: { highlights: { highlight: false } } } } } ), + finalState: removeHighlights( queriesParamRepresentation ), + msg: 'Conversion from filter representation to parameters and highlight cleanup' + }, + { + // New structure + input: $.extend( true, {}, queriesParamRepresentation ), + finalState: $.extend( true, {}, queriesParamRepresentation ), + msg: 'Parameter representation retains its queries structure' + }, + { + // Do not touch invalid color parameters from the initialization routine + // (Normalization, or "fixing" the query should only happen when we add new query or actively convert queries) + input: $.extend( true, { queries: { 1234: { data: { highlights: { group2__filter5_color: 'c2' } } } } }, exampleQueryStructure ), + finalState: $.extend( true, { queries: { 1234: { data: { highlights: { group2__filter5_color: 'c2' } } } } }, exampleQueryStructure ), + msg: 'Structure that contains invalid highlights remains the same in initialization' + }, + { + // Trim colors when highlight=false is stored + input: $.extend( true, { queries: { 1234: { data: { params: { highlight: '0' } } } } }, queriesParamRepresentation ), + finalState: removeHighlights( queriesParamRepresentation ), + msg: 'Colors are removed when highlight=false' + }, + { + // Remove highlight when it is true but no colors are specified + input: $.extend( true, { queries: { 1234: { data: { params: { highlight: '1' } } } } }, removeHighlights( queriesParamRepresentation ) ), + finalState: removeHighlights( queriesParamRepresentation ), + msg: 'remove highlight when it is true but there is no colors' + } + ]; + + filtersModel.initializeFilters( filterDefinition ); + + cases.forEach( function ( testCase ) { + queriesModel.initialize( testCase.input ); + assert.deepEqual( + queriesModel.getState(), + testCase.finalState, + testCase.msg + ); + } ); + } ); + + QUnit.test( 'Adding new queries', function ( assert ) { + var filtersModel = new mw.rcfilters.dm.FiltersViewModel(), + queriesModel = new mw.rcfilters.dm.SavedQueriesModel( filtersModel ), + cases = [ + { + methodParams: [ + 'label1', // Label + { // Data + filter1: '1', + filter2: '2', + group1__filter1_color: 'c2', + group1__filter3_color: 'c5' + }, + true, // isDefault + '1234' // ID + ], + result: { + itemState: { + label: 'label1', + data: { + params: { + filter1: '1', + filter2: '2' + }, + highlights: { + group1__filter1_color: 'c2', + group1__filter3_color: 'c5' + } + } + }, + isDefault: true, + id: '1234' + }, + msg: 'Given valid data is preserved.' + }, + { + methodParams: [ + 'label2', + { + filter1: '1', + invert: '1', + filter15: '1', // Invalid filter - removed + filter2: '0', // Falsey value - removed + group1__filter1_color: 'c3', + foobar: 'w00t' // Unrecognized parameter - removed + } + ], + result: { + itemState: { + label: 'label2', + data: { + params: { + filter1: '1' // Invert will be dropped because there are no namespaces + }, + highlights: { + group1__filter1_color: 'c3' + } + } + }, + isDefault: false + }, + msg: 'Given data with invalid filters and highlights is normalized' + } + ]; + + filtersModel.initializeFilters( filterDefinition ); + + // Start with an empty saved queries model + queriesModel.initialize( {} ); + + cases.forEach( function ( testCase ) { + var itemID = queriesModel.addNewQuery.apply( queriesModel, testCase.methodParams ), + item = queriesModel.getItemByID( itemID ); + + assert.deepEqual( + item.getState(), + testCase.result.itemState, + testCase.msg + ' (itemState)' + ); + + assert.equal( + item.isDefault(), + testCase.result.isDefault, + testCase.msg + ' (isDefault)' + ); + + if ( testCase.result.id !== undefined ) { + assert.equal( + item.getID(), + testCase.result.id, + testCase.msg + ' (item ID)' + ); + } + } ); + } ); + + QUnit.test( 'Manipulating queries', function ( assert ) { + var id1, id2, item1, matchingItem, + queriesStructure = {}, + filtersModel = new mw.rcfilters.dm.FiltersViewModel(), + queriesModel = new mw.rcfilters.dm.SavedQueriesModel( filtersModel ); + + filtersModel.initializeFilters( filterDefinition ); + + // Start with an empty saved queries model + queriesModel.initialize( {} ); + + // Add items + id1 = queriesModel.addNewQuery( + 'New query 1', + { + group2: 'filter5', + group1__filter1_color: 'c5', + group3__group3option1_color: 'c1' + } + ); + id2 = queriesModel.addNewQuery( + 'New query 2', + { + filter1: '1', + filter2: '1', + invert: '1' + } + ); + item1 = queriesModel.getItemByID( id1 ); + + assert.equal( + item1.getID(), + id1, + 'Item created and its data retained successfully' + ); + + // NOTE: All other methods that the item itself returns are + // tested in the dm.SavedQueryItemModel.test.js file + + // Build the query structure we expect per item + queriesStructure[ id1 ] = { + label: 'New query 1', + data: { + params: { + group2: 'filter5' + }, + highlights: { + group1__filter1_color: 'c5', + group3__group3option1_color: 'c1' + } + } + }; + queriesStructure[ id2 ] = { + label: 'New query 2', + data: { + params: { + filter1: '1', + filter2: '1' + }, + highlights: {} + } + }; + + assert.deepEqual( + queriesModel.getState(), + { + version: '2', + queries: queriesStructure + }, + 'Full query represents current state of items' + ); + + // Add default + queriesModel.setDefault( id2 ); + + assert.deepEqual( + queriesModel.getState(), + { + version: '2', + default: id2, + queries: queriesStructure + }, + 'Setting default is reflected in queries state' + ); + + // Remove default + queriesModel.setDefault( null ); + + assert.deepEqual( + queriesModel.getState(), + { + version: '2', + queries: queriesStructure + }, + 'Removing default is reflected in queries state' + ); + + // Find matching query + matchingItem = queriesModel.findMatchingQuery( + { + group2: 'filter5', + group1__filter1_color: 'c5', + group3__group3option1_color: 'c1' + } + ); + assert.deepEqual( + matchingItem.getID(), + id1, + 'Finding matching item by identical state' + ); + + // Find matching query with 0-values (base state) + matchingItem = queriesModel.findMatchingQuery( + { + group2: 'filter5', + filter1: '0', + filter2: '0', + group1__filter1_color: 'c5', + group3__group3option1_color: 'c1' + } + ); + assert.deepEqual( + matchingItem.getID(), + id1, + 'Finding matching item by "dirty" state with 0-base values' + ); + } ); + + QUnit.test( 'Testing invert property', function ( assert ) { + var itemID, item, + filtersModel = new mw.rcfilters.dm.FiltersViewModel(), + queriesModel = new mw.rcfilters.dm.SavedQueriesModel( filtersModel ), + viewsDefinition = { + namespace: { + label: 'Namespaces', + trigger: ':', + groups: [ { + name: 'namespace', + label: 'Namespaces', + type: 'string_options', + separator: ';', + filters: [ + { name: 0, label: 'Main', cssClass: 'namespace-0' }, + { name: 1, label: 'Talk', cssClass: 'namespace-1' }, + { name: 2, label: 'User', cssClass: 'namespace-2' }, + { name: 3, label: 'User talk', cssClass: 'namespace-3' } + ] + } ] + } + }; + + filtersModel.initializeFilters( filterDefinition, viewsDefinition ); + + // Start with an empty saved queries model + queriesModel.initialize( {} ); + + filtersModel.toggleFiltersSelected( { + group1__filter3: true, + invertGroup__invert: true + } ); + itemID = queriesModel.addNewQuery( + 'label1', // Label + filtersModel.getMinimizedParamRepresentation(), + true, // isDefault + '2345' // ID + ); + item = queriesModel.getItemByID( itemID ); + + assert.deepEqual( + item.getState(), + { + label: 'label1', + data: { + params: { + filter1: '1', + filter2: '1' + }, + highlights: {} + } + }, + 'Invert parameter is not saved if there are no namespaces.' + ); + + // Reset + filtersModel.initializeFilters( filterDefinition, viewsDefinition ); + filtersModel.toggleFiltersSelected( { + group1__filter3: true, + invertGroup__invert: true, + namespace__1: true + } ); + itemID = queriesModel.addNewQuery( + 'label1', // Label + filtersModel.getMinimizedParamRepresentation(), + true, // isDefault + '1234' // ID + ); + item = queriesModel.getItemByID( itemID ); + + assert.deepEqual( + item.getState(), + { + label: 'label1', + data: { + params: { + filter1: '1', + filter2: '1', + invert: '1', + namespace: '1' + }, + highlights: {} + } + }, + 'Invert parameter saved if there are namespaces.' + ); + } ); +}( mediaWiki ) ); diff --git a/www/wiki/tests/qunit/suites/resources/mediawiki.rcfilters/dm.SavedQueryItemModel.test.js b/www/wiki/tests/qunit/suites/resources/mediawiki.rcfilters/dm.SavedQueryItemModel.test.js new file mode 100644 index 00000000..181e9925 --- /dev/null +++ b/www/wiki/tests/qunit/suites/resources/mediawiki.rcfilters/dm.SavedQueryItemModel.test.js @@ -0,0 +1,89 @@ +/* eslint-disable camelcase */ +( function ( mw ) { + var itemData = { + params: { + param1: '1', + param2: 'foo|bar', + invert: '0' + }, + highlights: { + param1_color: 'c1', + param2_color: 'c2' + } + }; + + QUnit.module( 'mediawiki.rcfilters - SavedQueryItemModel' ); + + QUnit.test( 'Initializing and getters', function ( assert ) { + var model; + + model = new mw.rcfilters.dm.SavedQueryItemModel( + 'randomID', + 'Some label', + $.extend( true, {}, itemData ) + ); + + assert.equal( + model.getID(), + 'randomID', + 'Item ID is retained' + ); + + assert.equal( + model.getLabel(), + 'Some label', + 'Item label is retained' + ); + + assert.deepEqual( + model.getData(), + itemData, + 'Item data is retained' + ); + + assert.ok( + !model.isDefault(), + 'Item default state is retained.' + ); + } ); + + QUnit.test( 'Default', function ( assert ) { + var model; + + model = new mw.rcfilters.dm.SavedQueryItemModel( + 'randomID', + 'Some label', + $.extend( true, {}, itemData ) + ); + + assert.ok( + !model.isDefault(), + 'Default state represented when item initialized with default:false.' + ); + + model.toggleDefault( true ); + assert.ok( + model.isDefault(), + 'Default state toggles to true successfully' + ); + + model.toggleDefault( false ); + assert.ok( + !model.isDefault(), + 'Default state toggles to false successfully' + ); + + // Reset + model = new mw.rcfilters.dm.SavedQueryItemModel( + 'randomID', + 'Some label', + $.extend( true, {}, itemData ), + { default: true } + ); + + assert.ok( + model.isDefault(), + 'Default state represented when item initialized with default:true.' + ); + } ); +}( mediaWiki ) ); diff --git a/www/wiki/tests/qunit/suites/resources/mediawiki.special/mediawiki.special.recentchanges.test.js b/www/wiki/tests/qunit/suites/resources/mediawiki.special/mediawiki.special.recentchanges.test.js index 35b6b718..14c2bb4c 100644 --- a/www/wiki/tests/qunit/suites/resources/mediawiki.special/mediawiki.special.recentchanges.test.js +++ b/www/wiki/tests/qunit/suites/resources/mediawiki.special/mediawiki.special.recentchanges.test.js @@ -1,4 +1,4 @@ -( function ( mw, $ ) { +( function ( $ ) { QUnit.module( 'mediawiki.special.recentchanges', QUnit.newMwEnvironment() ); // TODO: verify checkboxes == [ 'nsassociated', 'nsinvert' ] @@ -61,4 +61,4 @@ // DOM cleanup $env.remove(); } ); -}( mediaWiki, jQuery ) ); +}( jQuery ) ); diff --git a/www/wiki/tests/qunit/suites/resources/mediawiki/mediawiki.RegExp.test.js b/www/wiki/tests/qunit/suites/resources/mediawiki/mediawiki.RegExp.test.js index 97c82fb3..4e15cf01 100644 --- a/www/wiki/tests/qunit/suites/resources/mediawiki/mediawiki.RegExp.test.js +++ b/www/wiki/tests/qunit/suites/resources/mediawiki/mediawiki.RegExp.test.js @@ -1,4 +1,4 @@ -( function ( mw, $ ) { +( function ( mw ) { QUnit.module( 'mediawiki.RegExp' ); QUnit.test( 'escape', function ( assert ) { @@ -28,11 +28,11 @@ '0123456789' ].join( '' ); - $.each( specials, function ( i, str ) { + specials.forEach( function ( str ) { assert.propEqual( str.match( new RegExp( mw.RegExp.escape( str ) ) ), [ str ], 'Match ' + str ); } ); assert.equal( mw.RegExp.escape( normal ), normal, 'Alphanumerals are left alone' ); } ); -}( mediaWiki, jQuery ) ); +}( mediaWiki ) ); diff --git a/www/wiki/tests/qunit/suites/resources/mediawiki/mediawiki.String.byteLength.test.js b/www/wiki/tests/qunit/suites/resources/mediawiki/mediawiki.String.byteLength.test.js new file mode 100644 index 00000000..ae3ebbf7 --- /dev/null +++ b/www/wiki/tests/qunit/suites/resources/mediawiki/mediawiki.String.byteLength.test.js @@ -0,0 +1,39 @@ +( function () { + var byteLength = require( 'mediawiki.String' ).byteLength; + + QUnit.module( 'mediawiki.String.byteLength', QUnit.newMwEnvironment() ); + + QUnit.test( 'Simple text', function ( assert ) { + var azLc = 'abcdefghijklmnopqrstuvwxyz', + azUc = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', + num = '0123456789', + x = '*', + space = ' '; + + assert.equal( byteLength( azLc ), 26, 'Lowercase a-z' ); + assert.equal( byteLength( azUc ), 26, 'Uppercase A-Z' ); + assert.equal( byteLength( num ), 10, 'Numbers 0-9' ); + assert.equal( byteLength( x ), 1, 'An asterisk' ); + assert.equal( byteLength( space ), 3, '3 spaces' ); + + } ); + + QUnit.test( 'Special text', function ( assert ) { + // https://en.wikipedia.org/wiki/UTF-8 + var u0024 = '$', + // Cent symbol + u00A2 = '\u00A2', + // Euro symbol + u20AC = '\u20AC', + // Character \U00024B62 (Han script) can't be represented in javascript as a single + // code point, instead it is composed as a surrogate pair of two separate code units. + // http://codepoints.net/U+24B62 + // http://www.fileformat.info/info/unicode/char/24B62/index.htm + u024B62 = '\uD852\uDF62'; + + assert.strictEqual( byteLength( u0024 ), 1, 'U+0024' ); + assert.strictEqual( byteLength( u00A2 ), 2, 'U+00A2' ); + assert.strictEqual( byteLength( u20AC ), 3, 'U+20AC' ); + assert.strictEqual( byteLength( u024B62 ), 4, 'U+024B62 (surrogate pair: \\uD852\\uDF62)' ); + } ); +}() ); diff --git a/www/wiki/tests/qunit/suites/resources/mediawiki/mediawiki.String.trimByteLength.test.js b/www/wiki/tests/qunit/suites/resources/mediawiki/mediawiki.String.trimByteLength.test.js new file mode 100644 index 00000000..e2eea94e --- /dev/null +++ b/www/wiki/tests/qunit/suites/resources/mediawiki/mediawiki.String.trimByteLength.test.js @@ -0,0 +1,150 @@ +( function ( $, mw ) { + var simpleSample, U_20AC, poop, mbSample, + trimByteLength = require( 'mediawiki.String' ).trimByteLength; + + QUnit.module( 'mediawiki.String.trimByteLength', QUnit.newMwEnvironment() ); + + // Simple sample (20 chars, 20 bytes) + simpleSample = '12345678901234567890'; + + // 3 bytes (euro-symbol) + U_20AC = '\u20AC'; + + // Outside of the BMP (pile of poo emoji) + poop = '\uD83D\uDCA9'; // "💩" + + // Multi-byte sample (22 chars, 26 bytes) + mbSample = '1234567890' + U_20AC + '1234567890' + U_20AC; + + /** + * Test factory for mw.String#trimByteLength + * + * @param {Object} options + * @param {string} options.description Test name + * @param {string} options.sample Sequence of characters to trim + * @param {string} [options.initial] Previous value of the sequence of characters, if any + * @param {Number} options.limit Length to trim to + * @param {Function} [options.fn] Filter function + * @param {string} options.expected Expected final value + */ + function byteLimitTest( options ) { + var opt = $.extend( { + description: '', + sample: '', + initial: '', + limit: 0, + fn: function ( a ) { return a; }, + expected: '' + }, options ); + + QUnit.test( opt.description, function ( assert ) { + var res = trimByteLength( opt.initial, opt.sample, opt.limit, opt.fn ); + + assert.equal( + res.newVal, + opt.expected, + 'New value matches the expected string' + ); + } ); + } + + byteLimitTest( { + description: 'Limit using the maxlength attribute', + limit: 10, + sample: simpleSample, + expected: '1234567890' + } ); + + byteLimitTest( { + description: 'Limit using a custom value (multibyte)', + limit: 14, + sample: mbSample, + expected: '1234567890' + U_20AC + '1' + } ); + + byteLimitTest( { + description: 'Limit using a custom value (multibyte, outside BMP)', + limit: 3, + sample: poop, + expected: '' + } ); + + byteLimitTest( { + description: 'Limit using a custom value (multibyte) overlapping a byte', + limit: 12, + sample: mbSample, + expected: '1234567890' + } ); + + byteLimitTest( { + description: 'Pass the limit and a callback as input filter', + limit: 6, + fn: function ( val ) { + var title = mw.Title.newFromText( String( val ) ); + // Return without namespace prefix + return title ? title.getMain() : ''; + }, + sample: 'User:Sample', + expected: 'User:Sample' + } ); + + byteLimitTest( { + description: 'Pass the limit and a callback as input filter', + limit: 6, + fn: function ( val ) { + var title = mw.Title.newFromText( String( val ) ); + // Return without namespace prefix + return title ? title.getMain() : ''; + }, + sample: 'User:Example', + // The callback alters the value to be used to calculeate + // the length. The altered value is "Exampl" which has + // a length of 6, the "e" would exceed the limit. + expected: 'User:Exampl' + } ); + + byteLimitTest( { + description: 'Input filter that increases the length', + limit: 10, + fn: function ( text ) { + return 'prefix' + text; + }, + sample: simpleSample, + // Prefix adds 6 characters, limit is reached after 4 + expected: '1234' + } ); + + byteLimitTest( { + description: 'Trim from insertion when limit exceeded', + limit: 3, + initial: 'abc', + sample: 'zabc', + // Trim from the insertion point (at 0), not the end + expected: 'abc' + } ); + + byteLimitTest( { + description: 'Trim from insertion when limit exceeded', + limit: 3, + initial: 'abc', + sample: 'azbc', + // Trim from the insertion point (at 1), not the end + expected: 'abc' + } ); + + byteLimitTest( { + description: 'Do not cut up false matching substrings in emoji insertions', + limit: 12, + initial: '\uD83D\uDCA9\uD83D\uDCA9', // "💩💩" + sample: '\uD83D\uDCA9\uD83D\uDCB9\uD83E\uDCA9\uD83D\uDCA9', // "💩💹🢩💩" + expected: '\uD83D\uDCA9\uD83D\uDCB9\uD83D\uDCA9' // "💩💹💩" + } ); + + byteLimitTest( { + description: 'Unpaired surrogates do not crash', + limit: 4, + sample: '\uD800\uD800\uDFFF', + expected: '\uD800' + } ); + +}( jQuery, mediaWiki ) ); diff --git a/www/wiki/tests/qunit/suites/resources/mediawiki/mediawiki.Uri.test.js b/www/wiki/tests/qunit/suites/resources/mediawiki/mediawiki.Uri.test.js index e56c9337..918c923a 100644 --- a/www/wiki/tests/qunit/suites/resources/mediawiki/mediawiki.Uri.test.js +++ b/www/wiki/tests/qunit/suites/resources/mediawiki/mediawiki.Uri.test.js @@ -1,4 +1,4 @@ -( function ( mw, $ ) { +( function ( mw ) { QUnit.module( 'mediawiki.Uri', QUnit.newMwEnvironment( { setup: function () { this.mwUriOrg = mw.Uri; @@ -10,7 +10,7 @@ } } ) ); - $.each( [ true, false ], function ( i, strictMode ) { + [ true, false ].forEach( function ( strictMode ) { QUnit.test( 'Basic construction and properties (' + ( strictMode ? '' : 'non-' ) + 'strict mode)', function ( assert ) { var uriString, uri; uriString = 'http://www.ietf.org/rfc/rfc2396.txt'; @@ -206,6 +206,8 @@ uri = uriBase.clone(); uri.fragment = 'frag'; assert.equal( uri.toString(), 'http://en.wiki.local/w/api.php#frag', 'add a fragment' ); + uri.fragment = 'café'; + assert.equal( uri.toString(), 'http://en.wiki.local/w/api.php#caf%C3%A9', 'fragment is url-encoded' ); uri = uriBase.clone(); uri.host = 'fr.wiki.local'; @@ -391,7 +393,7 @@ QUnit.test( 'Advanced URL', function ( assert ) { var uri, queryString, relativePath; - uri = new mw.Uri( 'http://auth@www.example.com:81/dir/dir.2/index.htm?q1=0&&test1&test2=value+%28escaped%29#top' ); + uri = new mw.Uri( 'http://auth@www.example.com:81/dir/dir.2/index.htm?q1=0&&test1&test2=value+%28escaped%29#caf%C3%A9' ); assert.deepEqual( { @@ -412,7 +414,7 @@ port: '81', path: '/dir/dir.2/index.htm', query: { q1: '0', test1: null, test2: 'value (escaped)' }, - fragment: 'top' + fragment: 'café' }, 'basic object properties' ); @@ -432,7 +434,7 @@ relativePath = uri.getRelativePath(); assert.ok( relativePath.indexOf( uri.path ) >= 0, 'path in relative path' ); assert.ok( relativePath.indexOf( uri.getQueryString() ) >= 0, 'query string in relative path' ); - assert.ok( relativePath.indexOf( uri.fragment ) >= 0, 'fragment in relative path' ); + assert.ok( relativePath.indexOf( mw.Uri.encode( uri.fragment ) ) >= 0, 'escaped fragment in relative path' ); } ); QUnit.test( 'Parse a uri with an @ symbol in the path and query', function ( assert ) { @@ -504,4 +506,4 @@ href = uri.toString(); assert.equal( href, testProtocol + testServer + ':' + testPort + testPath, 'Root-relative URL gets host, protocol, and port supplied' ); } ); -}( mediaWiki, jQuery ) ); +}( mediaWiki ) ); diff --git a/www/wiki/tests/qunit/suites/resources/mediawiki/mediawiki.errorLogger.test.js b/www/wiki/tests/qunit/suites/resources/mediawiki/mediawiki.errorLogger.test.js index 46d7837f..2a4d9912 100644 --- a/www/wiki/tests/qunit/suites/resources/mediawiki/mediawiki.errorLogger.test.js +++ b/www/wiki/tests/qunit/suites/resources/mediawiki/mediawiki.errorLogger.test.js @@ -1,4 +1,4 @@ -( function ( $, mw ) { +( function ( mw ) { QUnit.module( 'mediawiki.errorLogger', QUnit.newMwEnvironment() ); QUnit.test( 'installGlobalHandler', function ( assert ) { @@ -39,4 +39,4 @@ assert.strictEqual( w.onerror( errorMessage, errorUrl, errorLine ), true, 'Global handler preserves true return from previous handler' ); } ); -}( jQuery, mediaWiki ) ); +}( mediaWiki ) ); diff --git a/www/wiki/tests/qunit/suites/resources/mediawiki/mediawiki.jqueryMsg.test.js b/www/wiki/tests/qunit/suites/resources/mediawiki/mediawiki.jqueryMsg.test.js index db51fb32..0653dfd3 100644 --- a/www/wiki/tests/qunit/suites/resources/mediawiki/mediawiki.jqueryMsg.test.js +++ b/www/wiki/tests/qunit/suites/resources/mediawiki/mediawiki.jqueryMsg.test.js @@ -374,8 +374,7 @@ .then( function ( langClass ) { var parser; mw.config.set( 'wgUserLanguage', test.lang ); - // eslint-disable-next-line new-cap - parser = new mw.jqueryMsg.parser( { language: langClass } ); + parser = new mw.jqueryMsg.Parser( { language: langClass } ); assert.equal( parser.parse( test.key, test.args ).html(), test.result, @@ -504,11 +503,11 @@ ] ]; - $.each( testCases, function () { + testCases.forEach( function ( testCase ) { var - key = this[ 0 ], - input = this[ 1 ], - output = this[ 2 ]; + key = testCase[ 0 ], + input = testCase[ 1 ], + output = testCase[ 2 ]; mw.messages.set( key, input ); assert.htmlEqual( formatParse( key ), @@ -592,11 +591,11 @@ ] ]; - $.each( testCases, function () { + testCases.forEach( function ( testCase ) { var - key = this[ 0 ], - input = this[ 1 ], - output = this[ 2 ], + key = testCase[ 0 ], + input = testCase[ 1 ], + output = testCase[ 2 ], paramHref = key.slice( 0, 8 ) === 'wikilink' ? 'Example' : 'http://example.com', paramText = 'Text'; mw.messages.set( key, input ); @@ -898,15 +897,14 @@ var queue; mw.messages.set( 'formatnum-msg', '{{formatnum:$1}}' ); mw.messages.set( 'formatnum-msg-int', '{{formatnum:$1|R}}' ); - queue = $.map( formatnumTests, function ( test ) { + queue = formatnumTests.map( function ( test ) { var done = assert.async(); return function ( next, abort ) { getMwLanguage( test.lang ) .then( function ( langClass ) { var parser; mw.config.set( 'wgUserLanguage', test.lang ); - // eslint-disable-next-line new-cap - parser = new mw.jqueryMsg.parser( { language: langClass } ); + parser = new mw.jqueryMsg.Parser( { language: langClass } ); assert.equal( parser.parse( test.integer ? 'formatnum-msg-int' : 'formatnum-msg', [ test.number ] ).html(), diff --git a/www/wiki/tests/qunit/suites/resources/mediawiki/mediawiki.language.test.js b/www/wiki/tests/qunit/suites/resources/mediawiki/mediawiki.language.test.js index 5ce61ea7..e4db771c 100644 --- a/www/wiki/tests/qunit/suites/resources/mediawiki/mediawiki.language.test.js +++ b/www/wiki/tests/qunit/suites/resources/mediawiki/mediawiki.language.test.js @@ -49,11 +49,20 @@ mw.language.setData( 'en', 'digitGroupingPattern', null ); mw.language.setData( 'en', 'digitTransformTable', null ); mw.language.setData( 'en', 'separatorTransformTable', { ',': '.', '.': ',' } ); + mw.language.setData( 'en', 'minimumGroupingDigits', null ); mw.config.set( 'wgUserLanguage', 'en' ); mw.config.set( 'wgTranslateNumerals', true ); - assert.equal( mw.language.convertNumber( 1800 ), '1.800', 'formatting' ); + assert.equal( mw.language.convertNumber( 180 ), '180', 'formatting 3-digit' ); + assert.equal( mw.language.convertNumber( 1800 ), '1.800', 'formatting 4-digit' ); + assert.equal( mw.language.convertNumber( 18000 ), '18.000', 'formatting 5-digit' ); + assert.equal( mw.language.convertNumber( '1.800', true ), '1800', 'unformatting' ); + + mw.language.setData( 'en', 'minimumGroupingDigits', 2 ); + assert.equal( mw.language.convertNumber( 180 ), '180', 'formatting 3-digit with minimumGroupingDigits=2' ); + assert.equal( mw.language.convertNumber( 1800 ), '1800', 'formatting 4-digit with minimumGroupingDigits=2' ); + assert.equal( mw.language.convertNumber( 18000 ), '18.000', 'formatting 5-digit with minimumGroupingDigits=2' ); } ); QUnit.test( 'mw.language.convertNumber - digitTransformTable', function ( assert ) { @@ -61,6 +70,7 @@ mw.config.set( 'wgTranslateNumerals', true ); mw.language.setData( 'hi', 'digitGroupingPattern', null ); mw.language.setData( 'hi', 'separatorTransformTable', { ',': '.', '.': ',' } ); + mw.language.setData( 'hi', 'minimumGroupingDigits', null ); // Example from Hindi (MessagesHi.php) mw.language.setData( 'hi', 'digitTransformTable', { @@ -303,6 +313,18 @@ description: 'Grammar test for prepositional case, привилегия -> привилегии' }, { + word: 'университет', + grammarForm: 'prepositional', + expected: 'университете', + description: 'Grammar test for prepositional case, университет -> университете' + }, + { + word: 'университет', + grammarForm: 'genitive', + expected: 'университета', + description: 'Grammar test for prepositional case, университет -> университете' + }, + { word: 'установка', grammarForm: 'prepositional', expected: 'установке', diff --git a/www/wiki/tests/qunit/suites/resources/mediawiki/mediawiki.loader.test.js b/www/wiki/tests/qunit/suites/resources/mediawiki/mediawiki.loader.test.js index 64415e02..42bc0a76 100644 --- a/www/wiki/tests/qunit/suites/resources/mediawiki/mediawiki.loader.test.js +++ b/www/wiki/tests/qunit/suites/resources/mediawiki/mediawiki.loader.test.js @@ -1,7 +1,12 @@ ( function ( mw, $ ) { - QUnit.module( 'mediawiki (mw.loader)', QUnit.newMwEnvironment( { - setup: function () { + QUnit.module( 'mediawiki.loader', QUnit.newMwEnvironment( { + setup: function ( assert ) { mw.loader.store.enabled = false; + + // Expose for load.mock.php + mw.loader.testFail = function ( reason ) { + assert.ok( false, reason ); + }; }, teardown: function () { mw.loader.store.enabled = false; @@ -10,6 +15,10 @@ window.Set = this.nativeSet; mw.redefineFallbacksForTest(); } + // Remove any remaining temporary statics + // exposed for cross-file mocks. + delete mw.loader.testCallback; + delete mw.loader.testFail; } } ) ); @@ -82,66 +91,35 @@ ); } - QUnit.test( 'Basic', function ( assert ) { - var isAwesomeDone; - + QUnit.test( '.using( .., Function callback ) Promise', function ( assert ) { + var script = 0, callback = 0; mw.loader.testCallback = function () { - assert.strictEqual( isAwesomeDone, undefined, 'Implementing module is.awesome: isAwesomeDone should still be undefined' ); - isAwesomeDone = true; + script++; }; + mw.loader.implement( 'test.promise', [ QUnit.fixurl( mw.config.get( 'wgScriptPath' ) + '/tests/qunit/data/mwLoaderTestCallback.js' ) ] ); - mw.loader.implement( 'test.callback', [ QUnit.fixurl( mw.config.get( 'wgScriptPath' ) + '/tests/qunit/data/mwLoaderTestCallback.js' ) ] ); - - return mw.loader.using( 'test.callback', function () { - assert.strictEqual( isAwesomeDone, true, 'test.callback module should\'ve caused isAwesomeDone to be true' ); - delete mw.loader.testCallback; - - }, function () { - assert.ok( false, 'Error callback fired while loader.using "test.callback" module' ); + return mw.loader.using( 'test.promise', function () { + callback++; + } ).then( function () { + assert.strictEqual( script, 1, 'module script ran' ); + assert.strictEqual( callback, 1, 'using() callback ran' ); } ); } ); - QUnit.test( 'Object method as module name', function ( assert ) { - var isAwesomeDone; - + QUnit.test( 'Prototype method as module name', function ( assert ) { + var call = 0; mw.loader.testCallback = function () { - assert.strictEqual( isAwesomeDone, undefined, 'Implementing module hasOwnProperty: isAwesomeDone should still be undefined' ); - isAwesomeDone = true; + call++; }; - mw.loader.implement( 'hasOwnProperty', [ QUnit.fixurl( mw.config.get( 'wgScriptPath' ) + '/tests/qunit/data/mwLoaderTestCallback.js' ) ], {}, {} ); return mw.loader.using( 'hasOwnProperty', function () { - assert.strictEqual( isAwesomeDone, true, 'hasOwnProperty module should\'ve caused isAwesomeDone to be true' ); - delete mw.loader.testCallback; - - }, function () { - assert.ok( false, 'Error callback fired while loader.using "hasOwnProperty" module' ); + assert.strictEqual( call, 1, 'module script ran' ); } ); } ); - QUnit.test( '.using( .. ) Promise', function ( assert ) { - var isAwesomeDone; - - mw.loader.testCallback = function () { - assert.strictEqual( isAwesomeDone, undefined, 'Implementing module is.awesome: isAwesomeDone should still be undefined' ); - isAwesomeDone = true; - }; - - mw.loader.implement( 'test.promise', [ QUnit.fixurl( mw.config.get( 'wgScriptPath' ) + '/tests/qunit/data/mwLoaderTestCallback.js' ) ] ); - - return mw.loader.using( 'test.promise' ) - .done( function () { - assert.strictEqual( isAwesomeDone, true, 'test.promise module should\'ve caused isAwesomeDone to be true' ); - delete mw.loader.testCallback; - } ) - .fail( function () { - assert.ok( false, 'Error callback fired while loader.using "test.promise" module' ); - } ); - } ); - // Covers mw.loader#sortDependencies (with native Set if available) - QUnit.test( '.using() Error: Circular dependency [StringSet default]', function ( assert ) { + QUnit.test( '.using() - Error: Circular dependency [StringSet default]', function ( assert ) { var done = assert.async(); mw.loader.register( [ @@ -161,7 +139,7 @@ } ); // @covers mw.loader#sortDependencies (with fallback shim) - QUnit.test( '.using() Error: Circular dependency [StringSet shim]', function ( assert ) { + QUnit.test( '.using() - Error: Circular dependency [StringSet shim]', function ( assert ) { var done = assert.async(); if ( !window.Set ) { @@ -425,23 +403,34 @@ mw.loader.load( 'test.implement.d' ); } ); + QUnit.test( '.implement( messages before script )', function ( assert ) { + mw.loader.implement( + 'test.implement.order', + function () { + assert.equal( mw.loader.getState( 'test.implement.order' ), 'executing', 'state during script execution' ); + assert.equal( mw.msg( 'test-foobar' ), 'Hello Foobar, $1!', 'messages load before script execution' ); + }, + {}, + { + 'test-foobar': 'Hello Foobar, $1!' + } + ); + + return mw.loader.using( 'test.implement.order' ).then( function () { + assert.equal( mw.loader.getState( 'test.implement.order' ), 'ready', 'final success state' ); + } ); + } ); + // @import (T33676) - QUnit.test( '.implement( styles has @import )', function ( assert ) { - var isJsExecuted, $element, + QUnit.test( '.implement( styles with @import )', function ( assert ) { + var $element, done = assert.async(); mw.loader.implement( 'test.implement.import', function () { - assert.strictEqual( isJsExecuted, undefined, 'script not executed multiple times' ); - isJsExecuted = true; - - assert.equal( mw.loader.getState( 'test.implement.import' ), 'executing', 'module state during implement() script execution' ); - $element = $( '<div class="mw-test-implement-import">Foo bar</div>' ).appendTo( '#qunit-fixture' ); - assert.equal( mw.msg( 'test-foobar' ), 'Hello Foobar, $1!', 'messages load before script execution' ); - assertStyleAsync( assert, $element, 'float', 'right', function () { assert.equal( $element.css( 'text-align' ), 'center', 'CSS styles after the @import rule are working' @@ -457,16 +446,10 @@ + '\');\n' + '.mw-test-implement-import { text-align: center; }' ] - }, - { - 'test-foobar': 'Hello Foobar, $1!' } ); - mw.loader.using( 'test.implement.import' ).always( function () { - assert.strictEqual( isJsExecuted, true, 'script executed' ); - assert.equal( mw.loader.getState( 'test.implement.import' ), 'ready', 'module state after script execution' ); - } ); + return mw.loader.using( 'test.implement.import' ); } ); QUnit.test( '.implement( dependency with styles )', function ( assert ) { @@ -532,8 +515,6 @@ return mw.loader.using( 'test.implement.msgs', function () { assert.ok( mw.messages.exists( 'T31107' ), 'T31107: messages-only module should implement ok' ); - }, function () { - assert.ok( false, 'Error callback fired while implementing "test.implement.msgs" module' ); } ); } ); @@ -542,6 +523,73 @@ assert.strictEqual( mw.loader.getState( 'test.empty' ), 'ready' ); } ); + // @covers mw.loader#batchRequest + // This is a regression test because in the past we called getCombinedVersion() + // for all requested modules, before url splitting took place. + // Discovered as part of T188076, but not directly related. + QUnit.test( 'Url composition (modules considered for version)', function ( assert ) { + mw.loader.register( [ + // [module, version, dependencies, group, source] + [ 'testUrlInc', 'url', [], null, 'testloader' ], + [ 'testUrlIncDump', 'dump', [], null, 'testloader' ] + ] ); + + mw.config.set( 'wgResourceLoaderMaxQueryLength', 10 ); + + return mw.loader.using( [ 'testUrlIncDump', 'testUrlInc' ] ).then( function ( require ) { + assert.propEqual( + require( 'testUrlIncDump' ).query, + { + modules: 'testUrlIncDump', + // Expected: Wrapped hash just for this one module + // $hash = hash( 'fnv132', 'dump'); + // base_convert( $hash, 16, 36 ); // "13e9zzn" + // Previously: Wrapped hash for both modules, despite being in separate requests + // $hash = hash( 'fnv132', 'urldump' ); + // base_convert( $hash, 16, 36 ); // "18kz9ca" + version: '13e9zzn' + }, + 'Query parameters' + ); + + assert.strictEqual( mw.loader.getState( 'testUrlInc' ), 'ready', 'testUrlInc also loaded' ); + } ); + } ); + + // @covers mw.loader#batchRequest + // @covers mw.loader#buildModulesString + QUnit.test( 'Url composition (order of modules for version) – T188076', function ( assert ) { + mw.loader.register( [ + // [module, version, dependencies, group, source] + [ 'testUrlOrder', 'url', [], null, 'testloader' ], + [ 'testUrlOrder.a', '1', [], null, 'testloader' ], + [ 'testUrlOrder.b', '2', [], null, 'testloader' ], + [ 'testUrlOrderDump', 'dump', [], null, 'testloader' ] + ] ); + + return mw.loader.using( [ + 'testUrlOrderDump', + 'testUrlOrder.b', + 'testUrlOrder.a', + 'testUrlOrder' + ] ).then( function ( require ) { + assert.propEqual( + require( 'testUrlOrderDump' ).query, + { + modules: 'testUrlOrder,testUrlOrderDump|testUrlOrder.a,b', + // Expected: Combined in order after string packing + // $hash = hash( 'fnv132', 'urldump12' ); + // base_convert( $hash, 16, 36 ); // "1knqzan" + // Previously: Combined in order of before string packing + // $hash = hash( 'fnv132', 'url12dump' ); + // base_convert( $hash, 16, 36 ); // "11eo3in" + version: '1knqzan' + }, + 'Query parameters' + ); + } ); + } ); + QUnit.test( 'Broken indirect dependency', function ( assert ) { // don't emit an error event this.sandbox.stub( mw, 'track' ); @@ -638,9 +686,9 @@ ] ); function verifyModuleStates() { - assert.equal( mw.loader.getState( 'testMissing' ), 'missing', 'Module not known to server must have state "missing"' ); - assert.equal( mw.loader.getState( 'testUsesMissing' ), 'error', 'Module with missing dependency must have state "error"' ); - assert.equal( mw.loader.getState( 'testUsesNestedMissing' ), 'error', 'Module with indirect missing dependency must have state "error"' ); + assert.equal( mw.loader.getState( 'testMissing' ), 'missing', 'Module "testMissing" state' ); + assert.equal( mw.loader.getState( 'testUsesMissing' ), 'error', 'Module "testUsesMissing" state' ); + assert.equal( mw.loader.getState( 'testUsesNestedMissing' ), 'error', 'Module "testUsesNestedMissing" state' ); } mw.loader.using( [ 'testUsesNestedMissing' ], @@ -674,24 +722,16 @@ [ 'testUsesSkippable', '1', [ 'testSkipped', 'testNotSkipped' ], null, 'testloader' ] ] ); - function verifyModuleStates() { - assert.equal( mw.loader.getState( 'testSkipped' ), 'ready', 'Module is ready when skipped' ); - assert.equal( mw.loader.getState( 'testNotSkipped' ), 'ready', 'Module is ready when not skipped but loaded' ); - assert.equal( mw.loader.getState( 'testUsesSkippable' ), 'ready', 'Module is ready when skippable dependencies are ready' ); - } - - return mw.loader.using( [ 'testUsesSkippable' ], + return mw.loader.using( [ 'testUsesSkippable' ] ).then( function () { - assert.ok( true, 'Success handler should be invoked.' ); - assert.ok( true ); // Dummy to match error handler and reach QUnit expect() - - verifyModuleStates(); + assert.equal( mw.loader.getState( 'testSkipped' ), 'ready', 'Skipped module' ); + assert.equal( mw.loader.getState( 'testNotSkipped' ), 'ready', 'Regular module' ); + assert.equal( mw.loader.getState( 'testUsesSkippable' ), 'ready', 'Regular module with skippable dependency' ); }, function ( e, badmodules ) { - assert.ok( false, 'Error handler should not be invoked.' ); - assert.deepEqual( badmodules, [], 'Bad modules as expected.' ); - - verifyModuleStates(); + // Should not fail and QUnit would already catch this, + // but add a handler anyway to report details from 'badmodules + assert.deepEqual( badmodules, [], 'Bad modules' ); } ); } ); @@ -710,6 +750,7 @@ assert.equal( target.slice( 0, 2 ), '//', 'URL is protocol-relative' ); mw.loader.testCallback = function () { + // Ensure once, delete now delete mw.loader.testCallback; assert.ok( true, 'callback' ); done(); @@ -728,6 +769,7 @@ assert.equal( target.slice( 0, 1 ), '/', 'URL is relative to document root' ); mw.loader.testCallback = function () { + // Ensure once, delete now delete mw.loader.testCallback; assert.ok( true, 'callback' ); done(); @@ -812,18 +854,18 @@ } ); QUnit.test( 'Stale response caching - backcompat', function ( assert ) { - var count = 0; + var script = 0; mw.loader.store.enabled = true; mw.loader.register( 'test.stalebc', 'v2' ); assert.strictEqual( mw.loader.store.get( 'test.stalebc' ), false, 'Not in store' ); mw.loader.implement( 'test.stalebc', function () { - count++; + script++; } ); return mw.loader.using( 'test.stalebc' ) .then( function () { - assert.strictEqual( count, 1 ); + assert.strictEqual( script, 1, 'module script ran' ); assert.strictEqual( mw.loader.getState( 'test.stalebc' ), 'ready' ); assert.ok( mw.loader.store.get( 'test.stalebc' ), 'In store' ); } ) @@ -902,37 +944,33 @@ } catch ( e ) { assert.equal( null, String( e ), 'require works asynchrously in debug mode' ); } - }, function () { - assert.ok( false, 'Error callback fired while loader.using "test.require.callback" module' ); } ); } ); QUnit.test( 'Implicit dependencies', function ( assert ) { - var ranUser = false, - userSeesSite = false, - ranSite = false; + var user = 0, + site = 0, + siteFromUser = 0; mw.loader.implement( 'site', function () { - ranSite = true; + site++; } ); mw.loader.implement( 'user', function () { - userSeesSite = ranSite; - ranUser = true; + user++; + siteFromUser = site; } ); - assert.strictEqual( ranSite, false, 'verify site module not yet loaded' ); - assert.strictEqual( ranUser, false, 'verify user module not yet loaded' ); return mw.loader.using( 'user', function () { - assert.strictEqual( ranSite, true, 'ran site module' ); - assert.strictEqual( ranUser, true, 'ran user module' ); - assert.strictEqual( userSeesSite, true, 'ran site before user module' ); - + assert.strictEqual( site, 1, 'site module' ); + assert.strictEqual( user, 1, 'user module' ); + assert.strictEqual( siteFromUser, 1, 'site ran before user' ); + } ).always( function () { // Reset mw.loader.moduleRegistry[ 'site' ].state = 'registered'; mw.loader.moduleRegistry[ 'user' ].state = 'registered'; diff --git a/www/wiki/tests/qunit/suites/resources/mediawiki/mediawiki.toc.test.js b/www/wiki/tests/qunit/suites/resources/mediawiki/mediawiki.toc.test.js index b20b68f5..6a1b83cf 100644 --- a/www/wiki/tests/qunit/suites/resources/mediawiki/mediawiki.toc.test.js +++ b/www/wiki/tests/qunit/suites/resources/mediawiki/mediawiki.toc.test.js @@ -12,7 +12,7 @@ assert.strictEqual( $( '.toc' ).length, 0, 'There is no table of contents on the page at the beginning' ); tocHtml = '<div id="toc" class="toc">' + - '<div class="toctitle">' + + '<div class="toctitle" lang="en" dir="ltr">' + '<h2>Contents</h2>' + '</div>' + '<ul><li></li></ul>' + diff --git a/www/wiki/tests/qunit/suites/resources/mediawiki/mediawiki.user.test.js b/www/wiki/tests/qunit/suites/resources/mediawiki/mediawiki.user.test.js index bc126429..814a2075 100644 --- a/www/wiki/tests/qunit/suites/resources/mediawiki/mediawiki.user.test.js +++ b/www/wiki/tests/qunit/suites/resources/mediawiki/mediawiki.user.test.js @@ -97,6 +97,14 @@ assert.notEqual( result, result2, 'different when called multiple times' ); } ); + QUnit.test( 'stickyRandomId', function ( assert ) { + var result = mw.user.stickyRandomId(), + result2 = mw.user.stickyRandomId(); + assert.equal( typeof result, 'string', 'type' ); + assert.strictEqual( /^[a-f0-9]{16}$/.test( result ), true, '16 HEX symbols string' ); + assert.equal( result2, result, 'sticky' ); + } ); + QUnit.test( 'sessionId', function ( assert ) { var result = mw.user.sessionId(), result2 = mw.user.sessionId(); diff --git a/www/wiki/tests/qunit/suites/resources/mediawiki/mediawiki.util.test.js b/www/wiki/tests/qunit/suites/resources/mediawiki/mediawiki.util.test.js index bb276265..b8464e99 100644 --- a/www/wiki/tests/qunit/suites/resources/mediawiki/mediawiki.util.test.js +++ b/www/wiki/tests/qunit/suites/resources/mediawiki/mediawiki.util.test.js @@ -53,7 +53,7 @@ ]; Array.prototype.push.apply( IPV6_CASES, - $.map( [ + [ 'fc:100::', 'fc:100:a::', 'fc:100:a:d::', @@ -69,8 +69,8 @@ '::fc:100:a:d:1:e', '::fc:100:a:d:1:e:ac', 'fc:100:a:d:1:e:ac:0' - ], function ( el ) { - return [ [ true, el, el + ' is a valid IP' ] ]; + ].map( function ( el ) { + return [ true, el, el + ' is a valid IP' ]; } ) ); @@ -132,7 +132,7 @@ newExperimental = [ 'html5', 'html5-legacy' ]; // Test cases are kept in sync with SanitizerTest.php - $.each( [ + [ // Pure legacy: how MW worked before 2017 [ legacy, text, legacyEncoded ], // Transition to a new world: legacy links with HTML5 fallback @@ -145,7 +145,7 @@ [ experimentalLegacy, text, html5Experimental ], // Migration from $wgExperimentalHtmlIds to modern HTML5 [ newExperimental, text, html5Encoded ] - ], function ( index, testCase ) { + ].forEach( function ( testCase ) { mw.config.set( 'wgFragmentMode', testCase[ 0 ] ); assert.equal( util.escapeIdForAttribute( testCase[ 1 ] ), testCase[ 2 ] ); @@ -166,7 +166,7 @@ experimentalLegacy = [ 'html5-legacy', 'legacy' ], newExperimental = [ 'html5', 'html5-legacy' ]; - $.each( [ + [ // Pure legacy: how MW worked before 2017 [ legacy, text, legacyEncoded ], // Transition to a new world: legacy links with HTML5 fallback @@ -179,7 +179,7 @@ [ experimentalLegacy, text, html5Experimental ], // Migration from wgExperimentalHtmlIds to modern HTML5 [ newExperimental, text, html5Encoded ] - ], function ( index, testCase ) { + ].forEach( function ( testCase ) { mw.config.set( 'wgFragmentMode', testCase[ 0 ] ); assert.equal( util.escapeIdForLink( testCase[ 1 ] ), testCase[ 2 ] ); @@ -246,16 +246,26 @@ assert.equal( href, '/wiki/#Fragment', 'empty title with fragment' ); href = util.getUrl( '#Fragment', { action: 'edit' } ); - assert.equal( href, '/w/index.php?action=edit#Fragment', 'epmty title with query string and fragment' ); + assert.equal( href, '/w/index.php?action=edit#Fragment', 'empty title with query string and fragment' ); + mw.config.set( 'wgFragmentMode', [ 'legacy' ] ); href = util.getUrl( 'Foo:Sandbox \xC4#Fragment \xC4', { action: 'edit' } ); assert.equal( href, '/w/index.php?title=Foo:Sandbox_%C3%84&action=edit#Fragment_.C3.84', 'title with query string, fragment, and special characters' ); + mw.config.set( 'wgFragmentMode', [ 'html5' ] ); + href = util.getUrl( 'Foo:Sandbox \xC4#Fragment \xC4', { action: 'edit' } ); + assert.equal( href, '/w/index.php?title=Foo:Sandbox_%C3%84&action=edit#Fragment_Ä', 'title with query string, fragment, and special characters' ); + href = util.getUrl( 'Foo:%23#Fragment', { action: 'edit' } ); assert.equal( href, '/w/index.php?title=Foo:%2523&action=edit#Fragment', 'title containing %23 (#), fragment, and a query string' ); + mw.config.set( 'wgFragmentMode', [ 'legacy' ] ); href = util.getUrl( '#+&=:;@$-_.!*/[]<>\'§', { action: 'edit' } ); assert.equal( href, '/w/index.php?action=edit#.2B.26.3D:.3B.40.24-_..21.2A.2F.5B.5D.3C.3E.27.C2.A7', 'fragment with various characters' ); + + mw.config.set( 'wgFragmentMode', [ 'html5' ] ); + href = util.getUrl( '#+&=:;@$-_.!*/[]<>\'§', { action: 'edit' } ); + assert.equal( href, '/w/index.php?action=edit#+&=:;@$-_.!*/[]<>\'§', 'fragment with various characters' ); } ); QUnit.test( 'wikiScript', function ( assert ) { @@ -432,23 +442,23 @@ } ); QUnit.test( 'isIPv6Address', function ( assert ) { - $.each( IPV6_CASES, function ( i, ipCase ) { + IPV6_CASES.forEach( function ( ipCase ) { assert.strictEqual( util.isIPv6Address( ipCase[ 1 ] ), ipCase[ 0 ], ipCase[ 2 ] ); } ); } ); QUnit.test( 'isIPv4Address', function ( assert ) { - $.each( IPV4_CASES, function ( i, ipCase ) { + IPV4_CASES.forEach( function ( ipCase ) { assert.strictEqual( util.isIPv4Address( ipCase[ 1 ] ), ipCase[ 0 ], ipCase[ 2 ] ); } ); } ); QUnit.test( 'isIPAddress', function ( assert ) { - $.each( IPV4_CASES, function ( i, ipCase ) { + IPV4_CASES.forEach( function ( ipCase ) { assert.strictEqual( util.isIPv4Address( ipCase[ 1 ] ), ipCase[ 0 ], ipCase[ 2 ] ); } ); - $.each( IPV6_CASES, function ( i, ipCase ) { + IPV6_CASES.forEach( function ( ipCase ) { assert.strictEqual( util.isIPv6Address( ipCase[ 1 ] ), ipCase[ 0 ], ipCase[ 2 ] ); } ); } ); diff --git a/www/wiki/tests/qunit/suites/resources/mediawiki/mediawiki.visibleTimeout.test.js b/www/wiki/tests/qunit/suites/resources/mediawiki/mediawiki.visibleTimeout.test.js new file mode 100644 index 00000000..7f8819de --- /dev/null +++ b/www/wiki/tests/qunit/suites/resources/mediawiki/mediawiki.visibleTimeout.test.js @@ -0,0 +1,115 @@ +( function ( mw ) { + + QUnit.module( 'mediawiki.visibleTimeout', QUnit.newMwEnvironment( { + setup: function () { + // Document with just enough stuff to make the tests work. + var listeners = []; + this.mockDocument = { + hidden: false, + addEventListener: function ( type, listener ) { + if ( type === 'visibilitychange' ) { + listeners.push( listener ); + } + }, + removeEventListener: function ( type, listener ) { + var i; + if ( type === 'visibilitychange' ) { + i = listeners.indexOf( listener ); + if ( i >= 0 ) { + listeners.splice( i, 1 ); + } + } + }, + // Helper function to swap visibility and run listeners + toggleVisibility: function () { + var i; + this.hidden = !this.hidden; + for ( i = 0; i < listeners.length; i++ ) { + listeners[ i ](); + } + } + }; + this.visibleTimeout = require( 'mediawiki.visibleTimeout' ); + this.visibleTimeout.setDocument( this.mockDocument ); + + this.sandbox.useFakeTimers(); + // mw.now() doesn't respect the fake clock injected by useFakeTimers + this.stub( mw, 'now', ( function () { + return this.sandbox.clock.now; + } ).bind( this ) ); + } + } ) ); + + QUnit.test( 'basic usage', function ( assert ) { + var called = 0; + + this.visibleTimeout.set( function () { + called++; + }, 0 ); + assert.strictEqual( called, 0 ); + this.sandbox.clock.tick( 1 ); + assert.strictEqual( called, 1 ); + + this.sandbox.clock.tick( 100 ); + assert.strictEqual( called, 1 ); + + this.visibleTimeout.set( function () { + called++; + }, 10 ); + this.sandbox.clock.tick( 10 ); + assert.strictEqual( called, 2 ); + } ); + + QUnit.test( 'can cancel timeout', function ( assert ) { + var called = 0, + timeout = this.visibleTimeout.set( function () { + called++; + }, 0 ); + + this.visibleTimeout.clear( timeout ); + this.sandbox.clock.tick( 10 ); + assert.strictEqual( called, 0 ); + + timeout = this.visibleTimeout.set( function () { + called++; + }, 100 ); + this.sandbox.clock.tick( 50 ); + assert.strictEqual( called, 0 ); + this.visibleTimeout.clear( timeout ); + this.sandbox.clock.tick( 100 ); + assert.strictEqual( called, 0 ); + } ); + + QUnit.test( 'start hidden and become visible', function ( assert ) { + var called = 0; + + this.mockDocument.hidden = true; + this.visibleTimeout.set( function () { + called++; + }, 0 ); + this.sandbox.clock.tick( 10 ); + assert.strictEqual( called, 0 ); + + this.mockDocument.toggleVisibility(); + this.sandbox.clock.tick( 10 ); + assert.strictEqual( called, 1 ); + } ); + + QUnit.test( 'timeout is cumulative', function ( assert ) { + var called = 0; + + this.visibleTimeout.set( function () { + called++; + }, 100 ); + this.sandbox.clock.tick( 50 ); + assert.strictEqual( called, 0 ); + + this.mockDocument.toggleVisibility(); + this.sandbox.clock.tick( 1000 ); + assert.strictEqual( called, 0 ); + + this.mockDocument.toggleVisibility(); + this.sandbox.clock.tick( 50 ); + assert.strictEqual( called, 1 ); + } ); +}( mediaWiki ) ); diff --git a/www/wiki/tests/qunit/suites/resources/startup.test.js b/www/wiki/tests/qunit/suites/resources/startup.test.js index ee1340d6..6a704b5a 100644 --- a/www/wiki/tests/qunit/suites/resources/startup.test.js +++ b/www/wiki/tests/qunit/suites/resources/startup.test.js @@ -1,5 +1,5 @@ /* global isCompatible: true */ -( function ( $ ) { +( function () { var testcases = { tested: [ /* Grade A */ @@ -21,11 +21,8 @@ 'Mozilla/5.0 (Windows NT 6.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/28.0.1500.95 Safari/537.36 OPR/15.0.1147.153', 'Mozilla/5.0 (Windows NT 5.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/29.0.1547.57 Safari/537.36 OPR/16.0.1196.62', 'Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/36.0.1985.125 Safari/537.36 OPR/23.0.1522.75', - // Internet Explorer 10+ - 'Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.1; WOW64; Trident/6.0)', + // Internet Explorer 11 'Mozilla/5.0 (Windows NT 6.3; Trident/7.0; rv:11.0) like Gecko', - // IE Mobile - 'Mozilla/5.0 (compatible; MSIE 9.0; Windows Phone OS 7.5; Trident/5.0; IEMobile/9.0; NOKIA; Lumia 800)', // Edge 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.135 Safari/537.36 Edge/12.246', // Edge Mobile @@ -107,6 +104,10 @@ blacklisted: [ /* Grade C */ + // Internet Explorer 10 + 'Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.1; WOW64; Trident/6.0)', + // IE Mobile 10 + 'Mozilla/5.0 (compatible; MSIE 10.0; Windows Phone 8.0; Trident/6.0; IEMobile/10.0; ARM; Touch; HTC; Windows Phone 8X by HTC)', // PlayStation 'Mozilla/5.0 (PLAYSTATION 3; 1.10)', 'Mozilla/5.0 (PLAYSTATION 3; 3.55)', @@ -146,14 +147,14 @@ QUnit.module( 'startup', QUnit.newMwEnvironment() ); QUnit.test( 'isCompatible( featureTestable )', function ( assert ) { - $.each( testcases.tested, function ( i, ua ) { + testcases.tested.forEach( function ( ua ) { assert.strictEqual( isCompatible( ua ), true, ua ); } ); } ); QUnit.test( 'isCompatible( blacklisted )', function ( assert ) { - $.each( testcases.blacklisted, function ( i, ua ) { + testcases.blacklisted.forEach( function ( ua ) { assert.strictEqual( isCompatible( ua ), false, ua ); } ); } ); -}( jQuery ) ); +}() ); diff --git a/www/wiki/tests/selenium/README.md b/www/wiki/tests/selenium/README.md index 0c8b4473..b15d4073 100644 --- a/www/wiki/tests/selenium/README.md +++ b/www/wiki/tests/selenium/README.md @@ -28,24 +28,28 @@ environment variable to any value: To run only one file (for example page.js), you first need to spawn the chromedriver: - chromedriver --url-base=/wd/hub --port=4444 + chromedriver --url-base=wd/hub --port=4444 Then in another terminal: cd tests/selenium ../../node_modules/.bin/wdio --spec specs/page.js +To run only one test (name contains string 'preferences'): + + ../../node_modules/.bin/wdio --spec specs/user.js --mochaOpts.grep preferences + The runner reads the config file `wdio.conf.js` and runs the spec listed in `page.js`. -The defaults in the configuration files aim are targetting a MediaWiki-Vagrant -installation on installation on http://127.0.0.1:8080 with a user Admin and -password 'vagrant'. Those settings can be overriden using environment +The defaults in the configuration files aim are targeting a MediaWiki-Vagrant +installation on http://127.0.0.1:8080 with a user Admin and +password 'vagrant'. Those settings can be overridden using environment variables: `MW_SERVER`: to be set to the value of your $wgServer -`MW_SCRIPT_PATH`: ditto with $wgScriptPath -`MEDIAWIKI_USER`: username of an account that can create users on the wiki. +`MW_SCRIPT_PATH`: ditto with $wgScriptPath +`MEDIAWIKI_USER`: username of an account that can create users on the wiki `MEDIAWIKI_PASSWORD`: password for above user Example: diff --git a/www/wiki/tests/selenium/pageobjects/createaccount.page.js b/www/wiki/tests/selenium/pageobjects/createaccount.page.js index f54e31c8..105f4092 100644 --- a/www/wiki/tests/selenium/pageobjects/createaccount.page.js +++ b/www/wiki/tests/selenium/pageobjects/createaccount.page.js @@ -22,54 +22,26 @@ class CreateAccountPage extends Page { } apiCreateAccount( username, password ) { - const url = require( 'url' ), // https://nodejs.org/docs/latest/api/url.html - baseUrl = url.parse( browser.options.baseUrl ), // http://webdriver.io/guide/testrunner/browserobject.html - Bot = require( 'nodemw' ), // https://github.com/macbre/nodemw - client = new Bot( { - protocol: baseUrl.protocol, - server: baseUrl.hostname, - port: baseUrl.port, - path: baseUrl.path, - debug: false - } ); - return new Promise( ( resolve, reject ) => { - client.api.call( - { - action: 'query', - meta: 'tokens', - type: 'createaccount' - }, - /** - * @param {Error|null} err - * @param {Object} info Processed query result - * @param {Object} next More results? - * @param {Object} data Raw data - */ - function ( err, info, next, data ) { - if ( err ) { - reject( err ); - return; - } - client.api.call( { - action: 'createaccount', - createreturnurl: browser.options.baseUrl, - createtoken: data.query.tokens.createaccounttoken, - username: username, - password: password, - retype: password - }, function ( err ) { - if ( err ) { - reject( err ); - return; - } - resolve(); - }, 'POST' ); - }, - 'POST' - ); + const MWBot = require( 'mwbot' ), // https://github.com/Fannon/mwbot + Promise = require( 'bluebird' ); + let bot = new MWBot(); - } ); + return Promise.coroutine( function* () { + yield bot.loginGetCreateaccountToken( { + apiUrl: `${browser.options.baseUrl}/api.php`, + username: browser.options.username, + password: browser.options.password + } ); + yield bot.request( { + action: 'createaccount', + createreturnurl: browser.options.baseUrl, + createtoken: bot.createaccountToken, + username: username, + password: password, + retype: password + } ); + } ).call( this ); } diff --git a/www/wiki/tests/selenium/pageobjects/delete.page.js b/www/wiki/tests/selenium/pageobjects/delete.page.js new file mode 100644 index 00000000..d43cb9f6 --- /dev/null +++ b/www/wiki/tests/selenium/pageobjects/delete.page.js @@ -0,0 +1,39 @@ +'use strict'; +const Page = require( './page' ); + +class DeletePage extends Page { + + get reason() { return browser.element( '#wpReason' ); } + get watch() { return browser.element( '#wpWatch' ); } + get submit() { return browser.element( '#wpConfirmB' ); } + get displayedContent() { return browser.element( '#mw-content-text' ); } + + open( name ) { + super.open( name + '&action=delete' ); + } + + delete( name, reason ) { + this.open( name ); + this.reason.setValue( reason ); + this.submit.click(); + } + + apiDelete( name, reason ) { + + const MWBot = require( 'mwbot' ), // https://github.com/Fannon/mwbot + Promise = require( 'bluebird' ); + let bot = new MWBot(); + + return Promise.coroutine( function* () { + yield bot.loginGetEditToken( { + apiUrl: `${browser.options.baseUrl}/api.php`, + username: browser.options.username, + password: browser.options.password + } ); + yield bot.delete( name, reason ); + } ).call( this ); + + } + +} +module.exports = new DeletePage(); diff --git a/www/wiki/tests/selenium/pageobjects/edit.page.js b/www/wiki/tests/selenium/pageobjects/edit.page.js index 25da8cb8..33a27f0f 100644 --- a/www/wiki/tests/selenium/pageobjects/edit.page.js +++ b/www/wiki/tests/selenium/pageobjects/edit.page.js @@ -19,25 +19,20 @@ class EditPage extends Page { } apiEdit( name, content ) { - const url = require( 'url' ), // https://nodejs.org/docs/latest/api/url.html - baseUrl = url.parse( browser.options.baseUrl ), // http://webdriver.io/guide/testrunner/browserobject.html - Bot = require( 'nodemw' ), // https://github.com/macbre/nodemw - client = new Bot( { - protocol: baseUrl.protocol, - server: baseUrl.hostname, - port: baseUrl.port, - path: baseUrl.path, - debug: false - } ); - return new Promise( ( resolve, reject ) => { - client.edit( name, content, `Created page with "${content}"`, function ( err ) { - if ( err ) { - return reject( err ); - } - resolve(); + const MWBot = require( 'mwbot' ), // https://github.com/Fannon/mwbot + Promise = require( 'bluebird' ); + let bot = new MWBot(); + + return Promise.coroutine( function* () { + yield bot.loginGetEditToken( { + apiUrl: `${browser.options.baseUrl}/api.php`, + username: browser.options.username, + password: browser.options.password } ); - } ); + yield bot.edit( name, content, `Created page with "${content}"` ); + } ).call( this ); + } } diff --git a/www/wiki/tests/selenium/pageobjects/page.js b/www/wiki/tests/selenium/pageobjects/page.js index 864bdaea..77bb1f4e 100644 --- a/www/wiki/tests/selenium/pageobjects/page.js +++ b/www/wiki/tests/selenium/pageobjects/page.js @@ -1,11 +1,8 @@ // From http://webdriver.io/guide/testrunner/pageobjects.html 'use strict'; class Page { - constructor() { - this.title = 'My Page'; - } open( path ) { - browser.url( '/index.php?title=' + path ); + browser.url( browser.options.baseUrl + '/index.php?title=' + path ); } } module.exports = Page; diff --git a/www/wiki/tests/selenium/pageobjects/restore.page.js b/www/wiki/tests/selenium/pageobjects/restore.page.js new file mode 100644 index 00000000..071f7f98 --- /dev/null +++ b/www/wiki/tests/selenium/pageobjects/restore.page.js @@ -0,0 +1,21 @@ +'use strict'; +const Page = require( './page' ); + +class RestorePage extends Page { + + get reason() { return browser.element( '#wpComment' ); } + get submit() { return browser.element( '#mw-undelete-submit' ); } + get displayedContent() { return browser.element( '#mw-content-text' ); } + + open( name ) { + super.open( 'Special:Undelete/' + name ); + } + + restore( name, reason ) { + this.open( name ); + this.reason.setValue( reason ); + this.submit.click(); + } + +} +module.exports = new RestorePage(); diff --git a/www/wiki/tests/selenium/pageobjects/userlogin.page.js b/www/wiki/tests/selenium/pageobjects/userlogin.page.js index bdbd41bc..0061d0c2 100644 --- a/www/wiki/tests/selenium/pageobjects/userlogin.page.js +++ b/www/wiki/tests/selenium/pageobjects/userlogin.page.js @@ -19,5 +19,9 @@ class UserLoginPage extends Page { this.loginButton.click(); } + loginAdmin() { + this.login( browser.options.username, browser.options.password ); + } + } module.exports = new UserLoginPage(); diff --git a/www/wiki/tests/selenium/specs/page.js b/www/wiki/tests/selenium/specs/page.js index 06d3d60a..376dce59 100644 --- a/www/wiki/tests/selenium/specs/page.js +++ b/www/wiki/tests/selenium/specs/page.js @@ -1,5 +1,7 @@ 'use strict'; const assert = require( 'assert' ), + DeletePage = require( '../pageobjects/delete.page' ), + RestorePage = require( '../pageobjects/restore.page' ), EditPage = require( '../pageobjects/edit.page' ), HistoryPage = require( '../pageobjects/history.page' ), UserLoginPage = require( '../pageobjects/userlogin.page' ); @@ -9,6 +11,10 @@ describe( 'Page', function () { var content, name; + function getTestString() { + return Math.random().toString() + '-öäü-♠♣♥♦'; + } + before( function () { // disable VisualEditor welcome dialog UserLoginPage.open(); @@ -17,8 +23,8 @@ describe( 'Page', function () { beforeEach( function () { browser.deleteCookie(); - content = Math.random().toString(); - name = Math.random().toString(); + content = getTestString(); + name = getTestString(); } ); it( 'should be creatable', function () { @@ -32,9 +38,29 @@ describe( 'Page', function () { } ); - it( 'should be editable', function () { + it( 'should be re-creatable', function () { + let initialContent = getTestString(); + + // create + browser.call( function () { + return EditPage.apiEdit( name, initialContent ); + } ); + + // delete + browser.call( function () { + return DeletePage.apiDelete( name, 'delete prior to recreate' ); + } ); + + // create + EditPage.edit( name, content ); - var content2 = Math.random().toString(); + // check + assert.equal( EditPage.heading.getText(), name ); + assert.equal( EditPage.displayedContent.getText(), content ); + + } ); + + it( 'should be editable', function () { // create browser.call( function () { @@ -42,11 +68,11 @@ describe( 'Page', function () { } ); // edit - EditPage.edit( name, content2 ); + EditPage.edit( name, content ); // check assert.equal( EditPage.heading.getText(), name ); - assert.equal( EditPage.displayedContent.getText(), content2 ); + assert.equal( EditPage.displayedContent.getText(), content ); } ); @@ -63,4 +89,48 @@ describe( 'Page', function () { } ); + it( 'should be deletable', function () { + + // login + UserLoginPage.loginAdmin(); + + // create + browser.call( function () { + return EditPage.apiEdit( name, content ); + } ); + + // delete + DeletePage.delete( name, content + '-deletereason' ); + + // check + assert.equal( + DeletePage.displayedContent.getText(), + '"' + name + '" has been deleted. See deletion log for a record of recent deletions.\nReturn to Main Page.' + ); + + } ); + + it( 'should be restorable', function () { + + // login + UserLoginPage.loginAdmin(); + + // create + browser.call( function () { + return EditPage.apiEdit( name, content ); + } ); + + // delete + browser.call( function () { + return DeletePage.apiDelete( name, content + '-deletereason' ); + } ); + + // restore + RestorePage.restore( name, content + '-restorereason' ); + + // check + assert.equal( RestorePage.displayedContent.getText(), name + ' has been restored\nConsult the deletion log for a record of recent deletions and restorations.' ); + + } ); + } ); diff --git a/www/wiki/tests/selenium/wdio.conf.jenkins.js b/www/wiki/tests/selenium/wdio.conf.jenkins.js deleted file mode 100644 index 6049eb25..00000000 --- a/www/wiki/tests/selenium/wdio.conf.jenkins.js +++ /dev/null @@ -1,20 +0,0 @@ -/* eslint no-undef: "error" */ -/* eslint-env node */ -'use strict'; -var merge = require( 'deepmerge' ), - wdioConf = require( './wdio.conf.js' ); - -// Overwrite default settings -exports.config = merge( wdioConf.config, { - username: 'WikiAdmin', - password: 'testpass', - screenshotPath: '../log/', - baseUrl: process.env.MW_SERVER + process.env.MW_SCRIPT_PATH, - - reporters: [ 'spec', 'junit' ], - reporterOptions: { - junit: { - outputDir: '../log/' - } - } -} ); diff --git a/www/wiki/tests/selenium/wdio.conf.js b/www/wiki/tests/selenium/wdio.conf.js index 6b65034c..0930a0f1 100644 --- a/www/wiki/tests/selenium/wdio.conf.js +++ b/www/wiki/tests/selenium/wdio.conf.js @@ -1,20 +1,27 @@ -/* eslint-env node */ -/* eslint no-undef: "error" */ -/* eslint-disable no-console, comma-dangle */ 'use strict'; const fs = require( 'fs' ), path = require( 'path' ); +let logPath, password, username; + +// username and password will be used only if +// MEDIAWIKI_USER or MEDIAWIKI_PASSWORD environment variables are not set +if ( process.env.JENKINS_HOME ) { + logPath = '../log/'; + password = 'testpass'; + username = 'WikiAdmin'; +} else { + logPath = './log/'; + password = 'vagrant'; + username = 'Admin'; +} + function relPath( foo ) { return path.resolve( __dirname, '../..', foo ); } exports.config = { - - // - // ====== - // // ====== // Custom // ====== @@ -24,10 +31,10 @@ exports.config = { // Use if from tests with: // browser.options.username username: process.env.MEDIAWIKI_USER === undefined ? - 'Admin' : + username : process.env.MEDIAWIKI_USER, password: process.env.MEDIAWIKI_PASSWORD === undefined ? - 'vagrant' : + password : process.env.MEDIAWIKI_PASSWORD, // // ====== @@ -50,11 +57,11 @@ exports.config = { relPath( './tests/selenium/specs/**/*.js' ), relPath( './extensions/*/tests/selenium/specs/**/*.js' ), relPath( './extensions/VisualEditor/modules/ve-mw/tests/selenium/specs/**/*.js' ), - relPath( './skins/*/tests/selenium/specs/**/*.js' ), + relPath( './skins/*/tests/selenium/specs/**/*.js' ) ], // Patterns to exclude. exclude: [ - // 'path/to/excluded/files' + './extensions/CirrusSearch/tests/selenium/specs/**/*.js' ], // // ============ @@ -114,11 +121,20 @@ exports.config = { // Enables colors for log output. coloredLogs: true, // + // Warns when a deprecated command is used + deprecationWarnings: true, + // + // If you only want to run your tests until a specific amount of tests have failed use + // bail (default is 0 - don't bail, run all tests). + bail: 0, + // // Saves a screenshot to a given path if a command fails. - screenshotPath: './log/', + screenshotPath: logPath, // - // Set a base URL in order to shorten url command calls. If your url parameter starts - // with "/", then the base url gets prepended. + // Set a base URL in order to shorten url command calls. If your `url` parameter starts + // with `/`, the base url gets prepended, not including the path portion of your baseUrl. + // If your `url` parameter starts without a scheme or `/` (like `some/path`), the base url + // gets prepended directly. baseUrl: ( process.env.MW_SERVER === undefined ? 'http://127.0.0.1:8080' : @@ -130,7 +146,7 @@ exports.config = { ), // // Default timeout for all waitFor* commands. - waitforTimeout: 20000, + waitforTimeout: 10000, // // Default timeout in milliseconds for request // if Selenium Grid doesn't send response @@ -147,14 +163,14 @@ exports.config = { // WebdriverRTC: https://github.com/webdriverio/webdriverrtc // Browserevent: https://github.com/webdriverio/browserevent // plugins: { - // webdrivercss: { - // screenshotRoot: 'my-shots', - // failedComparisonsRoot: 'diffs', - // misMatchTolerance: 0.05, - // screenWidth: [320,480,640,1024] - // }, - // webdriverrtc: {}, - // browserevent: {} + // webdrivercss: { + // screenshotRoot: 'my-shots', + // failedComparisonsRoot: 'diffs', + // misMatchTolerance: 0.05, + // screenWidth: [320,480,640,1024] + // }, + // webdriverrtc: {}, + // browserevent: {} // }, // // Test runner services @@ -169,11 +185,16 @@ exports.config = { // Make sure you have the wdio adapter package for the specific framework installed // before running any tests. framework: 'mocha', - + // // Test reporter for stdout. // The only one supported by default is 'dot' // see also: http://webdriver.io/guide/testrunner/reporters.html - reporters: [ 'spec' ], + reporters: [ 'spec', 'junit' ], + reporterOptions: { + junit: { + outputDir: logPath + } + }, // // Options to be passed to Mocha. // See the full list at http://mochajs.org/ @@ -189,41 +210,65 @@ exports.config = { // it and to build services around it. You can either apply a single function or an array of // methods to it. If one of them returns with a promise, WebdriverIO will wait until that promise got // resolved to continue. - // - // Gets executed once before all workers get launched. - // onPrepare: function ( config, capabilities ) { - // } - // - // Gets executed before test execution begins. At this point you can access all global - // variables, such as `browser`. It is the perfect place to define custom commands. + /** + * Gets executed once before all workers get launched. + * @param {Object} config wdio configuration object + * @param {Array.<Object>} capabilities list of capabilities details + */ + // onPrepare: function (config, capabilities) { + // }, + /** + * Gets executed just before initialising the webdriver session and test framework. It allows you + * to manipulate configurations depending on the capability or spec. + * @param {Object} config wdio configuration object + * @param {Array.<Object>} capabilities list of capabilities details + * @param {Array.<String>} specs List of spec file paths that are to be run + */ + // beforeSession: function (config, capabilities, specs) { + // }, + /** + * Gets executed before test execution begins. At this point you can access to all global + * variables like `browser`. It is the perfect place to define custom commands. + * @param {Array.<Object>} capabilities list of capabilities details + * @param {Array.<String>} specs List of spec file paths that are to be run + */ // before: function (capabilities, specs) { // }, - // - // Hook that gets executed before the suite starts - // beforeSuite: function (suite) { + /** + * Runs before a WebdriverIO command gets executed. + * @param {String} commandName hook command name + * @param {Array} args arguments that command would receive + */ + // beforeCommand: function (commandName, args) { // }, - // - // Hook that gets executed _before_ a hook within the suite starts (e.g. runs before calling - // beforeEach in Mocha) - // beforeHook: function () { + /** + * Hook that gets executed before the suite starts + * @param {Object} suite suite details + */ + // beforeSuite: function (suite) { // }, - // - // Hook that gets executed _after_ a hook within the suite starts (e.g. runs after calling - // afterEach in Mocha) - // - // Function to be executed before a test (in Mocha/Jasmine) or a step (in Cucumber) starts. + /** + * Function to be executed before a test (in Mocha/Jasmine) or a step (in Cucumber) starts. + * @param {Object} test test details + */ // beforeTest: function (test) { // }, - // - // Runs before a WebdriverIO command gets executed. - // beforeCommand: function (commandName, args) { + /** + * Hook that gets executed _before_ a hook within the suite starts (e.g. runs before calling + * beforeEach in Mocha) + */ + // beforeHook: function () { // }, - // - // Runs after a WebdriverIO command gets executed - // afterCommand: function (commandName, args, result, error) { + /** + * Hook that gets executed _after_ a hook within the suite ends (e.g. runs after calling + * afterEach in Mocha) + */ + // afterHook: function () { // }, - // - // Function to be executed after a test (in Mocha/Jasmine) or a step (in Cucumber) starts. + /** + * Function to be executed after a test (in Mocha/Jasmine) or a step (in Cucumber) ends. + * @param {Object} test test details + */ // from https://github.com/webdriverio/webdriverio/issues/269#issuecomment-306342170 afterTest: function ( test ) { var filename, filePath; @@ -238,19 +283,46 @@ exports.config = { // save screenshot browser.saveScreenshot( filePath ); console.log( '\n\tScreenshot location:', filePath, '\n' ); - }, + } // - // Hook that gets executed after the suite has ended + /** + * Hook that gets executed after the suite has ended + * @param {Object} suite suite details + */ // afterSuite: function (suite) { // }, - // - // Gets executed after all tests are done. You still have access to all global variables from - // the test. + /** + * Runs after a WebdriverIO command gets executed + * @param {String} commandName hook command name + * @param {Array} args arguments that command would receive + * @param {Number} result 0 - command success, 1 - command error + * @param {Object} error error object if any + */ + // afterCommand: function (commandName, args, result, error) { + // }, + /** + * Gets executed after all tests are done. You still have access to all global variables from + * the test. + * @param {Number} result 0 - test pass, 1 - test fail + * @param {Array.<Object>} capabilities list of capabilities details + * @param {Array.<String>} specs List of spec file paths that ran + */ // after: function (result, capabilities, specs) { // }, - // - // Gets executed after all workers got shut down and the process is about to exit. It is not - // possible to defer the end of the process using a promise. - // onComplete: function(exitCode) { + /** + * Gets executed right after terminating the webdriver session. + * @param {Object} config wdio configuration object + * @param {Array.<Object>} capabilities list of capabilities details + * @param {Array.<String>} specs List of spec file paths that ran + */ + // afterSession: function (config, capabilities, specs) { + // }, + /** + * Gets executed after all workers got shut down and the process is about to exit. + * @param {Object} exitCode 0 - success, 1 - fail + * @param {Object} config wdio configuration object + * @param {Array.<Object>} capabilities list of capabilities details + */ + // onComplete: function(exitCode, config, capabilities) { // } }; diff --git a/www/wiki/tests/testHelpers.inc b/www/wiki/tests/testHelpers.inc deleted file mode 100644 index d04e0fcb..00000000 --- a/www/wiki/tests/testHelpers.inc +++ /dev/null @@ -1,874 +0,0 @@ -<?php -/** - * Recording for passing/failing tests. - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along - * with this program; if not, write to the Free Software Foundation, Inc., - * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - * http://www.gnu.org/copyleft/gpl.html - * - * @file - * @ingroup Testing - */ - -/** - * Interface to record parser test results. - * - * The ITestRecorder is a very simple interface to record the result of - * MediaWiki parser tests. One should call start() before running the - * full parser tests and end() once all the tests have been finished. - * After each test, you should use record() to keep track of your tests - * results. Finally, report() is used to generate a summary of your - * test run, one could dump it to the console for human consumption or - * register the result in a database for tracking purposes. - * - * @since 1.22 - */ -interface ITestRecorder { - - /** - * Called at beginning of the parser test run - */ - public function start(); - - /** - * Called after each test - * @param string $test - * @param integer $subtest - * @param bool $result - */ - public function record( $test, $subtest, $result ); - - /** - * Called before finishing the test run - */ - public function report(); - - /** - * Called at the end of the parser test run - */ - public function end(); - -} - -class TestRecorder implements ITestRecorder { - public $parent; - public $term; - - function __construct( $parent ) { - $this->parent = $parent; - $this->term = $parent->term; - } - - function start() { - $this->total = 0; - $this->success = 0; - } - - function record( $test, $subtest, $result ) { - $this->total++; - $this->success += ( $result ? 1 : 0 ); - } - - function end() { - // dummy - } - - function report() { - if ( $this->total > 0 ) { - $this->reportPercentage( $this->success, $this->total ); - } else { - throw new MWException( "No tests found.\n" ); - } - } - - function reportPercentage( $success, $total ) { - $ratio = wfPercent( 100 * $success / $total ); - print $this->term->color( 1 ) . "Passed $success of $total tests ($ratio)... "; - - if ( $success == $total ) { - print $this->term->color( 32 ) . "ALL TESTS PASSED!"; - } else { - $failed = $total - $success; - print $this->term->color( 31 ) . "$failed tests failed!"; - } - - print $this->term->reset() . "\n"; - - return ( $success == $total ); - } -} - -class DbTestPreviewer extends TestRecorder { - protected $lb; // /< Database load balancer - protected $db; // /< Database connection to the main DB - protected $curRun; // /< run ID number for the current run - protected $prevRun; // /< run ID number for the previous run, if any - protected $results; // /< Result array - - /** - * This should be called before the table prefix is changed - * @param TestRecorder $parent - */ - function __construct( $parent ) { - parent::__construct( $parent ); - - $this->lb = wfGetLBFactory()->newMainLB(); - // This connection will have the wiki's table prefix, not parsertest_ - $this->db = $this->lb->getConnection( DB_MASTER ); - } - - /** - * Set up result recording; insert a record for the run with the date - * and all that fun stuff - */ - function start() { - parent::start(); - - if ( !$this->db->tableExists( 'testrun', __METHOD__ ) - || !$this->db->tableExists( 'testitem', __METHOD__ ) - ) { - print "WARNING> `testrun` table not found in database.\n"; - $this->prevRun = false; - } else { - // We'll make comparisons against the previous run later... - $this->prevRun = $this->db->selectField( 'testrun', 'MAX(tr_id)' ); - } - - $this->results = []; - } - - function getName( $test, $subtest ) { - if ( $subtest ) { - return "$test subtest #$subtest"; - } else { - return $test; - } - } - - function record( $test, $subtest, $result ) { - parent::record( $test, $subtest, $result ); - $this->results[ $this->getName( $test, $subtest ) ] = $result; - } - - function report() { - if ( $this->prevRun ) { - // f = fail, p = pass, n = nonexistent - // codes show before then after - $table = [ - 'fp' => 'previously failing test(s) now PASSING! :)', - 'pn' => 'previously PASSING test(s) removed o_O', - 'np' => 'new PASSING test(s) :)', - - 'pf' => 'previously passing test(s) now FAILING! :(', - 'fn' => 'previously FAILING test(s) removed O_o', - 'nf' => 'new FAILING test(s) :(', - 'ff' => 'still FAILING test(s) :(', - ]; - - $prevResults = []; - - $res = $this->db->select( 'testitem', [ 'ti_name', 'ti_success' ], - [ 'ti_run' => $this->prevRun ], __METHOD__ ); - - foreach ( $res as $row ) { - if ( !$this->parent->regex - || preg_match( "/{$this->parent->regex}/i", $row->ti_name ) - ) { - $prevResults[$row->ti_name] = $row->ti_success; - } - } - - $combined = array_keys( $this->results + $prevResults ); - - # Determine breakdown by change type - $breakdown = []; - foreach ( $combined as $test ) { - if ( !isset( $prevResults[$test] ) ) { - $before = 'n'; - } elseif ( $prevResults[$test] == 1 ) { - $before = 'p'; - } else /* if ( $prevResults[$test] == 0 )*/ { - $before = 'f'; - } - - if ( !isset( $this->results[$test] ) ) { - $after = 'n'; - } elseif ( $this->results[$test] == 1 ) { - $after = 'p'; - } else /*if ( $this->results[$test] == 0 ) */ { - $after = 'f'; - } - - $code = $before . $after; - - if ( isset( $table[$code] ) ) { - $breakdown[$code][$test] = $this->getTestStatusInfo( $test, $after ); - } - } - - # Write out results - foreach ( $table as $code => $label ) { - if ( !empty( $breakdown[$code] ) ) { - $count = count( $breakdown[$code] ); - printf( "\n%4d %s\n", $count, $label ); - - foreach ( $breakdown[$code] as $differing_test_name => $statusInfo ) { - print " * $differing_test_name [$statusInfo]\n"; - } - } - } - } else { - print "No previous test runs to compare against.\n"; - } - - print "\n"; - parent::report(); - } - - /** - * Returns a string giving information about when a test last had a status change. - * Could help to track down when regressions were introduced, as distinct from tests - * which have never passed (which are more change requests than regressions). - * @param string $testname - * @param string $after - * @return string - */ - private function getTestStatusInfo( $testname, $after ) { - // If we're looking at a test that has just been removed, then say when it first appeared. - if ( $after == 'n' ) { - $changedRun = $this->db->selectField( 'testitem', - 'MIN(ti_run)', - [ 'ti_name' => $testname ], - __METHOD__ ); - $appear = $this->db->selectRow( 'testrun', - [ 'tr_date', 'tr_mw_version' ], - [ 'tr_id' => $changedRun ], - __METHOD__ ); - - return "First recorded appearance: " - . date( "d-M-Y H:i:s", strtotime( $appear->tr_date ) ) - . ", " . $appear->tr_mw_version; - } - - // Otherwise, this test has previous recorded results. - // See when this test last had a different result to what we're seeing now. - $conds = [ - 'ti_name' => $testname, - 'ti_success' => ( $after == 'f' ? "1" : "0" ) ]; - - if ( $this->curRun ) { - $conds[] = "ti_run != " . $this->db->addQuotes( $this->curRun ); - } - - $changedRun = $this->db->selectField( 'testitem', 'MAX(ti_run)', $conds, __METHOD__ ); - - // If no record of ever having had a different result. - if ( is_null( $changedRun ) ) { - if ( $after == "f" ) { - return "Has never passed"; - } else { - return "Has never failed"; - } - } - - // Otherwise, we're looking at a test whose status has changed. - // (i.e. it used to work, but now doesn't; or used to fail, but is now fixed.) - // In this situation, give as much info as we can as to when it changed status. - $pre = $this->db->selectRow( 'testrun', - [ 'tr_date', 'tr_mw_version' ], - [ 'tr_id' => $changedRun ], - __METHOD__ ); - $post = $this->db->selectRow( 'testrun', - [ 'tr_date', 'tr_mw_version' ], - [ "tr_id > " . $this->db->addQuotes( $changedRun ) ], - __METHOD__, - [ "LIMIT" => 1, "ORDER BY" => 'tr_id' ] - ); - - if ( $post ) { - $postDate = date( "d-M-Y H:i:s", strtotime( $post->tr_date ) ) . ", {$post->tr_mw_version}"; - } else { - $postDate = 'now'; - } - - return ( $after == "f" ? "Introduced" : "Fixed" ) . " between " - . date( "d-M-Y H:i:s", strtotime( $pre->tr_date ) ) . ", " . $pre->tr_mw_version - . " and $postDate"; - } - - /** - * Close the DB connection - */ - function end() { - $this->lb->closeAll(); - parent::end(); - } -} - -class DbTestRecorder extends DbTestPreviewer { - public $version; - - /** - * Set up result recording; insert a record for the run with the date - * and all that fun stuff - */ - function start() { - $this->db->begin( __METHOD__ ); - - if ( !$this->db->tableExists( 'testrun' ) - || !$this->db->tableExists( 'testitem' ) - ) { - print "WARNING> `testrun` table not found in database. Trying to create table.\n"; - $this->db->sourceFile( $this->db->patchPath( 'patch-testrun.sql' ) ); - echo "OK, resuming.\n"; - } - - parent::start(); - - $this->db->insert( 'testrun', - [ - 'tr_date' => $this->db->timestamp(), - 'tr_mw_version' => $this->version, - 'tr_php_version' => PHP_VERSION, - 'tr_db_version' => $this->db->getServerVersion(), - 'tr_uname' => php_uname() - ], - __METHOD__ ); - if ( $this->db->getType() === 'postgres' ) { - $this->curRun = $this->db->currentSequenceValue( 'testrun_id_seq' ); - } else { - $this->curRun = $this->db->insertId(); - } - } - - /** - * Record an individual test item's success or failure to the db - * - * @param string $test - * @param bool $result - */ - function record( $test, $subtest, $result ) { - parent::record( $test, $subtest, $result ); - - $this->db->insert( 'testitem', - [ - 'ti_run' => $this->curRun, - 'ti_name' => $this->getName( $test, $subtest ), - 'ti_success' => $result ? 1 : 0, - ], - __METHOD__ ); - } - - /** - * Commit transaction and clean up for result recording - */ - function end() { - $this->db->commit( __METHOD__ ); - parent::end(); - } -} - -class TestFileIterator implements Iterator { - private $file; - private $fh; - /** - * @var ParserTest|MediaWikiParserTest An instance of ParserTest (parserTests.php) - * or MediaWikiParserTest (phpunit) - */ - private $parserTest; - private $index = 0; - private $test; - private $section = null; - /** String|null: current test section being analyzed */ - private $sectionData = []; - private $lineNum; - private $eof; - # Create a fake parser tests which never run anything unless - # asked to do so. This will avoid running hooks for a disabled test - private $delayedParserTest; - private $nextSubTest = 0; - - function __construct( $file, $parserTest ) { - $this->file = $file; - $this->fh = fopen( $this->file, "rt" ); - - if ( !$this->fh ) { - throw new MWException( "Couldn't open file '$file'\n" ); - } - - $this->parserTest = $parserTest; - $this->delayedParserTest = new DelayedParserTest(); - - $this->lineNum = $this->index = 0; - } - - function rewind() { - if ( fseek( $this->fh, 0 ) ) { - throw new MWException( "Couldn't fseek to the start of '$this->file'\n" ); - } - - $this->index = -1; - $this->lineNum = 0; - $this->eof = false; - $this->next(); - - return true; - } - - function current() { - return $this->test; - } - - function key() { - return $this->index; - } - - function next() { - if ( $this->readNextTest() ) { - $this->index++; - return true; - } else { - $this->eof = true; - } - } - - function valid() { - return $this->eof != true; - } - - function setupCurrentTest() { - // "input" and "result" are old section names allowed - // for backwards-compatibility. - $input = $this->checkSection( [ 'wikitext', 'input' ], false ); - $result = $this->checkSection( [ 'html/php', 'html/*', 'html', 'result' ], false ); - // some tests have "with tidy" and "without tidy" variants - $tidy = $this->checkSection( [ 'html/php+tidy', 'html+tidy' ], false ); - if ( $tidy != false ) { - if ( $this->nextSubTest == 0 ) { - if ( $result != false ) { - $this->nextSubTest = 1; // rerun non-tidy variant later - } - $result = $tidy; - } else { - $this->nextSubTest = 0; // go on to next test after this - $tidy = false; - } - } - - if ( !isset( $this->sectionData['options'] ) ) { - $this->sectionData['options'] = ''; - } - - if ( !isset( $this->sectionData['config'] ) ) { - $this->sectionData['config'] = ''; - } - - $isDisabled = preg_match( '/\\bdisabled\\b/i', $this->sectionData['options'] ) && - !$this->parserTest->runDisabled; - $isParsoidOnly = preg_match( '/\\bparsoid\\b/i', $this->sectionData['options'] ) && - $result == 'html' && - !$this->parserTest->runParsoid; - $isFiltered = !preg_match( "/" . $this->parserTest->regex . "/i", $this->sectionData['test'] ); - if ( $input == false || $result == false || $isDisabled || $isParsoidOnly || $isFiltered ) { - # disabled test - return false; - } - - # We are really going to run the test, run pending hooks and hooks function - wfDebug( __METHOD__ . " unleashing delayed test for: {$this->sectionData['test']}" ); - $hooksResult = $this->delayedParserTest->unleash( $this->parserTest ); - if ( !$hooksResult ) { - # Some hook reported an issue. Abort. - throw new MWException( "Problem running requested parser hook from the test file" ); - } - - $this->test = [ - 'test' => ParserTest::chomp( $this->sectionData['test'] ), - 'subtest' => $this->nextSubTest, - 'input' => ParserTest::chomp( $this->sectionData[$input] ), - 'result' => ParserTest::chomp( $this->sectionData[$result] ), - 'options' => ParserTest::chomp( $this->sectionData['options'] ), - 'config' => ParserTest::chomp( $this->sectionData['config'] ), - ]; - if ( $tidy != false ) { - $this->test['options'] .= " tidy"; - } - return true; - } - - function readNextTest() { - # Run additional subtests of previous test - while ( $this->nextSubTest > 0 ) { - if ( $this->setupCurrentTest() ) { - return true; - } - } - - $this->clearSection(); - # Reset hooks for the delayed test object - $this->delayedParserTest->reset(); - - while ( false !== ( $line = fgets( $this->fh ) ) ) { - $this->lineNum++; - $matches = []; - - if ( preg_match( '/^!!\s*(\S+)/', $line, $matches ) ) { - $this->section = strtolower( $matches[1] ); - - if ( $this->section == 'endarticle' ) { - $this->checkSection( 'text' ); - $this->checkSection( 'article' ); - - $this->parserTest->addArticle( - ParserTest::chomp( $this->sectionData['article'] ), - $this->sectionData['text'], $this->lineNum ); - - $this->clearSection(); - - continue; - } - - if ( $this->section == 'endhooks' ) { - $this->checkSection( 'hooks' ); - - foreach ( explode( "\n", $this->sectionData['hooks'] ) as $line ) { - $line = trim( $line ); - - if ( $line ) { - $this->delayedParserTest->requireHook( $line ); - } - } - - $this->clearSection(); - - continue; - } - - if ( $this->section == 'endfunctionhooks' ) { - $this->checkSection( 'functionhooks' ); - - foreach ( explode( "\n", $this->sectionData['functionhooks'] ) as $line ) { - $line = trim( $line ); - - if ( $line ) { - $this->delayedParserTest->requireFunctionHook( $line ); - } - } - - $this->clearSection(); - - continue; - } - - if ( $this->section == 'endtransparenthooks' ) { - $this->checkSection( 'transparenthooks' ); - - foreach ( explode( "\n", $this->sectionData['transparenthooks'] ) as $line ) { - $line = trim( $line ); - - if ( $line ) { - $this->delayedParserTest->requireTransparentHook( $line ); - } - } - - $this->clearSection(); - - continue; - } - - if ( $this->section == 'end' ) { - $this->checkSection( 'test' ); - do { - if ( $this->setupCurrentTest() ) { - return true; - } - } while ( $this->nextSubTest > 0 ); - # go on to next test (since this was disabled) - $this->clearSection(); - $this->delayedParserTest->reset(); - continue; - } - - if ( isset( $this->sectionData[$this->section] ) ) { - throw new MWException( "duplicate section '$this->section' " - . "at line {$this->lineNum} of $this->file\n" ); - } - - $this->sectionData[$this->section] = ''; - - continue; - } - - if ( $this->section ) { - $this->sectionData[$this->section] .= $line; - } - } - - return false; - } - - /** - * Clear section name and its data - */ - private function clearSection() { - $this->sectionData = []; - $this->section = null; - - } - - /** - * Verify the current section data has some value for the given token - * name(s) (first parameter). - * Throw an exception if it is not set, referencing current section - * and adding the current file name and line number - * - * @param string|array $tokens Expected token(s) that should have been - * mentioned before closing this section - * @param bool $fatal True iff an exception should be thrown if - * the section is not found. - * @return bool|string - * @throws MWException - */ - private function checkSection( $tokens, $fatal = true ) { - if ( is_null( $this->section ) ) { - throw new MWException( __METHOD__ . " can not verify a null section!\n" ); - } - if ( !is_array( $tokens ) ) { - $tokens = [ $tokens ]; - } - if ( count( $tokens ) == 0 ) { - throw new MWException( __METHOD__ . " can not verify zero sections!\n" ); - } - - $data = $this->sectionData; - $tokens = array_filter( $tokens, function ( $token ) use ( $data ) { - return isset( $data[$token] ); - } ); - - if ( count( $tokens ) == 0 ) { - if ( !$fatal ) { - return false; - } - throw new MWException( sprintf( - "'%s' without '%s' at line %s of %s\n", - $this->section, - implode( ',', $tokens ), - $this->lineNum, - $this->file - ) ); - } - if ( count( $tokens ) > 1 ) { - throw new MWException( sprintf( - "'%s' with unexpected tokens '%s' at line %s of %s\n", - $this->section, - implode( ',', $tokens ), - $this->lineNum, - $this->file - ) ); - } - - return array_values( $tokens )[0]; - } -} - -/** - * An iterator for use as a phpunit data provider. Provides the test arguments - * in the order expected by NewParserTest::testParserTest(). - */ -class TestFileDataProvider extends TestFileIterator { - function current() { - $test = parent::current(); - if ( $test ) { - return [ - $test['test'], - $test['input'], - $test['result'], - $test['options'], - $test['config'], - ]; - } else { - return $test; - } - } -} - -/** - * A class to delay execution of a parser test hooks. - */ -class DelayedParserTest { - - /** Initialized on construction */ - private $hooks; - private $fnHooks; - private $transparentHooks; - - public function __construct() { - $this->reset(); - } - - /** - * Init/reset or forgot about the current delayed test. - * Call to this will erase any hooks function that were pending. - */ - public function reset() { - $this->hooks = []; - $this->fnHooks = []; - $this->transparentHooks = []; - } - - /** - * Called whenever we actually want to run the hook. - * Should be the case if we found the parserTest is not disabled - * @param ParserTest|NewParserTest $parserTest - * @return bool - * @throws MWException - */ - public function unleash( &$parserTest ) { - if ( !( $parserTest instanceof ParserTest || $parserTest instanceof NewParserTest ) ) { - throw new MWException( __METHOD__ . " must be passed an instance of ParserTest or " - . "NewParserTest classes\n" ); - } - - # Trigger delayed hooks. Any failure will make us abort - foreach ( $this->hooks as $hook ) { - $ret = $parserTest->requireHook( $hook ); - if ( !$ret ) { - return false; - } - } - - # Trigger delayed function hooks. Any failure will make us abort - foreach ( $this->fnHooks as $fnHook ) { - $ret = $parserTest->requireFunctionHook( $fnHook ); - if ( !$ret ) { - return false; - } - } - - # Trigger delayed transparent hooks. Any failure will make us abort - foreach ( $this->transparentHooks as $hook ) { - $ret = $parserTest->requireTransparentHook( $hook ); - if ( !$ret ) { - return false; - } - } - - # Delayed execution was successful. - return true; - } - - /** - * Similar to ParserTest object but does not run anything - * Use unleash() to really execute the hook - * @param string $hook - */ - public function requireHook( $hook ) { - $this->hooks[] = $hook; - } - - /** - * Similar to ParserTest object but does not run anything - * Use unleash() to really execute the hook function - * @param string $fnHook - */ - public function requireFunctionHook( $fnHook ) { - $this->fnHooks[] = $fnHook; - } - - /** - * Similar to ParserTest object but does not run anything - * Use unleash() to really execute the hook function - * @param string $hook - */ - public function requireTransparentHook( $hook ) { - $this->transparentHooks[] = $hook; - } - -} - -/** - * Initialize and detect the DjVu files support - */ -class DjVuSupport { - - /** - * Initialises DjVu tools global with default values - */ - public function __construct() { - global $wgDjvuRenderer, $wgDjvuDump, $wgDjvuToXML, $wgFileExtensions, $wgDjvuTxt; - - $wgDjvuRenderer = $wgDjvuRenderer ? $wgDjvuRenderer : '/usr/bin/ddjvu'; - $wgDjvuDump = $wgDjvuDump ? $wgDjvuDump : '/usr/bin/djvudump'; - $wgDjvuToXML = $wgDjvuToXML ? $wgDjvuToXML : '/usr/bin/djvutoxml'; - $wgDjvuTxt = $wgDjvuTxt ? $wgDjvuTxt : '/usr/bin/djvutxt'; - - if ( !in_array( 'djvu', $wgFileExtensions ) ) { - $wgFileExtensions[] = 'djvu'; - } - } - - /** - * Returns true if the DjVu tools are usable - * - * @return bool - */ - public function isEnabled() { - global $wgDjvuRenderer, $wgDjvuDump, $wgDjvuToXML, $wgDjvuTxt; - - return is_executable( $wgDjvuRenderer ) - && is_executable( $wgDjvuDump ) - && is_executable( $wgDjvuToXML ) - && is_executable( $wgDjvuTxt ); - } -} - -/** - * Initialize and detect the tidy support - */ -class TidySupport { - private $internalTidy; - private $externalTidy; - - /** - * Determine if there is a usable tidy. - */ - public function __construct() { - global $wgTidyBin; - - $this->internalTidy = extension_loaded( 'tidy' ) && - class_exists( 'tidy' ) && !wfIsHHVM(); - - $this->externalTidy = is_executable( $wgTidyBin ) || - Installer::locateExecutableInDefaultPaths( [ $wgTidyBin ] ) - !== false; - } - - /** - * Returns true if we should use internal tidy. - * - * @return bool - */ - public function isInternal() { - return $this->internalTidy; - } - - /** - * Returns true if tidy is usable - * - * @return bool - */ - public function isEnabled() { - return $this->internalTidy || $this->externalTidy; - } -} |