Fix dsstats partition dispatch and profile retention fallbacks (#8304)

* fix(core): preserve dsstats type and render retention fallbacks

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

* test(auth): use certificate expiry truth in rendering assertions

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

---------

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
This commit is contained in:
Thomas Vincent authored and GitHub committed 2026-10-11 21:32:00 -04:00
1 parent af9d1591a3
commit 76579e6292
10 files changed
+97 -1

No files matched your search

+1
View File
@@ -213,6 +213,7 @@ function color_import_processor(array &$colors) : array {
switch ($line_item) {
case 'hex':
$hexcol = $j;
// Fall through: retain the hex column alongside the name column.
case 'name':
if (!$first_column) {
$save_order .= ', ';
+1
View File
@@ -129,6 +129,7 @@ switch (grv('action')) {
case 'runall':
debug_runall_filtered();
// Fall through: render the refreshed list after running the selected sources.
default:
$refresh = [
'seconds' => grv('refresh'),
+2 -1
View File
@@ -1141,7 +1141,8 @@ function profile_edit() : void {
form_selectable_cell(filter_value($rra['name'], '', $url), $i);
form_selectable_cell('<em>' . get_span($profile['step'] * $rra['steps'] * $rra['rows']) . '</em>', $i);
form_selectable_cell('<em>' . isset($timespans[$rra['timespan']]) ? $timespans[$rra['timespan']] : get_span($rra['timespan']) . '</em>', $i); // @phpstan-ignore-line
$retention = $timespans[$rra['timespan']] ?? get_span($rra['timespan']);
form_selectable_cell('<em>' . $retention . '</em>', $i);
form_selectable_cell('<em>' . $rra['steps'] . '</em>', $i);
form_selectable_cell('<em>' . $rra['rows'] . '</em>', $i);
+1
View File
@@ -204,6 +204,7 @@ switch (grv('action')) {
case 'unlock':
$_SESSION['sess_graph_lock_id'] = gfrv('id');
$_SESSION['sess_graph_locked'] = (grv('action') == 'lock' ? true : false);
// Fall through: show the editor with the new lock state.
case 'graph_edit':
top_header();
graph_edit();
+1
View File
@@ -1290,6 +1290,7 @@ function aggregate_create_update(int &$local_graph_id, array $member_graphs, arr
(local_graph_id, graph_type_id, consolidation_function_id, text_format, value, hard_return, gprint_id, sequence)
VALUES (?, 1, ?, '', '', 'on', 2, ?)", [$local_graph_id, $cf_id, $next_item_sequence++]);
// Fall through: all totals also need the common totalling graph items.
case AGGREGATE_TOTAL_ONLY:
// use the prefix for totalling GPRINTs as given by the user
switch ($_total_type) {
+2
View File
@@ -2200,8 +2200,10 @@ function memory_bytes(mixed $val) : mixed {
switch($last) {
case 'g':
$val *= 1024;
// Fall through: gigabytes also need the megabyte and kilobyte multipliers.
case 'm':
$val *= 1024;
// Fall through: megabytes also need the kilobyte multiplier.
case 'k':
$val *= 1024;
}
+6
View File
@@ -85,6 +85,12 @@ if (cacti_sizeof($parms)) {
case '--partition':
$fpartition = true;
// Preserve the legacy --partition=TYPE spelling for QA callers.
if ($value !== '') {
$type = $value;
}
break;
case '--type':
$type = $value;
+1
View File
@@ -43,6 +43,7 @@ switch(grv('action')) {
rrdcheck_purge();
// Fall through: display the remaining problems after purging.
default:
rrdcheck_display_problems();
}
@@ -0,0 +1,40 @@
<?php
// Copyright (C) 2004-2026 The Cacti Group. GPL-2.0-or-later.
test('dsstats partition options retain the selected collector type', function (array $parameters, string $expectedType, bool $expectedPartition) {
$source = file_get_contents(dirname(__DIR__, 4) . '/poller_dsstats.php');
$parser = (new \PhpParser\ParserFactory())->createForNewestSupportedVersion();
$nodes = $parser->parse($source);
$block = (new \PhpParser\NodeFinder())->findFirst($nodes, static function ($node) {
return $node instanceof \PhpParser\Node\Stmt\If_ &&
$node->cond instanceof \PhpParser\Node\Expr\FuncCall &&
$node->cond->name instanceof \PhpParser\Node\Name &&
$node->cond->name->toString() === 'cacti_sizeof' &&
$node->cond->args[0]->value instanceof \PhpParser\Node\Expr\Variable &&
$node->cond->args[0]->value->name === 'parms';
});
expect($block)->not->toBeNull();
$fragment = substr($source, $block->getStartFilePos(), $block->getEndFilePos() - $block->getStartFilePos() + 1);
// Execute the actual repository argument parser, before DB/poller work.
$parse = static function (array $parms) use ($fragment) : array {
$debug = false;
$force = false;
$fpartition = false;
$type = 'pmaster';
$thread_id = 0;
eval($fragment);
return [$type, $fpartition];
};
expect($parse($parameters))->toBe([$expectedType, $expectedPartition]);
})->with([
'no partition request' => [[], 'pmaster', false],
'bare long option' => [['--partition'], 'pmaster', true],
'bare short option' => [['-p'], 'pmaster', true],
'type before partition' => [['--type=bmaster', '--partition'], 'bmaster', true],
'type after partition' => [['--partition', '--type=dmaster'], 'dmaster', true],
'legacy partition type' => [['--partition=bmaster'], 'bmaster', true],
'legacy short partition type' => [['-p=dmaster'], 'dmaster', true],
]);
@@ -0,0 +1,42 @@
<?php
// Copyright (C) 2004-2026 The Cacti Group. GPL-2.0-or-later.
test('profile retention labels choose the configured or computed span and retain emphasis', function (?string $configuredLabel) {
$source = file_get_contents(dirname(__DIR__, 4) . '/data_source_profiles.php');
$parser = (new \PhpParser\ParserFactory())->createForNewestSupportedVersion();
$nodes = $parser->parse($source);
$finder = new \PhpParser\NodeFinder();
$printer = new \PhpParser\PrettyPrinter\Standard();
$spanFunction = $finder->findFirst($nodes, static fn ($node) => $node instanceof \PhpParser\Node\Stmt\Function_ && $node->name->toString() === 'get_span');
expect($spanFunction)->not->toBeNull();
// Bind the actual page-local get_span body as a closure; never shadow core functions.
$spanNode = new \PhpParser\Node\Expr\Closure([
'params' => $spanFunction->params, 'stmts' => $spanFunction->stmts,
'returnType' => $spanFunction->returnType,
]);
$span = eval('return ' . $printer->prettyPrintExpr($spanNode) . ';');
$assignment = $finder->findFirst($nodes, static fn ($node) => $node instanceof \PhpParser\Node\Expr\Assign &&
$node->var instanceof \PhpParser\Node\Expr\Variable && $node->var->name === 'retention');
$call = $finder->findFirst($nodes, static function ($node) use ($printer) {
return $node instanceof \PhpParser\Node\Expr\FuncCall &&
$node->name instanceof \PhpParser\Node\Name &&
$node->name->toString() === 'form_selectable_cell' &&
preg_match('/\$(timespans|retention)\b/', $printer->prettyPrintExpr($node->args[0]->value));
});
expect($call)->not->toBeNull();
// Route only the page-local dependency to its production closure.
foreach ($finder->findInstanceOf($nodes, \PhpParser\Node\Expr\FuncCall::class) as $dependency) {
if ($dependency->name instanceof \PhpParser\Node\Name && $dependency->name->toString() === 'get_span') {
$dependency->name = new \PhpParser\Node\Expr\Variable('span');
}
}
$rra = ['timespan' => 7200];
$timespans = $configuredLabel === null ? [] : [7200 => $configuredLabel];
if ($assignment !== null) {
eval($printer->prettyPrintExpr($assignment) . ';');
}
$result = eval('return ' . $printer->prettyPrintExpr($call->args[0]->value) . ';');
expect($result)->toBe('<em>' . ($configuredLabel ?? '2 Hours') . '</em>');
})->with(['configured span' => ['Configured span'], 'computed span' => [null]]);