Files
cacti/tests/integration/AggregateTotallingGuardContractTest.php
Thomas VincentandTheWitness d3d99bb08a test(cdef): enforce resolver coverage (#7974)
* test(cdef): enforce resolver coverage

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>

* ci(cdef): pin cache action correctly

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>

* test: isolate CDEF coverage bootstrap

* fix(cdef): fail closed on invalid recursive definitions

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>

* fix(cdef): log corrupt resolver records

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>

* fix(cdef): bound and isolate recursive resolution

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>

* fix(cdef): preserve resolver failure state

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>

* fix(cdef): isolate invalid graph expressions

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>

* fix(cdef): abort unsafe consumer operations

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>

* fix(cdef): close consumer failure paths

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>

* fix(cdef): make aggregate updates atomic

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>

* fix(cdef): keep repair queries auditable

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>

* fix(cdef): bind bulk deletes and escape item types

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>

* fix(cdef): guard corrupt types and repair reporting

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>

* fix(aggregate): propagate push-out failures

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>

* fix(aggregate): document mutable message checks

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>

---------

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
Co-authored-by: TheWitness <thewitness@cacti.net>
2026-09-23 13:38:51 -04:00

93 lines
4.0 KiB
PHP

<?php
/*
+-------------------------------------------------------------------------+
| Copyright (C) 2004-2026 The Cacti Group |
+-------------------------------------------------------------------------+
*/
/*
* Cross-file contract test for the aggregate totalling path (#7216).
*
* Applying the totalling cdef to GPRINT items is safe only while
* rrdtool_function_graph() keeps the empty-cdef guard from #6985.
* The guard skips GPRINT items whose consolidation function does not
* match the data source instead of emitting a bare "CDEF:cdefX=".
* These two invariants must hold together.
*/
$root = dirname(__DIR__, 2);
test('rrd.php keeps the empty cdef guard that makes full totalling safe', function () use ($root) {
$src = file_get_contents($root . '/lib/rrd.php');
expect($src)->not->toBeFalse('Failed to read lib/rrd.php');
$pattern = '/if\s*\(\s*\$cdef_string\s*===\s*\'\'\s*\)\s*\{[^}]*continue;/s';
expect(preg_match($pattern, $src))->toBe(1,
'the empty cdef guard must stay in lib/rrd.php while totalling covers GPRINT items');
});
test('totalling replaces CURRENT_DATA_SOURCE for both total types', function () use ($root) {
$src = file_get_contents($root . '/lib/aggregate.php');
expect($src)->not->toBeFalse('Failed to read lib/aggregate.php');
foreach (['SIMILAR_DATA_SOURCES_NODUPS', 'ALL_DATA_SOURCES_NODUPS'] as $replacement) {
$pattern = "/str_replace\('CURRENT_DATA_SOURCE',\s*'" . $replacement . "'/";
expect(preg_match($pattern, $src))->toBe(1,
"totalling must build the $replacement cdef");
}
});
test('totalling query and cdef update operate on the same item set', function () use ($root) {
$src = file_get_contents($root . '/lib/aggregate.php');
expect($src)->not->toBeFalse('Failed to read lib/aggregate.php');
$fnPos = strpos($src, 'function aggregate_cdef_totalling(');
expect($fnPos)->not->toBeFalse('aggregate_cdef_totalling must exist');
$fnEnd = strpos($src, "\nfunction ", $fnPos + 1);
$body = substr($src, $fnPos, ($fnEnd === false ? strlen($src) : $fnEnd) - $fnPos);
// every selected item gets the new cdef; no filtering between select and update
expect(strpos($body, 'SELECT id, cdef_id'))->not->toBeFalse('item selection missing');
$pattern = '/UPDATE graph_templates_item\s+SET cdef_id\s*=\s*\?\s+WHERE id\s*=\s*\?/s';
expect(preg_match($pattern, $body))->toBe(1,
'cdef update must remain prepared and keyed by item id');
});
test('aggregate caller rolls back and surfaces CDEF totalling failures', function () use ($root) {
$src = file_get_contents($root . '/lib/api_aggregate.php');
expect($src)->not->toBeFalse('Failed to read lib/api_aggregate.php');
$fnPos = strpos($src, 'function aggregate_create_update(');
expect($fnPos)->not->toBeFalse('aggregate_create_update must exist');
$fnEnd = strpos($src, "\nfunction ", $fnPos + 1);
$body = substr($src, $fnPos, ($fnEnd === false ? strlen($src) : $fnEnd) - $fnPos);
expect($body)->toContain('db_begin_transaction()')
->and($body)->toContain('if (!aggregate_cdef_totalling(')
->and($body)->toContain('db_rollback_transaction()')
->and($body)->toContain("raise_message('aggregate_invalid_cdef'")
->and($body)->toContain('db_commit_transaction()');
});
test('aggregate callers preserve existing rows and honor update failures', function () use ($root) {
$api = file_get_contents($root . '/lib/api_aggregate.php');
$graphs = file_get_contents($root . '/graphs.php');
expect($api)->not->toBeFalse()
->and($graphs)->not->toBeFalse();
$pushStart = strpos($api, 'function push_out_aggregates(');
$pushEnd = strpos($api, "\nfunction ", $pushStart + 1);
$pushBody = substr($api, $pushStart, $pushEnd - $pushStart);
expect($pushBody)->not->toContain('DELETE FROM graph_templates_item')
->and($pushBody)->toContain('if (!aggregate_create_update(')
->and($pushBody)->toContain('return false;')
->and($graphs)->toContain('if (!aggregate_create_update($local_graph_id, $member_graphs, $attribs, false))')
->and($graphs)->toContain('db_rollback_transaction()')
->and($graphs)->toContain('db_commit_transaction()');
});