TitleTest: fix minor todo about test naming
[mediawiki.git] / tests / testHelpers.inc
blob62dccbf0b36ac4dd934bb0c1ff76d5ab00f80041
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 hook" );
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                                                         $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          */
622         private function checkSection( $tokens, $fatal = true ) {
623                 if ( is_null( $this->section ) ) {
624                         throw new MWException( __METHOD__ . " can not verify a null section!\n" );
625                 }
626                 if ( !is_array( $tokens ) ) {
627                         $tokens = array( $tokens );
628                 }
629                 if ( count( $tokens ) == 0 ) {
630                         throw new MWException( __METHOD__ . " can not verify zero sections!\n" );
631                 }
633                 $data = $this->sectionData;
634                 $tokens = array_filter( $tokens, function ( $token ) use ( $data ) {
635                         return isset( $data[$token] );
636                 } );
638                 if ( count( $tokens ) == 0 ) {
639                         if ( !$fatal ) {
640                                 return false;
641                         }
642                         throw new MWException( sprintf(
643                                 "'%s' without '%s' at line %s of %s\n",
644                                 $this->section,
645                                 implode( ',', $tokens ),
646                                 $this->lineNum,
647                                 $this->file
648                         ) );
649                 }
650                 if ( count( $tokens ) > 1 ) {
651                         throw new MWException( sprintf(
652                                 "'%s' with unexpected tokens '%s' at line %s of %s\n",
653                                 $this->section,
654                                 implode( ',', $tokens ),
655                                 $this->lineNum,
656                                 $this->file
657                         ) );
658                 }
660                 $tokens = array_values( $tokens );
661                 return $tokens[0];
662         }
666  * A class to delay execution of a parser test hooks.
667  */
668 class DelayedParserTest {
670         /** Initialized on construction */
671         private $hooks;
672         private $fnHooks;
673         private $transparentHooks;
675         public function __construct() {
676                 $this->reset();
677         }
679         /**
680          * Init/reset or forgot about the current delayed test.
681          * Call to this will erase any hooks function that were pending.
682          */
683         public function reset() {
684                 $this->hooks = array();
685                 $this->fnHooks = array();
686                 $this->transparentHooks = array();
687         }
689         /**
690          * Called whenever we actually want to run the hook.
691          * Should be the case if we found the parserTest is not disabled
692          * @param ParserTest|NewParserTest $parserTest
693          * @return bool
694          */
695         public function unleash( &$parserTest ) {
696                 if ( !( $parserTest instanceof ParserTest || $parserTest instanceof NewParserTest )     ) {
697                         throw new MWException( __METHOD__ . " must be passed an instance of ParserTest or "
698                                 . "NewParserTest classes\n" );
699                 }
701                 # Trigger delayed hooks. Any failure will make us abort
702                 foreach ( $this->hooks as $hook ) {
703                         $ret = $parserTest->requireHook( $hook );
704                         if ( !$ret ) {
705                                 return false;
706                         }
707                 }
709                 # Trigger delayed function hooks. Any failure will make us abort
710                 foreach ( $this->fnHooks as $fnHook ) {
711                         $ret = $parserTest->requireFunctionHook( $fnHook );
712                         if ( !$ret ) {
713                                 return false;
714                         }
715                 }
717                 # Trigger delayed transparent hooks. Any failure will make us abort
718                 foreach ( $this->transparentHooks as $hook ) {
719                         $ret = $parserTest->requireTransparentHook( $hook );
720                         if ( !$ret ) {
721                                 return false;
722                         }
723                 }
725                 # Delayed execution was successful.
726                 return true;
727         }
729         /**
730          * Similar to ParserTest object but does not run anything
731          * Use unleash() to really execute the hook
732          * @param string $hook
733          */
734         public function requireHook( $hook ) {
735                 $this->hooks[] = $hook;
736         }
738         /**
739          * Similar to ParserTest object but does not run anything
740          * Use unleash() to really execute the hook function
741          * @param string $fnHook
742          */
743         public function requireFunctionHook( $fnHook ) {
744                 $this->fnHooks[] = $fnHook;
745         }
747         /**
748          * Similar to ParserTest object but does not run anything
749          * Use unleash() to really execute the hook function
750          * @param string $hook
751          */
752         public function requireTransparentHook( $hook ) {
753                 $this->transparentHooks[] = $hook;
754         }
759  * Initialize and detect the DjVu files support
760  */
761 class DjVuSupport {
763         /**
764          * Initialises DjVu tools global with default values
765          */
766         public function __construct() {
767                 global $wgDjvuRenderer, $wgDjvuDump, $wgDjvuToXML, $wgFileExtensions, $wgDjvuTxt;
769                 $wgDjvuRenderer = $wgDjvuRenderer ? $wgDjvuRenderer : '/usr/bin/ddjvu';
770                 $wgDjvuDump = $wgDjvuDump ? $wgDjvuDump : '/usr/bin/djvudump';
771                 $wgDjvuToXML = $wgDjvuToXML ? $wgDjvuToXML : '/usr/bin/djvutoxml';
772                 $wgDjvuTxt = $wgDjvuTxt ? $wgDjvuTxt : '/usr/bin/djvutxt';
774                 if ( !in_array( 'djvu', $wgFileExtensions ) ) {
775                         $wgFileExtensions[] = 'djvu';
776                 }
777         }
779         /**
780          * Returns true if the DjVu tools are usable
781          *
782          * @return bool
783          */
784         public function isEnabled() {
785                 global $wgDjvuRenderer, $wgDjvuDump, $wgDjvuToXML, $wgDjvuTxt;
787                 return is_executable( $wgDjvuRenderer )
788                         && is_executable( $wgDjvuDump )
789                         && is_executable( $wgDjvuToXML )
790                         && is_executable( $wgDjvuTxt );
791         }
795  * Initialize and detect the tidy support
796  */
797 class TidySupport {
798         private $internalTidy;
799         private $externalTidy;
801         /**
802          * Determine if there is a usable tidy.
803          */
804         public function __construct() {
805                 global $wgTidyBin;
807                 $this->internalTidy = extension_loaded( 'tidy' ) &&
808                         class_exists( 'tidy' );
810                 $this->externalTidy = is_executable( $wgTidyBin ) ||
811                         Installer::locateExecutableInDefaultPaths( array( $wgTidyBin ) )
812                         !== false;
813         }
815         /**
816          * Returns true if we should use internal tidy.
817          *
818          * @return bool
819          */
820         public function isInternal() {
821                 return $this->internalTidy;
822         }
824         /**
825          * Returns true if tidy is usable
826          *
827          * @return bool
828          */
829         public function isEnabled() {
830                 return $this->internalTidy || $this->externalTidy;
831         }