Implement extension registration from an extension.json file
[mediawiki.git] / tests / testHelpers.inc
blob6d3ac2f5cbf5dce16edd8e1e68b4e2f926996d2d
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'] ) && !$this->parserTest->runDisabled;
462                 $isParsoidOnly = preg_match( '/\\bparsoid\\b/i', $this->sectionData['options'] ) && $result == 'html' && !$this->parserTest->runParsoid;
463                 $isFiltered = !preg_match( "/" . $this->parserTest->regex . "/i", $this->sectionData['test'] );
464                 if ( $input == false || $result == false || $isDisabled || $isParsoidOnly || $isFiltered ) {
465                         # disabled test
466                         return false;
467                 }
469                 # We are really going to run the test, run pending hooks and hooks function
470                 wfDebug( __METHOD__ . " unleashing delayed test for: {$this->sectionData['test']}" );
471                 $hooksResult = $this->delayedParserTest->unleash( $this->parserTest );
472                 if ( !$hooksResult ) {
473                         # Some hook reported an issue. Abort.
474                         throw new MWException( "Problem running requested parser hook from the test file" );
475                 }
477                 $this->test = array(
478                         'test' => ParserTest::chomp( $this->sectionData['test'] ),
479                         'input' => ParserTest::chomp( $this->sectionData[$input] ),
480                         'result' => ParserTest::chomp( $this->sectionData[$result] ),
481                         'options' => ParserTest::chomp( $this->sectionData['options'] ),
482                         'config' => ParserTest::chomp( $this->sectionData['config'] ),
483                 );
484                 if ( $tidy != false ) {
485                         $this->test['options'] .= " tidy";
486                 }
487                 return true;
488         }
490         function readNextTest() {
491                 # Run additional subtests of previous test
492                 while ( $this->nextSubTest > 0 ) {
493                         if ( $this->setupCurrentTest() ) {
494                                 return true;
495                         }
496                 }
498                 $this->clearSection();
499                 # Reset hooks for the delayed test object
500                 $this->delayedParserTest->reset();
502                 while ( false !== ( $line = fgets( $this->fh ) ) ) {
503                         $this->lineNum++;
504                         $matches = array();
506                         if ( preg_match( '/^!!\s*(\S+)/', $line, $matches ) ) {
507                                 $this->section = strtolower( $matches[1] );
509                                 if ( $this->section == 'endarticle' ) {
510                                         $this->checkSection( 'text' );
511                                         $this->checkSection( 'article' );
513                                         $this->parserTest->addArticle(
514                                                 ParserTest::chomp( $this->sectionData['article'] ),
515                                                 $this->sectionData['text'], $this->lineNum );
517                                         $this->clearSection();
519                                         continue;
520                                 }
522                                 if ( $this->section == 'endhooks' ) {
523                                         $this->checkSection( 'hooks' );
525                                         foreach ( explode( "\n", $this->sectionData['hooks'] ) as $line ) {
526                                                 $line = trim( $line );
528                                                 if ( $line ) {
529                                                         $this->delayedParserTest->requireHook( $line );
530                                                 }
531                                         }
533                                         $this->clearSection();
535                                         continue;
536                                 }
538                                 if ( $this->section == 'endfunctionhooks' ) {
539                                         $this->checkSection( 'functionhooks' );
541                                         foreach ( explode( "\n", $this->sectionData['functionhooks'] ) as $line ) {
542                                                 $line = trim( $line );
544                                                 if ( $line ) {
545                                                         $this->delayedParserTest->requireFunctionHook( $line );
546                                                 }
547                                         }
549                                         $this->clearSection();
551                                         continue;
552                                 }
554                                 if ( $this->section == 'endtransparenthooks' ) {
555                                         $this->checkSection( 'transparenthooks' );
557                                         foreach ( explode( "\n", $this->sectionData['transparenthooks'] ) as $line ) {
558                                                 $line = trim( $line );
560                                                 if ( $line ) {
561                                                         $this->delayedParserTest->requireTransparentHook( $line );
562                                                 }
563                                         }
565                                         $this->clearSection();
567                                         continue;
568                                 }
570                                 if ( $this->section == 'end' ) {
571                                         $this->checkSection( 'test' );
572                                         do {
573                                                 if ( $this->setupCurrentTest() ) {
574                                                         return true;
575                                                 }
576                                         } while ( $this->nextSubTest > 0 );
577                                         # go on to next test (since this was disabled)
578                                         $this->clearSection();
579                                         $this->delayedParserTest->reset();
580                                         continue;
581                                 }
583                                 if ( isset( $this->sectionData[$this->section] ) ) {
584                                         throw new MWException( "duplicate section '$this->section' "
585                                                 . "at line {$this->lineNum} of $this->file\n" );
586                                 }
588                                 $this->sectionData[$this->section] = '';
590                                 continue;
591                         }
593                         if ( $this->section ) {
594                                 $this->sectionData[$this->section] .= $line;
595                         }
596                 }
598                 return false;
599         }
601         /**
602          * Clear section name and its data
603          */
604         private function clearSection() {
605                 $this->sectionData = array();
606                 $this->section = null;
608         }
610         /**
611          * Verify the current section data has some value for the given token
612          * name(s) (first parameter).
613          * Throw an exception if it is not set, referencing current section
614          * and adding the current file name and line number
615          *
616          * @param string|array $tokens Expected token(s) that should have been
617          * mentioned before closing this section
618          * @param bool $fatal True iff an exception should be thrown if
619          * the section is not found.
620          * @return bool|string
621          * @throws MWException
622          */
623         private function checkSection( $tokens, $fatal = true ) {
624                 if ( is_null( $this->section ) ) {
625                         throw new MWException( __METHOD__ . " can not verify a null section!\n" );
626                 }
627                 if ( !is_array( $tokens ) ) {
628                         $tokens = array( $tokens );
629                 }
630                 if ( count( $tokens ) == 0 ) {
631                         throw new MWException( __METHOD__ . " can not verify zero sections!\n" );
632                 }
634                 $data = $this->sectionData;
635                 $tokens = array_filter( $tokens, function ( $token ) use ( $data ) {
636                         return isset( $data[$token] );
637                 } );
639                 if ( count( $tokens ) == 0 ) {
640                         if ( !$fatal ) {
641                                 return false;
642                         }
643                         throw new MWException( sprintf(
644                                 "'%s' without '%s' at line %s of %s\n",
645                                 $this->section,
646                                 implode( ',', $tokens ),
647                                 $this->lineNum,
648                                 $this->file
649                         ) );
650                 }
651                 if ( count( $tokens ) > 1 ) {
652                         throw new MWException( sprintf(
653                                 "'%s' with unexpected tokens '%s' at line %s of %s\n",
654                                 $this->section,
655                                 implode( ',', $tokens ),
656                                 $this->lineNum,
657                                 $this->file
658                         ) );
659                 }
661                 $tokens = array_values( $tokens );
662                 return $tokens[0];
663         }
667  * A class to delay execution of a parser test hooks.
668  */
669 class DelayedParserTest {
671         /** Initialized on construction */
672         private $hooks;
673         private $fnHooks;
674         private $transparentHooks;
676         public function __construct() {
677                 $this->reset();
678         }
680         /**
681          * Init/reset or forgot about the current delayed test.
682          * Call to this will erase any hooks function that were pending.
683          */
684         public function reset() {
685                 $this->hooks = array();
686                 $this->fnHooks = array();
687                 $this->transparentHooks = array();
688         }
690         /**
691          * Called whenever we actually want to run the hook.
692          * Should be the case if we found the parserTest is not disabled
693          * @param ParserTest|NewParserTest $parserTest
694          * @return bool
695          * @throws MWException
696          */
697         public function unleash( &$parserTest ) {
698                 if ( !( $parserTest instanceof ParserTest || $parserTest instanceof NewParserTest )     ) {
699                         throw new MWException( __METHOD__ . " must be passed an instance of ParserTest or "
700                                 . "NewParserTest classes\n" );
701                 }
703                 # Trigger delayed hooks. Any failure will make us abort
704                 foreach ( $this->hooks as $hook ) {
705                         $ret = $parserTest->requireHook( $hook );
706                         if ( !$ret ) {
707                                 return false;
708                         }
709                 }
711                 # Trigger delayed function hooks. Any failure will make us abort
712                 foreach ( $this->fnHooks as $fnHook ) {
713                         $ret = $parserTest->requireFunctionHook( $fnHook );
714                         if ( !$ret ) {
715                                 return false;
716                         }
717                 }
719                 # Trigger delayed transparent hooks. Any failure will make us abort
720                 foreach ( $this->transparentHooks as $hook ) {
721                         $ret = $parserTest->requireTransparentHook( $hook );
722                         if ( !$ret ) {
723                                 return false;
724                         }
725                 }
727                 # Delayed execution was successful.
728                 return true;
729         }
731         /**
732          * Similar to ParserTest object but does not run anything
733          * Use unleash() to really execute the hook
734          * @param string $hook
735          */
736         public function requireHook( $hook ) {
737                 $this->hooks[] = $hook;
738         }
740         /**
741          * Similar to ParserTest object but does not run anything
742          * Use unleash() to really execute the hook function
743          * @param string $fnHook
744          */
745         public function requireFunctionHook( $fnHook ) {
746                 $this->fnHooks[] = $fnHook;
747         }
749         /**
750          * Similar to ParserTest object but does not run anything
751          * Use unleash() to really execute the hook function
752          * @param string $hook
753          */
754         public function requireTransparentHook( $hook ) {
755                 $this->transparentHooks[] = $hook;
756         }
761  * Initialize and detect the DjVu files support
762  */
763 class DjVuSupport {
765         /**
766          * Initialises DjVu tools global with default values
767          */
768         public function __construct() {
769                 global $wgDjvuRenderer, $wgDjvuDump, $wgDjvuToXML, $wgFileExtensions, $wgDjvuTxt;
771                 $wgDjvuRenderer = $wgDjvuRenderer ? $wgDjvuRenderer : '/usr/bin/ddjvu';
772                 $wgDjvuDump = $wgDjvuDump ? $wgDjvuDump : '/usr/bin/djvudump';
773                 $wgDjvuToXML = $wgDjvuToXML ? $wgDjvuToXML : '/usr/bin/djvutoxml';
774                 $wgDjvuTxt = $wgDjvuTxt ? $wgDjvuTxt : '/usr/bin/djvutxt';
776                 if ( !in_array( 'djvu', $wgFileExtensions ) ) {
777                         $wgFileExtensions[] = 'djvu';
778                 }
779         }
781         /**
782          * Returns true if the DjVu tools are usable
783          *
784          * @return bool
785          */
786         public function isEnabled() {
787                 global $wgDjvuRenderer, $wgDjvuDump, $wgDjvuToXML, $wgDjvuTxt;
789                 return is_executable( $wgDjvuRenderer )
790                         && is_executable( $wgDjvuDump )
791                         && is_executable( $wgDjvuToXML )
792                         && is_executable( $wgDjvuTxt );
793         }
797  * Initialize and detect the tidy support
798  */
799 class TidySupport {
800         private $internalTidy;
801         private $externalTidy;
803         /**
804          * Determine if there is a usable tidy.
805          */
806         public function __construct() {
807                 global $wgTidyBin;
809                 $this->internalTidy = extension_loaded( 'tidy' ) &&
810                         class_exists( 'tidy' ) && !wfIsHHVM();
812                 $this->externalTidy = is_executable( $wgTidyBin ) ||
813                         Installer::locateExecutableInDefaultPaths( array( $wgTidyBin ) )
814                         !== false;
815         }
817         /**
818          * Returns true if we should use internal tidy.
819          *
820          * @return bool
821          */
822         public function isInternal() {
823                 return $this->internalTidy;
824         }
826         /**
827          * Returns true if tidy is usable
828          *
829          * @return bool
830          */
831         public function isEnabled() {
832                 return $this->internalTidy || $this->externalTidy;
833         }