fix(rpn): correct the expression evaluator against rrdtool semantics - #788
Conversation
|
The four Integration Test failures are an upstream break, not this PR. The workflow checks out That is #776 pins the checkout to |
There was a problem hiding this comment.
Pull request overview
This PR corrects several RPN-expression evaluation behaviors in thold_functions.php to better match RRDtool semantics (stack/set operators, special token dispatch, expression result selection, and safer CDEF division/modulo handling). It also introduces a PHPUnit-based unit-test harness with Docker + GitHub Actions CI to enforce changed-line coverage for the evaluator.
Changes:
- Fix stack/set operator behavior (e.g.,
EXC,REV,AVG) and make expression evaluation return the top-of-stack while rejecting unbalanced expressions. - Fix special-token dispatch for
CURRENT_GRAPH_MAXIMUM_VALUEand harden expression evaluation when a data source has no readings in the current cycle. - Add PHPUnit test suite + Docker runner + GitHub Actions workflow, including a “patch coverage” gate for changed PHP lines.
Reviewed changes
Copilot reviewed 14 out of 16 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
thold_functions.php |
Corrects RPN operator semantics, fixes special-token dispatch, hardens expression/CDEF evaluation edge cases. |
tests/Unit/TholdRpnCdefTest.php |
Adds unit coverage for CDEF arithmetic, including division/modulo by zero behavior. |
tests/Unit/TholdExpressionStackOpsTest.php |
Adds unit coverage for stack + set operators against RRDtool-like semantics. |
tests/Unit/TholdCalculateExpressionTest.php |
Adds end-to-end coverage for expression evaluation and special-token dispatch. |
tests/TestCase.php |
Defines shared PHPUnit base test case and plugin source loader helper. |
tests/Support/CactiStub.php |
Adds a recording stub for Cacti framework globals used by the plugin. |
tests/fixtures/optional-core-functions.php |
Adds optional-core function fallbacks to exercise function_exists() branches in isolation. |
tests/fixtures/cacti-lib/variables.php |
Provides a minimal file to satisfy runtime includes during tests. |
tests/docker/Dockerfile |
Provides a reproducible PHP 8.1 + pcov test runner image. |
tests/docker/docker-compose.yml |
Adds a local docker-compose entrypoint mirroring CI. |
tests/bootstrap.php |
Boots the test environment and defines Cacti-function shims. |
tests/bin/patch-coverage.php |
Implements changed-line coverage enforcement using git diff + Clover. |
phpunit.xml |
Configures PHPUnit and scopes coverage to thold_functions.php. |
composer.json |
Adds PHPUnit dependency and test scripts. |
.gitignore |
Ignores Composer/vendor and PHPUnit/coverage artifacts. |
.github/workflows/php-unit-tests.yml |
Adds CI job to run tests in Docker and enforce patch coverage on PRs. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
TheWitness
left a comment
There was a problem hiding this comment.
No composer.jaon in plugins.
87c1ed7 to
17ce93a
Compare
EXC and REV were both no-ops, because popping already reverses the span and the code then reversed it back. AVG raised an uncaught TypeError when an unknown sample reached its running total, killing the poller mid-cycle; it now skips unknowns and yields UNKN when every sample is unknown, as rrdtool does. Refs Cacti#782 Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
…token CURRENT_GRAPH_MAXIMUM_VALUE was absent from the dispatch list while CURRENT_GRAPH_MINIMUM_VALUE appeared twice, so every expression using it fell through to Unsupported Field and evaluated to zero. The result was then read from the bottom of the stack rather than the top, so an unbalanced expression silently returned its first operand instead of reporting the authoring error. Refs Cacti#782 Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
… zero Modulo by zero was unguarded and fatal on PHP 8. Division by zero returned -1, which the caller compares against the threshold bounds as though it were a reading; the function already uses an empty string for operands it cannot use. Refs Cacti#782 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>
17ce93a to
abc9a37
Compare
|
Review follow-up: TheWitness feedback is addressed—the plugin diff contains no composer.json, composer.lock, or vendor directory, and tests run through Cacti’s Composer-managed Pest toolchain. Copilot’s fractional set-count finding is fixed with a regression test, and its review thread is resolved. Current CI is rerunning on the updated branch. |
…or (#791) * test: add the PHP 8.1 unit-test harness Same harness as #773 and #788, with gmp added to the image so the 64-bit counter arithmetic can be tested exactly. Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * fix(thold): correct the counter delta and the percent denominator A previous counter reading of exactly zero was treated as no reading at all, so the first interval after a device reboot reported a rate of zero. The wrap modulus was 2^32-1 and 2^64-1 rather than 2^32 and 2^64, losing one count per wrap, and the 64-bit literal exceeded PHP_INT_MAX so it was parsed as a float and lost about eleven bits before the subtraction. The percent-of denominator was cast to int, so a denominator below one truncated to zero and forced the result to zero, keeping any configured low threshold in permanent breach. Refs #785 Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * fix(daemon): store the previous reading in oldvalue, not a timestamp When a data source produced no sample this cycle the daemon wrote $currenttime - $rrd_step into oldvalue, so the next poll computed a delta against a Unix timestamp, took the overflow branch and fabricated a rate in the billions. The non-daemon path already carries the previous oldvalue forward; this matches it. Refs #785 Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * test: follow Cacti's test layout and composer scripts 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> * ci: keep plugin PR integration checks on pinned Cacti * fix(counter): handle non-integer 64-bit readings safely * ci: bound package index refreshes --------- Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
…a source (#790) * test: add the PHP 8.1 unit-test harness Same harness as #773 and #788, so whichever lands first the others merge cleanly. Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * fix(thold): keep a zero reading through tag substitution 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 #787 Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * fix(thold): return zero when the requested data source is absent 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 #787 Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * test: follow Cacti's test layout and composer scripts 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> * ci: keep plugin PR integration checks on pinned Cacti * test: match the optional Cacti database stub signature * ci: bound package index refreshes --------- Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
Fixes #782.
Each defect below was confirmed by running the function, not by reading it. The tests were written first and fail on
develop.Stack and set operators
EXCandREVwere both no-ops. Popping a span already reverses it, and both then reversed it back:[1,2] EXCreturned[1,2]; rrdtool gives[2,1]. An expression using either operator to order operands before a non-commutative operator computed the wrong comparison, silently.AVGraised an uncaughtTypeError: Unsupported operand typeswhen'U'or'NAN'reached$total += $v. That is a fatal in the poller process, so every threshold after it in the run went unevaluated. It now follows rrdtool: unknown samples are skipped, the average is over the known ones, and an all-unknown span yieldsUNKN.Both operators now also stop on stack underflow rather than pushing
falseback and carrying on.Expression result
thold_calculate_expression()ended withreturn $stack[0]— the bottom of the stack. rrdtool yields the top. For a well-formed expression these are the same element, so this only bit when the stack was unbalanced, which theEXC/REVbugs made more likely.1,2,3,+returned1instead of5.It now returns the top, and an expression that does not reduce to a single value is reported as the authoring error it is rather than quietly returning an operand.
Missing dispatch entry
The minimum appeared twice and
CURRENT_GRAPH_MAXIMUM_VALUEwas absent, althoughthold_expression_specialtype_rpn()handles it. Expressions using it fell through toUnsupported Field, set$rpn_error, and returned 0 — so a low threshold on such an expression alerted permanently.CDEF division and modulo by zero
thold_rpn()guarded division but not modulo, so%by zero was fatal on PHP 8. The division guard returned-1, a plausible-looking number that is then compared against the bounds; the function already uses''as its sentinel for operands it cannot use, so both now do that.Also
$data_sources = $rrd_reindexed[$thold['local_data_id']]was an unguarded array read that producednullfor a data source with no readings this cycle, then aTypeErrorfromarray_key_exists(). It now defaults, and the no-op element-by-element copy that followed it is gone.Tests
43 tests covering the stack, set and CDEF operators plus end-to-end expression evaluation. Every changed line is covered.
The harness (
tests/,composer.json,phpunit.xml, the unit-test workflow) is the same one added in #773, byte-identical, so whichever lands first the other merges cleanly. If #773 goes in first this PR's diff shrinks to thethold_functions.phpchanges and its own test files.PHP version
The new code uses PHP 8 idioms (typed signatures,
??, short array syntax) to match the 8.1–8.4 CI matrix and Cacti 1.2.31's"php": ">=8.0". Worth noting that the plugin contained no PHP 8-only syntax before this, andINFOstill declarescompat = 1.2.25— if running under an older Cacti on PHP 7 is meant to be supported, say so and I will keep these functions 7.4-compatible.