(Bug 41352) Provide tests for edit conflicts.
authordaniel <daniel.kinzler@wikimedia.de>
Sat, 10 Nov 2012 16:24:48 +0000 (17:24 +0100)
committerdaniel <daniel.kinzler@wikimedia.de>
Sat, 10 Nov 2012 16:24:48 +0000 (17:24 +0100)
This provides several test cases for EditPage, including some for
detecting and resolving edit conflicts. This is in preparation for
finding and eliminating the root cause of bug 41352.

Change-Id: I8f23c6535f7b3ee6462d9c5a19dd06070e77f9d1

tests/phpunit/includes/EditPageTest.php

index eee5f37..b02b08a 100644 (file)
@@ -1,7 +1,13 @@
 <?php
 
 /**
- * @group Editing      
+ * @group Editing
+ *
+ * @group Database
+ *        ^--- tell jenkins this test needs the database
+ *
+ * @group medium
+ *        ^--- tell phpunit that these test cases may take longer than 2 seconds.
  */
 class EditPageTest extends MediaWikiTestCase {
 
@@ -37,4 +43,387 @@ class EditPageTest extends MediaWikiTestCase {
                        ),
                );
        }
+
+       protected function forceRevisionDate( WikiPage $page, $timestamp ) {
+               $dbw = wfGetDB( DB_MASTER );
+
+               $dbw->update( 'revision',
+                       array( 'rev_timestamp' => $timestamp ),
+                       array( 'rev_id' => $page->getLatest() ) );
+
+               $page->clear();
+       }
+
+       /**
+        * User input text is passed to rtrim() by edit page. This is a simple
+        * wrapper around assertEquals() which calls rrtrim() to normalize the
+        * expected and actual texts.
+        */
+       function assertEditedTextEquals( $expected, $actual, $msg='' ) {
+               return $this->assertEquals( rtrim($expected), rtrim($actual), $msg );
+       }
+
+       /**
+        * Performs an edit and checks the result.
+        *
+        * @param String|Title $title The title of the page to edit
+        * @param String|null $baseText Some text to create the page with before attempting the edit.
+        * @param User|String|null $user The user to perform the edit as.
+        * @param array $edit An array of request parameters used to define the edit to perform.
+        *              Some well known fields are:
+        *              * wpTextbox1: the text to submit
+        *              * wpSummary: the edit summary
+        *              * wpEditToken: the edit token (will be inserted if not provided)
+        *              * wpEdittime: timestamp of the edit's base revision (will be inserted if not provided)
+        *              * wpStarttime: timestamp when the edit started (will be inserted if not provided)
+        *              * wpSectionTitle: the section to edit
+        *              * wpMinorEdit: mark as minor edit
+        *              * wpWatchthis: whether to watch the page
+        * @param int|null $expectedCode The expected result code (EditPage::AS_XXX constants).
+        *                  Set to null to skip the check. Defaults to EditPage::AS_OK.
+        * @param String|null $expectedText The text expected to be on the page after the edit.
+        *                  Set to null to skip the check.
+        * @param String|null $message An optional message to show along with any error message.
+        *
+        * @return WikiPage The page that was just edited, useful for getting the edit's rev_id, etc.
+        */
+       protected function assertEdit( $title, $baseText, $user = null, array $edit,
+               $expectedCode = EditPage::AS_OK, $expectedText = null, $message = null
+       ) {
+               if ( is_string( $title ) ) {
+                       $ns = $this->getDefaultWikitextNS();
+                       $title = Title::newFromText( $title, $ns );
+               }
+
+               if ( is_string( $user ) ) {
+                       $user = User::newFromName( $user );
+
+                       if ( $user->getId() === 0 ) {
+                               $user->addToDatabase();
+                       }
+               }
+
+               $page = WikiPage::factory( $title );
+
+               if ( $baseText !== null ) {
+                       $content = ContentHandler::makeContent( $baseText, $title );
+                       $page->doEditContent( $content, "base text for test" );
+                       $this->forceRevisionDate( $page, '20120101000000' );
+
+                       //sanity check
+                       $page->clear();
+                       $currentText = ContentHandler::getContentText( $page->getContent() );
+
+                       # EditPage rtrim() the user input, so we alter our expected text
+                       # to reflect that.
+                       $this->assertEditedTextEquals( $baseText, $currentText );
+               }
+
+               if ( $user == null ) {
+                       $user = $GLOBALS['wgUser'];
+               } else {
+                       $this->setMwGlobals( 'wgUser', $user );
+               }
+
+               if ( !isset( $edit['wpEditToken'] ) ) {
+                       $edit['wpEditToken'] = $user->getEditToken();
+               }
+
+               if ( !isset( $edit['wpEdittime'] ) ) {
+                       $edit['wpEdittime'] = $page->exists() ? $page->getTimestamp() : '';
+               }
+
+               if ( !isset( $edit['wpStarttime'] ) ) {
+                       $edit['wpStarttime'] = wfTimestampNow();
+               }
+
+               $req = new FauxRequest( $edit, true ); // session ??
+
+               $ep = new EditPage( new Article( $title ) );
+               $ep->setContextTitle( $title );
+               $ep->importFormData( $req );
+
+               $bot = isset( $edit['bot'] ) ? (bool)$edit['bot'] : false;
+
+               // this is where the edit happens!
+               // Note: don't want to use EditPage::AttemptSave, because it messes with $wgOut
+               // and throws exceptions like PermissionsError
+               $status = $ep->internalAttemptSave( $result, $bot );
+
+               if ( $expectedCode !== null ) {
+                       // check edit code
+                       $this->assertEquals( $expectedCode, $status->value,
+                               "Expected result code mismatch. $message" );
+               }
+
+               $page = WikiPage::factory( $title );
+
+               if ( $expectedText !== null ) {
+                       // check resulting page text
+                       $content = $page->getContent();
+                       $text = ContentHandler::getContentText( $content );
+
+                       # EditPage rtrim() the user input, so we alter our expected text
+                       # to reflect that.
+                       $this->assertEditedTextEquals( $expectedText, $text,
+                               "Expected article text mismatch. $message" );
+               }
+
+               return $page;
+       }
+
+       public function testCreatePage() {
+               $text = "Hello World!";
+               $edit = array(
+                       'wpTextbox1' => $text,
+                       'wpSummary' => 'just testing',
+               );
+
+               $this->assertEdit( 'EditPageTest_testCreatePafe', null, null, $edit,
+                       EditPage::AS_SUCCESS_NEW_ARTICLE, $text,
+                       "expected successfull creation with given text" );
+       }
+
+       public function testUpdatePage() {
+               $text = "one";
+               $edit = array(
+                       'wpTextbox1' => $text,
+                       'wpSummary' => 'first update',
+               );
+
+               $page = $this->assertEdit( 'EditPageTest_testUpdatePage', "zero", null, $edit,
+                       EditPage::AS_SUCCESS_UPDATE, $text,
+                       "expected successfull update with given text" );
+
+               $this->forceRevisionDate( $page, '20120101000000' );
+
+               $text = "two";
+               $edit = array(
+                       'wpTextbox1' => $text,
+                       'wpSummary' => 'second update',
+               );
+
+               $this->assertEdit( 'EditPageTest_testUpdatePage', null, null, $edit,
+                       EditPage::AS_SUCCESS_UPDATE, $text,
+                       "expected successfull update with given text" );
+       }
+
+       public static function provideSectionEdit() {
+               $text =
+'Intro
+
+== one ==
+first section.
+
+== two ==
+second section.
+';
+
+               $sectionOne =
+'== one ==
+hello
+';
+
+               $newSection =
+'== new section ==
+
+hello
+';
+
+               $textWithNewSectionOne = preg_replace( '/== one ==.*== two ==/ms',
+                                                                               "$sectionOne\n== two ==", $text );
+
+               $textWithNewSectionAdded = "$text\n$newSection";
+
+               return array(
+                       array( #0
+                               $text,
+                               '',
+                               'hello',
+                               'replace all',
+                               'hello'
+                       ),
+
+                       array( #1
+                               $text,
+                               '1',
+                               $sectionOne,
+                               'replace first section',
+                               $textWithNewSectionOne,
+                       ),
+
+                       array( #2
+                               $text,
+                               'new',
+                               'hello',
+                               'new section',
+                               $textWithNewSectionAdded,
+                       ),
+               );
+       }
+
+       /**
+        * @dataProvider provideSectionEdit
+        */
+       public function testSectionEdit( $base, $section, $text, $summary, $expected ) {
+               $edit = array(
+                       'wpTextbox1' => $text,
+                       'wpSummary' => $summary,
+                       'wpSection' => $section,
+               );
+
+               $this->assertEdit( 'EditPageTest_testSectionEdit', $base, null, $edit,
+                       EditPage::AS_SUCCESS_UPDATE, $expected,
+                       "expected successfull update of section" );
+       }
+
+       public static function provideAutoMerge() {
+               $tests = array();
+
+               $tests[] = array( #0: plain conflict
+                       "Elmo", # base edit user
+                       "one\n\ntwo\n\nthree\n",
+                       array( #adam's edit
+                               'wpStarttime' => 1,
+                               'wpTextbox1' => "ONE\n\ntwo\n\nthree\n",
+                       ),
+                       array( #berta's edit
+                               'wpStarttime' => 2,
+                               'wpTextbox1' => "(one)\n\ntwo\n\nthree\n",
+                       ),
+                       EditPage::AS_CONFLICT_DETECTED, # expected code
+                       "ONE\n\ntwo\n\nthree\n", # expected text
+                       'expected edit conflict', # message
+               );
+
+               $tests[] = array( #1: successful merge
+                       "Elmo", # base edit user
+                       "one\n\ntwo\n\nthree\n",
+                       array( #adam's edit
+                               'wpStarttime' => 1,
+                               'wpTextbox1' => "ONE\n\ntwo\n\nthree\n",
+                       ),
+                       array( #berta's edit
+                               'wpStarttime' => 2,
+                               'wpTextbox1' => "one\n\ntwo\n\nTHREE\n",
+                       ),
+                       EditPage::AS_SUCCESS_UPDATE, # expected code
+                       "ONE\n\ntwo\n\nTHREE\n", # expected text
+                       'expected automatic merge', # message
+               );
+
+               $text = "Intro\n\n";
+               $text .= "== first section ==\n\n";
+               $text .= "one\n\ntwo\n\nthree\n\n";
+               $text .= "== second section ==\n\n";
+               $text .= "four\n\nfive\n\nsix\n\n";
+
+               // extract the first section.
+               $section = preg_replace( '/.*(== first section ==.*)== second section ==.*/sm', '$1', $text );
+
+               // generate expected text after merge
+               $expected = str_replace( 'one', 'ONE', str_replace( 'three', 'THREE', $text ) );
+
+               $tests[] = array( #2: merge in section
+                       "Elmo", # base edit user
+                       $text,
+                       array( #adam's edit
+                               'wpStarttime' => 1,
+                               'wpTextbox1' => str_replace( 'one', 'ONE', $section ),
+                               'wpSection' => '1'
+                       ),
+                       array( #berta's edit
+                               'wpStarttime' => 2,
+                               'wpTextbox1' => str_replace( 'three', 'THREE', $section ),
+                               'wpSection' => '1'
+                       ),
+                       EditPage::AS_SUCCESS_UPDATE, # expected code
+                       $expected, # expected text
+                       'expected automatic section merge', # message
+               );
+
+               // see whether it makes a difference who did the base edit
+               $testsWithAdam = array_map( function( $test ) {
+                       $test[0] = 'Adam'; // change base edit user
+                       return $test;
+               }, $tests );
+
+               $testsWithBerta = array_map( function( $test ) {
+                       $test[0] = 'Berta'; // change base edit user
+                       return $test;
+               }, $tests );
+
+               return array_merge( $tests, $testsWithAdam, $testsWithBerta );
+       }
+
+       function testHasValidDiff3() {
+               global $wgDiff3;
+
+               if ( !$wgDiff3 ) {
+                       $this->markTestSkipped( "Can't test conflict resolution because \$wgDiff3 is not configured" );
+               } elseif ( !file_exists( $wgDiff3 ) ) {
+                       #XXX: this sucks, since it uses arcane internal knowledge about TextContentHandler::merge3 and wfMerge.
+                       $this->markTestSkipped( "Can't test conflict resolution because \$wgDiff3 is misconfigured: can't find $wgDiff3" );
+               }
+               $this->assertTrue( true );
+       }
+
+       /**
+        * @depend testHasValidDiff3
+        * @dataProvider provideAutoMerge
+        */
+       public function testAutoMerge( $baseUser, $text, $adamsEdit, $bertasEdit,
+                               $expectedCode, $expectedText, $message = null
+       ) {
+
+               //create page
+               $ns = $this->getDefaultWikitextNS();
+               $title = Title::newFromText( 'EditPageTest_testAutoMerge', $ns );
+               $page = WikiPage::factory( $title );
+
+               if ( $page->exists() ) {
+                       $page->doDeleteArticle( "clean slate for testing" );
+               }
+
+               $baseEdit = array(
+                       'wpTextbox1' => $text,
+               );
+
+               $page = $this->assertEdit( 'EditPageTest_testAutoMerge', null,
+                                       $baseUser, $baseEdit, null, null, __METHOD__ );
+
+               $this->forceRevisionDate( $page, '20120101000000' );
+
+               $edittime = $page->getTimestamp();
+
+               // start timestamps for conflict detection
+               if ( !isset( $adamsEdit['wpStarttime'] ) ) {
+                       $adamsEdit['wpStarttime'] = 1;
+               }
+
+               if ( !isset( $bertasEdit['wpStarttime'] ) ) {
+                       $bertasEdit['wpStarttime'] = 2;
+               }
+
+               $starttime = wfTimestampNow();
+               $adamsTime = wfTimestamp( TS_MW, (int)wfTimestamp( TS_UNIX, $starttime ) + (int)$adamsEdit['wpStarttime'] );
+               $bertasTime = wfTimestamp( TS_MW, (int)wfTimestamp( TS_UNIX, $starttime ) + (int)$bertasEdit['wpStarttime'] );
+
+               $adamsEdit['wpStarttime'] = $adamsTime;
+               $bertasEdit['wpStarttime'] = $bertasTime;
+
+               $adamsEdit['wpSummary'] = 'Adam\'s edit';
+               $bertasEdit['wpSummary'] = 'Bertas\'s edit';
+
+               $adamsEdit['wpEdittime'] = $edittime;
+               $bertasEdit['wpEdittime'] = $edittime;
+
+               // first edit
+               $this->assertEdit( 'EditPageTest_testAutoMerge', null, 'Adam', $adamsEdit,
+                       EditPage::AS_SUCCESS_UPDATE, null, "expected successfull update" );
+
+               // second edit
+               $this->assertEdit( 'EditPageTest_testAutoMerge', null, 'Berta', $bertasEdit,
+                       $expectedCode, $expectedText, $message );
+       }
 }