Merge remote-tracking branch 'origin/support/3.1' into develop

This commit is contained in:
Molkobain
2023-08-11 11:06:21 +02:00
10 changed files with 471 additions and 53 deletions

View File

@@ -1397,13 +1397,23 @@ class utils
return APPROOT . 'env-' . MetaModel::GetEnvironment() . '/';
}
/**
* @return string A path to the folder into which data can be written
* @internal
* @since N°6097 2.7.10 3.0.4 3.1.1
*/
public static function GetDataPath(): string
{
return APPROOT.'data/';
}
/**
* @return string A path to a folder into which any module can store cache data
* The corresponding folder is created or cleaned upon code compilation
*/
public static function GetCachePath()
{
return APPROOT.'data/cache-'.MetaModel::GetEnvironment().'/';
return static::GetDataPath().'cache-'.MetaModel::GetEnvironment().'/';
}
/**

View File

@@ -431,6 +431,7 @@ class CMDBSource
{
self::$m_sDBName = '';
}
self::_TablesInfoCacheReset(); // reset the table info cache!
}
public static function CreateTable($sQuery)

View File

@@ -2,5 +2,12 @@
"require-dev": {
"phpunit/phpunit" : "^9",
"sempro/phpunit-pretty-print": "^1.4"
},
"autoload": {
"psr-4": {
"Combodo\\iTop\\Test\\UnitTest\\": "src/BaseTestCase/",
"Combodo\\iTop\\Test\\UnitTest\\Hook\\": "src/Hook/",
"Combodo\\iTop\\Test\\UnitTest\\Service\\": "src/Service/"
}
}
}

View File

@@ -0,0 +1,200 @@
<?php
/*
* @copyright Copyright (C) 2010-2023 Combodo SARL
* @license http://opensource.org/licenses/AGPL-3.0
*/
namespace Combodo\iTop\Test\UnitTest;
use CMDBSource;
use Combodo\iTop\Test\UnitTest\Hook\TestsRunStartHook;
use Combodo\iTop\Test\UnitTest\Service\UnitTestRunTimeEnvironment;
use Config;
use Exception;
use IssueLog;
use MetaModel;
use SetupUtils;
use utils;
/**
* Class ItopCustomDatamodelTestCase
*
* Helper class to extend for tests needing a custom DataModel (eg. classes, attributes, etc conditions not available in the standard DM)
* Usage:
* - Create a test case class extending this one
* - Override the {@see ItopCustomDatamodelTestCase::GetDatamodelDeltaAbsPath()} method to define where you XML delta is
* - Implement your test case methods as usual
*
* @since N°6097 2.7.9 3.0.4 3.1.0
*/
abstract class ItopCustomDatamodelTestCase extends ItopDataTestCase
{
/**
* @var bool[]
*/
protected static $aReadyCustomEnvironments = [];
/**
* @inheritDoc
* @since N°6097 Workaround to make the "runClassInSeparateProcess" directive work
*/
public function __construct($name = null, array $data = [], $dataName = '')
{
parent::__construct($name, $data, $dataName);
// Ensure that a test class derived from this one runs in a dedicated process as it changes the MetaModel / environment on the fly and
// for now we have no way of switching environments properly in memory and it will result in other (regular) test classes to fail as they won't be on the expected environment.
//
// If we don't do this, we would have to add the `@runTestsInSeparateProcesses` on *each* test classes which we want to avoid for obvious possible mistakes.
// Note that the `@runClassInSeparateProcess` don't work in PHPUnit yet.
$this->setRunClassInSeparateProcess(true);
}
/**
* @return string Abs path to the XML delta to use for the tests of that class
*/
abstract public function GetDatamodelDeltaAbsPath(): string;
/**
* @inheritDoc
*/
protected function LoadRequiredItopFiles(): void
{
parent::LoadRequiredItopFiles();
$this->RequireOnceItopFile('setup/setuputils.class.inc.php');
$this->RequireOnceItopFile('setup/runtimeenv.class.inc.php');
}
/**
* @return string Environment used as a base (conf. file, modules, DB, ...) to prepare the test environment
*/
protected function GetSourceEnvironment(): string
{
return 'production';
}
/**
* @inheritDoc
* @warning This should ONLY be overloaded if your test case XML deltas are NOT compatible with the others, as it will create / compile another environment, increasing the global testing time.
*/
public function GetTestEnvironment(): string
{
return 'php-unit-tests';
}
/**
* @return string Absolute path to the {@see \Combodo\iTop\Test\UnitTest\ItopCustomDatamodelTestCase::GetTestEnvironment()} folder
*/
final private function GetTestEnvironmentFolderAbsPath(): string
{
return APPROOT.'env-'.$this->GetTestEnvironment().'/';
}
/**
* Mark {@see \Combodo\iTop\Test\UnitTest\ItopDataTestCase::GetTestEnvironment()} as ready (compiled)
*
* @return void
*/
final private function MarkEnvironmentReady(): void
{
if (false === $this->IsEnvironmentReady()) {
touch(static::GetTestEnvironmentFolderAbsPath());
}
}
/**
* @return bool True if the {@see \Combodo\iTop\Test\UnitTest\ItopDataTestCase::GetTestEnvironment()} is ready (compiled, but not started)
*
* @details Having the environment ready means that it has been compiled for this global tests run, not that it is a relic from a previous global tests run
*/
final private function IsEnvironmentReady(): bool
{
// As these test cases run in separate processes, the best way we found to let know a process if its environment was already prepared for **this run** was to compare the modification times of:
// - its own env-<ENV> folder
// - a file generated at the beginning of the global test run {@see \Combodo\iTop\Test\UnitTest\Hook\TestsRunStartHook}
$sRunStartedFilePath = TestsRunStartHook::GetRunStartedFileAbsPath();
$sEnvFolderPath = static::GetTestEnvironmentFolderAbsPath();
clearstatcache();
if (false === file_exists($sRunStartedFilePath) || false === file_exists($sEnvFolderPath)) {
return false;
}
$iRunStartedFileModificationTime = filemtime($sRunStartedFilePath);
$iEnvFolderModificationTime = filemtime($sEnvFolderPath);
return $iEnvFolderModificationTime >= $iRunStartedFileModificationTime;
}
/**
* @inheritDoc
*/
protected function PrepareEnvironment(): void
{
$sSourceEnv = $this->GetSourceEnvironment();
$sTestEnv = $this->GetTestEnvironment();
// Check if test env. is already set and only prepare it if it's not up-to-date
//
// Note: To improve performances, we compile all XML deltas from test cases derived from this class and make a single environment where everything will be ran at once.
// This requires XML deltas to be compatible, but it is a known and accepted trade-off. See PR #457
if (false === $this->IsEnvironmentReady()) {
//----------------------------------------------------
// Clear any previous "$sTestEnv" environment
//----------------------------------------------------
// - Configuration file
$sConfFile = utils::GetConfigFilePath($sTestEnv);
$sConfFolder = dirname($sConfFile);
if (is_file($sConfFile)) {
chmod($sConfFile, 0777);
SetupUtils::tidydir($sConfFolder);
}
// - Datamodel delta files
// - Cache folder
// - Compiled folder
// We don't need to clean them as they are already by the compilation
// - Drop database
// We don't do that now, it will be done before re-creating the DB, once the metamodel is started
//----------------------------------------------------
// Prepare "$sTestEnv" environment
//----------------------------------------------------
// All the following is greatly inspired by the toolkit's sandbox script
// - Prepare config file
$oSourceConf = new Config(utils::GetConfigFilePath($sSourceEnv));
if ($oSourceConf->Get('source_dir') === '') {
throw new Exception('Missing entry source_dir from the config file');
}
$oTestConfig = clone($oSourceConf);
$oTestConfig->ChangeModulesPath($sSourceEnv, $sTestEnv);
// - Switch DB name to a dedicated one so we don't mess with the original one
$sTestEnvSanitizedForDBName = preg_replace('/[^\d\w]/', '', $sTestEnv);
$oTestConfig->Set('db_name', $oTestConfig->Get('db_name').'_'.$sTestEnvSanitizedForDBName);
// - Compile env. based on the existing 'production' env.
$oEnvironment = new UnitTestRunTimeEnvironment($sTestEnv);
$oEnvironment->WriteConfigFileSafe($oTestConfig);
$oEnvironment->CompileFrom($sSourceEnv, false);
// - Force re-creating a fresh DB
CMDBSource::InitFromConfig($oTestConfig);
if (CMDBSource::IsDB($oTestConfig->Get('db_name'))) {
CMDBSource::DropDB();
}
CMDBSource::CreateDB($oTestConfig->Get('db_name'));
MetaModel::Startup($sConfFile, false /* $bModelOnly */, true /* $bAllowCache */, false /* $bTraceSourceFiles */, $sTestEnv);
$this->MarkEnvironmentReady();
$this->debug('Preparation of custom environment "'.$sTestEnv.'" done.');
}
parent::PrepareEnvironment();
}
}

