From 0e6f6b592df4e513648fb1114f80e6b10387b7cf Mon Sep 17 00:00:00 2001 From: Kunal Mehta Date: Thu, 24 May 2018 18:55:47 -0700 Subject: [PATCH] Add structure test to ensure PSR-4 autoloader covers everything This ensures that there aren't any classes inside a PSR-4 autoloaded directory that aren't being autoloaded properly. Change-Id: I200a8535c2f47a6bf3287a7fe1182151493372f4 --- tests/phpunit/structure/AutoLoaderTest.php | 139 ++++++++++++++------- 1 file changed, 93 insertions(+), 46 deletions(-) diff --git a/tests/phpunit/structure/AutoLoaderTest.php b/tests/phpunit/structure/AutoLoaderTest.php index 217232e3b5..46255f31ab 100644 --- a/tests/phpunit/structure/AutoLoaderTest.php +++ b/tests/phpunit/structure/AutoLoaderTest.php @@ -35,6 +35,96 @@ class AutoLoaderTest extends MediaWikiTestCase { ); } + public function providePSR4Completeness() { + foreach ( AutoLoader::$psr4Namespaces as $prefix => $dir ) { + foreach ( $this->recurseFiles( $dir ) as $file ) { + yield [ $prefix, $file ]; + } + } + } + + private function recurseFiles( $dir ) { + return ( new File_Iterator_Facade() )->getFilesAsArray( $dir, [ '.php' ] ); + } + + /** + * @coversNothing + * @dataProvider providePSR4Completeness + */ + public function testPSR4Completeness( $prefix, $file ) { + global $wgAutoloadLocalClasses, $wgAutoloadClasses; + $contents = file_get_contents( $file ); + list( $classesInFile, $aliasesInFile ) = self::parseFile( $contents ); + $classes = array_keys( $classesInFile ); + $this->assertCount( 1, $classes, + "Only one class per file in PSR-4 autoloaded classes ($file)" ); + + $this->assertStringStartsWith( $prefix, $classes[0] ); + $this->assertTrue( + class_exists( $classes[0] ) || interface_exists( $classes[0] ) || trait_exists( $classes[0] ), + "Class {$classes[0]} not autoloaded properly" + ); + + $otherClasses = $wgAutoloadLocalClasses + $wgAutoloadClasses; + foreach ( $aliasesInFile as $alias => $class ) { + $this->assertArrayHasKey( $alias, $otherClasses, + 'Alias must be in the classmap autoloader' + ); + } + } + + private static function parseFile( $contents ) { + // We could use token_get_all() here, but this is faster + // Note: Keep in sync with ClassCollector + $matches = []; + preg_match_all( '/ + ^ [\t ]* (?: + (?:final\s+)? (?:abstract\s+)? (?:class|interface|trait) \s+ + (?P [a-zA-Z0-9_]+) + | + class_alias \s* \( \s* + ([\'"]) (?P [^\'"]+) \g{-2} \s* , \s* + ([\'"]) (?P [^\'"]+ ) \g{-2} \s* + \) \s* ; + | + class_alias \s* \( \s* + (?P [a-zA-Z0-9_]+)::class \s* , \s* + ([\'"]) (?P [^\'"]+ ) \g{-2} \s* + \) \s* ; + ) + /imx', $contents, $matches, PREG_SET_ORDER ); + + $namespaceMatch = []; + preg_match( '/ + ^ [\t ]* + namespace \s+ + ([a-zA-Z0-9_]+(\\\\[a-zA-Z0-9_]+)*) + \s* ; + /imx', $contents, $namespaceMatch ); + $fileNamespace = $namespaceMatch ? $namespaceMatch[1] . '\\' : ''; + + $classesInFile = []; + $aliasesInFile = []; + + foreach ( $matches as $match ) { + if ( !empty( $match['class'] ) ) { + // 'class Foo {}' + $class = $fileNamespace . $match['class']; + $classesInFile[$class] = true; + } else { + if ( !empty( $match['original'] ) ) { + // 'class_alias( "Foo", "Bar" );' + $aliasesInFile[$match['alias']] = $match['original']; + } else { + // 'class_alias( Foo::class, "Bar" );' + $aliasesInFile[$match['aliasString']] = $fileNamespace . $match['originalStatic']; + } + } + } + + return [ $classesInFile, $aliasesInFile ]; + } + protected static function checkAutoLoadConf() { global $wgAutoloadLocalClasses, $wgAutoloadClasses, $IP; @@ -67,53 +157,10 @@ class AutoLoaderTest extends MediaWikiTestCase { continue; } - // We could use token_get_all() here, but this is faster - // Note: Keep in sync with ClassCollector - $matches = []; - preg_match_all( '/ - ^ [\t ]* (?: - (?:final\s+)? (?:abstract\s+)? (?:class|interface|trait) \s+ - (?P [a-zA-Z0-9_]+) - | - class_alias \s* \( \s* - ([\'"]) (?P [^\'"]+) \g{-2} \s* , \s* - ([\'"]) (?P [^\'"]+ ) \g{-2} \s* - \) \s* ; - | - class_alias \s* \( \s* - (?P [a-zA-Z0-9_]+)::class \s* , \s* - ([\'"]) (?P [^\'"]+ ) \g{-2} \s* - \) \s* ; - ) - /imx', $contents, $matches, PREG_SET_ORDER ); - - $namespaceMatch = []; - preg_match( '/ - ^ [\t ]* - namespace \s+ - ([a-zA-Z0-9_]+(\\\\[a-zA-Z0-9_]+)*) - \s* ; - /imx', $contents, $namespaceMatch ); - $fileNamespace = $namespaceMatch ? $namespaceMatch[1] . '\\' : ''; - - $classesInFile = []; - $aliasesInFile = []; + list( $classesInFile, $aliasesInFile ) = self::parseFile( $contents ); - foreach ( $matches as $match ) { - if ( !empty( $match['class'] ) ) { - // 'class Foo {}' - $class = $fileNamespace . $match['class']; - $actual[$class] = $file; - $classesInFile[$class] = true; - } else { - if ( !empty( $match['original'] ) ) { - // 'class_alias( "Foo", "Bar" );' - $aliasesInFile[$match['alias']] = $match['original']; - } else { - // 'class_alias( Foo::class, "Bar" );' - $aliasesInFile[$match['aliasString']] = $fileNamespace . $match['originalStatic']; - } - } + foreach ( $classesInFile as $className => $ignore ) { + $actual[$className] = $file; } // Only accept aliases for classes in the same file, because for correct -- 2.20.1