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(); });