TestWatcher::configure()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 10
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 10
ccs 0
cts 7
cp 0
rs 9.4285
c 0
b 0
f 0
cc 1
eloc 7
nc 1
nop 1
crap 2
1
<?php
2
3
declare(strict_types = 1);
4
5
namespace PHPChunkit\Command;
6
7
use PHPChunkit\FileClassesHelper;
8
use PHPChunkit\TestRunner;
9
use PHPChunkit\Configuration;
10
use Symfony\Component\Console\Command\Command;
11
use Symfony\Component\Console\Input\InputInterface;
12
use Symfony\Component\Console\Input\InputOption;
13
use Symfony\Component\Console\Output\OutputInterface;
14
use Symfony\Component\Finder\Finder;
15
use Symfony\Component\Finder\SplFileInfo;
16
17
/**
18
 * @testClass PHPChunkit\Test\Command\TestWatcherTest
19
 */
20
class TestWatcher implements CommandInterface
21
{
22
    const NAME = 'watch';
23
24
    /**
25
     * @var TestRunner
26
     */
27
    private $testRunner;
28
29
    /**
30
     * @var Configuration
31
     */
32
    private $configuration;
33
34
    /**
35
     * @var FileClassesHelper
36
     */
37
    private $fileClassesHelper;
38
39 1
    public function __construct(
40
        TestRunner $testRunner,
41
        Configuration $configuration,
42
        FileClassesHelper $fileClassesHelper)
43
    {
44 1
        $this->testRunner = $testRunner;
45 1
        $this->configuration = $configuration;
46 1
        $this->fileClassesHelper = $fileClassesHelper;
47 1
    }
48
49
    public function getName() : string
50
    {
51
        return self::NAME;
52
    }
53
54
    public function configure(Command $command)
55
    {
56
        $command
57
            ->setDescription('Watch for changes to files and run the associated tests.')
58
            ->addOption('debug', null, InputOption::VALUE_NONE, 'Run tests in debug mode.')
59
            ->addOption('memory-limit', null, InputOption::VALUE_REQUIRED, 'Memory limit for PHP.')
60
            ->addOption('stop', null, InputOption::VALUE_NONE, 'Stop on failure or error.')
61
            ->addOption('failed', null, InputOption::VALUE_REQUIRED, 'Track tests that have failed.', true)
62
        ;
63
    }
64
65 1
    public function execute(InputInterface $input, OutputInterface $output)
66
    {
67 1
        if (empty($this->configuration->getWatchDirectories())) {
68
            throw new \InvalidArgumentException(
69
                'In order to use the watch feature you must configure watch directories.'
70
            );
71
        }
72
73 1
        $output->writeln('<info>Watching for changes to your code.</info>');
74
75 1
        $lastTime = time();
76
77 1
        while ($this->while()) {
78
            $this->sleep();
79
80
            $finder = $this->createFinder();
81
82
            foreach ($finder as $file) {
83
                $lastTime = $this->checkFile($file, $lastTime);
84
            }
85
        }
86 1
    }
87
88
    protected function sleep()
89
    {
90
        usleep(300000);
91
    }
92
93
    protected function while () : bool
94
    {
95
        return true;
96
    }
97
98
    private function checkFile(SplFileInfo $file, int $lastTime) : int
99
    {
100
        $fileLastModified = $file->getMTime();
101
102
        if ($fileLastModified > $lastTime) {
103
104
            $lastTime = $fileLastModified;
105
106
            if (!$this->isTestFile($file)) {
107
                // TODO figure out a better way
108
                // We have to wait a litte bit to look at the contents of the
109
                // file because it might be empty because of the save operation.
110
                usleep(10000);
111
112
                $files = $this->findAssociatedTestFiles($file);
113
114
                if (empty($files)) {
115
                    return $lastTime;
116
                }
117
            } else {
118
                $files = [$file->getPathName()];
119
            }
120
121
            $this->testRunner->runTestFiles($files);
122
        }
123
124
        return $lastTime;
125
    }
126
127
    private function createFinder() : Finder
128
    {
129
        return Finder::create()
130
            ->files()
131
            ->name('*.php')
132
            ->in($this->configuration->getWatchDirectories())
133
        ;
134
    }
135
136
    private function isTestFile(SplFileInfo $file) : bool
137
    {
138
        return strpos($file->getPathName(), 'Test.php') !== false;
139
    }
140
141
    private function findAssociatedTestFiles(SplFileInfo $file) : array
142
    {
143
        $classes = $this->getClassesInsideFile($file->getPathName());
144
145
        $testFiles = [];
146
147
        foreach ($classes as $className) {
148
149
            $reflectionClass = new \ReflectionClass($className);
150
151
            $docComment = $reflectionClass->getDocComment();
152
153
            if ($docComment !== false) {
154
                preg_match_all('/@testClass\s(.*)/', $docComment, $testClasses);
155
156
                if (isset($testClasses[1]) && $testClasses[1]) {
157
                    foreach ($testClasses[1] as $className) {
158
                        $testFiles[] = (new \ReflectionClass($className))->getFileName();
159
                    }
160
                }
161
            }
162
        }
163
164
        return $testFiles;
165
    }
166
167
    private function getClassesInsideFile(string $file) : array
168
    {
169
        return $this->fileClassesHelper->getFileClasses($file);
170
    }
171
}
172