mirror of
https://github.com/Combodo/iTop.git
synced 2026-03-01 23:24:12 +01:00
✅ Avoid unnecessary custom test environment compilations (base compilation of file modification time)
This commit is contained in:
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user