Skip to content

Commit 896bc89

Browse files
authored
Add PHPUnit integration testing (#53)
* Add debug to the failing test * Update bootstrap * Fix more errors * Get integration tests working * Add a fixture based test * Fix the fixtures * Fix static check, drupal_root check * Exclude Drupal from test discovery
1 parent 22b4d07 commit 896bc89

17 files changed

+206
-28
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@ composer.phar
22
composer.lock
33
/vendor/
44
/clover.xml
5+
/tests/fixtures/drupal/core

.travis.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,10 @@ script:
1919
- cd $TRAVIS_BUILD_DIR/../drupal
2020
- composer config repositories.1 path $TRAVIS_BUILD_DIR
2121
- composer require mglaman/phpstan-drupal *@dev
22-
- cp $TRAVIS_BUILD_DIR/tests/fixtures/drupal-phpstan.neon phpstan.neon
22+
- cp $TRAVIS_BUILD_DIR/tests/fixtures/config/drupal-phpstan.neon phpstan.neon
2323

2424
# Test that a known non-failing file doesn't error out.
25-
- ./vendor/bin/phpstan analyze web/core/install.php
25+
- ./vendor/bin/phpstan analyze web/core/install.php --debug
2626
# Verify test fixtures are ignored.
2727
- ./vendor/bin/phpstan analyze web/core/modules/migrate_drupal --no-progress | grep -q "tests/fixtures" && false || true
2828

composer.json

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,10 @@
1717
"require-dev": {
1818
"phpstan/phpstan-strict-rules": "^0.11",
1919
"squizlabs/php_codesniffer": "^3.3",
20-
"phpunit/phpunit": "^7.5"
20+
"phpunit/phpunit": "^7.5",
21+
"phpstan/phpstan-deprecation-rules": "^0.11.0",
22+
"composer/installers": "^1.6",
23+
"drupal/core": "^8.6"
2124
},
2225
"conflict": {
2326
"nette/di": ">=3.0"
@@ -36,8 +39,15 @@
3639
"classmap": ["tests/"]
3740
},
3841
"extra": {
39-
"branch-alias": {
40-
"dev-master": "0.12-dev"
41-
}
42+
"installer-paths": {
43+
"tests/fixtures/drupal/core": ["type:drupal-core"],
44+
"tests/fixtures/drupal/libraries/{$name}": ["type:drupal-library"],
45+
"tests/fixtures/drupal/modules/contrib/{$name}": ["type:drupal-module"],
46+
"tests/fixtures/drupal/profiles/contrib/{$name}": ["type:drupal-profile"],
47+
"tests/fixtures/drupal/themes/contrib/{$name}": ["type:drupal-theme"]
48+
},
49+
"branch-alias": {
50+
"dev-master": "0.12-dev"
51+
}
4252
}
4353
}

extension.neon

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ parameters:
22
excludes_analyse:
33
- *.api.php
44
- */tests/fixtures/*.php
5-
bootstrap: %rootDir%/../../mglaman/phpstan-drupal/phpstan-bootstrap.php
65
fileExtensions:
76
- module
87
- theme

phpunit.xml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,15 @@
22
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
33
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/7.5/phpunit.xsd"
44
bootstrap="vendor/autoload.php"
5-
forceCoversAnnotation="true"
5+
forceCoversAnnotation="false"
66
beStrictAboutCoversAnnotation="true"
77
beStrictAboutOutputDuringTests="true"
88
beStrictAboutTodoAnnotatedTests="true"
99
verbose="true">
1010
<testsuites>
1111
<testsuite name="default">
1212
<directory suffix="Test.php">tests</directory>
13+
<exclude>tests/fixtures/</exclude>
1314
</testsuite>
1415
</testsuites>
1516
<logging>

src/DependencyInjection/DrupalExtension.php

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
namespace PHPStan\DependencyInjection;
44

55
use DrupalFinder\DrupalFinder;
6+
use Nette;
67
use Nette\DI\CompilerExtension;
78
use Nette\DI\Config\Helpers;
89
use PHPStan\Drupal\ExtensionDiscovery;
@@ -18,13 +19,19 @@ class DrupalExtension extends CompilerExtension
1819
protected $defaultConfig = [
1920
'modules' => [],
2021
'themes' => [],
22+
'drupal_root' => '',
2123
];
2224

2325
/**
2426
* @var string
2527
*/
2628
private $drupalRoot;
2729

