You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

655 lines
22 KiB

1 year ago
<?php
/*
** Zabbix
** Copyright (C) 2001-2023 Zabbix SIA
**
** 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.
**
** You should have received a copy of the GNU General Public License
** along with this program; if not, write to the Free Software
** Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
**/
require_once dirname(__FILE__).'/../../include/CWebTest.php';
require_once dirname(__FILE__).'/../behaviors/CMessageBehavior.php';
require_once dirname(__FILE__).'/../traits/TableTrait.php';
class testSlaReport extends CWebTest {
use TableTrait;
/**
* Attach MessageBehavior to the test.
*
* @return array
*/
public function getBehaviors() {
return [
'class' => CMessageBehavior::class
];
}
public static $reporting_periods = [];
public static $period_headers = [
'Daily' => 'Day',
'Weekly' => 'Week',
'Monthly' => 'Month',
'Quarterly' => 'Quarter',
'Annually' => 'Year'
];
private static $actual_creation_time; // Actual timestamp when data source was executed.
private static $service_creation_time; // Service "Service with problem" creation time, needed for downtime calculation.
const SLA_CREATION_TIME = 1619827200; // SLA creation timestamp as per scenario - 01.05.2021
public function getSlaDataWithService() {
return [
// Daily with downtime.
[
[
'fields' => [
'SLA' => 'SLA Daily',
'Service' => 'Service with problem'
],
'reporting_period' => 'Daily',
'downtimes' => ['EXCLUDED DOWNTIME', 'Second downtime'],
'check_sorting' => true,
'expected' => [
'SLO' => '11.111'
]
]
],
// Daily without downtime.
[
[
'fields' => [
'SLA' => 'Update SLA',
'Service' => 'Parent for 2 levels of child services'
],
'reporting_period' => 'Daily',
'expected' => [
'SLO' => '99.99',
'SLI' => 100
]
]
],
// Weekly SLA.
[
[
'fields' => [
'SLA' => 'SLA Weekly',
'Service' => 'Simple actions service'
],
'reporting_period' => 'Weekly',
'expected' => [
'SLO' => '55.5555',
'SLI' => 100
]
]
],
// Monthly SLA.
[
[
'fields' => [
'SLA' => 'SLA Monthly',
'Service' => 'Simple actions service'
],
'reporting_period' => 'Monthly',
'expected' => [
'SLO' => '22.22',
'SLI' => 100
]
]
],
// Quarterly SLA.
[
[
'fields' => [
'SLA' => 'SLA Quarterly',
'Service' => 'Simple actions service'
],
'reporting_period' => 'Quarterly',
'expected' => [
'SLO' => '33.33',
'SLI' => 100
]
]
],
// Annual SLA.
[
[
'fields' => [
'SLA' => 'SLA Annual',
'Service' => 'Service with problem'
],
'reporting_period' => 'Annually',
'expected' => [
'SLO' => '44.44',
'SLI' => 100
]
]
],
// Incorrect SLA and Service combination.
[
[
'fields' => [
'SLA' => 'SLA Annual',
'Service' => 'Child 1'
],
'reporting_period' => 'Annually',
'no_data' => true
]
]
];
}
public function getSlaDataWithoutService() {
return [
// Daily with downtime.
[
[
'fields' => [
'SLA' => 'SLA Daily'
],
'check_sorting' => true,
'reporting_period' => 'Daily',
'expected' => [
'SLO' => '11.111',
'services' => ['Service with problem']
]
]
],
// Daily without downtime.
[
[
'fields' => [
'SLA' => 'Update SLA'
],
'reporting_period' => 'Daily',
'expected' => [
'SLO' => '99.99',
'SLI' => 100,
'services' => ['Parent for 2 levels of child services']
]
]
],
// Weekly SLA.
[
[
'fields' => [
'SLA' => 'SLA Weekly'
],
'reporting_period' => 'Weekly',
'expected' => [
'SLO' => '55.5555',
'SLI' => 100,
'services' => ['Service with multiple service tags', 'Simple actions service']
]
]
],
// Monthly SLA.
[
[
'fields' => [
'SLA' => 'SLA Monthly'
],
'reporting_period' => 'Monthly',
'expected' => [
'SLO' => '22.22',
'SLI' => 100,
'services' => ['Service with multiple service tags', 'Simple actions service']
]
]
],
// Quarterly SLA.
[
[
'fields' => [
'SLA' => 'SLA Quarterly'
],
'reporting_period' => 'Quarterly',
'expected' => [
'SLO' => '33.33',
'SLI' => 100,
'services' => ['Service with multiple service tags', 'Simple actions service']
]
]
],
// Annual SLA.
[
[
'fields' => [
'SLA' => 'SLA Annual'
],
'reporting_period' => 'Annually',
'expected' => [
'SLO' => '44.44',
'SLI' => 100,
'services' => ['Service with problem']
]
]
]
];
}
/**
* Create the reference array with reporting periods based on the SLA creation time and current date.
*/
public function getDateTimeData() {
self::$actual_creation_time = CDataHelper::get('Sla.creation_time');
self::$service_creation_time = CDBHelper::getValue(
'SELECT created_at FROM services WHERE name='.zbx_dbstr('Service with problem')
);
// Construct the reference reporting period array based on the period type.
foreach (['Daily', 'Weekly', 'Monthly', 'Quarterly', 'Annually'] as $reporting_period) {
$period_values = [];
switch ($reporting_period) {
case 'Daily':
// By default the last 20 periods are displayed.
for ($i = 0; $i < 20; $i++) {
$day = strtotime('today '.-$i.' day');
$period_values[$i]['value'] = date('Y-m-d', $day);
$period_values[$i]['start'] = $day;
$period_values[$i]['end'] = strtotime('tomorrow '.-$i.' day - 1 second');
}
break;
case 'Weekly':
for ($i = 1; $i <= 20; $i++) {
// Next Sunday should be taken as period start date in case if today is Sunday (0 represents Sunday).
$start_string = (date('w', time()) == 0) ? 'Sunday next week ' : 'next Sunday ';
$period_values[$i]['start'] = strtotime($start_string.-$i.' week');
$period_values[$i]['end'] = strtotime(date('Y-m-d', $period_values[$i]['start']).' + 7 days - 1 second');
$period_values[$i]['value'] = date('Y-m-d', $period_values[$i]['start']).' '.
date('m-d', $period_values[$i]['end']);
}
break;
case 'Monthly':
// Get the number of Months to be displayed as difference between today and SLA creation day in months.
$months = CDateTimeHelper::countMonthsBetweenDates(self::SLA_CREATION_TIME, time());
$months = ($months > 20) ? 20 : $months;
for ($i = 0; $i < $months; $i++) {
$month = strtotime('first day of this month '.-$i.' month');
$period_values[$i]['value'] = date('Y-m', $month);
$period_values[$i]['start'] = strtotime(date('Y-m').' '.-$i.' month');
$period_values[$i]['end'] = strtotime(date('Y-m').' '.(-$i+1).' month - 1 second');
}
break;
case 'Quarterly':
$quarters = ['01 03', '04 06', '07 09', '10 12'];
$current_year = date('Y');
$current_month = date('m');
$i = 0;
for ($year = date('Y', self::SLA_CREATION_TIME); $year <= date('Y'); $year++) {
foreach ($quarters as $quarter) {
// Get the last and the first month of the quarter under attention.
$period_end = ltrim(stristr($quarter, ' '), ' ');
$period_start = substr($quarter, 0, strpos($quarter, " "));
// Skip the quarters before SLA creation quarter in SLA creation year.
if ($year === date('Y', self::SLA_CREATION_TIME) && $period_end < date("m", self::SLA_CREATION_TIME)) {
continue;
}
// Write periods into reference array if period start is not later than current month.
if ($year < $current_year || ($year == $current_year && $period_start <= $current_month)) {
$period_values[$i]['value'] = $year.'-'.$quarter;
$period_values[$i]['start'] = strtotime($year.'-'.$period_start);
$period_values[$i]['end'] = strtotime($year.'-'.$period_end.' + 1 month - 1 second');
$i++;
}
}
}
$period_values = array_reverse($period_values);
break;
case 'Annually':
// Get the number of Years to be displayed as difference between this year and SLA creation year.
$years = (date('Y') - date('Y', self::SLA_CREATION_TIME));
for ($i = 0; $i <= $years; $i++) {
$year = strtotime('this year '.-$i.' years');
$period_values[$i]['value'] = date('Y', $year);
$period_values[$i]['start'] = strtotime(date('Y', $year).'-01-01');
$period_values[$i]['end'] = strtotime(date('Y', $year).'-01-01 +1 year -1 second');
}
break;
}
self::$reporting_periods[$reporting_period] = $period_values;
}
}
/**
* Check SLA report when SLA is specified together with the corresponding Service.
*
* @param array $data test case related data from data provider.
* @param boolean $widget flag that specifies whether the check is made in the SLA report or SLA report widget.
*/
public function checkLayoutWithService($data, $widget = false) {
$creation_day = date('Y-m-d', self::$actual_creation_time);
$table = ($widget)
? CDashboardElement::find()->one()->getWidget($data['fields']['Name'])->query('class:list-table')->asTable()->one()
: $this->query('class:list-table')->asTable()->one();
// Check empty result if non-related SLA + Service or disabled SLA (in widget) is selected and proceed with next test.
if (CTestArrayHelper::get($data, 'no_data')) {
$string = (array_key_exists('expected', $data)) ? $data['expected'] : 'No data found.';
$this->assertEquals([$string], $table->getRows()->asText());
$this->assertFalse($table->query('xpath://div[@class="table-stats"]')->one(false)->isValid());
return;
}
// Get the timestamp when screen was loaded and the reference reporting periods.
$load_time = time();
$reference_periods = self::$reporting_periods[$data['reporting_period']];
// Check table headers text and check that none of them are clickable.
$this->assertEquals([self::$period_headers[$data['reporting_period']], 'SLO', 'SLI', 'Uptime', 'Downtime',
'Error budget', 'Excluded downtimes'], $table->getHeadersText()
);
if (CTestArrayHelper::get($data, 'check_sorting')) {
$this->assertEquals([], $table->getSortableHeaders()->asText());
}
// This test is written taking into account that only SLA with daily reporting period has ongoing downtimes.
if (array_key_exists('downtimes', $data)) {
// Downtime starts from min(SLA creation timestamp, Service creation timestamp).
$downtime_start = min(self::$actual_creation_time, self::$service_creation_time);
$downtime_values = [];
/**
* If the date has changed since data source was executed, then downtimes will be divided into 2 days.
* Such case is covered in the else statement.
*/
if (date('Y-m-d') === $creation_day) {
foreach ($data['downtimes'] as $downtime_name) {
/**
* A second or two can pass from Downtime duration calculation till report is loaded.
* So an array of expected results is created and the presence of actual value in array is checked.
*/
$single_downtime = [];
for ($i = 0; $i <= 3; $i++) {
$single_downtime[] = date('Y-m-d H:i', $downtime_start).' '.$downtime_name.': '
.convertUnitsS($load_time - $downtime_start + $i);
}
$downtime_values[$downtime_name] = $single_downtime;
unset($single_downtime);
}
// Check that each of the obtained downtimes is present in the created reference arrays.
$row = $table->findRow(self::$period_headers[$data['reporting_period']], $creation_day);
$this->checkDowntimePresent($row, $downtime_values);
}
else {
foreach ([date('Y-m-d'), $creation_day] as $day) {
if ($day === $creation_day) {
foreach ($data['downtimes'] as $downtime_name) {
/**
* Time is counted from min(SLA creation timestamp, Service creation timestamp) till the start
* of next period. This time difference is not dependent on view load time, so no need for "for" cycle.
*/
$single_downtime = [];
$single_downtime[] = date('Y-m-d H:i', $downtime_start).' '.$downtime_name.': '
.convertUnitsS(strtotime('today') - $downtime_start);
$downtime_values[] = $single_downtime;
unset($single_downtime);
}
}
else {
foreach ($data['downtimes'] as $downtime_name) {
// Time is counted from period start till page load time.
$single_downtime = [];
for ($i = 0; $i <= 3; $i++) {
$single_downtime[] = date('Y-m-d H:i', strtotime('today')).' '.$downtime_name.': '
.convertUnitsS($load_time - strtotime('today') + $i);
}
$downtime_values[] = $single_downtime;
unset($single_downtime);
}
}
$row = $table->findRow(self::$period_headers[$data['reporting_period']], $day);
$this->checkDowntimePresent($row, $downtime_values);
}
}
}
else {
foreach ($reference_periods as $period) {
// If no downtime is expected, then check that the Downtime column is empty.
$row = $table->findRow(self::$period_headers[$data['reporting_period']], $period['value']);
$this->assertEquals('', $row->getColumn('Excluded downtimes')->getText());
}
}
// Check other columns of the displayed report.
foreach ($reference_periods as $period) {
$row = $table->findRow(self::$period_headers[$data['reporting_period']], $period['value']);
$this->assertEquals($data['expected']['SLO'].'%', $row->getColumn('SLO')->getText());
/**
* SLI is displayed for periods from SLA actual creation time to page load time.
* If SLI is expected, then Uptime and Error budget should be calculated and checked.
*/
if (array_key_exists('SLI', $data['expected']) && $period['end'] > self::$actual_creation_time) {
$this->assertEquals($data['expected']['SLI'], $row->getColumn('SLI')->getText());
// Check Uptime and Error budget values. These values are calculated only from the actual SLA creation time.
$uptime = $row->getColumn('Uptime')->getText();
if ($period['end'] > $load_time) {
$reference_uptime = [];
// If SLA created in current period, calculation starts from creation timestamp, else from period start.
$start_time = max($period['start'], min(self::$actual_creation_time, self::$service_creation_time));
/**
* Get array of Uptime possible values and check that the correct one is there.
* Sometimes uptime start is by 1 second larger than obtained 2 rows above, so $i counter starts from -1.
*/
for ($i = -1; $i <= 3; $i++) {
$reference_uptime[] = convertUnitsS($load_time - $start_time + $i);
}
$this->assertTrue(in_array($uptime, $reference_uptime), 'Uptime '.$uptime.' is not among values '.
implode(', ', $reference_uptime)
);
// Calculate the error budet based on the actual uptime and compare with actual error budget.
$uptime_seconds = 0;
foreach (explode(' ', $uptime) as $time_unit) {
$uptime_seconds = $uptime_seconds + timeUnitToSeconds($time_unit);
}
// In rare cases expected and actual error budget can slightly differ due to calculation precision.
foreach([-1, 0, 1] as $delta) {
$error_budget[] = convertUnitsS(intval($uptime_seconds / floatval($data['expected']['SLO']) * 100)
- $uptime_seconds + $delta
);
}
$this->assertTrue(in_array($actual_budget = $row->getColumn('Error budget')->getText(), $error_budget),
'Error budget '.$actual_budget.' is not present among values '.implode(', ', $error_budget)
);
}
else {
$reference_uptime = [];
$uptime_start = min(self::$actual_creation_time, self::$service_creation_time);
// Sometimes uptime start is by 1 second larger than obtained 2 rows above, so $i counter starts from -1.
for ($i = -1; $i <= 3; $i++) {
$reference_uptime[] = convertUnitsS($period['end'] - $uptime_start + $i);
}
$this->assertTrue(in_array($uptime, $reference_uptime), 'Uptime '.$uptime.' is not among values '.
implode(', ', $reference_uptime)
);
// Error budget is always 0 for periods that have already passed.
$this->assertEquals('0', $row->getColumn('Error budget')->getText());
}
}
else {
$this->assertEquals('N/A', $row->getColumn('SLI')->getText());
$this->assertEquals('0', $row->getColumn('Uptime')->getText());
$this->assertEquals('0', $row->getColumn('Error budget')->getText());
}
$this->assertEquals('0', $row->getColumn('Downtime')->getText());
}
}
/**
* Check the SLA report in case if only SLA is specified (without Service).
*
* @param array $data test case related data from data provider
* @param boolean $widget flag that specifies whether the check is made in the SLA report or SLA report widget.
*/
public function checkLayoutWithoutService($data, $widget = false) {
// This if condition is here specifically to check case when displaying disabled SLA on SLA report widget.
if (array_key_exists('no_data', $data)) {
$table = CDashboardElement::find()->one()->getWidget($data['fields']['Name'])->query('class:list-table')->asTable()->one();
$this->assertEquals([$data['expected']], $table->getRows()->asText());
return;
}
$reference_periods = self::$reporting_periods[$data['reporting_period']];
$count = count($data['expected']['services']);
if ($widget) {
$table = CDashboardElement::find()->one()->getWidget($data['fields']['Name'])->query('class:list-table')->asTable()->one();
$this->assertEquals('Displaying '.$count.' of '.$count.' found',
$table->query('xpath:.//td[@class="list-table-footer"]')->one()->getText()
);
}
else {
$table = $this->query('class:list-table')->asTable()->one();
$this->assertTableStats($count);
}
$headers = ['Service', 'SLO'];
foreach (array_reverse($reference_periods) as $period) {
$headers[] = $period['value'];
}
$this->assertEquals($headers, $table->getHeadersText());
if (CTestArrayHelper::get($data, 'check_sorting')) {
// Only "Service" column is sortable.
$this->assertEquals($widget ? [] : ['Service'], $table->getSortableHeaders()->asText());
}
foreach ($data['expected']['services'] as $service) {
$row = $table->findRow('Service', $service);
$this->assertEquals($data['expected']['SLO'].'%', $row->getColumn('SLO')->getText());
// For SLA without service periods are shown in ascending order, so reference array should be reversed.
foreach (array_reverse($reference_periods) as $period) {
if (array_key_exists('SLI', $data['expected']) && $period['end'] > self::$actual_creation_time) {
$this->assertEquals($data['expected']['SLI'], $row->getColumn($period['value'])->getText());
}
else {
$this->assertEquals('N/A', $row->getColumn($period['value'])->getText());
}
}
}
}
/**
* Split cell into active downtimes and check that it is present in the reference array.
*
* @param CTableRowElement $row row that contains the downtime values to be checked
* @param array $downtime_values reference array that should contain the downtime to be checked
*/
private function checkDowntimePresent($row, $downtime_values) {
// Split column value into downtimes.
foreach (explode("\n", $row->getColumn('Excluded downtimes')->getText()) as $downtime) {
// Record if downtime found in reference downtime arrays.
$match_found = false;
foreach ($downtime_values as $downtime_array) {
if (in_array($downtime, $downtime_array)) {
$match_found = true;
break;
}
}
$this->assertTrue($match_found, 'Downtime "'.$downtime.'" is not present in downtime reference array');
}
}
/**
* Check the layout and the contents on the dialogs in SLA and Service multiselect elements.
*
* @param array $dialog_data array that contains all of the reference data needed to check dialog layout
* @param boolean $widget flag that specified whether the check is made in the SLA report or SLA report widget
*/
public function checkDialogContents($dialog_data, $widget = false) {
$form_selector = ($widget) ? 'name:widget_dialogue_form' : 'name:zbx_filter';
$form = $this->query($form_selector)->one()->asForm();
$form->getField($dialog_data['field'])->query('button:Select')->waitUntilClickable()->one()->click();
$dialog = COverlayDialogElement::find()->waitUntilReady()->all()->last();
$this->assertEquals($dialog_data['field'], $dialog->getTitle());
if ($dialog_data['field'] === 'Service') {
// Check filter form.
$filter_form = $dialog->query('name:services_filter_form')->one();
$this->assertEquals('Name', $filter_form->query('xpath:.//label')->one()->getText());
$filter_input = $filter_form->query('name:filter_name')->one();
$this->assertEquals(255, $filter_input->getAttribute('maxlength'));
// Filter out all unwanted services before checking table content.
$filter_input->fill($dialog_data['check_row']['Name']);
$dialog->query('button:Filter')->one()->click();
$dialog->waitUntilReady();
// Check the content of the services list.
$this->assertTableData([$dialog_data['check_row']], $dialog_data['table_selector']);
$filter_form->query('button:Reset')->one()->click();
$dialog->waitUntilReady();
}
$this->assertEquals($dialog_data['headers'], $dialog->query('class:list-table')->asTable()->one()->getHeadersText());
if (array_key_exists('column_data', $dialog_data)) {
foreach ($dialog_data['column_data'] as $column => $values) {
$this->assertTableDataColumn($values, $column, $dialog_data['table_selector']);
}
}
else {
$table = $dialog->query('class:list-table')->asTable()->one();
$this->assertEquals(CDBHelper::getCount('SELECT serviceid FROM services'), $table->getRows()->count());
}
$this->assertEquals(count($dialog_data['buttons']), $dialog->query('button', $dialog_data['buttons'])->all()
->filter(new CElementFilter(CElementFilter::CLICKABLE))->count()
);
$dialog->query('button:Cancel')->one()->click();
}
}