From d28d20696c23cec75e63b2a3eec7025e590ecb1e Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Thu, 12 Jun 2014 14:16:28 +0200 Subject: [PATCH 1/3] Move binary testing stuff and phpunit.xml to its own module refs #6092 --- {test/php => modules/test}/Makefile | 0 {test/php => modules/test}/bin/README | 0 {test/php => modules/test}/bin/common.h | 0 {test/php => modules/test}/bin/extcmd_test.c | 0 {test/php => modules/test}/bin/shared.h | 0 {test/php => modules/test}/phpunit.xml | 15 +++++++-------- 6 files changed, 7 insertions(+), 8 deletions(-) rename {test/php => modules/test}/Makefile (100%) rename {test/php => modules/test}/bin/README (100%) rename {test/php => modules/test}/bin/common.h (100%) rename {test/php => modules/test}/bin/extcmd_test.c (100%) rename {test/php => modules/test}/bin/shared.h (100%) rename {test/php => modules/test}/phpunit.xml (72%) diff --git a/test/php/Makefile b/modules/test/Makefile similarity index 100% rename from test/php/Makefile rename to modules/test/Makefile diff --git a/test/php/bin/README b/modules/test/bin/README similarity index 100% rename from test/php/bin/README rename to modules/test/bin/README diff --git a/test/php/bin/common.h b/modules/test/bin/common.h similarity index 100% rename from test/php/bin/common.h rename to modules/test/bin/common.h diff --git a/test/php/bin/extcmd_test.c b/modules/test/bin/extcmd_test.c similarity index 100% rename from test/php/bin/extcmd_test.c rename to modules/test/bin/extcmd_test.c diff --git a/test/php/bin/shared.h b/modules/test/bin/shared.h similarity index 100% rename from test/php/bin/shared.h rename to modules/test/bin/shared.h diff --git a/test/php/phpunit.xml b/modules/test/phpunit.xml similarity index 72% rename from test/php/phpunit.xml rename to modules/test/phpunit.xml index b36e81fe8..83fc9d9ac 100644 --- a/test/php/phpunit.xml +++ b/modules/test/phpunit.xml @@ -1,25 +1,24 @@ - + - application/ - bin/ - library/ + ../../test/php/application/ + ../../test/php/library/ - ../../modules/*/test/php - ../../modules/*/test/php/regression + ../*/test/php + ../*/test/php/regression - regression/ - ../../modules/*/test/regression + ../../test/php/regression/ + ../*/test/php/regression From 32a7decc3e4ee153d177a7a7545d1ee10c95ffa7 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Thu, 12 Jun 2014 14:16:53 +0200 Subject: [PATCH 2/3] Remove python test-runners refs #6092 --- test/php/checkswag | 127 --------------------------------------------- test/php/runtests | 124 ------------------------------------------- 2 files changed, 251 deletions(-) delete mode 100755 test/php/checkswag delete mode 100755 test/php/runtests diff --git a/test/php/checkswag b/test/php/checkswag deleted file mode 100755 index c5abdb5d6..000000000 --- a/test/php/checkswag +++ /dev/null @@ -1,127 +0,0 @@ -#!/usr/bin/env python - -import os -import sys -import subprocess -from pipes import quote -from optparse import OptionParser, BadOptionError, AmbiguousOptionError - - -APPLICATION = 'phpcs' -DEFAULT_ARGS = ['-p', '--standard=PSR2', '--extensions=php', - '--encoding=utf-8'] - -VAGRANT_SCRIPT = '/vagrant/test/php/checkswag' -REPORT_DIRECTORY = '../../build/log' - - -class PassThroughOptionParser(OptionParser): - """ - An unknown option pass-through implementation of OptionParser. - - When unknown arguments are encountered, bundle with largs and try again, - until rargs is depleted. - - sys.exit(status) will still be called if a known argument is passed - incorrectly (e.g. missing arguments or bad argument types, etc.) - - Borrowed from: http://stackoverflow.com/a/9307174 - """ - def _process_args(self, largs, rargs, values): - while rargs: - try: - OptionParser._process_args(self, largs, rargs, values) - except (BadOptionError, AmbiguousOptionError), error: - largs.append(error.opt_str) - - -def execute_command(command, return_output=False, shell=False): - prog = subprocess.Popen(command, shell=shell, - stdout=subprocess.PIPE - if return_output - else None) - return prog.wait() if not return_output else \ - prog.communicate()[0] - - -def get_report_directory(): - path = os.path.abspath(REPORT_DIRECTORY) - - try: - os.makedirs(REPORT_DIRECTORY) - except OSError: - pass - - return path - - -def get_script_directory(): - return os.path.dirname(os.path.abspath(sys.argv[0])) - - -def parse_commandline(): - parser = PassThroughOptionParser(usage='%prog [options] [additional arguments' - ' for {0}]'.format(APPLICATION)) - parser.add_option('-b', '--build', action='store_true', - help='Enable reporting.') - parser.add_option('-v', '--verbose', action='store_true', - help='Be more verbose.') - parser.add_option('-i', '--include', metavar='PATTERN', action='append', - help='Include only specific files/test cases.' - ' (Can be supplied multiple times.)') - parser.add_option('-e', '--exclude', metavar='PATTERN', action='append', - help='Exclude specific files/test cases. ' - '(Can be supplied multiple times.)') - parser.add_option('-V', '--vagrant', action='store_true', - help='Run in vagrant VM') - return parser.parse_args() - - -def main(): - options, arguments = parse_commandline() - - if options.vagrant and os.environ['USER'] != 'vagrant': - # Check if vagrant is installed - vagrant_path = execute_command('which vagrant', True, True).strip() - if not vagrant_path: - print 'ERROR: vagrant not found!' - return 2 - - # Call the script in the Vagrant VM with the same parameters - commandline = ' '.join(quote(p) for p in sys.argv[1:]) - return execute_command('vagrant ssh -c "{0} {1}"' - ''.format(VAGRANT_SCRIPT, commandline), - shell=True) - else: - # Environment preparation and verification - os.chdir(get_script_directory()) - application_path = execute_command('which {0}'.format(APPLICATION), - True, True).strip() - if not application_path: - print 'ERROR: {0} not found!'.format(APPLICATION) - return 2 - - # Commandline preparation - command_options = [] - if options.verbose: - command_options.append('-v') - if options.build: - result_path = os.path.join(get_report_directory(), - 'phpcs_results.xml') - command_options.append('--report-checkstyle=' + result_path) - if options.exclude: - command_options.append('--ignore=' + ','.join(options.exclude)) - if options.include: - arguments.extend(options.include) - else: - arguments.extend(['../../application', '../../bin', - '../../library/Icinga']) - - # Application invocation.. - execute_command([application_path] + DEFAULT_ARGS + - command_options + arguments) - return 0 - - -if __name__ == '__main__': - sys.exit(main()) diff --git a/test/php/runtests b/test/php/runtests deleted file mode 100755 index 13f53db1f..000000000 --- a/test/php/runtests +++ /dev/null @@ -1,124 +0,0 @@ -#!/usr/bin/env python - -import os -import sys -import subprocess -from pipes import quote -from fnmatch import fnmatch -from optparse import OptionParser, BadOptionError, AmbiguousOptionError - - -APPLICATION = 'phpunit' -DEFAULT_ARGS = [] - -VAGRANT_SCRIPT = '/vagrant/test/php/runtests' -REPORT_DIRECTORY = '../../build/log' - - -class PassThroughOptionParser(OptionParser): - """ - An unknown option pass-through implementation of OptionParser. - - When unknown arguments are encountered, bundle with largs and try again, - until rargs is depleted. - - sys.exit(status) will still be called if a known argument is passed - incorrectly (e.g. missing arguments or bad argument types, etc.) - - Borrowed from: http://stackoverflow.com/a/9307174 - """ - def _process_args(self, largs, rargs, values): - while rargs: - try: - OptionParser._process_args(self, largs, rargs, values) - except (BadOptionError, AmbiguousOptionError), error: - largs.append(error.opt_str) - - -def execute_command(command, return_output=False, shell=False): - prog = subprocess.Popen(command, shell=shell, - stdout=subprocess.PIPE - if return_output - else None) - return prog.wait() if not return_output else \ - prog.communicate()[0] - - -def get_report_directory(): - path = os.path.abspath(REPORT_DIRECTORY) - - try: - os.makedirs(REPORT_DIRECTORY) - except OSError: - pass - - return path - - -def get_script_directory(): - return os.path.dirname(os.path.abspath(sys.argv[0])) - - -def parse_commandline(): - parser = PassThroughOptionParser(usage='%prog [options] [additional arguments' - ' for {0}]'.format(APPLICATION)) - parser.add_option('-b', '--build', action='store_true', - help='Enable reporting.') - parser.add_option('-v', '--verbose', action='store_true', - help='Be more verbose.') - parser.add_option('-i', '--include', metavar='PATTERN', - help='Include only specific files/test cases.') - parser.add_option('-V', '--vagrant', action='store_true', - help='Run in vagrant VM') - return parser.parse_args() - - -def main(): - options, arguments = parse_commandline() - - if options.vagrant and os.environ['USER'] != 'vagrant': - # Check if vagrant is installed - vagrant_path = execute_command('which vagrant', True, True).strip() - if not vagrant_path: - print 'ERROR: vagrant not found!' - return 2 - - # Call the script in the Vagrant VM with the same parameters - commandline = ' '.join(quote(p) for p in sys.argv[1:]) - return execute_command('vagrant ssh -c "{0} {1}"' - ''.format(VAGRANT_SCRIPT, commandline), - shell=True) - else: - # Environment preparation and verification - os.chdir(get_script_directory()) - application_path = execute_command('which {0}'.format(APPLICATION), - True, True).strip() - if not application_path: - print 'ERROR: {0} not found!'.format(APPLICATION) - return 2 - if not os.path.isfile('./bin/extcmd_test'): - execute_command('make', shell=True) - - # Commandline preparation - command_options = [] - if options.verbose: - command_options.append('--verbose') - if options.build: - report_directory = get_report_directory() - command_options.append('--log-junit') - command_options.append(os.path.join(report_directory, - 'phpunit_results.xml')) - command_options.append('--coverage-html') - command_options.append(os.path.join(report_directory, - 'php_html_coverage')) - if options.include: - command_options.append('--filter') - command_options.append(options.include) - - # Application invocation.. - execute_command([application_path] + DEFAULT_ARGS + - command_options + arguments) - return 0 - -if __name__ == '__main__': - sys.exit(main()) From 0805d73e34d3692ffb77a74c31bd04fbdb67ba27 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Thu, 12 Jun 2014 16:07:25 +0200 Subject: [PATCH 3/3] Add clicommands to run unit- and style-tests refs #6092 --- library/Icinga/Cli/Params.php | 24 +- library/Icinga/Util/Process.php | 205 ++++++++++++++++++ .../application/clicommands/PhpCommand.php | 163 ++++++++++++++ 3 files changed, 390 insertions(+), 2 deletions(-) create mode 100644 library/Icinga/Util/Process.php create mode 100644 modules/test/application/clicommands/PhpCommand.php diff --git a/library/Icinga/Cli/Params.php b/library/Icinga/Cli/Params.php index 1fc47a150..63ae06ea7 100644 --- a/library/Icinga/Cli/Params.php +++ b/library/Icinga/Cli/Params.php @@ -10,12 +10,20 @@ class Params public function __construct($argv) { + $noOptionFlag = false; $this->program = array_shift($argv); for ($i = 0; $i < count($argv); $i++) { - if (substr($argv[$i], 0, 2) === '--') { + if ($argv[$i] === '--') { + $noOptionFlag = true; + } elseif (!$noOptionFlag && substr($argv[$i], 0, 2) === '--') { $key = substr($argv[$i], 2); if (! isset($argv[$i + 1]) || substr($argv[$i + 1], 0, 2) === '--') { $this->params[$key] = true; + } elseif (array_key_exists($key, $this->params)) { + if (!is_array($this->params[$key])) { + $this->params[$key] = array($this->params[$key]); + } + $this->params[$key][] = $argv[++$i]; } else { $this->params[$key] = $argv[++$i]; } @@ -43,6 +51,11 @@ class Params return $this->params; } + public function getAllStandalone() + { + return $this->standalone; + } + public function __get($key) { return $this->get($key); @@ -95,7 +108,14 @@ class Params return $default; } $result = $this->get($key, $default); - $this->remove($key); + if (is_array($result) && !is_array($default)) { + $result = array_shift($result) || $default; + if ($result === $default) { + $this->remove($key); + } + } else { + $this->remove($key); + } return $result; } diff --git a/library/Icinga/Util/Process.php b/library/Icinga/Util/Process.php new file mode 100644 index 000000000..b3d2b9fe4 --- /dev/null +++ b/library/Icinga/Util/Process.php @@ -0,0 +1,205 @@ +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 (isset($this->pipes[0])) { + $write[] = $this->pipes[0]; + } + if (isset($this->pipes[1])) { + $read[] = $this->pipes[1]; + stream_set_blocking($this->pipes[1], 0); + } + if (isset($this->pipes[2])) { + $read[] = $this->pipes[2]; + stream_set_blocking($this->pipes[2], 0); + } + + $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(500000); + } + $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; + } +}