From 14f74d141ab5580688bfd46d2f74c026e43ed967 Mon Sep 17 00:00:00 2001 From: Pierre Schmitz Date: Wed, 1 Apr 2015 06:11:44 +0200 Subject: Update to MediaWiki 1.24.2 --- tests/phpunit/structure/AutoLoaderTest.php | 135 +++++++++++++++ tests/phpunit/structure/ResourcesTest.php | 269 +++++++++++++++++++++++++++++ tests/phpunit/structure/StructureTest.php | 67 +++++++ 3 files changed, 471 insertions(+) create mode 100644 tests/phpunit/structure/AutoLoaderTest.php create mode 100644 tests/phpunit/structure/ResourcesTest.php create mode 100644 tests/phpunit/structure/StructureTest.php (limited to 'tests/phpunit/structure') diff --git a/tests/phpunit/structure/AutoLoaderTest.php b/tests/phpunit/structure/AutoLoaderTest.php new file mode 100644 index 00000000..2bdc9c9a --- /dev/null +++ b/tests/phpunit/structure/AutoLoaderTest.php @@ -0,0 +1,135 @@ +testLocalClasses = array( + 'TestAutoloadedLocalClass' => __DIR__ . '/../data/autoloader/TestAutoloadedLocalClass.php', + 'TestAutoloadedCamlClass' => __DIR__ . '/../data/autoloader/TestAutoloadedCamlClass.php', + 'TestAutoloadedSerializedClass' => + __DIR__ . '/../data/autoloader/TestAutoloadedSerializedClass.php', + ); + $this->setMwGlobals( + 'wgAutoloadLocalClasses', + $this->testLocalClasses + $wgAutoloadLocalClasses + ); + AutoLoader::resetAutoloadLocalClassesLower(); + + $this->testExtensionClasses = array( + 'TestAutoloadedClass' => __DIR__ . '/../data/autoloader/TestAutoloadedClass.php', + ); + $this->setMwGlobals( 'wgAutoloadClasses', $this->testExtensionClasses + $wgAutoloadClasses ); + } + + /** + * Assert that there were no classes loaded that are not registered with the AutoLoader. + * + * For example foo.php having class Foo and class Bar but only registering Foo. + * This is important because we should not be relying on Foo being used before Bar. + */ + public function testAutoLoadConfig() { + $results = self::checkAutoLoadConf(); + + $this->assertEquals( + $results['expected'], + $results['actual'] + ); + } + + protected static function checkAutoLoadConf() { + global $wgAutoloadLocalClasses, $wgAutoloadClasses, $IP; + + // wgAutoloadLocalClasses has precedence, just like in includes/AutoLoader.php + $expected = $wgAutoloadLocalClasses + $wgAutoloadClasses; + $actual = array(); + + $files = array_unique( $expected ); + + foreach ( $files as $file ) { + // Only prefix $IP if it doesn't have it already. + // Generally local classes don't have it, and those from extensions and test suites do. + if ( substr( $file, 0, 1 ) != '/' && substr( $file, 1, 1 ) != ':' ) { + $filePath = "$IP/$file"; + } else { + $filePath = $file; + } + + $contents = file_get_contents( $filePath ); + + // We could use token_get_all() here, but this is faster + $matches = array(); + preg_match_all( '/ + ^ [\t ]* (?: + (?:final\s+)? (?:abstract\s+)? (?:class|interface) \s+ + (?P [a-zA-Z0-9_]+) + | + class_alias \s* \( \s* + ([\'"]) (?P [^\'"]+) \g{-2} \s* , \s* + ([\'"]) (?P [^\'"]+ ) \g{-2} \s* + \) \s* ; + ) + /imx', $contents, $matches, PREG_SET_ORDER ); + + $namespaceMatch = array(); + preg_match( '/ + ^ [\t ]* + namespace \s+ + ([a-zA-Z0-9_]+(\\\\[a-zA-Z0-9_]+)*) + \s* ; + /imx', $contents, $namespaceMatch ); + $fileNamespace = $namespaceMatch ? $namespaceMatch[1] . '\\' : ''; + + $classesInFile = array(); + $aliasesInFile = array(); + + foreach ( $matches as $match ) { + if ( !empty( $match['class'] ) ) { + $class = $fileNamespace . $match['class']; + $actual[$class] = $file; + $classesInFile[$class] = true; + } else { + $aliasesInFile[$match['alias']] = $match['original']; + } + } + + // Only accept aliases for classes in the same file, because for correct + // behavior, all aliases for a class must be set up when the class is loaded + // (see ). + foreach ( $aliasesInFile as $alias => $class ) { + if ( isset( $classesInFile[$class] ) ) { + $actual[$alias] = $file; + } else { + $actual[$alias] = "[original class not in $file]"; + } + } + } + + return array( + 'expected' => $expected, + 'actual' => $actual, + ); + } + + function testCoreClass() { + $this->assertTrue( class_exists( 'TestAutoloadedLocalClass' ) ); + } + + function testExtensionClass() { + $this->assertTrue( class_exists( 'TestAutoloadedClass' ) ); + } + + function testWrongCaseClass() { + $this->assertTrue( class_exists( 'testautoLoadedcamlCLASS' ) ); + } + + function testWrongCaseSerializedClass() { + $dummyCereal = 'O:29:"testautoloadedserializedclass":0:{}'; + $uncerealized = unserialize( $dummyCereal ); + $this->assertFalse( $uncerealized instanceof __PHP_Incomplete_Class, + "unserialize() can load classes case-insensitively." ); + } +} diff --git a/tests/phpunit/structure/ResourcesTest.php b/tests/phpunit/structure/ResourcesTest.php new file mode 100644 index 00000000..2396ea29 --- /dev/null +++ b/tests/phpunit/structure/ResourcesTest.php @@ -0,0 +1,269 @@ +assertFileExists( $filename, + "File '$resource' referenced by '$module' must exist." + ); + } + + /** + * @dataProvider provideMediaStylesheets + */ + public function testStyleMedia( $moduleName, $media, $filename, $css ) { + $cssText = CSSMin::minify( $css->cssText ); + + $this->assertTrue( + strpos( $cssText, '@media' ) === false, + 'Stylesheets should not both specify "media" and contain @media' + ); + } + + /** + * Verify that nothing explicitly depends on the 'jquery' and 'mediawiki' modules. + * They are always loaded, depending on them is unsupported and leads to unexpected behaviour. + */ + public function testIllegalDependencies() { + $data = self::getAllModules(); + $illegalDeps = array( 'jquery', 'mediawiki' ); + + /** @var ResourceLoaderModule $module */ + foreach ( $data['modules'] as $moduleName => $module ) { + foreach ( $illegalDeps as $illegalDep ) { + $this->assertNotContains( + $illegalDep, + $module->getDependencies(), + "Module '$moduleName' must not depend on '$illegalDep'" + ); + } + } + } + + /** + * Verify that all modules specified as dependencies of other modules actually exist. + */ + public function testMissingDependencies() { + $data = self::getAllModules(); + $validDeps = array_keys( $data['modules'] ); + + /** @var ResourceLoaderModule $module */ + foreach ( $data['modules'] as $moduleName => $module ) { + foreach ( $module->getDependencies() as $dep ) { + $this->assertContains( + $dep, + $validDeps, + "The module '$dep' required by '$moduleName' must exist" + ); + } + } + } + + /** + * Verify that all dependencies of all modules are always satisfiable with the 'targets' defined + * for the involved modules. + * + * Example: A depends on B. A has targets: mobile, desktop. B has targets: desktop. Therefore the + * dependency is sometimes unsatisfiable: it's impossible to load module A on mobile. + */ + public function testUnsatisfiableDependencies() { + $data = self::getAllModules(); + $validDeps = array_keys( $data['modules'] ); + + /** @var ResourceLoaderModule $module */ + foreach ( $data['modules'] as $moduleName => $module ) { + $moduleTargets = $module->getTargets(); + foreach ( $module->getDependencies() as $dep ) { + $targets = $data['modules'][$dep]->getTargets(); + foreach ( $moduleTargets as $moduleTarget ) { + $this->assertContains( + $moduleTarget, + $targets, + "The module '$moduleName' must not have target '$moduleTarget' " + . "because its dependency '$dep' does not have it" + ); + } + } + } + } + + /** + * Get all registered modules from ResouceLoader. + * @return array + */ + protected static function getAllModules() { + global $wgEnableJavaScriptTest; + + // Test existance of test suite files as well + // (can't use setUp or setMwGlobals because providers are static) + $org_wgEnableJavaScriptTest = $wgEnableJavaScriptTest; + $wgEnableJavaScriptTest = true; + + // Initialize ResourceLoader + $rl = new ResourceLoader(); + + $modules = array(); + + foreach ( $rl->getModuleNames() as $moduleName ) { + $modules[$moduleName] = $rl->getModule( $moduleName ); + } + + // Restore settings + $wgEnableJavaScriptTest = $org_wgEnableJavaScriptTest; + + return array( + 'modules' => $modules, + 'resourceloader' => $rl, + 'context' => new ResourceLoaderContext( $rl, new FauxRequest() ) + ); + } + + /** + * Get all stylesheet files from modules that are an instance of + * ResourceLoaderFileModule (or one of its subclasses). + */ + public static function provideMediaStylesheets() { + $data = self::getAllModules(); + $cases = array(); + + foreach ( $data['modules'] as $moduleName => $module ) { + if ( !$module instanceof ResourceLoaderFileModule ) { + continue; + } + + $reflectedModule = new ReflectionObject( $module ); + + $getStyleFiles = $reflectedModule->getMethod( 'getStyleFiles' ); + $getStyleFiles->setAccessible( true ); + + $readStyleFile = $reflectedModule->getMethod( 'readStyleFile' ); + $readStyleFile->setAccessible( true ); + + $styleFiles = $getStyleFiles->invoke( $module, $data['context'] ); + + $flip = $module->getFlip( $data['context'] ); + + foreach ( $styleFiles as $media => $files ) { + if ( $media && $media !== 'all' ) { + foreach ( $files as $file ) { + $cases[] = array( + $moduleName, + $media, + $file, + // XXX: Wrapped in an object to keep it out of PHPUnit output + (object)array( 'cssText' => $readStyleFile->invoke( $module, $file, $flip ) ), + ); + } + } + } + } + + return $cases; + } + + /** + * Get all resource files from modules that are an instance of + * ResourceLoaderFileModule (or one of its subclasses). + * + * Since the raw data is stored in protected properties, we have to + * overrride this through ReflectionObject methods. + */ + public static function provideResourceFiles() { + $data = self::getAllModules(); + $cases = array(); + + // See also ResourceLoaderFileModule::__construct + $filePathProps = array( + // Lists of file paths + 'lists' => array( + 'scripts', + 'debugScripts', + 'loaderScripts', + 'styles', + ), + + // Collated lists of file paths + 'nested-lists' => array( + 'languageScripts', + 'skinScripts', + 'skinStyles', + ), + ); + + foreach ( $data['modules'] as $moduleName => $module ) { + if ( !$module instanceof ResourceLoaderFileModule ) { + continue; + } + + $reflectedModule = new ReflectionObject( $module ); + + $files = array(); + + foreach ( $filePathProps['lists'] as $propName ) { + $property = $reflectedModule->getProperty( $propName ); + $property->setAccessible( true ); + $list = $property->getValue( $module ); + foreach ( $list as $key => $value ) { + // 'scripts' are numeral arrays. + // 'styles' can be numeral or associative. + // In case of associative the key is the file path + // and the value is the 'media' attribute. + if ( is_int( $key ) ) { + $files[] = $value; + } else { + $files[] = $key; + } + } + } + + foreach ( $filePathProps['nested-lists'] as $propName ) { + $property = $reflectedModule->getProperty( $propName ); + $property->setAccessible( true ); + $lists = $property->getValue( $module ); + foreach ( $lists as $list ) { + foreach ( $list as $key => $value ) { + // We need the same filter as for 'lists', + // due to 'skinStyles'. + if ( is_int( $key ) ) { + $files[] = $value; + } else { + $files[] = $key; + } + } + } + } + + // Get method for resolving the paths to full paths + $method = $reflectedModule->getMethod( 'getLocalPath' ); + $method->setAccessible( true ); + + // Populate cases + foreach ( $files as $file ) { + $cases[] = array( + $method->invoke( $module, $file ), + $moduleName, + ( $file instanceof ResourceLoaderFilePath ? $file->getPath() : $file ), + ); + } + } + + return $cases; + } +} diff --git a/tests/phpunit/structure/StructureTest.php b/tests/phpunit/structure/StructureTest.php new file mode 100644 index 00000000..14461be6 --- /dev/null +++ b/tests/phpunit/structure/StructureTest.php @@ -0,0 +1,67 @@ +markTestSkipped( 'This test does not work on Windows' ); + } + $rootPath = escapeshellarg( __DIR__ . '/..' ); + $testClassRegex = implode( '|', array( + 'ApiFormatTestBase', + 'ApiTestCase', + 'ApiQueryTestBase', + 'ApiQueryContinueTestBase', + 'MediaWikiLangTestCase', + 'MediaWikiMediaTestCase', + 'MediaWikiTestCase', + 'ResourceLoaderTestCase', + 'PHPUnit_Framework_TestCase', + 'DumpTestCase', + ) ); + $testClassRegex = "^class .* extends ($testClassRegex)"; + $finder = "find $rootPath -name '*.php' '!' -name '*Test.php'" . + " | xargs grep -El '$testClassRegex|function suite\('"; + + $results = null; + $exitCode = null; + exec( $finder, $results, $exitCode ); + + $this->assertEquals( + 0, + $exitCode, + 'Verify find/grep command succeeds.' + ); + + $results = array_filter( + $results, + array( $this, 'filterSuites' ) + ); + $strip = strlen( $rootPath ) - 1; + foreach ( $results as $k => $v ) { + $results[$k] = substr( $v, $strip ); + } + $this->assertEquals( + array(), + $results, + "Unit test file in $rootPath must end with Test." + ); + } + + /** + * Filter to remove testUnitTestFileNamesEndWithTest false positives. + * @param string $filename + * @return bool + */ + public function filterSuites( $filename ) { + return strpos( $filename, __DIR__ . '/../suites/' ) !== 0; + } +} -- cgit v1.2.3-54-g00ecf