Completed
Push — add/new-site-link-nav ( 198138...1aa4fa )
by
unknown
08:30
created

test_autoloader_resolves_cached_symlinks()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
nc 1
nop 0
dl 0
loc 13
rs 9.8333
c 0
b 0
f 0
1
<?php // phpcs:ignore WordPress.Files.FileName
2
/**
3
 * Integration test suite for the full autoloader.
4
 *
5
 * @package automattic/jetpack-autoloader
6
 */
7
8
use Automattic\Jetpack\Autoloader\AutoloadFileWriter;
9
use Jetpack\AutoloaderTestData\Plugin\Test;
0 ignored issues
show
Bug introduced by
This use statement conflicts with another class in this namespace, Test.

Let’s assume that you have a directory layout like this:

.
|-- OtherDir
|   |-- Bar.php
|   `-- Foo.php
`-- SomeDir
    `-- Foo.php

and let’s assume the following content of Bar.php:

// Bar.php
namespace OtherDir;

use SomeDir\Foo; // This now conflicts the class OtherDir\Foo

If both files OtherDir/Foo.php and SomeDir/Foo.php are loaded in the same runtime, you will see a PHP error such as the following:

PHP Fatal error:  Cannot use SomeDir\Foo as Foo because the name is already in use in OtherDir/Foo.php

However, as OtherDir/Foo.php does not necessarily have to be loaded and the error is only triggered if it is loaded before OtherDir/Bar.php, this problem might go unnoticed for a while. In order to prevent this error from surfacing, you must import the namespace with a different alias:

// Bar.php
namespace OtherDir;

use SomeDir\Foo as SomeDirFoo; // There is no conflict anymore.
Loading history...
10
use PHPUnit\Framework\TestCase;
11
12
/**
13
 * Test suite class for testing the autoloader in different deployment configurations.
14
 *
15
 * @runTestsInSeparateProcesses Ensure each test has a fresh process to work with, replicating real requests.
16
 * @preserveGlobalState disabled
17
 */
18
class Test_Autoloader_Scenarios extends TestCase {
19
20
	/**
21
	 * Indicates whether or not the autoloader has been reset by a load operation.
22
	 *
23
	 * @var bool
24
	 */
25
	private $autoloader_reset;
26
27
	/**
28
	 * Setup runs before each test.
29
	 *
30
	 * @before
31
	 */
32
	public function set_up() {
33
		// We need to make sure there's an autoloader containing the current files for testing.
34
		$this->generate_autoloader( 'plugin_current' );
35
		$this->generate_autoloader( 'plugin_newer' );
36
37
		// We also want a symlink version of the plugin for testing.
38
		// phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
39
		@symlink( WP_PLUGIN_DIR . '/plugin_current', WP_PLUGIN_DIR . '/plugin_symlink' );
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
40
	}
41
42
	/**
43
	 * Teardown runs after each test.
44
	 *
45
	 * @after
46
	 */
47
	public function tear_down() {
48
		cleanup_test_wordpress_data();
49
50
		// Make sure all of the tests have no cache file.
51
		// phpcs:disable WordPress.PHP.NoSilencedErrors.Discouraged
52
		@unlink( TEST_DATA_PATH . '/cache/jetpack-autoloader-' . Plugins_Handler::TRANSIENT_KEY . '.json' );
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
53
		@rmdir( TEST_DATA_PATH . '/cache' );
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
54
		@unlink( WP_PLUGIN_DIR . '/plugin_symlink' );
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
55
		// phpcs:enable WordPress.PHP.NoSilencedErrors.Discouraged
56
	}
57
58
	/**
59
	 * Tests that the autoloader works as expected.
60
	 */
61
	public function test_autoloader_init() {
62
		$this->activate_plugin( 'plugin_current' );
63
64
		$this->load_autoloader( 'plugin_current' );
65
66
		$this->assertAutoloaderVersion( '2.6.0.0' );
67
68
		$this->shutdown_autoloader( true );
69
		$this->assertAutoloaderCache( array( 'plugin_current' ) );
70
	}
71
72
	/**
73
	 * Tests that the autoloader does not initialize twice.
74
	 */
75
	public function test_autoloader_init_once() {
76
		$this->activate_plugin( 'plugin_current' );
77
78
		$this->load_autoloader( 'plugin_current' );
79
		$this->load_autoloader( 'plugin_current' );
80
81
		$this->assertFalse( $this->autoloader_reset );
82
		$this->assertAutoloaderVersion( '2.6.0.0' );
83
84
		$this->shutdown_autoloader( true );
85
		$this->assertAutoloaderCache( array( 'plugin_current' ) );
86
	}
87
88
	/**
89
	 * Tests that the autoloader loads the latest when loading an older one first.
90
	 */
91 View Code Duplication
	public function test_autoloader_loads_latest() {
92
		$this->activate_plugin( 'plugin_current' );
93
		$this->activate_plugin( 'plugin_newer' );
94
95
		$this->load_autoloader( 'plugin_current' );
96
		$this->load_autoloader( 'plugin_newer' );
97
98
		$this->assertFalse( $this->autoloader_reset );
99
		$this->assertAutoloaderVersion( '2.7.0.0' );
100
101
		$this->shutdown_autoloader( true );
102
		$this->assertAutoloaderCache( array( 'plugin_current', 'plugin_newer' ) );
103
	}
104
105
	/**
106
	 * Tests that the autoloader does not conflict with a v1 autoloader.
107
	 */
108
	public function test_autoloader_overrides_v1() {
109
		$this->activate_plugin( 'plugin_v1' );
110
		$this->activate_plugin( 'plugin_current' );
111
112
		$this->load_autoloader( 'plugin_v1' );
113
		$this->load_autoloader( 'plugin_current' );
114
115
		$this->assertTrue( $this->autoloader_reset );
116
		$this->assertAutoloaderVersion( '2.6.0.0' );
117
118
		$this->shutdown_autoloader( true );
119
		$this->assertAutoloaderCache( array( 'plugin_current' ) );
120
	}
121
122
	/**
123
	 * Tests that the autoloader is not reset when an older V2 initializes after the latest.
124
	 */
125 View Code Duplication
	public function test_autoloader_not_reset_by_older_v2() {
126
		$this->activate_plugin( 'plugin_current' );
127
		$this->activate_plugin( 'plugin_v2_2_0' );
128
129
		$this->load_autoloader( 'plugin_current' );
130
		$this->load_autoloader( 'plugin_v2_2_0' );
131
132
		$this->assertFalse( $this->autoloader_reset );
133
		$this->assertAutoloaderVersion( '2.6.0.0' );
134
135
		$this->shutdown_autoloader( true );
136
		$this->assertAutoloaderCache( array( 'plugin_current', 'plugin_v2_2_0' ) );
137
	}
138
139
	/**
140
	 * Tests that the autoloader resets when an unknown plugin is encountered, and that it does not
141
	 * reset a second time once the unknown plugin has been recorded.
142
	 */
143
	public function test_autoloader_resets_when_unknown_plugin_is_encountered() {
144
		$this->activate_plugin( 'plugin_current' );
145
146
		$this->load_autoloader( 'plugin_current' );
147
		$this->load_autoloader( 'plugin_newer' );
148
149
		$this->assertTrue( $this->autoloader_reset );
150
		$this->assertAutoloaderVersion( '2.7.0.0' );
151
152
		$this->shutdown_autoloader( true );
153
		$this->assertAutoloaderCache( array( 'plugin_current', 'plugin_newer' ) );
154
	}
155
156
	/**
157
	 * Tests that the autoloader uses the cache to avoid resetting when an known plugin is encountered.
158
	 */
159 View Code Duplication
	public function test_autoloader_uses_cache_to_avoid_resets() {
160
		$this->activate_plugin( 'plugin_current' );
161
162
		// Write the plugins to the cache so that the autoloader will see them.
163
		$this->cache_plugins( array( 'plugin_current', 'plugin_newer' ) );
164
165
		$this->load_autoloader( 'plugin_current' );
166
		$this->load_autoloader( 'plugin_newer' );
167
168
		$this->assertFalse( $this->autoloader_reset );
169
		$this->assertAutoloaderVersion( '2.7.0.0' );
170
171
		$this->shutdown_autoloader( true );
172
		$this->assertAutoloaderCache( array( 'plugin_current', 'plugin_newer' ) );
173
	}
174
175
	/**
176
	 * Tests that the autoloader updates the cache.
177
	 */
178
	public function test_autoloader_updates_cache() {
179
		$this->activate_plugin( 'plugin_current' );
180
181
		// Write an empty cache so we can make sure it was updated.
182
		$this->cache_plugins( array() );
183
184
		$this->load_autoloader( 'plugin_current' );
185
		$this->shutdown_autoloader( true );
186
187
		$this->assertAutoloaderVersion( '2.6.0.0' );
188
		$this->assertAutoloaderCache( array( 'plugin_current' ) );
189
	}
190
191
	/**
192
	 * Tests that the autoloader does not update the cache if it has not changed.
193
	 */
194
	public function test_autoloader_does_not_update_unchanged_cache() {
195
		$this->activate_plugin( 'plugin_current' );
196
197
		// Write a cache that we can use when loading the autoloader.
198
		$this->cache_plugins( array( 'plugin_current' ) );
199
200
		$this->load_autoloader( 'plugin_current' );
201
202
		// Erase the cache and then shut the autoloader down.
203
		// It shouldn't update the transient since the cached plugins changed.
204
		$this->cache_plugins( array() );
205
206
		$this->shutdown_autoloader( true );
207
208
		$this->assertAutoloaderVersion( '2.6.0.0' );
209
		$this->assertAutoloaderCache( array() );
210
	}
211
212
	/**
213
	 * Tests that the autoloader empties the cache if shutdown happens before plugins_loaded.
214
	 */
215
	public function test_autoloader_empties_cache_on_early_shutdown() {
216
		$this->activate_plugin( 'plugin_current' );
217
218
		// Write a cache that we can use when loading the autoloader.
219
		$this->cache_plugins( array( 'plugin_current' ) );
220
221
		$this->load_autoloader( 'plugin_current' );
222
223
		// Make sure to shutdown prematurely so that the cache will be erased instead of saved.
224
		$this->shutdown_autoloader( false );
225
226
		$this->assertAutoloaderVersion( '2.6.0.0' );
227
		$this->assertAutoloaderCache( array() );
228
	}
229
230
	/**
231
	 * Tests that the autoloader is able to resolve symbolic links to avoid duplicate plugin entries.
232
	 */
233
	public function test_autoloader_resolves_symlinks() {
234
		$this->activate_plugin( 'plugin_current', 'plugin_symlink' );
235
236
		$this->load_autoloader( 'plugin_symlink' );
237
238
		$this->assertAutoloaderVersion( '2.6.0.0' );
239
240
		$this->shutdown_autoloader( true );
241
		// Since there's no cache we should expect the resolved path.
242
		$this->assertAutoloaderCache( array( 'plugin_current' ) );
243
	}
244
245
	/**
246
	 * Tests that the autoloader can handle cases where the cached path is a symlink.
247
	 */
248
	public function test_autoloader_resolves_cached_symlinks() {
249
		$this->cache_plugins( array( 'plugin_symlink' ) );
250
251
		$this->activate_plugin( 'plugin_current', 'plugin_symlink' );
252
253
		$this->load_autoloader( 'plugin_symlink' );
254
255
		$this->assertAutoloaderVersion( '2.6.0.0' );
256
257
		$this->shutdown_autoloader( true );
258
		// The cache shouldn't be updated since internally real paths are always used.
259
		$this->assertAutoloaderCache( array( 'plugin_symlink' ) );
260
	}
261
262
	/**
263
	 * Generates a new autoloader from the current source files for the "plugin_current" plugin.
264
	 *
265
	 * @param string $plugin The plugin to generate the autoloader for.
266
	 */
267
	private function generate_autoloader( $plugin ) {
268
		// phpcs:disable WordPress.PHP.NoSilencedErrors.Discouraged
269
270
		$autoload_dir = TEST_DATA_PATH . '/plugins/' . $plugin . '/vendor/jetpack-autoloader';
271
272
		// Erase the existing autoloader files if they exist.
273
		@mkdir( $autoload_dir );
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
274
		$files = scandir( $autoload_dir );
275
		foreach ( $files as $file ) {
276
			@unlink( $autoload_dir . '/' . $file );
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
277
		}
278
		@unlink( $autoload_dir . '/../autoload_packages.php' );
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
279
280
		// Copy the autoloader files to the plugin directory.
281
		$suffix = md5( uniqid( '', true ) );
282
		AutoloadFileWriter::copyAutoloaderFiles( null, $autoload_dir, $suffix );
283
284
		// phpcs:enable WordPress.PHP.NoSilencedErrors.Discouraged
285
	}
286
287
	/**
288
	 * "Activate" a plugin so that the autoloader can detect it.
289
	 *
290
	 * @param string $plugin The plugin we want to activate.
291
	 * @param string $folder The folder that the plugin is in. If empty this will default to $plugin.
292
	 */
293
	private function activate_plugin( $plugin, $folder = '' ) {
294
		$active_plugins = get_option( 'active_plugins' );
295
		if ( ! $active_plugins ) {
296
			$active_plugins = array();
297
		}
298
299
		if ( empty( $folder ) ) {
300
			$folder = $plugin;
301
		}
302
303
		$active_plugins[] = $folder . '/' . $plugin . '.php';
304
305
		add_test_option( 'active_plugins', $active_plugins );
306
	}
307
308
	/**
309
	 * Loads the given autoloader and initializes it.
310
	 *
311
	 * @param string $plugin The plugin to load the autoloader from.
312
	 */
313
	private function load_autoloader( $plugin ) {
314
		// We're going to use changes in the hooks to detect if the autoloader has been reset.
315
		global $test_filters;
316
		$temp = $test_filters;
317
318
		require TEST_DATA_PATH . '/plugins/' . $plugin . '/vendor/autoload_packages.php';
319
320
		// The first time the autoloader is loaded we didn't reset.
321
		if ( ! isset( $this->autoloader_reset ) ) {
322
			$this->autoloader_reset = false;
323
		} else {
324
			$this->autoloader_reset = $temp !== $test_filters;
325
		}
326
	}
327
328
	/**
329
	 * Writes the plugins to the cache so that they can be read by the autoloader.
330
	 *
331
	 * @param string[] $plugins The plugins to cache.
332
	 */
333
	private function cache_plugins( $plugins ) {
334
		$paths = array();
335
		foreach ( $plugins as $plugin ) {
336
			$paths[] = '{{WP_PLUGIN_DIR}}/' . $plugin;
337
		}
338
339
		// The cached plugins are always sorted!
340
		sort( $paths );
341
342
		set_transient( Plugins_Handler::TRANSIENT_KEY, $paths );
343
	}
344
345
	/**
346
	 * Runs the autoloader's shutdown action.
347
	 *
348
	 * @param bool $plugins_loaded Indicates whether or not the plugins_loaded action should have fired.
349
	 */
350
	private function shutdown_autoloader( $plugins_loaded = true ) {
351
		if ( $plugins_loaded ) {
352
			do_action( 'plugins_loaded' );
353
		}
354
355
		do_action( 'shutdown' );
356
	}
357
358
	/**
359
	 * Asserts that the latest autoloader version is the one given.
360
	 *
361
	 * @param string $version The version to check.
362
	 */
363
	private function assertAutoloaderVersion( $version ) {
364
		$this->assertTrue( class_exists( Test::class ) );
365
		$this->assertEquals( $version, Test::VERSION, 'The class version is incorrect.' );
366
367
		global $jetpack_autoloader_latest_version;
368
		$this->assertEquals( $version, $jetpack_autoloader_latest_version, 'The autoloader version is incorrect.' );
369
	}
370
371
	/**
372
	 * Asserts that the autoloader cache contains the plugins given.
373
	 *
374
	 * @param array $plugins The plugins to check the cache for.
375
	 */
376
	private function assertAutoloaderCache( $plugins ) {
377
		$paths = array();
378
		foreach ( $plugins as $plugin ) {
379
			$paths[] = '{{WP_PLUGIN_DIR}}/' . $plugin;
380
		}
381
382
		// The cached plugins are always sorted!
383
		sort( $paths );
384
385
		$this->assertTrue(
386
			test_has_transient( Plugins_Handler::TRANSIENT_KEY, $paths ),
387
			'The autoloader cache does not match'
388
		);
389
	}
390
}
391