From ccaf2dc5b797da634e67188ce10b5708c37ff695 Mon Sep 17 00:00:00 2001 From: Romain Quetiez Date: Fri, 3 May 2024 15:24:59 +0200 Subject: [PATCH 1/2] Make the tests compatible with windows (and linux) --- .../unitary-tests/core/iTopConfigParserTest.php | 6 +++--- .../unattended-install/InstallationFileServiceTest.php | 4 ++-- .../setup/unattended-install/UnattendedInstallTest.php | 8 +++++++- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/tests/php-unit-tests/unitary-tests/core/iTopConfigParserTest.php b/tests/php-unit-tests/unitary-tests/core/iTopConfigParserTest.php index 335016c39..60a39d413 100644 --- a/tests/php-unit-tests/unitary-tests/core/iTopConfigParserTest.php +++ b/tests/php-unit-tests/unitary-tests/core/iTopConfigParserTest.php @@ -21,7 +21,7 @@ class iTopConfigParserTest extends ItopTestCase clearstatcache(); $this->sConfigPath = utils::GetConfigFilePath(); - $this->tmpSavePath = tempnam('/tmp/', 'config-itop'); + $this->tmpSavePath = tempnam(sys_get_temp_dir(), 'config-itop'); $this->conf_exists = is_file($this->sConfigPath); if ($this->conf_exists) @@ -156,8 +156,8 @@ class iTopConfigParserTest extends ItopTestCase */ public function testConfigWriteToFile() { - $tmpConfigFileBeforePath = tempnam( '/tmp/', 'config-itop'); - $tmpConfigFileAfterPath = tempnam( '/tmp/', 'config-itop'); + $tmpConfigFileBeforePath = tempnam( sys_get_temp_dir(), 'config-itop'); + $tmpConfigFileAfterPath = tempnam( sys_get_temp_dir(), 'config-itop'); //create new config file $sConfigFile = utils::GetConfig()->GetLoadedFile(); 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 b463cff36..6b2f8196c 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 @@ -287,8 +287,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); + } } } From 53dc452d615bd8536e641fe97b7c46f9f54c7a54 Mon Sep 17 00:00:00 2001 From: Romain Quetiez Date: Tue, 30 Apr 2024 09:45:44 +0200 Subject: [PATCH 2/2] :white_check_mark: Avoid unnecessary custom test environment compilations (base compilation of file modification time) --- .../module_integration.xml.dist | 4 - tests/php-unit-tests/phpunit.xml.dist | 4 - .../postbuild_integration.xml.dist | 4 - .../ItopCustomDatamodelTestCase.php | 69 ++---- .../src/BaseTestCase/ItopTestCase.php | 17 +- .../src/Hook/TestsRunStartHook.php | 63 ------ .../Service/UnitTestRunTimeEnvironment.php | 209 ++++++++++-------- 7 files changed, 147 insertions(+), 223 deletions(-) delete mode 100644 tests/php-unit-tests/src/Hook/TestsRunStartHook.php diff --git a/tests/php-unit-tests/module_integration.xml.dist b/tests/php-unit-tests/module_integration.xml.dist index 7c646412f..b8bbd28ed 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\PrettyPrinter" > - - - - diff --git a/tests/php-unit-tests/phpunit.xml.dist b/tests/php-unit-tests/phpunit.xml.dist index 1e322b14a..fed259d40 100644 --- a/tests/php-unit-tests/phpunit.xml.dist +++ b/tests/php-unit-tests/phpunit.xml.dist @@ -19,10 +19,6 @@ printerClass="Sempro\PHPUnitPrettyPrinter\PrettyPrinter" > - - - - diff --git a/tests/php-unit-tests/postbuild_integration.xml.dist b/tests/php-unit-tests/postbuild_integration.xml.dist index 3cb2ad5cf..224ebe280 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\PrettyPrinter" > - - - - diff --git a/tests/php-unit-tests/src/BaseTestCase/ItopCustomDatamodelTestCase.php b/tests/php-unit-tests/src/BaseTestCase/ItopCustomDatamodelTestCase.php index b7e62717b..2eaf146b9 100644 --- a/tests/php-unit-tests/src/BaseTestCase/ItopCustomDatamodelTestCase.php +++ b/tests/php-unit-tests/src/BaseTestCase/ItopCustomDatamodelTestCase.php @@ -7,11 +7,9 @@ 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; @@ -31,9 +29,9 @@ use utils; abstract class ItopCustomDatamodelTestCase extends ItopDataTestCase { /** - * @var bool[] - */ - protected static $aReadyCustomEnvironments = []; + * @var UnitTestRunTimeEnvironment + */ + protected $oEnvironment = null; /** * @inheritDoc @@ -51,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 */ @@ -93,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 @@ -141,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 //---------------------------------------------------- @@ -153,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 //---------------------------------------------------- @@ -179,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, false); @@ -194,8 +174,7 @@ abstract class ItopCustomDatamodelTestCase extends ItopDataTestCase // In 2.7, we can't call MetaModel::DBCreate() directly as the views creation will fail $this->InvokeNonPublicStaticMethod(MetaModel::class, 'DBCreateTables', []); - $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 9158c9a61..90df080d0 100644 --- a/tests/php-unit-tests/src/BaseTestCase/ItopTestCase.php +++ b/tests/php-unit-tests/src/BaseTestCase/ItopTestCase.php @@ -28,20 +28,8 @@ abstract class ItopTestCase extends TestCase protected static $aBackupStaticProperties = []; - /** @noinspection UsingInclusionOnceReturnValueInspection avoid errors for approot includes */ protected function setUp(): void { - $sAppRootRelPath = 'approot.inc.php'; - $sDepthSeparator = '../'; - for ($iDepth = 0; $iDepth < 8; $iDepth++) { - if (file_exists($sAppRootRelPath)) { - require_once $sAppRootRelPath; - break; - } - - $sAppRootRelPath = $sDepthSeparator.$sAppRootRelPath; - } - $this->LoadRequiredItopFiles(); $this->LoadRequiredTestFiles(); } @@ -68,8 +56,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