Files
cacti/tests/Unit/Core/Boost/BoostArchiveDropGateTest.php
T
Thomas Vincent 2b6e96b23a style(php): [develop] wrap CLI and poller calls without changing execution (#8303)
* style(php): wrap CLI and poller calls without changing execution

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>

* fix(ci): review security inventories after poller wrapping

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>

* test(boost): compare handoff source contracts by PHP tokens

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>

* style(php): wrap poller constant strings without changing values

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>

* style(php): wrap poller concatenations with token-preserving source contracts

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>

* test(cli): assert CDEF repair queries after constant folding

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>

* test: verify PHP formatting preserves parsed behavior

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>

---------

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
2026-10-14 00:01:26 -07:00

95 lines
4.4 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 |
+-------------------------------------------------------------------------+
*/
/*
* Regression test for #7520: the archive-table DROP in poller_boost.php was
* gated on `$rrd_updates > 0`, an aggregate SUM(status) over whatever
* completion rows landed. If 3 of 4 shards succeeded and the 4th child
* crashed before writing a completion row, the sum from the 3 successful
* shards was still positive and every archive table for the run -- including
* the crashed child's entire unprocessed shard -- got dropped.
*
* The drain loop a few lines above already computes the correct signal
* (boost_completed_children() < $expected_children) to detect a crashed
* child; the fix wires that same completeness check into the drop decision.
*/
$boostPollerPath = CACTI_PATH_BASE . '/poller_boost.php';
test('archive-table drop is gated on shard completeness, not the status sum', function () use ($boostPollerPath) {
$contents = file_get_contents($boostPollerPath);
$rrd_updates_pos = strpos($contents, 'COALESCE(SUM(CAST(status AS SIGNED)), 0)');
expect($rrd_updates_pos)->not->toBeFalse();
$drop_pos = strpos($contents, 'DROP TABLE IF EXISTS `$table`', $rrd_updates_pos);
expect($drop_pos)->not->toBeFalse();
$segment = substr($contents, $rrd_updates_pos, $drop_pos - $rrd_updates_pos);
// The completeness check must appear between the SUM() read and the
// actual DROP, gating it.
expect($segment)->toContain('boost_completed_children($run_id) >= $expected_children');
expect($segment)->toContain('$failed_children === 0');
// A bare "if ($rrd_updates > 0) { ... DROP ... }" with nothing else
// between them is exactly the bug: the drop must not be reachable
// without the completeness check in between.
$gate_pos = strpos($segment, 'boost_completed_children($run_id) >= $expected_children');
expect($gate_pos)->toBeGreaterThan(0);
});
test('partial-completion path logs a warning instead of silently keeping the tables', function () use ($boostPollerPath) {
$contents = file_get_contents($boostPollerPath);
$parser = (new \PhpParser\ParserFactory())->createForNewestSupportedVersion();
$finder = new \PhpParser\NodeFinder();
$printer = new \PhpParser\PrettyPrinter\Standard();
$branches = $finder->findInstanceOf($parser->parse($contents), \PhpParser\Node\Stmt\If_::class);
$gate = null;
foreach ($branches as $branch) {
$condition = $printer->prettyPrintExpr($branch->cond);
if (str_contains($condition, 'boost_completed_children($run_id) >= $expected_children')) {
$gate = $branch;
break;
}
}
expect($gate)->not->toBeNull();
expect($gate->else)->not->toBeNull();
$logs = $finder->find($gate->else->stmts, fn ($node) => $node instanceof \PhpParser\Node\Expr\FuncCall &&
$node->name instanceof \PhpParser\Node\Name &&
$node->name->toString() === 'cacti_log'
);
expect($logs)->toHaveCount(1);
$format = array_values($logs)[0]->args[0]->value;
expect($format)->toBeInstanceOf(\PhpParser\Node\Expr\FuncCall::class);
expect($format->name->toString())->toBe('sprintf');
$message = (new \PhpParser\ConstExprEvaluator())->evaluateDirectly($format->args[0]->value);
expect($message)->toContain('leaving archive tables in place for the next run to pick up');
});
test('boost_completed_children and expected_children are still computed the same way the drain loop uses', function () use ($boostPollerPath) {
$contents = file_get_contents($boostPollerPath);
// The drain loop's own completeness check must still be present and
// unchanged; the drop-gate fix reuses these exact symbols rather than
// inventing a parallel accounting mechanism that could drift from it.
expect($contents)->toContain('boost_completed_children($run_id) < $expected_children');
expect(substr_count($contents, '$expected_children'))->toBeGreaterThanOrEqual(4);
});