mirror of
https://github.com/Cacti/cacti.git
synced 2026-10-26 21:18:40 +09:00
* 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>
269 lines
11 KiB
PHP
269 lines
11 KiB
PHP
<?php
|
|
/*
|
|
+-------------------------------------------------------------------------+
|
|
| Copyright (C) 2004-2026 The Cacti Group |
|
|
| |
|
|
| This program is free software; you can redistribute it and/or |
|
|
| modify it under the terms of the GNU General Public License |
|
|
| as published by the Free Software Foundation; either version 2 |
|
|
| of the License, or (at your option) any later version. |
|
|
| |
|
|
| This program is distributed in the hope that it will be useful, |
|
|
| but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
|
| MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
|
| GNU General Public License for more details. |
|
|
+-------------------------------------------------------------------------+
|
|
| Cacti: The Complete RRDTool-based Graphing Solution |
|
|
+-------------------------------------------------------------------------+
|
|
*/
|
|
|
|
beforeEach(function () : void {
|
|
$GLOBALS['cdef_test_items'] = [];
|
|
$GLOBALS['cdef_test_lists'] = [];
|
|
$GLOBALS['cdef_test_names'] = [];
|
|
$GLOBALS['cdef_test_queries'] = [];
|
|
$GLOBALS['cdef_test_logs'] = [];
|
|
$GLOBALS['cdef_test_assoc_callback'] = null;
|
|
$GLOBALS['cdef_test_cell_callback'] = null;
|
|
$GLOBALS['cdef_functions'] = [7 => 'Maximum'];
|
|
$GLOBALS['cdef_operators'] = [3 => '*'];
|
|
});
|
|
|
|
test('CDEF item names cover every supported item type and the unknown fallback', function () : void {
|
|
$GLOBALS['cdef_test_items'] = [
|
|
1 => ['type' => '1', 'value' => 7],
|
|
2 => ['type' => '2', 'value' => 3],
|
|
3 => ['type' => '4', 'value' => 'CURRENT_DATA_SOURCE'],
|
|
4 => ['type' => '5', 'value' => 42],
|
|
5 => ['type' => '6', 'value' => '8'],
|
|
6 => ['type' => '99', 'value' => 'ignored'],
|
|
7 => ['type' => '1', 'value' => 999],
|
|
8 => ['type' => '2', 'value' => 999],
|
|
9 => ['type' => '1'],
|
|
10 => ['type' => '5', 'value' => 999],
|
|
11 => ['type' => '1', 'value' => 59],
|
|
];
|
|
$GLOBALS['cdef_test_names'][42] = 'Nested CDEF';
|
|
|
|
expect(get_cdef_item_name(1))->toBe('Maximum')
|
|
->and(get_cdef_item_name(2))->toBe('*')
|
|
->and(get_cdef_item_name(3))->toBe('CURRENT_DATA_SOURCE')
|
|
->and(get_cdef_item_name(4))->toBe('Nested CDEF')
|
|
->and(get_cdef_item_name(5))->toBe('8')
|
|
->and(get_cdef_item_name(6))->toBeNull()
|
|
->and(get_cdef_item_name(7))->toBeNull()
|
|
->and(get_cdef_item_name(8))->toBeNull()
|
|
->and(get_cdef_item_name(9))->toBeNull()
|
|
->and(get_cdef_item_name(10))->toBeNull()
|
|
->and(get_cdef_item_name(11))->toBeNull()
|
|
->and(get_cdef_item_name(999))->toBeNull();
|
|
|
|
$messages = implode("\n", array_column($GLOBALS['cdef_test_logs'], 0));
|
|
expect($messages)->toContain('unknown type')
|
|
->and($messages)->toContain('unknown function')
|
|
->and($messages)->toContain('unknown operator')
|
|
->and($messages)->toContain('missing definition')
|
|
->and($messages)->toContain('missing or corrupt');
|
|
|
|
expect($GLOBALS['cdef_test_queries'][0][1])->toBe([1])
|
|
->and($GLOBALS['cdef_test_queries'][4][1])->toBe([42]);
|
|
});
|
|
|
|
test('CDEF resolution handles empty, ordered, and recursively nested definitions', function () : void {
|
|
$GLOBALS['cdef_test_items'] = [
|
|
10 => ['type' => '4', 'value' => 'CURRENT_DATA_SOURCE'],
|
|
11 => ['type' => '6', 'value' => '8'],
|
|
12 => ['type' => '2', 'value' => 3],
|
|
20 => ['type' => '6', 'value' => '2'],
|
|
7 => ['type' => '1', 'value' => 999],
|
|
];
|
|
$GLOBALS['cdef_test_names'][1] = 1;
|
|
$GLOBALS['cdef_test_lists'] = [
|
|
1 => [],
|
|
2 => [
|
|
['id' => 10, 'type' => '4', 'value' => 'CURRENT_DATA_SOURCE'],
|
|
['id' => 11, 'type' => '6', 'value' => '8'],
|
|
['id' => 12, 'type' => '2', 'value' => '3'],
|
|
],
|
|
3 => [
|
|
['id' => 0, 'type' => '5', 'value' => '2'],
|
|
['id' => 20, 'type' => '6', 'value' => '2'],
|
|
],
|
|
4 => [['id' => 0, 'type' => '5', 'value' => '999']],
|
|
5 => [['id' => 0, 'type' => '5', 'value' => '5']],
|
|
6 => [['id' => 0, 'type' => '5', 'value' => '7']],
|
|
7 => [['id' => 0, 'type' => '5', 'value' => '6']],
|
|
8 => [['id' => 7, 'type' => '1', 'value' => '999']],
|
|
9 => [
|
|
['id' => 0, 'type' => '5', 'value' => '2'],
|
|
['id' => 0, 'type' => '5', 'value' => '2'],
|
|
],
|
|
];
|
|
|
|
expect(get_cdef(1))->toBe('')
|
|
->and(get_cdef(2))->toBe('CURRENT_DATA_SOURCE,8,*')
|
|
->and(get_cdef(3))->toBe('CURRENT_DATA_SOURCE,8,*,2')
|
|
->and(get_cdef(4))->toBeNull()
|
|
->and(get_cdef(5))->toBeNull()
|
|
->and(get_cdef(6))->toBeNull()
|
|
->and(get_cdef(8))->toBeNull()
|
|
->and(get_cdef(9))->toBe('CURRENT_DATA_SOURCE,8,*,CURRENT_DATA_SOURCE,8,*');
|
|
|
|
expect($GLOBALS['cdef_test_queries'][0][1])->toBe([1])
|
|
->and($GLOBALS['cdef_test_queries'][0][0])->toContain('ORDER BY sequence');
|
|
|
|
$messages = implode("\n", array_column($GLOBALS['cdef_test_logs'], 0));
|
|
expect($messages)->toContain('recursive cycle')
|
|
->and($messages)->toContain('unknown function');
|
|
});
|
|
|
|
test('CDEF resolution rejects nesting deeper than the safety limit', function () : void {
|
|
for ($id = 1; $id <= 65; $id++) {
|
|
$GLOBALS['cdef_test_lists'][$id] = [['id' => 0, 'type' => '5', 'value' => $id + 1]];
|
|
}
|
|
|
|
expect(get_cdef(1))->toBeNull()
|
|
->and($GLOBALS['cdef_test_logs'][0][0])->toContain('nesting depth');
|
|
});
|
|
|
|
test('CDEF resolution accepts the maximum safe nesting depth', function () : void {
|
|
$GLOBALS['cdef_test_items'][10] = ['type' => '4', 'value' => 'CURRENT_DATA_SOURCE'];
|
|
|
|
for ($id = 1000; $id < 1063; $id++) {
|
|
$GLOBALS['cdef_test_lists'][$id] = [['id' => 0, 'type' => '5', 'value' => $id + 1]];
|
|
}
|
|
|
|
$GLOBALS['cdef_test_lists'][1063] = [['id' => 10, 'type' => '4', 'value' => 'CURRENT_DATA_SOURCE']];
|
|
|
|
expect(get_cdef(1000))->toBe('CURRENT_DATA_SOURCE');
|
|
});
|
|
|
|
test('CDEF resolution rejects an excessive expanded output size', function () : void {
|
|
$GLOBALS['cdef_test_items'][10] = ['type' => '4', 'value' => 'CURRENT_DATA_SOURCE'];
|
|
|
|
for ($id = 100; $id < 117; $id++) {
|
|
$GLOBALS['cdef_test_lists'][$id] = [
|
|
['id' => 0, 'type' => '5', 'value' => $id + 1],
|
|
['id' => 0, 'type' => '5', 'value' => $id + 1],
|
|
];
|
|
}
|
|
|
|
$GLOBALS['cdef_test_lists'][117] = [['id' => 10, 'type' => '4', 'value' => 'CURRENT_DATA_SOURCE']];
|
|
|
|
expect(get_cdef(100))->toBeNull()
|
|
->and($GLOBALS['cdef_test_logs'])->not->toBeEmpty()
|
|
->and($GLOBALS['cdef_test_logs'][count($GLOBALS['cdef_test_logs']) - 1][0])->toContain('output budget');
|
|
});
|
|
|
|
test('CDEF resolution rejects too many unique definitions', function () : void {
|
|
$GLOBALS['cdef_test_items'][10] = ['type' => '4', 'value' => 'CURRENT_DATA_SOURCE'];
|
|
$GLOBALS['cdef_test_lists'][2000] = [];
|
|
|
|
for ($id = 3000; $id < 7097; $id++) {
|
|
$GLOBALS['cdef_test_lists'][2000][] = ['id' => 0, 'type' => '5', 'value' => $id];
|
|
$GLOBALS['cdef_test_lists'][$id] = [['id' => 10, 'type' => '4', 'value' => 'CURRENT_DATA_SOURCE']];
|
|
}
|
|
|
|
expect(get_cdef(2000))->toBeNull()
|
|
->and($GLOBALS['cdef_test_logs'][0][0])->toContain('expansion budget');
|
|
});
|
|
|
|
test('CDEF resolution bounds a single stored item value', function () : void {
|
|
$GLOBALS['cdef_test_items'][40] = ['type' => '6', 'value' => str_repeat('x', 1048577)];
|
|
$GLOBALS['cdef_test_lists'][40] = [['id' => 40, 'type' => '6', 'value' => str_repeat('x', 1048577)]];
|
|
|
|
expect(get_cdef(40))->toBeNull()
|
|
->and($GLOBALS['cdef_test_logs'][0][0])->toContain('output budget');
|
|
});
|
|
|
|
test('CDEF resolution stops caching without rejecting a valid deep expression', function () : void {
|
|
$GLOBALS['cdef_test_items'][50] = ['type' => '6', 'value' => str_repeat('x', 200000)];
|
|
|
|
for ($id = 5000; $id < 5063; $id++) {
|
|
$GLOBALS['cdef_test_lists'][$id] = [['id' => 0, 'type' => '5', 'value' => $id + 1]];
|
|
}
|
|
|
|
$GLOBALS['cdef_test_lists'][5063] = [['id' => 50, 'type' => '6', 'value' => str_repeat('x', 200000)]];
|
|
|
|
expect(get_cdef(5000))->toBe(str_repeat('x', 200000))
|
|
->and(implode("\n", array_column($GLOBALS['cdef_test_logs'], 0)))->not->toContain('cache budget');
|
|
});
|
|
|
|
test('CDEF resolution distinguishes query errors from missing and empty definitions', function () : void {
|
|
$GLOBALS['cdef_test_names'][3] = 3;
|
|
$GLOBALS['cdef_test_assoc_callback'] = static function (string $sql, array $params) : mixed {
|
|
if (str_contains($sql, 'FROM cdef_items') && (int) $params[0] === 1) {
|
|
return false;
|
|
}
|
|
|
|
if (str_contains($sql, 'FROM cdef WHERE id') && (int) $params[0] === 2) {
|
|
return false;
|
|
}
|
|
|
|
return null;
|
|
};
|
|
|
|
expect(get_cdef(1))->toBeNull()
|
|
->and(get_cdef(2))->toBeNull()
|
|
->and(get_cdef(3))->toBe('')
|
|
->and(get_cdef(4))->toBeNull();
|
|
|
|
$messages = implode("\n", array_column($GLOBALS['cdef_test_logs'], 0));
|
|
expect($messages)->toContain('Unable to load CDEF 1')
|
|
->and($messages)->toContain('Unable to verify CDEF 2')
|
|
->and($messages)->toContain('CDEF 4 does not exist');
|
|
});
|
|
|
|
test('CDEF deletion dependency checks cover graph, aggregate, nested, and database failures', function () : void {
|
|
$counts = [];
|
|
$GLOBALS['cdef_test_assoc_callback'] = static function (string $sql, array $params) use (&$counts) : mixed {
|
|
foreach ($counts as $table => $value) {
|
|
if (str_contains($sql, "FROM $table")) {
|
|
return $value === false ? false : ($value > 0 ? [['cdef_id' => $params[0]]] : []);
|
|
}
|
|
}
|
|
|
|
return str_contains($sql, 'SELECT DISTINCT cdef_id') ? [] : null;
|
|
};
|
|
|
|
expect(cdef_is_in_use(10, [10]))->toBeFalse();
|
|
|
|
foreach (['graph_templates_item', 'aggregate_graph_templates_item', 'aggregate_graphs_graph_item'] as $table) {
|
|
$counts = [$table => 1];
|
|
expect(cdef_is_in_use(10, [10]))->toBeTrue();
|
|
}
|
|
|
|
$counts = ['graph_templates_item' => false];
|
|
expect(cdef_is_in_use(10, [10]))->toBeTrue();
|
|
|
|
$counts = [];
|
|
$GLOBALS['cdef_test_assoc_callback'] = static fn (string $sql, array $params) : mixed => str_contains($sql, 'SELECT DISTINCT cdef_id') ? [['cdef_id' => 20]] : null;
|
|
expect(cdef_is_in_use(10, [10]))->toBeTrue()
|
|
->and(cdef_is_in_use(10, [10, 20]))->toBeFalse();
|
|
|
|
$GLOBALS['cdef_test_assoc_callback'] = static fn (string $sql, array $params) : mixed => str_contains($sql, 'SELECT DISTINCT cdef_id') ? false : null;
|
|
expect(cdef_is_in_use(10, [10]))->toBeTrue();
|
|
});
|
|
|
|
test('empty nested definitions and stored items fail closed', function () : void {
|
|
$GLOBALS['cdef_test_items'] = [
|
|
10 => ['type' => '4', 'value' => 'CURRENT_DATA_SOURCE'],
|
|
11 => ['type' => '6', 'value' => '8'],
|
|
30 => ['type' => '6', 'value' => ''],
|
|
31 => ['type' => '4', 'value' => ''],
|
|
];
|
|
$GLOBALS['cdef_test_names'][20] = 20;
|
|
$GLOBALS['cdef_test_lists'] = [
|
|
20 => [],
|
|
21 => [['id' => 0, 'type' => '5', 'value' => '20'], ['id' => 10, 'type' => '4', 'value' => 'CURRENT_DATA_SOURCE']],
|
|
22 => [['id' => 10, 'type' => '4', 'value' => 'CURRENT_DATA_SOURCE'], ['id' => 0, 'type' => '5', 'value' => '20'], ['id' => 11, 'type' => '6', 'value' => '8']],
|
|
23 => [['id' => 10, 'type' => '4', 'value' => 'CURRENT_DATA_SOURCE'], ['id' => 0, 'type' => '5', 'value' => '20']],
|
|
24 => [['id' => 10, 'type' => '4', 'value' => 'CURRENT_DATA_SOURCE'], ['id' => 30, 'type' => '6', 'value' => ''], ['id' => 31, 'type' => '4', 'value' => ''], ['id' => 11, 'type' => '6', 'value' => '8']],
|
|
];
|
|
|
|
expect(get_cdef(21))->toBeNull()
|
|
->and(get_cdef(22))->toBeNull()
|
|
->and(get_cdef(23))->toBeNull()
|
|
->and(get_cdef(24))->toBeNull();
|
|
});
|