From b935cb34fb3b08b3bc97939819721a272af44406 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Fri, 13 Jun 2014 09:27:27 +0200 Subject: [PATCH] Add clicommands to run unit- and style-tests refs #6092 --- library/Icinga/Util/Process.php | 203 ++++++++++++++++++ .../application/clicommands/PhpCommand.php | 163 ++++++++++++++ 2 files changed, 366 insertions(+) create mode 100644 library/Icinga/Util/Process.php create mode 100644 modules/test/application/clicommands/PhpCommand.php diff --git a/library/Icinga/Util/Process.php b/library/Icinga/Util/Process.php new file mode 100644 index 000000000..a73b691c0 --- /dev/null +++ b/library/Icinga/Util/Process.php @@ -0,0 +1,203 @@ +resource = proc_open( + $cmd, + $descriptorSpec, + $this->pipes, + $cwd, + $env + ); + + if (!is_resource($this->resource)) { + throw new RuntimeException("Cannot start process: $cmd"); + } + } + + /** + * Start and return a new process + * + * @param string $cmd The command to start the process + * @param string $cwd The working directory of the new process (Must be an absolute path or null) + * @param string $stdout A filedescriptor, "pipe" or a filepath + * @param string $stderr A filedescriptor, "pipe" or a filepath + * @param string $stdin A filedescriptor, "pipe" or a filepath + * @param array $env The environment variables (Must be an array or null) + * + * @return Process + * + * @throws RuntimeException When the process could not be started + */ + public static function start($cmd, $cwd = null, $stdout = null, $stderr = null, $stdin = null, $env = array()) + { + return new static($cmd, $cwd, $stdout, $stderr, $stdin, $env); + } + + /** + * Interact with process + * + * Send data to stdin. Read data from stdout and stderr, until end-of-file is reached. + * Wait for process to terminate. The optional input argument should be a string to be + * sent to the child process, or null, if no data should be sent to the child. + * + * Note that you need to pass the equivalent pipes to the constructor for this to work. + * + * @param string $input Data to send to the child. + * + * @return array The data from stdout and stderr. + */ + public function communicate($input = null) + { + if (!isset($this->pipes[1]) && !isset($this->pipes[2])) { + $this->wait(); + return array(); + } + + $read = $write = array(); + if ($input !== null && isset($this->pipes[0])) { + $write[] = $this->pipes[0]; + } + if (isset($this->pipes[1])) { + $read[] = $this->pipes[1]; + } + if (isset($this->pipes[2])) { + $read[] = $this->pipes[2]; + } + + $stdout = $stderr = ''; + $readToWatch = $read; + $writeToWatch = $write; + $exceptToWatch = array(); + while (stream_select($readToWatch, $writeToWatch, $exceptToWatch, 0, 20000) !== false) { + if (!empty($writeToWatch) && $input) { + $input = substr($input, fwrite($writeToWatch[0], $input)); + } + foreach ($readToWatch as $pipe) { + if (isset($this->pipes[1]) && $pipe === $this->pipes[1]) { + $stdout .= stream_get_contents($pipe); + } elseif (isset($this->pipes[2]) && $pipe === $this->pipes[2]) { + $stderr .= stream_get_contents($pipe); + } + } + + $readToWatch = array_filter($read, function ($h) { return !feof($h); }); + $writeToWatch = $input ? $write : array(); + if (empty($readToWatch) && empty($writeToWatch)) { + break; + } + } + + $this->wait(); // To ensure the process is actually stopped when calling cleanUp() we utilize wait() + return array($stdout, $stderr); + } + + /** + * Return whether the process is still alive and set the returncode + * + * @return bool + */ + public function poll() + { + if ($this->resource !== null) { + $info = @proc_get_status($this->resource); + if ($info !== false) { + if ($info['running']) { + return true; + } elseif ($info['exitcode'] !== -1) { + $this->returnCode = $info['exitcode']; + } + } + } + + return false; + } + + /** + * Wait for process to terminate and return its returncode + * + * @return int + */ + public function wait() + { + if ($this->returnCode === null && $this->resource !== null) { + while ($this->poll()) { + usleep(50000); + } + $this->cleanUp(); + } + + return $this->returnCode; + } + + /** + * Cleanup the process resource and its associated pipes + */ + protected function cleanUp() + { + foreach ($this->pipes as $pipe) { + if (is_resource($pipe)) { + fclose($pipe); + } + } + + proc_close($this->resource); + $this->resource = null; + } +} diff --git a/modules/test/application/clicommands/PhpCommand.php b/modules/test/application/clicommands/PhpCommand.php new file mode 100644 index 000000000..f5a4b49fe --- /dev/null +++ b/modules/test/application/clicommands/PhpCommand.php @@ -0,0 +1,163 @@ +params->shift('build'); + $include = $this->params->shift('include'); + + $phpUnit = exec('which phpunit'); + if (!file_exists($phpUnit)) { + $this->fail('PHPUnit not found. Please install PHPUnit to be able to run the unit-test suites.'); + } + + $options = array(); + if ($this->isVerbose) { + $options[] = '--verbose'; + } + if ($build) { + $reportPath = $this->setupAndReturnReportDirectory(); + echo $reportPath; + $options[] = '--log-junit'; + $options[] = $reportPath . '/phpunit_results.xml'; + $options[] = '--coverage-html'; + $options[] = $reportPath . '/php_html_coverage'; + } + if ($include !== null) { + $options[] = '--filter'; + $options[] = $include; + } + + Process::start( + $phpUnit . ' ' . join(' ', array_merge($options, $this->params->getAllStandalone())), + realpath(__DIR__ . '/../..') + )->wait(); + } + + /** + * Run code-style checks + * + * This command checks whether icingaweb and installed modules match the PSR-2 coding standard. + * + * USAGE + * + * icingacli test php style [options] + * + * OPTIONS + * + * --verbose Be more verbose. + * --build Enable reporting. + * --include Include only specific files. (Can be supplied multiple times.) + * --exclude Pattern to use for excluding files. (Can be supplied multiple times.) + * + * EXAMPLES + * + * icingacli test php style --verbose + * icingacli test php style --build + * icingacli test php style --include path/to/your/file + * icingacli test php style --exclude *someFile* --exclude someOtherFile* + */ + public function styleAction() + { + $build = $this->params->shift('build'); + $include = (array) $this->params->shift('include', array()); + $exclude = (array) $this->params->shift('exclude', array()); + + $phpcs = exec('which phpcs'); + if (!file_exists($phpcs)) { + $this->fail( + 'PHP_CodeSniffer not found. Please install PHP_CodeSniffer to be able to run code style tests.' + ); + } + + $options = array(); + if ($this->isVerbose) { + $options[] = '-v'; + } + if ($build) { + $options[] = '--report-checkstyle=' . $this->setupAndReturnReportDirectory(); + } + if (!empty($exclude)) { + $options[] = '--ignore=' . join(',', $exclude); + } + $arguments = array_filter(array_map(function ($p) { return realpath($p); }, $include)); + if (empty($arguments)) { + $arguments = array( + realpath(__DIR__ . '/../../../../application'), + realpath(__DIR__ . '/../../../../library/Icinga') + ); + } + + Process::start( + $phpcs . ' ' . join( + ' ', + array_merge( + $options, + $this->phpcsDefaultParams, + $arguments, + $this->params->getAllStandalone() + ) + ), + realpath(__DIR__ . '/../..') + )->wait(); + } + + /** + * Setup the directory where to put report files and return its path + * + * @return string + */ + protected function setupAndReturnReportDirectory() + { + $path = realpath(__DIR__ . '/../../../..') . '/build/log'; + if (!file_exists($path) && !@mkdir($path, 0755, true)) { + $this->fail("Could not create directory: $path"); + } + + return $path; + } +}