diff --git a/color.php b/color.php
index 7ddb4f3e2..3e6d027e3 100644
--- a/color.php
+++ b/color.php
@@ -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 .= ', ';
diff --git a/data_debug.php b/data_debug.php
index 9fb51b182..b0d6a8f14 100644
--- a/data_debug.php
+++ b/data_debug.php
@@ -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'),
diff --git a/data_source_profiles.php b/data_source_profiles.php
index bcafc2f4c..d89092f64 100644
--- a/data_source_profiles.php
+++ b/data_source_profiles.php
@@ -1141,7 +1141,8 @@ function profile_edit() : void {
form_selectable_cell(filter_value($rra['name'], '', $url), $i);
form_selectable_cell('' . get_span($profile['step'] * $rra['steps'] * $rra['rows']) . '', $i);
- form_selectable_cell('' . isset($timespans[$rra['timespan']]) ? $timespans[$rra['timespan']] : get_span($rra['timespan']) . '', $i); // @phpstan-ignore-line
+ $retention = $timespans[$rra['timespan']] ?? get_span($rra['timespan']);
+ form_selectable_cell('' . $retention . '', $i);
form_selectable_cell('' . $rra['steps'] . '', $i);
form_selectable_cell('' . $rra['rows'] . '', $i);
diff --git a/graphs.php b/graphs.php
index b2b7fd2c0..a01a767d4 100644
--- a/graphs.php
+++ b/graphs.php
@@ -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();
diff --git a/lib/api_aggregate.php b/lib/api_aggregate.php
index 9c5987cb8..4da91e386 100644
--- a/lib/api_aggregate.php
+++ b/lib/api_aggregate.php
@@ -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) {
diff --git a/lib/utility.php b/lib/utility.php
index 49bb515e4..44938da54 100644
--- a/lib/utility.php
+++ b/lib/utility.php
@@ -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;
}
diff --git a/poller_dsstats.php b/poller_dsstats.php
index 16ab2d751..b8e5d1890 100644
--- a/poller_dsstats.php
+++ b/poller_dsstats.php
@@ -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;
diff --git a/rrdcheck.php b/rrdcheck.php
index 8704eeab6..ed7d686aa 100644
--- a/rrdcheck.php
+++ b/rrdcheck.php
@@ -43,6 +43,7 @@ switch(grv('action')) {
rrdcheck_purge();
+ // Fall through: display the remaining problems after purging.
default:
rrdcheck_display_problems();
}
diff --git a/tests/Unit/DataCollection/Poller/DsstatsPartitionOptionsTest.php b/tests/Unit/DataCollection/Poller/DsstatsPartitionOptionsTest.php
new file mode 100644
index 000000000..5d9e90a17
--- /dev/null
+++ b/tests/Unit/DataCollection/Poller/DsstatsPartitionOptionsTest.php
@@ -0,0 +1,40 @@
+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],
+]);
diff --git a/tests/Unit/Ui/Graph/ProfileRetentionCellTest.php b/tests/Unit/Ui/Graph/ProfileRetentionCellTest.php
new file mode 100644
index 000000000..a81e0954a
--- /dev/null
+++ b/tests/Unit/Ui/Graph/ProfileRetentionCellTest.php
@@ -0,0 +1,42 @@
+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('' . ($configuredLabel ?? '2 Hours') . '');
+})->with(['configured span' => ['Configured span'], 'computed span' => [null]]);