1
0
mirror of https://github.com/danog/psalm.git synced 2024-12-11 08:49:52 +01:00
psalm/tests/EndToEnd/PsalmEndToEndTest.php

299 lines
9.7 KiB
PHP
Raw Permalink Normal View History

<?php
namespace Psalm\Tests\EndToEnd;
2021-12-03 21:40:18 +01:00
use Exception;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Process\Process;
2019-07-05 22:24:00 +02:00
use function closedir;
use function copy;
2021-12-03 21:07:25 +01:00
use function file_exists;
use function file_get_contents;
use function file_put_contents;
use function getcwd;
2019-07-05 22:24:00 +02:00
use function is_dir;
use function is_string;
2019-07-05 22:24:00 +02:00
use function mkdir;
use function opendir;
use function preg_replace;
use function readdir;
use function rmdir;
2021-12-03 21:07:25 +01:00
use function str_replace;
2019-07-05 22:24:00 +02:00
use function sys_get_temp_dir;
use function tempnam;
use function unlink;
use const DIRECTORY_SEPARATOR;
use const PHP_BINARY;
/**
* Tests some of the most important use cases of the psalm and psalter commands, by launching a new
* process as if invoked by a real user.
*
* This is primarily intended to test the code in `psalm`, `src/psalm.php` and related files.
*/
class PsalmEndToEndTest extends TestCase
{
use PsalmRunnerTrait;
2022-12-16 19:58:47 +01:00
private static string $tmpDir;
public static function setUpBeforeClass(): void
{
self::$tmpDir = tempnam(sys_get_temp_dir(), 'PsalmEndToEndTest_');
unlink(self::$tmpDir);
mkdir(self::$tmpDir);
$getcwd = getcwd();
2019-07-05 22:24:00 +02:00
if (!is_string($getcwd)) {
2021-12-03 21:40:18 +01:00
throw new Exception('Couldn\'t get working directory');
}
copy(__DIR__ . '/../fixtures/DummyProjectWithErrors/composer.json', self::$tmpDir . '/composer.json');
$process = new Process(['composer', 'install', '--no-plugins'], self::$tmpDir, null, null, 120);
$process->mustRun();
}
public static function tearDownAfterClass(): void
{
self::recursiveRemoveDirectory(self::$tmpDir);
parent::tearDownAfterClass();
}
public function setUp(): void
{
mkdir(self::$tmpDir . '/src');
copy(
__DIR__ . '/../fixtures/DummyProjectWithErrors/src/FileWithErrors.php',
2022-12-18 17:15:15 +01:00
self::$tmpDir . '/src/FileWithErrors.php',
);
parent::setUp();
}
public function tearDown(): void
{
@unlink(self::$tmpDir . '/psalm.xml');
2021-12-03 21:07:25 +01:00
if (file_exists(self::$tmpDir . '/cache')) {
self::recursiveRemoveDirectory(self::$tmpDir . '/cache');
}
if (file_exists(self::$tmpDir . '/src')) {
self::recursiveRemoveDirectory(self::$tmpDir . '/src');
}
parent::tearDown();
}
public function testHelpReturnsMessage(): void
{
$this->assertStringContainsString('Usage:', $this->runPsalm(['--help'], self::$tmpDir)['STDOUT']);
}
public function testInit(): void
{
$this->assertStringStartsWith(
'Calculating best config level based on project files',
2022-12-18 17:15:15 +01:00
$this->runPsalmInit()['STDOUT'],
);
2019-06-26 02:55:30 +02:00
$this->assertFileExists(self::$tmpDir . '/psalm.xml');
}
public function testAlter(): void
{
$this->runPsalmInit();
$this->assertStringContainsString(
'No errors found!',
2022-12-18 17:15:15 +01:00
$this->runPsalm(['--alter', '--issues=all'], self::$tmpDir, false, true)['STDOUT'],
);
$this->assertSame(0, $this->runPsalm([], self::$tmpDir)['CODE']);
}
public function testPsalter(): void
{
$this->runPsalmInit();
(new Process([PHP_BINARY, $this->psalter, '--alter', '--issues=InvalidReturnType'], self::$tmpDir))->mustRun();
$this->assertSame(0, $this->runPsalm([], self::$tmpDir)['CODE']);
}
public function testPsalm(): void
{
2020-02-17 22:33:28 +01:00
$this->runPsalmInit(1);
$result = $this->runPsalm([], self::$tmpDir, true);
$this->assertStringContainsString(
'Target PHP version: 7.1 (inferred from composer.json)',
2022-12-18 17:15:15 +01:00
$result['STDERR'],
);
$this->assertStringContainsString('InvalidReturnType', $result['STDOUT']);
$this->assertStringContainsString('InvalidReturnStatement', $result['STDOUT']);
$this->assertStringContainsString('2 errors', $result['STDOUT']);
$this->assertSame(2, $result['CODE']);
}
public function testPsalmWithPHPVersionOverride(): void
{
$this->runPsalmInit(1);
$result = $this->runPsalm(['--php-version=8.0'], self::$tmpDir, true);
$this->assertStringContainsString(
'Target PHP version: 8.0 (set by CLI argument)',
2022-12-18 17:15:15 +01:00
$result['STDERR'],
);
}
public function testPsalmWithPHPVersionFromConfig(): void
{
$this->runPsalmInit(1, '7.4');
$result = $this->runPsalm([], self::$tmpDir, true);
$this->assertStringContainsString(
'Target PHP version: 7.4 (set by config file)',
2022-12-18 17:15:15 +01:00
$result['STDERR'],
);
}
public function testPsalmDiff(): void
{
2020-06-06 17:23:05 +02:00
copy(__DIR__ . '/../fixtures/DummyProjectWithErrors/diff_composer.lock', self::$tmpDir . '/composer.lock');
2020-06-06 16:34:49 +02:00
$this->runPsalmInit(1);
$result = $this->runPsalm(['--diff', '-m'], self::$tmpDir, true);
$this->assertStringContainsString('InvalidReturnType', $result['STDOUT']);
$this->assertStringContainsString('InvalidReturnStatement', $result['STDOUT']);
$this->assertStringContainsString('2 errors', $result['STDOUT']);
$this->assertStringContainsString('E', $result['STDERR']);
$this->assertSame(2, $result['CODE']);
$result = $this->runPsalm(['--diff', '-m'], self::$tmpDir, true);
$this->assertStringContainsString('InvalidReturnType', $result['STDOUT']);
$this->assertStringContainsString('InvalidReturnStatement', $result['STDOUT']);
$this->assertStringContainsString('2 errors', $result['STDOUT']);
$this->assertStringNotContainsString('E', $result['STDERR']);
$this->assertSame(2, $result['CODE']);
2020-06-06 16:34:49 +02:00
@unlink(self::$tmpDir . '/composer.lock');
}
public function testTainting(): void
{
$this->runPsalmInit(1);
$result = $this->runPsalm(['--taint-analysis'], self::$tmpDir, true);
$this->assertStringContainsString('TaintedHtml', $result['STDOUT']);
2021-06-11 00:07:41 +02:00
$this->assertStringContainsString('TaintedTextWithQuotes', $result['STDOUT']);
$this->assertStringContainsString('2 errors', $result['STDOUT']);
$this->assertSame(2, $result['CODE']);
}
public function testTaintingWithoutInit(): void
{
2020-11-21 01:07:48 +01:00
$result = $this->runPsalm(['--taint-analysis'], self::$tmpDir, true, false);
$this->assertStringContainsString('TaintedHtml', $result['STDOUT']);
2021-06-11 00:07:41 +02:00
$this->assertStringContainsString('TaintedTextWithQuotes', $result['STDOUT']);
$this->assertStringContainsString('2 errors', $result['STDOUT']);
$this->assertSame(2, $result['CODE']);
}
public function testTaintGraphDumping(): void
{
$this->runPsalmInit(1);
$result = $this->runPsalm(
[
'--taint-analysis',
'--dump-taint-graph='.self::$tmpDir.'/taints.dot',
],
self::$tmpDir,
2022-12-18 17:15:15 +01:00
true,
);
$this->assertSame(2, $result['CODE']);
$this->assertFileEquals(
__DIR__ . '/../fixtures/expected_taint_graph.dot',
2022-12-18 17:15:15 +01:00
self::$tmpDir.'/taints.dot',
);
}
public function testLegacyConfigWithoutresolveFromConfigFile(): void
{
2020-02-17 22:33:28 +01:00
$this->runPsalmInit(1);
$psalmXmlContent = file_get_contents(self::$tmpDir . '/psalm.xml');
$count = 0;
2020-10-05 16:14:39 +02:00
$psalmXmlContent = preg_replace('/resolveFromConfigFile="true"/', 'resolveFromConfigFile="false"', $psalmXmlContent, -1, $count);
$this->assertEquals(1, $count);
file_put_contents(self::$tmpDir . '/src/psalm.xml', $psalmXmlContent);
$process = new Process([PHP_BINARY, $this->psalm, '--config=src/psalm.xml'], self::$tmpDir);
$process->run();
$this->assertSame(2, $process->getExitCode());
$this->assertStringContainsString('InvalidReturnType', $process->getOutput());
}
public function testPsalmWithNoProgressDoesNotProduceOutputOnStderr(): void
{
$this->runPsalmInit();
$psalmXml = file_get_contents(self::$tmpDir . '/psalm.xml');
2023-06-13 16:59:25 +02:00
$psalmXml = preg_replace('/findUnusedCode="(true|false)"/', '', $psalmXml, 1);
file_put_contents(self::$tmpDir . '/psalm.xml', $psalmXml);
$result = $this->runPsalm(['--no-progress'], self::$tmpDir);
$this->assertSame('', $result['STDERR']);
}
/**
* @return array{STDOUT: string, STDERR: string, CODE: int|null}
*/
private function runPsalmInit(?int $level = null, ?string $php_version = null): array
{
2020-02-17 22:33:28 +01:00
$args = ['--init'];
if ($level) {
$args[] = 'src';
$args[] = (string) $level;
}
$ret = $this->runPsalm($args, self::$tmpDir, false, false);
$psalm_config_contents = file_get_contents(self::$tmpDir . '/psalm.xml');
2021-12-03 21:07:25 +01:00
$psalm_config_contents = str_replace(
'errorLevel="1"',
'errorLevel="1" '
. 'cacheDirectory="' . self::$tmpDir . '/cache" '
. ($php_version ? ('phpVersion="' . $php_version . '"') : ''),
2022-12-18 17:15:15 +01:00
$psalm_config_contents,
);
file_put_contents(self::$tmpDir . '/psalm.xml', $psalm_config_contents);
return $ret;
}
/** from comment by itay at itgoldman dot com at
* https://www.php.net/manual/en/function.rmdir.php#117354
*/
private static function recursiveRemoveDirectory(string $src): void
{
$dir = opendir($src);
2019-07-05 22:24:00 +02:00
while (false !== ($file = readdir($dir))) {
if (($file !== '.') && ($file !== '..')) {
$full = $src . DIRECTORY_SEPARATOR . $file;
if (is_dir($full)) {
self::recursiveRemoveDirectory($full);
} else {
unlink($full);
}
}
}
closedir($dir);
rmdir($src);
}
}