Skip to content

Commit

Permalink
Merge pull request #6470 from Codeception/fix-dry-run
Browse files Browse the repository at this point in the history
Make dry-run work with module methods having return types
  • Loading branch information
Naktibalda committed Jun 16, 2022
2 parents 29df192 + 52ccc2a commit 49b06ae
Show file tree
Hide file tree
Showing 11 changed files with 220 additions and 5 deletions.
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();
}
}

0 comments on commit 49b06ae

Please sign in to comment.