From: daniel Date: Fri, 30 Mar 2018 11:29:33 +0000 (+0200) Subject: [MCR] Introduce RevisionSlotsUpdate. X-Git-Tag: 1.34.0-rc.0~5485^2 X-Git-Url: http://git.cyclocoop.org/fichier?a=commitdiff_plain;h=8b0506bd8ba9d64453df121cde457d4c95f3f60e;p=lhc%2Fweb%2Fwiklou.git [MCR] Introduce RevisionSlotsUpdate. The RevisionSlotsUpdate interface represents a change to a pages slots, as applied by an edit. This also introduces RevisionSlots::hasSameContent and pulls up getTouchedSlots() and getInheritedSlots() from MutableRevisionStore to RevisionStore, in preparation of using these classes in the refactoring of WikiPage::doEditContent and friends. Bug: T174038 Change-Id: Idb0ef885b343a76137b640fdfc1bf36104b00895 --- diff --git a/autoload.php b/autoload.php index 12958ca089..ece4661d1a 100644 --- a/autoload.php +++ b/autoload.php @@ -965,6 +965,7 @@ $wgAutoloadLocalClasses = [ 'MediaWiki\\Storage\\RevisionLookup' => __DIR__ . '/includes/Storage/RevisionLookup.php', 'MediaWiki\\Storage\\RevisionRecord' => __DIR__ . '/includes/Storage/RevisionRecord.php', 'MediaWiki\\Storage\\RevisionSlots' => __DIR__ . '/includes/Storage/RevisionSlots.php', + 'MediaWiki\\Storage\\RevisionSlotsUpdate' => __DIR__ . '/includes/Storage/RevisionSlotsUpdate.php', 'MediaWiki\\Storage\\RevisionStore' => __DIR__ . '/includes/Storage/RevisionStore.php', 'MediaWiki\\Storage\\RevisionStoreRecord' => __DIR__ . '/includes/Storage/RevisionStoreRecord.php', 'MediaWiki\\Storage\\SlotRecord' => __DIR__ . '/includes/Storage/SlotRecord.php', diff --git a/includes/Storage/MutableRevisionSlots.php b/includes/Storage/MutableRevisionSlots.php index 2e675c8937..4cc3730d92 100644 --- a/includes/Storage/MutableRevisionSlots.php +++ b/includes/Storage/MutableRevisionSlots.php @@ -102,36 +102,4 @@ class MutableRevisionSlots extends RevisionSlots { unset( $this->slots[$role] ); } - /** - * Return all slots that are not inherited. - * - * @note This may cause the slot meta-data for the revision to be lazy-loaded. - * - * @return SlotRecord[] - */ - public function getTouchedSlots() { - return array_filter( - $this->getSlots(), - function ( SlotRecord $slot ) { - return !$slot->isInherited(); - } - ); - } - - /** - * Return all slots that are inherited. - * - * @note This may cause the slot meta-data for the revision to be lazy-loaded. - * - * @return SlotRecord[] - */ - public function getInheritedSlots() { - return array_filter( - $this->getSlots(), - function ( SlotRecord $slot ) { - return $slot->isInherited(); - } - ); - } - } diff --git a/includes/Storage/RevisionSlots.php b/includes/Storage/RevisionSlots.php index 7fa5431d38..c7dcd136e0 100644 --- a/includes/Storage/RevisionSlots.php +++ b/includes/Storage/RevisionSlots.php @@ -54,6 +54,8 @@ class RevisionSlots { * @param SlotRecord[] $slots */ private function setSlotsInternal( array $slots ) { + Assert::parameterElementType( SlotRecord::class, $slots, '$slots' ); + $this->slots = []; // re-key the slot array @@ -199,4 +201,71 @@ class RevisionSlots { }, null ); } + /** + * Return all slots that are not inherited. + * + * @note This may cause the slot meta-data for the revision to be lazy-loaded. + * + * @return SlotRecord[] + */ + public function getTouchedSlots() { + return array_filter( + $this->getSlots(), + function ( SlotRecord $slot ) { + return !$slot->isInherited(); + } + ); + } + + /** + * Return all slots that are inherited. + * + * @note This may cause the slot meta-data for the revision to be lazy-loaded. + * + * @return SlotRecord[] + */ + public function getInheritedSlots() { + return array_filter( + $this->getSlots(), + function ( SlotRecord $slot ) { + return $slot->isInherited(); + } + ); + } + + /** + * Checks whether the other RevisionSlots instance has the same content + * as this instance. Note that this does not mean that the slots have to be the same: + * they could for instance belong to different revisions. + * + * @param RevisionSlots $other + * + * @return bool + */ + public function hasSameContent( RevisionSlots $other ) { + if ( $other === $this ) { + return true; + } + + $aSlots = $this->getSlots(); + $bSlots = $other->getSlots(); + + ksort( $aSlots ); + ksort( $bSlots ); + + if ( array_keys( $aSlots ) !== array_keys( $bSlots ) ) { + return false; + } + + foreach ( $aSlots as $role => $s ) { + $t = $bSlots[$role]; + + if ( !$s->hasSameContent( $t ) ) { + return false; + } + } + + return true; + } + } diff --git a/includes/Storage/RevisionSlotsUpdate.php b/includes/Storage/RevisionSlotsUpdate.php new file mode 100644 index 0000000000..0eef90f2bc --- /dev/null +++ b/includes/Storage/RevisionSlotsUpdate.php @@ -0,0 +1,242 @@ +getSlots(); + $removed = []; + + if ( $parentSlots ) { + foreach ( $parentSlots->getSlots() as $role => $slot ) { + if ( !isset( $modified[$role] ) ) { + $removed[] = $role; + } elseif ( $slot->hasSameContent( $modified[$role] ) ) { + // Unset slots that had the same content in the parent revision from $modified. + unset( $modified[$role] ); + } + } + } + + return new RevisionSlotsUpdate( $modified, $removed ); + } + + /** + * @param SlotRecord[] $modifiedSlots + * @param string[] $removedRoles + */ + public function __construct( array $modifiedSlots = [], array $removedRoles = [] ) { + foreach ( $modifiedSlots as $slot ) { + $this->modifySlot( $slot ); + } + + foreach ( $removedRoles as $role ) { + $this->removeSlot( $role ); + } + } + + /** + * Returns a list of modified slot roles, that is, roles modified by calling modifySlot(), + * and not later removed by calling removeSlot(). + * + * @return string[] + */ + public function getModifiedRoles() { + return array_keys( $this->modifiedSlots ); + } + + /** + * Returns a list of removed slot roles, that is, roles removed by calling removeSlot(), + * and not later re-introduced by calling modifySlot(). + * + * @return string[] + */ + public function getRemovedRoles() { + return array_keys( $this->removedRoles ); + } + + /** + * Returns a list of all slot roles that modified or removed. + * + * @return string[] + */ + public function getTouchedRoles() { + return array_merge( $this->getModifiedRoles(), $this->getRemovedRoles() ); + } + + /** + * Sets the given slot to be modified. + * If a slot with the same role is already present, it is replaced. + * + * The roles used with modifySlot() will be returned from getModifiedRoles(), + * unless overwritten with removeSlot(). + * + * @param SlotRecord $slot + */ + public function modifySlot( SlotRecord $slot ) { + $role = $slot->getRole(); + + // XXX: We should perhaps require this to be an unsaved slot! + unset( $this->removedRoles[$role] ); + $this->modifiedSlots[$role] = $slot; + } + + /** + * Sets the content for the slot with the given role to be modified. + * If a slot with the same role is already present, it is replaced. + * + * @param string $role + * @param Content $content + */ + public function modifyContent( $role, Content $content ) { + $slot = SlotRecord::newUnsaved( $role, $content ); + $this->modifySlot( $slot ); + } + + /** + * Remove the slot for the given role, discontinue the corresponding stream. + * + * The roles used with removeSlot() will be returned from getRemovedSlots(), + * unless overwritten with modifySlot(). + * + * @param string $role + */ + public function removeSlot( $role ) { + unset( $this->modifiedSlots[$role] ); + $this->removedRoles[$role] = true; + } + + /** + * Returns the SlotRecord associated with the given role, if the slot with that role + * was modified (and not again removed). + * + * @note If the SlotRecord returned by this method returns a non-inherited slot, + * the content of that slot may or may not already have PST applied. Methods + * that take a RevisionSlotsUpdate as a parameter should specify whether they + * expect PST to already have been applied to all slots. Inherited slots + * should never have PST applied again. + * + * @param string $role The role name of the desired slot + * + * @throws RevisionAccessException if the slot does not exist or was removed. + * @return SlotRecord + */ + public function getModifiedSlot( $role ) { + if ( isset( $this->modifiedSlots[$role] ) ) { + return $this->modifiedSlots[$role]; + } else { + throw new RevisionAccessException( 'No such slot: ' . $role ); + } + } + + /** + * Returns whether getModifiedSlot() will return a SlotRecord for the given role. + * + * Will return true for the role names returned by getModifiedRoles(), false otherwise. + * + * @param string $role The role name of the desired slot + * + * @return bool + */ + public function isModifiedSlot( $role ) { + return isset( $this->modifiedSlots[$role] ); + } + + /** + * Returns whether the given role is to be removed from the page. + * + * Will return true for the role names returned by getRemovedRoles(), false otherwise. + * + * @param string $role The role name of the desired slot + * + * @return bool + */ + public function isRemovedSlot( $role ) { + return isset( $this->removedRoles[$role] ); + } + + /** + * Returns true if $other represents the same update - that is, + * if all methods defined by RevisionSlotsUpdate when called on $this or $other + * will yield the same result when called with the same parameters. + * + * SlotRecords for the same role are compared based on their model and content. + * + * @param RevisionSlotsUpdate $other + * @return bool + */ + public function hasSameUpdates( RevisionSlotsUpdate $other ) { + // NOTE: use != not !==, since the order of entries is not significant! + + if ( $this->getModifiedRoles() != $other->getModifiedRoles() ) { + return false; + } + + if ( $this->getRemovedRoles() != $other->getRemovedRoles() ) { + return false; + } + + foreach ( $this->getModifiedRoles() as $role ) { + $s = $this->getModifiedSlot( $role ); + $t = $other->getModifiedSlot( $role ); + + if ( !$s->hasSameContent( $t ) ) { + return false; + } + } + + return true; + } + +} diff --git a/includes/Storage/SlotRecord.php b/includes/Storage/SlotRecord.php index 50d1100547..9462518ffe 100644 --- a/includes/Storage/SlotRecord.php +++ b/includes/Storage/SlotRecord.php @@ -565,4 +565,50 @@ class SlotRecord { return \Wikimedia\base_convert( sha1( $blob ), 16, 36, 31 ); } + /** + * Returns true if $other has the same content as this slot. + * The check is performed based on the model, address size, and hash. + * Two slots can have the same content if they use different content addresses, + * but if they have the same address and the same model, they have the same content. + * Two slots can have the same content if they belong to different + * revisions or pages. + * + * Note that hasSameContent() may return false even if Content::equals returns true for + * the content of two slots. This may happen if the two slots have different serializations + * representing equivalent Content. Such false negatives are considered acceptable. Code + * that has to be absolutely sure the Content is really not the same if hasSameContent() + * returns false should call getContent() and compare the Content objects directly. + * + * @since 1.32 + * + * @param SlotRecord $other + * @return bool + */ + public function hasSameContent( SlotRecord $other ) { + if ( $other === $this ) { + return true; + } + + if ( $this->getModel() !== $other->getModel() ) { + return false; + } + + if ( $this->hasAddress() + && $other->hasAddress() + && $this->getAddress() == $other->getAddress() + ) { + return true; + } + + if ( $this->getSize() !== $other->getSize() ) { + return false; + } + + if ( $this->getSha1() !== $other->getSha1() ) { + return false; + } + + return true; + } + } diff --git a/tests/phpunit/includes/Storage/MutableRevisionSlotsTest.php b/tests/phpunit/includes/Storage/MutableRevisionSlotsTest.php index 0416bcfa33..f19be3bb1f 100644 --- a/tests/phpunit/includes/Storage/MutableRevisionSlotsTest.php +++ b/tests/phpunit/includes/Storage/MutableRevisionSlotsTest.php @@ -2,8 +2,10 @@ namespace MediaWiki\Tests\Storage; +use InvalidArgumentException; use MediaWiki\Storage\MutableRevisionSlots; use MediaWiki\Storage\RevisionAccessException; +use MediaWiki\Storage\RevisionSlots; use MediaWiki\Storage\SlotRecord; use WikitextContent; @@ -12,6 +14,33 @@ use WikitextContent; */ class MutableRevisionSlotsTest extends RevisionSlotsTest { + /** + * @param SlotRecord[] $slots + * @return RevisionSlots + */ + protected function newRevisionSlots( $slots = [] ) { + return new MutableRevisionSlots( $slots ); + } + + public function provideConstructorFailue() { + yield 'array or the wrong thing' => [ + [ 1, 2, 3 ] + ]; + } + + /** + * @dataProvider provideConstructorFailue + * @param $slots + * + * @covers \MediaWiki\Storage\RevisionSlots::__construct + * @covers \MediaWiki\Storage\RevisionSlots::setSlotsInternal + */ + public function testConstructorFailue( $slots ) { + $this->setExpectedException( InvalidArgumentException::class ); + + new MutableRevisionSlots( $slots ); + } + public function testSetMultipleSlots() { $slots = new MutableRevisionSlots(); diff --git a/tests/phpunit/includes/Storage/RevisionSlotsTest.php b/tests/phpunit/includes/Storage/RevisionSlotsTest.php index b9f833caa6..95bba47bed 100644 --- a/tests/phpunit/includes/Storage/RevisionSlotsTest.php +++ b/tests/phpunit/includes/Storage/RevisionSlotsTest.php @@ -2,10 +2,12 @@ namespace MediaWiki\Tests\Storage; +use InvalidArgumentException; use MediaWiki\Storage\RevisionAccessException; use MediaWiki\Storage\RevisionSlots; use MediaWiki\Storage\SlotRecord; use MediaWikiTestCase; +use TextContent; use WikitextContent; class RevisionSlotsTest extends MediaWikiTestCase { @@ -18,6 +20,28 @@ class RevisionSlotsTest extends MediaWikiTestCase { return new RevisionSlots( $slots ); } + public function provideConstructorFailue() { + yield 'not an array or callable' => [ + 'foo' + ]; + yield 'array of the wrong thing' => [ + [ 1, 2, 3 ] + ]; + } + + /** + * @dataProvider provideConstructorFailue + * @param $slots + * + * @covers \MediaWiki\Storage\RevisionSlots::__construct + * @covers \MediaWiki\Storage\RevisionSlots::setSlotsInternal + */ + public function testConstructorFailue( $slots ) { + $this->setExpectedException( InvalidArgumentException::class ); + + new RevisionSlots( $slots ); + } + /** * @covers \MediaWiki\Storage\RevisionSlots::getSlot */ @@ -94,6 +118,40 @@ class RevisionSlotsTest extends MediaWikiTestCase { $this->assertEquals( [ 'main' => $mainSlot, 'aux' => $auxSlot ], $slots->getSlots() ); } + /** + * @covers \MediaWiki\Storage\RevisionSlots::getInheritedSlots + */ + public function testGetInheritedSlots() { + $mainSlot = SlotRecord::newUnsaved( 'main', new WikitextContent( 'A' ) ); + $auxSlot = SlotRecord::newInherited( + SlotRecord::newSaved( + 7, 7, 'foo', + SlotRecord::newUnsaved( 'aux', new WikitextContent( 'B' ) ) + ) + ); + $slotsArray = [ $mainSlot, $auxSlot ]; + $slots = $this->newRevisionSlots( $slotsArray ); + + $this->assertEquals( [ 'aux' => $auxSlot ], $slots->getInheritedSlots() ); + } + + /** + * @covers \MediaWiki\Storage\RevisionSlots::getTouchedSlots + */ + public function testGetTouchedSlots() { + $mainSlot = SlotRecord::newUnsaved( 'main', new WikitextContent( 'A' ) ); + $auxSlot = SlotRecord::newInherited( + SlotRecord::newSaved( + 7, 7, 'foo', + SlotRecord::newUnsaved( 'aux', new WikitextContent( 'B' ) ) + ) + ); + $slotsArray = [ $mainSlot, $auxSlot ]; + $slots = $this->newRevisionSlots( $slotsArray ); + + $this->assertEquals( [ 'main' => $mainSlot ], $slots->getTouchedSlots() ); + } + public function provideComputeSize() { yield [ 1, [ 'A' ] ]; yield [ 2, [ 'AA' ] ]; @@ -136,4 +194,34 @@ class RevisionSlotsTest extends MediaWikiTestCase { $this->assertSame( $expected, $slots->computeSha1() ); } + public function provideHasSameContent() { + $fooX = SlotRecord::newUnsaved( 'x', new TextContent( 'Foo' ) ); + $barZ = SlotRecord::newUnsaved( 'z', new TextContent( 'Bar' ) ); + $fooY = SlotRecord::newUnsaved( 'y', new TextContent( 'Foo' ) ); + $barZS = SlotRecord::newSaved( 7, 7, 'xyz', $barZ ); + $barZ2 = SlotRecord::newUnsaved( 'z', new TextContent( 'Baz' ) ); + + $a = $this->newRevisionSlots( [ 'x' => $fooX, 'z' => $barZ ] ); + $a2 = $this->newRevisionSlots( [ 'x' => $fooX, 'z' => $barZ ] ); + $a3 = $this->newRevisionSlots( [ 'x' => $fooX, 'z' => $barZS ] ); + $b = $this->newRevisionSlots( [ 'y' => $fooY, 'z' => $barZ ] ); + $c = $this->newRevisionSlots( [ 'x' => $fooX, 'z' => $barZ2 ] ); + + yield 'same instance' => [ $a, $a, true ]; + yield 'same slots' => [ $a, $a2, true ]; + yield 'same content' => [ $a, $a3, true ]; + + yield 'different roles' => [ $a, $b, false ]; + yield 'different content' => [ $a, $c, false ]; + } + + /** + * @dataProvider provideHasSameContent + * @covers \MediaWiki\Storage\RevisionSlots::hasSameContent + */ + public function testHasSameContent( RevisionSlots $a, RevisionSlots $b, $same ) { + $this->assertSame( $same, $a->hasSameContent( $b ) ); + $this->assertSame( $same, $b->hasSameContent( $a ) ); + } + } diff --git a/tests/phpunit/includes/Storage/RevisionSlotsUpdateTest.php b/tests/phpunit/includes/Storage/RevisionSlotsUpdateTest.php new file mode 100644 index 0000000000..5b392c878d --- /dev/null +++ b/tests/phpunit/includes/Storage/RevisionSlotsUpdateTest.php @@ -0,0 +1,207 @@ + $slotA, + 'B' => $slotB, + 'C' => $slotC, + ] ); + + $newSlots = new RevisionSlots( [ + 'A' => $slotA, + 'B' => $slotB2, + ] ); + + yield [ $newSlots, null, [ 'A', 'B' ], [] ]; + yield [ $newSlots, $parentSlots, [ 'B' ], [ 'C' ] ]; + } + + /** + * @dataProvider provideNewFromRevisionSlots + * + * @param RevisionSlots $newSlots + * @param RevisionSlots $parentSlots + * @param $modified + * @param $removed + */ + public function testNewFromRevisionSlots( + RevisionSlots $newSlots, + RevisionSlots $parentSlots = null, + array $modified = [], + array $removed = [] + ) { + $update = RevisionSlotsUpdate::newFromRevisionSlots( $newSlots, $parentSlots ); + + $this->assertEquals( $modified, $update->getModifiedRoles() ); + $this->assertEquals( $removed, $update->getRemovedRoles() ); + + foreach ( $modified as $role ) { + $this->assertSame( $newSlots->getSlot( $role ), $update->getModifiedSlot( $role ) ); + } + } + + public function testConstructor() { + $update = new RevisionSlotsUpdate(); + + $this->assertEmpty( $update->getModifiedRoles() ); + $this->assertEmpty( $update->getRemovedRoles() ); + + $slotA = SlotRecord::newUnsaved( 'A', new WikitextContent( 'A' ) ); + $update = new RevisionSlotsUpdate( [ 'A' => $slotA ] ); + + $this->assertEquals( [ 'A' ], $update->getModifiedRoles() ); + $this->assertEmpty( $update->getRemovedRoles() ); + + $update = new RevisionSlotsUpdate( [ 'A' => $slotA ], [ 'X' ] ); + + $this->assertEquals( [ 'A' ], $update->getModifiedRoles() ); + $this->assertEquals( [ 'X' ], $update->getRemovedRoles() ); + } + + public function testModifySlot() { + $slots = new RevisionSlotsUpdate(); + + $this->assertSame( [], $slots->getModifiedRoles() ); + $this->assertSame( [], $slots->getRemovedRoles() ); + + $slotA = SlotRecord::newUnsaved( 'some', new WikitextContent( 'A' ) ); + $slots->modifySlot( $slotA ); + $this->assertTrue( $slots->isModifiedSlot( 'some' ) ); + $this->assertFalse( $slots->isRemovedSlot( 'some' ) ); + $this->assertSame( $slotA, $slots->getModifiedSlot( 'some' ) ); + $this->assertSame( [ 'some' ], $slots->getModifiedRoles() ); + $this->assertSame( [], $slots->getRemovedRoles() ); + + $slotB = SlotRecord::newUnsaved( 'other', new WikitextContent( 'B' ) ); + $slots->modifySlot( $slotB ); + $this->assertTrue( $slots->isModifiedSlot( 'other' ) ); + $this->assertFalse( $slots->isRemovedSlot( 'other' ) ); + $this->assertSame( $slotB, $slots->getModifiedSlot( 'other' ) ); + $this->assertSame( [ 'some', 'other' ], $slots->getModifiedRoles() ); + $this->assertSame( [], $slots->getRemovedRoles() ); + + // modify slot A again + $slots->modifySlot( $slotA ); + $this->assertArrayEquals( [ 'some', 'other' ], $slots->getModifiedRoles() ); + + // remove modified slot + $slots->removeSlot( 'some' ); + $this->assertSame( [ 'other' ], $slots->getModifiedRoles() ); + $this->assertSame( [ 'some' ], $slots->getRemovedRoles() ); + + // modify removed slot + $slots->modifySlot( $slotA ); + $this->assertArrayEquals( [ 'some', 'other' ], $slots->getModifiedRoles() ); + $this->assertSame( [], $slots->getRemovedRoles() ); + } + + public function testRemoveSlot() { + $slots = new RevisionSlotsUpdate(); + + $slotA = SlotRecord::newUnsaved( 'main', new WikitextContent( 'A' ) ); + $slots->modifySlot( $slotA ); + + $this->assertSame( [ 'main' ], $slots->getModifiedRoles() ); + + $slots->removeSlot( 'main' ); + $slots->removeSlot( 'other' ); + $this->assertSame( [], $slots->getModifiedRoles() ); + $this->assertSame( [ 'main', 'other' ], $slots->getRemovedRoles() ); + $this->assertTrue( $slots->isRemovedSlot( 'main' ) ); + $this->assertTrue( $slots->isRemovedSlot( 'other' ) ); + $this->assertFalse( $slots->isModifiedSlot( 'main' ) ); + + // removing the same slot again should not trigger an error + $slots->removeSlot( 'main' ); + + // getting a slot after removing it should fail + $this->setExpectedException( RevisionAccessException::class ); + $slots->getModifiedSlot( 'main' ); + } + + public function testGetModifiedRoles() { + $slots = new RevisionSlotsUpdate( [], [ 'xyz' ] ); + + $this->assertSame( [], $slots->getModifiedRoles() ); + + $slots->modifyContent( 'main', new WikitextContent( 'A' ) ); + $slots->modifyContent( 'foo', new WikitextContent( 'Foo' ) ); + $this->assertSame( [ 'main', 'foo' ], $slots->getModifiedRoles() ); + + $slots->removeSlot( 'main' ); + $this->assertSame( [ 'foo' ], $slots->getModifiedRoles() ); + } + + public function testGetRemovedRoles() { + $slotA = SlotRecord::newUnsaved( 'main', new WikitextContent( 'A' ) ); + $slots = new RevisionSlotsUpdate( [ $slotA ] ); + + $this->assertSame( [], $slots->getRemovedRoles() ); + + $slots->removeSlot( 'main', new WikitextContent( 'A' ) ); + $slots->removeSlot( 'foo', new WikitextContent( 'Foo' ) ); + + $this->assertSame( [ 'main', 'foo' ], $slots->getRemovedRoles() ); + + $slots->modifyContent( 'main', new WikitextContent( 'A' ) ); + $this->assertSame( [ 'foo' ], $slots->getRemovedRoles() ); + } + + public function provideHasSameUpdates() { + $fooX = SlotRecord::newUnsaved( 'x', new WikitextContent( 'Foo' ) ); + $barZ = SlotRecord::newUnsaved( 'z', new WikitextContent( 'Bar' ) ); + + $a = new RevisionSlotsUpdate(); + $a->modifySlot( $fooX ); + $a->modifySlot( $barZ ); + $a->removeSlot( 'Q' ); + + $a2 = new RevisionSlotsUpdate(); + $a2->modifySlot( $fooX ); + $a2->modifySlot( $barZ ); + $a2->removeSlot( 'Q' ); + + $b = new RevisionSlotsUpdate(); + $b->modifySlot( $barZ ); + $b->removeSlot( 'Q' ); + + $c = new RevisionSlotsUpdate(); + $c->modifySlot( $fooX ); + $c->modifySlot( $barZ ); + + yield 'same instance' => [ $a, $a, true ]; + yield 'same udpates' => [ $a, $a2, true ]; + + yield 'different modified' => [ $a, $b, false ]; + yield 'different removed' => [ $a, $c, false ]; + } + + /** + * @dataProvider provideHasSameUpdates + */ + public function testHasSameUpdates( RevisionSlotsUpdate $a, RevisionSlotsUpdate $b, $same ) { + $this->assertSame( $same, $a->hasSameUpdates( $b ) ); + $this->assertSame( $same, $b->hasSameUpdates( $a ) ); + } + +} diff --git a/tests/phpunit/includes/Storage/SlotRecordTest.php b/tests/phpunit/includes/Storage/SlotRecordTest.php index 8f26494dc6..feeb538440 100644 --- a/tests/phpunit/includes/Storage/SlotRecordTest.php +++ b/tests/phpunit/includes/Storage/SlotRecordTest.php @@ -295,4 +295,104 @@ class SlotRecordTest extends MediaWikiTestCase { SlotRecord::newSaved( $revisionId, $contentId, $contentAddress, $protoSlot ); } + public function provideHasSameContent() { + $fail = function () { + self::fail( 'There should be no need to actually load the content.' ); + }; + + $a100a1 = new SlotRecord( + $this->makeRow( + [ + 'model_name' => 'A', + 'content_size' => 100, + 'content_sha1' => 'hash-a', + 'content_address' => 'xxx:a1', + ] + ), + $fail + ); + $a100a1b = new SlotRecord( + $this->makeRow( + [ + 'model_name' => 'A', + 'content_size' => 100, + 'content_sha1' => 'hash-a', + 'content_address' => 'xxx:a1', + ] + ), + $fail + ); + $a100null = new SlotRecord( + $this->makeRow( + [ + 'model_name' => 'A', + 'content_size' => 100, + 'content_sha1' => 'hash-a', + 'content_address' => null, + ] + ), + $fail + ); + $a100a2 = new SlotRecord( + $this->makeRow( + [ + 'model_name' => 'A', + 'content_size' => 100, + 'content_sha1' => 'hash-a', + 'content_address' => 'xxx:a2', + ] + ), + $fail + ); + $b100a1 = new SlotRecord( + $this->makeRow( + [ + 'model_name' => 'B', + 'content_size' => 100, + 'content_sha1' => 'hash-a', + 'content_address' => 'xxx:a1', + ] + ), + $fail + ); + $a200a1 = new SlotRecord( + $this->makeRow( + [ + 'model_name' => 'A', + 'content_size' => 200, + 'content_sha1' => 'hash-a', + 'content_address' => 'xxx:a2', + ] + ), + $fail + ); + $a100x1 = new SlotRecord( + $this->makeRow( + [ + 'model_name' => 'A', + 'content_size' => 100, + 'content_sha1' => 'hash-x', + 'content_address' => 'xxx:x1', + ] + ), + $fail + ); + + yield 'same instance' => [ $a100a1, $a100a1, true ]; + yield 'no address' => [ $a100a1, $a100null, true ]; + yield 'same address' => [ $a100a1, $a100a1b, true ]; + yield 'different address' => [ $a100a1, $a100a2, true ]; + yield 'different model' => [ $a100a1, $b100a1, false ]; + yield 'different size' => [ $a100a1, $a200a1, false ]; + yield 'different hash' => [ $a100a1, $a100x1, false ]; + } + + /** + * @dataProvider provideHasSameContent + */ + public function testHasSameContent( SlotRecord $a, SlotRecord $b, $sameContent ) { + $this->assertSame( $sameContent, $a->hasSameContent( $b ) ); + $this->assertSame( $sameContent, $b->hasSameContent( $a ) ); + } + }