Skip to content

fix(rpn): correct the expression evaluator against rrdtool semantics - #788

Open
somethingwithproof wants to merge 4 commits into
Cacti:developfrom
somethingwithproof:fix/rpn-evaluator
Open

fix(rpn): correct the expression evaluator against rrdtool semantics#788
somethingwithproof wants to merge 4 commits into
Cacti:developfrom
somethingwithproof:fix/rpn-evaluator

Conversation

@somethingwithproof

Copy link
Copy Markdown
Member

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

EXC and REV were both no-ops. Popping a span already reverses it, and both then reversed it back:

// EXC, before
$v1 = pop();  // top
$v2 = pop();  // next
push($v2); push($v1);   // original order restored

[1,2] EXC returned [1,2]; rrdtool gives [2,1]. An expression using either operator to order operands before a non-commutative operator computed the wrong comparison, silently.

AVG raised an uncaught TypeError: Unsupported operand types when '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 yields UNKN.

Both operators now also stop on stack underflow rather than pushing false back and carrying on.

Expression result

thold_calculate_expression() ended with return $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 the EXC/REV bugs made more likely. 1,2,3,+ returned 1 instead of 5.

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

$spectypes = ['CURRENT_DATA_SOURCE', 'CURRENT_GRAPH_MINIMUM_VALUE',
    'CURRENT_GRAPH_MINIMUM_VALUE', ...];

The minimum appeared twice and CURRENT_GRAPH_MAXIMUM_VALUE was absent, although thold_expression_specialtype_rpn() handles it. Expressions using it fell through to Unsupported 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 produced null for a data source with no readings this cycle, then a TypeError from array_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 the thold_functions.php changes 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, and INFO still declares compat = 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.

@somethingwithproof

Copy link
Copy Markdown
Member Author

The four Integration Test failures are an upstream break, not this PR. The workflow checks out Cacti/cacti unpinned, and Install Cacti via CLI dies in core:

PHP Fatal error: Uncaught Error: Call to undefined function __()
  in cacti/lib/functions.php:7973

That is format_cacti_version_text() calling __() before the translation layer is loaded, on Cacti develop at 96033267c. Nothing in this PR touches Cacti core.

#776 pins the checkout to release/1.2.31 and its integration run is green, which is the same failure mode it was opened for. The PHPUnit job here passes.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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_VALUE and 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.

Comment thread thold_functions.php
Comment on lines +682 to +686
$count = thold_expression_rpn_pop($stack);

sort($v, SORT_NUMERIC);
if ($rpn_error || !is_numeric($count) || $count <= 0) {
return;
}

@TheWitness TheWitness left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No composer.jaon in plugins.

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>
somethingwithproof added a commit to somethingwithproof/plugin_thold that referenced this pull request Aug 17, 2026
Same harness as Cacti#773 and Cacti#788, so whichever lands first the others merge cleanly.

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
somethingwithproof added a commit to somethingwithproof/plugin_thold that referenced this pull request Aug 17, 2026
Same harness as Cacti#773 and Cacti#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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

RPN expression evaluator: EXC/REV are no-ops, AVG is fatal on unknown, result read from stack bottom

3 participants