wiki.techinc.nl/tests/phpunit/phpunit.php

868 lines
24 KiB
PHP
Raw Normal View History

#!/usr/bin/env php
<?php
/**
* Bootstrapping for MediaWiki PHPUnit tests
*
* @file
*/
use MediaWiki\MediaWikiServices;
use MediaWiki\Shell\Shell;
use Wikimedia\Rdbms\IDatabase;
use Wikimedia\Rdbms\LBFactory;
if ( defined( 'MEDIAWIKI' ) ) {
exit( 'Wrong entry point?' );
}
define( 'MW_ENTRY_POINT', 'cli' );
// Bail on old versions of PHP, or if composer has not been run yet to install
// dependencies.
require_once __DIR__ . '/../../includes/PHPVersionCheck.php';
wfEntryPointCheck( 'text' );
// Some extensions rely on MW_INSTALL_PATH to find core files to include. Setting it here helps them
// if they're included by a core script (like DatabaseUpdater) after Maintenance.php has already
// been run.
if ( strval( getenv( 'MW_INSTALL_PATH' ) ) === '' ) {
putenv( 'MW_INSTALL_PATH=' . realpath( __DIR__ . '/../..' ) );
}
class PHPUnitMaintClass {
/**
* Accessible via getConfig()
*
* @var Config|null
*/
private $config;
/**
* Array of desired/allowed params
* @var array[]
* @phan-var array<string,array{desc:string,require:bool,withArg:string,shortName:string,multiOccurrence:bool}>
*/
private $mParams = [];
/** @var array Mapping short parameters to long ones */
private $mShortParamsMap = [];
/**
* Generic options added by addDefaultParams()
* @var array[]
* @phan-var array<string,array{desc:string,require:bool,withArg:string,shortName:string,multiOccurrence:bool}>
*/
private $mGenericParameters = [];
/**
* Generic options which might or not be supported by the script
* @var array[]
* @phan-var array<string,array{desc:string,require:bool,withArg:string,shortName:string,multiOccurrence:bool}>
*/
private $mDependentParameters = [];
/** @var array This is the list of options that were actually passed */
private $mOptions = [];
/** @var bool Have we already loaded our user input? */
private $mInputLoaded = false;
/**
* Used to read the options in the order they were passed.
* Useful for option chaining (Ex. dumpBackup.php). It will
* be an empty array if the options are passed in through
* loadParamsAndArgs( $self, $opts, $args ).
*
* This is an array of arrays where
* 0 => the option and 1 => parameter value.
*
* @var array
*/
private $orderedOptions = [];
public function __construct() {
$this->addOption( 'help', 'Display this help message', false, false, 'h' );
$this->addOption( 'wiki', 'For specifying the wiki ID', false, true );
$this->mGenericParameters = $this->mParams;
$this->addOption( 'dbuser', 'The DB user to use for this script', false, true );
$this->addOption( 'dbpass', 'The password to use for this script', false, true );
$this->addOption( 'dbgroupdefault', 'The default DB group to use.', false, true );
$this->mDependentParameters = array_diff_key( $this->mParams, $this->mGenericParameters );
register_shutdown_function( [ $this, 'outputChanneled' ], false );
$this->addOption( 'use-filebackend', 'Use filebackend', false, true );
$this->addOption( 'use-bagostuff', 'Use bagostuff', false, true );
$this->addOption( 'use-jobqueue', 'Use jobqueue', false, true );
$this->addOption( 'use-normal-tables', 'Use normal DB tables.', false, false );
$this->addOption(
'reuse-db', 'Init DB only if tables are missing and keep after finish.',
false,
false
);
}
/**
* Add a parameter to the script. Will be displayed on --help
* with the associated description
*
* @param string $name The name of the param (help, version, etc)
* @param string $description The description of the param to show on --help
* @param bool $required Is the param required?
* @param bool $withArg Is an argument required with this option?
* @param string|bool $shortName Character to use as short name
* @param bool $multiOccurrence Can this option be passed multiple times?
*/
private function addOption( $name, $description, $required = false,
$withArg = false, $shortName = false, $multiOccurrence = false
) {
$this->mParams[$name] = [
'desc' => $description,
'require' => $required,
'withArg' => $withArg,
'shortName' => $shortName,
'multiOccurrence' => $multiOccurrence
];
if ( $shortName !== false ) {
$this->mShortParamsMap[$shortName] = $name;
}
}
phpunit: Repair GLOBALS reset in MediaWikiUnitTestCase This code didn't work because the $GLOBALS array is exposed by reference. Once this reference was broken by unset(), the rest just manipulated a local array that happens to be called "GLOBALS". It must not be unset or re-assigned. It can only be changed in-place. Before this, the execution of a MediaWikiUnitTestCase test stored a copy of GLOBALS in unitGlobals, then lost the GLOBALS pointer and created a new variable called "GLOBALS". As such, the tearDown() function didn't do what it meant to do, either – which then results in odd failures like T230023 Rewrite it as follows: * In setup, store the current GLOBALS keys and values, then reduce GLOBALS to only the whitelisted keys and values. * In teardown, restore the original state. * As optimisation, do this from setUpBeforeClass as well, so that there are relatively few globals to reset between tests. (Thanks @Simetrical!) The following tests were previously passing by accident under MediaWikiUnitTestCase but actually did depend on global config. * MainSlotRoleHandlerTest (…, ContentHandler, $wgContentHandlers) * SlotRecordTest (…, ContentHandler, $wgContentHandlers) * WikiReferenceTest (wfParseUrl, $wgUrlProtocols) * DifferenceEngineSlotDiffRendererTest (DifferenceEngine, wfDebug, …) * SlotDiffRendererTest (…, ContentHandler, $wgContentHandlers) * FileBackendDBRepoWrapperTest (wfWikiID, "Backend domain ID not provided") * JpegMetadataExtractorTest (…, wfDebug, …, LoggerFactory, …) * ParserFactoryTest (…, wfDebug, …, LoggerFactory, InvalidArgumentException) * MediaWikiPageNameNormalizerTest (…, wfDebug, …, LoggerFactory, …) * SiteExporterTest (SiteImporter, wfLogWarning, …) * SiteImporterTest (Site::newForType, $wgSiteTypes) * ZipDirectoryReaderTest (…, wfDebug, …, LoggerFactory, …) Bug: T230023 Change-Id: Ic22075bb5e81b7c2c4c1b8647547aa55306a10a7
2019-08-07 13:40:55 +00:00
public function setup() {
Safer autoloading with respect to file-scope code Many files were in the autoloader despite having potentially harmful file-scope code. * Exclude all CommandLineInc maintenance scripts from the autoloader. * Introduce "NO_AUTOLOAD" tag which excludes the file containing it from the autoloader. Use it on CommandLineInc.php and a few suspicious-looking files without classes in case they are refactored to add classes in the future. * Add a test which parses all non-PSR4 class files and confirms that they do not contain dangerous file-scope code. It's slow (15s) but its results were enlightening. * Several maintenance scripts define constants in the file scope, intending to modify the behaviour of MediaWiki. Either move the define() to a later setup function, or protect with NO_AUTOLOAD. * Use require_once consistently with Maintenance.php and doMaintenance.php, per the original convention which is supposed to allow one maintenance script to use the class of another maintenance script. Using require breaks autoloading of these maintenance class files. * When Maintenance.php is included, check if MediaWiki has already started, and if so, return early. Revert the fix for T250003 which is incompatible with this safety measure. Hopefully it was superseded by splitting out the class file. * In runScript.php add a redundant PHP_SAPI check since it does some things in file-scope code before any other check will be run. * Change the if(false) class_alias(...) to something more hackish and more compatible with the new test. * Some site-related scripts found Maintenance.php in a non-standard way. Use the standard way. * fileOpPerfTest.php called error_reporting(). Probably debugging code left in; removed. * Moved mediawiki.compress.7z registration from the class file to the caller. Change-Id: I1b1be90343a5ab678df6f1b1bdd03319dcf6537f
2021-01-08 02:16:02 +00:00
// Set a flag which can be used to detect when other scripts have been entered
// through this entry point or not.
define( 'MW_PHPUNIT_TEST', true );
global $IP, $wgCommandLineMode;
# Abort if called from a web server
# wfIsCLI() is not available yet
if ( PHP_SAPI !== 'cli' && PHP_SAPI !== 'phpdbg' ) {
$this->fatalError( 'This script must be run from the command line' );
}
if ( $IP === null ) {
$this->fatalError( "\$IP not set, aborting!\n" .
'(Did you forget to call parent::__construct() in your maintenance script?)' );
}
# Make sure we can handle script parameters
if ( !ini_get( 'register_argc_argv' ) ) {
$this->fatalError( 'Cannot get command line arguments, register_argc_argv is set to false' );
}
// Send PHP warnings and errors to stderr instead of stdout.
// This aids in diagnosing problems, while keeping messages
// out of redirected output.
if ( ini_get( 'display_errors' ) ) {
ini_set( 'display_errors', 'stderr' );
}
$this->loadParamsAndArgs();
# Set the memory limit
# Note we need to set it again later in cache LocalSettings changed it
$this->adjustMemoryLimit();
# Set max execution time to 0 (no limit). PHP.net says that
# "When running PHP from the command line the default setting is 0."
# But sometimes this doesn't seem to be the case.
ini_set( 'max_execution_time', 0 );
$wgCommandLineMode = true;
# Turn off output buffering if it's on
while ( ob_get_level() > 0 ) {
ob_end_flush();
}
phpunit: Repair GLOBALS reset in MediaWikiUnitTestCase This code didn't work because the $GLOBALS array is exposed by reference. Once this reference was broken by unset(), the rest just manipulated a local array that happens to be called "GLOBALS". It must not be unset or re-assigned. It can only be changed in-place. Before this, the execution of a MediaWikiUnitTestCase test stored a copy of GLOBALS in unitGlobals, then lost the GLOBALS pointer and created a new variable called "GLOBALS". As such, the tearDown() function didn't do what it meant to do, either – which then results in odd failures like T230023 Rewrite it as follows: * In setup, store the current GLOBALS keys and values, then reduce GLOBALS to only the whitelisted keys and values. * In teardown, restore the original state. * As optimisation, do this from setUpBeforeClass as well, so that there are relatively few globals to reset between tests. (Thanks @Simetrical!) The following tests were previously passing by accident under MediaWikiUnitTestCase but actually did depend on global config. * MainSlotRoleHandlerTest (…, ContentHandler, $wgContentHandlers) * SlotRecordTest (…, ContentHandler, $wgContentHandlers) * WikiReferenceTest (wfParseUrl, $wgUrlProtocols) * DifferenceEngineSlotDiffRendererTest (DifferenceEngine, wfDebug, …) * SlotDiffRendererTest (…, ContentHandler, $wgContentHandlers) * FileBackendDBRepoWrapperTest (wfWikiID, "Backend domain ID not provided") * JpegMetadataExtractorTest (…, wfDebug, …, LoggerFactory, …) * ParserFactoryTest (…, wfDebug, …, LoggerFactory, InvalidArgumentException) * MediaWikiPageNameNormalizerTest (…, wfDebug, …, LoggerFactory, …) * SiteExporterTest (SiteImporter, wfLogWarning, …) * SiteImporterTest (Site::newForType, $wgSiteTypes) * ZipDirectoryReaderTest (…, wfDebug, …, LoggerFactory, …) Bug: T230023 Change-Id: Ic22075bb5e81b7c2c4c1b8647547aa55306a10a7
2019-08-07 13:40:55 +00:00
require_once __DIR__ . '/../common/TestSetup.php';
TestSetup::snapshotGlobals();
}
/**
* Checks to see if a particular option exists.
* @param string $name The name of the option
* @return bool
*/
private function hasOption( $name ) {
return isset( $this->mOptions[$name] );
}
/**
* Get an option, or return the default.
*
* If the option was added to support multiple occurrences,
* this will return an array.
*
* @param string $name The name of the param
* @param mixed|null $default Anything you want, default null
* @return mixed
* @return-taint none
*/
private function getOption( $name, $default = null ) {
if ( $this->hasOption( $name ) ) {
return $this->mOptions[$name];
} else {
// Set it so we don't have to provide the default again
$this->mOptions[$name] = $default;
return $this->mOptions[$name];
}
}
/**
* Normally we disable the memory_limit when running admin scripts.
* Some scripts may wish to actually set a limit, however, to avoid
* blowing up unexpectedly. We also support a --memory-limit option,
* to allow sysadmins to explicitly set one if they'd prefer to override
* defaults (or for people using Suhosin which yells at you for trying
* to disable the limits)
* @stable for overriding
* @return string
*/
private function memoryLimit() {
$limit = $this->getOption( 'memory-limit', 'max' );
$limit = trim( $limit, "\" '" ); // trim quotes in case someone misunderstood
return $limit;
}
/**
* Adjusts PHP's memory limit to better suit our needs, if needed.
*/
private function adjustMemoryLimit() {
$limit = $this->memoryLimit();
if ( $limit == 'max' ) {
$limit = -1; // no memory limit
}
if ( $limit != 'default' ) {
ini_set( 'memory_limit', $limit );
}
}
/**
* Process command line arguments
* $mOptions becomes an array with keys set to the option names
* $mArgs becomes a zero-based array containing the non-option arguments
*/
private function loadParamsAndArgs() {
# If we've already loaded input (either by user values or from $argv)
# skip on loading it again. The array_shift() will corrupt values if
# it's run again and again
if ( $this->mInputLoaded ) {
$this->loadSpecialVars();
return;
}
global $argv;
$this->loadWithArgv( array_slice( $argv, 1 ) );
}
/**
* Load params and arguments from a given array
* of command-line arguments
*
* @since 1.27
* @param array $argv
*/
private function loadWithArgv( $argv ) {
$options = [];
$this->orderedOptions = [];
# Parse arguments
for ( $arg = reset( $argv ); $arg !== false; $arg = next( $argv ) ) {
if ( $arg !== '--' && substr( $arg, 0, 2 ) == '--' ) {
# Long options
$option = substr( $arg, 2 );
if ( isset( $this->mParams[$option] ) && $this->mParams[$option]['withArg'] ) {
$param = next( $argv );
if ( $param === false ) {
$this->error( "\nERROR: $option parameter needs a value after it\n" );
$this->maybeHelp( true );
}
$this->setParam( $options, $option, $param );
} else {
$bits = explode( '=', $option, 2 );
$this->setParam( $options, $bits[0], $bits[1] ?? 1 );
}
} elseif ( $arg !== '-' && substr( $arg, 0, 1 ) == '-' ) {
# Short options
$argLength = strlen( $arg );
for ( $p = 1; $p < $argLength; $p++ ) {
$option = $arg[$p];
if ( !isset( $this->mParams[$option] ) && isset( $this->mShortParamsMap[$option] ) ) {
$option = $this->mShortParamsMap[$option];
}
if ( isset( $this->mParams[$option]['withArg'] ) && $this->mParams[$option]['withArg'] ) {
$param = next( $argv );
if ( $param === false ) {
$this->error( "\nERROR: $option parameter needs a value after it\n" );
$this->maybeHelp( true );
}
$this->setParam( $options, $option, $param );
} else {
$this->setParam( $options, $option, 1 );
}
}
}
}
$this->mOptions = $options;
$this->loadSpecialVars();
$this->mInputLoaded = true;
}
/**
* Maybe show the help. If the help is shown, exit.
*
* @param bool $force Whether to force the help to show, default false
*/
private function maybeHelp( $force = false ) {
if ( !$force && !$this->hasOption( 'help' ) ) {
return;
}
$this->showHelp();
die( 1 );
}
/**
* Helper function used solely by loadParamsAndArgs
* to prevent code duplication
*
* This sets the param in the options array based on
* whether or not it can be specified multiple times.
*
* @since 1.27
* @param array &$options
* @param string $option
* @param mixed $value
*/
private function setParam( &$options, $option, $value ) {
$this->orderedOptions[] = [ $option, $value ];
if ( isset( $this->mParams[$option] ) ) {
$multi = $this->mParams[$option]['multiOccurrence'];
} else {
$multi = false;
}
$exists = array_key_exists( $option, $options );
if ( $multi && $exists ) {
$options[$option][] = $value;
} elseif ( $multi ) {
$options[$option] = [ $value ];
} elseif ( !$exists ) {
$options[$option] = $value;
} else {
$this->error( "\nERROR: $option parameter given twice\n" );
$this->maybeHelp( true );
}
}
private $mDbUser, $mDbPass;
/**
* Handle the special variables that are global to all scripts
* @stable for overriding
*/
private function loadSpecialVars() {
if ( $this->hasOption( 'dbuser' ) ) {
$this->mDbUser = $this->getOption( 'dbuser' );
}
if ( $this->hasOption( 'dbpass' ) ) {
$this->mDbPass = $this->getOption( 'dbpass' );
}
}
/**
* Output a message and terminate the current script.
*
* @stable for overriding
* @param string $msg Error message
* @param int $exitCode PHP exit status. Should be in range 1-254.
* @since 1.31
*/
private function fatalError( $msg, $exitCode = 1 ) {
$this->error( $msg );
exit( $exitCode );
}
/**
* Throw an error to the user. Doesn't respect --quiet, so don't use
* this for non-error output
* @stable for overriding
* @param string $err The error to display
*/
private function error( $err ) {
$this->outputChanneled( false );
print $err;
}
private $atLineStart = true;
private $lastChannel = null;
/**
* Clean up channeled output. Output a newline if necessary.
*/
private function cleanupChanneled() {
if ( !$this->atLineStart ) {
print "\n";
$this->atLineStart = true;
}
}
/**
* Message outputter with channeled message support. Messages on the
* same channel are concatenated, but any intervening messages in another
* channel start a new line.
* @param string $msg The message without trailing newline
* @param string|null $channel Channel identifier or null for no
* channel. Channel comparison uses ===.
*/
public function outputChanneled( $msg, $channel = null ) {
if ( $msg === false ) {
$this->cleanupChanneled();
return;
}
// End the current line if necessary
if ( !$this->atLineStart && $channel !== $this->lastChannel ) {
print "\n";
}
print $msg;
$this->atLineStart = false;
if ( $channel === null ) {
// For unchanneled messages, output trailing newline immediately
print "\n";
$this->atLineStart = true;
}
$this->lastChannel = $channel;
}
public function finalSetup() {
global $wgCommandLineMode, $wgServer, $wgShowExceptionDetails, $wgShowHostnames;
global $wgDBadminuser, $wgDBadminpassword, $wgDBDefaultGroup;
global $wgDBuser, $wgDBpassword, $wgDBservers, $wgLBFactoryConf;
# Turn off output buffering again, it might have been turned on in the settings files
if ( ob_get_level() ) {
ob_end_flush();
}
# Same with these
$wgCommandLineMode = true;
# Override $wgServer
if ( $this->hasOption( 'server' ) ) {
$wgServer = $this->getOption( 'server', $wgServer );
}
# If these were passed, use them
if ( $this->mDbUser ) {
$wgDBadminuser = $this->mDbUser;
}
if ( $this->mDbPass ) {
$wgDBadminpassword = $this->mDbPass;
}
if ( $this->hasOption( 'dbgroupdefault' ) ) {
$wgDBDefaultGroup = $this->getOption( 'dbgroupdefault', null );
$service = MediaWikiServices::getInstance()->peekService( 'DBLoadBalancerFactory' );
if ( $service ) {
$service->destroy();
}
}
if ( isset( $wgDBadminuser ) ) {
$wgDBuser = $wgDBadminuser;
$wgDBpassword = $wgDBadminpassword;
if ( $wgDBservers ) {
/**
* @var array $wgDBservers
*/
foreach ( $wgDBservers as $i => $server ) {
$wgDBservers[$i]['user'] = $wgDBuser;
$wgDBservers[$i]['password'] = $wgDBpassword;
}
}
if ( isset( $wgLBFactoryConf['serverTemplate'] ) ) {
$wgLBFactoryConf['serverTemplate']['user'] = $wgDBuser;
$wgLBFactoryConf['serverTemplate']['password'] = $wgDBpassword;
}
$service = MediaWikiServices::getInstance()->peekService( 'DBLoadBalancerFactory' );
if ( $service ) {
$service->destroy();
}
}
$wgShowExceptionDetails = true;
$wgShowHostnames = true;
Wikimedia\suppressWarnings();
set_time_limit( 0 );
Wikimedia\restoreWarnings();
$this->adjustMemoryLimit();
require_once __DIR__ . '/../common/TestsAutoLoader.php';
Refactor parser tests Merge the PHPUnit parser test runner with the old parserTests.inc, taking the good bits of both. Reviewed, pared down and documented the setup code. parserTests.php is now a frontend to a fully featured parser test system, with lots of developer options, whereas PHPUnit provides a simpler interface with increased isolation between test cases. Performance of both frontends is much improved, perhaps 2x faster for parserTests.php and 10x faster for PHPUnit. General: * Split out the pre-Setup.php global variable configuration from phpunit.php into a new class called TestSetup, also called it from parserTests.php. * Factored out the setup of TestsAutoLoader into a static method in Maintenance. * In Setup.php improved "caches" debug output. PHPUnit frontend: * Delete the entire contents of NewParserTest and replace it with a small wrapper around ParserTestRunner. It doesn't inherit from MediaWikiTestCase anymore since integrating the setup code was an unnecessary complication. * Rename MediaWikiParserTest to ParserTestTopLevelSuite and made it an instantiable TestSuite class instead of just a static method. Got rid of the eval(), just construct TestCase objects directly with a specified name, it works just as well. * Introduce ParserTestFileSuite for per-file setup. * Remove parser-related options from phpunit.php, since we don't support them anymore. Note that --filter now works just as well as --regex used to. * Add CoreParserTestSuite, equivalent to ExtensionsParserTestSuite, for clarity. * Make it possible to call MediaWikiTestCase::setupTestDB() more than once, as is implied by the documentation. parserTests.php frontend: * Made parserTests.php into a Maintenance subclass, moved CLI-specific code to it. * Renamed ParserTest to ParserTestRunner, this is now the generic backend. * Add --upload-dir option which sets up an FSFileBackend, similar to the old default behaviour Test file reading and interpretation: * Rename TestFileIterator to TestFileReader, and make it read and buffer an entire file, instead of iterating. * The previous code had an associative array representation of test specifications. Used this form more widely to pass around test data. * Remove the idea of !!hooks copying hooks from $wgParser, this is unnecessary now that all extensions use ParserFirstCallInit. Resurrect an old interpretation of the feature which was accidentally broken: if a named hook does not exist, skip all tests in the file. * Got rid of the "subtest" idea for tidy variants, instead use a human-readable description that appears in the output. * When all tests in a file are filtered or skipped, don't create the articles in them. This greatly speeds up execution time when --regex matches a small number of tests. It may possibly break extensions, but they would have been randomly broken anyway since there is no guarantee of test file execution order. * Remove integrated testing of OutputPage::addCategoryLinks() category link formatting, life is complicated enough already. It can go in OutputPageTest if that's a thing we really need. Result recording and display: * Make TestRecorder into a generic plugin interface for progress output etc., which needs to be abstracted for PHPUnit integration. * Introduce MultiTestRecorder for recorder chaining, instead of using a long inheritance chain. All test recorders now directly inherit from TestRecorder. * Move all console-related code to the new ParserTestPrinter. * Introduce PhpunitTestRecorder, which is the recorder for the PHPUnit frontend. Most events are ignored since they are never emitted in the PHPUnit frontend, which does not call runTests(). * Put more information into ParserTestResult and use it more often. Setup and teardown: * Introduce a new API for setup/teardown where setup functions return a ScopedCallback object which automatically performs the corresponding teardown when it goes out of scope. * Rename setUp() to staticSetup(), rewrite. There was a lot of cruft in here which was simply copied from Setup.php without review, and had nothing to do with parser tests. * Rename setupGlobals() to perTestSetup(), mostly rewrite. For performance, give staticSetup() precedence in cases where they were both setting up the same thing. * In support of merged setup code, allow Hooks::clear() to be called from parserTests.php. * Remove wgFileExtensions -- it is only used by UploadBase which we don't call. * Remove wgUseImageResize -- superseded by MockMediaHandlerFactory which I imported from NewParserTest. * Import MockFileBackend from NewParserTest. But instead of customising the configuration globals, I injected services. * Remove thumbnail deletion from upload teardown. This makes glob handling as in the old parserTests.php unnecessary. * Remove math file from upload teardown, math is actually an extension now! Also, the relevant parser tests were removed from the Math extension two years ago in favour of unit tests. * Make addArticle() private, and introduce addArticles() instead, which allows setup/teardown to be done once for each batch of articles instead of every time. * Remove $wgNamespaceAliases and $wgNamespaceProtection setup. These were copied in from Setup.php in 2010, and are redundant since we do actually run Setup.php. * Use NullLockManager, don't set up a temporary directory just for this alone. Fuzz tests: * Use the new TestSetup class. * Updated for ParserTestRunner interface change. * Remove some obsolete references to fuzz tests from the two frontends where they used to reside. Bug: T41473 Change-Id: Ia8e17008cb9d9b62ce5645e15a41a3b402f4026a
2016-09-08 01:25:22 +00:00
TestSetup::applyInitialConfig();
ExtensionRegistry::getInstance()->setLoadTestClassesAndNamespaces( true );
}
public function execute() {
// Deregister handler from MWExceptionHandler::installHandle so that PHPUnit's own handler
// stays in tact.
// Has to in execute() instead of finalSetup(), because finalSetup() runs before
// doMaintenance.php includes Setup.php, which calls MWExceptionHandler::installHandle().
restore_error_handler();
$this->forceFormatServerArgv();
if ( !class_exists( 'PHPUnit\\Framework\\TestCase' ) ) {
echo "PHPUnit not found. Please install it and other dev dependencies by
running `composer install` in MediaWiki root directory.\n";
exit( 1 );
}
// Start an output buffer to avoid headers being sent by constructors,
// data providers, etc. (T206476)
ob_start();
fwrite( STDERR, 'Using PHP ' . PHP_VERSION . "\n" );
foreach ( MediaWikiCliOptions::$additionalOptions as $option => $default ) {
MediaWikiCliOptions::$additionalOptions[$option] = $this->getOption( $option );
}
$command = new MediaWikiPHPUnitCommand();
$command->run( $_SERVER['argv'], true );
}
/**
* Force the format of elements in $_SERVER['argv']
* - Split args such as "wiki=enwiki" into two separate arg elements "wiki" and "enwiki"
*/
private function forceFormatServerArgv() {
$argv = [];
for ( $key = 0; $key < count( $_SERVER['argv'] ); $key++ ) {
$arg = $_SERVER['argv'][$key];
if ( $key === 0 ) {
$argv[0] = $arg;
continue;
}
if ( preg_match( '/^--(.*)$/', $arg, $match ) ) {
$opt = $match[1];
$parts = explode( '=', $opt, 2 );
$opt = $parts[0];
// Avoid confusing PHPUnit with MediaWiki-specific parameters
if ( isset( $this->mParams[$opt] ) ) {
if ( $this->mParams[$opt]['withArg'] && !isset( $parts[1] ) ) {
// skip the value after the option name as well
$key++;
}
continue;
}
}
$argv[] = $arg;
}
$_SERVER['argv'] = $argv;
}
/**
* Get the terminal size as a two-element array where the first element
* is the width (number of columns) and the second element is the height
* (number of rows).
*
* @return array
*/
private static function getTermSize() {
static $termSize = null;
if ( $termSize !== null ) {
return $termSize;
}
$default = [ 80, 50 ];
if ( wfIsWindows() || Shell::isDisabled() ) {
$termSize = $default;
return $termSize;
}
// It's possible to get the screen size with VT-100 terminal escapes,
// but reading the responses is not possible without setting raw mode
// (unless you want to require the user to press enter), and that
// requires an ioctl(), which we can't do. So we have to shell out to
// something that can do the relevant syscalls. There are a few
// options. Linux and Mac OS X both have "stty size" which does the
// job directly.
$result = Shell::command( 'stty', 'size' )->passStdin()->execute();
if ( $result->getExitCode() !== 0 ||
!preg_match( '/^(\d+) (\d+)$/', $result->getStdout(), $m )
) {
$termSize = $default;
return $termSize;
}
$termSize = [ intval( $m[2] ), intval( $m[1] ) ];
return $termSize;
}
/**
* @since 1.24
* @stable for overriding
* @return Config
*/
private function getConfig() {
if ( $this->config === null ) {
$this->config = MediaWikiServices::getInstance()->getMainConfig();
}
return $this->config;
}
/**
* Throw some output to the user. Scripts can call this with no fears,
* as we handle all --quiet stuff here
* @stable for overriding
* @param string $out The text to show to the user
* @param mixed|null $channel Unique identifier for the channel. See function outputChanneled.
*/
private function output( $out, $channel = null ) {
// This is sometimes called very early, before Setup.php is included.
if ( class_exists( MediaWikiServices::class ) ) {
// Try to periodically flush buffered metrics to avoid OOMs
$stats = MediaWikiServices::getInstance()->getStatsdDataFactory();
if ( $stats->getDataCount() > 1000 ) {
MediaWiki::emitBufferedStatsdData( $stats, $this->getConfig() );
}
}
if ( $channel === null ) {
$this->cleanupChanneled();
print $out;
} else {
$out = preg_replace( '/\n\z/', '', $out );
$this->outputChanneled( $out, $channel );
}
}
private function showHelp() {
$screenWidth = self::getTermSize()[0];
$tab = " ";
$descWidth = $screenWidth - ( 2 * strlen( $tab ) );
ksort( $this->mParams );
$output = "\nUsage: php tests/phpunit.php";
// ... append parameters ...
if ( $this->mParams ) {
$output .= " [--" . implode( "|--", array_keys( $this->mParams ) ) . "]";
}
$this->output( "$output\n\n" );
$this->formatHelpItems(
$this->mGenericParameters,
'Generic maintenance parameters',
$descWidth, $tab
);
$this->formatHelpItems(
$this->mDependentParameters,
'Script dependent parameters',
$descWidth, $tab
);
// Script-specific parameters not defined on construction by
// Maintenance::addDefaultParams()
$scriptSpecificParams = array_diff_key(
# all script parameters:
$this->mParams,
# remove the Maintenance default parameters:
$this->mGenericParameters,
$this->mDependentParameters
);
$this->formatHelpItems(
$scriptSpecificParams,
'Script specific parameters',
$descWidth, $tab
);
$this->output( "PHPUnit options are also accepted:\n\n" );
$command = new MediaWikiPHPUnitCommand();
$command->publicShowHelp();
}
private function formatHelpItems( array $items, $heading, $descWidth, $tab ) {
if ( $items === [] ) {
return;
}
$this->output( "$heading:\n" );
foreach ( $items as $name => $info ) {
if ( $info['shortName'] !== false ) {
$name .= ' (-' . $info['shortName'] . ')';
}
$this->output(
wordwrap(
"$tab--$name: " . $info['desc'],
$descWidth,
"\n$tab$tab"
) . "\n"
);
}
$this->output( "\n" );
}
/**
* Generic setup for most installs. Returns the location of LocalSettings
* @return string
*/
public function loadSettings() {
global $wgCommandLineMode, $IP;
if ( defined( "MW_CONFIG_FILE" ) ) {
$settingsFile = MW_CONFIG_FILE;
} else {
$settingsFile = "$IP/LocalSettings.php";
}
if ( isset( $this->mOptions['wiki'] ) ) {
$bits = explode( '-', $this->mOptions['wiki'], 2 );
define( 'MW_DB', $bits[0] );
define( 'MW_PREFIX', $bits[1] ?? '' );
}
if ( !is_readable( $settingsFile ) ) {
$this->fatalError( "A copy of your installation's LocalSettings.php\n" .
"must exist and be readable in the source directory.\n" .
"Use --conf to specify it." );
}
$wgCommandLineMode = true;
return $settingsFile;
}
/**
* @since 1.24
* @param Config $config
*/
public function setConfig( Config $config ) {
$this->config = $config;
}
/**
* Set triggers like when to try to run deferred updates
* @since 1.28
*/
public function setAgentAndTriggers() {
if ( function_exists( 'posix_getpwuid' ) ) {
$agent = posix_getpwuid( posix_geteuid() )['name'];
} else {
$agent = 'sysadmin';
}
$agent .= '@' . wfHostname();
$lbFactory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory();
// Add a comment for easy SHOW PROCESSLIST interpretation
$lbFactory->setAgentName(
mb_strlen( $agent ) > 15 ? mb_substr( $agent, 0, 15 ) . '...' : $agent
);
self::setLBFactoryTriggers( $lbFactory, $this->getConfig() );
}
/**
* @param LBFactory $lbFactory
* @param Config $config
* @since 1.28
*/
private static function setLBFactoryTriggers( LBFactory $lbFactory, Config $config ) {
$services = MediaWikiServices::getInstance();
$stats = $services->getStatsdDataFactory();
// Hook into period lag checks which often happen in long-running scripts
$lbFactory->setWaitForReplicationListener(
__METHOD__,
static function () use ( $stats, $config ) {
// Check config in case of JobRunner and unit tests
if ( $config->get( 'CommandLineMode' ) ) {
DeferredUpdates::tryOpportunisticExecute( 'run' );
}
// Try to periodically flush buffered metrics to avoid OOMs
MediaWiki::emitBufferedStatsdData( $stats, $config );
}
);
// Check for other windows to run them. A script may read or do a few writes
// to the master but mostly be writing to something else, like a file store.
$lbFactory->getMainLB()->setTransactionListener(
__METHOD__,
static function ( $trigger ) use ( $stats, $config ) {
// Check config in case of JobRunner and unit tests
if ( $config->get( 'CommandLineMode' ) && $trigger === IDatabase::TRIGGER_COMMIT ) {
DeferredUpdates::tryOpportunisticExecute( 'run' );
}
// Try to periodically flush buffered metrics to avoid OOMs
MediaWiki::emitBufferedStatsdData( $stats, $config );
}
);
}
/**
* Run some validation checks on the params, etc
* @stable for overriding
*/
public function validateParamsAndArgs() {
$die = false;
# Check to make sure we've got all the required options
foreach ( $this->mParams as $opt => $info ) {
if ( $info['require'] && !$this->hasOption( $opt ) ) {
$this->error( "Param $opt required!" );
$die = true;
}
}
$this->maybeHelp( $die );
}
}
// Define the MediaWiki entrypoint
define( 'MEDIAWIKI', true );
// This environment variable is ensured present by Maintenance.php.
$IP = getenv( 'MW_INSTALL_PATH' );
// Get an object to start us off
$maintenance = new PHPUnitMaintClass();
// Basic sanity checks and such
$maintenance->setup();
// Define how settings are loaded (e.g. LocalSettings.php)
if ( !defined( 'MW_CONFIG_FILE' ) ) {
define( 'MW_CONFIG_FILE', $maintenance->loadSettings() );
}
function wfMaintenanceSetup() {
// phpcs:ignore MediaWiki.NamingConventions.ValidGlobalName.allowedPrefix
global $maintenance;
$maintenance->finalSetup();
}
define( 'MW_SETUP_CALLBACK', 'wfMaintenanceSetup' );
require_once "$IP/includes/Setup.php";
$maintenance->setConfig( MediaWikiServices::getInstance()->getMainConfig() );
$maintenance->setAgentAndTriggers();
$maintenance->validateParamsAndArgs();
$maintenance->execute();