diff --git a/dev/tests/unit/Magento/FunctionalTestFramework/Suite/SuiteGeneratorTest.php b/dev/tests/unit/Magento/FunctionalTestFramework/Suite/SuiteGeneratorTest.php index 136b32e2a..15d6b4a5f 100644 --- a/dev/tests/unit/Magento/FunctionalTestFramework/Suite/SuiteGeneratorTest.php +++ b/dev/tests/unit/Magento/FunctionalTestFramework/Suite/SuiteGeneratorTest.php @@ -3,12 +3,15 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace tests\unit\Magento\FunctionalTestFramework\Suite; -use AspectMock\Test as AspectMock; +use Exception; use Magento\FunctionalTestingFramework\Exceptions\TestReferenceException; -use Magento\FunctionalTestingFramework\ObjectManager\ObjectManager; +use Magento\FunctionalTestingFramework\ObjectManager; use Magento\FunctionalTestingFramework\ObjectManagerFactory; +use Magento\FunctionalTestingFramework\Suite\Service\SuiteGeneratorService; use Magento\FunctionalTestingFramework\Suite\SuiteGenerator; use Magento\FunctionalTestingFramework\Suite\Generators\GroupClassGenerator; use Magento\FunctionalTestingFramework\Suite\Handlers\SuiteObjectHandler; @@ -18,41 +21,32 @@ use Magento\FunctionalTestingFramework\Test\Parsers\TestDataParser; use Magento\FunctionalTestingFramework\Util\GenerationErrorHandler; use Magento\FunctionalTestingFramework\Util\Manifest\DefaultTestManifest; +use ReflectionProperty; use tests\unit\Util\MagentoTestCase; use Magento\FunctionalTestingFramework\Util\Manifest\TestManifestFactory; use tests\unit\Util\SuiteDataArrayBuilder; use tests\unit\Util\TestDataArrayBuilder; use tests\unit\Util\TestLoggingUtil; -use tests\unit\Util\MockModuleResolverBuilder; class SuiteGeneratorTest extends MagentoTestCase { /** - * Setup entry append and clear for Suite Generator - */ - public static function setUpBeforeClass(): void - { - AspectMock::double(SuiteGenerator::class, [ - 'clearPreviousSessionConfigEntries' => null, - 'appendEntriesToConfig' => null - ]); - } - - /** - * Before test functionality + * Before test functionality. + * * @return void */ - public function setUp(): void + protected function setUp(): void { TestLoggingUtil::getInstance()->setMockLoggingUtil(); - $resolverMock = new MockModuleResolverBuilder(); - $resolverMock->setup(); } /** - * Tests generating a single suite given a set of parsed test data + * Tests generating a single suite given a set of parsed test data. + * + * @return void + * @throws Exception */ - public function testGenerateSuite() + public function testGenerateSuite(): void { $suiteDataArrayBuilder = new SuiteDataArrayBuilder(); $mockData = $suiteDataArrayBuilder @@ -74,20 +68,23 @@ public function testGenerateSuite() // parse and generate suite object with mocked data $mockSuiteGenerator = SuiteGenerator::getInstance(); - $mockSuiteGenerator->generateSuite("basicTestSuite"); + $mockSuiteGenerator->generateSuite('basicTestSuite'); // assert that expected suite is generated TestLoggingUtil::getInstance()->validateMockLogStatement( 'info', - "suite generated", - ['suite' => 'basicTestSuite', 'relative_path' => "_generated" . DIRECTORY_SEPARATOR . "basicTestSuite"] + 'suite generated', + ['suite' => 'basicTestSuite', 'relative_path' => '_generated' . DIRECTORY_SEPARATOR . 'basicTestSuite'] ); } /** - * Tests generating all suites given a set of parsed test data + * Tests generating all suites given a set of parsed test data. + * + * @return void + * @throws Exception */ - public function testGenerateAllSuites() + public function testGenerateAllSuites(): void { $suiteDataArrayBuilder = new SuiteDataArrayBuilder(); $mockData = $suiteDataArrayBuilder @@ -108,22 +105,25 @@ public function testGenerateAllSuites() $this->setMockTestAndSuiteParserOutput($mockTestData, $mockData); // parse and retrieve suite object with mocked data - $exampleTestManifest = new DefaultTestManifest([], "sample" . DIRECTORY_SEPARATOR . "path"); + $exampleTestManifest = new DefaultTestManifest([], 'sample' . DIRECTORY_SEPARATOR . 'path'); $mockSuiteGenerator = SuiteGenerator::getInstance(); $mockSuiteGenerator->generateAllSuites($exampleTestManifest); // assert that expected suites are generated TestLoggingUtil::getInstance()->validateMockLogStatement( 'info', - "suite generated", - ['suite' => 'basicTestSuite', 'relative_path' => "_generated" . DIRECTORY_SEPARATOR . "basicTestSuite"] + 'suite generated', + ['suite' => 'basicTestSuite', 'relative_path' => '_generated' . DIRECTORY_SEPARATOR . 'basicTestSuite'] ); } /** - * Tests attempting to generate a suite with no included/excluded tests and no hooks + * Tests attempting to generate a suite with no included/excluded tests and no hooks. + * + * @return void + * @throws Exception */ - public function testGenerateEmptySuite() + public function testGenerateEmptySuite(): void { $testDataArrayBuilder = new TestDataArrayBuilder(); $mockTestData = $testDataArrayBuilder @@ -142,17 +142,20 @@ public function testGenerateEmptySuite() $this->setMockTestAndSuiteParserOutput($mockTestData, $mockData); // set expected error message - $this->expectExceptionMessage("Suite basicTestSuite is not defined in xml or is invalid"); + $this->expectExceptionMessage('Suite basicTestSuite is not defined in xml or is invalid'); // parse and generate suite object with mocked data $mockSuiteGenerator = SuiteGenerator::getInstance(); - $mockSuiteGenerator->generateSuite("basicTestSuite"); + $mockSuiteGenerator->generateSuite('basicTestSuite'); } /** - * Tests generating all suites with a suite containing invalid test reference + * Tests generating all suites with a suite containing invalid test reference. + * + * @return void + * @throws TestReferenceException */ - public function testInvalidSuiteTestPair() + public function testInvalidSuiteTestPair(): void { // Mock Suite1 => Test1 and Suite2 => Test2 $suiteDataArrayBuilder = new SuiteDataArrayBuilder(); @@ -198,9 +201,12 @@ public function testInvalidSuiteTestPair() } /** - * Tests generating all suites with a non-existing suite + * Tests generating all suites with a non-existing suite. + * + * @return void + * @throws TestReferenceException */ - public function testNonExistentSuiteTestPair() + public function testNonExistentSuiteTestPair(): void { $testDataArrayBuilder = new TestDataArrayBuilder(); $mockSimpleTest = $testDataArrayBuilder @@ -227,9 +233,12 @@ public function testNonExistentSuiteTestPair() } /** - * Tests generating split suites for parallel test generation + * Tests generating split suites for parallel test generation. + * + * @return void + * @throws TestReferenceException */ - public function testGenerateSplitSuiteFromTest() + public function testGenerateSplitSuiteFromTest(): void { $suiteDataArrayBuilder = new SuiteDataArrayBuilder(); $mockSuiteData = $suiteDataArrayBuilder @@ -272,8 +281,8 @@ public function testGenerateSplitSuiteFromTest() // assert last split suite group generated TestLoggingUtil::getInstance()->validateMockLogStatement( 'info', - "suite generated", - ['suite' => 'mockSuite_1_G', 'relative_path' => "_generated" . DIRECTORY_SEPARATOR . "mockSuite_1_G"] + 'suite generated', + ['suite' => 'mockSuite_1_G', 'relative_path' => '_generated' . DIRECTORY_SEPARATOR . 'mockSuite_1_G'] ); } @@ -282,75 +291,127 @@ public function testGenerateSplitSuiteFromTest() * * @param array $testData * @param array $suiteData - * @throws \Exception + * + * @return void + * @throws Exception */ - private function setMockTestAndSuiteParserOutput($testData, $suiteData) + private function setMockTestAndSuiteParserOutput(array $testData, array $suiteData): void { - $property = new \ReflectionProperty(SuiteGenerator::class, 'instance'); + $this->clearMockResolverProperties(); + $mockSuiteGeneratorService = $this->createMock(SuiteGeneratorService::class); + $mockVoidReturnCallback = function () {};// phpcs:ignore + + $mockSuiteGeneratorService + ->method('clearPreviousSessionConfigEntries') + ->will($this->returnCallback($mockVoidReturnCallback)); + + $mockSuiteGeneratorService + ->method('appendEntriesToConfig') + ->will($this->returnCallback($mockVoidReturnCallback)); + + $mockSuiteGeneratorService + ->method('generateRelevantGroupTests') + ->will($this->returnCallback($mockVoidReturnCallback)); + + $suiteGeneratorServiceProperty = new ReflectionProperty(SuiteGeneratorService::class, 'INSTANCE'); + $suiteGeneratorServiceProperty->setAccessible(true); + $suiteGeneratorServiceProperty->setValue($mockSuiteGeneratorService); + + $mockDataParser = $this->createMock(TestDataParser::class); + $mockDataParser + ->method('readTestData') + ->willReturn($testData); + + $mockSuiteDataParser = $this->createMock(SuiteDataParser::class); + $mockSuiteDataParser + ->method('readSuiteData') + ->willReturn($suiteData); + + $mockGroupClass = $this->createMock(GroupClassGenerator::class); + $mockGroupClass + ->method('generateGroupClass') + ->willReturn('namespace'); + + $objectManager = ObjectManagerFactory::getObjectManager(); + $objectManagerMockInstance = $this->createMock(ObjectManager::class); + $objectManagerMockInstance + ->method('create') + ->will( + $this->returnCallback( + function ( + string $class, + array $arguments = [] + ) use ( + $mockDataParser, + $mockSuiteDataParser, + $mockGroupClass, + $objectManager + ) { + if ($class == TestDataParser::class) { + return $mockDataParser; + } + if ($class == SuiteDataParser::class) { + return $mockSuiteDataParser; + } + if ($class == GroupClassGenerator::class) { + return $mockGroupClass; + } + + return $objectManager->create($class, $arguments); + } + ) + ); + + $objectManagerProperty = new ReflectionProperty(ObjectManager::class, 'instance'); + $objectManagerProperty->setAccessible(true); + $objectManagerProperty->setValue($objectManagerMockInstance); + } + + /** + * Function used to clear mock properties. + * + * @return void + */ + private function clearMockResolverProperties(): void + { + $property = new ReflectionProperty(SuiteGenerator::class, 'instance'); $property->setAccessible(true); $property->setValue(null); // clear test object handler value to inject parsed content - $property = new \ReflectionProperty(TestObjectHandler::class, 'testObjectHandler'); + $property = new ReflectionProperty(TestObjectHandler::class, 'testObjectHandler'); $property->setAccessible(true); $property->setValue(null); // clear suite object handler value to inject parsed content - $property = new \ReflectionProperty(SuiteObjectHandler::class, 'instance'); + $property = new ReflectionProperty(SuiteObjectHandler::class, 'instance'); $property->setAccessible(true); $property->setValue(null); - - $mockDataParser = AspectMock::double(TestDataParser::class, ['readTestData' => $testData])->make(); - $mockSuiteDataParser = AspectMock::double(SuiteDataParser::class, ['readSuiteData' => $suiteData])->make(); - $mockGroupClass = AspectMock::double( - GroupClassGenerator::class, - ['generateGroupClass' => 'namespace'] - )->make(); - $mockSuiteClass = AspectMock::double(SuiteGenerator::class, ['generateRelevantGroupTests' => null])->make(); - $instance = AspectMock::double( - ObjectManager::class, - ['create' => function ($clazz) use ( - $mockDataParser, - $mockSuiteDataParser, - $mockGroupClass, - $mockSuiteClass - ) { - if ($clazz == TestDataParser::class) { - return $mockDataParser; - } - if ($clazz == SuiteDataParser::class) { - return $mockSuiteDataParser; - } - if ($clazz == GroupClassGenerator::class) { - return $mockGroupClass; - } - if ($clazz == SuiteGenerator::class) { - return $mockSuiteClass; - } - }] - )->make(); - // bypass the private constructor - AspectMock::double(ObjectManagerFactory::class, ['getObjectManager' => $instance]); - - $property = new \ReflectionProperty(SuiteGenerator::class, 'groupClassGenerator'); - $property->setAccessible(true); - $property->setValue($instance, $instance); } /** - * clean up function runs after each test + * @inheritDoc */ - public function tearDown(): void + protected function tearDown(): void { GenerationErrorHandler::getInstance()->reset(); } /** - * clean up function runs after all tests + * @inheritDoc */ public static function tearDownAfterClass(): void { - TestLoggingUtil::getInstance()->clearMockLoggingUtil(); parent::tearDownAfterClass(); + + $objectManagerProperty = new ReflectionProperty(ObjectManager::class, 'instance'); + $objectManagerProperty->setAccessible(true); + $objectManagerProperty->setValue(null); + + $suiteGeneratorServiceProperty = new ReflectionProperty(SuiteGeneratorService::class, 'INSTANCE'); + $suiteGeneratorServiceProperty->setAccessible(true); + $suiteGeneratorServiceProperty->setValue(null); + + TestLoggingUtil::getInstance()->clearMockLoggingUtil(); } } diff --git a/src/Magento/FunctionalTestingFramework/Suite/Service/SuiteGeneratorService.php b/src/Magento/FunctionalTestingFramework/Suite/Service/SuiteGeneratorService.php new file mode 100644 index 000000000..58823e195 --- /dev/null +++ b/src/Magento/FunctionalTestingFramework/Suite/Service/SuiteGeneratorService.php @@ -0,0 +1,160 @@ + $entry) { + if (preg_match('/(Group\\\\.*)/', $entry)) { + unset($newYmlArray[SuiteGenerator::YAML_EXTENSIONS_TAG][SuiteGenerator::YAML_ENABLED_TAG][$key]); + } + } + // needed for proper yml file generation based on indices + $newYmlArray[SuiteGenerator::YAML_EXTENSIONS_TAG][SuiteGenerator::YAML_ENABLED_TAG] = + array_values($newYmlArray[SuiteGenerator::YAML_EXTENSIONS_TAG][SuiteGenerator::YAML_ENABLED_TAG]); + } + + if (array_key_exists(SuiteGenerator::YAML_GROUPS_TAG, $newYmlArray)) { + unset($newYmlArray[SuiteGenerator::YAML_GROUPS_TAG]); + } + $ymlText = SuiteGenerator::YAML_COPYRIGHT_TEXT . Yaml::dump($newYmlArray, 10); + file_put_contents(self::getYamlConfigFilePath() . SuiteGenerator::YAML_CODECEPTION_CONFIG_FILENAME, $ymlText); + } + + /** + * Function which accepts a suite name and suite path and appends a new group entry to the codeception.yml.dist + * file in order to register the set of tests as a new group. Also appends group object location if required + * by suite. + * + * @param string $suiteName + * @param string $suitePath + * @param string|null $groupNamespace + * + * @return void + * @throws TestFrameworkException + */ + public function appendEntriesToConfig(string $suiteName, string $suitePath, ?string $groupNamespace): void + { + $relativeSuitePath = substr($suitePath, strlen(TESTS_BP)); + $relativeSuitePath = ltrim($relativeSuitePath, DIRECTORY_SEPARATOR); + $ymlArray = self::getYamlFileContents(); + + if (!array_key_exists(SuiteGenerator::YAML_GROUPS_TAG, $ymlArray)) { + $ymlArray[SuiteGenerator::YAML_GROUPS_TAG] = []; + } + + if ($groupNamespace) { + $ymlArray[SuiteGenerator::YAML_EXTENSIONS_TAG][SuiteGenerator::YAML_ENABLED_TAG][] = $groupNamespace; + } + + $ymlArray[SuiteGenerator::YAML_GROUPS_TAG][$suiteName] = [$relativeSuitePath]; + $ymlText = SuiteGenerator::YAML_COPYRIGHT_TEXT . Yaml::dump($ymlArray, 10); + file_put_contents(self::getYamlConfigFilePath() . SuiteGenerator::YAML_CODECEPTION_CONFIG_FILENAME, $ymlText); + } + + /** + * Function which takes a string which is the desired output directory (under _generated) and an array of tests + * relevant to the suite to be generated. The function takes this information and creates a new instance of the + * test generator which is then called to create all the test files for the suite. + * + * @param string $path + * @param array $tests + * + * @return void + * @throws TestReferenceException + */ + public function generateRelevantGroupTests(string $path, array $tests): void + { + $testGenerator = TestGenerator::getInstance($path, $tests); + $testGenerator->createAllTestFiles(null, []); + } + + /** + * Function to return contents of codeception.yml file for config changes. + * + * @return array + * @throws TestFrameworkException + */ + private static function getYamlFileContents(): array + { + $configYmlFile = self::getYamlConfigFilePath() . SuiteGenerator::YAML_CODECEPTION_CONFIG_FILENAME; + $defaultConfigYmlFile = self::getYamlConfigFilePath() . SuiteGenerator::YAML_CODECEPTION_DIST_FILENAME; + + if (file_exists($configYmlFile)) { + $ymlContents = file_get_contents($configYmlFile); + } else { + $ymlContents = file_get_contents($defaultConfigYmlFile); + } + + return Yaml::parse($ymlContents) ?? []; + } + + /** + * Static getter for the Config yml filepath (as path cannot be stored in a const). + * + * @return string + * @throws TestFrameworkException + */ + private static function getYamlConfigFilePath(): string + { + return FilePathFormatter::format(TESTS_BP); + } +} diff --git a/src/Magento/FunctionalTestingFramework/Suite/SuiteGenerator.php b/src/Magento/FunctionalTestingFramework/Suite/SuiteGenerator.php index 1f8fec586..ced0ac552 100644 --- a/src/Magento/FunctionalTestingFramework/Suite/SuiteGenerator.php +++ b/src/Magento/FunctionalTestingFramework/Suite/SuiteGenerator.php @@ -8,12 +8,12 @@ use Magento\FunctionalTestingFramework\Exceptions\Collector\ExceptionCollector; use Magento\FunctionalTestingFramework\Exceptions\FastFailException; -use Magento\FunctionalTestingFramework\Exceptions\TestFrameworkException; use Magento\FunctionalTestingFramework\Exceptions\TestReferenceException; use Magento\FunctionalTestingFramework\Exceptions\XmlException; use Magento\FunctionalTestingFramework\Suite\Generators\GroupClassGenerator; use Magento\FunctionalTestingFramework\Suite\Handlers\SuiteObjectHandler; use Magento\FunctionalTestingFramework\Suite\Objects\SuiteObject; +use Magento\FunctionalTestingFramework\Suite\Service\SuiteGeneratorService; use Magento\FunctionalTestingFramework\Test\Handlers\TestObjectHandler; use Magento\FunctionalTestingFramework\Util\Filesystem\DirSetupUtil; use Magento\FunctionalTestingFramework\Util\GenerationErrorHandler; @@ -21,7 +21,6 @@ use Magento\FunctionalTestingFramework\Util\Manifest\BaseTestManifest; use Magento\FunctionalTestingFramework\Util\Path\FilePathFormatter; use Magento\FunctionalTestingFramework\Util\TestGenerator; -use Symfony\Component\Yaml\Yaml; use Magento\FunctionalTestingFramework\Config\MftfApplicationConfig; /** @@ -268,7 +267,7 @@ private function generateSplitSuiteFromTest($suiteName, $suiteContent) try { $this->generateSuiteFromTest($suiteSplitName, $tests, $suiteName); } catch (FastFailException $e) { - throw $e; + throw $e; } catch (\Exception $e) { // There are suites that include tests that reference tests from other Magento editions // To keep backward compatibility, we will catch such exceptions with no error. @@ -331,21 +330,7 @@ private function generateGroupFile($suiteName, $tests, $originalSuiteName) */ private function appendEntriesToConfig($suiteName, $suitePath, $groupNamespace) { - $relativeSuitePath = substr($suitePath, strlen(TESTS_BP)); - $relativeSuitePath = ltrim($relativeSuitePath, DIRECTORY_SEPARATOR); - - $ymlArray = self::getYamlFileContents(); - if (!array_key_exists(self::YAML_GROUPS_TAG, $ymlArray)) { - $ymlArray[self::YAML_GROUPS_TAG]= []; - } - - if ($groupNamespace) { - $ymlArray[self::YAML_EXTENSIONS_TAG][self::YAML_ENABLED_TAG][] = $groupNamespace; - } - $ymlArray[self::YAML_GROUPS_TAG][$suiteName] = [$relativeSuitePath]; - - $ymlText = self::YAML_COPYRIGHT_TEXT . Yaml::dump($ymlArray, 10); - file_put_contents(self::getYamlConfigFilePath() . self::YAML_CODECEPTION_CONFIG_FILENAME, $ymlText); + SuiteGeneratorService::getInstance()->appendEntriesToConfig($suiteName, $suitePath, $groupNamespace); } /** @@ -356,43 +341,23 @@ private function appendEntriesToConfig($suiteName, $suitePath, $groupNamespace) */ private static function clearPreviousSessionConfigEntries() { - $ymlArray = self::getYamlFileContents(); - $newYmlArray = $ymlArray; - // if the yaml entries haven't already been cleared - if (array_key_exists(self::YAML_EXTENSIONS_TAG, $ymlArray)) { - foreach ($ymlArray[self::YAML_EXTENSIONS_TAG][self::YAML_ENABLED_TAG] as $key => $entry) { - if (preg_match('/(Group\\\\.*)/', $entry)) { - unset($newYmlArray[self::YAML_EXTENSIONS_TAG][self::YAML_ENABLED_TAG][$key]); - } - } - - // needed for proper yml file generation based on indices - $newYmlArray[self::YAML_EXTENSIONS_TAG][self::YAML_ENABLED_TAG] = - array_values($newYmlArray[self::YAML_EXTENSIONS_TAG][self::YAML_ENABLED_TAG]); - } - - if (array_key_exists(self::YAML_GROUPS_TAG, $newYmlArray)) { - unset($newYmlArray[self::YAML_GROUPS_TAG]); - } - - $ymlText = self::YAML_COPYRIGHT_TEXT . Yaml::dump($newYmlArray, 10); - file_put_contents(self::getYamlConfigFilePath() . self::YAML_CODECEPTION_CONFIG_FILENAME, $ymlText); + SuiteGeneratorService::getInstance()->clearPreviousSessionConfigEntries(); } /** * Function which takes a string which is the desired output directory (under _generated) and an array of tests - * relevant to the suite to be generated. The function takes this information and creates a new instance of the test - * generator which is then called to create all the test files for the suite. + * relevant to the suite to be generated. The function takes this information and creates a new instance of the + * test generator which is then called to create all the test files for the suite. * * @param string $path * @param array $tests + * * @return void * @throws TestReferenceException */ private function generateRelevantGroupTests($path, $tests) { - $testGenerator = TestGenerator::getInstance($path, $tests); - $testGenerator->createAllTestFiles(null, []); + SuiteGeneratorService::getInstance()->generateRelevantGroupTests($path, $tests); } /** @@ -406,37 +371,6 @@ private static function clearPreviousGroupPreconditions() array_map('unlink', glob("$groupFilePath*.php")); } - /** - * Function to return contents of codeception.yml file for config changes. - * - * @return array - */ - private static function getYamlFileContents() - { - $configYmlFile = self::getYamlConfigFilePath() . self::YAML_CODECEPTION_CONFIG_FILENAME; - $defaultConfigYmlFile = self::getYamlConfigFilePath() . self::YAML_CODECEPTION_DIST_FILENAME; - - $ymlContents = null; - if (file_exists($configYmlFile)) { - $ymlContents = file_get_contents($configYmlFile); - } else { - $ymlContents = file_get_contents($defaultConfigYmlFile); - } - - return Yaml::parse($ymlContents) ?? []; - } - - /** - * Static getter for the Config yml filepath (as path cannot be stored in a const) - * - * @return string - * @throws TestFrameworkException - */ - private static function getYamlConfigFilePath() - { - return FilePathFormatter::format(TESTS_BP); - } - /** * Log error and throw collected exceptions *