3 namespace MediaWiki\Tests\Maintenance
;
7 use MediaWiki\Content\ContentHandler
;
8 use MediaWiki\Revision\SlotRecord
;
9 use MediaWiki\Title\Title
;
10 use MediaWikiIntegrationTestCase
;
11 use PHPUnit\Framework\ExpectationFailedException
;
16 * Mock for the input/output of FetchText
18 * FetchText internally tries to access stdin and stdout. We mock those aspects
21 class SemiMockedFetchText
extends FetchText
{
24 * @var string|null Text to pass as stdin
26 private $mockStdinText = null;
29 * @var bool Whether or not a text for stdin has been provided
31 private $mockSetUp = false;
34 * @var array Invocation counters for the mocked aspects
36 private $mockInvocations = [ 'getStdin' => 0 ];
39 * Data for the fake stdin
41 * @param string $stdin The string to be used instead of stdin
43 public function mockStdin( $stdin ) {
44 $this->mockStdinText
= $stdin;
45 $this->mockSetUp
= true;
49 * Gets invocation counters for mocked methods.
51 * @return array An array, whose keys are function names. The corresponding values
52 * denote the number of times the function has been invoked.
54 public function mockGetInvocations() {
55 return $this->mockInvocations
;
58 // -----------------------------------------------------------------
59 // Mocked functions from FetchText follow.
61 public function getStdin( $len = null ) {
62 $this->mockInvocations
['getStdin']++
;
63 if ( $len !== null ) {
64 throw new ExpectationFailedException(
65 "Tried to get stdin with non null parameter" );
68 if ( !$this->mockSetUp
) {
69 throw new ExpectationFailedException(
70 "Tried to get stdin before setting up rerouting" );
73 return fopen( 'data://text/plain,' . $this->mockStdinText
, 'r' );
78 * TestCase for FetchText
84 class FetchTextTest
extends MediaWikiIntegrationTestCase
{
86 // We add 5 Revisions for this test. Their corresponding text id's
87 // are stored in the following 5 variables.
89 protected static $textId1;
91 protected static $textId2;
93 protected static $textId3;
95 protected static $textId4;
97 protected static $textId5;
100 * @var Exception|null As the current MediaWikiIntegrationTestCase::run is not
101 * robust enough to recover from thrown exceptions directly, we cannot
102 * throw frow within addDBData, although it would be appropriate. Hence,
103 * we catch the exception and store it until we are in setUp and may
104 * finally rethrow the exception without crashing the test suite.
106 protected static $exceptionFromAddDBDataOnce;
109 * @var FetchText The (mocked) FetchText that is to test
114 * Adds a revision to a page and returns the main slot's blob address
116 * @param WikiPage $page The page to add the revision to
117 * @param string $text The revisions text
118 * @param string $summary The revisions summare
121 private function addRevision( $page, $text, $summary ) {
122 $status = $page->doUserEditContent(
123 ContentHandler
::makeContent( $text, $page->getTitle() ),
124 $this->getTestSysop()->getUser(),
128 if ( $status->isGood() ) {
129 $revision = $status->getNewRevision();
130 $address = $revision->getSlot( SlotRecord
::MAIN
)->getAddress();
134 throw new RuntimeException( "Could not create revision" );
137 public function addDBDataOnce() {
138 $wikitextNamespace = $this->getDefaultWikitextNS();
139 $wikiPageFactory = $this->getServiceContainer()->getWikiPageFactory();
142 $title = Title
::makeTitle( $wikitextNamespace, 'FetchTextTestPage1' );
143 $page = $wikiPageFactory->newFromTitle( $title );
144 self
::$textId1 = $this->addRevision(
146 "FetchTextTestPage1Text1",
147 "FetchTextTestPage1Summary1"
150 $title = Title
::makeTitle( $wikitextNamespace, 'FetchTextTestPage2' );
151 $page = $wikiPageFactory->newFromTitle( $title );
152 self
::$textId2 = $this->addRevision(
154 "FetchTextTestPage2Text1",
155 "FetchTextTestPage2Summary1"
157 self
::$textId3 = $this->addRevision(
159 "FetchTextTestPage2Text2",
160 "FetchTextTestPage2Summary2"
162 self
::$textId4 = $this->addRevision(
164 "FetchTextTestPage2Text3",
165 "FetchTextTestPage2Summary3"
167 self
::$textId5 = $this->addRevision(
169 "FetchTextTestPage2Text4 some additional Text ",
170 "FetchTextTestPage2Summary4 extra "
172 } catch ( Exception
$e ) {
173 // We'd love to pass $e directly. However, ... see
174 // documentation of exceptionFromAddDBDataOnce
175 self
::$exceptionFromAddDBDataOnce = $e;
179 protected function setUp(): void
{
182 // Check if any Exception is stored for rethrowing from addDBData
183 if ( self
::$exceptionFromAddDBDataOnce !== null ) {
184 throw self
::$exceptionFromAddDBDataOnce;
187 $this->fetchText
= new SemiMockedFetchText();
191 * Helper to relate FetchText's input and output
192 * @param string $input
193 * @param string $expectedOutput
195 private function assertFilter( $input, $expectedOutput ) {
196 $this->fetchText
->mockStdin( $input );
197 $this->fetchText
->execute();
198 $invocations = $this->fetchText
->mockGetInvocations();
199 $this->assertSame( 1, $invocations['getStdin'],
200 "getStdin invocation counter" );
201 $this->expectOutputString( $expectedOutput );
204 // Instead of the following functions, a data provider would be great.
205 // However, as data providers are evaluated /before/ addDBData, a data
206 // provider would not know the required ids.
208 public function testExistingSimple() {
209 $this->assertFilter( self
::$textId2,
210 self
::$textId2 . "\n23\nFetchTextTestPage2Text1" );
213 public function testExistingSimpleWithNewline() {
214 $this->assertFilter( self
::$textId2 . "\n",
215 self
::$textId2 . "\n23\nFetchTextTestPage2Text1" );
218 public function testExistingInteger() {
219 $this->assertFilter( (int)preg_replace( '/^tt:/', '', self
::$textId2 ),
220 self
::$textId2 . "\n23\nFetchTextTestPage2Text1" );
223 public function testExistingSeveral() {
232 self
::$textId1 . "\n23\nFetchTextTestPage1Text1",
233 self
::$textId5 . "\n44\nFetchTextTestPage2Text4 "
234 . "some additional Text",
235 self
::$textId3 . "\n23\nFetchTextTestPage2Text2",
236 self
::$textId3 . "\n23\nFetchTextTestPage2Text2"
240 public function testEmpty() {
241 $this->assertFilter( "", "" );
244 public function testNonExisting() {
245 @$this->assertFilter( 'tt:77889911', 'tt:77889911' . "\n-1\n" );
248 public function testNonExistingInteger() {
249 @$this->assertFilter( '77889911', 'tt:77889911' . "\n-1\n" );
252 public function testBadBlobAddressWithColon() {
253 $this->assertFilter( 'foo:bar', 'foo:bar' . "\n-1\n" );
256 public function testNegativeInteger() {
257 $this->assertFilter( "-42", "tt:-42\n-1\n" );
260 public function testFloatingPointNumberExisting() {
261 // float -> int -> address -> revision
262 $id = intval( preg_replace( '/^tt:/', '', self
::$textId3 ) ) +
0.14159;
263 $this->assertFilter( 'tt:' . intval( $id ),
264 self
::$textId3 . "\n23\nFetchTextTestPage2Text2" );
267 public function testFloatingPointNumberNonExisting() {
268 $id = intval( preg_replace( '/^tt:/', '', self
::$textId5 ) ) +
3.14159;
269 @$this->assertFilter( $id, 'tt:' . intval( $id ) . "\n-1\n" );
272 public function testCharacters() {
273 $this->assertFilter( "abc", "abc\n-1\n" );
276 public function testMix() {
277 $this->assertFilter( "ab\n" . self
::$textId4 . ".5cd\n\nefg\nfoo:bar\n" . self
::$textId2
278 . "\n" . self
::$textId3,
281 self
::$textId4 . ".5cd\n-1\n",
285 self
::$textId2 . "\n23\nFetchTextTestPage2Text1",
286 self
::$textId3 . "\n23\nFetchTextTestPage2Text2"