diff --git a/.travis.yml b/.travis.yml index 15d67f2a91..c04c907d08 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,11 +11,11 @@ matrix: fast_finish: true include: - php: 7.4 - env: SKIP_STYLE_CHECK=1 + env: SKIP_STYLE_CHECK=1 SKIP_WEB_CHECK=1 - php: 7.3 - env: SKIP_UNIT_CHECK=1 BROWSER_TEST=1 CHROME_HEADLESS=1 + env: SKIP_UNIT_CHECK=1 - php: 7.2 - env: SKIP_STYLE_CHECK=1 EXECUTE_BUILD_DOCS=true + env: SKIP_STYLE_CHECK=1 SKIP_WEB_CHECK=1 EXECUTE_BUILD_DOCS=true cache: directories: @@ -31,26 +31,15 @@ before_install: install: - travis_retry composer install --no-interaction --prefer-dist --no-suggest - - pip3 install --user snmpsim - - pip install --user mysql-python pylint + - pip3 install --user snmpsim PyMySQL pylint + - test -n "$SKIP_WEB_CHECK" || php artisan dusk:update --detect after_failure: - tail /tmp/snmpsimd.log before_script: - phpenv config-rm xdebug.ini - - test -z "$BROWSER_TEST" || php artisan dusk:update --detect - - test -z "$BROWSER_TEST" || php artisan serve --env=dusk.testing 2>/dev/null & + - test -n "$SKIP_WEB_CHECK" || php artisan serve --env=dusk.testing 2>/dev/null & script: - - set -e - - export FILES=$(git diff --diff-filter=d --name-only master | tr '\n' ' '|sed 's/,*$//g') - - php scripts/pre-commit.php -q -l - - php scripts/pre-commit.php -q -s - - php scripts/pre-commit.php -u --db --snmpsim --fail-fast - - test -z "$BROWSER_TEST" || php artisan config:clear - - test -z "$BROWSER_TEST" || php artisan dusk - - bash -n daily.sh - - pylint -E poller-wrapper.py discovery-wrapper.py services-wrapper.py - - bash scripts/deploy-docs.sh - - set +e + - php artisan dev:check ci diff --git a/LibreNMS/Util/Categorizer.php b/LibreNMS/Util/Categorizer.php new file mode 100644 index 0000000000..083a4b9cc1 --- /dev/null +++ b/LibreNMS/Util/Categorizer.php @@ -0,0 +1,71 @@ +. + * + * @package LibreNMS + * @link http://librenms.org + * @copyright 2020 Tony Murray + * @author Tony Murray + */ + +namespace LibreNMS\Util; + +class Categorizer +{ + protected $items; + protected $categorized = []; + protected $categories = []; + protected $skippable; + + public function __construct($items = []) + { + $this->skippable = function ($item) { + return false; + }; + $this->items = $items; + } + + public function addCategory(string $category, callable $function) + { + $this->categories[$category] = $function; + $this->categorized[$category] = []; + } + + public function setSkippable(callable $function) + { + $this->skippable = $function; + } + + public function categorize() + { + foreach ($this->items as $item) { + foreach ($this->categories as $category => $test) { + if (call_user_func($this->skippable, $item)) { + continue; + } + + $result = call_user_func($test, $item); + if ($result !== false) { + $this->categorized[$category][] = $result; + } + } + } + + return $this->categorized; + } +} diff --git a/LibreNMS/Util/CiHelper.php b/LibreNMS/Util/CiHelper.php new file mode 100644 index 0000000000..8d0770a361 --- /dev/null +++ b/LibreNMS/Util/CiHelper.php @@ -0,0 +1,521 @@ +. + * + * @package LibreNMS + * @link http://librenms.org + * @copyright 2020 Tony Murray + * @author Tony Murray + */ + +namespace LibreNMS\Util; + +use Illuminate\Support\Arr; +use Symfony\Component\Process\Process; + +class CiHelper +{ + private $changedFiles; + private $changed; + private $os; + private $unitEnv = []; + private $duskEnv = ['APP_ENV' => 'testing']; + + private $completedChecks = [ + 'lint' => false, + 'style' => false, + 'unit' => false, + 'web' => false, + ]; + private $ciDefaults = [ + 'quiet' => [ + 'lint' => true, + 'style' => true, + 'unit' => false, + 'web' => false, + ], + ]; + private $flags = [ + 'lint_enable' => true, + 'style_enable' => true, + 'unit_enable' => true, + 'web_enable' => false, + 'lint_skip' => false, + 'style_skip' => false, + 'unit_skip' => false, + 'web_skip' => false, + 'lint_skip_php' => false, + 'lint_skip_python' => false, + 'lint_skip_bash' => false, + 'unit_os' => false, + 'unit_docs' => false, + 'unit_svg' => false, + 'unit_modules' => false, + 'docs_changed' => false, + 'ci' => false, + 'commands' => false, + 'fail-fast' => false, + 'full' => false, + 'quiet' => false, + ]; + + public function __construct() + { + } + + public function enable($check, $enabled = true) + { + $this->flags["{$check}_enable"] = $enabled; + } + + public function duskHeadless() + { + $this->duskEnv['CHROME_HEADLESS'] = 1; + } + + public function enableDb() + { + $this->unitEnv['DBTEST'] = 1; + } + + public function enableSnmpsim() + { + $this->unitEnv['SNMPSIM'] = 1; + } + + public function setModules(array $modules) + { + $this->unitEnv['TEST_MODULES'] = implode(',', $modules); + $this->flags['unit_modules'] = true; + $this->enableDb(); + $this->enableSnmpsim(); + } + + public function setOS(array $os) + { + $this->os = $os; + $this->flags['unit_os'] = true; + $this->enableDb(); + $this->enableSnmpsim(); + } + + public function setFlags(array $flags) + { + foreach (array_intersect_key($flags, $this->flags) as $key => $value) { + $this->flags[$key] = $value; + } + } + + public function run() + { + $return = 0; + foreach (array_keys($this->completedChecks) as $check) { + $ret = $this->runCheck($check); + + if ($this->flags['fail-fast'] && $ret !== 0 && $ret !== 250) { + return $return; + } else { + $return += $ret; + } + } + + return $return; + } + + /** + * Confirm that all possible checks have been completed + * + * @return bool + */ + public function allChecksComplete() + { + return array_reduce($this->completedChecks, function ($result, $check) { + return $result && $check; + }, false); + } + + /** + * Get a flag value + * @param string $name + * @return bool + */ + public function getFlag($name) + { + return $this->flags[$name] ?? null; + } + + /** + * Fetch all flags + * @return bool[] + */ + public function getFlags() + { + return $this->flags; + } + + /** + * Runs phpunit + * + * @return int the return value from phpunit (0 = success) + */ + public function checkUnit() + { + $phpunit_bin = $this->checkPhpExec('phpunit'); + + $phpunit_cmd = "$phpunit_bin --colors=always"; + + if ($this->flags['fail-fast']) { + $phpunit_cmd .= ' --stop-on-error --stop-on-failure'; + } + + // exclusive tests + if ($this->flags['unit_os']) { + $selected_os = $this->os ?: $this->changed['os']; + echo 'Only checking os: ' . implode(', ', $selected_os) . PHP_EOL; + $filter = implode('.*|', $selected_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.*\"$/'"; + } elseif ($this->flags['unit_docs']) { + $phpunit_cmd .= " --group docs"; + } elseif ($this->flags['unit_svg']) { + $phpunit_cmd .= ' tests/SVGTest.php'; + } elseif ($this->flags['unit_modules']) { + $phpunit_cmd .= ' tests/OSModulesTest.php'; + } + + return $this->execute('unit', $phpunit_cmd, false, $this->unitEnv); + } + + /** + * Runs phpcs --standard=PSR2 against the code base + * + * @return int the return value from phpcs (0 = success) + */ + public function checkStyle() + { + $phpcs_bin = $this->checkPhpExec('phpcs'); + + $files = ($this->flags['full']) ? './' : implode(' ', $this->changed['php']); + + $cs_cmd = "$phpcs_bin -n -p --colors --extensions=php --standard=misc/phpcs_librenms.xml $files"; + + return $this->execute('style', $cs_cmd); + } + + public function checkWeb() + { + if (!$this->getFlag('ci')) { + echo "Warning: dusk may erase your primary database, do not use yet\n"; + return 0; + } + + if ($this->canCheck('web')) { + echo "Preparing for web checks\n"; + $this->execute('config:clear', ['php', 'artisan', 'config:clear'], true); + $this->execute('dusk:update', ['php', 'artisan', 'dusk:update', '--detect'], true); + + // check if web server is running + $server = new Process(['php', '-S', '127.0.0.1:8000', base_path('server.php')], public_path(), ['APP_ENV' => 'dusk.testing']); + $server->setTimeout(3600) + ->setIdleTimeout(3600) + ->start(); + $server->waitUntil(function ($type, $output) { + return strpos($output, 'Development Server (http://127.0.0.1:8000) started') !== false; + }); + if ($server->isRunning()) { + echo "Started server http://127.0.0.1:8000\n"; + } + } + + $dusk_cmd = "php artisan dusk"; + + if ($this->flags['fail-fast']) { + $dusk_cmd .= ' --stop-on-error --stop-on-failure'; + } + + return $this->execute('web', $dusk_cmd, false, $this->duskEnv); + } + + /** + * Runs php -l and tests for any syntax errors + * + * @return int the return value from running php -l (0 = success) + */ + public function checkLint() + { + $return = 0; + if (!$this->flags['lint_skip_php']) { + $parallel_lint_bin = $this->checkPhpExec('parallel-lint'); + + // matches a substring of the relative path, leading / is treated as absolute path + $lint_excludes = ['vendor/']; + $lint_exclude = $this->buildPhpLintExcludes('--exclude ', $lint_excludes); + + $files = $this->flags['full'] ? './' : implode(' ', $this->changed['php']); + + $php_lint_cmd = "$parallel_lint_bin $lint_exclude $files"; + + $return += $this->execute('PHP lint', $php_lint_cmd); + } + + if (!$this->flags['lint_skip_python']) { + $pylint_bin = $this->checkPythonExec('pylint'); + + $files = $this->flags['full'] + ? str_replace(PHP_EOL, ' ', rtrim(shell_exec("find . -name '*.py' -not -path './vendor/*' -not -path './tests/*'"))) + : implode(' ', $this->changed['python']); + + $py_lint_cmd = "$pylint_bin -E -j 0 $files"; + $return += $this->execute('Python lint', $py_lint_cmd); + } + + if (!$this->flags['lint_skip_bash']) { + $files = $this->flags['full'] + ? explode(PHP_EOL, rtrim(shell_exec("find . -name '*.sh' -not -path './node_modules/*' -not -path './vendor/*'"))) + : $this->changed['bash']; + + $bash_cmd = implode(' && ', array_map(function ($file) { + return "bash -n $file"; + }, $files)); + $return += $this->execute('Bash lint', $bash_cmd); + } + + return $return; + } + + /** + * 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 + * @return int the return value from the check (0 = success) + */ + private function runCheck($type) + { + if ($method = $this->canCheck($type)) { + $ret = $this->$method(); + $this->completedChecks[$type] = true; + return $ret; + } + + if ($this->flags["{$type}_skip"]) { + echo ucfirst($type) . " check skipped.\n"; + } + return 0; + } + + /** + * @param string $type + * @return false|string the method name to run + */ + private function canCheck($type) + { + if ($this->flags["{$type}_skip"] || $this->completedChecks[$type]) { + return false; + } + + $method = 'check' . ucfirst($type); + if (method_exists($this, $method) && $this->flags["{$type}_enable"]) { + return $method; + } + + return false; + } + + /** + * Run a check command + * + * @param string $name name for status output + * @param string|array $command + * @param bool $silence silence the status ouput (still shows error output) + * @param array $env environment to set + * @return int + */ + private function execute(string $name, $command, $silence = false, $env = null): int + { + $start = microtime(true); + $proc = new Process($command, null, $env); + + if ($this->flags['commands']) { + $prefix = ''; + if ($env) { + $prefix .= http_build_query($env, '', ' ') . ' '; + } + + echo $prefix . $proc->getCommandLine() . PHP_EOL; + return 250; + } + + if (!$silence) { + echo "Running $name check... "; + } + + $space = strrpos($name, ' '); + $type = substr($name, $space ? $space + 1 : 0); + $quiet = ($this->flags['ci'] && isset($this->ciDefaults['quiet'][$type])) ? $this->ciDefaults['quiet'][$type] : $this->flags['quiet']; + + $proc->setTimeout(3600)->setIdleTimeout(3600); + if (!($silence || $quiet)) { + echo PHP_EOL; + $proc->setTty(Process::isTtySupported()); + } + + $proc->run(); + + $duration = sprintf('%.2fs', microtime(true) - $start); + if ($proc->getExitCode() > 0) { + if (!$silence) { + echo "failed ($duration)\n"; + } + if ($quiet || $silence) { + echo $proc->getOutput() . PHP_EOL; + echo $proc->getErrorOutput() . PHP_EOL; + } + } elseif (!$silence) { + echo "success ($duration)\n"; + } + + return $proc->getExitCode(); + } + + + public function checkEnvSkips() + { + $this->flags['unit_skip'] = (bool)getenv('SKIP_UNIT_CHECK'); + $this->flags['lint_skip'] = (bool)getenv('SKIP_LINT_CHECK'); + $this->flags['web_skip'] = (bool)getenv('SKIP_WEB_CHECK'); + $this->flags['style_skip'] = (bool)getenv('SKIP_STYLE_CHECK'); + } + + public function detectChangedFiles() + { + $files = trim(getenv('FILES')); + $changed_files = $files ?: shell_exec("git diff --diff-filter=d --name-only master | tr '\n' ' '|sed 's/,*$//g'"); + $this->changedFiles = $changed_files ? explode(' ', $changed_files) : []; + + $this->changed = (new FileCategorizer($this->changedFiles))->categorize(); + + $this->parseChangedFiles(); + } + + private function parseChangedFiles() + { + if (empty($this->changedFiles) || $this->flags['full']) { + // nothing to do + return; + } + + $this->setFlags([ + 'lint_skip_php' => empty($this->changed['php']), + 'lint_skip_python' => empty($this->changed['python']), + 'lint_skip_bash' => empty($this->changed['bash']), + 'unit_os' => $this->getFlag('unit_os') || (!empty($this->changed['os']) && empty(array_diff($this->changed['php'], $this->changed['os-files']))), + 'unit_docs' => !empty($this->changed['docs']) && empty($this->changed['php']), + 'unit_svg' => !empty($this->changed['svg']) && empty($this->changed['php']), + 'docs_changed' => !empty($this->changed['docs']), + 'full' => !empty($this->changed['full-checks']), + ]); + + $this->setFlags([ + 'unit_skip' => empty($this->changed['php']) && !array_sum(Arr::only($this->getFlags(), ['unit_os', 'unit_docs', 'unit_svg', 'unit_modules', 'docs_changed'])), + 'lint_skip' => array_sum(Arr::only($this->getFlags(), ['lint_skip_php', 'lint_skip_python', 'lint_skip_bash'])) === 3, + 'style_skip' => empty($this->changed['php']), + 'web_skip' => empty($this->changed['php']) && empty($this->changed['resources']), + ]); + } + + /** + * Check for a PHP executable and return the path to it + * If it does not exist, run composer. + * 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 + */ + private function checkPhpExec($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); + } + + /** + * Check for a Python executable and return the path to it + * If it does not exist, run pip3. + * If pip3 isn't installed, print error and exit. + * + * @param string $exec the name of the executable to check + * @return string path to the executable + */ + private function checkPythonExec($exec) + { + $home = getenv('HOME'); + $path = "$home/.local/bin/$exec"; + + if (is_executable($path)) { + return $path; + } + + // check system + $system_path = rtrim(exec("which pylint 2>/dev/null")); + if (is_executable($system_path)) { + return $system_path; + } + + echo "Running pip3 install to install developer dependencies.\n"; + passthru("pip3 install $exec"); // probably wrong in other cases... + + if (is_executable($path)) { + return $path; + } + + echo "\nRunning installing deps with pip3 failed.\n You should try running 'pip3 install -r requirements.txt' by hand\n"; + echo "You can find more info at http://docs.librenms.org/Developing/Validating-Code/\n"; + exit(1); + } + + /** + * 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 + */ + private function buildPhpLintExcludes($exclude_string, $excludes) + { + $result = ''; + foreach ($excludes as $exclude) { + $result .= $exclude_string . $exclude . ' '; + } + + return $result; + } +} diff --git a/LibreNMS/Util/FileCategorizer.php b/LibreNMS/Util/FileCategorizer.php new file mode 100644 index 0000000000..1690443136 --- /dev/null +++ b/LibreNMS/Util/FileCategorizer.php @@ -0,0 +1,137 @@ +. + * + * @package LibreNMS + * @link http://librenms.org + * @copyright 2020 Tony Murray + * @author Tony Murray + */ + +namespace LibreNMS\Util; + +use Illuminate\Support\Str; + +class FileCategorizer extends Categorizer +{ + private const TESTS_REGEX = '#^tests/(snmpsim|data)/(([0-9a-z\-]+)(_[0-9a-z\-]+)?)(_[0-9a-z\-]+)?\.(json|snmprec)$#'; + + public function __construct($items = []) + { + parent::__construct($items); + + if (getenv('CIHELPER_DEBUG')) { + $this->setSkippable(function ($item) { + return in_array($item, ['.travis.yml', 'LibreNMS/Util/CiHelper.php', 'LibreNMS/Util/FileCategorizer.php']); + }); + } + + $this->addCategory('php', function ($item) { + return Str::endsWith($item, '.php') ? $item : false; + }); + $this->addCategory('docs', function ($item) { + return (Str::startsWith($item, 'doc/') || $item == 'mkdocs.yml') ? $item : false; + }); + $this->addCategory('python', function ($item) { + return Str::endsWith($item, '.py') ? $item : false; + }); + $this->addCategory('bash', function ($item) { + return Str::endsWith($item, '.sh') ? $item : false; + }); + $this->addCategory('svg', function ($item) { + return Str::endsWith($item, '.svg') ? $item : false; + }); + $this->addCategory('resources', function ($item) { + return Str::startsWith($item, 'resources/') ? $item : false; + }); + $this->addCategory('full-checks', function ($item) { + return in_array($item, ['composer.lock', '.travis.yml']) ? $item : false; + }); + $this->addCategory('os-files', function ($item) { + if (($os_name = $this->osFromFile($item)) !== null) { + return ['os' => $os_name, 'file' => $item]; + } + + return false; + }); + } + + public function categorize() + { + parent::categorize(); + + // split out os + $this->categorized['os'] = array_unique(array_column($this->categorized['os-files'], 'os')); + $this->categorized['os-files'] = array_column($this->categorized['os-files'], 'file'); + + // 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($this->categorized['os']) > 4) { + $this->categorized['full-checks'] = [true]; + } + + return $this->categorized; + } + + private function validateOs($os) + { + return file_exists("includes/definitions/$os.yaml") ? $os : null; + } + + private function osFromFile($file) + { + if (Str::startsWith($file, 'includes/definitions/')) { + return basename($file, '.yaml'); + } elseif (Str::startsWith($file, ['includes/polling', 'includes/discovery'])) { + return $this->validateOs(basename($file, '.inc.php')); + } elseif (preg_match('#LibreNMS/OS/[^/]+.php#', $file)) { + return $this->osFromClass(basename($file, '.php')); + } elseif (preg_match(self::TESTS_REGEX, $file, $matches)) { + if ($this->validateOs($matches[3])) { + return $matches[3]; + } + if ($this->validateOs($matches[2])) { + return $matches[2]; + } + } + + return null; + } + + /** + * convert class name to os name + * + * @param string $class + * @return string|null + */ + private function osFromClass($class) + { + preg_match_all("/[A-Z][a-z0-9]*/", $class, $segments); + $osname = implode('-', array_map('strtolower', $segments[0])); + $osname = preg_replace( + ['/^zero-/', '/^one-/', '/^two-/', '/^three-/', '/^four-/', '/^five-/', '/^six-/', '/^seven-/', '/^eight-/', '/^nine-/',], + ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'], + $osname + ); + + if ($os = $this->validateOs($osname)) { + return $os; + } + return $this->validateOs(str_replace('-', '_', $osname)); + } +} diff --git a/LibreNMS/__init__.py b/LibreNMS/__init__.py index 785e0642ed..4099cb8a90 100644 --- a/LibreNMS/__init__.py +++ b/LibreNMS/__init__.py @@ -35,8 +35,8 @@ def call_script(script, args=()): base_dir = os.path.realpath(os.path.dirname(__file__) + "/..") cmd = base + ("{}/{}".format(base_dir, script),) + tuple(map(str, args)) debug("Running {}".format(cmd)) - # preexec_fn=os.setsid here keeps process signals from propagating - return subprocess.check_output(cmd, stderr=subprocess.STDOUT, preexec_fn=os.setsid, close_fds=True).decode() + # preexec_fn=os.setsid here keeps process signals from propagating (close_fds=True is default) + return subprocess.check_output(cmd, stderr=subprocess.STDOUT, preexec_fn=os.setsid).decode() class DB: diff --git a/app/Console/Commands/DevCheckCommand.php b/app/Console/Commands/DevCheckCommand.php new file mode 100644 index 0000000000..26392822a6 --- /dev/null +++ b/app/Console/Commands/DevCheckCommand.php @@ -0,0 +1,122 @@ +. + * + * @package LibreNMS + * @link http://librenms.org + * @copyright 2020 Tony Murray + * @author Tony Murray + */ + +namespace App\Console\Commands; + +use App\Console\LnmsCommand; +use Illuminate\Support\Arr; +use LibreNMS\Util\CiHelper; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputOption; + +class DevCheckCommand extends LnmsCommand +{ + protected $developer = true; + protected $name = 'dev:check'; + + /** @var CiHelper */ + protected $helper; + + public function __construct() + { + parent::__construct(); + $this->addArgument('check', InputArgument::OPTIONAL, __('commands.dev:check.arguments.check', ['checks' => '[unit, lint, style, dusk]']), 'all'); + $this->addOption('os', 'o', InputOption::VALUE_REQUIRED); + $this->addOption('module', 'm', InputOption::VALUE_REQUIRED); + $this->addOption('fail-fast', 'f', InputOption::VALUE_NONE); + $this->addOption('quiet', 'q', InputOption::VALUE_NONE); + $this->addOption('db', null, InputOption::VALUE_NONE); + $this->addOption('snmpsim', null, InputOption::VALUE_NONE); + $this->addOption('full', null, InputOption::VALUE_NONE); + $this->addOption('commands', 'c', InputOption::VALUE_NONE); + } + + /** + * Execute the console command. + * + * @return mixed + */ + public function handle() + { + $this->helper = new CiHelper(); + $this->parseInput(); + $this->helper->detectChangedFiles(); + $this->helper->checkEnvSkips(); + + $result = $this->helper->run(); + + if (getenv('EXECUTE_BUILD_DOCS') && $this->helper->getFlag('docs_changed')) { + exec('bash scripts/deploy-docs.sh'); + } + + if ($result == 0 && $this->helper->allChecksComplete()) { + $this->line("\033[32mTests ok, submit away :)\033[0m"); + } + + return $result; + } + + private function parseInput() + { + $check = $this->argument('check'); + if (!in_array($check, ['all', 'lint', 'style', 'unit', 'web', 'ci'])) { + $this->error("Invalid check: $check"); + exit(1); + } + + $this->helper->setFlags(Arr::only($this->options(), ['quiet', 'commands', 'fail-fast', 'full'])); + + $all = $check == 'all' || $check == 'ci'; + $this->helper->enable('style', $all || $check === 'style'); + $this->helper->enable('lint', $all || $check === 'lint'); + $this->helper->enable('unit', $all || $check === 'unit'); + $this->helper->enable('web', $all || $check === 'web'); + + if ($os = $this->option('os')) { + $this->helper->setFlags(['style_enable' => false, 'lint_enable' => false, 'unit_enable' => true, 'web_enable' => false]); + $this->helper->setOS(explode(',', $os)); + } + + if ($modules = $this->option('module')) { + $this->helper->setFlags(['style_enable' => false, 'lint_enable' => false, 'unit_enable' => true, 'web_enable' => false]); + $this->helper->setModules(explode(',', $modules)); + } + + if ($check == 'ci') { + $this->helper->setFlags(['ci' => true]); + $this->helper->duskHeadless(); + $this->helper->enableSnmpsim(); + $this->helper->enableDb(); + } + + if ($this->option('snmpsim')) { + $this->helper->enableSnmpsim(); + } + + if ($this->option('db')) { + $this->helper->enableDb(); + } + } +} diff --git a/app/Console/LnmsCommand.php b/app/Console/LnmsCommand.php index ff61746309..8424592e55 100644 --- a/app/Console/LnmsCommand.php +++ b/app/Console/LnmsCommand.php @@ -47,7 +47,8 @@ abstract class LnmsCommand extends Command public function isHidden() { - return $this->hidden || ($this->developer && $this->getLaravel()->environment() !== 'production'); + $env = $this->getLaravel() ? $this->getLaravel()->environment() : getenv('APP_ENV'); + return $this->hidden || ($this->developer && $env !== 'production'); } /** diff --git a/resources/lang/en/commands.php b/resources/lang/en/commands.php index 9d184d4422..32fa350b89 100644 --- a/resources/lang/en/commands.php +++ b/resources/lang/en/commands.php @@ -27,6 +27,22 @@ return [ 'no-validation' => 'Cannot set :setting, it is missing validation definition.', ] ], + 'dev:check' => [ + 'description' => 'LibreNMS code checks. Running with no options runs all checks', + 'arguments' => [ + 'check' => 'Run the specified check :checks', + ], + 'options' => [ + 'commands' => 'Print commands that would be run only, no checks', + 'db' => 'Run unit tests that require a database connection', + 'fail-fast' => 'Stop checks when any failure is encountered', + 'full' => 'Run full checks ignoring changed file filtering', + 'module' => 'Specific Module to run tests on. Implies unit, --db, --snmpsim', + 'os' => 'Specific OS to run tests on. Implies unit, --db, --snmpsim', + 'quiet' => 'Hide output unless there is an error', + 'snmpsim' => 'Use snmpsim for unit tests', + ] + ], 'user:add' => [ 'description' => 'Add a local user, you can only log in with this user if auth is set to mysql', 'arguments' => [ diff --git a/scripts/pre-commit.php b/scripts/pre-commit.php index e867352122..0d0c7a18d8 100755 --- a/scripts/pre-commit.php +++ b/scripts/pre-commit.php @@ -1,456 +1,5 @@ #!/usr/bin/env php 0, - 'python' => 0, - 'bash' => 0, - 'php' => 0, - 'os-php' => 0, - 'os' => [], -]; - -foreach ($changed_files as $file) { - if (Str::startsWith($file, 'doc/')) { - $map['docs']++; - } - if (Str::endsWith($file, '.py')) { - $map['python']++; - } - if (Str::endsWith($file, '.sh')) { - $map['bash']++; - } - - // cause full tests to run - if ($file == 'composer.lock' || $file == '.travis.yml') { - $map['php']++; - } - - // check if os owned file or generic php file - if (!empty($os_name = os_from_file($file))) { - $map['os'][] = $os_name; - if (Str::endsWith($file, '.php')) { - $map['os-php']++; - } - } elseif (Str::endsWith($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, -); -$docs_only = 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 (!empty($changed_files) && $map['php'] === 0 && $map['os-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 (!empty($changed_files) && $map['php'] === 0 && empty($map['os']) && !$os) { - if ($map['docs'] > 0) { - $docs_only = true; - } else { - 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', 'docs_only')); - } - - 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 (Str::startsWith($file, 'includes/definitions/')) { - return basename($file, '.yaml'); - } elseif (Str::startsWith($file, ['includes/polling', 'includes/discovery'])) { - return os_from_php($file); - } elseif (Str::startsWith($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 (Str::startsWith($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['docs_only']) { - $phpunit_cmd .= " --group docs"; - } - - 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); -} +echo "Use ./lnms dev:check\n"; +exit(1); diff --git a/tests/Unit/CiHelperTest.php b/tests/Unit/CiHelperTest.php new file mode 100644 index 0000000000..617b23df21 --- /dev/null +++ b/tests/Unit/CiHelperTest.php @@ -0,0 +1,282 @@ +. + * + * @package LibreNMS + * @link http://librenms.org + * @copyright 2020 Tony Murray + * @author Tony Murray + */ + +namespace LibreNMS\Tests\Unit; + +use LibreNMS\Tests\TestCase; +use LibreNMS\Util\CiHelper; + +class CiHelperTest extends TestCase +{ + public function testSetFlags() + { + $helper = new CiHelper(); + $allFalse = array_map(function ($flag) { + return false; + }, $this->getDefaultFlags()); + $allTrue = array_map(function ($flag) { + return false; + }, $this->getDefaultFlags()); + + $helper->setFlags($allFalse); + $this->assertEquals($allFalse, $helper->getFlags()); + + $helper->setFlags($allTrue); + $this->assertEquals($allTrue, $helper->getFlags()); + + $helper->setFlags(['undefined_flag' => false]); + $this->assertEquals($allTrue, $helper->getFlags()); + + $helper->setFlags(['full' => false]); + $testOne = $allTrue; + $testOne['full'] = false; + $this->assertEquals($testOne, $helper->getFlags()); + } + + public function testDefaults() + { + $helper = new CiHelper(); + $this->assertEquals($this->getDefaultFlags(), $helper->getFlags()); + } + + public function testNoFiles() + { + putenv('FILES=none'); + $helper = new CiHelper(); + $helper->detectChangedFiles(); + $this->assertFlagsSet($helper, [ + 'lint_skip' => true, + 'style_skip' => true, + 'unit_skip' => true, + 'web_skip' => true, + 'lint_skip_php' => true, + 'lint_skip_python' => true, + 'lint_skip_bash' => true, + ]); + } + + public function testSetOs() + { + $helper = new CiHelper(); + $helper->setOS(['netonix', 'e3meter']); + $this->assertFlagsSet($helper, [ + 'unit_os' => true, + ]); + + putenv('FILES=none'); + $helper = new CiHelper(); + $helper->setOS(['netonix']); + $helper->detectChangedFiles(); + $this->assertFlagsSet($helper, [ + 'lint_skip' => true, + 'style_skip' => true, + 'web_skip' => true, + 'unit_os' => true, + 'lint_skip_php' => true, + 'lint_skip_python' => true, + 'lint_skip_bash' => true, + ]); + + putenv('FILES=includes/definitions/ios.yaml tests/data/fxos.json'); + $helper = new CiHelper(); + $helper->detectChangedFiles(); + $this->assertFlagsSet($helper, [ + 'lint_skip' => true, + 'style_skip' => true, + 'web_skip' => true, + 'unit_os' => true, + 'lint_skip_php' => true, + 'lint_skip_python' => true, + 'lint_skip_bash' => true, + ]); + } + + public function testSetModules() + { + $helper = new CiHelper(); + $helper->setModules(['sensors', 'processors']); + $this->assertFlagsSet($helper, [ + 'unit_modules' => true, + ]); + + putenv('FILES=none'); + $helper = new CiHelper(); + $helper->setModules(['os']); + $helper->detectChangedFiles(); + $this->assertFlagsSet($helper, [ + 'lint_skip' => true, + 'style_skip' => true, + 'web_skip' => true, + 'unit_modules' => true, + 'lint_skip_php' => true, + 'lint_skip_python' => true, + 'lint_skip_bash' => true, + ]); + + putenv('FILES=none'); + $helper = new CiHelper(); + $helper->setOS(['linux']); + $helper->setModules(['os']); + $helper->detectChangedFiles(); + $this->assertFlagsSet($helper, [ + 'lint_skip' => true, + 'style_skip' => true, + 'web_skip' => true, + 'unit_os' => true, + 'unit_modules' => true, + 'lint_skip_php' => true, + 'lint_skip_python' => true, + 'lint_skip_bash' => true, + ]); + + putenv('FILES=includes/definitions/ios.yaml tests/data/fxos.json'); + $helper = new CiHelper(); + $helper->detectChangedFiles(); + $this->assertFlagsSet($helper, [ + 'lint_skip' => true, + 'style_skip' => true, + 'web_skip' => true, + 'unit_os' => true, + 'lint_skip_php' => true, + 'lint_skip_python' => true, + 'lint_skip_bash' => true, + ]); + + putenv('FILES=includes/definitions/ios.yaml tests/data/fxos.json'); + $helper = new CiHelper(); + $helper->detectChangedFiles(); + $this->assertFlagsSet($helper, [ + 'lint_skip' => true, + 'style_skip' => true, + 'web_skip' => true, + 'unit_os' => true, + 'lint_skip_php' => true, + 'lint_skip_python' => true, + 'lint_skip_bash' => true, + ]); + } + + public function testFileCategorization() + { + putenv('FILES=LibreNMS/Alert/Transport/Sensu.php includes/services.inc.php'); + $helper = new CiHelper(); + $helper->detectChangedFiles(); + $this->assertFlagsSet($helper, [ + 'lint_skip_python' => true, + 'lint_skip_bash' => true, + ]); + + putenv('FILES=/daily.sh includes/services.inc.php'); + $helper = new CiHelper(); + $helper->detectChangedFiles(); + + $this->assertFlagsSet($helper, [ + 'lint_skip_python' => true, + ]); + + putenv('FILES=daily.sh LibreNMS/__init__.py'); + $helper = new CiHelper(); + $helper->detectChangedFiles(); + $this->assertFlagsSet($helper, [ + 'style_skip' => true, + 'unit_skip' => true, + 'web_skip' => true, + 'lint_skip_php' => true, + ]); + + putenv('FILES=includes/polling/sensors/ios.inc.php'); + $helper = new CiHelper(); + $helper->detectChangedFiles(); + $this->assertFlagsSet($helper, [ + 'lint_skip_python' => true, + 'lint_skip_bash' => true, + 'unit_os' => true, + ]); + + putenv('FILES=html/images/os/ios.svg'); + $helper = new CiHelper(); + $helper->detectChangedFiles(); + $this->assertFlagsSet($helper, [ + 'lint_skip' => true, + 'style_skip' => true, + 'web_skip' => true, + 'lint_skip_php' => true, + 'lint_skip_python' => true, + 'lint_skip_bash' => true, + 'unit_svg' => true, + ]); + + putenv('FILES=html/images/os/ios.svg'); + $helper = new CiHelper(); + $helper->detectChangedFiles(); + $this->assertFlagsSet($helper, [ + 'lint_skip' => true, + 'style_skip' => true, + 'web_skip' => true, + 'lint_skip_php' => true, + 'lint_skip_python' => true, + 'lint_skip_bash' => true, + 'unit_svg' => true, + ]); + } + + private function assertFlagsSet(CiHelper $helper, $flags = []) + { + $full = $this->getDefaultFlags(); + foreach ($flags as $name => $value) { + $full[$name] = $value; + $this->assertEquals($value, $helper->getFlag($name), "Flag $name incorrect."); + } + + $this->assertEquals($full, $helper->getFlags()); + } + + private function getDefaultFlags() + { + return [ + 'lint_enable' => true, + 'style_enable' => true, + 'unit_enable' => true, + 'web_enable' => false, + 'lint_skip' => false, + 'style_skip' => false, + 'unit_skip' => false, + 'web_skip' => false, + 'lint_skip_php' => false, + 'lint_skip_python' => false, + 'lint_skip_bash' => false, + 'unit_os' => false, + 'unit_docs' => false, + 'unit_svg' => false, + 'unit_modules' => false, + 'docs_changed' => false, + 'ci' => false, + 'commands' => false, + 'fail-fast' => false, + 'full' => false, + 'quiet' => false, + ]; + } +} diff --git a/tests/Unit/FileCategorizerTest.php b/tests/Unit/FileCategorizerTest.php new file mode 100644 index 0000000000..53f78ed424 --- /dev/null +++ b/tests/Unit/FileCategorizerTest.php @@ -0,0 +1,232 @@ +. + * + * @package LibreNMS + * @link http://librenms.org + * @copyright 2020 Tony Murray + * @author Tony Murray + */ + +namespace LibreNMS\Tests\Unit; + +use Illuminate\Support\Arr; +use LibreNMS\Tests\TestCase; +use LibreNMS\Util\FileCategorizer; + +class FileCategorizerTest extends TestCase +{ + public function testEmptyFiles() + { + $cat = new FileCategorizer(); + $this->assertEquals($this->getCategorySkeleton(), $cat->categorize()); + } + + public function testIgnoredFiles() + { + $this->assertCategorized([], [ + 'docs/Nothing.md', + 'none', + 'includes/something.yaml', + 'html/test.css', + 'falsepythonpy', + 'falsephp', + 'falsebash', + 'resource', + 'vendor/misc/composer.lock', + '/mibs/3com', + ]); + } + + public function testPhpFiles() + { + $this->assertCategorized([ + 'php' => [ + 'includes/polling/sensors.inc.php', + 'misc/test.php', + 'app/Http/Kernel.php', + 'LibreNMS/Modules/Mpls.php' + ], + ]); + } + + public function testDocsFiles() + { + $this->assertCategorized([ + 'docs' => [ + 'doc/CNAME', + 'doc/Developing/Creating-Release.md', + 'mkdocs.yml', + ] + ]); + } + + public function testPython() + { + $this->assertCategorized([ + 'python' => [ + 'python.py', + 'LibreNMS/__init__.py', + ] + ]); + } + + public function testBash() + { + $this->assertCategorized([ + 'bash' => [ + 'daily.sh', + 'scripts/deploy-docs.sh', + ] + ]); + } + + public function testSvg() + { + $this->assertCategorized([ + 'svg' => [ + 'html/images/os/zte.svg', + 'html/images/logos/zyxel.svg', + 'html/svg/403.svg' + ] + ]); + } + + public function testResources() + { + $this->assertCategorized([ + 'resources' => [ + 'resources/js/app.js', + 'resources/js/components/LibrenmsSetting.vue', + 'resources/views/layouts/librenmsv1.blade.php' + ], + 'php' => [ + 'resources/views/layouts/librenmsv1.blade.php' + ] + ]); + } + + public function testOsFiles() + { + $this->assertCategorized([ + 'os' => ['ftd', '3com', 'adva_fsp150', 'saf-integra-b'], + 'os-files' => [ + 'tests/data/ftd.json', + 'tests/data/3com_4200.json', + 'tests/data/adva_fsp150_ge114pro.json', + 'tests/data/saf-integra-b.json' + ] + ]); + + $this->assertCategorized([ + 'os' => ['ciscowap', 'xos', 'ciscosb', 'linux'], + 'os-files' => [ + 'tests/snmpsim/ciscowap.snmprec', + 'tests/snmpsim/xos_x480.snmprec', + 'tests/snmpsim/ciscosb_esw540_8p.snmprec', + 'tests/snmpsim/linux_fbsd-nfs-client-v1.snmprec' + ] + ]); + + $this->assertCategorized([ + 'os' => ['arris-c4', 'ios'], + 'os-files' => [ + 'includes/discovery/sensors/temperature/arris-c4.inc.php', + 'includes/polling/entity-physical/ios.inc.php' + ], + 'php' => [ + 'includes/discovery/sensors/temperature/arris-c4.inc.php', + 'includes/polling/entity-physical/ios.inc.php' + ] + ]); + + $this->assertCategorized([ + 'os' => ['3com', 'arris-dsr4410md', 'adva_fsp3kr7', 'xirrus_aos'], + 'os-files' => [ + 'LibreNMS/OS/ThreeCom.php', + 'LibreNMS/OS/ArrisDsr4410md.php', + 'LibreNMS/OS/AdvaFsp3kr7.php', + 'LibreNMS/OS/XirrusAos.php', + ], + 'php' => [ + 'LibreNMS/OS/ThreeCom.php', + 'LibreNMS/OS/ArrisDsr4410md.php', + 'LibreNMS/OS/AdvaFsp3kr7.php', + 'LibreNMS/OS/XirrusAos.php', + ] + ]); + + $this->assertCategorized([ + 'os' => ['dlink', 'eltex-olt'], + 'os-files' => [ + 'includes/definitions/dlink.yaml', + 'includes/definitions/discovery/eltex-olt.yaml', + ] + ]); + } + + public function testFullChecks() + { + $this->assertCategorized(['full-checks' => ['composer.lock']]); + $this->assertCategorized(['full-checks' => ['.travis.yml']], ['other', '.travis.yml']); + + $this->assertCategorized([ + 'os' => ['3com', 'calix', 'ptp650', 'dd-wrt', 'arista_eos'], + 'os-files' => [ + 'tests/data/3com.json', + 'tests/snmpsim/calix.snmprec', + 'LibreNMS/OS/Ptp650.php', + 'includes/definitions/dd-wrt.yaml', + 'includes/definitions/discovery/arista_eos.yaml', + ], + 'php' => [ + 'LibreNMS/OS/Ptp650.php' + ], + 'full-checks' => [true] + ], [ + 'tests/data/3com.json', + 'tests/snmpsim/calix.snmprec', + 'LibreNMS/OS/Ptp650.php', + 'includes/definitions/dd-wrt.yaml', + 'includes/definitions/discovery/arista_eos.yaml', + ]); + } + + private function assertCategorized($expected, $input = null, $message = '') + { + $files = $input ?? array_unique(Arr::flatten(Arr::except($expected, ['os']))); // os is a virtual category + $expected = array_merge($this->getCategorySkeleton(), $expected); + + $this->assertEquals($expected, (new FileCategorizer($files))->categorize(), $message); + } + + private function getCategorySkeleton() + { + return [ + 'php' => [], + 'docs' => [], + 'python' => [], + 'bash' => [], + 'svg' => [], + 'resources' => [], + 'full-checks' => [], + 'os-files' => [], + 'os' => [], + ]; + } +}