Complex classes like Worker often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.
Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.
While breaking up the class, it is a good idea to analyze how other classes use Worker, and based on these observations, apply Extract Interface, too.
1 | <?php |
||
6 | class Worker |
||
7 | { |
||
8 | private static $descriptorspec = array( |
||
9 | 0 => array("pipe", "r"), |
||
10 | 1 => array("pipe", "w"), |
||
11 | 2 => array("pipe", "w") |
||
12 | ); |
||
13 | private $proc; |
||
14 | private $pipes; |
||
15 | private $inExecution = 0; |
||
16 | private $isRunning = false; |
||
17 | private $exitCode = null; |
||
18 | private $commands = array(); |
||
19 | private $chunks = ''; |
||
20 | private $alreadyReadOutput = ''; |
||
21 | /** |
||
22 | * @var ExecutableTest |
||
23 | */ |
||
24 | private $currentlyExecuting; |
||
25 | |||
26 | 5 | public function start($wrapperBinary, $token = 1, $uniqueToken = null) |
|
27 | { |
||
28 | 5 | $bin = 'PARATEST=1 '; |
|
29 | 5 | if (is_numeric($token)) { |
|
30 | 5 | $bin .= "TEST_TOKEN=$token "; |
|
31 | 5 | } |
|
32 | 5 | if ($uniqueToken) { |
|
33 | $bin .= "UNIQUE_TEST_TOKEN=$uniqueToken "; |
||
34 | } |
||
35 | 5 | $bin .= "exec \"$wrapperBinary\""; |
|
36 | 5 | $pipes = array(); |
|
37 | 5 | $this->proc = proc_open($bin, self::$descriptorspec, $pipes); |
|
38 | 5 | $this->pipes = $pipes; |
|
39 | 5 | $this->isRunning = true; |
|
40 | 5 | } |
|
41 | |||
42 | public function stdout() |
||
46 | |||
47 | 4 | public function execute($testCmd) |
|
48 | { |
||
49 | 4 | $this->checkStarted(); |
|
50 | 4 | $this->commands[] = $testCmd; |
|
51 | 4 | fwrite($this->pipes[0], $testCmd . "\n"); |
|
52 | 4 | $this->inExecution++; |
|
53 | 4 | } |
|
54 | |||
55 | public function assign(ExecutableTest $test, $phpunit, $phpunitOptions) |
||
56 | { |
||
57 | if ($this->currentlyExecuting !== null) { |
||
58 | throw new Exception("Worker already has a test assigned - did you forget to call reset()?"); |
||
59 | } |
||
60 | $this->currentlyExecuting = $test; |
||
61 | $this->execute($test->command($phpunit, $phpunitOptions)); |
||
62 | } |
||
63 | |||
64 | public function printFeedback($printer) |
||
65 | { |
||
66 | if ($this->currentlyExecuting !== null) { |
||
67 | $printer->printFeedback($this->currentlyExecuting); |
||
68 | } |
||
69 | } |
||
70 | |||
71 | public function reset() |
||
75 | |||
76 | 6 | public function isStarted() |
|
80 | |||
81 | 4 | private function checkStarted() |
|
82 | { |
||
83 | 4 | if (!$this->isStarted()) { |
|
84 | throw new \RuntimeException("You have to start the Worker first!"); |
||
85 | } |
||
86 | 4 | } |
|
87 | |||
88 | 3 | public function stop() |
|
89 | { |
||
90 | 3 | fwrite($this->pipes[0], "EXIT\n"); |
|
91 | 3 | fclose($this->pipes[0]); |
|
92 | 3 | } |
|
93 | |||
94 | /** |
||
95 | * This is an utility function for tests. |
||
96 | * Refactor or write it only in the test case. |
||
97 | */ |
||
98 | 2 | public function waitForFinishedJob() |
|
99 | { |
||
100 | 2 | if ($this->inExecution == 0) { |
|
101 | return; |
||
102 | } |
||
103 | 2 | $tellsUsItHasFinished = false; |
|
104 | 2 | stream_set_blocking($this->pipes[1], 1); |
|
105 | 2 | while ($line = fgets($this->pipes[1])) { |
|
106 | 2 | if (strstr($line, "FINISHED\n")) { |
|
107 | 2 | $tellsUsItHasFinished = true; |
|
108 | 2 | $this->inExecution--; |
|
109 | 2 | break; |
|
110 | } |
||
111 | 2 | } |
|
112 | 2 | if (!$tellsUsItHasFinished) { |
|
113 | throw new \RuntimeException("The Worker terminated without finishing the job."); |
||
114 | } |
||
115 | 2 | } |
|
116 | |||
117 | 1 | public function isFree() |
|
118 | { |
||
119 | 1 | $this->checkNotCrashed(); |
|
120 | 1 | $this->updateStateFromAvailableOutput(); |
|
121 | 1 | return $this->inExecution == 0; |
|
122 | } |
||
123 | |||
124 | /** |
||
125 | * @deprecated |
||
126 | * This function consumes a lot of CPU while waiting for |
||
127 | * the worker to finish. Use it only in testing paratest |
||
128 | * itself. |
||
129 | */ |
||
130 | 3 | public function waitForStop() |
|
131 | { |
||
132 | 3 | $status = proc_get_status($this->proc); |
|
133 | 3 | while ($status['running']) { |
|
134 | 3 | $status = proc_get_status($this->proc); |
|
135 | 3 | $this->setExitCode($status); |
|
136 | 3 | } |
|
137 | 3 | } |
|
138 | |||
139 | public function getCoverageFileName() |
||
140 | { |
||
141 | if ($this->currentlyExecuting !== null) { |
||
142 | return $this->currentlyExecuting->getCoverageFileName(); |
||
143 | } else { |
||
144 | return null; |
||
145 | } |
||
146 | } |
||
147 | |||
148 | 4 | private function setExitCode($status) |
|
149 | { |
||
150 | 4 | if (!$status['running']) { |
|
151 | 3 | if ($this->exitCode === null) { |
|
152 | 3 | $this->exitCode = $status['exitcode']; |
|
153 | 3 | } |
|
154 | 3 | } |
|
155 | 4 | } |
|
156 | |||
157 | 1 | public function isRunning() |
|
158 | { |
||
159 | 1 | $this->checkNotCrashed(); |
|
160 | 1 | $this->updateStateFromAvailableOutput(); |
|
161 | 1 | return $this->isRunning; |
|
162 | } |
||
163 | |||
164 | 3 | public function isCrashed() |
|
165 | { |
||
166 | 3 | if (!$this->isStarted()) { |
|
167 | 1 | return false; |
|
168 | } |
||
169 | 3 | $status = proc_get_status($this->proc); |
|
170 | |||
171 | 3 | $this->updateStateFromAvailableOutput(); |
|
172 | 3 | if (!$this->isRunning) { |
|
173 | 2 | return false; |
|
174 | } |
||
175 | |||
176 | 2 | $this->setExitCode($status); |
|
177 | 2 | if ($this->exitCode === null) { |
|
178 | 2 | return false; |
|
179 | } |
||
180 | return $this->exitCode != 0; |
||
181 | } |
||
182 | |||
183 | 2 | private function checkNotCrashed() |
|
184 | { |
||
185 | 2 | if ($this->isCrashed()) { |
|
186 | throw new \RuntimeException( |
||
187 | "This worker has crashed. Last executed command: " . end($this->commands) . PHP_EOL |
||
188 | . "Output:" . PHP_EOL |
||
189 | . "----------------------" . PHP_EOL |
||
190 | . $this->alreadyReadOutput . PHP_EOL |
||
191 | . "----------------------" . PHP_EOL |
||
192 | . $this->readAllStderr() |
||
193 | ); |
||
194 | } |
||
195 | 2 | } |
|
196 | |||
197 | private function readAllStderr() |
||
201 | |||
202 | /** |
||
203 | * Have to read even incomplete lines to play nice with stream_select() |
||
204 | * Otherwise it would continue to non-block because there are bytes to be read, |
||
205 | * but fgets() won't pick them up. |
||
206 | */ |
||
207 | 3 | private function updateStateFromAvailableOutput() |
|
208 | { |
||
209 | 3 | if (isset($this->pipes[1])) { |
|
210 | 2 | stream_set_blocking($this->pipes[1], 0); |
|
211 | 2 | while ($chunk = fread($this->pipes[1], 4096)) { |
|
212 | 1 | $this->chunks .= $chunk; |
|
213 | 1 | $this->alreadyReadOutput .= $chunk; |
|
214 | 1 | } |
|
215 | 2 | $lines = explode("\n", $this->chunks); |
|
233 | } |
||
234 |