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.

648 lines
17 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 'vendor/autoload.php';
require_once dirname(__FILE__).'/../../include/defines.inc.php';
require_once dirname(__FILE__).'/../../include/hosts.inc.php';
require_once dirname(__FILE__).'/helpers/CDBHelper.php';
require_once dirname(__FILE__).'/helpers/CConfigHelper.php';
require_once dirname(__FILE__).'/helpers/CAPIHelper.php';
require_once dirname(__FILE__).'/helpers/CAPIScimHelper.php';
require_once dirname(__FILE__).'/helpers/CDataHelper.php';
require_once dirname(__FILE__).'/helpers/CExceptionHelper.php';
require_once dirname(__FILE__).'/helpers/CTestArrayHelper.php';
require_once dirname(__FILE__).'/helpers/CDateTimeHelper.php';
define('USER_ACTION_ADD', 'add');
define('USER_ACTION_UPDATE', 'update');
define('USER_ACTION_REMOVE', 'remove');
define('STRING_6000', str_repeat('long_string_', 500));
define('STRING_2200', substr(STRING_6000, 0, 2200));
define('STRING_2048', substr(STRING_6000, 0, 2048));
define('STRING_2000', substr(STRING_6000, 0, 2000));
define('STRING_1024', substr(STRING_6000, 0, 1024));
define('STRING_255', substr(STRING_6000, 0, 255));
define('STRING_128', substr(STRING_6000, 0, 128));
define('STRING_64', substr(STRING_6000, 0, 64));
/**
* Base class of php unit tests.
*/
use PHPUnit\Framework\TestCase;
class CTest extends TestCase {
// Table that should be backed up at the test suite level.
protected static $suite_backup = null;
// Table that should be backed up at the test case level.
protected $case_backup = null;
// Table that should be backed up at the test case level once (for multiple case executions).
protected static $case_backup_once = null;
// zabbix.conf.php should be backed up at the test suite level.
protected static $suite_backup_config = false;
// zabbix.conf.php should be backed up at the test case level.
protected $case_backup_config = false;
// Name of the last executed test.
protected static $last_test_case = null;
// Test case data key.
protected $data_key = null;
// Lists of test case data set keys.
protected static $test_data_sets = [];
// Test case annotations.
protected $annotations = null;
// Test case warnings.
protected static $warnings = [];
// Skip test suite execution.
protected static $skip_suite = false;
// Callbacks that should be executed at the test case level.
protected $case_callbacks = [];
// Callbacks that should be executed at the test suite level.
protected static $suite_callbacks = [
'afterOnce' => [],
'beforeEach' => [],
'afterEach' => [],
'after' => []
];
// Instances counter to keep track of test count.
protected static $instances = 0;
// List of behaviors.
protected $behaviors = null;
/**
* Overridden constructor for collecting data on data sets from dataProvider annotations.
*
* @param string $name
* @param array $data
* @param string $data_name
*/
public function __construct($name = null, array $data = [], $data_name = '') {
parent::__construct($name, $data, $data_name);
// If data limits are enabled and test case uses data.
if (defined('PHPUNIT_ENABLE_DATA_LIMITS') && PHPUNIT_ENABLE_DATA_LIMITS && $data) {
$this->data_key = $data_name;
self::$test_data_sets[$name][] = $data_name;
}
self::$instances++;
}
/**
* Destructor to run callback when all tests are executed.
*/
public function __destruct() {
self::$instances--;
if (self::$instances === 0) {
static::onAfterAllTests();
}
}
/**
* Get annotations by type name.
* Helper function for method / class annotation processing.
*
* @param array $annotations annotations
* @param string $type type name
*
* @return array or null
*/
protected function getAnnotationsByType($annotations, $type) {
if ($annotations === null || !array_key_exists($type, $annotations) || !is_array($annotations[$type])) {
return null;
}
return $annotations[$type];
}
/**
* Get annotation tokens by annotation name.
* Helper function for method / class annotation processing.
*
* @param array $annotations annotations
* @param string $name annotation name
*
* @return array
*/
protected function getAnnotationTokensByName($annotations, $name) {
if ($annotations === null || !array_key_exists($name, $annotations) || !is_array($annotations[$name])) {
return [];
}
$result = [];
foreach ($annotations[$name] as $annotation) {
foreach (explode(',', $annotation) as $token) {
$result[] = trim($token);
}
}
return $result;
}
/**
* Execute callbacks specified at some point of test execution.
*
* @param mixed $context class instance or class name
* @param array $callbacks callbacks to be called
* @param bool $required flag marking callbacks required
*
* @return boolean
*/
protected static function executeCallbacks($context, $callbacks, $required = false) {
if (!$callbacks) {
return true;
}
CDataHelper::setSessionId(null);
$class = new ReflectionClass($context);
if (!is_object($context)) {
$context = null;
}
foreach ($callbacks as $callback) {
try {
$method = $class->getMethod($callback);
}
catch (ReflectionException $exception) {
$method = null;
}
if (!$method) {
$error = 'Callback "'.$callback.'" is not defined in requested context.';
if (!$required) {
self::zbxAddWarning($error);
}
else {
throw new Exception($error);
}
continue;
}
try {
$method->invoke(!$method->isStatic() ? $context : null);
} catch (Exception $e) {
$error = 'Failed to execute callback "'.$callback.'": '.$e->getMessage()."\n\n".$e->getTraceAsString();
if (!$required) {
self::zbxAddWarning($error);
}
else {
throw new Exception($error);
}
return false;
}
}
return true;
}
/**
* Callback executed before every test suite.
*/
protected function onBeforeTestSuite() {
// Test suite level annotations.
$class_annotations = $this->getAnnotationsByType($this->annotations, 'class');
// Data sources are processed before the backups.
$data_source = $this->getAnnotationTokensByName($class_annotations, 'dataSource');
if ($data_source) {
CDataHelper::load($data_source);
}
// Backup performed before test suite execution.
$suite_backup = $this->getAnnotationTokensByName($class_annotations, 'backup');
if ($suite_backup) {
self::$suite_backup = $suite_backup;
CDBHelper::backupTables(self::$suite_backup);
}
$suite_backup_config = $this->getAnnotationTokensByName($class_annotations, 'backupConfig');
if ($suite_backup_config) {
self::$suite_backup_config = true;
CConfigHelper::backupConfig();
}
self::$skip_suite = false;
// Callbacks to be performed before test suite execution.
$callbacks = $this->getAnnotationTokensByName($class_annotations, 'onBefore');
if (!self::executeCallbacks($this, $callbacks)) {
self::markTestSuiteSkipped();
throw new Exception(implode("\n", static::$warnings));
}
// Store callback to be executed later.
self::$suite_callbacks = ['afterOnce' => []];
foreach (['beforeEach', 'afterEach', 'after'] as $key) {
self::$suite_callbacks[$key] = $this->getAnnotationTokensByName($class_annotations, 'on'.ucfirst($key));
}
}
/**
* Callback executed before every test case.
*
* @before
*/
public function onBeforeTestCase() {
global $DB;
static $suite = null;
$class_name = get_class($this);
$case_name = $this->getName(false);
self::$warnings = [];
// Clear contents of error log.
if (defined('PHPUNIT_ERROR_LOG') && file_exists(PHPUNIT_ERROR_LOG)) {
@file_put_contents(PHPUNIT_ERROR_LOG, '');
}
if (!isset($DB['DB'])) {
DBconnect($error);
}
$this->annotations = $this->getAnnotations();
if (self::$last_test_case !== $case_name) {
// Restore data from backup if test case changed.
if (self::$case_backup_once !== null) {
CDBHelper::restoreTables();
self::$case_backup_once = null;
}
self::executeCallbacks($this, self::$suite_callbacks['afterOnce']);
self::$suite_callbacks['afterOnce'] = [];
}
// Class name change is used to determine suite change.
if ($suite !== $class_name) {
$suite = $class_name;
$this->onBeforeTestSuite();
}
// Execute callbacks that should be executed before every test case.
self::executeCallbacks($this, self::$suite_callbacks['beforeEach'], true);
// Test case level annotations.
$method_annotations = $this->getAnnotationsByType($this->annotations, 'method');
if ($method_annotations !== null) {
// Data sources are processed before the backups.
$data_source = $this->getAnnotationTokensByName($method_annotations, 'dataSource');
if ($data_source) {
CDataHelper::load($data_source);
}
// Backup performed before every test case execution.
$case_backup = $this->getAnnotationTokensByName($method_annotations, 'backup');
if ($case_backup) {
$this->case_backup = $case_backup;
CDBHelper::backupTables($this->case_backup);
}
$case_backup_config = $this->getAnnotationTokensByName($method_annotations, 'backupConfig');
if ($case_backup_config) {
$this->case_backup_config = true;
CConfigHelper::backupConfig();
}
if (self::$last_test_case !== $case_name) {
if (array_key_exists($case_name, self::$test_data_sets)) {
// Check for data set limit.
$limit = $this->getAnnotationTokensByName($method_annotations, 'dataLimit');
if (count($limit) === 1 && is_numeric($limit[0]) && $limit[0] >= 1
&& count(self::$test_data_sets[$case_name]) > $limit[0]) {
$sets = self::$test_data_sets[$case_name];
shuffle($sets);
self::$test_data_sets[$case_name] = array_slice($sets, 0, (int)$limit[0]);
}
}
// Backup performed once before first test case execution.
$case_backup_once = $this->getAnnotationTokensByName($method_annotations, 'backupOnce');
if ($case_backup_once) {
self::$case_backup_once = $case_backup_once;
CDBHelper::backupTables(self::$case_backup_once);
}
// Execute callbacks that should be executed once for multiple test cases.
self::executeCallbacks($this, $this->getAnnotationTokensByName($method_annotations, 'onBeforeOnce'), true);
// Store callback to be executed after test case is executed for all data sets.
self::$suite_callbacks['afterOnce'] = $this->getAnnotationTokensByName($method_annotations,
'onAfterOnce'
);
}
// Execute callbacks that should be executed before specific test case.
self::executeCallbacks($this, $this->getAnnotationTokensByName($method_annotations, 'onBefore'), true);
// Store callback to be executed after test case.
$this->case_callbacks = $this->getAnnotationTokensByName($method_annotations, 'onAfter');
}
self::$last_test_case = $case_name;
// Mark excessive test cases as skipped.
if (array_key_exists($case_name, self::$test_data_sets)
&& !in_array($this->data_key, self::$test_data_sets[$case_name])) {
self::markTestSkipped('Test case skipped by data provider limit check.');
}
if (self::$skip_suite) {
self::markTestSkipped();
}
}
/**
* Callback executed after every test case.
*
* @after
*/
public function onAfterTestCase() {
$errors = @file_get_contents(PHPUNIT_ERROR_LOG);
if ($this->case_backup_config) {
CConfigHelper::restoreConfig();
$this->case_backup_config = false;
}
if (CDataHelper::getSessionId() !== null) {
foreach (CDBHelper::$backups as $backup) {
if (in_array('sessions', $backup)) {
CDataHelper::reset();
}
}
}
if ($this->case_backup !== null) {
CDBHelper::restoreTables();
}
// Execute callbacks that should be executed after specific test case.
self::executeCallbacks($this, $this->case_callbacks);
// Execute callbacks that should be executed after every test case.
self::executeCallbacks($this, self::$suite_callbacks['afterEach']);
DBclose();
if (defined('PHPUNIT_REPORT_WARNINGS') && PHPUNIT_REPORT_WARNINGS && self::$warnings) {
throw new PHPUnit_Framework_Warning(implode("\n", self::$warnings));
}
if ($errors !== '' && $errors !== false) {
$this->fail("Runtime errors:\n".$errors);
}
}
/**
* Callback executed after every test suite.
*
* @afterClass
*/
public static function onAfterTestSuite() {
global $DB;
if (self::$suite_backup_config) {
CConfigHelper::restoreConfig();
self::$suite_backup_config = false;
}
if (self::$suite_backup === null && self::$case_backup_once === null && !self::$suite_callbacks['afterOnce']
&& !self::$suite_callbacks['after']) {
// Nothing to do after test suite.
return;
}
DBconnect($error);
// Restore case level backups.
if (self::$case_backup_once !== null) {
CDBHelper::restoreTables();
self::$case_backup_once = null;
}
// Restore suite level backups.
if (self::$suite_backup !== null) {
CDBHelper::restoreTables();
self::$suite_backup = null;
}
$context = get_called_class();
self::executeCallbacks($context, self::$suite_callbacks['afterOnce']);
self::executeCallbacks($context, self::$suite_callbacks['after']);
DBclose();
}
/**
* Add warning to test case warning list.
*
* @param string $warning warning text
*/
public static function zbxAddWarning($warning) {
if (!in_array($warning, self::$warnings)) {
self::$warnings[] = $warning;
}
}
/**
* Mark test suite skipped.
*/
public static function markTestSuiteSkipped() {
self::$skip_suite = true;
}
/**
* Callback to be executed after all test cases.
*/
public static function onAfterAllTests() {
// Code is not missing here.
}
/**
* Get list of static behaviors.
* Static behaviors get attached when object is created.
*
* @return array
*/
public function getBehaviors() {
return [];
}
/**
* Load static behaviors.
*/
public function loadBehaviors() {
if ($this->behaviors !== null) {
return;
}
$this->behaviors = [];
foreach ($this->getBehaviors() as $name => $behavior) {
if (is_int($name)) {
$name = null;
}
$this->attachBehavior($behavior, $name);
}
}
/**
* Attach dynamic behavior.
*
* @param string|CBehavior $behavior behavior or behavior class name
* @param string $name name of the behavior or null for anonymous behavior
*
* @throws Exception on invalid configuration
*/
public function attachBehavior($behavior, $name = null) {
$this->loadBehaviors();
if (is_string($behavior)) {
$behavior = ['class' => $behavior];
}
if (is_array($behavior) && array_key_exists('class', $behavior) && class_exists($behavior['class'])) {
$class = $behavior['class'];
unset($behavior['class']);
$behavior = new $class($behavior);
}
if ($behavior instanceof CBehavior) {
if ($name !== null) {
$this->detachBehavior($name);
}
$behavior->setTest($this);
if ($name !== null) {
$this->behaviors[$name] = $behavior;
}
else {
$this->behaviors[] = $behavior;
}
}
else {
throw new Exception('Cannot attach behavior that is not an instance of CBehavior class');
}
}
/**
* Detach dynamic behavior.
*
* @param string $name name of the behavior or null for anonymous behavior
*/
public function detachBehavior($name) {
$this->loadBehaviors();
unset($this->behaviors[$name]);
}
/**
* Detach all behaviors.
*/
public function detachBehaviors() {
$this->behaviors = [];
}
/**
* Magic method to execute methods defined in behaviors.
*
* @param string $name method name
* @param array $params method params
*
* @return mixed
*
* @throws Exception
*/
public function __call($name, $params) {
$this->loadBehaviors();
$target = null;
foreach ($this->behaviors as $behavior) {
if ($behavior->hasMethod($name)) {
$target = $behavior;
}
}
if ($target !== null) {
return call_user_func_array([$target, $name], $params);
}
throw new Exception('Cannot call method '.get_class($this).'::'.$name.'(): unknown method.');
}
/**
* Magic method to get attributes defined in behaviors.
*
* @param string $name attribute name
*
* @return mixed
*
* @throws Exception
*/
public function __get($name) {
$this->loadBehaviors();
foreach ($this->behaviors as $behavior) {
if ($behavior->hasAttribute($name)) {
return $behavior->$name;
}
}
throw new Exception('Cannot get attribute "'.$name.'": unknown attribute.');
}
/**
* Magic method to set attributes defined in behaviors.
*
* @param string $name attribute name
* @param array $value attribute value
*
* @return mixed
*
* @throws Exception
*/
public function __set($name, $value) {
$this->loadBehaviors();
foreach ($this->behaviors as $behavior) {
if ($behavior->hasAttribute($name)) {
$behavior->$name = $value;
return;
}
}
throw new Exception('Cannot set attribute "'.$name.'": unknown attribute.');
}
}