Completed
Pull Request — master (#20)
by Jitendra
01:57
created

TestCommand::convertToTest()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 14
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 11
nc 1
nop 2
dl 0
loc 14
rs 9.9
c 0
b 0
f 0
1
<?php
2
3
namespace Ahc\Phint\Console;
4
5
use Ahc\Cli\Exception\RuntimeException;
6
use Ahc\Cli\IO\Interactor;
7
use Ahc\Phint\Generator\TwigGenerator;
8
use Ahc\Phint\Util\Composer;
9
10
class TestCommand extends BaseCommand
11
{
12
    /** @var string Command name */
13
    protected $_name = 'test';
14
15
    /** @var string Command description */
16
    protected $_desc = 'Generate test stubs';
17
18
    /** @var string Current working dir */
19
    protected $_workDir;
20
21
    /**
22
     * Configure the command options/arguments.
23
     *
24
     * @return void
25
     */
26
    protected function onConstruct()
27
    {
28
        $this->_workDir  = \realpath(\getcwd());
29
30
        $this
31
            ->option('-t --no-teardown', 'Dont add teardown method')
32
            ->option('-s --no-setup', 'Dont add setup method')
33
            ->option('-n --naming', 'Test method naming format')
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
            );
40
    }
41
42
    public function interact(Interactor $io)
43
    {
44
        $promptConfig = [
45
            'naming' => [
46
                'choices' => ['t' => 'testMethod', 'i' => 'it_tests_', 'm' => 'test_method'],
47
                'default' => 't',
48
            ],
49
            'phpunit' => [
50
                'default' => \class_exists('\\PHPUnit\\Framework\\TestCase')
51
                    ? 'PHPUnit\\Framework\\TestCase'
52
                    : 'PHPUnit_Framework_TestCase',
53
            ],
54
        ];
55
56
        $this->promptAll($io, $promptConfig);
57
    }
58
59
    /**
60
     * Generate test stubs.
61
     *
62
     * @return void
63
     */
64
    public function execute()
65
    {
66
        $io = $this->app()->io();
67
68
        // Generate namespace mappings
69
        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...
70
            $io->colors('Running <cyanBold>composer dumpautoload</end> <comment>(takes some time)</end><eol>');
71
            $this->_composer->dumpAutoload();
72
        }
73
74
        $io->comment('Preparing metadata ...', true);
75
        $metadata = $this->prepare();
76
77
        if (empty($metadata)) {
78
            $io->bgGreen('Looks like nothing to do here', true);
79
80
            return;
81
        }
82
83
        $io->comment('Generating tests ...', true);
84
        $generated = $this->generate($metadata);
85
86
        if ($generated) {
87
            $io->cyan("$generated test(s) generated", true);
88
        }
89
90
        $io->ok('Done', true);
91
    }
92
93
    protected function prepare(): array
94
    {
95
        // Sorry psr-0!
96
        $namespaces  = $this->_composer->config('autoload.psr-4');
97
        $namespaces += $this->_composer->config('autoload-dev.psr-4');
98
99
        $testNs = [];
100
        foreach ($namespaces as $ns => $path) {
101
            if (!\preg_match('!src/?|lib/?!', $path)) {
102
                unset($namespaces[$ns]);
103
            }
104
105
            if (\strpos($path, 'test') === 0) {
106
                $path   = \rtrim($path, '/\\');
107
                $nsPath = "{$this->_workDir}/$path";
108
                $testNs = \compact('ns', 'nsPath');
109
            } elseif ([] === $testNs) {
110
                $ns     = $ns . '\\Test';
111
                $nsPath = "{$this->_workDir}/tests";
112
                $testNs = \compact('ns', 'nsPath');
113
            }
114
        }
115
116
        $classMap = require $this->_workDir . '/vendor/composer/autoload_classmap.php';
117
118
        return $this->getTestMetadata($classMap, $namespaces, $testNs);
119
    }
120
121
    protected function getTestMetadata(array $classMap, array $namespaces, array $testNs): array
122
    {
123
        $testMeta = [];
124
125
        require_once $this->_workDir . '/vendor/autoload.php';
126
127
        foreach ($classMap as $classFqcn => $classPath) {
128
            foreach ($namespaces as $ns => $nsPath) {
129
                if (\strpos($classFqcn, $ns) !== 0) {
130
                    continue;
131
                }
132
133
                if ([] === $meta = $this->getClassMetadata($classFqcn)) {
134
                    continue;
135
                }
136
137
                $data       = \compact('classFqcn', 'classPath', 'ns', 'nsPath');
138
                $testMeta[] = $meta + $this->convertToTest($data, $testNs);
139
            }
140
        }
141
142
        return $testMeta;
143
    }
144
145
    protected function getClassMetadata(string $classFqcn)
146
    {
147
        $reflex = new \ReflectionClass($classFqcn);
148
149
        if ($reflex->isInterface() || $reflex->isAbstract()) {
150
            return [];
151
        }
152
153
        $methods = [];
154
        $isTrait = $reflex->isTrait();
155
        $newable = $reflex->isInstantiable();
156
        $exclude = ['__construct', '__destruct'];
157
158
        foreach ($reflex->getMethods(\ReflectionMethod::IS_PUBLIC) as $m) {
159
            if ($m->class !== $classFqcn || \in_array($m->name, $exclude) || $m->isAbstract()) {
160
                continue;
161
            }
162
163
            $methods[\ltrim($m->name, '_')] = ['static' => $m->isStatic()];
164
        }
165
166
        return \compact('classFqcn', 'isTrait', 'newable', 'methods');
167
    }
168
169
    private function convertToTest(array $metadata, array $testNs): array
170
    {
171
        $classFqcn  = $metadata['classFqcn'];
172
        $classPath  = \realpath($metadata['classPath']);
173
        $nsFullPath = $this->_workDir . '/' . \trim($metadata['nsPath'], '/\\') . '/';
174
        $testPath   = \preg_replace('!^' . \preg_quote($nsFullPath) . '!', $testNs['nsPath'] . '/', $classPath);
175
        $testPath   = \preg_replace('!\.php$!i', 'Test.php', $testPath);
176
        $testFqcn   = \preg_replace('!^' . \preg_quote($metadata['ns']) . '!', $testNs['ns'], $classFqcn);
177
        $fqcnParts  = \explode('\\', $testFqcn);
178
        $className  = \array_pop($fqcnParts);
179
        $testFqns   = \implode('\\', $fqcnParts);
180
        $testFqcn   = $testFqcn . '\\Test';
181
182
        return compact('className', 'testFqns', 'testFqcn', 'testPath');
183
    }
184
185
    protected function generate(array $testMetadata): int
186
    {
187
        $templatePath = __DIR__ . '/../../resources';
188
        $generator    = new TwigGenerator($templatePath, $this->getCachePath());
189
190
        return $generator->generateTests($testMetadata, $this->values());
191
    }
192
}
193