Rewrite development helper to lnms dev:check (#11650)

* Refactor pre-commit to class

* docs build

* dusk check

* ci mode for checks

* full checks

* other mysql

* make other lint checks actually work
fix pylint finding

* ci is a long opt

* fix undefined index

* dusk fully working

* ask for forgiveness, not permission

* fix whitespace

* skip dusk sometimes

* Handle 3com and other os with digits

* flags instead of if else spaghetti

* convert to command

* cleanup

* missed check

* fixes

* case

* self-check :D

* argument now

* fix bugs from refactors

* another fix

* adjust file change parsing

* refactor execut a bit

* fallback to global quiet when unknown type.

* allow quiet override for specific commands

* output cleanup

* check flow

* start of tests

* file categorizer tests and fixes

* fixes and cleanup

* skipable not implemented...

* more tests, fix bugs

* more tests and cleanup

* wrong command

* fix canCheck and set env properly

* full env fix

* don't allow dusk on user's run as it will erase their db.

* fix os option

* fix whitespace

* don't need to start server

* ci doesn't like that
This commit is contained in:
Tony Murray 2020-05-22 20:27:48 -05:00 committed by GitHub
parent 0b7373d973
commit ce21011aff
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 1394 additions and 474 deletions

View File

@ -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

View File

@ -0,0 +1,71 @@
<?php
/**
* Categorizer.php
*
* Categorize a list of items according to a dynamic list
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* @package LibreNMS
* @link http://librenms.org
* @copyright 2020 Tony Murray
* @author Tony Murray <murraytony@gmail.com>
*/
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;
}
}

521
LibreNMS/Util/CiHelper.php Normal file
View File

@ -0,0 +1,521 @@
<?php
/**
* CiHelper.php
*
* Code for CI operation
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* @package LibreNMS
* @link http://librenms.org
* @copyright 2020 Tony Murray
* @author Tony Murray <murraytony@gmail.com>
*/
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;
}
}

View File

@ -0,0 +1,137 @@
<?php
/**
* FileCategorizer.php
*
* Categorizes files in LibreNMS
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* @package LibreNMS
* @link http://librenms.org
* @copyright 2020 Tony Murray
* @author Tony Murray <murraytony@gmail.com>
*/
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));
}
}

View File

@ -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:

View File

@ -0,0 +1,122 @@
<?php
/**
* DevCheckCommand.php
*
* -Description-
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* @package LibreNMS
* @link http://librenms.org
* @copyright 2020 Tony Murray
* @author Tony Murray <murraytony@gmail.com>
*/
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();
}
}
}

View File

@ -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');
}
/**

View File

@ -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' => [

View File

@ -1,456 +1,5 @@
#!/usr/bin/env php
<?php
use Illuminate\Support\Str;
$filename = basename(__FILE__);
$install_dir = realpath(__DIR__ . '/..');
chdir($install_dir);
require $install_dir . '/vendor/autoload.php';
if (getenv('FILES')) {
$changed_files = rtrim(getenv('FILES'));
} else {
$changed_files = exec("git diff --diff-filter=d --name-only master | tr '\n' ' '|sed 's/,*$//g'");
}
$changed_files = $changed_files ? explode(' ', $changed_files) : [];
$map = [
'docs' => 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);

282
tests/Unit/CiHelperTest.php Normal file
View File

@ -0,0 +1,282 @@
<?php
/**
* CiHelperTest.php
*
* -Description-
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* @package LibreNMS
* @link http://librenms.org
* @copyright 2020 Tony Murray
* @author Tony Murray <murraytony@gmail.com>
*/
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,
];
}
}

View File

@ -0,0 +1,232 @@
<?php
/**
* FileCategorizerTest.php
*
* -Description-
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* @package LibreNMS
* @link http://librenms.org
* @copyright 2020 Tony Murray
* @author Tony Murray <murraytony@gmail.com>
*/
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' => [],
];
}
}