diff --git a/tests/php-unit-tests/module_integration.xml.dist b/tests/php-unit-tests/module_integration.xml.dist index 8c18521e6..ad7ff7a99 100644 --- a/tests/php-unit-tests/module_integration.xml.dist +++ b/tests/php-unit-tests/module_integration.xml.dist @@ -19,10 +19,6 @@ printerClass="\Sempro\PHPUnitPrettyPrinter\PrettyPrinterForPhpUnit9" > - - - - diff --git a/tests/php-unit-tests/phpunit.xml.dist b/tests/php-unit-tests/phpunit.xml.dist index 5bab63d01..59c451488 100644 --- a/tests/php-unit-tests/phpunit.xml.dist +++ b/tests/php-unit-tests/phpunit.xml.dist @@ -19,10 +19,6 @@ printerClass="\Sempro\PHPUnitPrettyPrinter\PrettyPrinterForPhpUnit9" > - - - - diff --git a/tests/php-unit-tests/postbuild_integration.xml.dist b/tests/php-unit-tests/postbuild_integration.xml.dist index 95315e322..6f422e804 100644 --- a/tests/php-unit-tests/postbuild_integration.xml.dist +++ b/tests/php-unit-tests/postbuild_integration.xml.dist @@ -19,10 +19,6 @@ printerClass="\Sempro\PHPUnitPrettyPrinter\PrettyPrinterForPhpUnit9" > - - - - diff --git a/tests/php-unit-tests/src/BaseTestCase/ItopCustomDatamodelTestCase.php b/tests/php-unit-tests/src/BaseTestCase/ItopCustomDatamodelTestCase.php index 87d3f734f..6be47bd7f 100644 --- a/tests/php-unit-tests/src/BaseTestCase/ItopCustomDatamodelTestCase.php +++ b/tests/php-unit-tests/src/BaseTestCase/ItopCustomDatamodelTestCase.php @@ -7,7 +7,6 @@ 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; @@ -30,9 +29,9 @@ use utils; abstract class ItopCustomDatamodelTestCase extends ItopDataTestCase { /** - * @var bool[] - */ - protected static $aReadyCustomEnvironments = []; + * @var UnitTestRunTimeEnvironment + */ + protected $oEnvironment = null; /** * @inheritDoc @@ -50,11 +49,19 @@ abstract class ItopCustomDatamodelTestCase extends ItopDataTestCase $this->setRunClassInSeparateProcess(true); } - /** + /** * @return string Abs path to the XML delta to use for the tests of that class */ abstract public function GetDatamodelDeltaAbsPath(): string; + protected function setUp(): void + { + static::LoadRequiredItopFiles(); + $this->oEnvironment = new UnitTestRunTimeEnvironment('production', $this->GetTestEnvironment()); + + parent::setUp(); + } + /** * @inheritDoc */ @@ -92,40 +99,16 @@ abstract class ItopCustomDatamodelTestCase extends ItopDataTestCase } /** - * Mark {@see \Combodo\iTop\Test\UnitTest\ItopDataTestCase::GetTestEnvironment()} as ready (compiled) - * - * @return void - */ - 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 + * @return bool True if the {@see \Combodo\iTop\Test\UnitTest\ItopDataTestCase::GetTestEnvironment()} is ready (compiled, up-to-date, but not necessarily started) */ final protected 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- 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)) { + if (false === file_exists($this->GetTestEnvironmentFolderAbsPath())) { return false; } - - $iRunStartedFileModificationTime = filemtime($sRunStartedFilePath); - $iEnvFolderModificationTime = filemtime($sEnvFolderPath); - - return $iEnvFolderModificationTime >= $iRunStartedFileModificationTime; - } + return $this->oEnvironment->IsUpToDate(); + } /** * @inheritDoc @@ -140,6 +123,12 @@ abstract class ItopCustomDatamodelTestCase extends ItopDataTestCase // 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()) { + + $this->debug("Preparing custom environment '$sTestEnv' with the following datamodel files:"); + foreach ($this->oEnvironment->GetCustomDatamodelFiles() as $sCustomDatamodelFile) { + $this->debug(" - $sCustomDatamodelFile"); + } + //---------------------------------------------------- // Clear any previous "$sTestEnv" environment //---------------------------------------------------- @@ -152,14 +141,6 @@ abstract class ItopCustomDatamodelTestCase extends ItopDataTestCase 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 //---------------------------------------------------- @@ -178,7 +159,7 @@ abstract class ItopCustomDatamodelTestCase extends ItopDataTestCase $oTestConfig->Set('db_name', $oTestConfig->Get('db_name').'_'.$sTestEnvSanitizedForDBName); // - Compile env. based on the existing 'production' env. - $oEnvironment = new UnitTestRunTimeEnvironment($sTestEnv); + $oEnvironment = new UnitTestRunTimeEnvironment($sSourceEnv, $sTestEnv); $oEnvironment->WriteConfigFileSafe($oTestConfig); $oEnvironment->CompileFrom($sSourceEnv); @@ -192,8 +173,7 @@ abstract class ItopCustomDatamodelTestCase extends ItopDataTestCase // N°7446 For some reason we need to create the DB schema before starting the MM, then only we can create the tables. MetaModel::DBCreate(); - $this->MarkEnvironmentReady(); - $this->debug('Preparation of custom environment "'.$sTestEnv.'" done.'); + $this->debug("Custom environment '$sTestEnv' is ready!"); } parent::PrepareEnvironment(); diff --git a/tests/php-unit-tests/src/BaseTestCase/ItopTestCase.php b/tests/php-unit-tests/src/BaseTestCase/ItopTestCase.php index 9d96bc8b4..9bb5b51d2 100644 --- a/tests/php-unit-tests/src/BaseTestCase/ItopTestCase.php +++ b/tests/php-unit-tests/src/BaseTestCase/ItopTestCase.php @@ -50,7 +50,7 @@ abstract class ItopTestCase extends TestCase static::$DEBUG_UNIT_TEST = getenv('DEBUG_UNIT_TEST'); - require_once static::GetAppRoot() . 'approot.inc.php'; + require_once __DIR__.'/../../../../approot.inc.php'; if ((static::DISABLE_DEPRECATEDCALLSLOG_ERRORHANDLER) && (false === defined(ITOP_PHPUNIT_RUNNING_CONSTANT_NAME))) { @@ -78,6 +78,7 @@ abstract class ItopTestCase extends TestCase } } + /** * @param array $args * @param string $sExportFileName relative to log folder @@ -93,43 +94,41 @@ abstract class ItopTestCase extends TestCase * @return string * @throws \ReflectionException */ - public static function ExportFunctionParameterValues(array $args, string $sExportFileName, array $aExcludedParams = []): string - { + public static function ExportFunctionParameterValues(array $args, string $sExportFileName, array $aExcludedParams = []): string + { // get sclass et function dans la callstrack - // in the callstack get the call function name - $aCallStack = debug_backtrace(); - $sCallFunction = $aCallStack[1]['function']; - // in the casll stack get the call class name - $sCallClass = $aCallStack[1]['class']; - $reflectionFunc = new ReflectionMethod($sCallClass, $sCallFunction); - $parameters = $reflectionFunc->getParameters(); + // in the callstack get the call function name + $aCallStack = debug_backtrace(); + $sCallFunction = $aCallStack[1]['function']; + // in the casll stack get the call class name + $sCallClass = $aCallStack[1]['class']; + $reflectionFunc = new ReflectionMethod($sCallClass, $sCallFunction); + $parameters = $reflectionFunc->getParameters(); - $aParamValues = []; - foreach ($parameters as $index => $param) { - $aParamValues[$param->getName()] = $args[$index] ?? null; - } + $aParamValues = []; + foreach ($parameters as $index => $param) { + $aParamValues[$param->getName()] = $args[$index] ?? null; + } - $paramValues = $aParamValues; - foreach ($aExcludedParams as $sExcludedParam) { - unset($paramValues[$sExcludedParam]); - } + $paramValues = $aParamValues; + foreach ($aExcludedParams as $sExcludedParam) { + unset($paramValues[$sExcludedParam]); + } - // extract oPage from the array in parameters and make a foreach on exlucded parameters - foreach ($aExcludedParams as $sExcludedParam) { - unset($paramValues[$sExcludedParam]); - } + // extract oPage from the array in parameters and make a foreach on exlucded parameters + foreach ($aExcludedParams as $sExcludedParam) { + unset($paramValues[$sExcludedParam]); + } - $var_export = var_export($paramValues, true); - file_put_contents(APPROOT.'/log/' .$sExportFileName, $var_export); + $var_export = var_export($paramValues, true); + file_put_contents(APPROOT.'/log/' .$sExportFileName, $var_export); return $var_export; - } + } - protected function setUp(): void { + protected function setUp(): void { parent::setUp(); - $this->debug("\n----------\n---------- ".$this->getName()."\n----------\n"); - $this->LoadRequiredItopFiles(); $this->LoadRequiredTestFiles(); } @@ -178,8 +177,9 @@ abstract class ItopTestCase extends TestCase */ protected function LoadRequiredItopFiles(): void { - // Empty until we actually need to require some files in the class - } + // At least make sure that the autoloader will be loaded, and that the APPROOT constant is defined + require_once __DIR__.'/../../../../approot.inc.php'; + } /** * Overload this method to require necessary files through {@see \Combodo\iTop\Test\UnitTest\ItopTestCase::RequireOnceUnitTestFile()} diff --git a/tests/php-unit-tests/src/Hook/TestsRunStartHook.php b/tests/php-unit-tests/src/Hook/TestsRunStartHook.php deleted file mode 100644 index 7d2f6c212..000000000 --- a/tests/php-unit-tests/src/Hook/TestsRunStartHook.php +++ /dev/null @@ -1,63 +0,0 @@ - - * @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- folder as we have multiple 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()); - } - } - - -} \ No newline at end of file diff --git a/tests/php-unit-tests/src/Service/UnitTestRunTimeEnvironment.php b/tests/php-unit-tests/src/Service/UnitTestRunTimeEnvironment.php index 0c76e727f..7de18f928 100644 --- a/tests/php-unit-tests/src/Service/UnitTestRunTimeEnvironment.php +++ b/tests/php-unit-tests/src/Service/UnitTestRunTimeEnvironment.php @@ -11,6 +11,8 @@ use Combodo\iTop\Test\UnitTest\ItopCustomDatamodelTestCase; use IssueLog; use LogChannels; use MFCoreModule; +use RecursiveDirectoryIterator; +use RecursiveIteratorIterator; use ReflectionClass; use RunTimeEnvironment; use utils; @@ -27,111 +29,140 @@ use utils; class UnitTestRunTimeEnvironment extends RunTimeEnvironment { /** + * @var string[] + */ + protected $aCustomDatamodelFiles = null; + + /** + * @var string + */ + protected $sSourceEnv; + + public function __construct($sSourceEnv, $sTargetEnv) + { + parent::__construct($sTargetEnv); + $this->sSourceEnv = $sSourceEnv; + } + + public function GetEnvironment(): string + { + return $this->sFinalEnv; + } + + public function IsUpToDate() + { + clearstatcache(); + $fLastCompilationTime = filemtime(APPROOT.'env-'.$this->sFinalEnv); + $aModifiedFiles = []; + $this->FindFilesModifiedAfter($fLastCompilationTime, APPROOT.'datamodels/2.x', $aModifiedFiles); + $this->FindFilesModifiedAfter($fLastCompilationTime, APPROOT.'extensions', $aModifiedFiles); + $this->FindFilesModifiedAfter($fLastCompilationTime, APPROOT.'data/production-modules', $aModifiedFiles); + foreach ($this->GetCustomDatamodelFiles() as $sCustomDatamodelFile) { + if (filemtime($sCustomDatamodelFile) > $fLastCompilationTime) { + $aModifiedFiles[] = $sCustomDatamodelFile; + } + } + if (count($aModifiedFiles) > 0) { + echo "The following files have been modified after the last compilation:\n"; + foreach ($aModifiedFiles as $sFile) { + echo " - $sFile\n"; + } + } + return (count($aModifiedFiles) === 0); + } + + /** * @inheritDoc */ protected function GetMFModulesToCompile($sSourceEnv, $sSourceDir) { $aRet = parent::GetMFModulesToCompile($sSourceEnv, $sSourceDir); - /** @var string[] $aDeltaFiles Referential of loaded deltas. Mostly to avoid duplicates. */ - $aDeltaFiles = []; - $aRelatedClasses = $this->GetClassesExtending( - ItopCustomDatamodelTestCase::class, - array( - '[\\\\/]tests[\\\\/]php-unit-tests[\\\\/]vendor[\\\\/]', - '[\\\\/]tests[\\\\/]php-unit-tests[\\\\/]unitary-tests[\\\\/]datamodels[\\\\/]2.x[\\\\/]authent-local', - )); - //Combodo\iTop\Test\UnitTest\Application\ApplicationExtensionTest - //Combodo\iTop\Test\UnitTest\Application\ApplicationExtensionTest - foreach ($aRelatedClasses as $sClass) { - $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; + foreach ($this->GetCustomDatamodelFiles() as $sDeltaFile) { + $sDeltaId = preg_replace('/[^\d\w]/', '', $sDeltaFile); + $sDeltaName = basename($sDeltaFile); + $sDeltaDir = dirname($sDeltaFile); + $oDelta = new MFCoreModule($sDeltaName, "$sDeltaDir/$sDeltaName", $sDeltaFile); + $aRet[$sDeltaId] = $oDelta; } - return $aRet; } - protected function GetClassesExtending (string $sExtendedClass, array $aExcludedPath = []) : array { - $aMatchingClasses = []; - - $aAutoloadClassMaps =[__DIR__."/../../vendor/composer/autoload_classmap.php"]; - - $aClassMap = []; - $aAutoloaderErrors = []; - foreach ($aAutoloadClassMaps as $sAutoloadFile) { - $aTmpClassMap = include $sAutoloadFile; - /** @noinspection SlowArrayOperationsInLoopInspection we are getting an associative array so the documented workarounds cannot be used */ - $aClassMap = array_merge($aClassMap, $aTmpClassMap); + public function GetCustomDatamodelFiles() + { + if (!is_null($this->aCustomDatamodelFiles)) { + return $this->aCustomDatamodelFiles; } - foreach ($aClassMap as $sPHPClass => $sPHPFile) { - $bSkipped = false; - if (utils::IsNotNullOrEmptyString($sPHPFile)) { - $sPHPFile = utils::LocalPath($sPHPFile); - if ($sPHPFile !== false) { - $sPHPFile = '/'.$sPHPFile; // for regex - foreach ($aExcludedPath as $sExcludedPath) { - // Note: We use '#' as delimiters as usual '/' is often used in paths. - if ($sExcludedPath !== '' && preg_match('#'.$sExcludedPath.'#', $sPHPFile) === 1) { - $bSkipped = true; - break; - } - } - } else { - $bSkipped = true; // file not found - } - } + $this->aCustomDatamodelFiles = []; - if (!$bSkipped) { - try { - $oRefClass = new ReflectionClass($sPHPClass); - if ($oRefClass->isSubclassOf($sExtendedClass) && - !$oRefClass->isInterface() && !$oRefClass->isAbstract() && !$oRefClass->isTrait()) { - $aMatchingClasses[] = $sPHPClass; - } + // Search for the PHP files implementing the method GetDatamodelDeltaAbsPath + // and extract the delta file path from the method + foreach(['unitary-tests', 'integration-tests'] as $sTestDir) { + // Iterate on all PHP files in subdirectories + // Note: grep is not available on Windows, so we will use the PHP Reflection API + foreach (new RecursiveIteratorIterator(new RecursiveDirectoryIterator(__DIR__."/../../$sTestDir")) as $oFile) { + if ($oFile->isDir()){ + continue; } - catch (Exception $e) { + if (pathinfo($oFile->getFilename(), PATHINFO_EXTENSION) !== 'php') { + continue; + } + $sFile = $oFile->getPathname(); + $sContent = file_get_contents($sFile); + if (strpos($sContent, 'GetDatamodelDeltaAbsPath') === false) { + continue; + } + $sClass = ''; + $aMatches = []; + if (preg_match('/namespace\s+([^;]+);/', $sContent, $aMatches)) { + $sNamespace = $aMatches[1]; + $sClass = $sNamespace.'\\'.basename($sFile, '.php'); + } + if (preg_match('/\s+class\s+([^ ]+)\s+/', $sContent, $aMatches)) { + $sClass = $sNamespace.'\\'.$aMatches[1]; + } + if ($sClass === '') { + continue; + } + require_once $sFile; + $oReflectionClass = new ReflectionClass($sClass); + if ($oReflectionClass->isAbstract()) { + continue; + } + // Check if the class extends ItopCustomDatamodelTestCase + if (!$oReflectionClass->isSubclassOf(ItopCustomDatamodelTestCase::class)) { + continue; + } + /** @var \Combodo\iTop\Test\UnitTest\ItopCustomDatamodelTestCase $oTestClassInstance */ + $oTestClassInstance = new $sClass(); + if ($oTestClassInstance->GetTestEnvironment() !== $this->sFinalEnv) { + continue; + } + $sDeltaFile = $oTestClassInstance->GetDatamodelDeltaAbsPath(); + if (!is_file($sDeltaFile)) { + throw new \Exception("Unknown delta file: $sDeltaFile, from test class '$sClass'"); + } + if (!in_array($sDeltaFile, $this->aCustomDatamodelFiles)) { + $this->aCustomDatamodelFiles[] = $sDeltaFile; } } } - return $aMatchingClasses; + + return $this->aCustomDatamodelFiles; } - + private function FindFilesModifiedAfter(float $fReferenceTimestamp, string $sPathToScan, array &$aModifiedFiles) + { + if (!is_dir($sPathToScan)) { + return; + } + foreach (new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($sPathToScan)) as $oFile) { + if ($oFile->isDir()) { + continue; + } + if (filemtime($oFile->getPathname()) > $fReferenceTimestamp) { + $aModifiedFiles[] = $oFile->getPathname(); + } + } + } } \ No newline at end of file diff --git a/tests/php-unit-tests/unitary-tests/setup/unattended-install/InstallationFileServiceTest.php b/tests/php-unit-tests/unitary-tests/setup/unattended-install/InstallationFileServiceTest.php index 080af0cfc..8e0cba695 100644 --- a/tests/php-unit-tests/unitary-tests/setup/unattended-install/InstallationFileServiceTest.php +++ b/tests/php-unit-tests/unitary-tests/setup/unattended-install/InstallationFileServiceTest.php @@ -289,8 +289,8 @@ class InstallationFileServiceTest extends ItopTestCase { private function GetMockListOfFoundModules() : array { $sJsonContent = file_get_contents(realpath(__DIR__ . '/resources/AnalyzeInstallation.json')); - $sJsonContent = str_replace('ROOTDIR_TOREPLACE', APPROOT, $sJsonContent); - return json_decode($sJsonContent, true); + $sJsonContent = str_replace('ROOTDIR_TOREPLACE', addslashes(APPROOT), $sJsonContent); + return json_decode($sJsonContent, true); } /** diff --git a/tests/php-unit-tests/unitary-tests/setup/unattended-install/UnattendedInstallTest.php b/tests/php-unit-tests/unitary-tests/setup/unattended-install/UnattendedInstallTest.php index cde7af01a..77116c351 100644 --- a/tests/php-unit-tests/unitary-tests/setup/unattended-install/UnattendedInstallTest.php +++ b/tests/php-unit-tests/unitary-tests/setup/unattended-install/UnattendedInstallTest.php @@ -69,6 +69,12 @@ class UnattendedInstallTest extends ItopDataTestCase $sOutput = implode('\n', $aOutput); var_dump($sOutput); $this->assertStringContainsString("Missing mandatory argument `--param-file`", $sOutput); - $this->assertEquals(255, $iCode); + if (DIRECTORY_SEPARATOR === '\\') { + // Windows + $this->assertEquals(-1, $iCode); + } else { + // Linux + $this->assertEquals(255, $iCode); + } } }