#!/usr/bin/env php 0, 'python' => 0, 'bash' => 0, 'php' => 0, 'os' => [], ]; foreach ($changed_files as $file) { if (starts_with($file, 'doc/')) { $map['docs']++; } if (ends_with($file, '.py')) { $map['python']++; } if (ends_with($file, '.sh')) { $map['bash']++; } if ($file == 'composer.lock') { $map['php']++; // cause full tests to run } // check if os owned file or generic php file if (!empty($os_name = os_from_file($file))) { $map['os'][] = $os_name; } elseif (ends_with($file, '.php')) { $map['php']++; } } $map['os'] = array_unique($map['os']); $short_opts = 'lsufqcho:m:'; $long_opts = array( 'lint', 'style', 'unit', 'os:', 'module:', 'fail-fast', 'quiet', 'snmpsim', 'db', 'commands', 'help', ); $options = getopt($short_opts, $long_opts); if (check_opt($options, 'h', 'help')) { echo "LibreNMS Code Tests Script Running $filename without options runs all checks. -l, --lint Run php lint checks to test for valid syntax -s, --style Run phpcs check to check for PSR-2 compliance -u, --unit Run phpunit tests -o, --os Specific OS to run tests on. Implies --unit, --db, --snmpsim -m, --module Specific Module to run tests on. Implies --unit, --db, --snmpsim -f, --fail-fast Quit when any failure is encountered -q, --quiet Hide output unless there is an error --db Run unit tests that require a database --snmpsim Use snmpsim for unit tests -c, --commands Print commands only, no checks -h, --help Show this help text.\n"; exit(); } // set up some variables $passthru = !check_opt($options, 'q', 'quiet'); $command_only = check_opt($options, 'c', 'commands'); $fail_fast = check_opt($options, 'f', 'fail-fast'); $return = 0; $completed_tests = array( 'lint' => false, 'style' => false, 'unit' => false, ); if ($os = check_opt($options, 'os', 'o')) { // enable unit tests, snmpsim, and db $options['u'] = false; $options['snmpsim'] = false; $options['db'] = false; } if ($module = check_opt($options, 'm', 'module')) { putenv("TEST_MODULES=$module"); // enable unit tests, snmpsim, and db $options['u'] = false; $options['snmpsim'] = false; $options['db'] = false; } $all = !check_opt($options, 'l', 'lint', 's', 'style', 'u', 'unit'); if ($all) { // no test specified, run all tests in this order $options += array('u' => false, 's' => false, 'l' => false); } if (check_opt($options, 'snmpsim')) { putenv('SNMPSIM=1'); } if (check_opt($options, 'db')) { putenv('DBTEST=1'); } // No php files, skip the php checks. if ($map['php'] === 0) { putenv('SKIP_LINT_CHECK=1'); putenv('SKIP_STYLE_CHECK=1'); } // If we have no php files and no OS' found then also skip unit checks. if ($map['php'] === 0 && empty($map['os']) && !$os) { putenv('SKIP_UNIT_CHECK=1'); } // If we have more than 4 (arbitrary number) of OS' then blank them out // Unit tests may take longer to run in a loop so fall back to all. if (count($map['os']) > 4) { unset($map['os']); } // run tests in the order they were specified foreach (array_keys($options) as $opt) { $ret = 0; if ($opt == 'l' || $opt == 'lint') { $ret = run_check('lint', $passthru, $command_only); } elseif ($opt == 's' || $opt == 'style') { $ret = run_check('style', $passthru, $command_only); } elseif ($opt == 'u' || $opt == 'unit') { if (!empty($map['os']) && $map['php'] === 0) { $os = $map['os']; } if (!empty($os)) { echo 'Only checking os: ' . implode(', ', (array)$os) . PHP_EOL; } $ret = run_check('unit', $passthru, $command_only, compact('fail_fast', 'os', 'module')); } if ($fail_fast && $ret !== 0 && $ret !== 250) { exit($ret); } else { $return += $ret; } } // output Tests ok, if no arguments passed if ($all && $return === 0) { echo "\033[32mTests ok, submit away :)\033[0m \n"; } exit($return); //return the combined/single return value of tests function os_from_file($file) { if (starts_with($file, 'includes/definitions/')) { return basename($file, '.yaml'); } elseif (starts_with($file, ['includes/polling', 'includes/discovery'])) { return os_from_php($file); } elseif (starts_with($file, 'LibreNMS/OS/')) { if (preg_match('#LibreNMS/OS/[^/]+.php#', $file)) { // convert class name to os name preg_match_all("/[A-Z][a-z]*/", basename($file, '.php'), $segments); $osname = implode('-', array_map('strtolower', $segments[0])); $os = os_from_php($osname); if ($os) { return $os; } return os_from_php(str_replace('-', '_', $osname)); } } elseif (starts_with($file, ['tests/snmpsim/', 'tests/data/'])) { list($os,) = explode('_', basename(basename($file, '.json'), '.snmprec'), 2); return $os; } return null; } /** * Extract os name from path and validate it exists. * * @param $php_file * @return null|string */ function os_from_php($php_file) { $os = basename($php_file, '.inc.php'); if (file_exists("includes/definitions/$os.yaml")) { return $os; } return null; } /** * Run the specified check and return the return value. * Make sure it isn't skipped by SKIP_TYPE_CHECK env variable and hasn't been run already * * @param string $type type of check lint, style, or unit * @param bool $passthru display the output as comes in * @param bool $command_only only display the intended command, no checks * @param array $options command specific options * @return int the return value from the check (0 = success) */ function run_check($type, $passthru, $command_only, $options = array()) { global $completed_tests; if (getenv('SKIP_' . strtoupper($type) . '_CHECK') || $completed_tests[$type]) { echo ucfirst($type) . " check skipped.\n"; return 0; } $function = 'check_' . $type; if (function_exists($function)) { $completed_tests[$type] = true; return $function($passthru, $command_only, $options); } return 1; } /** * Runs php -l and tests for any syntax errors * * @param bool $passthru display the output as comes in * @param bool $command_only only display the intended command, no checks * @return int the return value from running php -l (0 = success) */ function check_lint($passthru = false, $command_only = false) { $parallel_lint_bin = check_exec('parallel-lint'); // matches a substring of the relative path, leading / is treated as absolute path $lint_excludes = array('vendor/'); $lint_exclude = build_excludes('--exclude ', $lint_excludes); $lint_cmd = "$parallel_lint_bin $lint_exclude ./"; if ($command_only) { echo $lint_cmd . PHP_EOL; return 250; } echo 'Running lint check... '; if ($passthru) { echo PHP_EOL; passthru($lint_cmd, $lint_ret); } else { exec($lint_cmd, $lint_output, $lint_ret); if ($lint_ret > 0) { print(implode(PHP_EOL, $lint_output) . PHP_EOL); } else { echo "success\n"; } } return $lint_ret; } /** * Runs phpcs --standard=PSR2 against the code base * * @param bool $passthru display the output as comes in * @param bool $command_only only display the intended command, no checks * @return int the return value from phpcs (0 = success) */ function check_style($passthru = false, $command_only = false) { $phpcs_bin = check_exec('phpcs'); $cs_cmd = "$phpcs_bin -n -p --colors --extensions=php --standard=misc/phpcs_librenms.xml ./"; if ($command_only) { echo $cs_cmd . PHP_EOL; return 250; } echo 'Running style check... '; if ($passthru) { echo PHP_EOL; passthru($cs_cmd, $cs_ret); } else { exec($cs_cmd, $cs_output, $cs_ret); if ($cs_ret > 0) { echo "failed\n"; print(implode(PHP_EOL, $cs_output) . PHP_EOL); } else { echo "success\n"; } } return $cs_ret; } /** * Runs phpunit * * @param bool $passthru display the output as comes in * @param bool $command_only only display the intended command, no checks * @param array $options Supported: fail_fast, os, module * @return int the return value from phpunit (0 = success) */ function check_unit($passthru = false, $command_only = false, $options = array()) { echo 'Running unit tests... '; $phpunit_bin = check_exec('phpunit'); $phpunit_cmd = "$phpunit_bin --colors=always"; if ($options['fail_fast']) { $phpunit_cmd .= ' --stop-on-error --stop-on-failure'; } if ($options['os']) { $filter = implode('.*|', (array)$options['os']); // include tests that don't have data providers and only data sets that match $phpunit_cmd .= " --group os --filter '/::test[A-Za-z]+$|::test[A-Za-z]+ with data set \"$filter.*\"$/'"; } if ($options['module']) { $phpunit_cmd .= ' tests/OSModulesTest.php'; } if ($command_only) { echo $phpunit_cmd . PHP_EOL; return 250; } if ($passthru) { echo PHP_EOL; passthru($phpunit_cmd, $phpunit_ret); } else { exec($phpunit_cmd, $phpunit_output, $phpunit_ret); if ($phpunit_ret > 0) { echo "failed\n"; echo implode(PHP_EOL, $phpunit_output) . PHP_EOL; echo 'snmpsimd: output at /tmp/snmpsimd.log'; } else { echo "success\n"; } } return $phpunit_ret; } /** * Check if the given options array contains any of the $opts specified * * @param array $options the array from getopt() * @param string ...$opts options to check for * @return bool If one of the specified options is set */ function check_opt($options, ...$opts) { foreach ($opts as $option) { if (isset($options[$option])) { if ($options[$option] === false) { // no data, return that option is enabled return true; } return $options[$option]; } } return false; } /** * Build a list of exclude arguments from an array * * @param string $exclude_string such as "--exclude" * @param array $excludes array of directories to exclude * @return string resulting string */ function build_excludes($exclude_string, $excludes) { $result = ''; foreach ($excludes as $exclude) { $result .= $exclude_string . $exclude . ' '; } return $result; } /** * Check for an executable and return the path to it * If it does not exist, run composer update. * If composer isn't installed, print error and exit. * * @param string $exec the name of the executable to check * @return string path to the executable */ function check_exec($exec) { $path = "vendor/bin/$exec"; if (is_executable($path)) { return $path; } echo "Running composer install to install developer dependencies.\n"; passthru("scripts/composer_wrapper.php install"); if (is_executable($path)) { return $path; } echo "\nRunning installing deps with composer failed.\n You should try running './scripts/composer_wrapper.php install' by hand\n"; echo "You can find more info at http://docs.librenms.org/Developing/Validating-Code/\n"; exit(1); }