From 52ccc2ad5d3c6d8c7031636df5397d0aa6732970 Mon Sep 17 00:00:00 2001 From: Gintautas Miselis Date: Thu, 16 Jun 2022 08:05:29 +0300 Subject: [PATCH] Make dry-run work with module methods having return types --- src/Codeception/Command/DryRun.php | 120 +++++++++++++++++- src/Codeception/Lib/Generator/Actions.php | 6 +- src/Codeception/Step.php | 1 + tests/cli/DryRunCest.php | 16 +++ tests/data/typed_helper/codeception.yml | 9 ++ .../typed_helper/tests/_output/.gitignore | 2 + .../tests/_support/Helper/Unit.php | 43 +++++++ .../tests/_support/UnitTester.php | 6 + .../tests/_support/_generated/.gitignore | 2 + tests/data/typed_helper/tests/unit.suite.yml | 4 + .../tests/unit/UseTypedHelperCest.php | 16 +++ 11 files changed, 220 insertions(+), 5 deletions(-) create mode 100644 tests/data/typed_helper/codeception.yml create mode 100644 tests/data/typed_helper/tests/_output/.gitignore create mode 100644 tests/data/typed_helper/tests/_support/Helper/Unit.php create mode 100644 tests/data/typed_helper/tests/_support/UnitTester.php create mode 100644 tests/data/typed_helper/tests/_support/_generated/.gitignore create mode 100644 tests/data/typed_helper/tests/unit.suite.yml create mode 100644 tests/data/typed_helper/tests/unit/UseTypedHelperCest.php diff --git a/src/Codeception/Command/DryRun.php b/src/Codeception/Command/DryRun.php index ec7e977021..7e9987fa07 100644 --- a/src/Codeception/Command/DryRun.php +++ b/src/Codeception/Command/DryRun.php @@ -6,16 +6,25 @@ use Codeception\Event\SuiteEvent; use Codeception\Event\TestEvent; use Codeception\Events; +use Codeception\Lib\Generator\Actions; +use Codeception\Lib\ModuleContainer; +use Codeception\Module; +use Codeception\Step; +use Codeception\Stub; use Codeception\Subscriber\Bootstrap as BootstrapLoader; use Codeception\Subscriber\Console as ConsolePrinter; use Codeception\SuiteManager; use Codeception\Test\Interfaces\ScenarioDriven; use Codeception\Test\Test; use Codeception\Util\Maybe; +use PHPUnit\Framework\MockObject\MockObject; +use ReflectionIntersectionType; +use ReflectionMethod; +use ReflectionType; +use ReflectionUnionType; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; -use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\EventDispatcher\EventDispatcher; @@ -77,7 +86,7 @@ public function execute(InputInterface $input, OutputInterface $output) $suiteManager = new SuiteManager($dispatcher, $suite, $settings); $moduleContainer = $suiteManager->getModuleContainer(); foreach (Configuration::modules($settings) as $module) { - $moduleContainer->mock($module, new Maybe()); + $this->mockModule($module, $moduleContainer); } $suiteManager->loadTests($test); $tests = $suiteManager->getSuite()->tests(); @@ -131,4 +140,111 @@ protected function dryRunTest(OutputInterface $output, EventDispatcher $dispatch } $output->writeln(''); } + + /** + * @return Module&MockObject + */ + private function mockModule($moduleName, ModuleContainer $moduleContainer) + { + $module = $moduleContainer->getModule($moduleName); + $class = new \ReflectionClass($module); + $methodResults = []; + foreach ($class->getMethods(ReflectionMethod::IS_PUBLIC) as $method) { + if ($method->isConstructor()) { + continue; + } + $methodResults[$method->getName()] = $this->getDefaultResultForMethod($class, $method); + } + + $moduleContainer->mock($moduleName, Stub::makeEmpty($module, $methodResults)); + } + + private function getDefaultResultForMethod(\ReflectionClass $class, ReflectionMethod $method) + { + if (PHP_VERSION_ID < 70000) { + return new Maybe(); + } + + $returnType = $method->getReturnType(); + + if ($returnType === null || $returnType->allowsNull()) { + return null; + } + + if ($returnType instanceof ReflectionUnionType) { + return $this->getDefaultValueOfUnionType($returnType); + } + if ($returnType instanceof ReflectionIntersectionType) { + return $this->returnDefaultValueForIntersectionType($returnType); + } + + if (PHP_VERSION_ID >= 70100 && $returnType->isBuiltin()) { + return $this->getDefaultValueForBuiltinType($returnType); + } + + $typeName = Actions::stringifyNamedType($returnType, $class); + return Stub::makeEmpty($typeName); + } + + + + private function getDefaultValueForBuiltinType(ReflectionType $returnType) + { + switch ($returnType->getName()) { + case 'mixed': + case 'void': + return null; + case 'string': + return ''; + case 'int': + return 0; + case 'float': + return 0.0; + case 'bool': + return false; + case 'array': + return []; + case 'resource': + return fopen('data://text/plain;base64,', 'r'); + default: + throw new \Exception('Unsupported return type ' . $returnType->getName()); + } + } + + private function getDefaultValueOfUnionType($returnType) + { + $unionTypes = $returnType->getTypes(); + foreach ($unionTypes as $type) { + if ($type->isBuiltin()) { + return $this->getDefaultValueForBuiltinType($type); + } + } + + return Stub::makeEmpty($unionTypes[0]); + } + + private function returnDefaultValueForIntersectionType(ReflectionIntersectionType $returnType) + { + $extends = null; + $implements = []; + foreach ($returnType->getTypes() as $type) { + if (class_exists($type)) { + $extends = $type; + } else { + $implements [] = $type; + } + } + $className = uniqid('anonymous_class_'); + $code = "abstract class $className"; + if ($extends !== null) { + $code .= " extends \\$extends"; + } + if (count($implements) > 0) { + $code .= ' implements ' . implode(', ', $implements); + } + $code .= ' {}'; + eval($code); + + return Stub::makeEmpty($className); + } } diff --git a/src/Codeception/Lib/Generator/Actions.php b/src/Codeception/Lib/Generator/Actions.php index eaa98db21c..981fefed1b 100644 --- a/src/Codeception/Lib/Generator/Actions.php +++ b/src/Codeception/Lib/Generator/Actions.php @@ -253,7 +253,7 @@ private function stringifyType(\ReflectionType $type, \ReflectionClass $moduleCl return sprintf( '%s%s', (PHP_VERSION_ID >= 70100 && $type->allowsNull() && $returnTypeString !== 'mixed') ? '?' : '', - $this->stringifyNamedType($type, $moduleClass) + self::stringifyNamedType($type, $moduleClass) ); } @@ -267,7 +267,7 @@ private function stringifyNamedTypes(array $types, \ReflectionClass $moduleClass { $strings = []; foreach ($types as $type) { - $strings []= $this->stringifyNamedType($type, $moduleClass); + $strings []= self::stringifyNamedType($type, $moduleClass); } return implode($separator, $strings); @@ -278,7 +278,7 @@ private function stringifyNamedTypes(array $types, \ReflectionClass $moduleClass * @return string * @todo param is only \ReflectionNamedType in Codeception 5 */ - private function stringifyNamedType($type, \ReflectionClass $moduleClass) + public static function stringifyNamedType($type, \ReflectionClass $moduleClass) { if (PHP_VERSION_ID < 70100) { $typeName = (string)$type; diff --git a/src/Codeception/Step.php b/src/Codeception/Step.php index 035d1ed7d1..9b4f08d5bb 100644 --- a/src/Codeception/Step.php +++ b/src/Codeception/Step.php @@ -293,6 +293,7 @@ public function run(ModuleContainer $container = null) } throw $e; } + return $res; } diff --git a/tests/cli/DryRunCest.php b/tests/cli/DryRunCest.php index 1360b4350a..45ee84b6e1 100644 --- a/tests/cli/DryRunCest.php +++ b/tests/cli/DryRunCest.php @@ -1,4 +1,5 @@ seeInShellOutput('INCOMPLETE'); $I->seeInShellOutput('Step definition for `I have only idea of what\'s going on here` not found'); } + + public function runTestsWithTypedHelper(CliGuy $I) + { + if (PHP_VERSION_ID < 80100) { + $I->markTestSkipped('Requires PHP 8.1'); + } + + $I->amInPath(\codecept_data_dir('typed_helper')); + $I->executeCommand('build'); + $I->executeCommand('dry-run unit --no-ansi'); + $I->seeInShellOutput('print comment'); + $I->seeInShellOutput('I get int'); + $I->seeInShellOutput('I get dom document'); + $I->seeInShellOutput('I see something'); + } } diff --git a/tests/data/typed_helper/codeception.yml b/tests/data/typed_helper/codeception.yml new file mode 100644 index 0000000000..23e6178faa --- /dev/null +++ b/tests/data/typed_helper/codeception.yml @@ -0,0 +1,9 @@ +paths: + tests: tests + output: tests/_output + data: tests/_data + support: tests/_support + envs: tests/_envs +actor_suffix: Tester +settings: + colors: false \ No newline at end of file diff --git a/tests/data/typed_helper/tests/_output/.gitignore b/tests/data/typed_helper/tests/_output/.gitignore new file mode 100644 index 0000000000..c96a04f008 --- /dev/null +++ b/tests/data/typed_helper/tests/_output/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore \ No newline at end of file diff --git a/tests/data/typed_helper/tests/_support/Helper/Unit.php b/tests/data/typed_helper/tests/_support/Helper/Unit.php new file mode 100644 index 0000000000..8c5918a1cd --- /dev/null +++ b/tests/data/typed_helper/tests/_support/Helper/Unit.php @@ -0,0 +1,43 @@ +comment('print comment'); + $I->getInt(); + $I->getDomDocument(); + $I->seeSomething(); + $I->getUnion(); + $I->getIntersection(); + $I->getSelf(); + $I->getParent(); + } +}