MediaWiki  REL1_24
testHelpers.inc
Go to the documentation of this file.
00001 <?php
00037 interface ITestRecorder {
00038 
00042     public function start();
00043 
00049     public function record( $test, $result );
00050 
00054     public function report();
00055 
00059     public function end();
00060 
00061 }
00062 
00063 class TestRecorder implements ITestRecorder {
00064     public $parent;
00065     public $term;
00066 
00067     function __construct( $parent ) {
00068         $this->parent = $parent;
00069         $this->term = $parent->term;
00070     }
00071 
00072     function start() {
00073         $this->total = 0;
00074         $this->success = 0;
00075     }
00076 
00077     function record( $test, $result ) {
00078         $this->total++;
00079         $this->success += ( $result ? 1 : 0 );
00080     }
00081 
00082     function end() {
00083         // dummy
00084     }
00085 
00086     function report() {
00087         if ( $this->total > 0 ) {
00088             $this->reportPercentage( $this->success, $this->total );
00089         } else {
00090             throw new MWException( "No tests found.\n" );
00091         }
00092     }
00093 
00094     function reportPercentage( $success, $total ) {
00095         $ratio = wfPercent( 100 * $success / $total );
00096         print $this->term->color( 1 ) . "Passed $success of $total tests ($ratio)... ";
00097 
00098         if ( $success == $total ) {
00099             print $this->term->color( 32 ) . "ALL TESTS PASSED!";
00100         } else {
00101             $failed = $total - $success;
00102             print $this->term->color( 31 ) . "$failed tests failed!";
00103         }
00104 
00105         print $this->term->reset() . "\n";
00106 
00107         return ( $success == $total );
00108     }
00109 }
00110 
00111 class DbTestPreviewer extends TestRecorder {
00112     protected $lb; // /< Database load balancer
00113     protected $db; // /< Database connection to the main DB
00114     protected $curRun; // /< run ID number for the current run
00115     protected $prevRun; // /< run ID number for the previous run, if any
00116     protected $results; // /< Result array
00117 
00122     function __construct( $parent ) {
00123         parent::__construct( $parent );
00124 
00125         $this->lb = wfGetLBFactory()->newMainLB();
00126         // This connection will have the wiki's table prefix, not parsertest_
00127         $this->db = $this->lb->getConnection( DB_MASTER );
00128     }
00129 
00134     function start() {
00135         parent::start();
00136 
00137         if ( !$this->db->tableExists( 'testrun', __METHOD__ )
00138             || !$this->db->tableExists( 'testitem', __METHOD__ )
00139         ) {
00140             print "WARNING> `testrun` table not found in database.\n";
00141             $this->prevRun = false;
00142         } else {
00143             // We'll make comparisons against the previous run later...
00144             $this->prevRun = $this->db->selectField( 'testrun', 'MAX(tr_id)' );
00145         }
00146 
00147         $this->results = array();
00148     }
00149 
00150     function record( $test, $result ) {
00151         parent::record( $test, $result );
00152         $this->results[$test] = $result;
00153     }
00154 
00155     function report() {
00156         if ( $this->prevRun ) {
00157             // f = fail, p = pass, n = nonexistent
00158             // codes show before then after
00159             $table = array(
00160                 'fp' => 'previously failing test(s) now PASSING! :)',
00161                 'pn' => 'previously PASSING test(s) removed o_O',
00162                 'np' => 'new PASSING test(s) :)',
00163 
00164                 'pf' => 'previously passing test(s) now FAILING! :(',
00165                 'fn' => 'previously FAILING test(s) removed O_o',
00166                 'nf' => 'new FAILING test(s) :(',
00167                 'ff' => 'still FAILING test(s) :(',
00168             );
00169 
00170             $prevResults = array();
00171 
00172             $res = $this->db->select( 'testitem', array( 'ti_name', 'ti_success' ),
00173                 array( 'ti_run' => $this->prevRun ), __METHOD__ );
00174 
00175             foreach ( $res as $row ) {
00176                 if ( !$this->parent->regex
00177                     || preg_match( "/{$this->parent->regex}/i", $row->ti_name )
00178                 ) {
00179                     $prevResults[$row->ti_name] = $row->ti_success;
00180                 }
00181             }
00182 
00183             $combined = array_keys( $this->results + $prevResults );
00184 
00185             # Determine breakdown by change type
00186             $breakdown = array();
00187             foreach ( $combined as $test ) {
00188                 if ( !isset( $prevResults[$test] ) ) {
00189                     $before = 'n';
00190                 } elseif ( $prevResults[$test] == 1 ) {
00191                     $before = 'p';
00192                 } else /* if ( $prevResults[$test] == 0 )*/ {
00193                     $before = 'f';
00194                 }
00195 
00196                 if ( !isset( $this->results[$test] ) ) {
00197                     $after = 'n';
00198                 } elseif ( $this->results[$test] == 1 ) {
00199                     $after = 'p';
00200                 } else /*if ( $this->results[$test] == 0 ) */ {
00201                     $after = 'f';
00202                 }
00203 
00204                 $code = $before . $after;
00205 
00206                 if ( isset( $table[$code] ) ) {
00207                     $breakdown[$code][$test] = $this->getTestStatusInfo( $test, $after );
00208                 }
00209             }
00210 
00211             # Write out results
00212             foreach ( $table as $code => $label ) {
00213                 if ( !empty( $breakdown[$code] ) ) {
00214                     $count = count( $breakdown[$code] );
00215                     printf( "\n%4d %s\n", $count, $label );
00216 
00217                     foreach ( $breakdown[$code] as $differing_test_name => $statusInfo ) {
00218                         print "      * $differing_test_name  [$statusInfo]\n";
00219                     }
00220                 }
00221             }
00222         } else {
00223             print "No previous test runs to compare against.\n";
00224         }
00225 
00226         print "\n";
00227         parent::report();
00228     }
00229 
00238     private function getTestStatusInfo( $testname, $after ) {
00239         // If we're looking at a test that has just been removed, then say when it first appeared.
00240         if ( $after == 'n' ) {
00241             $changedRun = $this->db->selectField( 'testitem',
00242                 'MIN(ti_run)',
00243                 array( 'ti_name' => $testname ),
00244                 __METHOD__ );
00245             $appear = $this->db->selectRow( 'testrun',
00246                 array( 'tr_date', 'tr_mw_version' ),
00247                 array( 'tr_id' => $changedRun ),
00248                 __METHOD__ );
00249 
00250             return "First recorded appearance: "
00251                 . date( "d-M-Y H:i:s", strtotime( $appear->tr_date ) )
00252                 . ", " . $appear->tr_mw_version;
00253         }
00254 
00255         // Otherwise, this test has previous recorded results.
00256         // See when this test last had a different result to what we're seeing now.
00257         $conds = array(
00258             'ti_name' => $testname,
00259             'ti_success' => ( $after == 'f' ? "1" : "0" ) );
00260 
00261         if ( $this->curRun ) {
00262             $conds[] = "ti_run != " . $this->db->addQuotes( $this->curRun );
00263         }
00264 
00265         $changedRun = $this->db->selectField( 'testitem', 'MAX(ti_run)', $conds, __METHOD__ );
00266 
00267         // If no record of ever having had a different result.
00268         if ( is_null( $changedRun ) ) {
00269             if ( $after == "f" ) {
00270                 return "Has never passed";
00271             } else {
00272                 return "Has never failed";
00273             }
00274         }
00275 
00276         // Otherwise, we're looking at a test whose status has changed.
00277         // (i.e. it used to work, but now doesn't; or used to fail, but is now fixed.)
00278         // In this situation, give as much info as we can as to when it changed status.
00279         $pre = $this->db->selectRow( 'testrun',
00280             array( 'tr_date', 'tr_mw_version' ),
00281             array( 'tr_id' => $changedRun ),
00282             __METHOD__ );
00283         $post = $this->db->selectRow( 'testrun',
00284             array( 'tr_date', 'tr_mw_version' ),
00285             array( "tr_id > " . $this->db->addQuotes( $changedRun ) ),
00286             __METHOD__,
00287             array( "LIMIT" => 1, "ORDER BY" => 'tr_id' )
00288         );
00289 
00290         if ( $post ) {
00291             $postDate = date( "d-M-Y H:i:s", strtotime( $post->tr_date ) ) . ", {$post->tr_mw_version}";
00292         } else {
00293             $postDate = 'now';
00294         }
00295 
00296         return ( $after == "f" ? "Introduced" : "Fixed" ) . " between "
00297             . date( "d-M-Y H:i:s", strtotime( $pre->tr_date ) ) . ", " . $pre->tr_mw_version
00298             . " and $postDate";
00299     }
00300 
00304     function end() {
00305         $this->lb->commitMasterChanges();
00306         $this->lb->closeAll();
00307         parent::end();
00308     }
00309 }
00310 
00311 class DbTestRecorder extends DbTestPreviewer {
00312     public $version;
00313 
00318     function start() {
00319         $this->db->begin( __METHOD__ );
00320 
00321         if ( !$this->db->tableExists( 'testrun' )
00322             || !$this->db->tableExists( 'testitem' )
00323         ) {
00324             print "WARNING> `testrun` table not found in database. Trying to create table.\n";
00325             $this->db->sourceFile( $this->db->patchPath( 'patch-testrun.sql' ) );
00326             echo "OK, resuming.\n";
00327         }
00328 
00329         parent::start();
00330 
00331         $this->db->insert( 'testrun',
00332             array(
00333                 'tr_date' => $this->db->timestamp(),
00334                 'tr_mw_version' => $this->version,
00335                 'tr_php_version' => PHP_VERSION,
00336                 'tr_db_version' => $this->db->getServerVersion(),
00337                 'tr_uname' => php_uname()
00338             ),
00339             __METHOD__ );
00340         if ( $this->db->getType() === 'postgres' ) {
00341             $this->curRun = $this->db->currentSequenceValue( 'testrun_id_seq' );
00342         } else {
00343             $this->curRun = $this->db->insertId();
00344         }
00345     }
00346 
00353     function record( $test, $result ) {
00354         parent::record( $test, $result );
00355 
00356         $this->db->insert( 'testitem',
00357             array(
00358                 'ti_run' => $this->curRun,
00359                 'ti_name' => $test,
00360                 'ti_success' => $result ? 1 : 0,
00361             ),
00362             __METHOD__ );
00363     }
00364 }
00365 
00366 class TestFileIterator implements Iterator {
00367     private $file;
00368     private $fh;
00373     private $parserTest;
00374     private $index = 0;
00375     private $test;
00376     private $section = null;
00378     private $sectionData = array();
00379     private $lineNum;
00380     private $eof;
00381     # Create a fake parser tests which never run anything unless
00382     # asked to do so. This will avoid running hooks for a disabled test
00383     private $delayedParserTest;
00384     private $nextSubTest = 0;
00385 
00386     function __construct( $file, $parserTest ) {
00387         $this->file = $file;
00388         $this->fh = fopen( $this->file, "rt" );
00389 
00390         if ( !$this->fh ) {
00391             throw new MWException( "Couldn't open file '$file'\n" );
00392         }
00393 
00394         $this->parserTest = $parserTest;
00395         $this->delayedParserTest = new DelayedParserTest();
00396 
00397         $this->lineNum = $this->index = 0;
00398     }
00399 
00400     function rewind() {
00401         if ( fseek( $this->fh, 0 ) ) {
00402             throw new MWException( "Couldn't fseek to the start of '$this->file'\n" );
00403         }
00404 
00405         $this->index = -1;
00406         $this->lineNum = 0;
00407         $this->eof = false;
00408         $this->next();
00409 
00410         return true;
00411     }
00412 
00413     function current() {
00414         return $this->test;
00415     }
00416 
00417     function key() {
00418         return $this->index;
00419     }
00420 
00421     function next() {
00422         if ( $this->readNextTest() ) {
00423             $this->index++;
00424             return true;
00425         } else {
00426             $this->eof = true;
00427         }
00428     }
00429 
00430     function valid() {
00431         return $this->eof != true;
00432     }
00433 
00434     function setupCurrentTest() {
00435         // "input" and "result" are old section names allowed
00436         // for backwards-compatibility.
00437         $input = $this->checkSection( array( 'wikitext', 'input' ), false );
00438         $result = $this->checkSection( array( 'html/php', 'html/*', 'html', 'result' ), false );
00439         // some tests have "with tidy" and "without tidy" variants
00440         $tidy = $this->checkSection( array( 'html/php+tidy', 'html+tidy' ), false );
00441         if ( $tidy != false ) {
00442             if ( $this->nextSubTest == 0 ) {
00443                 if ( $result != false ) {
00444                     $this->nextSubTest = 1; // rerun non-tidy variant later
00445                 }
00446                 $result = $tidy;
00447             } else {
00448                 $this->nextSubTest = 0; // go on to next test after this
00449                 $tidy = false;
00450             }
00451         }
00452 
00453         if ( !isset( $this->sectionData['options'] ) ) {
00454             $this->sectionData['options'] = '';
00455         }
00456 
00457         if ( !isset( $this->sectionData['config'] ) ) {
00458             $this->sectionData['config'] = '';
00459         }
00460 
00461         $isDisabled = preg_match( '/\\bdisabled\\b/i', $this->sectionData['options'] ) && !$this->parserTest->runDisabled;
00462         $isParsoidOnly = preg_match( '/\\bparsoid\\b/i', $this->sectionData['options'] ) && $result == 'html' && !$this->parserTest->runParsoid;
00463         $isFiltered = !preg_match( "/" . $this->parserTest->regex . "/i", $this->sectionData['test'] );
00464         if ( $input == false || $result == false || $isDisabled || $isParsoidOnly || $isFiltered ) {
00465             # disabled test
00466             return false;
00467         }
00468 
00469         # We are really going to run the test, run pending hooks and hooks function
00470         wfDebug( __METHOD__ . " unleashing delayed test for: {$this->sectionData['test']}" );
00471         $hooksResult = $this->delayedParserTest->unleash( $this->parserTest );
00472         if ( !$hooksResult ) {
00473             # Some hook reported an issue. Abort.
00474             throw new MWException( "Problem running hook" );
00475         }
00476 
00477         $this->test = array(
00478             'test' => ParserTest::chomp( $this->sectionData['test'] ),
00479             'input' => ParserTest::chomp( $this->sectionData[$input] ),
00480             'result' => ParserTest::chomp( $this->sectionData[$result] ),
00481             'options' => ParserTest::chomp( $this->sectionData['options'] ),
00482             'config' => ParserTest::chomp( $this->sectionData['config'] ),
00483         );
00484         if ( $tidy != false ) {
00485             $this->test['options'] .= " tidy";
00486         }
00487         return true;
00488     }
00489 
00490     function readNextTest() {
00491         # Run additional subtests of previous test
00492         while ( $this->nextSubTest > 0 ) {
00493             if ( $this->setupCurrentTest() ) {
00494                 return true;
00495             }
00496         }
00497 
00498         $this->clearSection();
00499         # Reset hooks for the delayed test object
00500         $this->delayedParserTest->reset();
00501 
00502         while ( false !== ( $line = fgets( $this->fh ) ) ) {
00503             $this->lineNum++;
00504             $matches = array();
00505 
00506             if ( preg_match( '/^!!\s*(\S+)/', $line, $matches ) ) {
00507                 $this->section = strtolower( $matches[1] );
00508 
00509                 if ( $this->section == 'endarticle' ) {
00510                     $this->checkSection( 'text' );
00511                     $this->checkSection( 'article' );
00512 
00513                     $this->parserTest->addArticle(
00514                         ParserTest::chomp( $this->sectionData['article'] ),
00515                         $this->sectionData['text'], $this->lineNum );
00516 
00517                     $this->clearSection();
00518 
00519                     continue;
00520                 }
00521 
00522                 if ( $this->section == 'endhooks' ) {
00523                     $this->checkSection( 'hooks' );
00524 
00525                     foreach ( explode( "\n", $this->sectionData['hooks'] ) as $line ) {
00526                         $line = trim( $line );
00527 
00528                         if ( $line ) {
00529                             $this->delayedParserTest->requireHook( $line );
00530                         }
00531                     }
00532 
00533                     $this->clearSection();
00534 
00535                     continue;
00536                 }
00537 
00538                 if ( $this->section == 'endfunctionhooks' ) {
00539                     $this->checkSection( 'functionhooks' );
00540 
00541                     foreach ( explode( "\n", $this->sectionData['functionhooks'] ) as $line ) {
00542                         $line = trim( $line );
00543 
00544                         if ( $line ) {
00545                             $this->delayedParserTest->requireFunctionHook( $line );
00546                         }
00547                     }
00548 
00549                     $this->clearSection();
00550 
00551                     continue;
00552                 }
00553 
00554                 if ( $this->section == 'endtransparenthooks' ) {
00555                     $this->checkSection( 'transparenthooks' );
00556 
00557                     foreach ( explode( "\n", $this->sectionData['transparenthooks'] ) as $line ) {
00558                         $line = trim( $line );
00559 
00560                         if ( $line ) {
00561                             $delayedParserTest->requireTransparentHook( $line );
00562                         }
00563                     }
00564 
00565                     $this->clearSection();
00566 
00567                     continue;
00568                 }
00569 
00570                 if ( $this->section == 'end' ) {
00571                     $this->checkSection( 'test' );
00572                     do {
00573                         if ( $this->setupCurrentTest() ) {
00574                             return true;
00575                         }
00576                     } while ( $this->nextSubTest > 0 );
00577                     # go on to next test (since this was disabled)
00578                     $this->clearSection();
00579                     $this->delayedParserTest->reset();
00580                     continue;
00581                 }
00582 
00583                 if ( isset( $this->sectionData[$this->section] ) ) {
00584                     throw new MWException( "duplicate section '$this->section' "
00585                         . "at line {$this->lineNum} of $this->file\n" );
00586                 }
00587 
00588                 $this->sectionData[$this->section] = '';
00589 
00590                 continue;
00591             }
00592 
00593             if ( $this->section ) {
00594                 $this->sectionData[$this->section] .= $line;
00595             }
00596         }
00597 
00598         return false;
00599     }
00600 
00604     private function clearSection() {
00605         $this->sectionData = array();
00606         $this->section = null;
00607 
00608     }
00609 
00622     private function checkSection( $tokens, $fatal = true ) {
00623         if ( is_null( $this->section ) ) {
00624             throw new MWException( __METHOD__ . " can not verify a null section!\n" );
00625         }
00626         if ( !is_array( $tokens ) ) {
00627             $tokens = array( $tokens );
00628         }
00629         if ( count( $tokens ) == 0 ) {
00630             throw new MWException( __METHOD__ . " can not verify zero sections!\n" );
00631         }
00632 
00633         $data = $this->sectionData;
00634         $tokens = array_filter( $tokens, function ( $token ) use ( $data ) {
00635             return isset( $data[$token] );
00636         } );
00637 
00638         if ( count( $tokens ) == 0 ) {
00639             if ( !$fatal ) {
00640                 return false;
00641             }
00642             throw new MWException( sprintf(
00643                 "'%s' without '%s' at line %s of %s\n",
00644                 $this->section,
00645                 implode( ',', $tokens ),
00646                 $this->lineNum,
00647                 $this->file
00648             ) );
00649         }
00650         if ( count( $tokens ) > 1 ) {
00651             throw new MWException( sprintf(
00652                 "'%s' with unexpected tokens '%s' at line %s of %s\n",
00653                 $this->section,
00654                 implode( ',', $tokens ),
00655                 $this->lineNum,
00656                 $this->file
00657             ) );
00658         }
00659 
00660         $tokens = array_values( $tokens );
00661         return $tokens[0];
00662     }
00663 }
00664 
00668 class DelayedParserTest {
00669 
00671     private $hooks;
00672     private $fnHooks;
00673     private $transparentHooks;
00674 
00675     public function __construct() {
00676         $this->reset();
00677     }
00678 
00683     public function reset() {
00684         $this->hooks = array();
00685         $this->fnHooks = array();
00686         $this->transparentHooks = array();
00687     }
00688 
00695     public function unleash( &$parserTest ) {
00696         if ( !( $parserTest instanceof ParserTest || $parserTest instanceof NewParserTest ) ) {
00697             throw new MWException( __METHOD__ . " must be passed an instance of ParserTest or "
00698                 . "NewParserTest classes\n" );
00699         }
00700 
00701         # Trigger delayed hooks. Any failure will make us abort
00702         foreach ( $this->hooks as $hook ) {
00703             $ret = $parserTest->requireHook( $hook );
00704             if ( !$ret ) {
00705                 return false;
00706             }
00707         }
00708 
00709         # Trigger delayed function hooks. Any failure will make us abort
00710         foreach ( $this->fnHooks as $fnHook ) {
00711             $ret = $parserTest->requireFunctionHook( $fnHook );
00712             if ( !$ret ) {
00713                 return false;
00714             }
00715         }
00716 
00717         # Trigger delayed transparent hooks. Any failure will make us abort
00718         foreach ( $this->transparentHooks as $hook ) {
00719             $ret = $parserTest->requireTransparentHook( $hook );
00720             if ( !$ret ) {
00721                 return false;
00722             }
00723         }
00724 
00725         # Delayed execution was successful.
00726         return true;
00727     }
00728 
00734     public function requireHook( $hook ) {
00735         $this->hooks[] = $hook;
00736     }
00737 
00743     public function requireFunctionHook( $fnHook ) {
00744         $this->fnHooks[] = $fnHook;
00745     }
00746 
00752     public function requireTransparentHook( $hook ) {
00753         $this->transparentHooks[] = $hook;
00754     }
00755 
00756 }
00757 
00761 class DjVuSupport {
00762 
00766     public function __construct() {
00767         global $wgDjvuRenderer, $wgDjvuDump, $wgDjvuToXML, $wgFileExtensions, $wgDjvuTxt;
00768 
00769         $wgDjvuRenderer = $wgDjvuRenderer ? $wgDjvuRenderer : '/usr/bin/ddjvu';
00770         $wgDjvuDump = $wgDjvuDump ? $wgDjvuDump : '/usr/bin/djvudump';
00771         $wgDjvuToXML = $wgDjvuToXML ? $wgDjvuToXML : '/usr/bin/djvutoxml';
00772         $wgDjvuTxt = $wgDjvuTxt ? $wgDjvuTxt : '/usr/bin/djvutxt';
00773 
00774         if ( !in_array( 'djvu', $wgFileExtensions ) ) {
00775             $wgFileExtensions[] = 'djvu';
00776         }
00777     }
00778 
00784     public function isEnabled() {
00785         global $wgDjvuRenderer, $wgDjvuDump, $wgDjvuToXML, $wgDjvuTxt;
00786 
00787         return is_executable( $wgDjvuRenderer )
00788             && is_executable( $wgDjvuDump )
00789             && is_executable( $wgDjvuToXML )
00790             && is_executable( $wgDjvuTxt );
00791     }
00792 }
00793 
00797 class TidySupport {
00798     private $internalTidy;
00799     private $externalTidy;
00800 
00804     public function __construct() {
00805         global $wgTidyBin;
00806 
00807         $this->internalTidy = extension_loaded( 'tidy' ) &&
00808             class_exists( 'tidy' );
00809 
00810         $this->externalTidy = is_executable( $wgTidyBin ) ||
00811             Installer::locateExecutableInDefaultPaths( array( $wgTidyBin ) )
00812             !== false;
00813     }
00814 
00820     public function isInternal() {
00821         return $this->internalTidy;
00822     }
00823 
00829     public function isEnabled() {
00830         return $this->internalTidy || $this->externalTidy;
00831     }
00832 }