Merge "ApiMessage: Improve documentation"
[mediawiki.git] / tests / testHelpers.inc
blobd73496f070d3a2fe7bfed696cb10119ee75b6295
1 <?php
2 /**
3  * Recording for passing/failing tests.
4  *
5  * This program is free software; you can redistribute it and/or modify
6  * it under the terms of the GNU General Public License as published by
7  * the Free Software Foundation; either version 2 of the License, or
8  * (at your option) any later version.
9  *
10  * This program is distributed in the hope that it will be useful,
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13  * GNU General Public License for more details.
14  *
15  * You should have received a copy of the GNU General Public License along
16  * with this program; if not, write to the Free Software Foundation, Inc.,
17  * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18  * http://www.gnu.org/copyleft/gpl.html
19  *
20  * @file
21  * @ingroup Testing
22  */
24 /**
25  * Interface to record parser test results.
26  *
27  * The ITestRecorder is a very simple interface to record the result of
28  * MediaWiki parser tests. One should call start() before running the
29  * full parser tests and end() once all the tests have been finished.
30  * After each test, you should use record() to keep track of your tests
31  * results. Finally, report() is used to generate a summary of your
32  * test run, one could dump it to the console for human consumption or
33  * register the result in a database for tracking purposes.
34  *
35  * @since 1.22
36  */
37 interface ITestRecorder {
39         /**
40          * Called at beginning of the parser test run
41          */
42         public function start();
44         /**
45          * Called after each test
46          * @param string $test
47          * @param bool $result
48          */
49         public function record( $test, $result );
51         /**
52          * Called before finishing the test run
53          */
54         public function report();
56         /**
57          * Called at the end of the parser test run
58          */
59         public function end();
63 class TestRecorder implements ITestRecorder {
64         public $parent;
65         public $term;
67         function __construct( $parent ) {
68                 $this->parent = $parent;
69                 $this->term = $parent->term;
70         }
72         function start() {
73                 $this->total = 0;
74                 $this->success = 0;
75         }
77         function record( $test, $result ) {
78                 $this->total++;
79                 $this->success += ( $result ? 1 : 0 );
80         }
82         function end() {
83                 // dummy
84         }
86         function report() {
87                 if ( $this->total > 0 ) {
88                         $this->reportPercentage( $this->success, $this->total );
89                 } else {
90                         throw new MWException( "No tests found.\n" );
91                 }
92         }
94         function reportPercentage( $success, $total ) {
95                 $ratio = wfPercent( 100 * $success / $total );
96                 print $this->term->color( 1 ) . "Passed $success of $total tests ($ratio)... ";
98                 if ( $success == $total ) {
99                         print $this->term->color( 32 ) . "ALL TESTS PASSED!";
100                 } else {
101                         $failed = $total - $success;
102                         print $this->term->color( 31 ) . "$failed tests failed!";
103                 }
105                 print $this->term->reset() . "\n";
107                 return ( $success == $total );
108         }
111 class DbTestPreviewer extends TestRecorder {
112         protected $lb; // /< Database load balancer
113         protected $db; // /< Database connection to the main DB
114         protected $curRun; // /< run ID number for the current run
115         protected $prevRun; // /< run ID number for the previous run, if any
116         protected $results; // /< Result array
118         /**
119          * This should be called before the table prefix is changed
120          * @param TestRecorder $parent
121          */
122         function __construct( $parent ) {
123                 parent::__construct( $parent );
125                 $this->lb = wfGetLBFactory()->newMainLB();
126                 // This connection will have the wiki's table prefix, not parsertest_
127                 $this->db = $this->lb->getConnection( DB_MASTER );
128         }
130         /**
131          * Set up result recording; insert a record for the run with the date
132          * and all that fun stuff
133          */
134         function start() {
135                 parent::start();
137                 if ( !$this->db->tableExists( 'testrun', __METHOD__ )
138                         || !$this->db->tableExists( 'testitem', __METHOD__ )
139                 ) {
140                         print "WARNING> `testrun` table not found in database.\n";
141                         $this->prevRun = false;
142                 } else {
143                         // We'll make comparisons against the previous run later...
144                         $this->prevRun = $this->db->selectField( 'testrun', 'MAX(tr_id)' );
145                 }
147                 $this->results = array();
148         }
150         function record( $test, $result ) {
151                 parent::record( $test, $result );
152                 $this->results[$test] = $result;
153         }
155         function report() {
156                 if ( $this->prevRun ) {
157                         // f = fail, p = pass, n = nonexistent
158                         // codes show before then after
159                         $table = array(
160                                 'fp' => 'previously failing test(s) now PASSING! :)',
161                                 'pn' => 'previously PASSING test(s) removed o_O',
162                                 'np' => 'new PASSING test(s) :)',
164                                 'pf' => 'previously passing test(s) now FAILING! :(',
165                                 'fn' => 'previously FAILING test(s) removed O_o',
166                                 'nf' => 'new FAILING test(s) :(',
167                                 'ff' => 'still FAILING test(s) :(',
168                         );
170                         $prevResults = array();
172                         $res = $this->db->select( 'testitem', array( 'ti_name', 'ti_success' ),
173                                 array( 'ti_run' => $this->prevRun ), __METHOD__ );
175                         foreach ( $res as $row ) {
176                                 if ( !$this->parent->regex
177                                         || preg_match( "/{$this->parent->regex}/i", $row->ti_name )
178                                 ) {
179                                         $prevResults[$row->ti_name] = $row->ti_success;
180                                 }
181                         }
183                         $combined = array_keys( $this->results + $prevResults );
185                         # Determine breakdown by change type
186                         $breakdown = array();
187                         foreach ( $combined as $test ) {
188                                 if ( !isset( $prevResults[$test] ) ) {
189                                         $before = 'n';
190                                 } elseif ( $prevResults[$test] == 1 ) {
191                                         $before = 'p';
192                                 } else /* if ( $prevResults[$test] == 0 )*/ {
193                                         $before = 'f';
194                                 }
196                                 if ( !isset( $this->results[$test] ) ) {
197                                         $after = 'n';
198                                 } elseif ( $this->results[$test] == 1 ) {
199                                         $after = 'p';
200                                 } else /*if ( $this->results[$test] == 0 ) */ {
201                                         $after = 'f';
202                                 }
204                                 $code = $before . $after;
206                                 if ( isset( $table[$code] ) ) {
207                                         $breakdown[$code][$test] = $this->getTestStatusInfo( $test, $after );
208                                 }
209                         }
211                         # Write out results
212                         foreach ( $table as $code => $label ) {
213                                 if ( !empty( $breakdown[$code] ) ) {
214                                         $count = count( $breakdown[$code] );
215                                         printf( "\n%4d %s\n", $count, $label );
217                                         foreach ( $breakdown[$code] as $differing_test_name => $statusInfo ) {
218                                                 print "      * $differing_test_name  [$statusInfo]\n";
219                                         }
220                                 }
221                         }
222                 } else {
223                         print "No previous test runs to compare against.\n";
224                 }
226                 print "\n";
227                 parent::report();
228         }
230         /**
231          * Returns a string giving information about when a test last had a status change.
232          * Could help to track down when regressions were introduced, as distinct from tests
233          * which have never passed (which are more change requests than regressions).
234          * @param string $testname
235          * @param string $after
236          * @return string
237          */
238         private function getTestStatusInfo( $testname, $after ) {
239                 // If we're looking at a test that has just been removed, then say when it first appeared.
240                 if ( $after == 'n' ) {
241                         $changedRun = $this->db->selectField( 'testitem',
242                                 'MIN(ti_run)',
243                                 array( 'ti_name' => $testname ),
244                                 __METHOD__ );
245                         $appear = $this->db->selectRow( 'testrun',
246                                 array( 'tr_date', 'tr_mw_version' ),
247                                 array( 'tr_id' => $changedRun ),
248                                 __METHOD__ );
250                         return "First recorded appearance: "
251                                 . date( "d-M-Y H:i:s", strtotime( $appear->tr_date ) )
252                                 . ", " . $appear->tr_mw_version;
253                 }
255                 // Otherwise, this test has previous recorded results.
256                 // See when this test last had a different result to what we're seeing now.
257                 $conds = array(
258                         'ti_name' => $testname,
259                         'ti_success' => ( $after == 'f' ? "1" : "0" ) );
261                 if ( $this->curRun ) {
262                         $conds[] = "ti_run != " . $this->db->addQuotes( $this->curRun );
263                 }
265                 $changedRun = $this->db->selectField( 'testitem', 'MAX(ti_run)', $conds, __METHOD__ );
267                 // If no record of ever having had a different result.
268                 if ( is_null( $changedRun ) ) {
269                         if ( $after == "f" ) {
270                                 return "Has never passed";
271                         } else {
272                                 return "Has never failed";
273                         }
274                 }
276                 // Otherwise, we're looking at a test whose status has changed.
277                 // (i.e. it used to work, but now doesn't; or used to fail, but is now fixed.)
278                 // In this situation, give as much info as we can as to when it changed status.
279                 $pre = $this->db->selectRow( 'testrun',
280                         array( 'tr_date', 'tr_mw_version' ),
281                         array( 'tr_id' => $changedRun ),
282                         __METHOD__ );
283                 $post = $this->db->selectRow( 'testrun',
284                         array( 'tr_date', 'tr_mw_version' ),
285                         array( "tr_id > " . $this->db->addQuotes( $changedRun ) ),
286                         __METHOD__,
287                         array( "LIMIT" => 1, "ORDER BY" => 'tr_id' )
288                 );
290                 if ( $post ) {
291                         $postDate = date( "d-M-Y H:i:s", strtotime( $post->tr_date ) ) . ", {$post->tr_mw_version}";
292                 } else {
293                         $postDate = 'now';
294                 }
296                 return ( $after == "f" ? "Introduced" : "Fixed" ) . " between "
297                         . date( "d-M-Y H:i:s", strtotime( $pre->tr_date ) ) . ", " . $pre->tr_mw_version
298                         . " and $postDate";
299         }
301         /**
302          * Commit transaction and clean up for result recording
303          */
304         function end() {
305                 $this->lb->commitMasterChanges();
306                 $this->lb->closeAll();
307                 parent::end();
308         }
311 class DbTestRecorder extends DbTestPreviewer {
312         public $version;
314         /**
315          * Set up result recording; insert a record for the run with the date
316          * and all that fun stuff
317          */
318         function start() {
319                 $this->db->begin( __METHOD__ );
321                 if ( !$this->db->tableExists( 'testrun' )
322                         || !$this->db->tableExists( 'testitem' )
323                 ) {
324                         print "WARNING> `testrun` table not found in database. Trying to create table.\n";
325                         $this->db->sourceFile( $this->db->patchPath( 'patch-testrun.sql' ) );
326                         echo "OK, resuming.\n";
327                 }
329                 parent::start();
331                 $this->db->insert( 'testrun',
332                         array(
333                                 'tr_date' => $this->db->timestamp(),
334                                 'tr_mw_version' => $this->version,
335                                 'tr_php_version' => PHP_VERSION,
336                                 'tr_db_version' => $this->db->getServerVersion(),
337                                 'tr_uname' => php_uname()
338                         ),
339                         __METHOD__ );
340                 if ( $this->db->getType() === 'postgres' ) {
341                         $this->curRun = $this->db->currentSequenceValue( 'testrun_id_seq' );
342                 } else {
343                         $this->curRun = $this->db->insertId();
344                 }
345         }
347         /**
348          * Record an individual test item's success or failure to the db
349          *
350          * @param string $test
351          * @param bool $result
352          */
353         function record( $test, $result ) {
354                 parent::record( $test, $result );
356                 $this->db->insert( 'testitem',
357                         array(
358                                 'ti_run' => $this->curRun,
359                                 'ti_name' => $test,
360                                 'ti_success' => $result ? 1 : 0,
361                         ),
362                         __METHOD__ );
363         }
366 class TestFileIterator implements Iterator {
367         private $file;
368         private $fh;
369         /**
370          * @var ParserTest|MediaWikiParserTest An instance of ParserTest (parserTests.php)
371          *  or MediaWikiParserTest (phpunit)
372          */
373         private $parserTest;
374         private $index = 0;
375         private $test;
376         private $section = null;
377         /** String|null: current test section being analyzed */
378         private $sectionData = array();
379         private $lineNum;
380         private $eof;
381         # Create a fake parser tests which never run anything unless
382         # asked to do so. This will avoid running hooks for a disabled test
383         private $delayedParserTest;
384         private $nextSubTest = 0;
386         function __construct( $file, $parserTest ) {
387                 $this->file = $file;
388                 $this->fh = fopen( $this->file, "rt" );
390                 if ( !$this->fh ) {
391                         throw new MWException( "Couldn't open file '$file'\n" );
392                 }
394                 $this->parserTest = $parserTest;
395                 $this->delayedParserTest = new DelayedParserTest();
397                 $this->lineNum = $this->index = 0;
398         }
400         function rewind() {
401                 if ( fseek( $this->fh, 0 ) ) {
402                         throw new MWException( "Couldn't fseek to the start of '$this->file'\n" );
403                 }
405                 $this->index = -1;
406                 $this->lineNum = 0;
407                 $this->eof = false;
408                 $this->next();
410                 return true;
411         }
413         function current() {
414                 return $this->test;
415         }
417         function key() {
418                 return $this->index;
419         }
421         function next() {
422                 if ( $this->readNextTest() ) {
423                         $this->index++;
424                         return true;
425                 } else {
426                         $this->eof = true;
427                 }
428         }
430         function valid() {
431                 return $this->eof != true;
432         }
434         function setupCurrentTest() {
435                 // "input" and "result" are old section names allowed
436                 // for backwards-compatibility.
437                 $input = $this->checkSection( array( 'wikitext', 'input' ), false );
438                 $result = $this->checkSection( array( 'html/php', 'html/*', 'html', 'result' ), false );
439                 // some tests have "with tidy" and "without tidy" variants
440                 $tidy = $this->checkSection( array( 'html/php+tidy', 'html+tidy' ), false );
441                 if ( $tidy != false ) {
442                         if ( $this->nextSubTest == 0 ) {
443                                 if ( $result != false ) {
444                                         $this->nextSubTest = 1; // rerun non-tidy variant later
445                                 }
446                                 $result = $tidy;
447                         } else {
448                                 $this->nextSubTest = 0; // go on to next test after this
449                                 $tidy = false;
450                         }
451                 }
453                 if ( !isset( $this->sectionData['options'] ) ) {
454                         $this->sectionData['options'] = '';
455                 }
457                 if ( !isset( $this->sectionData['config'] ) ) {
458                         $this->sectionData['config'] = '';
459                 }
461                 $isDisabled = preg_match( '/\\bdisabled\\b/i', $this->sectionData['options'] ) &&
462                         !$this->parserTest->runDisabled;
463                 $isParsoidOnly = preg_match( '/\\bparsoid\\b/i', $this->sectionData['options'] ) &&
464                         $result == 'html' &&
465                         !$this->parserTest->runParsoid;
466                 $isFiltered = !preg_match( "/" . $this->parserTest->regex . "/i", $this->sectionData['test'] );
467                 if ( $input == false || $result == false || $isDisabled || $isParsoidOnly || $isFiltered ) {
468                         # disabled test
469                         return false;
470                 }
472                 # We are really going to run the test, run pending hooks and hooks function
473                 wfDebug( __METHOD__ . " unleashing delayed test for: {$this->sectionData['test']}" );
474                 $hooksResult = $this->delayedParserTest->unleash( $this->parserTest );
475                 if ( !$hooksResult ) {
476                         # Some hook reported an issue. Abort.
477                         throw new MWException( "Problem running requested parser hook from the test file" );
478                 }
480                 $this->test = array(
481                         'test' => ParserTest::chomp( $this->sectionData['test'] ),
482                         'input' => ParserTest::chomp( $this->sectionData[$input] ),
483                         'result' => ParserTest::chomp( $this->sectionData[$result] ),
484                         'options' => ParserTest::chomp( $this->sectionData['options'] ),
485                         'config' => ParserTest::chomp( $this->sectionData['config'] ),
486                 );
487                 if ( $tidy != false ) {
488                         $this->test['options'] .= " tidy";
489                 }
490                 return true;
491         }
493         function readNextTest() {
494                 # Run additional subtests of previous test
495                 while ( $this->nextSubTest > 0 ) {
496                         if ( $this->setupCurrentTest() ) {
497                                 return true;
498                         }
499                 }
501                 $this->clearSection();
502                 # Reset hooks for the delayed test object
503                 $this->delayedParserTest->reset();
505                 while ( false !== ( $line = fgets( $this->fh ) ) ) {
506                         $this->lineNum++;
507                         $matches = array();
509                         if ( preg_match( '/^!!\s*(\S+)/', $line, $matches ) ) {
510                                 $this->section = strtolower( $matches[1] );
512                                 if ( $this->section == 'endarticle' ) {
513                                         $this->checkSection( 'text' );
514                                         $this->checkSection( 'article' );
516                                         $this->parserTest->addArticle(
517                                                 ParserTest::chomp( $this->sectionData['article'] ),
518                                                 $this->sectionData['text'], $this->lineNum );
520                                         $this->clearSection();
522                                         continue;
523                                 }
525                                 if ( $this->section == 'endhooks' ) {
526                                         $this->checkSection( 'hooks' );
528                                         foreach ( explode( "\n", $this->sectionData['hooks'] ) as $line ) {
529                                                 $line = trim( $line );
531                                                 if ( $line ) {
532                                                         $this->delayedParserTest->requireHook( $line );
533                                                 }
534                                         }
536                                         $this->clearSection();
538                                         continue;
539                                 }
541                                 if ( $this->section == 'endfunctionhooks' ) {
542                                         $this->checkSection( 'functionhooks' );
544                                         foreach ( explode( "\n", $this->sectionData['functionhooks'] ) as $line ) {
545                                                 $line = trim( $line );
547                                                 if ( $line ) {
548                                                         $this->delayedParserTest->requireFunctionHook( $line );
549                                                 }
550                                         }
552                                         $this->clearSection();
554                                         continue;
555                                 }
557                                 if ( $this->section == 'endtransparenthooks' ) {
558                                         $this->checkSection( 'transparenthooks' );
560                                         foreach ( explode( "\n", $this->sectionData['transparenthooks'] ) as $line ) {
561                                                 $line = trim( $line );
563                                                 if ( $line ) {
564                                                         $this->delayedParserTest->requireTransparentHook( $line );
565                                                 }
566                                         }
568                                         $this->clearSection();
570                                         continue;
571                                 }
573                                 if ( $this->section == 'end' ) {
574                                         $this->checkSection( 'test' );
575                                         do {
576                                                 if ( $this->setupCurrentTest() ) {
577                                                         return true;
578                                                 }
579                                         } while ( $this->nextSubTest > 0 );
580                                         # go on to next test (since this was disabled)
581                                         $this->clearSection();
582                                         $this->delayedParserTest->reset();
583                                         continue;
584                                 }
586                                 if ( isset( $this->sectionData[$this->section] ) ) {
587                                         throw new MWException( "duplicate section '$this->section' "
588                                                 . "at line {$this->lineNum} of $this->file\n" );
589                                 }
591                                 $this->sectionData[$this->section] = '';
593                                 continue;
594                         }
596                         if ( $this->section ) {
597                                 $this->sectionData[$this->section] .= $line;
598                         }
599                 }
601                 return false;
602         }
604         /**
605          * Clear section name and its data
606          */
607         private function clearSection() {
608                 $this->sectionData = array();
609                 $this->section = null;
611         }
613         /**
614          * Verify the current section data has some value for the given token
615          * name(s) (first parameter).
616          * Throw an exception if it is not set, referencing current section
617          * and adding the current file name and line number
618          *
619          * @param string|array $tokens Expected token(s) that should have been
620          * mentioned before closing this section
621          * @param bool $fatal True iff an exception should be thrown if
622          * the section is not found.
623          * @return bool|string
624          * @throws MWException
625          */
626         private function checkSection( $tokens, $fatal = true ) {
627                 if ( is_null( $this->section ) ) {
628                         throw new MWException( __METHOD__ . " can not verify a null section!\n" );
629                 }
630                 if ( !is_array( $tokens ) ) {
631                         $tokens = array( $tokens );
632                 }
633                 if ( count( $tokens ) == 0 ) {
634                         throw new MWException( __METHOD__ . " can not verify zero sections!\n" );
635                 }
637                 $data = $this->sectionData;
638                 $tokens = array_filter( $tokens, function ( $token ) use ( $data ) {
639                         return isset( $data[$token] );
640                 } );
642                 if ( count( $tokens ) == 0 ) {
643                         if ( !$fatal ) {
644                                 return false;
645                         }
646                         throw new MWException( sprintf(
647                                 "'%s' without '%s' at line %s of %s\n",
648                                 $this->section,
649                                 implode( ',', $tokens ),
650                                 $this->lineNum,
651                                 $this->file
652                         ) );
653                 }
654                 if ( count( $tokens ) > 1 ) {
655                         throw new MWException( sprintf(
656                                 "'%s' with unexpected tokens '%s' at line %s of %s\n",
657                                 $this->section,
658                                 implode( ',', $tokens ),
659                                 $this->lineNum,
660                                 $this->file
661                         ) );
662                 }
664                 $tokens = array_values( $tokens );
665                 return $tokens[0];
666         }
670  * A class to delay execution of a parser test hooks.
671  */
672 class DelayedParserTest {
674         /** Initialized on construction */
675         private $hooks;
676         private $fnHooks;
677         private $transparentHooks;
679         public function __construct() {
680                 $this->reset();
681         }
683         /**
684          * Init/reset or forgot about the current delayed test.
685          * Call to this will erase any hooks function that were pending.
686          */
687         public function reset() {
688                 $this->hooks = array();
689                 $this->fnHooks = array();
690                 $this->transparentHooks = array();
691         }
693         /**
694          * Called whenever we actually want to run the hook.
695          * Should be the case if we found the parserTest is not disabled
696          * @param ParserTest|NewParserTest $parserTest
697          * @return bool
698          * @throws MWException
699          */
700         public function unleash( &$parserTest ) {
701                 if ( !( $parserTest instanceof ParserTest || $parserTest instanceof NewParserTest ) ) {
702                         throw new MWException( __METHOD__ . " must be passed an instance of ParserTest or "
703                                 . "NewParserTest classes\n" );
704                 }
706                 # Trigger delayed hooks. Any failure will make us abort
707                 foreach ( $this->hooks as $hook ) {
708                         $ret = $parserTest->requireHook( $hook );
709                         if ( !$ret ) {
710                                 return false;
711                         }
712                 }
714                 # Trigger delayed function hooks. Any failure will make us abort
715                 foreach ( $this->fnHooks as $fnHook ) {
716                         $ret = $parserTest->requireFunctionHook( $fnHook );
717                         if ( !$ret ) {
718                                 return false;
719                         }
720                 }
722                 # Trigger delayed transparent hooks. Any failure will make us abort
723                 foreach ( $this->transparentHooks as $hook ) {
724                         $ret = $parserTest->requireTransparentHook( $hook );
725                         if ( !$ret ) {
726                                 return false;
727                         }
728                 }
730                 # Delayed execution was successful.
731                 return true;
732         }
734         /**
735          * Similar to ParserTest object but does not run anything
736          * Use unleash() to really execute the hook
737          * @param string $hook
738          */
739         public function requireHook( $hook ) {
740                 $this->hooks[] = $hook;
741         }
743         /**
744          * Similar to ParserTest object but does not run anything
745          * Use unleash() to really execute the hook function
746          * @param string $fnHook
747          */
748         public function requireFunctionHook( $fnHook ) {
749                 $this->fnHooks[] = $fnHook;
750         }
752         /**
753          * Similar to ParserTest object but does not run anything
754          * Use unleash() to really execute the hook function
755          * @param string $hook
756          */
757         public function requireTransparentHook( $hook ) {
758                 $this->transparentHooks[] = $hook;
759         }
764  * Initialize and detect the DjVu files support
765  */
766 class DjVuSupport {
768         /**
769          * Initialises DjVu tools global with default values
770          */
771         public function __construct() {
772                 global $wgDjvuRenderer, $wgDjvuDump, $wgDjvuToXML, $wgFileExtensions, $wgDjvuTxt;
774                 $wgDjvuRenderer = $wgDjvuRenderer ? $wgDjvuRenderer : '/usr/bin/ddjvu';
775                 $wgDjvuDump = $wgDjvuDump ? $wgDjvuDump : '/usr/bin/djvudump';
776                 $wgDjvuToXML = $wgDjvuToXML ? $wgDjvuToXML : '/usr/bin/djvutoxml';
777                 $wgDjvuTxt = $wgDjvuTxt ? $wgDjvuTxt : '/usr/bin/djvutxt';
779                 if ( !in_array( 'djvu', $wgFileExtensions ) ) {
780                         $wgFileExtensions[] = 'djvu';
781                 }
782         }
784         /**
785          * Returns true if the DjVu tools are usable
786          *
787          * @return bool
788          */
789         public function isEnabled() {
790                 global $wgDjvuRenderer, $wgDjvuDump, $wgDjvuToXML, $wgDjvuTxt;
792                 return is_executable( $wgDjvuRenderer )
793                         && is_executable( $wgDjvuDump )
794                         && is_executable( $wgDjvuToXML )
795                         && is_executable( $wgDjvuTxt );
796         }
800  * Initialize and detect the tidy support
801  */
802 class TidySupport {
803         private $internalTidy;
804         private $externalTidy;
806         /**
807          * Determine if there is a usable tidy.
808          */
809         public function __construct() {
810                 global $wgTidyBin;
812                 $this->internalTidy = extension_loaded( 'tidy' ) &&
813                         class_exists( 'tidy' ) && !wfIsHHVM();
815                 $this->externalTidy = is_executable( $wgTidyBin ) ||
816                         Installer::locateExecutableInDefaultPaths( array( $wgTidyBin ) )
817                         !== false;
818         }
820         /**
821          * Returns true if we should use internal tidy.
822          *
823          * @return bool
824          */
825         public function isInternal() {
826                 return $this->internalTidy;
827         }
829         /**
830          * Returns true if tidy is usable
831          *
832          * @return bool
833          */
834         public function isEnabled() {
835                 return $this->internalTidy || $this->externalTidy;
836         }