Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 18 additions & 10 deletions .github/workflows/plugin-ci-workflow.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,21 +35,12 @@ jobs:
integration-test:
runs-on: ${{ matrix.os }}

# A failure against the pinned release is a real failure. The develop entry
# is advisory: it is how a core regression becomes visible here, but it must
# not turn the plugin's own pull requests red.
continue-on-error: ${{ matrix.cacti != 'release/1.2.31' }}

strategy:
fail-fast: false
matrix:
php: ['8.1', '8.2', '8.3', '8.4']
os: [ubuntu-latest]
cacti: ['release/1.2.31']
include:
- php: '8.4'
os: ubuntu-latest
cacti: 'develop'

services:
mariadb:
Expand Down Expand Up @@ -95,7 +86,24 @@ jobs:
echo "PHP_BINARY=$(command -v php)" >> "$GITHUB_ENV"

- name: Run apt-get update
run: sudo apt-get update
run: |
for attempt in 1 2 3; do
if sudo timeout 3m apt-get \
-o Dpkg::Lock::Timeout=60 \
-o Acquire::Retries=3 \
-o Acquire::http::Timeout=30 \
-o Acquire::https::Timeout=30 \
update; then
exit 0
fi

if [ "$attempt" -lt 3 ]; then
sleep 10
fi
done

echo 'apt-get update failed after three bounded attempts.' >&2
exit 1

- name: Install System Dependencies
run: sudo apt-get install -y apache2 snmp snmpd rrdtool fping
Expand Down
6 changes: 3 additions & 3 deletions tests/Helpers/CactiStubs.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ final class CactiStubs {
/**
* Every Cacti function call the plugin made, in order.
*
* @var array<int, array{fn: string, sql: string, params: array<int, mixed>}>
* @var array<int, array{fn: string, sql: string, params: array<int|string, mixed>}>
*/
public static $calls = [];

Expand Down Expand Up @@ -107,7 +107,7 @@ public static function reset() {
*
* @param string $fn Cacti function name.
* @param string $sql SQL text, or '' for non-query calls.
* @param array<int, mixed> $params Bound parameters, if any.
* @param array<int|string, mixed> $params Bound parameters, if any.
*
* @return void
*/
Expand Down Expand Up @@ -197,7 +197,7 @@ public static function nextReturn($fn, $default, $sql = '') {
*
* @param string $fn Cacti function name.
*
* @return array<int, array{fn: string, sql: string, params: array<int, mixed>}>
* @return array<int, array{fn: string, sql: string, params: array<int|string, mixed>}>
*/
public static function callsTo($fn) {
return array_values(array_filter(self::$calls, function ($call) use ($fn) {
Expand Down
205 changes: 205 additions & 0 deletions tests/Helpers/ThresholdOutcome.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
<?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. |
+-------------------------------------------------------------------------+
| Cacti: The Complete RRDtool-based Graphing Solution |
+-------------------------------------------------------------------------+
| http://www.cacti.net/ |
+-------------------------------------------------------------------------+
*/

/**
* What one poll of thold_check_threshold() emitted.
*
* Reads the recorded calls rather than the SQL text wherever it can, so a
* reworded query does not break a test that is about behaviour.
*/
final class ThresholdOutcome {
/**
* The threshold row as the function left it.
*
* @var array<string, mixed>
*/
public $thold;

/**
* @param array<string, mixed> $thold
*/
public function __construct(array $thold) {
$this->thold = $thold;
}

/**
* Subject lines of the mail that was sent, in order.
*
* @return array<int, string>
*/
public function subjects() {
return array_column(CactiStubs::$mail, 'subject');
}

/**
* Recipients of the mail that was sent, in order.
*
* @return array<int, string>
*/
public function recipients() {
return array_column(CactiStubs::$mail, 'to');
}

/**
* @return int
*/
public function mailCount() {
return count(CactiStubs::$mail);
}

/**
* Status codes written to plugin_thold_log, in order.
*
* The log row goes through sql_save(), so the status is available as data
* rather than having to be parsed back out of a query.
*
* @return array<int, int>
*/
public function logStatuses() {
$statuses = [];

foreach (CactiStubs::callsTo('sql_save') as $call) {
if ($call['sql'] === 'plugin_thold_log' && isset($call['params']['status'])) {
$statuses[] = (int) $call['params']['status'];
}
}

return $statuses;
}

/**
* @return int
*/
public function trapCount() {
return count(CactiStubs::callsTo('cacti_snmp_send'));
}

/**
* Whether the run marked the threshold as having changed state.
*
* @return bool
*/
public function touchedLastChanged() {
foreach (CactiStubs::callsTo('db_execute_prepared') as $call) {
if (strpos($call['sql'], 'lastchanged = NOW()') !== false) {
return true;
}
}

return false;
}

/**
* Whether the run set the acknowledgment flag.
*
* @return bool
*/
public function acknowledged() {
foreach (CactiStubs::callsTo('db_execute_prepared') as $call) {
if (strpos($call['sql'], 'acknowledgment = "on"') !== false) {
return true;
}
}

return false;
}

/**
* Columns the run wrote to thold_data, resolved to their values.
*
* The statements mix placeholders and literals in the same SET clause, so
* the clause is parsed and each "?" resolved against the bound parameters
* in order. Returns the merge of every such statement, later writes last.
*
* @return array<string, string>
*/
public function persistedColumns() {
$columns = [];

foreach (CactiStubs::callsTo('db_execute_prepared') as $call) {
if (strpos($call['sql'], 'UPDATE thold_data') === false) {
continue;
}

if (!preg_match('/SET\s+(.*?)\s+WHERE/s', $call['sql'], $clause)) {
continue;
}

$position = 0;

foreach (explode(',', $clause[1]) as $assignment) {
$parts = explode('=', $assignment, 2);

if (count($parts) !== 2) {
continue;
}

$name = trim($parts[0]);
$value = trim($parts[1]);

if ($value === '?') {
$value = isset($call['params'][$position]) ? (string) $call['params'][$position] : '';
$position++;
}

$columns[$name] = trim($value, '"\'');
}
}

return $columns;
}

/**
* The alert state the run persisted, or null when it wrote none.
*
* @return int|null
*/
public function persistedAlertState() {
$columns = $this->persistedColumns();

return isset($columns['thold_alert']) ? (int) $columns['thold_alert'] : null;
}

/**
* The fail counts the run persisted, or null when it wrote neither.
*
* @return array{alert: int|null, warning: int|null}|null
*/
public function persistedFailCounts() {
$columns = $this->persistedColumns();

if (!isset($columns['thold_fail_count']) && !isset($columns['thold_warning_fail_count'])) {
return null;
}

return [
'alert' => isset($columns['thold_fail_count']) ? (int) $columns['thold_fail_count'] : null,
'warning' => isset($columns['thold_warning_fail_count']) ? (int) $columns['thold_warning_fail_count'] : null,
];
}

/**
* Whether the run did nothing at all beyond reading.
*
* @return bool
*/
public function isSilent() {
return $this->mailCount() === 0
&& $this->logStatuses() === []
&& $this->trapCount() === 0
&& CactiStubs::callsTo('thold_command_execution') === [];
}
}
Loading
Loading