Updated to Drupal 8.5. Core Media not yet in use.
[yaffs-website] / vendor / drush / drush / tests / CommandUnishTestCase.php
1 <?php
2
3 namespace Unish;
4
5 use Symfony\Component\Process\Process;
6 use Symfony\Component\Process\Exception\ProcessTimedOutException;
7 use Webmozart\PathUtil\Path;
8
9 abstract class CommandUnishTestCase extends UnishTestCase
10 {
11
12   // Unix exit codes.
13     const EXIT_SUCCESS  = 0;
14     const EXIT_ERROR = 1;
15     const UNISH_EXITCODE_USER_ABORT = 75; // Same as DRUSH_EXITCODE_USER_ABORT
16
17   /**
18    * Code coverage data collected during a single test.
19    *
20    * @var array
21    */
22     protected $coverage_data = [];
23
24   /**
25    * Process of last executed command.
26    *
27    * @var Process
28    */
29     private $process;
30
31   /**
32    * Default timeout for commands.
33    *
34    * @var int
35    */
36     private $defaultTimeout = 60;
37
38   /**
39    * Timeout for command.
40    *
41    * Reset to $defaultTimeout after executing a command.
42    *
43    * @var int
44    */
45     protected $timeout = 60;
46
47   /**
48    * Default idle timeout for commands.
49    *
50    * @var int
51    */
52     private $defaultIdleTimeout = 15;
53
54   /**
55    * Idle timeouts for commands.
56    *
57    * Reset to $defaultIdleTimeout after executing a command.
58    *
59    * @var int
60    */
61     protected $idleTimeout = 15;
62
63   /**
64    * Get command output and simplify away things like full paths and extra
65    * whitespace.
66    */
67     protected function getSimplifiedOutput()
68     {
69         return $this->simplifyOutput($this->getOutput());
70     }
71
72     /**
73      * Returns a simplified version of the error output to facilitate testing.
74      *
75      * @return string
76      *   A simplified version of the error output that has things like full
77      *   paths and superfluous whitespace removed from it.
78      */
79     protected function getSimplifiedErrorOutput()
80     {
81         return $this->simplifyOutput($this->getErrorOutput());
82     }
83
84     /**
85      * Remove things like full paths and extra whitespace from the given string.
86      *
87      * @param string $output
88      *   The output string to simplify.
89      *
90      * @return string
91      *   The simplified output.
92      */
93     protected function simplifyOutput($output)
94     {
95         // We do not care if Drush inserts a -t or not in the string. Depends on whether there is a tty.
96         $output = preg_replace('# -t #', ' ', $output);
97         // Remove double spaces from output to help protect test from false negatives if spacing changes subtlely
98         $output = preg_replace('#  *#', ' ', $output);
99         // Remove leading and trailing spaces.
100         $output = preg_replace('#^ *#m', '', $output);
101         $output = preg_replace('# *$#m', '', $output);
102         // Debug flags may be added to command strings if we are in debug mode. Take those out so that tests in phpunit --debug mode work
103         $output = preg_replace('# --debug #', ' ', $output);
104         $output = preg_replace('# --verbose #', ' ', $output);
105         // Get rid of any full paths in the output
106         $output = str_replace(__DIR__, '__DIR__', $output);
107         $output = str_replace(self::getSandbox(), '__SANDBOX__', $output);
108         $output = str_replace(self::getSut(), '__SUT__', $output);
109
110         return $output;
111     }
112
113   /**
114    * Accessor for the last output, trimmed.
115    *
116    * @return string
117    *   Trimmed output as text.
118    *
119    * @access public
120    */
121     public function getOutput()
122     {
123         return trim($this->getOutputRaw());
124     }
125
126   /**
127    * Accessor for the last output, non-trimmed.
128    *
129    * @return string
130    *   Raw output as text.
131    *
132    * @access public
133    */
134     public function getOutputRaw()
135     {
136         return $this->process ? $this->process->getOutput() : '';
137     }
138
139   /**
140    * Accessor for the last output, rtrimmed and split on newlines.
141    *
142    * @return array
143    *   Output as array of lines.
144    *
145    * @access public
146    */
147     public function getOutputAsList()
148     {
149         return array_map('rtrim', explode("\n", $this->getOutput()));
150     }
151
152   /**
153    * Accessor for the last stderr output, trimmed.
154    *
155    * @return string
156    *   Trimmed stderr as text.
157    *
158    * @access public
159    */
160     public function getErrorOutput()
161     {
162         return trim($this->getErrorOutputRaw());
163     }
164
165   /**
166    * Accessor for the last stderr output, non-trimmed.
167    *
168    * @return string
169    *   Raw stderr as text.
170    *
171    * @access public
172    */
173     public function getErrorOutputRaw()
174     {
175         return $this->process ? $this->process->getErrorOutput() : '';
176     }
177
178   /**
179    * Accessor for the last stderr output, rtrimmed and split on newlines.
180    *
181    * @return array
182    *   Stderr as array of lines.
183    *
184    * @access public
185    */
186     public function getErrorOutputAsList()
187     {
188         return array_map('rtrim', explode("\n", $this->getErrorOutput()));
189     }
190
191   /**
192    * Accessor for the last output, decoded from json.
193    *
194    * @param string $key
195    *   Optionally return only a top level element from the json object.
196    *
197    * @return object
198    *   Decoded object.
199    */
200     public function getOutputFromJSON($key = null)
201     {
202         $json = json_decode($this->getOutput());
203         if (isset($key)) {
204             $json = $json->{$key}; // http://stackoverflow.com/questions/2925044/hyphens-in-keys-of-object
205         }
206         return $json;
207     }
208
209   /**
210    * Actually runs the command.
211    *
212    * @param string $command
213    *   The actual command line to run.
214    * @param integer $expected_return
215    *   The return code to expect
216    * @param sting cd
217    *   The directory to run the command in.
218    * @param array $env
219    *  @todo: Not fully implemented yet. Inheriting environment is hard - http://stackoverflow.com/questions/3780866/why-is-my-env-empty.
220    *         @see drush_env().
221    *  Extra environment variables.
222    * @param string $input
223    *   A string representing the STDIN that is piped to the command.
224    * @return integer
225    *   Exit code. Usually self::EXIT_ERROR or self::EXIT_SUCCESS.
226    */
227     public function execute($command, $expected_return = self::EXIT_SUCCESS, $cd = null, $env = null, $input = null)
228     {
229         $return = 1;
230         $this->tick();
231
232         // Apply the environment variables we need for our test to the head of the
233         // command (excludes Windows). Process does have an $env argument, but it replaces the entire
234         // environment with the one given. This *could* be used for ensuring the
235         // test ran with a clean environment, but it also makes tests fail hard on
236         // Travis, for unknown reasons.
237         // @see https://github.com/drush-ops/drush/pull/646
238         $prefix = '';
239         if ($env && !$this->isWindows()) {
240             foreach ($env as $env_name => $env_value) {
241                 $prefix .= $env_name . '=' . self::escapeshellarg($env_value) . ' ';
242             }
243         }
244         $this->log("Executing: $command", 'verbose');
245
246         try {
247             // Process uses a default timeout of 60 seconds, set it to 0 (none).
248             $this->process = new Process($command, $cd, null, $input, 0);
249             if (!getenv('UNISH_NO_TIMEOUTS')) {
250                 $this->process->setTimeout($this->timeout)
251                 ->setIdleTimeout($this->idleTimeout);
252             }
253             $return = $this->process->run();
254             if ($expected_return !== $return) {
255                 $message = 'Unexpected exit code ' . $return . ' (expected ' . $expected_return . ") for command:\n" .  $command;
256                 throw new UnishProcessFailedError($message, $this->process);
257             }
258             // Reset timeouts to default.
259             $this->timeout = $this->defaultTimeout;
260             $this->idleTimeout = $this->defaultIdleTimeout;
261             return $return;
262         } catch (ProcessTimedOutException $e) {
263             if ($e->isGeneralTimeout()) {
264                 $message = 'Command runtime exceeded ' . $this->timeout . " seconds:\n" .  $command;
265             } else {
266                 $message = 'Command had no output for ' . $this->idleTimeout . " seconds:\n" .  $command;
267             }
268             throw new UnishProcessFailedError($message, $this->process);
269         }
270     }
271
272   /**
273    * Invoke drush in via execute().
274    *
275    * @param command
276     *   A defined drush command such as 'cron', 'status' or any of the available ones such as 'drush pm'.
277     * @param args
278     *   Command arguments.
279     * @param $options
280     *   An associative array containing options.
281     * @param $site_specification
282     *   A site alias or site specification. Include the '@' at start of a site alias.
283     * @param $cd
284     *   A directory to change into before executing.
285     * @param $expected_return
286     *   The expected exit code. Usually self::EXIT_ERROR or self::EXIT_SUCCESS.
287     * @param $suffix
288     *   Any code to append to the command. For example, redirection like 2>&1.
289     * @param array $env
290     *   Not used. Environment variables to pass along to the subprocess.
291    *    @todo Look into inheritEnvironmentVariables() - available since Process 3.1. See https://github.com/symfony/symfony/pull/19053/files.
292     * @return integer
293     *   An exit code.
294     */
295     public function drush($command, array $args = [], array $options = [], $site_specification = null, $cd = null, $expected_return = self::EXIT_SUCCESS, $suffix = null, $env = [])
296     {
297         // cd is added for the benefit of siteSshTest which tests a strict command.
298         $global_option_list = ['simulate', 'root', 'uri', 'include', 'config', 'alias-path', 'ssh-options', 'backend', 'cd'];
299         $options += ['uri' => 'dev']; // Default value.
300         $hide_stderr = false;
301         $cmd[] = self::getDrush();
302
303         // Insert global options.
304         foreach ($options as $key => $value) {
305             if (in_array($key, $global_option_list)) {
306                 unset($options[$key]);
307                 if ($key == 'backend') {
308                     $hide_stderr = true;
309                     $value = null;
310                 }
311                 if ($key == 'uri' && $value == 'OMIT') {
312                     continue;
313                 }
314                 if (!isset($value)) {
315                     $cmd[] = "--$key";
316                 } else {
317                     $cmd[] = "--$key=" . self::escapeshellarg($value);
318                 }
319             }
320         }
321
322         if ($level = $this->logLevel()) {
323             $cmd[] = '--' . $level;
324         }
325         $cmd[] = "--no-interaction";
326
327         // Insert code coverage argument before command, in order for it to be
328         // parsed as a global option. This matters for commands like ssh and rsync
329         // where options after the command are passed along to external commands.
330         $result = $this->getTestResultObject();
331         if ($result->getCollectCodeCoverageInformation()) {
332             $coverage_file = tempnam($this->getTmp(), 'drush_coverage');
333             if ($coverage_file) {
334                 $cmd[] = "--drush-coverage=" . $coverage_file;
335             }
336         }
337
338         // Insert site specification and drush command.
339         $cmd[] = empty($site_specification) ? null : self::escapeshellarg($site_specification);
340         $cmd[] = $command;
341
342         // Insert drush command arguments.
343         foreach ($args as $arg) {
344             $cmd[] = self::escapeshellarg($arg);
345         }
346         // insert drush command options
347         foreach ($options as $key => $value) {
348             if (!isset($value)) {
349                 $cmd[] = "--$key";
350             } else {
351                 $cmd[] = "--$key=" . self::escapeshellarg($value);
352             }
353         }
354
355         $cmd[] = $suffix;
356         if ($hide_stderr) {
357             $cmd[] = '2>' . $this->bitBucket();
358         }
359         $exec = array_filter($cmd, 'strlen'); // Remove NULLs
360         // Set sendmail_path to 'true' to disable any outgoing emails
361         // that tests might cause Drupal to send.
362
363         $php_options = (array_key_exists('PHP_OPTIONS', $env)) ? $env['PHP_OPTIONS'] . " " : "";
364         // @todo The PHP Options below are not yet honored by execute(). See .travis.yml for an alternative way.
365         $env['PHP_OPTIONS'] = "${php_options}-d sendmail_path='true'";
366         $cmd = implode(' ', $exec);
367         $return = $this->execute($cmd, $expected_return, $cd, $env);
368
369         // Save code coverage information.
370         if (!empty($coverage_file)) {
371             $data = unserialize(file_get_contents($coverage_file));
372             unlink($coverage_file);
373             // Save for appending after the test finishes.
374             $this->coverage_data[] = $data;
375         }
376
377         return $return;
378     }
379
380   /**
381    * Override the run method, so we can add in our code coverage data after the
382    * test has run.
383    *
384    * We have to collect all coverage data, merge them and append them as one, to
385    * avoid having phpUnit duplicating the test function as many times as drush
386    * has been invoked.
387    *
388    * Runs the test case and collects the results in a TestResult object.
389    * If no TestResult object is passed a new one will be created.
390    *
391    * @param  \PHPUnit_Framework_TestResult $result
392    * @return \PHPUnit_Framework_TestResult
393    * @throws \PHPUnit_Framework_Exception
394    */
395     public function run(\PHPUnit_Framework_TestResult $result = null)
396     {
397         $result = parent::run($result);
398         $data = [];
399         foreach ($this->coverage_data as $merge_data) {
400             foreach ($merge_data as $file => $lines) {
401                 if (!isset($data[$file])) {
402                     $data[$file] = $lines;
403                 } else {
404                     foreach ($lines as $num => $executed) {
405                         if (!isset($data[$file][$num])) {
406                             $data[$file][$num] = $executed;
407                         } else {
408                             $data[$file][$num] = ($executed == 1 ? $executed : $data[$file][$num]);
409                         }
410                     }
411                 }
412             }
413         }
414
415         // Reset coverage data.
416         $this->coverage_data = [];
417         if (!empty($data)) {
418             $result->getCodeCoverage()->append($data, $this);
419         }
420         return $result;
421     }
422
423   /**
424    * A slightly less functional copy of drush_backend_parse_output().
425    */
426     public function parseBackendOutput($string)
427     {
428         $regex = sprintf(self::getBackendOutputDelimiter(), '(.*)');
429         preg_match("/$regex/s", $string, $match);
430         if (isset($match[1])) {
431             // we have our JSON encoded string
432             $output = $match[1];
433             // remove the match we just made and any non printing characters
434             $string = trim(str_replace(sprintf(self::getBackendOutputDelimiter(), $match[1]), '', $string));
435         }
436
437         if (!empty($output)) {
438             $data = json_decode($output, true);
439             if (is_array($data)) {
440                 return $data;
441             }
442         }
443         return $string;
444     }
445
446   /**
447    * Ensure that an expected log message appears in the Drush log.
448    *
449    *     $this->drush('command', array(), array('backend' => NULL));
450    *     $parsed = $this->parse_backend_output($this->getOutput());
451    *     $this->assertLogHasMessage($parsed['log'], "Expected message", 'debug')
452    *
453    * @param $log Parsed log entries from backend invoke
454    * @param $message The expected message that must be contained in
455    *   some log entry's 'message' field.  Substrings will match.
456    * @param $logType The type of log message to look for; all other
457    *   types are ignored. If FALSE (the default), then all log types
458    *   will be searched.
459    */
460     public function assertLogHasMessage($log, $message, $logType = false)
461     {
462         foreach ($log as $entry) {
463             if (!$logType || ($entry['type'] == $logType)) {
464                 $logMessage = $this->getLogMessage($entry);
465                 if (strpos($logMessage, $message) !== false) {
466                     return true;
467                 }
468             }
469         }
470         $this->fail("Could not find expected message in log: " . $message);
471     }
472
473     protected function getLogMessage($entry)
474     {
475         return $this->interpolate($entry['message'], $entry);
476     }
477
478     protected function interpolate($message, array $context)
479     {
480         // build a replacement array with braces around the context keys
481         $replace = [];
482         foreach ($context as $key => $val) {
483             if (!is_array($val) && (!is_object($val) || method_exists($val, '__toString'))) {
484                 $replace[sprintf('{%s}', $key)] = $val;
485             }
486         }
487         // interpolate replacement values into the message and return
488         return strtr($message, $replace);
489     }
490
491     public function drushMajorVersion()
492     {
493         static $major;
494
495         if (!isset($major)) {
496             $this->drush('version', [], ['field' => 'drush-version']);
497             $version = trim($this->getOutput());
498             list($major) = explode('.', $version);
499         }
500         return (int)$major;
501     }
502
503     protected function assertOutputEquals($expected, $filter = '')
504     {
505         $output = $this->getSimplifiedOutput();
506         if (!empty($filter)) {
507             $output = preg_replace($filter, '', $output);
508         }
509         $this->assertEquals($expected, $output);
510     }
511
512     /**
513      * Checks that the error output matches the expected output.
514      *
515      * This matches against a simplified version of the actual output that has
516      * absolute paths and duplicate whitespace removed, to avoid false negatives
517      * on minor differences.
518      *
519      * @param string $expected
520      *   The expected output.
521      * @param string $filter
522      *   Optional regular expression that should be ignored in the error output.
523      */
524     protected function assertErrorOutputEquals($expected, $filter = '')
525     {
526         $output = $this->getSimplifiedErrorOutput();
527         if (!empty($filter)) {
528             $output = preg_replace($filter, '', $output);
529         }
530         $this->assertEquals($expected, $output);
531     }
532 }