summaryrefslogtreecommitdiff
path: root/tests/phpunit/structure/AutoLoaderTest.php
blob: 8674329af20d0f2cfcc25adef5618aab5c2a3373 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
<?php

class AutoLoaderTest extends MediaWikiTestCase {
	protected function setUp() {
		parent::setUp();

		// Fancy dance to trigger a rebuild of AutoLoader::$autoloadLocalClassesLower
		$this->mergeMwGlobalArrayValue( 'wgAutoloadLocalClasses', array(
			'TestAutoloadedLocalClass' =>
				__DIR__ . '/../data/autoloader/TestAutoloadedLocalClass.php',
			'TestAutoloadedCamlClass' =>
				__DIR__ . '/../data/autoloader/TestAutoloadedCamlClass.php',
			'TestAutoloadedSerializedClass' =>
				__DIR__ . '/../data/autoloader/TestAutoloadedSerializedClass.php',
		) );
		AutoLoader::resetAutoloadLocalClassesLower();

		$this->mergeMwGlobalArrayValue( 'wgAutoloadClasses', array(
			'TestAutoloadedClass' => __DIR__ . '/../data/autoloader/TestAutoloadedClass.php',
		) );
	}

	/**
	 * 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 $class => $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;
			}

			if ( !file_exists( $filePath ) ) {
				$actual[$class] = "[file '$filePath' does not exist]";
				continue;
			}

			MediaWiki\suppressWarnings();
			$contents = file_get_contents( $filePath );
			MediaWiki\restoreWarnings();

			if ( $contents === false ) {
				$actual[$class] = "[couldn't read file '$filePath']";
				continue;
			}

			// 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<class> [a-zA-Z0-9_]+)
				|
					class_alias \s* \( \s*
						([\'"]) (?P<original> [^\'"]+) \g{-2} \s* , \s*
						([\'"]) (?P<alias> [^\'"]+ ) \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 <https://bugs.php.net/bug.php?id=61422>).
			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->setMwGlobals( 'wgAutoloadAttemptLowercase', true );

		$this->assertTrue( class_exists( 'testautoLoadedcamlCLASS' ) );
	}

	function testWrongCaseSerializedClass() {
		$this->setMwGlobals( 'wgAutoloadAttemptLowercase', true );

		$dummyCereal = 'O:29:"testautoloadedserializedclass":0:{}';
		$uncerealized = unserialize( $dummyCereal );
		$this->assertFalse( $uncerealized instanceof __PHP_Incomplete_Class,
			"unserialize() can load classes case-insensitively." );
	}
}