mirror of
https://github.com/librenms/librenms.git
synced 2024-10-07 16:52:45 +00:00
505 lines
16 KiB
PHP
505 lines
16 KiB
PHP
<?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 $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 $ret;
|
|
} 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_cmd = [$this->checkPhpExec('phpunit'), '--colors=always'];
|
|
|
|
if ($this->flags['fail-fast']) {
|
|
array_push($phpunit_cmd, '--stop-on-error', '--stop-on-failure');
|
|
}
|
|
|
|
// exclusive tests
|
|
if ($this->flags['unit_os']) {
|
|
echo 'Only checking os: ' . implode(', ', $this->os) . PHP_EOL;
|
|
$filter = implode('.*|', $this->os);
|
|
// include tests that don't have data providers and only data sets that match
|
|
array_push($phpunit_cmd, '--group', 'os');
|
|
array_push($phpunit_cmd, '--filter', "/::test[A-Za-z]+$|::test[A-Za-z]+ with data set \"$filter.*\"$/");
|
|
} elseif ($this->flags['unit_docs']) {
|
|
array_push($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()
|
|
{
|
|
$cs_cmd = [
|
|
$this->checkPhpExec('phpcs'),
|
|
'-n',
|
|
'-p',
|
|
'--colors',
|
|
'--extensions=php',
|
|
'--standard=misc/phpcs_librenms.xml'
|
|
];
|
|
|
|
$files = $this->flags['full'] ? ['./'] : $this->changed['php'];
|
|
$cs_cmd = array_merge($cs_cmd, $files);
|
|
|
|
return $this->execute('style', $cs_cmd);
|
|
}
|
|
|
|
public function checkWeb()
|
|
{
|
|
if (!$this->flags['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']) {
|
|
array_push($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']) {
|
|
$php_lint_cmd = [$this->checkPhpExec('parallel-lint')];
|
|
|
|
// matches a substring of the relative path, leading / is treated as absolute path
|
|
array_push($php_lint_cmd, '--exclude', 'vendor/');
|
|
|
|
$files = $this->flags['full'] ? ['./'] : $this->changed['php'];
|
|
$php_lint_cmd = array_merge($php_lint_cmd, $files);
|
|
|
|
$return += $this->execute('PHP lint', $php_lint_cmd);
|
|
}
|
|
|
|
if (!$this->flags['lint_skip_python']) {
|
|
$py_lint_cmd = [$this->checkPythonExec('pylint'), '-E', '-j', '0'];
|
|
|
|
$files = $this->flags['full']
|
|
? explode(PHP_EOL, rtrim(shell_exec("find . -name '*.py' -not -path './vendor/*' -not -path './tests/*'")))
|
|
: $this->changed['python'];
|
|
|
|
$py_lint_cmd = array_merge($py_lint_cmd, $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 = array_merge(['scripts/bash_lint.sh'], $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}_enable"] && $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 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'] = $this->flags['unit_skip'] || getenv('SKIP_UNIT_CHECK');
|
|
$this->flags['lint_skip'] = $this->flags['lint_skip'] || getenv('SKIP_LINT_CHECK');
|
|
$this->flags['web_skip'] = $this->flags['web_skip'] || getenv('SKIP_WEB_CHECK');
|
|
$this->flags['style_skip'] = $this->flags['style_skip'] || getenv('SKIP_STYLE_CHECK');
|
|
}
|
|
|
|
public function detectChangedFiles()
|
|
{
|
|
$changed_files = trim(getenv('FILES')) ?:
|
|
exec("git diff --diff-filter=d --name-only master | tr '\n' ' '|sed 's/,*$//g'");
|
|
|
|
$this->flags['full'] = $this->flags['full'] || empty($changed_files); // don't disable full if already set
|
|
$files = $changed_files ? explode(' ', $changed_files) : [];
|
|
|
|
$this->changed = (new FileCategorizer($files))->categorize();
|
|
$this->parseChangedFiles();
|
|
}
|
|
|
|
private function parseChangedFiles()
|
|
{
|
|
if ($this->flags['full'] || !empty($this->changed['full-checks'])) {
|
|
$this->flags['full'] = true; // make sure full is set and skip changed file parsing
|
|
return;
|
|
}
|
|
$this->os = $this->os ?: $this->changed['os'];
|
|
|
|
$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']),
|
|
]);
|
|
|
|
$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);
|
|
}
|
|
}
|