Completed
Push — master ( 39c07f...aa04c1 )
by Jitendra
11s
created

TestCommand::onConstruct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 15
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 12
nc 1
nop 0
dl 0
loc 15
rs 9.8666
c 0
b 0
f 0
1
<?php
2
3
namespace Ahc\Phint\Console;
4
5
use Ahc\Cli\IO\Interactor;
6
use Ahc\Phint\Generator\TwigGenerator;
7
use Ahc\Phint\Util\Composer;
8
9
class TestCommand extends BaseCommand
10
{
11
    /** @var string Command name */
12
    protected $_name = 'test';
13
14
    /** @var string Command description */
15
    protected $_desc = 'Generate test stubs';
16
17
    /** @var string Current working dir */
18
    protected $_workDir;
19
20
    /**
21
     * Configure the command options/arguments.
22
     *
23
     * @return void
24
     */
25
    protected function onConstruct()
26
    {
27
        $this->_workDir  = \realpath(\getcwd());
28
29
        $this
30
            ->option('-t --no-teardown', 'Dont add teardown method')
31
            ->option('-s --no-setup', 'Dont add setup method')
32
            ->option('-n --naming', 'Test method naming format')
33
            ->option('-a --with-abstract', 'Create stub for abstract/interface class')
34
            ->option('-p --phpunit [classFqcn]', 'Base PHPUnit class to extend from')
35
            ->option('-d --dump-autoload', 'Force composer dumpautoload (slow)', null, false)
36
            ->usage(
37
                '<bold>  phint test</end> <comment>-n i</end>        With `it_` naming<eol/>' .
38
                '<bold>  phint t</end> <comment>--no-teardown</end>  Without `tearDown()`<eol/>' .
39
                '<bold>  phint test</end> <comment>-a</end>          With stubs for abstract method<eol/>'
40
            );
41
    }
42
43
    public function interact(Interactor $io)
44
    {
45
        $promptConfig = [
46
            'naming' => [
47
                'choices' => ['t' => 'testMethod', 'i' => 'it_tests_', 'm' => 'test_method'],
48
                'default' => 't',
49
            ],
50
            'phpunit' => [
51
                'default' => \class_exists('\\PHPUnit\\Framework\\TestCase')
52
                    ? 'PHPUnit\\Framework\\TestCase'
53
                    : 'PHPUnit_Framework_TestCase',
54
            ],
55
        ];
56
57
        $this->promptAll($io, $promptConfig);
58
    }
59
60
    /**
61
     * Generate test stubs.
62
     *
63
     * @return void
64
     */
65
    public function execute()
66
    {
67
        $io = $this->app()->io();
68
69
        // Generate namespace mappings
70
        if ($this->dumpAutoload) {
0 ignored issues
show
Bug Best Practice introduced by
The property dumpAutoload does not exist on Ahc\Phint\Console\TestCommand. Since you implemented __get, consider adding a @property annotation.
Loading history...
71
            $io->colors('Running <cyanBold>composer dumpautoload</end> <comment>(takes some time)</end><eol>');
72
            $this->_composer->dumpAutoload();
73
        }
74
75
        $io->comment('Preparing metadata ...', true);
76
        $metadata = $this->prepare();
77
78
        if (empty($metadata)) {
79
            $io->bgGreen('Looks like nothing to do here', true);
80
81
            return;
82
        }
83
84
        $io->comment('Generating tests ...', true);
85
        $generated = $this->generate($metadata);
86
87
        if ($generated) {
88
            $io->cyan("$generated test(s) generated", true);
89
        }
90
91
        $io->ok('Done', true);
92
    }
93
94
    protected function prepare(): array
95
    {
96
        // Sorry psr-0!
97
        $namespaces  = $this->_composer->config('autoload.psr-4');
98
        $namespaces += $this->_composer->config('autoload-dev.psr-4');
99
100
        $testNs = [];
101
        foreach ($namespaces as $ns => $path) {
102
            if (!\preg_match('!src/?|lib/?!', $path)) {
103
                unset($namespaces[$ns]);
104
            }
105
106
            if (\strpos($path, 'test') === 0) {
107
                $path   = \rtrim($path, '/\\');
108
                $nsPath = "{$this->_workDir}/$path";
109
                $testNs = \compact('ns', 'nsPath');
110
            } elseif ([] === $testNs) {
111
                $ns     = $ns . '\\Test';
112
                $nsPath = "{$this->_workDir}/tests";
113
                $testNs = \compact('ns', 'nsPath');
114
            }
115
        }
116
117
        $classMap = require $this->_workDir . '/vendor/composer/autoload_classmap.php';
118
119
        return $this->getTestMetadata($classMap, $namespaces, $testNs);
120
    }
121
122
    protected function getTestMetadata(array $classMap, array $namespaces, array $testNs): array
123
    {
124
        $testMeta = [];
125
126
        require_once $this->_workDir . '/vendor/autoload.php';
127
128
        foreach ($classMap as $classFqcn => $classPath) {
129
            foreach ($namespaces as $ns => $nsPath) {
130
                if (\strpos($classFqcn, $ns) !== 0 || \strpos($classFqcn, $testNs['ns']) === 0) {
131
                    continue;
132
                }
133
134
                if ([] === $meta = $this->getClassMetadata($classFqcn, $testNs['ns'])) {
0 ignored issues
show
Unused Code introduced by
The call to Ahc\Phint\Console\TestCommand::getClassMetadata() has too many arguments starting with $testNs['ns']. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

134
                if ([] === $meta = $this->/** @scrutinizer ignore-call */ getClassMetadata($classFqcn, $testNs['ns'])) {

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
135
                    continue;
136
                }
137
138
                $data       = \compact('classFqcn', 'classPath', 'ns', 'nsPath');
139
                $testMeta[] = $meta + $this->convertToTest($data, $testNs);
140
            }
141
        }
142
143
        return $testMeta;
144
    }
145
146
    protected function getClassMetadata(string $classFqcn): array
147
    {
148
        $reflex = new \ReflectionClass($classFqcn);
149
150
        if (!$this->shouldGenerateTest($reflex)) {
151
            return [];
152
        }
153
154
        $methods     = [];
155
        $isTrait     = $reflex->isTrait();
156
        $newable     = $reflex->isInstantiable();
157
        $isAbstract  = $reflex->isAbstract();
158
        $isInterface = $reflex->isInterface();
159
        $excludes    = ['__construct', '__destruct'];
160
161
        foreach ($reflex->getMethods(\ReflectionMethod::IS_PUBLIC) as $m) {
162
            if ($m->class !== $classFqcn || \in_array($m->name, $excludes)) {
163
                continue;
164
            }
165
166
            $methods[\ltrim($m->name, '_')] = ['static' => $m->isStatic(), 'abstract' => $m->isAbstract()];
167
        }
168
169
        return \compact('classFqcn', 'isTrait', 'isAbstract', 'isInterface', 'newable', 'methods');
170
    }
171
172
    protected function shouldGenerateTest(\ReflectionClass $reflex): bool
173
    {
174
        if ($this->abstract) {
0 ignored issues
show
Bug Best Practice introduced by
The property abstract does not exist on Ahc\Phint\Console\TestCommand. Since you implemented __get, consider adding a @property annotation.
Loading history...
175
            return true;
176
        }
177
178
        return !$reflex->isInterface() && !$reflex->isAbstract();
179
    }
180
181
    private function convertToTest(array $metadata, array $testNs): array
182
    {
183
        $classFqcn  = $metadata['classFqcn'];
184
        $classPath  = \realpath($metadata['classPath']);
185
        $nsFullPath = $this->_workDir . '/' . \trim($metadata['nsPath'], '/\\') . '/';
186
        $testPath   = \preg_replace('!^' . \preg_quote($nsFullPath) . '!', $testNs['nsPath'] . '/', $classPath);
187
        $testPath   = \preg_replace('!\.php$!i', 'Test.php', $testPath);
188
        $testFqcn   = \preg_replace('!^' . \preg_quote($metadata['ns']) . '!', $testNs['ns'], $classFqcn);
189
        $fqcnParts  = \explode('\\', $testFqcn);
190
        $className  = \array_pop($fqcnParts);
191
        $testFqns   = \implode('\\', $fqcnParts);
192
        $testFqcn   = $testFqcn . '\\Test';
193
194
        return compact('className', 'testFqns', 'testFqcn', 'testPath');
195
    }
196
197
    protected function generate(array $testMetadata): int
198
    {
199
        $templatePath = __DIR__ . '/../../resources';
200
        $generator    = new TwigGenerator($templatePath, $this->getCachePath());
201
202
        return $generator->generateTests($testMetadata, $this->values());
203
    }
204
}
205