mirror of
https://github.com/Combodo/iTop.git
synced 2026-04-22 01:58:47 +02:00
Merge remote-tracking branch 'origin/support/3.0' into support/3.1
# Conflicts: # datamodels/2.x/combodo-db-tools/dictionaries/zh_cn.dict.combodo-db-tools.php # datamodels/2.x/itop-attachments/dictionaries/zh_cn.dict.itop-attachments.php # datamodels/2.x/itop-backup/dictionaries/da.dict.itop-backup.php # datamodels/2.x/itop-backup/dictionaries/it.dict.itop-backup.php # datamodels/2.x/itop-backup/dictionaries/ja.dict.itop-backup.php # datamodels/2.x/itop-backup/dictionaries/sk.dict.itop-backup.php # datamodels/2.x/itop-backup/dictionaries/tr.dict.itop-backup.php # datamodels/2.x/itop-config-mgmt/dictionaries/it.dict.itop-config-mgmt.php # datamodels/2.x/itop-config/dictionaries/da.dict.itop-config.php # datamodels/2.x/itop-config/dictionaries/it.dict.itop-config.php # datamodels/2.x/itop-config/dictionaries/ja.dict.itop-config.php # datamodels/2.x/itop-config/dictionaries/sk.dict.itop-config.php # datamodels/2.x/itop-config/dictionaries/tr.dict.itop-config.php # datamodels/2.x/itop-core-update/dictionaries/cs.dict.itop-core-update.php # datamodels/2.x/itop-core-update/dictionaries/da.dict.itop-core-update.php # datamodels/2.x/itop-core-update/dictionaries/it.dict.itop-core-update.php # datamodels/2.x/itop-core-update/dictionaries/ja.dict.itop-core-update.php # datamodels/2.x/itop-core-update/dictionaries/sk.dict.itop-core-update.php # datamodels/2.x/itop-core-update/dictionaries/tr.dict.itop-core-update.php # datamodels/2.x/itop-core-update/dictionaries/zh_cn.dict.itop-core-update.php # datamodels/2.x/itop-faq-light/dictionaries/zh_cn.dict.itop-faq-light.php # datamodels/2.x/itop-files-information/dictionaries/de.dict.itop-files-information.php # datamodels/2.x/itop-files-information/dictionaries/pl.dict.itop-files-information.php # datamodels/2.x/itop-files-information/dictionaries/zh_cn.dict.itop-files-information.php # datamodels/2.x/itop-hub-connector/dictionaries/pt_br.dict.itop-hub-connector.php # datamodels/2.x/itop-oauth-client/dictionaries/cs.dict.itop-oauth-client.php # datamodels/2.x/itop-oauth-client/dictionaries/da.dict.itop-oauth-client.php # datamodels/2.x/itop-oauth-client/dictionaries/es_cr.dict.itop-oauth-client.php # datamodels/2.x/itop-oauth-client/dictionaries/it.dict.itop-oauth-client.php # datamodels/2.x/itop-oauth-client/dictionaries/ja.dict.itop-oauth-client.php # datamodels/2.x/itop-oauth-client/dictionaries/nl.dict.itop-oauth-client.php # datamodels/2.x/itop-oauth-client/dictionaries/pt_br.dict.itop-oauth-client.php # datamodels/2.x/itop-oauth-client/dictionaries/ru.dict.itop-oauth-client.php # datamodels/2.x/itop-oauth-client/dictionaries/sk.dict.itop-oauth-client.php # datamodels/2.x/itop-oauth-client/dictionaries/tr.dict.itop-oauth-client.php # datamodels/2.x/itop-oauth-client/dictionaries/zh_cn.dict.itop-oauth-client.php # datamodels/2.x/itop-service-mgmt-provider/dictionaries/de.dict.itop-service-mgmt-provider.php # datamodels/2.x/itop-tickets/dictionaries/da.dict.itop-tickets.php # datamodels/2.x/itop-tickets/dictionaries/hu.dict.itop-tickets.php # datamodels/2.x/itop-tickets/dictionaries/it.dict.itop-tickets.php # datamodels/2.x/itop-tickets/dictionaries/ja.dict.itop-tickets.php # dictionaries/cs.dictionary.itop.ui.php # dictionaries/da.dictionary.itop.core.php # dictionaries/da.dictionary.itop.ui.php # dictionaries/de.dictionary.itop.ui.php # dictionaries/en.dictionary.itop.ui.php # dictionaries/es_cr.dictionary.itop.ui.php # dictionaries/fr.dictionary.itop.ui.php # dictionaries/hu.dictionary.itop.ui.php # dictionaries/it.dictionary.itop.core.php # dictionaries/it.dictionary.itop.ui.php # dictionaries/ja.dictionary.itop.core.php # dictionaries/ja.dictionary.itop.ui.php # dictionaries/nl.dictionary.itop.ui.php # dictionaries/pl.dictionary.itop.core.php # dictionaries/pl.dictionary.itop.ui.php # dictionaries/pt_br.dictionary.itop.core.php # dictionaries/pt_br.dictionary.itop.ui.php # dictionaries/ru.dictionary.itop.core.php # dictionaries/ru.dictionary.itop.ui.php # dictionaries/sk.dictionary.itop.core.php # dictionaries/sk.dictionary.itop.ui.php # dictionaries/tr.dictionary.itop.core.php # dictionaries/tr.dictionary.itop.ui.php # dictionaries/zh_cn.dictionary.itop.core.php # dictionaries/zh_cn.dictionary.itop.ui.php # tests/php-unit-tests/integration-tests/DictionariesConsistencyTest.php
This commit is contained in:
@@ -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