From: jenkins-bot Date: Tue, 17 Oct 2017 19:22:57 +0000 (+0000) Subject: Merge "RCFilters: Move parameter operations to ViewModel" X-Git-Tag: 1.31.0-rc.0~1739 X-Git-Url: https://git.heureux-cyclage.org/?p=lhc%2Fweb%2Fwiklou.git;a=commitdiff_plain;h=cb615a80599a409976518e7564cc6d993b772714;hp=38c19921e32d2ff14c2edaf327ad8519d185f18e Merge "RCFilters: Move parameter operations to ViewModel" --- diff --git a/includes/FeedUtils.php b/includes/FeedUtils.php index 0def6a0402..b1c3ce615a 100644 --- a/includes/FeedUtils.php +++ b/includes/FeedUtils.php @@ -236,18 +236,18 @@ class FeedUtils { */ public static function applyDiffStyle( $text ) { $styles = [ - 'diff' => 'background-color: white; color:black;', - 'diff-otitle' => 'background-color: white; color:black; text-align: center;', - 'diff-ntitle' => 'background-color: white; color:black; text-align: center;', - 'diff-addedline' => 'color:black; font-size: 88%; border-style: solid; ' + 'diff' => 'background-color: #fff; color: #222;', + 'diff-otitle' => 'background-color: #fff; color: #222; text-align: center;', + 'diff-ntitle' => 'background-color: #fff; color: #222; text-align: center;', + 'diff-addedline' => 'color: #222; font-size: 88%; border-style: solid; ' . 'border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #a3d3ff; ' . 'vertical-align: top; white-space: pre-wrap;', - 'diff-deletedline' => 'color:black; font-size: 88%; border-style: solid; ' + 'diff-deletedline' => 'color: #222; font-size: 88%; border-style: solid; ' . 'border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #ffe49c; ' . 'vertical-align: top; white-space: pre-wrap;', - 'diff-context' => 'background-color: #f9f9f9; color: #333333; font-size: 88%; ' + 'diff-context' => 'background-color: #f8f9fa; color: #222; font-size: 88%; ' . 'border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; ' - . 'border-color: #e6e6e6; vertical-align: top; white-space: pre-wrap;', + . 'border-color: #eaecf0; vertical-align: top; white-space: pre-wrap;', 'diffchange' => 'font-weight: bold; text-decoration: none;', ]; diff --git a/includes/GitInfo.php b/includes/GitInfo.php index 3c600ed984..8095fd7308 100644 --- a/includes/GitInfo.php +++ b/includes/GitInfo.php @@ -23,6 +23,8 @@ * @file */ +use MediaWiki\Shell\Shell; + class GitInfo { /** @@ -221,13 +223,19 @@ class GitInfo { is_executable( $wgGitBin ) && $this->getHead() !== false ) { - $environment = [ "GIT_DIR" => $this->basedir ]; - $cmd = wfEscapeShellArg( $wgGitBin ) . - " show -s --format=format:%ct HEAD"; - $retc = false; - $commitDate = wfShellExec( $cmd, $retc, $environment ); - if ( $retc === 0 ) { - $date = (int)$commitDate; + $cmd = [ + $wgGitBin, + 'show', + '-s', + '--format=format:%ct', + 'HEAD', + ]; + $result = Shell::command( $cmd ) + ->environment( [ 'GIT_DIR' => $this->basedir ] ) + ->execute(); + + if ( $result->getExitCode() === 0 ) { + $date = (int)$result->getStdout(); } } $this->cache['headCommitDate'] = $date; diff --git a/includes/GlobalFunctions.php b/includes/GlobalFunctions.php index ffdfc92c4d..1cff881c21 100644 --- a/includes/GlobalFunctions.php +++ b/includes/GlobalFunctions.php @@ -3232,6 +3232,7 @@ function wfGetParserCacheStorage() { * @deprecated since 1.25 - use Hooks::run */ function wfRunHooks( $event, array $args = [], $deprecatedVersion = null ) { + wfDeprecated( __METHOD__, '1.25' ); return Hooks::run( $event, $args, $deprecatedVersion ); } diff --git a/includes/changes/ChangesListStringOptionsFilterGroup.php b/includes/changes/ChangesListStringOptionsFilterGroup.php index 59efd82bca..2d1521d5d0 100644 --- a/includes/changes/ChangesListStringOptionsFilterGroup.php +++ b/includes/changes/ChangesListStringOptionsFilterGroup.php @@ -242,4 +242,14 @@ class ChangesListStringOptionsFilterGroup extends ChangesListFilterGroup { return $output; } + + /** + * Check if this filter group is currently active + * + * @param {boolean} $isStructuredUI Is structured filters UI current enabled + */ + public function isActive( $isStructuredUI ) { + // STRING_OPTIONS filter groups are exclusively active on Structured UI + return $isStructuredUI; + } } diff --git a/includes/installer/MssqlUpdater.php b/includes/installer/MssqlUpdater.php index a2aa8c0f1a..411d2c8cb2 100644 --- a/includes/installer/MssqlUpdater.php +++ b/includes/installer/MssqlUpdater.php @@ -104,6 +104,7 @@ class MssqlUpdater extends DatabaseUpdater { // 1.30 [ 'modifyField', 'image', 'img_media_type', 'patch-add-3d.sql' ], + [ 'addIndex', 'site_stats', 'PRIMARY', 'patch-site_stats-pk.sql' ], ]; } diff --git a/includes/installer/OracleUpdater.php b/includes/installer/OracleUpdater.php index 00b96614f8..040b54a124 100644 --- a/includes/installer/OracleUpdater.php +++ b/includes/installer/OracleUpdater.php @@ -125,6 +125,7 @@ class OracleUpdater extends DatabaseUpdater { // 1.30 [ 'doAutoIncrementTriggers' ], + [ 'addIndex', 'site_stats', 'PRIMARY', 'patch-site_stats-pk.sql' ], // KEEP THIS AT THE BOTTOM!! [ 'doRebuildDuplicateFunction' ], diff --git a/includes/installer/PostgresUpdater.php b/includes/installer/PostgresUpdater.php index 0475fe4078..1f17fecc2b 100644 --- a/includes/installer/PostgresUpdater.php +++ b/includes/installer/PostgresUpdater.php @@ -481,6 +481,7 @@ class PostgresUpdater extends DatabaseUpdater { [ 'changeNullableField', 'protected_titles', 'pt_reason', 'NOT NULL', true ], [ 'addPgField', 'protected_titles', 'pt_reason_id', 'INTEGER NOT NULL DEFAULT 0' ], [ 'addTable', 'comment', 'patch-comment-table.sql' ], + [ 'addIndex', 'site_stats', 'PRIMARY', 'patch-site_stats-pk.sql' ], ]; } diff --git a/includes/shell/Command.php b/includes/shell/Command.php index d5a1bb3f6c..4fc282c8c4 100644 --- a/includes/shell/Command.php +++ b/includes/shell/Command.php @@ -135,6 +135,11 @@ class Command { * @return $this */ public function limits( array $limits ) { + if ( !isset( $limits['walltime'] ) && isset( $limits['time'] ) ) { + // Emulate the behavior of old wfShellExec() where walltime fell back on time + // if the latter was overridden and the former wasn't + $limits['walltime'] = $limits['time']; + } $this->limits = $limits + $this->limits; return $this; @@ -227,8 +232,6 @@ class Command { if ( is_executable( '/bin/bash' ) ) { $time = intval( $this->limits['time'] ); $wallTime = intval( $this->limits['walltime'] ); - // for b/c, wall time falls back to time - $wallTime = min( $time, $wallTime ); $mem = intval( $this->limits['memory'] ); $filesize = intval( $this->limits['filesize'] ); diff --git a/includes/specialpage/ChangesListSpecialPage.php b/includes/specialpage/ChangesListSpecialPage.php index 20fd06a52c..3f45250508 100644 --- a/includes/specialpage/ChangesListSpecialPage.php +++ b/includes/specialpage/ChangesListSpecialPage.php @@ -1295,8 +1295,10 @@ abstract class ChangesListSpecialPage extends SpecialPage { // URL parameters can be per-group, like 'userExpLevel', // or per-filter, like 'hideminor'. if ( $filterGroup->isPerGroupRequestParameter() ) { - $filterGroup->modifyQuery( $dbr, $this, $tables, $fields, $conds, - $query_options, $join_conds, $opts[$filterGroup->getName()] ); + if ( $filterGroup->isActive( $isStructuredUI ) ) { + $filterGroup->modifyQuery( $dbr, $this, $tables, $fields, $conds, + $query_options, $join_conds, $opts[$filterGroup->getName()] ); + } } else { foreach ( $filterGroup->getFilters() as $filter ) { if ( $filter->isActive( $opts, $isStructuredUI ) ) { diff --git a/languages/data/Names.php b/languages/data/Names.php index e2ed910686..1266561f9f 100644 --- a/languages/data/Names.php +++ b/languages/data/Names.php @@ -370,7 +370,7 @@ class Names { 'sd' => 'سنڌي', # Sindhi 'sdc' => 'Sassaresu', # Sassarese 'sdh' => 'کوردی خوارگ', # Southern Kurdish - 'se' => 'sámegiella', # Northern Sami + 'se' => 'davvisámegiella', # Northern Sami 'sei' => 'Cmique Itom', # Seri 'ses' => 'Koyraboro Senni', # Koyraboro Senni 'sg' => 'Sängö', # Sango/Sangho diff --git a/maintenance/mssql/archives/patch-site_stats-pk.sql b/maintenance/mssql/archives/patch-site_stats-pk.sql new file mode 100644 index 0000000000..7533719d9f --- /dev/null +++ b/maintenance/mssql/archives/patch-site_stats-pk.sql @@ -0,0 +1,2 @@ +DROP INDEX ss_row_id ON site_stats; +ALTER TABLE /*_*/site_stats ADD CONSTRAINT /*i*/ss_row_id PRIMARY KEY (ss_row_id); diff --git a/maintenance/mssql/tables.sql b/maintenance/mssql/tables.sql index 2a672945ea..119cd5b874 100644 --- a/maintenance/mssql/tables.sql +++ b/maintenance/mssql/tables.sql @@ -450,7 +450,7 @@ CREATE INDEX /*i*/iwl_prefix_from_title ON /*_*/iwlinks (iwl_prefix, iwl_from, i -- CREATE TABLE /*_*/site_stats ( -- The single row should contain 1 here. - ss_row_id int NOT NULL, + ss_row_id int NOT NULL CONSTRAINT /*i*/ss_row_id PRIMARY KEY, -- Total number of edits performed. ss_total_edits bigint default 0, @@ -475,9 +475,6 @@ CREATE TABLE /*_*/site_stats ( ss_images int default 0 ); --- Pointless index to assuage developer superstitions -CREATE UNIQUE INDEX /*i*/ss_row_id ON /*_*/site_stats (ss_row_id); - -- -- The internet is full of jerks, alas. Sometimes it's handy diff --git a/maintenance/oracle/archives/patch-site_stats-pk.sql b/maintenance/oracle/archives/patch-site_stats-pk.sql new file mode 100644 index 0000000000..a288c08d4d --- /dev/null +++ b/maintenance/oracle/archives/patch-site_stats-pk.sql @@ -0,0 +1,4 @@ +define mw_prefix='{$wgDBprefix}'; + +ALTER TABLE &mw_prefix.site_stats DROP CONSTRAINT &mw_prefix.site_stats_u01; +ALTER TABLE &mw_prefix.site_stats ADD CONSTRAINT &mw_prefix.site_stats_pk PRIMARY KEY(ss_row_id); diff --git a/maintenance/oracle/tables.sql b/maintenance/oracle/tables.sql index 44c907c4d4..e6e2e5657c 100644 --- a/maintenance/oracle/tables.sql +++ b/maintenance/oracle/tables.sql @@ -321,7 +321,7 @@ CREATE UNIQUE INDEX &mw_prefix.iwlinks_ui01 ON &mw_prefix.iwlinks (iwl_from, iwl CREATE UNIQUE INDEX &mw_prefix.iwlinks_ui02 ON &mw_prefix.iwlinks (iwl_prefix, iwl_title, iwl_from); CREATE TABLE &mw_prefix.site_stats ( - ss_row_id NUMBER NOT NULL , + ss_row_id NUMBER NOT NULL PRIMARY KEY, ss_total_edits NUMBER DEFAULT 0, ss_good_articles NUMBER DEFAULT 0, ss_total_pages NUMBER DEFAULT -1, @@ -329,7 +329,6 @@ CREATE TABLE &mw_prefix.site_stats ( ss_active_users NUMBER DEFAULT -1, ss_images NUMBER DEFAULT 0 ); -CREATE UNIQUE INDEX &mw_prefix.site_stats_u01 ON &mw_prefix.site_stats (ss_row_id); CREATE SEQUENCE ipblocks_ipb_id_seq; CREATE TABLE &mw_prefix.ipblocks ( diff --git a/maintenance/postgres/archives/patch-site_stats-pk.sql b/maintenance/postgres/archives/patch-site_stats-pk.sql new file mode 100644 index 0000000000..faa5e9f846 --- /dev/null +++ b/maintenance/postgres/archives/patch-site_stats-pk.sql @@ -0,0 +1,3 @@ +ALTER TABLE site_stats DROP CONSTRAINT site_stats_ss_row_id_key; +ALTER TABLE site_stats ADD PRIMARY KEY (ss_row_id); +ALTER TABLE site_stats ALTER ss_row_id SET DEFAULT 0; diff --git a/maintenance/postgres/tables.sql b/maintenance/postgres/tables.sql index eea9e68514..d6d2f24c11 100644 --- a/maintenance/postgres/tables.sql +++ b/maintenance/postgres/tables.sql @@ -296,7 +296,7 @@ CREATE INDEX langlinks_lang_title ON langlinks (ll_lang,ll_title); CREATE TABLE site_stats ( - ss_row_id INTEGER NOT NULL UNIQUE, + ss_row_id INTEGER NOT NULL PRIMARY KEY DEFAULT 0, ss_total_edits INTEGER DEFAULT 0, ss_good_articles INTEGER DEFAULT 0, ss_total_pages INTEGER DEFAULT -1, diff --git a/resources/src/mediawiki.action/mediawiki.action.view.redirect.js b/resources/src/mediawiki.action/mediawiki.action.view.redirect.js index 39a122d981..4e73354637 100644 --- a/resources/src/mediawiki.action/mediawiki.action.view.redirect.js +++ b/resources/src/mediawiki.action/mediawiki.action.view.redirect.js @@ -31,7 +31,7 @@ history.replaceState( /* data= */ history.state, /* title= */ document.title, /* url= */ canonical ); if ( shouldChangeFragment ) { // Specification for history.replaceState() doesn't require browser to scroll, - // so scroll to be sure (see also T110501). Support for IE9 and IE10. + // so scroll to be sure (see also T110501). Support for IE10. node = document.getElementById( fragment.slice( 1 ) ); if ( node ) { node.scrollIntoView(); diff --git a/resources/src/mediawiki.widgets/mw.widgets.TitleWidget.js b/resources/src/mediawiki.widgets/mw.widgets.TitleWidget.js index db56bd3f2e..83a261258d 100644 --- a/resources/src/mediawiki.widgets/mw.widgets.TitleWidget.js +++ b/resources/src/mediawiki.widgets/mw.widgets.TitleWidget.js @@ -22,6 +22,7 @@ * @cfg {boolean} [showRedirectTargets=true] Show the targets of redirects * @cfg {boolean} [showImages] Show page images * @cfg {boolean} [showDescriptions] Show page descriptions + * @cfg {boolean} [showMissing=true] Show missing pages * @cfg {boolean} [excludeCurrentPage] Exclude the current page from suggestions * @cfg {boolean} [validateTitle=true] Whether the input must be a valid title (if set to true, * the widget will marks itself red for invalid inputs, including an empty query). @@ -44,6 +45,7 @@ this.showRedirectTargets = config.showRedirectTargets !== false; this.showImages = !!config.showImages; this.showDescriptions = !!config.showDescriptions; + this.showMissing = config.showMissing !== false; this.excludeCurrentPage = !!config.excludeCurrentPage; this.validateTitle = config.validateTitle !== undefined ? config.validateTitle : true; this.cache = config.cache; @@ -131,35 +133,35 @@ // Do nothing. This is just so OOUI doesn't break due to abort being undefined. } }; - if ( mw.Title.newFromText( query ) ) { - return this.getInterwikiPrefixesPromise().then( function ( interwikiPrefixes ) { - var interwiki = query.substring( 0, query.indexOf( ':' ) ); - if ( - interwiki && interwiki !== '' && - interwikiPrefixes.indexOf( interwiki ) !== -1 - ) { - return $.Deferred().resolve( { query: { - pages: [ { - title: query - } ] - } } ).promise( promiseAbortObject ); - } else { - req = api.get( widget.getApiParams( query ) ); - promiseAbortObject.abort = req.abort.bind( req ); // TODO ew - return req.then( function ( ret ) { - if ( ret.query === undefined ) { - ret = api.get( { action: 'query', titles: query } ); - promiseAbortObject.abort = ret.abort.bind( ret ); - } - return ret; - } ); - } - } ).promise( promiseAbortObject ); - } else { + if ( !mw.Title.newFromText( query ) ) { // Don't send invalid titles to the API. // Just pretend it returned nothing so we can show the 'invalid title' section return $.Deferred().resolve( {} ).promise( promiseAbortObject ); } + + return this.getInterwikiPrefixesPromise().then( function ( interwikiPrefixes ) { + var interwiki = query.substring( 0, query.indexOf( ':' ) ); + if ( + interwiki && interwiki !== '' && + interwikiPrefixes.indexOf( interwiki ) !== -1 + ) { + return $.Deferred().resolve( { query: { + pages: [ { + title: query + } ] + } } ).promise( promiseAbortObject ); + } else { + req = api.get( widget.getApiParams( query ) ); + promiseAbortObject.abort = req.abort.bind( req ); // TODO ew + return req.then( function ( ret ) { + if ( widget.showMissing && ret.query === undefined ) { + ret = api.get( { action: 'query', titles: query } ); + promiseAbortObject.abort = ret.abort.bind( ret ); + } + return ret; + } ); + } + } ).promise( promiseAbortObject ); }; /** @@ -227,6 +229,7 @@ for ( index in data.pages ) { suggestionPage = data.pages[ index ]; + // When excludeCurrentPage is set, don't list the current page unless the user has type the full title if ( this.excludeCurrentPage && suggestionPage.title === currentPageName && suggestionPage.title !== titleObj.getPrefixedText() ) { continue; diff --git a/resources/src/mediawiki/mediawiki.diff.styles.css b/resources/src/mediawiki/mediawiki.diff.styles.css index 7a73e98487..634502087f 100644 --- a/resources/src/mediawiki/mediawiki.diff.styles.css +++ b/resources/src/mediawiki/mediawiki.diff.styles.css @@ -71,9 +71,9 @@ td.diff-marker { } .diff-context { - background: #f9f9f9; - border-color: #e6e6e6; - color: #333; + background: #f8f9fa; + border-color: #eaecf0; + color: #222; } .diffchange { diff --git a/tests/common/TestsAutoLoader.php b/tests/common/TestsAutoLoader.php index a965d3e493..ca31fbc7ec 100644 --- a/tests/common/TestsAutoLoader.php +++ b/tests/common/TestsAutoLoader.php @@ -66,7 +66,6 @@ $wgAutoloadClasses += [ # tests/phpunit/includes 'RevisionTestModifyableContent' => "$testDir/phpunit/includes/RevisionTestModifyableContent.php", 'RevisionTestModifyableContentHandler' => "$testDir/phpunit/includes/RevisionTestModifyableContentHandler.php", - 'RevisionStorageTest' => "$testDir/phpunit/includes/RevisionStorageTest.php", 'TestLogger' => "$testDir/phpunit/includes/TestLogger.php", # tests/phpunit/includes/api diff --git a/tests/phpunit/includes/HooksTest.php b/tests/phpunit/includes/HooksTest.php index 87acb52e1a..666051666b 100644 --- a/tests/phpunit/includes/HooksTest.php +++ b/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; diff --git a/tests/phpunit/includes/RevisionIntegrationTest.php b/tests/phpunit/includes/RevisionIntegrationTest.php new file mode 100644 index 0000000000..ac7331a0f5 --- /dev/null +++ b/tests/phpunit/includes/RevisionIntegrationTest.php @@ -0,0 +1,873 @@ +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', + ] + ); + + MWNamespace::clearCaches(); + // Reset namespace cache + $wgContLang->resetNamespaces(); + if ( !$this->testPage ) { + $this->testPage = WikiPage::factory( Title::newFromText( 'UTPage' ) ); + } + } + + protected function tearDown() { + global $wgContLang; + + parent::tearDown(); + + MWNamespace::clearCaches(); + // Reset namespace cache + $wgContLang->resetNamespaces(); + } + + private function makeRevisionWithProps( $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->testPage->getId(); + } + + $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::__construct + */ + public function testConstructFromRow() { + $latestRevisionId = $this->testPage->getLatest(); + $latestRevision = $this->testPage->getRevision(); + + $dbr = wfGetDB( DB_REPLICA ); + $res = $dbr->select( + 'revision', + Revision::selectFields(), + [ 'rev_id' => $latestRevisionId ] + ); + $this->assertTrue( is_object( $res ), 'query failed' ); + + $row = $res->fetchObject(); + $res->free(); + + $this->assertRevEquals( $latestRevision, new Revision( $row ) ); + } + + /** + * @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 ); + $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 ); + } + + public function provideTrueFalse() { + yield [ true ]; + yield [ false ]; + } + + /** + * @dataProvider provideTrueFalse + * @covers Revision::newFromArchiveRow + */ + public function testNewFromArchiveRow( $contentHandlerUseDB ) { + $this->setMwGlobals( 'wgContentHandlerUseDB', $contentHandlerUseDB ); + + $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->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() { + $this->testPage->doEditContent( new WikitextContent( __METHOD__ ), __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(); + + $res = Revision::fetchRevision( $this->testPage->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::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( __METHOD__, $rev->getContent()->getNativeData() ); + } + + /** + * @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(), $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 ]; + } + + /** + * @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, + 'summary' => '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, + 'summary' => '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, + 'summary' => '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, + 'summary' => '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, + '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 ); + } + + /** + * @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' ], + [ 'hello world', 'User:hello/there.css', null, null, 'CssContentHandler' ], + [ serialize( 'hello world' ), 'Dummy:Hello', null, null, 'DummyContentHandlerForTesting' ], + ]; + } + + /** + * @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 + ] ); + + $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." ); + } + + 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->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() + ) + ); + } + +} diff --git a/tests/phpunit/includes/RevisionStorageTest.php b/tests/phpunit/includes/RevisionStorageTest.php deleted file mode 100644 index a15b9b4407..0000000000 --- a/tests/phpunit/includes/RevisionStorageTest.php +++ /dev/null @@ -1,624 +0,0 @@ -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 $wgContLang; - - parent::setUp(); - - $this->mergeMwGlobalArrayValue( - 'wgExtraNamespaces', - [ - 12312 => 'Dummy', - 12313 => 'Dummy_talk', - ] - ); - - $this->mergeMwGlobalArrayValue( - 'wgNamespaceContentModels', - [ - 12312 => 'DUMMY', - ] - ); - - $this->mergeMwGlobalArrayValue( - 'wgContentHandlers', - [ - 'DUMMY' => 'DummyContentHandlerForTesting', - ] - ); - - MWNamespace::clearCaches(); - // Reset namespace cache - $wgContLang->resetNamespaces(); - 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 $wgContLang; - - parent::tearDown(); - - MWNamespace::clearCaches(); - // Reset namespace cache - $wgContLang->resetNamespaces(); - } - - 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; - } - - /** - * @param string $titleString - * @param string $text - * @param string|null $model - * - * @return WikiPage - */ - protected 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; - } - - 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::newFromTitle - */ - public function testNewFromTitle_withoutId() { - $page = $this->createPage( - __METHOD__, - 'GOAT', - CONTENT_MODEL_WIKITEXT - ); - $latestRevId = $page->getLatest(); - - $rev = Revision::newFromTitle( $page->getTitle() ); - - $this->assertTrue( $page->getTitle()->equals( $rev->getTitle() ) ); - $this->assertEquals( $latestRevId, $rev->getId() ); - } - - /** - * @covers Revision::newFromTitle - */ - public function testNewFromTitle_withId() { - $page = $this->createPage( - __METHOD__, - 'GOAT', - CONTENT_MODEL_WIKITEXT - ); - $latestRevId = $page->getLatest(); - - $rev = Revision::newFromTitle( $page->getTitle(), $latestRevId ); - - $this->assertTrue( $page->getTitle()->equals( $rev->getTitle() ) ); - $this->assertEquals( $latestRevId, $rev->getId() ); - } - - /** - * @covers Revision::newFromTitle - */ - public function testNewFromTitle_withBadId() { - $page = $this->createPage( - __METHOD__, - 'GOAT', - CONTENT_MODEL_WIKITEXT - ); - $latestRevId = $page->getLatest(); - - $rev = Revision::newFromTitle( $page->getTitle(), $latestRevId + 1 ); - - $this->assertNull( $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() { - 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 ]; - } - - /** - * @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, - 'summary' => '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, - 'summary' => '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, - 'summary' => '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, - 'summary' => '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, - 'summary' => 'edit four' - ] ); - $revisions[4]->insertOn( $dbw ); - - // test it --------------------------------- - $since = $revisions[$sinceIdx]->getTimestamp(); - - $wasLast = Revision::userWasLastToEdit( $dbw, $page->getId(), $userA->getId(), $since ); - - $this->assertEquals( $expectedLast, $wasLast ); - } -} diff --git a/tests/phpunit/includes/RevisionTest.php b/tests/phpunit/includes/RevisionTest.php deleted file mode 100644 index ef4d127d85..0000000000 --- a/tests/phpunit/includes/RevisionTest.php +++ /dev/null @@ -1,478 +0,0 @@ -setMwGlobals( [ - 'wgContLang' => Language::factory( 'en' ), - 'wgLanguageCode' => 'en', - 'wgLegacyEncoding' => false, - 'wgCompressRevisions' => false, - - 'wgContentHandlerTextFallback' => 'ignore', - ] ); - - $this->mergeMwGlobalArrayValue( - 'wgExtraNamespaces', - [ - 12312 => 'Dummy', - 12313 => 'Dummy_talk', - ] - ); - - $this->mergeMwGlobalArrayValue( - 'wgNamespaceContentModels', - [ - 12312 => 'testing', - ] - ); - - $this->mergeMwGlobalArrayValue( - 'wgContentHandlers', - [ - 'testing' => 'DummyContentHandlerForTesting', - 'RevisionTestModifyableContent' => 'RevisionTestModifyableContentHandler', - ] - ); - - MWNamespace::clearCaches(); - // Reset namespace cache - $wgContLang->resetNamespaces(); - } - - protected function tearDown() { - global $wgContLang; - - MWNamespace::clearCaches(); - // Reset namespace cache - $wgContLang->resetNamespaces(); - - parent::tearDown(); - } - - public function provideConstructFromArray() { - yield 'with text' => [ - [ - 'text' => 'hello world.', - 'content_model' => CONTENT_MODEL_JAVASCRIPT - ], - ]; - yield 'with content' => [ - [ - 'content' => new JavaScriptContent( 'hellow world.' ) - ], - ]; - } - - /** - * @dataProvider provideConstructFromArray - */ - public function testConstructFromArray( $rowArray ) { - $rev = new Revision( $rowArray ); - $this->assertNotNull( $rev->getContent(), 'no content object available' ); - $this->assertEquals( CONTENT_MODEL_JAVASCRIPT, $rev->getContent()->getModel() ); - $this->assertEquals( CONTENT_MODEL_JAVASCRIPT, $rev->getContentModel() ); - } - - public function provideConstructFromArrayThrowsExceptions() { - yield 'content and text_id both not empty' => [ - [ - '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 MWException( 'Revision constructor passed invalid row format.' ) - ]; - } - - /** - * @dataProvider provideConstructFromArrayThrowsExceptions - */ - public function testConstructFromArrayThrowsExceptions( $rowArray, Exception $expectedException ) { - $this->setExpectedException( - get_class( $expectedException ), - $expectedException->getMessage(), - $expectedException->getCode() - ); - new Revision( $rowArray ); - } - - public function provideGetRevisionText() { - yield 'Generic test' => [ - 'This is a goat of revision text.', - [ - 'old_flags' => '', - 'old_text' => 'This is a goat of revision text.', - ], - ]; - } - - /** - * @covers Revision::getRevisionText - * @dataProvider provideGetRevisionText - */ - public function testGetRevisionText( $expected, $rowData, $prefix = 'old_', $wiki = false ) { - $this->assertEquals( - $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 testGetRevisionWithZlibExtension( $expected, $rowData ) { - $this->checkPHPExtension( 'zlib' ); - $this->testGetRevisionText( $expected, $rowData ); - } - - public function provideGetRevisionTextWithLegacyEncoding() { - yield 'Utf8Native' => [ - "Wiki est l'\xc3\xa9cole superieur !", - 'iso-8859-1', - [ - 'old_flags' => 'utf-8', - 'old_text' => "Wiki est l'\xc3\xa9cole superieur !", - ] - ]; - yield 'Utf8Legacy' => [ - "Wiki est l'\xc3\xa9cole superieur !", - 'iso-8859-1', - [ - 'old_flags' => '', - 'old_text' => "Wiki est l'\xe9cole superieur !", - ] - ]; - } - - /** - * @covers Revision::getRevisionText - * @dataProvider provideGetRevisionTextWithLegacyEncoding - */ - public function testGetRevisionWithLegacyEncoding( $expected, $encoding, $rowData ) { - $GLOBALS['wgLegacyEncoding'] = $encoding; - $this->testGetRevisionText( $expected, $rowData ); - } - - public function provideGetRevisionTextWithGzipAndLegacyEncoding() { - yield 'Utf8NativeGzip' => [ - "Wiki est l'\xc3\xa9cole superieur !", - '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 !", - 'iso-8859-1', - [ - 'old_flags' => 'gzip', - 'old_text' => gzdeflate( "Wiki est l'\xe9cole superieur !" ), - ] - ]; - } - - /** - * @covers Revision::getRevisionText - * @dataProvider provideGetRevisionTextWithGzipAndLegacyEncoding - */ - public function testGetRevisionWithGzipAndLegacyEncoding( $expected, $encoding, $rowData ) { - $this->checkPHPExtension( 'zlib' ); - $GLOBALS['wgLegacyEncoding'] = $encoding; - $this->testGetRevisionText( $expected, $rowData ); - } - - /** - * @covers Revision::compressRevisionText - */ - public function testCompressRevisionTextUtf8() { - $row = new stdClass; - $row->old_text = "Wiki est l'\xc3\xa9cole superieur !"; - $row->old_flags = Revision::compressRevisionText( $row->old_text ); - $this->assertTrue( false !== strpos( $row->old_flags, 'utf-8' ), - "Flags should contain 'utf-8'" ); - $this->assertFalse( false !== strpos( $row->old_flags, 'gzip' ), - "Flags should not contain 'gzip'" ); - $this->assertEquals( "Wiki est l'\xc3\xa9cole superieur !", - $row->old_text, "Direct check" ); - $this->assertEquals( "Wiki est l'\xc3\xa9cole superieur !", - Revision::getRevisionText( $row ), "getRevisionText" ); - } - - /** - * @covers Revision::compressRevisionText - */ - public function testCompressRevisionTextUtf8Gzip() { - $this->checkPHPExtension( 'zlib' ); - $this->setMwGlobals( 'wgCompressRevisions', true ); - - $row = new stdClass; - $row->old_text = "Wiki est l'\xc3\xa9cole superieur !"; - $row->old_flags = Revision::compressRevisionText( $row->old_text ); - $this->assertTrue( false !== strpos( $row->old_flags, 'utf-8' ), - "Flags should contain 'utf-8'" ); - $this->assertTrue( false !== strpos( $row->old_flags, 'gzip' ), - "Flags should contain 'gzip'" ); - $this->assertEquals( "Wiki est l'\xc3\xa9cole superieur !", - gzinflate( $row->old_text ), "Direct check" ); - $this->assertEquals( "Wiki est l'\xc3\xa9cole superieur !", - Revision::getRevisionText( $row ), "getRevisionText" ); - } - - /** - * @param string $text - * @param string $title - * @param string $model - * @param 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, "testing" ], - ]; - } - - /** - * @group Database - * @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, "testing" ], - ]; - } - - /** - * @group Database - * @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' ], - [ 'hello world', 'User:hello/there.css', null, null, 'CssContentHandler' ], - [ serialize( 'hello world' ), 'Dummy:Hello', null, null, 'DummyContentHandlerForTesting' ], - ]; - } - - /** - * @group Database - * @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', - "testing", - null, - Revision::FOR_PUBLIC, - serialize( 'hello world' ) - ], - [ - serialize( 'hello world' ), - 'Dummy:Hello', - null, - null, - Revision::FOR_PUBLIC, - serialize( 'hello world' ) - ], - ]; - } - - /** - * @group Database - * @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 ) - ); - } - - public function provideGetSize() { - return [ - [ "hello world.", CONTENT_MODEL_WIKITEXT, 12 ], - [ serialize( "hello world." ), "testing", 12 ], - ]; - } - - /** - * @covers Revision::getSize - * @group Database - * @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." ), - "testing", - Revision::base36Sha1( serialize( "hello world." ) ) - ], - ]; - } - - /** - * @covers Revision::getSha1 - * @group Database - * @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. - * - * @group Database - * @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. - * - * @group Database - * @covers Revision::getContent - */ - public function testGetContentUncloned() { - $rev = $this->newTestRevision( "hello", "testGetContentUncloned_dummy", CONTENT_MODEL_WIKITEXT ); - $content = $rev->getContent( Revision::RAW ); - $content2 = $rev->getContent( Revision::RAW ); - - // for immutable content like wikitext, this should be the same object - $this->assertSame( $content, $content2 ); - } -} diff --git a/tests/phpunit/includes/RevisionTestModifyableContent.php b/tests/phpunit/includes/RevisionTestModifyableContent.php index 11e4e183f3..6dcba53c6e 100644 --- a/tests/phpunit/includes/RevisionTestModifyableContent.php +++ b/tests/phpunit/includes/RevisionTestModifyableContent.php @@ -2,8 +2,10 @@ class RevisionTestModifyableContent extends TextContent { + const MODEL_ID = "RevisionTestModifyableContent"; + public function __construct( $text ) { - parent::__construct( $text, "RevisionTestModifyableContent" ); + parent::__construct( $text, self::MODEL_ID ); } public function copy() { diff --git a/tests/phpunit/includes/RevisionTestModifyableContentHandler.php b/tests/phpunit/includes/RevisionTestModifyableContentHandler.php index e262b40d05..bc4e40a417 100644 --- a/tests/phpunit/includes/RevisionTestModifyableContentHandler.php +++ b/tests/phpunit/includes/RevisionTestModifyableContentHandler.php @@ -3,7 +3,7 @@ class RevisionTestModifyableContentHandler extends TextContentHandler { public function __construct() { - parent::__construct( "RevisionTestModifyableContent", [ CONTENT_FORMAT_TEXT ] ); + parent::__construct( RevisionTestModifyableContent::MODEL_ID, [ CONTENT_FORMAT_TEXT ] ); } public function unserializeContent( $text, $format = null ) { diff --git a/tests/phpunit/includes/RevisionUnitTest.php b/tests/phpunit/includes/RevisionUnitTest.php new file mode 100644 index 0000000000..89f5a0ecc4 --- /dev/null +++ b/tests/phpunit/includes/RevisionUnitTest.php @@ -0,0 +1,493 @@ + [ + [ + 'text' => 'hello world.', + 'content_model' => CONTENT_MODEL_JAVASCRIPT + ], + ]; + yield 'with content' => [ + [ + 'content' => new JavaScriptContent( 'hellow world.' ) + ], + ]; + } + + /** + * @dataProvider provideConstructFromArray + */ + public function testConstructFromArray( $rowArray ) { + $rev = new Revision( $rowArray ); + $this->assertNotNull( $rev->getContent(), 'no content object available' ); + $this->assertEquals( CONTENT_MODEL_JAVASCRIPT, $rev->getContent()->getModel() ); + $this->assertEquals( CONTENT_MODEL_JAVASCRIPT, $rev->getContentModel() ); + } + + public function provideConstructFromArrayThrowsExceptions() { + yield 'content and text_id both not empty' => [ + [ + '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 MWException( 'Revision constructor passed invalid row format.' ) + ]; + } + + /** + * @dataProvider provideConstructFromArrayThrowsExceptions + */ + public function testConstructFromArrayThrowsExceptions( $rowArray, Exception $expectedException ) { + $this->setExpectedException( + get_class( $expectedException ), + $expectedException->getMessage(), + $expectedException->getCode() + ); + new Revision( $rowArray ); + } + + 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 ); + $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( [] ); + $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( [] ); + $rev->setUserIdAndName( $inputId, $name ); + $this->assertSame( $expectedId, $rev->getUser( Revision::RAW ) ); + $this->assertEquals( $name, $rev->getUserText( Revision::RAW ) ); + } + + public function provideGetTextId() { + yield [ [], null ]; + yield [ [ 'text_id' => '123' ], 123 ]; + yield [ [ 'text_id' => 456 ], 456 ]; + } + + /** + * @dataProvider provideGetTextId + * @covers Revision::getTextId() + */ + public function testGetTextId( $rowArray, $expected ) { + $rev = new Revision( $rowArray ); + $this->assertSame( $expected, $rev->getTextId() ); + } + + 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 ); + $this->assertSame( $expected, $rev->getParentId() ); + } + + /** + * @covers Revision::getRevisionText + * @dataProvider provideGetRevisionText + */ + public function testGetRevisionText( $expected, $rowData, $prefix = 'old_', $wiki = false ) { + $this->assertEquals( + $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 testGetRevisionWithZlibExtension( $expected, $rowData ) { + $this->checkPHPExtension( 'zlib' ); + $this->testGetRevisionText( $expected, $rowData ); + } + + public function provideGetRevisionTextWithLegacyEncoding() { + yield 'Utf8Native' => [ + "Wiki est l'\xc3\xa9cole superieur !", + 'iso-8859-1', + [ + 'old_flags' => 'utf-8', + 'old_text' => "Wiki est l'\xc3\xa9cole superieur !", + ] + ]; + yield 'Utf8Legacy' => [ + "Wiki est l'\xc3\xa9cole superieur !", + 'iso-8859-1', + [ + 'old_flags' => '', + 'old_text' => "Wiki est l'\xe9cole superieur !", + ] + ]; + } + + /** + * @covers Revision::getRevisionText + * @dataProvider provideGetRevisionTextWithLegacyEncoding + */ + public function testGetRevisionWithLegacyEncoding( $expected, $encoding, $rowData ) { + $this->setMwGlobals( 'wgLegacyEncoding', $encoding ); + $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 !", + '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 !", + 'iso-8859-1', + [ + 'old_flags' => 'gzip', + 'old_text' => gzdeflate( "Wiki est l'\xe9cole superieur !" ), + ] + ]; + } + + /** + * @covers Revision::getRevisionText + * @dataProvider provideGetRevisionTextWithGzipAndLegacyEncoding + */ + public function testGetRevisionWithGzipAndLegacyEncoding( $expected, $encoding, $rowData ) { + $this->checkPHPExtension( 'zlib' ); + $this->setMwGlobals( 'wgLegacyEncoding', $encoding ); + $this->testGetRevisionText( $expected, $rowData ); + } + + /** + * @covers Revision::compressRevisionText + */ + public function testCompressRevisionTextUtf8() { + $row = new stdClass; + $row->old_text = "Wiki est l'\xc3\xa9cole superieur !"; + $row->old_flags = Revision::compressRevisionText( $row->old_text ); + $this->assertTrue( false !== strpos( $row->old_flags, 'utf-8' ), + "Flags should contain 'utf-8'" ); + $this->assertFalse( false !== strpos( $row->old_flags, 'gzip' ), + "Flags should not contain 'gzip'" ); + $this->assertEquals( "Wiki est l'\xc3\xa9cole superieur !", + $row->old_text, "Direct check" ); + $this->assertEquals( "Wiki est l'\xc3\xa9cole superieur !", + Revision::getRevisionText( $row ), "getRevisionText" ); + } + + /** + * @covers Revision::compressRevisionText + */ + public function testCompressRevisionTextUtf8Gzip() { + $this->checkPHPExtension( 'zlib' ); + $this->setMwGlobals( 'wgCompressRevisions', true ); + + $row = new stdClass; + $row->old_text = "Wiki est l'\xc3\xa9cole superieur !"; + $row->old_flags = Revision::compressRevisionText( $row->old_text ); + $this->assertTrue( false !== strpos( $row->old_flags, 'utf-8' ), + "Flags should contain 'utf-8'" ); + $this->assertTrue( false !== strpos( $row->old_flags, 'gzip' ), + "Flags should contain 'gzip'" ); + $this->assertEquals( "Wiki est l'\xc3\xa9cole superieur !", + gzinflate( $row->old_text ), "Direct check" ); + $this->assertEquals( "Wiki est l'\xc3\xa9cole superieur !", + Revision::getRevisionText( $row ), "getRevisionText" ); + } + + /** + * @covers Revision::userJoinCond + */ + public function testUserJoinCond() { + $this->assertEquals( + [ 'LEFT JOIN', [ 'rev_user != 0', 'user_id = rev_user' ] ], + Revision::userJoinCond() + ); + } + + /** + * @covers Revision::pageJoinCond + */ + public function testPageJoinCond() { + $this->assertEquals( + [ 'INNER JOIN', [ 'page_id = rev_page' ] ], + Revision::pageJoinCond() + ); + } + + public function provideSelectFields() { + yield [ + true, + [ + 'rev_id', + 'rev_page', + 'rev_text_id', + 'rev_timestamp', + 'rev_user_text', + 'rev_user', + 'rev_minor_edit', + 'rev_deleted', + 'rev_len', + 'rev_parent_id', + 'rev_sha1', + 'rev_comment_text' => 'rev_comment', + 'rev_comment_data' => 'NULL', + 'rev_comment_cid' => 'NULL', + 'rev_content_format', + 'rev_content_model', + ] + ]; + yield [ + false, + [ + 'rev_id', + 'rev_page', + 'rev_text_id', + 'rev_timestamp', + 'rev_user_text', + 'rev_user', + 'rev_minor_edit', + 'rev_deleted', + 'rev_len', + 'rev_parent_id', + 'rev_sha1', + 'rev_comment_text' => 'rev_comment', + 'rev_comment_data' => 'NULL', + 'rev_comment_cid' => 'NULL', + ] + ]; + } + + /** + * @dataProvider provideSelectFields + * @covers Revision::selectFields + * @todo a true unit test would mock CommentStore + */ + public function testSelectFields( $contentHandlerUseDB, $expected ) { + $this->setMwGlobals( 'wgContentHandlerUseDB', $contentHandlerUseDB ); + $this->assertEquals( $expected, Revision::selectFields() ); + } + + public function provideSelectArchiveFields() { + yield [ + true, + [ + 'ar_id', + 'ar_page_id', + 'ar_rev_id', + 'ar_text', + 'ar_text_id', + 'ar_timestamp', + 'ar_user_text', + 'ar_user', + 'ar_minor_edit', + 'ar_deleted', + 'ar_len', + 'ar_parent_id', + 'ar_sha1', + 'ar_comment_text' => 'ar_comment', + 'ar_comment_data' => 'NULL', + 'ar_comment_cid' => 'NULL', + 'ar_content_format', + 'ar_content_model', + ] + ]; + yield [ + false, + [ + 'ar_id', + 'ar_page_id', + 'ar_rev_id', + 'ar_text', + 'ar_text_id', + 'ar_timestamp', + 'ar_user_text', + 'ar_user', + 'ar_minor_edit', + 'ar_deleted', + 'ar_len', + 'ar_parent_id', + 'ar_sha1', + 'ar_comment_text' => 'ar_comment', + 'ar_comment_data' => 'NULL', + 'ar_comment_cid' => 'NULL', + ] + ]; + } + + /** + * @dataProvider provideSelectArchiveFields + * @covers Revision::selectArchiveFields + * @todo a true unit test would mock CommentStore + */ + public function testSelectArchiveFields( $contentHandlerUseDB, $expected ) { + $this->setMwGlobals( 'wgContentHandlerUseDB', $contentHandlerUseDB ); + $this->assertEquals( $expected, Revision::selectArchiveFields() ); + } + + /** + * @covers Revision::selectTextFields + */ + public function testSelectTextFields() { + $this->assertEquals( + [ + 'old_text', + 'old_flags', + ], + Revision::selectTextFields() + ); + } + + /** + * @covers Revision::selectPageFields + */ + public function testSelectPageFields() { + $this->assertEquals( + [ + 'page_namespace', + 'page_title', + 'page_id', + 'page_latest', + 'page_is_redirect', + 'page_len', + ], + Revision::selectPageFields() + ); + } + + /** + * @covers Revision::selectUserFields + */ + public function testSelectUserFields() { + $this->assertEquals( + [ + 'user_name', + ], + Revision::selectUserFields() + ); + } + + public function provideFetchFromConds() { + yield [ 0, [] ]; + yield [ Revision::READ_LOCKING, [ 'FOR UPDATE' ] ]; + } + + /** + * @dataProvider provideFetchFromConds + * @covers Revision::fetchFromConds + */ + public function testFetchFromConds( $flags, array $options ) { + $conditions = [ 'conditionsArray' ]; + + $db = $this->getMock( IDatabase::class ); + $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->equalTo( 'Revision::fetchFromConds' ), + $this->equalTo( $options ), + // We don't really care about the join conds are they come from the joinCond methods + $this->isType( 'array' ) + ) + ->willReturn( 'RETURNVALUE' ); + + $wrapper = TestingAccessWrapper::newFromClass( Revision::class ); + $result = $wrapper->fetchFromConds( $db, $conditions, $flags ); + + $this->assertEquals( 'RETURNVALUE', $result ); + } +} diff --git a/tests/phpunit/includes/specialpage/ChangesListSpecialPageTest.php b/tests/phpunit/includes/specialpage/ChangesListSpecialPageTest.php index d96758892e..5a0834a651 100644 --- a/tests/phpunit/includes/specialpage/ChangesListSpecialPageTest.php +++ b/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( diff --git a/tests/phpunit/mocks/content/DummyContentForTesting.php b/tests/phpunit/mocks/content/DummyContentForTesting.php index cdb3f78ab8..4392964982 100644 --- a/tests/phpunit/mocks/content/DummyContentForTesting.php +++ b/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; } diff --git a/tests/phpunit/mocks/content/DummyContentHandlerForTesting.php b/tests/phpunit/mocks/content/DummyContentHandlerForTesting.php index 6b9b782397..78d5dc7b28 100644 --- a/tests/phpunit/mocks/content/DummyContentHandlerForTesting.php +++ b/tests/phpunit/mocks/content/DummyContentHandlerForTesting.php @@ -3,7 +3,7 @@ class DummyContentHandlerForTesting extends ContentHandler { public function __construct( $dataModel ) { - parent::__construct( $dataModel, [ "testing" ] ); + parent::__construct( $dataModel, [ DummyContentForTesting::MODEL_ID ] ); } /**