30+
/**
31+
* @var string
32+
*/
33+
private $drupalVendorDir;
34+
2835
/**
2936
* List of available modules.
3037
*
@@ -51,16 +58,28 @@ class DrupalExtension extends CompilerExtension
5158

5259
public function loadConfiguration(): void
5360
{
61+
/** @var array */
62+
$config = Helpers::merge($this->config, $this->defaultConfig);
63+
5464
$finder = new DrupalFinder();
55-
$finder->locateRoot(dirname($GLOBALS['autoloaderInWorkingDirectory'], 2));
65+
66+
if ($config['drupal_root'] !== '' && realpath($config['drupal_root']) !== false && is_dir($config['drupal_root'])) {
67+
$start_path = $config['drupal_root'];
68+
} else {
69+
$start_path = dirname($GLOBALS['autoloaderInWorkingDirectory'], 2);
70+
}
71+
72+
$finder->locateRoot($start_path);
5673
$this->drupalRoot = $finder->getDrupalRoot();
74+
$this->drupalVendorDir = $finder->getVendorDir();
75+
if (! (bool) $this->drupalRoot || ! (bool) $this->drupalVendorDir) {
76+
throw new \RuntimeException("Unable to detect Drupal at $start_path");
77+
}
5778

5879
$builder = $this->getContainerBuilder();
80+
$builder->parameters['bootstrap'] = dirname(__DIR__, 2) . '/phpstan-bootstrap.php';
5981
$builder->parameters['drupalRoot'] = $this->drupalRoot;
6082

61-
/** @var array */
62-
$config = Helpers::merge($this->config, $this->defaultConfig);
63-
6483
$this->modules = $config['modules'] ?? [];
6584
$this->themes = $config['themes'] ?? [];
6685

@@ -81,7 +100,7 @@ public function loadConfiguration(): void
81100
$extensionDiscovery = new ExtensionDiscovery($this->drupalRoot);
82101
$extensionDiscovery->setProfileDirectories([]);
83102
$profiles = $extensionDiscovery->scan('profile');
84-
$profile_directories = array_map(function (\PHPStan\Drupal\Extension $profile) : string {
103+
$profile_directories = array_map(static function (\PHPStan\Drupal\Extension $profile) : string {
85104
return $profile->getPath();
86105
}, $profiles);
87106
$extensionDiscovery->setProfileDirectories($profile_directories);
@@ -152,4 +171,11 @@ protected function camelize(string $id): string
152171
{
153172
return strtr(ucwords(strtr($id, ['_' => ' ', '.' => '_ ', '\\' => '_ '])), [' ' => '']);
154173
}
174+
175+
public function afterCompile(Nette\PhpGenerator\ClassType $class)
176+
{
177+
// @todo find a non-hack way to pass the Drupal roots to the bootstrap file.
178+
$class->getMethod('initialize')->addBody('$GLOBALS["drupalRoot"] = ?;', [$this->drupalRoot]);
179+
$class->getMethod('initialize')->addBody('$GLOBALS["drupalVendorDir"] = ?;', [$this->drupalVendorDir]);
180+
}
155181
}

src/Drupal/Bootstrap.php

Lines changed: 13 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -61,10 +61,12 @@ class Bootstrap
6161

6262
public function register(): void
6363
{
64-
$finder = new DrupalFinder();
65-
$finder->locateRoot(dirname($GLOBALS['autoloaderInWorkingDirectory'], 2));
66-
$this->autoloader = include $finder->getVendorDir() . '/autoload.php';
67-
$this->drupalRoot = $finder->getDrupalRoot();
64+
$drupalRoot = realpath($GLOBALS['drupalRoot']);
65+
if ($drupalRoot === false) {
66+
throw new \RuntimeException('Cannot determine the Drupal root from ' . $drupalRoot);
67+
}
68+
$this->drupalRoot = $drupalRoot;
69+
$this->autoloader = include realpath($GLOBALS['drupalVendorDir']) . '/autoload.php';
6870

6971
$this->extensionDiscovery = new ExtensionDiscovery($this->drupalRoot);
7072
$this->extensionDiscovery->setProfileDirectories([]);
@@ -80,13 +82,11 @@ public function register(): void
8082
return $a->getName() === 'blazy_test' ? 10 : 0;
8183
});
8284
$this->themeData = $this->extensionDiscovery->scan('theme');
83-
84-
$this->loadLegacyIncludes();
85-
8685
$this->addCoreNamespaces();
8786
$this->addModuleNamespaces();
8887
$this->addThemeNamespaces();
8988
$this->registerPs4Namespaces($this->namespaces);
89+
$this->loadLegacyIncludes();
9090

