fix(rpn): correct the expression evaluator against rrdtool semantics - #788
fix(rpn): correct the expression evaluator against rrdtool semantics#788somethingwithproof wants to merge 4 commits into
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.
| $count = thold_expression_rpn_pop($stack); | ||
|
|
||
| sort($v, SORT_NUMERIC); | ||
| if ($rpn_error || !is_numeric($count) || $count <= 0) { | ||
| return; | ||
| } |
TheWitness
left a comment
There was a problem hiding this comment.
No composer.jaon in plugins.
3f6ae2b to
87c1ed7
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>
87c1ed7 to
17ce93a
Compare
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.