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(); } }