Files
cacti/tests/integration/BoostProcessTableUpgradeIntegrationTest.php
TheWitness 2780258e7c Add 'Deepness' dark emerald-green theme to develop (with billboard overload CSS) (#8206)
* Add 'Deepness' theme to develop; midwinter billboard + deviceStatus hook

Forward-ports the Deepness theme (1.2.x #8202 + #8205) to develop as one change: the full include/themes/deepness/ theme (emerald-dark :root palette, Paw layout/top-tab JS, full billboard.js 3.13 overload CSS, dark widget CSS), the themes allowlist entry, the i18n_themes name, the midwinter billboard overload rules, and the shared 'deviceStatus' hook class in get_colored_device_status() that the theme styles as status pills. Paper-plane is intentionally untouched (worked separately). The lib/html.php favicon/arrow tweaks from 1.2.x are omitted - develop handles those via get_md5_include_icon()/get_theme_paths() and the theme's own .actionsDropdownArrow CSS.

* deepness: align main.js with Paw parent (drop stale host_id autocomplete & graphDrillDown, fix search-icon/handler leaks)

* deepness: drop bundled billboard.css (obsolete per-theme file; central include/css/billboard.css + main.css .bb overloads cover it)

* i18n: add 'Deepness' theme name to cacti.pot translation template

* Deepness: apply php-cs-fixer formatting and refresh sink inventory baseline

php-cs-fixer fixes the single-quote and // comment style in the deepness theme index.php/rrdtheme.php files, and the sink inventory baseline records the header_redirect sinks in the new deepness index.php files.

* tests: stop the boost upgrade integration test leaking IN_CACTI_INSTALL

BoostProcessTableUpgradeIntegrationTest defined IN_CACTI_INSTALL at file (collection) scope, so it was set for the whole shared pest process and made CactiErrorHandler() take its install-mode early-return. That broke CactiErrorHandlerSuppressionTest ('an enabled level is logged...' expected false, got true) in the pest coverage gate and the locked integration job. The upgrade files required here do not check the constant at load time (only lib/database.php does, at runtime inside its schema probes), so define it in beforeEach after the DB connects instead -- it is never set at collection time and, with no DB in CI, the test skips without defining it. Full pest suite: 2664 passed, 0 failed.
2026-10-07 14:16:56 -04:00

255 lines
10 KiB
PHP

<?php
/*
+-------------------------------------------------------------------------+
| Copyright (C) 2004-2026 The Cacti Group |
| |
| 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 2 |
| of the License, or (at your option) any later version. |
+-------------------------------------------------------------------------+
| Cacti: The Complete RRDtool-based Graphing Solution |
+-------------------------------------------------------------------------+
*/
/*
* upgrade_boost_process_table() has to add the run_child UNIQUE key to
* poller_output_boost_processes, and nothing stops cron from starting
* poller_boost.php while the installer is working. Rows written before this
* upgrade carry no run identifier, so the ALTERs default every one of them to
* ('', 0) and the key cannot build over them; rows written by a child of an
* in-flight run carry a real run_id and are what the parent counts when it
* decides whether every child finished.
*
* These cases run against a real server because the discriminating behaviour
* is the engine's: whether the UNIQUE key builds, and which rows survive.
*
* Requires a MySQL/MariaDB instance; see BoostArchiveHandoffIntegrationTest.php
* for the same env vars. Skips (does not fail) if unreachable.
*/
require_once __DIR__ . '/../Helpers/UnitStubs.php';
require_once dirname(__DIR__, 2) . '/lib/database.php';
require_once dirname(__DIR__, 2) . '/lib/boost.php';
require_once dirname(__DIR__, 2) . '/install/functions.php';
require_once dirname(__DIR__, 2) . '/install/upgrades/1_3_0.php';
beforeEach(function () {
global $database_sessions, $database_hostname, $database_port, $database_default;
$this->db_globals = [$database_sessions, $database_hostname, $database_port, $database_default];
$dsn = getenv('CACTI_TEST_MYSQL_DSN') ?: 'mysql:host=127.0.0.1;port=33061;dbname=cacti_test';
$user = getenv('CACTI_TEST_MYSQL_USER') ?: 'root';
$pass = getenv('CACTI_TEST_MYSQL_PASS') ?: 'root';
try {
$this->pdo = new PDO($dsn, $user, $pass, [PDO::ATTR_ERRMODE => PDO::ERRMODE_SILENT, PDO::ATTR_TIMEOUT => 3]);
} catch (\PDOException $e) {
$this->pdo = null;
test()->markTestSkipped('No MySQL/MariaDB test instance reachable at ' . $dsn . ': ' . $e->getMessage());
return;
}
// Define install mode only once the DB is in play, so collection-time loading
// of this integration test never leaks IN_CACTI_INSTALL into the rest of the
// shared-process unit suite. Upgrade files need it before they run, and it
// makes schema probes bypass their runtime cache while this test drops and
// recreates the same table between cases.
if (!defined('IN_CACTI_INSTALL')) {
define('IN_CACTI_INSTALL', 1);
}
$database_hostname = 'unit-test-host';
$database_port = 3306;
$database_default = 'cacti_boost_upgrade_test';
$database_sessions["$database_hostname:$database_port:$database_default"] = $this->pdo;
$this->pdo->exec("SET SESSION sql_mode = CONCAT_WS(',', @@sql_mode, 'ANSI_QUOTES')");
// db_install_add_cache() stamps install_updated on every statement.
$this->pdo->exec('CREATE TABLE IF NOT EXISTS settings (
name varchar(50) NOT NULL default \'\',
value varchar(2048) NOT NULL default \'\',
PRIMARY KEY (name))');
$this->pdo->exec('DROP TABLE IF EXISTS poller_output_boost_processes');
});
afterEach(function () {
global $database_sessions, $database_hostname, $database_port, $database_default;
if (isset($this->pdo) && $this->pdo instanceof PDO) {
$this->pdo->exec('DROP TABLE IF EXISTS poller_output_boost_processes');
}
// Put the default db_* connection back so the test handle does not answer
// every later read_config_option() in the run.
[$database_sessions, $database_hostname, $database_port, $database_default] = $this->db_globals;
});
function boostProcessIndexColumns(PDO $pdo) : array {
$rows = $pdo->query("SHOW INDEX FROM poller_output_boost_processes WHERE Key_name = 'run_child'");
if ($rows === false) {
return [];
}
$columns = [];
foreach ($rows->fetchAll(PDO::FETCH_ASSOC) as $row) {
$columns[(int) $row['Seq_in_index']] = $row['Column_name'] . ':' . $row['Non_unique'];
}
ksort($columns);
return array_values($columns);
}
test('the 1.2.x table gains the run_child key even though its rows cannot satisfy it', function () {
if (!isset($this->pdo) || !$this->pdo instanceof PDO) {
return;
}
$this->pdo->exec('CREATE TABLE poller_output_boost_processes (
sock_int_value bigint(20) unsigned NOT NULL auto_increment,
status varchar(255) default NULL,
PRIMARY KEY (sock_int_value)) ENGINE=MEMORY');
$this->pdo->exec("INSERT INTO poller_output_boost_processes (status) VALUES ('118'), ('92'), ('0')");
upgrade_boost_process_table();
expect(boostProcessIndexColumns($this->pdo))->toBe(['run_id:0', 'child_id:0'])
->and((int) $this->pdo->query('SELECT COUNT(*) FROM poller_output_boost_processes')->fetchColumn())->toBe(0);
});
test('an in-flight run keeps its completion rows while the legacy rows are dropped', function () {
if (!isset($this->pdo) || !$this->pdo instanceof PDO) {
return;
}
// A collector already carrying the 1.3.0 columns but not yet the key, which
// is what a develop-to-develop upgrade finds.
$this->pdo->exec("CREATE TABLE poller_output_boost_processes (
sock_int_value bigint(20) unsigned NOT NULL auto_increment,
run_id char(32) NOT NULL default '',
child_id int(10) unsigned NOT NULL default 0,
status varchar(255) default NULL,
PRIMARY KEY (sock_int_value)) ENGINE=MEMORY");
$run_id = '3f2a91c4d80b47e6a15c9f0e7b32d5a8';
$this->pdo->exec("INSERT INTO poller_output_boost_processes (run_id, child_id, status) VALUES
('$run_id', 1, '4210'),
('$run_id', 2, '3987'),
('', 0, '77')");
upgrade_boost_process_table();
$survivors = $this->pdo->query('SELECT run_id, child_id, status
FROM poller_output_boost_processes
ORDER BY child_id')->fetchAll(PDO::FETCH_ASSOC);
expect($survivors)->toBe([
['run_id' => $run_id, 'child_id' => 1, 'status' => '4210'],
['run_id' => $run_id, 'child_id' => 2, 'status' => '3987'],
]);
// The parent drains on COUNT(*) for its own run_id, so those two rows are
// the difference between finishing the run and warning about a crashed
// child while holding the archive tables.
$completed = $this->pdo->prepare('SELECT COUNT(*) FROM poller_output_boost_processes WHERE run_id = ?');
$completed->execute([$run_id]);
expect((int) $completed->fetchColumn())->toBe(2)
->and(boostProcessIndexColumns($this->pdo))->toBe(['run_id:0', 'child_id:0']);
// What the upgrade used to do, on the same rows.
$this->pdo->exec('TRUNCATE TABLE poller_output_boost_processes');
$completed->execute([$run_id]);
expect((int) $completed->fetchColumn())->toBe(0);
});
test('a missing process table is recreated with the complete current schema', function () {
if (!isset($this->pdo) || !$this->pdo instanceof PDO) {
return;
}
upgrade_boost_process_table();
$columns = $this->pdo->query('SHOW COLUMNS FROM poller_output_boost_processes')->fetchAll(PDO::FETCH_COLUMN);
$engine = $this->pdo->query("SELECT ENGINE FROM information_schema.TABLES
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'poller_output_boost_processes'")->fetchColumn();
expect($columns)->toBe(['sock_int_value', 'run_id', 'child_id', 'status'])
->and(boostProcessIndexColumns($this->pdo))->toBe(['run_id:0', 'child_id:0'])
->and($engine)->toBe('MEMORY');
});
test('an already upgraded process table and its completion row are unchanged', function () {
if (!isset($this->pdo) || !$this->pdo instanceof PDO) {
return;
}
$this->pdo->exec("CREATE TABLE poller_output_boost_processes (
sock_int_value bigint(20) unsigned NOT NULL auto_increment,
run_id char(32) NOT NULL default '',
child_id int(10) unsigned NOT NULL default 0,
status varchar(255) default NULL,
PRIMARY KEY (sock_int_value),
UNIQUE KEY run_child (run_id, child_id)) ENGINE=MEMORY");
$this->pdo->exec("INSERT INTO poller_output_boost_processes (run_id, child_id, status)
VALUES ('3f2a91c4d80b47e6a15c9f0e7b32d5a8', 4, '123')");
upgrade_boost_process_table();
$row = $this->pdo->query('SELECT run_id, child_id, status FROM poller_output_boost_processes')->fetch(PDO::FETCH_ASSOC);
expect($row)->toBe([
'run_id' => '3f2a91c4d80b47e6a15c9f0e7b32d5a8',
'child_id' => 4,
'status' => '123',
]);
});
test('the run_child index rejects duplicate completion reports for one child', function () {
if (!isset($this->pdo) || !$this->pdo instanceof PDO) {
return;
}
upgrade_boost_process_table();
$insert = $this->pdo->prepare('INSERT INTO poller_output_boost_processes (run_id, child_id, status) VALUES (?, ?, ?)');
expect($insert->execute(['3f2a91c4d80b47e6a15c9f0e7b32d5a8', 1, '10']))->toBeTrue();
$duplicate = $insert->execute(['3f2a91c4d80b47e6a15c9f0e7b32d5a8', 1, '20']);
expect($duplicate)->toBeFalse()
->and((int) $this->pdo->query('SELECT COUNT(*) FROM poller_output_boost_processes')->fetchColumn())->toBe(1);
});
test('runtime repair makes a legacy process table safe before a child records completion', function () {
if (!isset($this->pdo) || !$this->pdo instanceof PDO) {
return;
}
$this->pdo->exec('CREATE TABLE poller_output_boost_processes (
sock_int_value bigint(20) unsigned NOT NULL auto_increment,
status varchar(255) default NULL,
PRIMARY KEY (sock_int_value)) ENGINE=MEMORY');
$this->pdo->exec("INSERT INTO poller_output_boost_processes (status) VALUES ('legacy')");
expect(boost_ensure_process_table(true))->toBeTrue()
->and(boostProcessIndexColumns($this->pdo))->toBe(['run_id:0', 'child_id:0'])
->and((int) $this->pdo->query('SELECT COUNT(*) FROM poller_output_boost_processes')->fetchColumn())->toBe(0);
$insert = $this->pdo->prepare('INSERT INTO poller_output_boost_processes
(run_id, child_id, status) VALUES (?, ?, ?)
ON DUPLICATE KEY UPDATE status = VALUES(status)');
expect($insert->execute(['3f2a91c4d80b47e6a15c9f0e7b32d5a8', 1, '10']))->toBeTrue();
});