9191
foreach ($this->moduleData as $extension) {
9292
$this->loadExtension($extension);
@@ -95,8 +95,10 @@ public function register(): void
9595
$module_dir = $this->drupalRoot . '/' . $extension->getPath();
9696
// Add .install
9797
if (file_exists($module_dir . '/' . $module_name . '.install')) {
98-
// Causes errors on Drupal container not initialized
99-
// require $module_dir . '/' . $module_name . '.install';
98+
$ignored_install_files = ['entity_test', 'entity_test_update', 'update_test_schema'];
99+
if (!in_array($module_name, $ignored_install_files, true)) {
100+
require $module_dir . '/' . $module_name . '.install';
101+
}
100102
}
101103
// Add .post_update.php
102104
if (file_exists($module_dir . '/' . $module_name . '.post_update.php')) {
@@ -131,16 +133,12 @@ protected function loadLegacyIncludes(): void
131133

132134
protected function addCoreNamespaces(): void
133135
{
136+
require $this->drupalRoot . '/core/lib/Drupal.php';
134137
foreach (['Core', 'Component'] as $parent_directory) {
135138
$path = $this->drupalRoot . '/core/lib/Drupal/' . $parent_directory;
136139
$parent_namespace = 'Drupal\\' . $parent_directory;
137140
foreach (new \DirectoryIterator($path) as $component) {
138-
$pathname = $component->getPathname();
139-
if (!$component->isDot() && $component->isDir() && (
140-
is_dir($pathname . '/Plugin') ||
141-
is_dir($pathname . '/Entity') ||
142-
is_dir($pathname . '/Element')
143-
)) {
141+
if (!$component->isDot() && $component->isDir()) {
144142
$this->namespaces[$parent_namespace . '\\' . $component->getFilename()] = $path . '/' . $component->getFilename();
145143
}
146144
}

tests/DrupalIntegrationTest.php

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace PHPStan\Drupal;
4+
5+
use PHPStan\Analyser\Analyser;
6+
use PHPStan\DependencyInjection\ContainerFactory;
7+
use PHPStan\File\FileHelper;
8+
use PHPUnit\Framework\TestCase;
9+
10+
final class DrupalIntegrationTest extends TestCase {
11+
12+
public function testInstallPhp() {
13+
$errors = $this->runAnalyze(__DIR__ . '/fixtures/drupal/core/install.php');
14+
$this->assertCount(0, $errors);
15+
}
16+
17+
public function testDeprecatedUrlFunction() {
18+
$errors = $this->runAnalyze(__DIR__ . '/fixtures/drupal/modules/phpstan_fixtures/src/UsesDeprecatedUrlFunction.php');
19+
$this->assertCount(2, $errors);
20+
$error = array_shift($errors);
21+
$this->assertEquals('\Drupal calls should be avoided in classes, use dependency injection instead', $error->getMessage());
22+
$error = array_shift($errors);
23+
$this->assertEquals('Call to deprecated method url() of class Drupal.', $error->getMessage());
24+
}
25+
26+
public function testDeprecatedImplements() {
27+
$errors = $this->runAnalyze(__DIR__ . '/fixtures/drupal/core/lib/Drupal/Core/Entity/EntityManager.php');
28+
$this->assertCount(1, $errors);
29+
$error = reset($errors);
30+
$this->assertEquals('Class Drupal\Core\Entity\EntityManager implements deprecated interface Drupal\Core\Entity\EntityManagerInterface.', $error->getMessage());
31+
}
32+
33+
private function runAnalyze(string $path) {
34+
$rootDir = __DIR__ . '/fixtures/drupal';
35+
$containerFactory = new ContainerFactory($rootDir);
36+
$container = $containerFactory->create(
37+
sys_get_temp_dir() . '/' . time() . 'phpstan',
38+
[__DIR__ . '/fixtures/config/phpunit-drupal-phpstan.neon'],
39+
[]
40+
);
41+
$fileHelper = $container->getByType(FileHelper::class);
42+
43+
$bootstrapFile = $container->parameters['bootstrap'];
44+
$this->assertEquals(realpath(__DIR__ . '/../phpstan-bootstrap.php'), $bootstrapFile);
45+
// Mock the autoloader.
46+
$GLOBALS['drupalVendorDir'] = realpath(__DIR__) . '/../vendor';
47+
if ($bootstrapFile !== null) {
48+
$bootstrapFile = $fileHelper->normalizePath($bootstrapFile);
49+
if (!is_file($bootstrapFile)) {
50+
$this->fail('Bootstrap file not found');
51+
}
52+
try {
53+
(static function (string $file): void {
54+
require_once $file;
55+
})($bootstrapFile);
56+
} catch (\Throwable $e) {
57+
$this->fail('Could not load the bootstrap file');
58+
}
59+
}
60+
61+
$analyser = $container->getByType(Analyser::class);
62+
63+
$file = $fileHelper->normalizePath($path);
64+
$errors = $analyser->analyse(
65+
[$file],
66+
false,
67+
null,
68+
null,
69+
true
70+
);
71+
foreach ($errors as $error) {
72+
$this->assertSame($fileHelper->normalizePath($file), $error->getFile());
73+
}
74+
return $errors;
75+
}
76+
77+
}
File renamed without changes.
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
parameters:
2+
excludes_analyse:
3+
- *Test.php
4+
- *TestBase.php
5+
level: 7
6+
drupal:
7+
drupal_root: %currentWorkingDirectory%
8+
includes:
9+
- ../../../extension.neon
10+
- ../../../vendor/phpstan/phpstan-deprecation-rules/rules.neon

0 commit comments

Comments
 (0)