fix(thold): keep zero readings and stop falling back to the wrong data source - #790
fix(thold): keep zero readings and stop falling back to the wrong data source#790somethingwithproof wants to merge 4 commits into
Conversation
|
The four Integration Test failures are the upstream break, not this PR. The workflow checks out
|
There was a problem hiding this comment.
Pull request overview
This PR fixes two correctness issues in thold’s tag/data-source substitution paths that can surface in normal operation (zero readings being blanked, and unknown “data source names” returning the first DS value). It also introduces a PHPUnit-based harness plus CI coverage gating to lock the fixes in.
Changes:
- Fix
thold_str_replace()so0/'0'survive substitutions instead of becoming empty. - Fix
get_current_value()to (a) treat anarray_search()miss correctly and (b) handle missing/unreadable RRDlastoutput without aTypeError. - Add PHPUnit test harness + Docker runner + GitHub Actions workflow with changed-lines coverage enforcement.
Reviewed changes
Copilot reviewed 13 out of 15 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
thold_functions.php |
Corrects substitution edge cases in get_current_value() and thold_str_replace() to prevent wrong/blank values. |
tests/Unit/TholdStrReplaceTest.php |
Unit coverage for zero/absent substitution behavior. |
tests/Unit/GetCurrentValueTest.php |
Unit coverage for unknown DS lookup and unreadable RRD behavior. |
tests/TestCase.php |
Shared PHPUnit base providing consistent stub reset and plugin source loading. |
tests/Support/CactiStub.php |
Framework-function recording/programmable stub to unit-test procedural plugin code. |
tests/fixtures/optional-core-functions.php |
Optional core-function shims for exercising function_exists() branches. |
tests/fixtures/cacti-lib/variables.php |
Minimal fixture file to satisfy runtime include_once() expectations. |
tests/docker/Dockerfile |
Reproducible PHP 8.1 + pcov test runner image. |
tests/docker/docker-compose.yml |
Local “mirror” invocation of the CI unit-test job. |
tests/bootstrap.php |
Test bootstrap providing stubbed Cacti globals/functions and plugin loader. |
tests/bin/patch-coverage.php |
Script enforcing statement coverage for changed PHP lines only. |
phpunit.xml |
PHPUnit config targeting thold_functions.php coverage and strictness flags. |
composer.json |
Adds PHPUnit dependency + scripts for local/CI test running. |
.gitignore |
Ignores vendor, coverage, and PHPUnit cache artifacts. |
.github/workflows/php-unit-tests.yml |
CI job to run lint + PHPUnit with coverage + enforce changed-line coverage. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| if (!function_exists('db_qstr')) { | ||
| function db_qstr($string) { | ||
| return "'" . str_replace("'", "''", (string) $string) . "'"; | ||
| } | ||
| } |
18868d4 to
65380e0
Compare
thold_str_replace() treated 0 and '0' as absent, so an alert for a value that had dropped to zero rendered as "Current value is " with a blank, and a trigger command invoked as --value <CURRENTVALUE> lost the argument and shifted the ones after it. Refs Cacti#787 Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
array_search() reports a miss as false, so a guard written against null let it through and $result['values'][false] read index 0. A lookup for a data source that does not exist returned the first one's value, which the caller then compared against the threshold bounds. Reached today from thold_expression_specialtype_rpn() and the CDEF substitutions, which pass column names such as upper_limit rather than data source names. Those call sites still need to read the real column; this only stops them silently receiving another metric. Refs Cacti#787 Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
Adopts the conventions from Cacti core: tests/bootstrap-unit.php, tests/Helpers for the stubs, a phpunit.xml carrying error_reporting -1 and CACTI_TEST_BOOTSTRAP, and composer lint / test / test:coverage scripts so CI runs the same commands a developer does. The dev toolchain is Cacti's, pinned to the same platform php 8.1.0. Cacti core runs Pest and this suite does not, because pest ^2 does not resolve on PHP 8.1 -- the platform Cacti's own composer.json pins. Releases up to v2.36.0 conflict with phpunit 10.5.62 and later, every earlier 10.x release is blocked by advisory PKSA-z3gr-8qht-p93v, and v2.36.1, which does resolve, requires PHP 8.2. The stack installs on 8.2 and above; 8.1 is the floor this plugin's CI matrix targets. The tests are written in the plain PHPUnit class style that Cacti's tests/Pest.php explicitly supports, so they run unchanged under Pest wherever it is installable. Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
65380e0 to
c887db6
Compare
Fixes #787.
Two substitution helpers returned wrong values for inputs that occur in normal operation. Tests were written first and fail on
develop.A zero reading was blanked
empty('0')is true, so0and'0'both became an empty string.thold_str_replace()performs every tag substitution in notification bodies, subjects and trigger commands, so:Current value iswith a blank — exactly the case an operator most needs to see;--value <CURRENTVALUE>lost the argument entirely and shifted the ones after it.Only
nullnow counts as absent, which is what the guard was for.An unknown data source returned the first one's value
array_search()reports a miss asfalse, nevernull, so the guard never fired.$result['values'][false]is$result['values'][0]under PHP's bool-to-int key cast, so a lookup for a data source that does not exist quietly returned the first data source's value — which the caller then compares against the threshold bounds.This is reachable today:
thold_expression_specialtype_rpn()and the CDEF substitutions callget_current_value()withupper_limit,lower_limit,rrd_minimum,rrd_maximumandhdd_total, none of which are data source names.Note the scope: this stops those call sites receiving another metric's value. It does not make them read the column they actually want — they still need to look up
graph_templates_graph.upper_limitand friends. Worth a follow-up; I did not want to fold a behaviour change into a correctness fix.While in the same function:
thold_rrd_last()returns whateverrrdtool lastprinted, and a missing or unreadable RRD prints nothing. That empty string reached the timestamp arithmetic and threwTypeError: Unsupported operand types: string / int. The existing guard only handled-1.Tests
20 tests, 100% of the changed lines covered.
The harness commit is the same one as #773 and #788, byte-identical, so whichever lands first the others merge cleanly. Once any of them merges this PR's diff is just the two
thold_functions.phpchanges and its own tests.