N°6097 - Tests: Optimize performances by creating custom env. only once and re-using it across test classes

This commit is contained in:
Molkobain
2023-07-20 19:47:25 +02:00
parent 1ad28312ec
commit ed6df77cbb
9 changed files with 155 additions and 135 deletions

View File

@@ -6,6 +6,7 @@
"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

@@ -6,10 +6,13 @@
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;
@@ -17,38 +20,37 @@ use utils;
/**
* Class ItopCustomDatamodelTestCase
*
* Helper class to extend for tests needing a custom DataModel access to iTop's metamodel
* 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
*
* **⚠ Warning** Each class extending this one needs to NOT have @runTestsInSeparateProcesses annotation; otherwise the test env. will be re-compiled each time.
*
* @runTestsInSeparateProcesseszzz
* @preserveGlobalState disabled
* @backupGlobals disabled
*
* @since 2.7.9 3.0.4 3.1.0
* @since N°6097 2.7.9 3.0.4 3.1.0
*/
abstract class ItopCustomDatamodelTestCase extends ItopDataTestCase
{
/**
* @var bool
* @since N°6097 2.7.10 3.0.4 3.1.1 3.2.0
*
* @note If we change this to an array (with {@see \Combodo\iTop\Test\UnitTest\ItopCustomDatamodelTestCase::GetTestEnvironment()} as the key), we could eventually have several environments in // to test incompatible DMs / deltas.
* @var bool[]
*/
protected static $bIsCustomEnvironmentReady = false;
protected static $aReadyCustomEnvironments = [];
/**
* @inheritDoc
* @since N°6097 Workaround to make the "runClassInSeparateProcess" directive work
*/
protected function setUp(): void
public function __construct($name = null, array $data = [], $dataName = '')
{
parent::setUp();
parent::__construct($name, $data, $dataName);
$sLogFileAbsPath = APPROOT.'log/php_unit_tests_-_custom_datamodel_for_env_-_'.$this->GetTestEnvironment().'.log';
IssueLog::Enable($sLogFileAbsPath);
// 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
*/
@@ -57,8 +59,10 @@ abstract class ItopCustomDatamodelTestCase extends ItopDataTestCase
/**
* @inheritDoc
*/
protected function LoadRequiredFiles(): void
protected function LoadRequiredItopFiles(): void
{
parent::LoadRequiredItopFiles();
$this->RequireOnceItopFile('setup/setuputils.class.inc.php');
$this->RequireOnceItopFile('setup/runtimeenv.class.inc.php');
}
@@ -73,15 +77,57 @@ abstract class ItopCustomDatamodelTestCase extends ItopDataTestCase
/**
* @inheritDoc
*
* This is final for now as we don't support yet to have several environments in // to test incompatible DMs / deltas.
* When / if we do this, keep in mind that 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 test time.
* @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.
*/
final public function GetTestEnvironment(): string
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
*/
@@ -90,11 +136,11 @@ abstract class ItopCustomDatamodelTestCase extends ItopDataTestCase
$sSourceEnv = $this->GetSourceEnvironment();
$sTestEnv = $this->GetTestEnvironment();
// Check if test env. if already set and only prepare it if it doesn't already exist
// 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 === static::$bIsCustomEnvironmentReady) {
if (false === $this->IsEnvironmentReady()) {
//----------------------------------------------------
// Clear any previous "$sTestEnv" environment
//----------------------------------------------------
@@ -137,16 +183,16 @@ abstract class ItopCustomDatamodelTestCase extends ItopDataTestCase
$oEnvironment->WriteConfigFileSafe($oTestConfig);
$oEnvironment->CompileFrom($sSourceEnv, false);
// - Force re-creating of the DB
// // TODO: Create tmp DB
// But how to use it now when the metamodel is not started yet ??
// MetaModel::LoadConfig($oTestConfig);
// if (MetaModel::DBExists()) {
// MetaModel::DBDrop();
// }
// MetaModel::DBCreate();
// - 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);
static::$bIsCustomEnvironmentReady = true;
$this->MarkEnvironmentReady();
$this->debug('Preparation of custom environment "'.$sTestEnv.'" done.');
}
parent::PrepareEnvironment();

View File

@@ -15,6 +15,7 @@ namespace Combodo\iTop\Test\UnitTest;
use ArchivedObjectException;
use CMDBSource;
use Config;
use Contact;
use DBObject;
use DBObjectSet;
@@ -63,6 +64,10 @@ abstract class ItopDataTestCase extends ItopTestCase
// For cleanup
private $aCreatedObjects = array();
/**
* @var string Default environment to use for test cases
*/
const DEFAULT_TEST_ENVIRONMENT = 'production';
const USE_TRANSACTION = true;
const CREATE_TEST_ORG = false;
@@ -126,12 +131,12 @@ abstract class ItopDataTestCase extends ItopTestCase
}
/**
* @return string Environment in the test will run
* @return string Environment the test will run in
* @since 2.7.9 3.0.4 3.1.0
*/
protected function GetTestEnvironment(): string
{
return 'production';
return self::DEFAULT_TEST_ENVIRONMENT;
}
/**

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

@@ -9,7 +9,7 @@ namespace Combodo\iTop\Test\UnitTest\Service;
use Combodo\iTop\Test\UnitTest\ItopCustomDatamodelTestCase;
use IssueLog;
use MFDeltaModule;
use MFCoreModule;
use ReflectionClass;
use RunTimeEnvironment;
@@ -17,7 +17,7 @@ use RunTimeEnvironment;
/**
* Class UnitTestRunTimeEnvironment
*
* Runtime env. dedicated to creating an temp. environment for a group of unit tests with XML deltas.
* 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
@@ -68,7 +68,8 @@ class UnitTestRunTimeEnvironment extends RunTimeEnvironment
// Prepare fake module name for delta
$sDeltaName = preg_replace('/[^\d\w]/', '', $sDeltaFile);
$oDelta = new MFDeltaModule($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,

View File

@@ -1,14 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<itop_design xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="1.7">
<classes>
<class id="Person">
<fields>
<field id="tested_attribute2" xsi:type="AttributeString" _delta="define">
<sql>tested_attribute2</sql>
<default_value/>
<is_null_allowed>true</is_null_allowed>
</field>
</fields>
</class>
</classes>
</itop_design>

View File

@@ -1,34 +0,0 @@
<?php
/*
* @copyright Copyright (C) 2010-2023 Combodo SARL
* @license http://opensource.org/licenses/AGPL-3.0
*/
namespace Combodo\iTop\Test\UnitTest\Core;
use Combodo\iTop\Test\UnitTest\ItopCustomDatamodelTestCase;
use DBObjectSet;
use DBSearch;
use MetaModel;
/**
* @runTestsInSeparateProcesseszzz
* @preserveGlobalState disabled
* @backupGlobals disabled
*/
class TestGLA2Test extends ItopCustomDatamodelTestCase
{
/**
* @inheritDoc
*/
public function GetDatamodelDeltaAbsPath(): string
{
return APPROOT.'tests/php-unit-tests/unitary-tests/core/TestGLA/TestGLA2Test.delta.xml';
}
public function testFoo()
{
static::assertFalse(MetaModel::IsValidAttCode('Person', 'non_existing_attribute2'));
static::assertTrue(MetaModel::IsValidAttCode('Person', 'tested_attribute2'));
}
}

View File

@@ -1,14 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<itop_design xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="1.7">
<classes>
<class id="Person">
<fields>
<field id="tested_attribute" xsi:type="AttributeString" _delta="define">
<sql>tested_attribute</sql>
<default_value/>
<is_null_allowed>true</is_null_allowed>
</field>
</fields>
</class>
</classes>
</itop_design>

View File

@@ -1,34 +0,0 @@
<?php
/*
* @copyright Copyright (C) 2010-2023 Combodo SARL
* @license http://opensource.org/licenses/AGPL-3.0
*/
namespace Combodo\iTop\Test\UnitTest\Core;
use Combodo\iTop\Test\UnitTest\ItopCustomDatamodelTestCase;
use DBObjectSet;
use DBSearch;
use MetaModel;
/**
* @runTestsInSeparateProcesseszzz
* @preserveGlobalState disabled
* @backupGlobals disabled
*/
class TestGLATest extends ItopCustomDatamodelTestCase
{
/**
* @inheritDoc
*/
public function GetDatamodelDeltaAbsPath(): string
{
return APPROOT.'tests/php-unit-tests/unitary-tests/core/TestGLA/TestGLATest.delta.xml';
}
public function testFoo()
{
static::assertFalse(MetaModel::IsValidAttCode('Person', 'non_existing_attribute'));
static::assertTrue(MetaModel::IsValidAttCode('Person', 'tested_attribute'));
}
}