mirror of
https://github.com/Combodo/iTop.git
synced 2026-04-23 18:48:51 +02:00
Merge remote-tracking branch 'origin/support/3.1' into support/3.2
# Conflicts: # datamodels/2.x/itop-files-information/dictionaries/zh_cn.dict.itop-files-information.php # datamodels/2.x/itop-oauth-client/dictionaries/zh_cn.dict.itop-oauth-client.php # datamodels/2.x/itop-portal-base/dictionaries/zh_cn.dict.itop-portal-base.php # dictionaries/cs.dictionary.itop.core.php # dictionaries/cs.dictionary.itop.ui.php # dictionaries/zh_cn.dictionary.itop.core.php # dictionaries/zh_cn.dictionary.itop.ui.php # tests/php-unit-tests/README.md
This commit is contained in:
@@ -3,34 +3,24 @@
|
||||
Documentation on creating and maintaining tests in iTop.
|
||||
|
||||
|
||||
Table of content:
|
||||
<!-- TOC -->
|
||||
* [Prerequisites](#prerequisites)
|
||||
* [Create an iTop PHPUnit test](#create-an-itop-phpunit-test)
|
||||
* [Tips: generic PHPUnit](#tips-generic-phpunit)
|
||||
* [Tips: iTop tests](#tips-itop-tests)
|
||||
* [Test performances](#test-performances)
|
||||
* [PHPUnit process isolation](#phpunit-process-isolation)
|
||||
<!-- TOC -->
|
||||
|
||||
|
||||
|
||||
## Prerequisites
|
||||
|
||||
### PHPUnit configuration file
|
||||
### PHPUnit configuration file
|
||||
A default file is located in `/tests/php-unit-tests/phpunit.xml.dist`
|
||||
|
||||
If you need to customize it, copy it to `phpunit.xml` (not versioned).
|
||||
If you need to customize it, copy it to `phpunit.xml` (not versioned).
|
||||
|
||||
### PHP configuration
|
||||
* PHPUnit configuration file
|
||||
- `memory_limit`: as the tests are for the most part ran in the same process, memory usage may become an issue! A default value is set in default PHPUnit configuration XML file, don't hesitate to update it if needed
|
||||
- `memory_limit`: as the tests are for the most part ran in the same process, memory usage may become an issue! A default value is set in default PHPUnit configuration XML file, don't hesitate to update it if needed
|
||||
* PHP CLI php.ini
|
||||
- enable OpCache
|
||||
- disable Xdebug (xdebug.mode=off) : huge performance improvements (between X2 and X3), and we can still debug using PHPStorm !
|
||||
- enable OpCache
|
||||
- disable Xdebug (xdebug.mode=off) : huge performance improvements (between X2 and X3), and we can still debug using PHPStorm !
|
||||
|
||||
### Dependencies
|
||||
Whereas iTop dependencies are bundled inside its repository, the tests dependencies are not, and must be added manually. To do so, run `composer install` in the `/tests/php-unit-tests` directory.
|
||||
Whereas iTop dependencies are bundled inside its repository, the tests dependencies are not, and must be added manually. To do so, run `composer install` in the `/tests/php-unit-tests` directory.
|
||||
|
||||
### iTop instance prerequisites to run its test suite
|
||||
Install iTop with default setup options :
|
||||
@@ -40,11 +30,7 @@ Install iTop with default setup options :
|
||||
- Simple Change Management
|
||||
|
||||
Plus :
|
||||
- Additional ITIL tickets : check "Known Errors Management and FAQ"
|
||||
|
||||
|
||||
|
||||
|
||||
- Additional ITIL tickets : check Known Errors Management and FAQ
|
||||
|
||||
## Create an iTop PHPUnit test
|
||||
|
||||
@@ -53,7 +39,7 @@ Plus :
|
||||
- Covers the consistency of some data through the app? => Most likely in `integration-tests` directory
|
||||
|
||||
### iTop test parent classes
|
||||
iTop provides PHPUnit TestCase children that provides some helpers and setUp/tearDown overrides :
|
||||
iTop provides PHPUnit TestCase children that provides some helpers and setUp/tearDown overrides :
|
||||
- `\Combodo\iTop\Test\UnitTest\ItopTestCase` : for the most simple iTop tests
|
||||
- `\Combodo\iTop\Test\UnitTest\ItopDataTestCase` : to get a started metamodel and have cleanup of CRUD operations on iTop objects (transactions by default)
|
||||
- `\Combodo\iTop\Test\UnitTest\ItopCustomDatamodelTestCase` : to test a non standard datamodel (available since iTop 2.7.9, 3.0.4, 3.1.0 N°6097)
|
||||
@@ -65,7 +51,10 @@ iTop provides PHPUnit TestCase children that provides some helpers and setUp/tea
|
||||
|
||||
Source [PHPUnit Manual – Chapter 2. Writing Tests for PHPUnit](https://docs.phpunit.de/en/9.6/writing-tests-for-phpunit.html#writing-tests-for-phpunit)
|
||||
|
||||
### What about skipped tests ?
|
||||
A test can be marked as skipped by using the `markTestAsSkipped()` PHPUnit method. Please use it only for temporary disabled tests, for example the ones that are pushed before their corresponding fix.
|
||||
|
||||
For other cases like non-relevant data provider cases, just mark the test valid with `assertTrue(true)` and `return`.
|
||||
|
||||
|
||||
|
||||
@@ -80,7 +69,7 @@ $this->markTestSkipped('explanation');
|
||||
### Test an exception
|
||||
Just before calling the code throwing the exception, call `\PHPUnit\Framework\TestCase::expectException`. You might also use `expectExceptionMessage` and/or `expectExceptionMessageMatches`.
|
||||
|
||||
Example :
|
||||
Example :
|
||||
|
||||
```php
|
||||
// Try to delete the tag, must complain !
|
||||
@@ -91,7 +80,7 @@ Example :
|
||||
|
||||
Warning : when the condition is met the test is finished and following code will be ignored !
|
||||
|
||||
Another way to do is using try/catch blocks, for example :
|
||||
Another way to do is using try/catch blocks, for example :
|
||||
```php
|
||||
$validator = new FormValidator();
|
||||
|
||||
@@ -136,10 +125,19 @@ Use `UserRights::Login()`
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## Test performances
|
||||
|
||||
### Memory limit
|
||||
|
||||
As the tests are run in the same process, memory usage
|
||||
may become an issue as soon as tests are all executed at once.
|
||||
|
||||
Fix that in the XML configuration in the PHP section
|
||||
```xml
|
||||
<ini name="memory_limit" value="512M"/>
|
||||
```
|
||||
|
||||
|
||||
### Measure the time spent in a test
|
||||
|
||||
Simply cut'n paste the following line at several places within the test function:
|
||||
@@ -189,8 +187,6 @@ If you can't, then ok you will have to isolate it!
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## PHPUnit process isolation
|
||||
|
||||
### Understand tests interactions
|
||||
@@ -240,7 +236,6 @@ the exact same effect as `@runTestsInSeparateProcesses`.
|
||||
Note : this option is documented only in the [attributes part of the documentation](https://docs.phpunit.de/en/10.0/attributes.html).
|
||||
|
||||
### Traps
|
||||
|
||||
#### Doc block comment format : when it is a matter of stars
|
||||
```php
|
||||
/*
|
||||
|
||||
@@ -16,6 +16,10 @@
|
||||
namespace Combodo\iTop\Test\UnitTest\Integration;
|
||||
|
||||
use Combodo\iTop\Test\UnitTest\ItopTestCase;
|
||||
use Error;
|
||||
use Exception;
|
||||
use const ARRAY_FILTER_USE_BOTH;
|
||||
use const DIRECTORY_SEPARATOR;
|
||||
|
||||
|
||||
/**
|
||||
@@ -26,11 +30,54 @@ use Combodo\iTop\Test\UnitTest\ItopTestCase;
|
||||
*/
|
||||
class Dict
|
||||
{
|
||||
/**
|
||||
* @var bool if true will keep entries in {@see m_aData}
|
||||
*/
|
||||
private static $bLoadEntries = false;
|
||||
|
||||
private static $bSaveKeyDuplicates = false;
|
||||
|
||||
/**
|
||||
* @var array same as the real Dict class : language code as key, value containing array of dict key / label
|
||||
*/
|
||||
public static $m_aData = [];
|
||||
|
||||
public static $aKeysDuplicate = [];
|
||||
|
||||
public static $sLastAddedLanguageCode = null;
|
||||
|
||||
public static function EnableLoadEntries(bool $bSaveKeyDuplicates = false) :void {
|
||||
self::$sLastAddedLanguageCode = null;
|
||||
self::$m_aData = [];
|
||||
self::$aKeysDuplicate = [];
|
||||
self::$bLoadEntries = true;
|
||||
self::$bSaveKeyDuplicates = $bSaveKeyDuplicates;
|
||||
}
|
||||
|
||||
public static function Add($sLanguageCode, $sEnglishLanguageDesc, $sLocalizedLanguageDesc, $aEntries)
|
||||
{
|
||||
if (false === static::$bLoadEntries) {
|
||||
return;
|
||||
}
|
||||
|
||||
static::$sLastAddedLanguageCode = $sLanguageCode;
|
||||
foreach ($aEntries as $sDictKey => $sDictLabel) {
|
||||
if (self::$bSaveKeyDuplicates) {
|
||||
if (isset(static::$m_aData[$sLanguageCode][$sDictKey])) {
|
||||
if (array_key_exists($sDictKey, self::$aKeysDuplicate)) {
|
||||
self::$aKeysDuplicate[$sDictKey]++;
|
||||
} else {
|
||||
self::$aKeysDuplicate[$sDictKey] = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
static::$m_aData[$sLanguageCode][$sDictKey] = $sDictLabel;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* For tests on compiled dict files, see {@see CompiledDictionariesConsistencyTest}
|
||||
* @group beforeSetup
|
||||
@@ -152,23 +199,35 @@ class DictionariesConsistencyTest extends ItopTestCase
|
||||
$this->CheckDictionarySyntax(__DIR__.'/dictionaries-test/fr.dictionary.itop.core.OK.php', true);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $sDictFile complete path for the file to check
|
||||
* @param bool $bIsSyntaxValid expected assert value
|
||||
*/
|
||||
private function CheckDictionarySyntax(string $sDictFile, $bIsSyntaxValid = true): void
|
||||
{
|
||||
private function GetPhpCodeFromDictFile(string $sDictFile) : string {
|
||||
$sPHP = file_get_contents($sDictFile);
|
||||
// Strip php tag to allow "eval"
|
||||
$sPHP = substr(trim($sPHP), strlen('<?php'));
|
||||
// Make sure the Dict class is the one declared in the current file
|
||||
$sPHP = 'namespace '.__NAMESPACE__.";\n".$sPHP;
|
||||
$iLineShift = 1; // Cope with the shift due to the namespace statement
|
||||
|
||||
// we are replacing instead of defining the constant so that if the constant is inside the string it will trigger an error
|
||||
// eg `'UI:Audit:Title' => 'ITOP_APPLICATION_SHORT - CMDB Audit',`
|
||||
// which should be `'UI:Audit:Title' => ITOP_APPLICATION_SHORT.' - CMDB Audit',`
|
||||
// Also we are replacing with - instead of _ as ITOP_APPLICATION_SHORT contains ITOP_APPLICATION and we don't want this replacement to occur
|
||||
$sPHP = str_replace(
|
||||
['ITOP_APPLICATION_SHORT', 'ITOP_APPLICATION', 'ITOP_VERSION_NAME'],
|
||||
['\'itop\'', '\'itop\'', '\'1.2.3\''],
|
||||
['\'CONST__ITOP-APPLICATION-SHORT\'', '\'CONST__ITOP-APPLICATION\'', '\'CONST__ITOP-VERSION-NAME\''],
|
||||
$sPHP
|
||||
);
|
||||
|
||||
return $sPHP;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $sDictFile complete path for the file to check
|
||||
* @param bool $bIsSyntaxValid expected assert value
|
||||
*/
|
||||
private function CheckDictionarySyntax(string $sDictFile, bool $bIsSyntaxValid = true): void
|
||||
{
|
||||
$sPHP = $this->GetPhpCodeFromDictFile($sDictFile);
|
||||
$iLineShift = 1; // Cope with the shift due to the namespace statement added in GetPhpCodeFromDictFile
|
||||
|
||||
try {
|
||||
eval($sPHP);
|
||||
// Reaching this point => No syntax error
|
||||
@@ -176,13 +235,13 @@ class DictionariesConsistencyTest extends ItopTestCase
|
||||
$this->fail("Failed to detect syntax error in dictionary `{$sDictFile}` (which is known as being INCORRECT)");
|
||||
}
|
||||
}
|
||||
catch (\Error $e) {
|
||||
catch (Error $e) {
|
||||
if ($bIsSyntaxValid) {
|
||||
$iLine = $e->getLine() - $iLineShift;
|
||||
$this->fail("Invalid dictionary: {$e->getMessage()} in {$sDictFile}:{$iLine}");
|
||||
}
|
||||
}
|
||||
catch (\Exception $e) {
|
||||
catch (Exception $e) {
|
||||
if ($bIsSyntaxValid) {
|
||||
$iLine = $e->getLine() - $iLineShift;
|
||||
$sExceptionClass = get_class($e);
|
||||
@@ -270,4 +329,139 @@ EOF
|
||||
'templates-base',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider DictionaryFileProvider
|
||||
*/
|
||||
public function testDictKeyDefinedOncePerFile(string $sDictFileToTestFullPath): void {
|
||||
Dict::EnableLoadEntries(true);
|
||||
|
||||
$sDictFileToTestPhp = $this->GetPhpCodeFromDictFile($sDictFileToTestFullPath);
|
||||
eval($sDictFileToTestPhp);
|
||||
|
||||
$aDictKeysDefinedMultipleTimes = [];
|
||||
foreach (Dict::$aKeysDuplicate as $sDictKey => $iNumberOfDuplicates) {
|
||||
$sFirstKeyDeclaration = $this->FindDictKeyLineNumberInContent($sDictFileToTestPhp, $sDictKey);
|
||||
$aDictKeysDefinedMultipleTimes[$sDictKey] = $this->MakeFilePathClickable($sDictFileToTestFullPath, $sFirstKeyDeclaration);
|
||||
}
|
||||
$this->assertEmpty(Dict::$aKeysDuplicate, 'Some keys are defined multiple times in this file:'.var_export($aDictKeysDefinedMultipleTimes, true));
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider DictionaryFileProvider
|
||||
*/
|
||||
public function testNoRemainingTildesInTranslatedKeys(string $sDictFileToTestFullPath): void
|
||||
{
|
||||
Dict::EnableLoadEntries();
|
||||
$sReferenceLangCode = 'EN US';
|
||||
$sReferenceDictName = 'en';
|
||||
|
||||
|
||||
$sDictFileToTestPhp = $this->GetPhpCodeFromDictFile($sDictFileToTestFullPath);
|
||||
eval($sDictFileToTestPhp);
|
||||
|
||||
$sLanguageCodeToTest = Dict::$sLastAddedLanguageCode;
|
||||
if (is_null($sLanguageCodeToTest)) {
|
||||
$this->assertTrue(true, 'No Dict::Add call in this file !');
|
||||
return;
|
||||
}
|
||||
if ($sLanguageCodeToTest === $sReferenceLangCode) {
|
||||
$this->assertTrue(true, 'Not testing reference lang !');
|
||||
return;
|
||||
}
|
||||
if (empty(Dict::$m_aData[$sLanguageCodeToTest])) {
|
||||
$this->assertTrue(true, 'No Dict key defined in this file !');
|
||||
return;
|
||||
}
|
||||
|
||||
$oDictFileToTestInfo = pathinfo($sDictFileToTestFullPath);
|
||||
$sDictFilesDir = $oDictFileToTestInfo['dirname'];
|
||||
$sDictFileToTestFilename = $oDictFileToTestInfo['basename'];
|
||||
$sDictFileReferenceFilename = preg_replace('/^[^.]*./', $sReferenceDictName.'.', $sDictFileToTestFilename);
|
||||
$sDictFileReferenceFullPath = $sDictFilesDir.DIRECTORY_SEPARATOR.$sDictFileReferenceFilename;
|
||||
$sDictFileReferencePhp = $this->GetPhpCodeFromDictFile($sDictFileReferenceFullPath);
|
||||
eval($sDictFileReferencePhp);
|
||||
if (empty(Dict::$m_aData[$sReferenceLangCode])) {
|
||||
$this->assertTrue(true, 'No Dict key defined in the reference file !');
|
||||
return;
|
||||
}
|
||||
|
||||
$aLangToTestDictEntries = Dict::$m_aData[$sLanguageCodeToTest];
|
||||
$aReferenceLangDictEntries = Dict::$m_aData[$sReferenceLangCode];
|
||||
|
||||
|
||||
$this->assertGreaterThan(0, count($aLangToTestDictEntries), 'There should be at least one entry in the dictionary file to test');
|
||||
$aLangToTestDictEntriesNotEmptyValues = array_filter(
|
||||
$aLangToTestDictEntries,
|
||||
static function ($value, $key) {
|
||||
return !empty($value);
|
||||
},
|
||||
ARRAY_FILTER_USE_BOTH
|
||||
);
|
||||
$this->assertNotEmpty($aLangToTestDictEntriesNotEmptyValues);
|
||||
|
||||
|
||||
$aTranslatedKeysWithTildes = [];
|
||||
foreach ($aReferenceLangDictEntries as $sDictKey => $sReferenceLangLabel) {
|
||||
if (false === array_key_exists($sDictKey, $aLangToTestDictEntries)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$sTranslatedLabel = $aLangToTestDictEntries[$sDictKey];
|
||||
|
||||
$bTranslatedLabelHasTildes = preg_match('/~~$/', $sTranslatedLabel) === 1;
|
||||
if (false === $bTranslatedLabelHasTildes) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$sTranslatedLabelWithoutTildes = preg_replace('/~~$/', '', $sTranslatedLabel);
|
||||
if ($sTranslatedLabelWithoutTildes === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($sTranslatedLabelWithoutTildes === $sReferenceLangLabel) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$sDictKeyLineNumberInDictFileToTest = $this->FindDictKeyLineNumberInContent($sDictFileToTestPhp, $sDictKey);
|
||||
$sDictKeyLineNumberInDictFileReference = $this->FindDictKeyLineNumberInContent($sDictFileReferencePhp, $sDictKey);
|
||||
$aTranslatedKeysWithTildes[$sDictKey] = [
|
||||
$sLanguageCodeToTest.'_file_location' => $this->MakeFilePathClickable($sDictFileToTestFullPath, $sDictKeyLineNumberInDictFileToTest),
|
||||
$sLanguageCodeToTest => $sTranslatedLabel,
|
||||
$sReferenceLangCode.'_file_location' => $this->MakeFilePathClickable($sDictFileReferenceFullPath, $sDictKeyLineNumberInDictFileReference),
|
||||
$sReferenceLangCode => $sReferenceLangLabel
|
||||
];
|
||||
}
|
||||
|
||||
$sPathRoot = static::GetAppRoot();
|
||||
$sDictFileToTestRelativePath = str_replace($sPathRoot, '', $sDictFileToTestFullPath);
|
||||
$this->assertEmpty($aTranslatedKeysWithTildes, "In {$sDictFileToTestRelativePath} \n following keys are different from their '{$sReferenceDictName}' counterpart (translated ?) but have tildes at the end:\n" . var_export($aTranslatedKeysWithTildes, true));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $sFullPath
|
||||
* @param int $iLineNumber
|
||||
*
|
||||
* @return string a path that is clickable in PHPStorm 🤩
|
||||
* For this to happen we need full path with correct dir sep + line number
|
||||
* If it is not, check in File | Settings | Tools | Terminal the hyperlink option is checked
|
||||
*/
|
||||
private function MakeFilePathClickable(string $sFullPath, int $iLineNumber):string {
|
||||
return str_replace(array('//', '/'), array('/', DIRECTORY_SEPARATOR), $sFullPath).':'.$iLineNumber;
|
||||
}
|
||||
|
||||
private function FindDictKeyLineNumberInContent(string $sFileContent, string $sDictKey): int
|
||||
{
|
||||
$aContentLines = explode("\n", $sFileContent);
|
||||
$sDictKeyToFind = "'{$sDictKey}'"; // adding string delimiters to match exact dict key (eg if not we would match 'Core:AttributeDateTime?SmartSearch' for 'Core:AttributeDateTime')
|
||||
|
||||
foreach($aContentLines as $iLineNumber => $line) {
|
||||
if(strpos($line, $sDictKeyToFind) !== false){
|
||||
return $iLineNumber;
|
||||
}
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user