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

TestCommand::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 13
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

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