Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make dry-run work with module methods having return types #6470

Merged
merged 1 commit into from Jun 16, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
120 changes: 118 additions & 2 deletions src/Codeception/Command/DryRun.php
Expand Up @@ -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;

Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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);
}
}
6 changes: 3 additions & 3 deletions src/Codeception/Lib/Generator/Actions.php
Expand Up @@ -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)
);
}

Expand All @@ -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);
Expand All @@ -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;
Expand Down
1 change: 1 addition & 0 deletions src/Codeception/Step.php
Expand Up @@ -293,6 +293,7 @@ public function run(ModuleContainer $container = null)
}
throw $e;
}

return $res;
}

Expand Down
16 changes: 16 additions & 0 deletions tests/cli/DryRunCest.php
@@ -1,4 +1,5 @@
<?php

class DryRunCest
{
public function _before(CliGuy $I)
Expand All @@ -24,4 +25,19 @@ public function runFeature(CliGuy $I)
$I->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');
}
}
9 changes: 9 additions & 0 deletions 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
2 changes: 2 additions & 0 deletions tests/data/typed_helper/tests/_output/.gitignore
@@ -0,0 +1,2 @@
*
!.gitignore
43 changes: 43 additions & 0 deletions tests/data/typed_helper/tests/_support/Helper/Unit.php
@@ -0,0 +1,43 @@
<?php
namespace Helper;

// here you can define custom actions
// all public methods declared in helper class will be available in $I

class Unit extends \Codeception\Module
{
public function seeSomething(): void
{

}

public function getInt(): int
{
throw new \RuntimeException(__METHOD__ . ' should not be executed');
}

public function getDomDocument(): \DOMDocument
{
throw new \RuntimeException(__METHOD__ . ' should not be executed');
}

public function getUnion(): int|\DOMDocument
{
throw new \RuntimeException(__METHOD__ . ' should not be executed');
}

public function getIntersection(): \Iterator&\Countable&\DOMDocument
{
throw new \RuntimeException(__METHOD__ . ' should not be executed');
}

public function getSelf(): self
{
throw new \RuntimeException(__METHOD__ . ' should not be executed');
}

public function getParent(): parent
{
throw new \RuntimeException(__METHOD__ . ' should not be executed');
}
}
6 changes: 6 additions & 0 deletions tests/data/typed_helper/tests/_support/UnitTester.php
@@ -0,0 +1,6 @@
<?php

class UnitTester extends \Codeception\Actor
{
use _generated\UnitTesterActions;
}
2 changes: 2 additions & 0 deletions tests/data/typed_helper/tests/_support/_generated/.gitignore
@@ -0,0 +1,2 @@
*
!.gitignore
4 changes: 4 additions & 0 deletions tests/data/typed_helper/tests/unit.suite.yml
@@ -0,0 +1,4 @@
actor: UnitTester
modules:
enabled:
- \Helper\Unit
16 changes: 16 additions & 0 deletions tests/data/typed_helper/tests/unit/UseTypedHelperCest.php
@@ -0,0 +1,16 @@
<?php

class UseTypedHelperCest
{
public function executeActions(UnitTester $I)
{
$I->comment('print comment');
$I->getInt();
$I->getDomDocument();
$I->seeSomething();
$I->getUnion();
$I->getIntersection();
$I->getSelf();
$I->getParent();
}
}