View File

@@ -1,21 +1,8 @@
<?php
// Copyright (c) 2010-2023 Combodo SARL
//
// This file is part of iTop.
//
// iTop is free software; you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// iTop 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 Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with iTop. If not, see <http://www.gnu.org/licenses/>
//
/*
* @copyright Copyright (C) 2010-2023 Combodo SARL
* @license http://opensource.org/licenses/AGPL-3.0
*/
namespace Combodo\iTop\Test\UnitTest;
@@ -31,6 +18,7 @@ use CMDBObject;
use CMDBSource;
use Combodo\iTop\Service\Events\EventData;
use Combodo\iTop\Service\Events\EventService;
use Config;
use Contact;
use DBObject;
use DBObjectSet;
@@ -46,11 +34,13 @@ use MetaModel;
use Person;
use PluginManager;
use Server;
use SetupUtils;
use TagSetFieldData;
use Ticket;
use URP_UserProfile;
use User;
use UserRequest;
use utils;
use VirtualHost;
use VirtualMachine;
use XMLDataLoader;
@@ -61,6 +51,8 @@ define('TAG_CLASS', 'FAQ');
define('TAG_ATTCODE', 'domains');
/**
* Class ItopDataTestCase
*
* Helper class to extend for tests needing access to iTop's metamodel
*
* ** Warning** Each class extending this one needs to add the following annotations :
@@ -72,7 +64,7 @@ define('TAG_ATTCODE', 'domains');
* @since 2.7.7 3.0.1 3.1.0 N°4624 processIsolation is disabled by default and must be enabled in each test needing it (basically all tests using
* iTop datamodel)
*/
class ItopDataTestCase extends ItopTestCase
abstract class ItopDataTestCase extends ItopTestCase
{
private $iTestOrgId;
@@ -82,6 +74,10 @@ class ItopDataTestCase extends ItopTestCase
// Counts
public $aReloadCount = [];
/**
* @var string Default environment to use for test cases
*/
const DEFAULT_TEST_ENVIRONMENT = 'production';
const USE_TRANSACTION = true;
const CREATE_TEST_ORG = false;
@@ -91,11 +87,8 @@ class ItopDataTestCase extends ItopTestCase
protected function setUp(): void
{
parent::setUp();
$this->RequireOnceItopFile('application/utils.inc.php');
$sEnv = 'production';
$sConfigFile = APPCONF.$sEnv.'/'.ITOP_CONFIG_FILE;
MetaModel::Startup($sConfigFile, false /* $bModelOnly */, true /* $bAllowCache */, false /* $bTraceSourceFiles */, $sEnv);
$this->PrepareEnvironment();
if (static::USE_TRANSACTION)
{
@@ -139,6 +132,52 @@ class ItopDataTestCase extends ItopTestCase
parent::tearDown();
}
/**
* @inheritDoc
*/
protected function LoadRequiredItopFiles(): void
{
parent::LoadRequiredItopFiles();
$this->RequireOnceItopFile('application/utils.inc.php');
}
/**
* @return string Environment the test will run in
* @since 2.7.9 3.0.4 3.1.0
*/
protected function GetTestEnvironment(): string
{
return self::DEFAULT_TEST_ENVIRONMENT;
}
/**
* @return string Absolute path of the configuration file used for the test
* @since 2.7.9 3.0.4 3.1.0
*/
protected function GetConfigFileAbsPath(): string
{
return utils::GetConfigFilePath($this->GetTestEnvironment());
}
/**
* Prepare the iTop environment for test to run
*
* @return void
* @throws \CoreException
* @throws \DictExceptionUnknownLanguage
* @throws \MySQLException
* @since 2.7.9 3.0.4 3.1.0
*/
protected function PrepareEnvironment(): void
{
$sEnv = $this->GetTestEnvironment();
$sConfigFile = $this->GetConfigFileAbsPath();
// Start MetaModel for the prepared environment
MetaModel::Startup($sConfigFile, false /* $bModelOnly */, true /* $bAllowCache */, false /* $bTraceSourceFiles */, $sEnv);
}
/**
* @return mixed
*/

View File

@@ -1,36 +1,25 @@
<?php
/**
* Copyright (C) 2013-2023 Combodo SARL
*
* This file is part of iTop.
*
* iTop is free software; you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* iTop 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 Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
/*
* @copyright Copyright (C) 2010-2023 Combodo SARL
* @license http://opensource.org/licenses/AGPL-3.0
*/
namespace Combodo\iTop\Test\UnitTest;
/**
* Created by PhpStorm.
* User: Eric
* Date: 20/11/2017
* Time: 11:21
*/
use CMDBSource;
use MySQLTransactionNotClosedException;
use PHPUnit\Framework\TestCase;
use SetupUtils;
class ItopTestCase extends TestCase
/**
* Class ItopTestCase
*
* Helper class to extend for tests that DO NOT need to access the DataModel or the Database
*
* @author Eric Espie <eric.espie@combodo.com>
* @package Combodo\iTop\Test\UnitTest
*/
abstract class ItopTestCase extends TestCase
{
const TEST_LOG_DIR = 'test';
static $DEBUG_UNIT_TEST = false;
@@ -56,6 +45,9 @@ class ItopTestCase extends TestCase
// setUp might be called multiple times, so protecting the define() call !
define(ITOP_PHPUNIT_RUNNING_CONSTANT_NAME, true);
}
$this->LoadRequiredItopFiles();
$this->LoadRequiredTestFiles();
}
/**
@@ -72,6 +64,28 @@ class ItopTestCase extends TestCase
}
}
/**
* Overload this method to require necessary files through {@see \Combodo\iTop\Test\UnitTest\ItopTestCase::RequireOnceItopFile()}
*
* @return void
* @since 2.7.9 3.0.4 3.1.0
*/
protected function LoadRequiredItopFiles(): void
{
// Empty until we actually need to require some files in the class
}
/**
* Overload this method to require necessary files through {@see \Combodo\iTop\Test\UnitTest\ItopTestCase::RequireOnceUnitTestFile()}
*
* @return void
* @since 2.7.10 3.0.4 3.1.0
*/
protected function LoadRequiredTestFiles(): void
{
// Empty until we actually need to require some files in the class
}
/**
* Require once an iTop file (core or extension) from its relative path to the iTop root dir.
* This ensure to always use the right absolute path, especially in {@see \Combodo\iTop\Test\UnitTest\ItopTestCase::RequireOnceUnitTestFile()}

View File

@@ -0,0 +1,63 @@
<?php
declare(strict_types=1);
/*
* @copyright Copyright (C) 2010-2023 Combodo SARL
* @license http://opensource.org/licenses/AGPL-3.0
*/
namespace Combodo\iTop\Test\UnitTest\Hook;
require_once __DIR__ . '/../../../../approot.inc.php';
use PHPUnit\Runner\AfterLastTestHook;
use PHPUnit\Runner\BeforeFirstTestHook;
use utils;
/**
* Class TestsRunStartHook
*
* IMPORTANT: This will no longer work in PHPUnit 10.0 and there is no alternative for now, so we will have to migrate it when the time comes
* @link https://localheinz.com/articles/2023/02/14/extending-phpunit-with-its-new-event-system/#content-hooks-event-system
*
* @author Guillaume Lajarige <guillaume.lajarige@combodo.com>
* @package Combodo\iTop\Test\UnitTest\Hook
* @since N°6097 2.7.10 3.0.4 3.1.1
*/
class TestsRunStartHook implements BeforeFirstTestHook, AfterLastTestHook
{
/**
* Use the modification time on this file to check whereas it is newer than the requirements in a test case
*
* @return string Abs. path to a file generated when the global tests run starts.
*/
public static function GetRunStartedFileAbsPath(): string
{
// Note: This can't be put in the cache-<ENV> folder as we have multiple <ENV> running across the test cases
// We also don't want to put it in the unit tests folder as it is not supposed to be writable
return APPROOT.'data/.php-unit-tests-run-started';
}
/**
* @inheritDoc
*/
public function executeBeforeFirstTest(): void
{
// Create / change modification timestamp of file marking the beginning of the tests run
touch(static::GetRunStartedFileAbsPath());
}
/**
* @inheritDoc
*/
public function executeAfterLastTest(): void
{
// Cleanup of file marking the beginning of the tests run
if (file_exists(static::GetRunStartedFileAbsPath())) {
unlink(static::GetRunStartedFileAbsPath());
}
}
}

View File

@@ -0,0 +1,86 @@
<?php
/*
* @copyright Copyright (C) 2010-2023 Combodo SARL
* @license http://opensource.org/licenses/AGPL-3.0
*/
namespace Combodo\iTop\Test\UnitTest\Service;
use Combodo\iTop\Test\UnitTest\ItopCustomDatamodelTestCase;
use IssueLog;
use MFCoreModule;
use ReflectionClass;
use RunTimeEnvironment;
/**
* Class UnitTestRunTimeEnvironment
*
* Runtime env. dedicated to creating a temp. environment for a group of unit tests with XML deltas.
*
* @author Guillaume Lajarige <guillaume.lajarige@combodo.com>
* @since N°6097 2.7.10 3.0.4 3.1.1
*/
class UnitTestRunTimeEnvironment extends RunTimeEnvironment
{
/**
* @inheritDoc
*/
protected function GetMFModulesToCompile($sSourceEnv, $sSourceDir)
{
$aRet = parent::GetMFModulesToCompile($sSourceEnv, $sSourceDir);
/** @var string[] $aDeltaFiles Referential of loaded deltas. Mostly to avoid duplicates. */
$aDeltaFiles = [];
foreach (get_declared_classes() as $sClass) {
// Filter on classes derived from this \Combodo\iTop\Test\UnitTest\ItopCustomDatamodelTestCaseItopCustomDatamodelTestCase
if (false === is_a($sClass, ItopCustomDatamodelTestCase::class, true)) {
continue;
}
$oReflectionClass = new ReflectionClass($sClass);
$oReflectionMethod = $oReflectionClass->getMethod('GetDatamodelDeltaAbsPath');
// Filter on classes with an actual XML delta (eg. not \Combodo\iTop\Test\UnitTest\ItopCustomDatamodelTestCase and maybe some other deriving from a class with a delta)
if ($oReflectionMethod->isAbstract()) {
continue;
}
/** @var \Combodo\iTop\Test\UnitTest\ItopCustomDatamodelTestCase $oTestClassInstance */
$oTestClassInstance = new $sClass();
// Check test class is for desired environment
if ($oTestClassInstance->GetTestEnvironment() !== $this->sFinalEnv) {
continue;
}
// Check XML delta actually exists
$sDeltaFile = $oTestClassInstance->GetDatamodelDeltaAbsPath();
if (false === is_file($sDeltaFile)) {
$this->fail("Could not prepare '$this->sFinalEnv' as the XML delta file '$sDeltaFile' (used in $sClass) does not seem to exist");
}
// Avoid duplicates
if (in_array($sDeltaFile, $aDeltaFiles)) {
continue;
}
// Prepare fake module name for delta
$sDeltaName = preg_replace('/[^\d\w]/', '', $sDeltaFile);
// Note: We can't use \MFDeltaModule as we can't specify the ID which leads to only 1 delta being applied... In the future we might introduce a new MFXXXModule, but in the meantime it feels alright (GLA / RQU)
$oDelta = new MFCoreModule($sDeltaName, $sDeltaName, $sDeltaFile);
IssueLog::Debug('XML delta found for unit tests', static::class, [
'Unit test class' => $sClass,
'Delta file path' => $sDeltaFile,
]);
$aDeltaFiles[] = $sDeltaFile;
$aRet[$sDeltaName] = $oDelta;
}
return $aRet;
}
}

View File

@@ -5,7 +5,7 @@ namespace Combodo\iTop\Test\UnitTest\Core;
use CMDBSource;
use Combodo\iTop\Test\UnitTest\iTopDataTestCase;
use Combodo\iTop\Test\UnitTest\ItopDataTestCase;
use DateInterval;
use DateTime;
use Expression;
@@ -18,7 +18,7 @@ use ScalarExpression;
* @preserveGlobalState disabled
* @backupGlobals disabled
*/
class ExpressionEvaluateTest extends iTopDataTestCase
class ExpressionEvaluateTest extends ItopDataTestCase
{
const USE_TRANSACTION = false;

View File

@@ -1,9 +1,7 @@
<?php
// Main autoload, this is the one to use in the PHPUnit configuration
// It is customized to include both
// - Vendors
//
// It was previously used to include both the vendor autoloader and our custom base test case classes, but these are now autoloaded from ./src/BasetestCase
// This file should then no longer be necessary, but we have to keep it until projects / branches / modules have been corrected.
require_once 'vendor/autoload.php';
// - Custom test case PHP classes
require_once 'ItopTestCase.php';
require_once 'ItopDataTestCase.php';