diff --git a/dev/tests/functional/tests/MFTF/DevDocs/Page/MFTFDocPage.xml b/dev/tests/functional/tests/MFTF/DevDocsTest/Page/MFTFDocPage.xml similarity index 100% rename from dev/tests/functional/tests/MFTF/DevDocs/Page/MFTFDocPage.xml rename to dev/tests/functional/tests/MFTF/DevDocsTest/Page/MFTFDocPage.xml diff --git a/dev/tests/functional/tests/MFTF/DevDocs/Section/ContentSection.xml b/dev/tests/functional/tests/MFTF/DevDocsTest/Section/ContentSection.xml similarity index 100% rename from dev/tests/functional/tests/MFTF/DevDocs/Section/ContentSection.xml rename to dev/tests/functional/tests/MFTF/DevDocsTest/Section/ContentSection.xml diff --git a/dev/tests/functional/tests/MFTF/DevDocs/Test/DevDocsTest.xml b/dev/tests/functional/tests/MFTF/DevDocsTest/Test/DevDocsTest.xml similarity index 100% rename from dev/tests/functional/tests/MFTF/DevDocs/Test/DevDocsTest.xml rename to dev/tests/functional/tests/MFTF/DevDocsTest/Test/DevDocsTest.xml diff --git a/dev/tests/unit/Magento/FunctionalTestFramework/Suite/Handlers/SuiteObjectHandlerTest.php b/dev/tests/unit/Magento/FunctionalTestFramework/Suite/Handlers/SuiteObjectHandlerTest.php index 99125a9e3..5bf29507c 100644 --- a/dev/tests/unit/Magento/FunctionalTestFramework/Suite/Handlers/SuiteObjectHandlerTest.php +++ b/dev/tests/unit/Magento/FunctionalTestFramework/Suite/Handlers/SuiteObjectHandlerTest.php @@ -15,9 +15,16 @@ use Magento\FunctionalTestingFramework\Util\MagentoTestCase; use tests\unit\Util\SuiteDataArrayBuilder; use tests\unit\Util\TestDataArrayBuilder; +use tests\unit\Util\MockModuleResolverBuilder; class SuiteObjectHandlerTest extends MagentoTestCase { + public function setUp() + { + $resolverMock = new MockModuleResolverBuilder(); + $resolverMock->setup(); + } + /** * Tests basic parsing and accesors of suite object and suite object supporting classes */ diff --git a/dev/tests/unit/Magento/FunctionalTestFramework/Suite/SuiteGeneratorTest.php b/dev/tests/unit/Magento/FunctionalTestFramework/Suite/SuiteGeneratorTest.php index eb6298afc..29158b0f5 100644 --- a/dev/tests/unit/Magento/FunctionalTestFramework/Suite/SuiteGeneratorTest.php +++ b/dev/tests/unit/Magento/FunctionalTestFramework/Suite/SuiteGeneratorTest.php @@ -20,10 +20,10 @@ 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 */ @@ -42,6 +42,8 @@ public static function setUpBeforeClass() public function setUp() { TestLoggingUtil::getInstance()->setMockLoggingUtil(); + $resolverMock = new MockModuleResolverBuilder(); + $resolverMock->setup(); } /** diff --git a/dev/tests/unit/Magento/FunctionalTestFramework/Test/Handlers/TestObjectHandlerTest.php b/dev/tests/unit/Magento/FunctionalTestFramework/Test/Handlers/TestObjectHandlerTest.php index 1dbeb50db..08571b507 100644 --- a/dev/tests/unit/Magento/FunctionalTestFramework/Test/Handlers/TestObjectHandlerTest.php +++ b/dev/tests/unit/Magento/FunctionalTestFramework/Test/Handlers/TestObjectHandlerTest.php @@ -8,7 +8,6 @@ use AspectMock\Test as AspectMock; -use Go\Aop\Aspect; use Magento\FunctionalTestingFramework\ObjectManager; use Magento\FunctionalTestingFramework\ObjectManagerFactory; use Magento\FunctionalTestingFramework\Test\Handlers\TestObjectHandler; @@ -16,10 +15,10 @@ use Magento\FunctionalTestingFramework\Test\Objects\TestHookObject; use Magento\FunctionalTestingFramework\Test\Objects\TestObject; use Magento\FunctionalTestingFramework\Test\Parsers\TestDataParser; -use Magento\FunctionalTestingFramework\Test\Util\ActionObjectExtractor; use Magento\FunctionalTestingFramework\Test\Util\TestObjectExtractor; use Magento\FunctionalTestingFramework\Util\MagentoTestCase; use tests\unit\Util\TestDataArrayBuilder; +use tests\unit\Util\MockModuleResolverBuilder; class TestObjectHandlerTest extends MagentoTestCase { @@ -40,10 +39,13 @@ public function testGetTestObject() ->withTestActions() ->build(); + $resolverMock = new MockModuleResolverBuilder(); + $resolverMock->setup(); $this->setMockParserOutput(['tests' => $mockData]); // run object handler method $toh = TestObjectHandler::getInstance(); + $mockConfig = AspectMock::double(TestObjectHandler::class, ['initTestData' => false]); $actualTestObject = $toh->getObject($testDataArrayBuilder->testName); // perform asserts @@ -130,6 +132,8 @@ public function testGetTestsByGroup() ->withTestActions() ->build(); + $resolverMock = new MockModuleResolverBuilder(); + $resolverMock->setup(); $this->setMockParserOutput(['tests' => array_merge($includeTest, $excludeTest)]); // execute test method @@ -150,16 +154,20 @@ public function testGetTestsByGroup() public function testGetTestWithModuleName() { // set up Test Data - $moduleExpected = "SomeTestModule"; + $moduleExpected = "SomeModuleName"; + $moduleExpectedTest = $moduleExpected . "Test"; $filepath = DIRECTORY_SEPARATOR . - "user" . + "user" . DIRECTORY_SEPARATOR . "magento2ce" . DIRECTORY_SEPARATOR . "dev" . DIRECTORY_SEPARATOR . "tests" . DIRECTORY_SEPARATOR . "acceptance" . DIRECTORY_SEPARATOR . "tests" . DIRECTORY_SEPARATOR . - $moduleExpected . DIRECTORY_SEPARATOR . - "Tests" . DIRECTORY_SEPARATOR . + "functional" . DIRECTORY_SEPARATOR . + "Vendor" . DIRECTORY_SEPARATOR . + $moduleExpectedTest; + $file = $filepath . DIRECTORY_SEPARATOR . + "Test" . DIRECTORY_SEPARATOR . "text.xml"; // set up mock data $testDataArrayBuilder = new TestDataArrayBuilder(); @@ -169,8 +177,12 @@ public function testGetTestWithModuleName() ->withAfterHook() ->withBeforeHook() ->withTestActions() - ->withFileName($filepath) + ->withFileName($file) ->build(); + + $resolverMock = new MockModuleResolverBuilder(); + $resolverMock->setup(['Vendor_' . $moduleExpected => [$filepath]]); + $this->setMockParserOutput(['tests' => $mockData]); // Execute Test Method $toh = TestObjectHandler::getInstance(); @@ -198,4 +210,14 @@ private function setMockParserOutput($data) ->make(); // bypass the private constructor AspectMock::double(ObjectManagerFactory::class, ['getObjectManager' => $instance]); } + + /** + * After method functionality + * + * @return void + */ + public function tearDown() + { + AspectMock::clean(); + } } diff --git a/dev/tests/unit/Magento/FunctionalTestFramework/Test/Util/ObjectExtensionUtilTest.php b/dev/tests/unit/Magento/FunctionalTestFramework/Test/Util/ObjectExtensionUtilTest.php index b023b16c7..3b43ba00f 100644 --- a/dev/tests/unit/Magento/FunctionalTestFramework/Test/Util/ObjectExtensionUtilTest.php +++ b/dev/tests/unit/Magento/FunctionalTestFramework/Test/Util/ObjectExtensionUtilTest.php @@ -20,6 +20,7 @@ use PHPUnit\Framework\TestCase; use tests\unit\Util\TestDataArrayBuilder; use tests\unit\Util\TestLoggingUtil; +use tests\unit\Util\MockModuleResolverBuilder; class ObjectExtensionUtilTest extends TestCase { @@ -30,6 +31,8 @@ class ObjectExtensionUtilTest extends TestCase public function setUp() { TestLoggingUtil::getInstance()->setMockLoggingUtil(); + $resolverMock = new MockModuleResolverBuilder(); + $resolverMock->setup(); } /** diff --git a/dev/tests/unit/Magento/FunctionalTestFramework/Util/ModulePathExtractorTest.php b/dev/tests/unit/Magento/FunctionalTestFramework/Util/ModulePathExtractorTest.php index 5efa6384b..4fb18d383 100644 --- a/dev/tests/unit/Magento/FunctionalTestFramework/Util/ModulePathExtractorTest.php +++ b/dev/tests/unit/Magento/FunctionalTestFramework/Util/ModulePathExtractorTest.php @@ -7,103 +7,128 @@ namespace tests\unit\Magento\FunctionalTestFramework\Test\Util; use Magento\FunctionalTestingFramework\Util\ModulePathExtractor; -use PHPUnit\Framework\TestCase; +use Magento\FunctionalTestingFramework\Util\MagentoTestCase; +use tests\unit\Util\MockModuleResolverBuilder; -class ModulePathExtractorTest extends TestCase +class ModulePathExtractorTest extends MagentoTestCase { - const EXTENSION_PATH = "app" - . DIRECTORY_SEPARATOR - . "code" - . DIRECTORY_SEPARATOR - . "TestExtension" - . DIRECTORY_SEPARATOR - . "[Analytics]" - . DIRECTORY_SEPARATOR - . "Test" - . DIRECTORY_SEPARATOR - . "Mftf" - . DIRECTORY_SEPARATOR - . "Test" - . DIRECTORY_SEPARATOR - . "SomeText.xml"; - - const MAGENTO_PATH = "dev" - . DIRECTORY_SEPARATOR - . "tests" - . DIRECTORY_SEPARATOR - . "acceptance" - . DIRECTORY_SEPARATOR - . "tests" - . DIRECTORY_SEPARATOR - . "functional" - . DIRECTORY_SEPARATOR - . "Magento" - . DIRECTORY_SEPARATOR - . "FunctionalTest" - . DIRECTORY_SEPARATOR - . "[Analytics]" - . DIRECTORY_SEPARATOR - . "Test" - . DIRECTORY_SEPARATOR - . "SomeText.xml"; + /** + * Mock test module paths + * + * @var array + */ + private $mockTestModulePaths = [ + 'Magento_ModuleA' => ['/base/path/app/code/Magento/ModuleA/Test/Mftf'], + 'VendorB_ModuleB' => ['/base/path/app/code/VendorB/ModuleB/Test/Mftf'], + 'Magento_ModuleC' => ['/base/path/dev/tests/acceptance/tests/functional/Magento/ModuleCTest'], + 'VendorD_ModuleD' => ['/base/path/dev/tests/acceptance/tests/functional/VendorD/ModuleDTest'], + 'SomeModuleE' => ['/base/path/dev/tests/acceptance/tests/functional/FunctionalTest/SomeModuleE'], + 'Magento_ModuleF' => ['/base/path/vendor/magento/module-modulef/Test/Mftf'], + 'VendorG_ModuleG' => ['/base/path/vendor/vendorg/module-moduleg-test'], + ]; + + /** + * Validate module for app/code path + * + * @throws \Exception + */ + public function testGetModuleAppCode() + { + $mockPath = '/base/path/app/code/Magento/ModuleA/Test/Mftf/Test/SomeTest.xml'; + + $resolverMock = new MockModuleResolverBuilder(); + $resolverMock->setup($this->mockTestModulePaths); + $extractor = new ModulePathExtractor(); + $this->assertEquals('ModuleA', $extractor->extractModuleName($mockPath)); + } /** - * Validate correct module is returned for dev/tests path + * Validate vendor for app/code path + * * @throws \Exception */ - public function testGetMagentoModule() + public function testGetVendorAppCode() { - $modulePathExtractor = new ModulePathExtractor(); - $this->assertEquals( - '[Analytics]', - $modulePathExtractor->extractModuleName( - self::MAGENTO_PATH - ) - ); + $mockPath = '/base/path/app/code/VendorB/ModuleB/Test/Mftf/Test/SomeTest.xml'; + + $resolverMock = new MockModuleResolverBuilder(); + $resolverMock->setup($this->mockTestModulePaths); + $extractor = new ModulePathExtractor(); + $this->assertEquals('VendorB', $extractor->getExtensionPath($mockPath)); } /** - * Validate correct module is returned for extension path + * Validate module for dev/tests path + * * @throws \Exception */ - public function testGetExtensionModule() + public function testGetModuleDevTests() { - $modulePathExtractor = new ModulePathExtractor(); - $this->assertEquals( - '[Analytics]', - $modulePathExtractor->extractModuleName( - self::EXTENSION_PATH - ) - ); + $mockPath = '/base/path/dev/tests/acceptance/tests/functional/Magento/ModuleCTest/Test/SomeTest.xml'; + + $resolverMock = new MockModuleResolverBuilder(); + $resolverMock->setup($this->mockTestModulePaths); + $extractor = new ModulePathExtractor(); + $this->assertEquals('ModuleC', $extractor->extractModuleName($mockPath)); } /** - * Validate Magento is returned for dev/tests/acceptance + * Validate vendor for dev/tests path + * * @throws \Exception */ - public function testMagentoModulePath() + public function testGetVendorDevTests() { - $modulePathExtractor = new ModulePathExtractor(); - $this->assertEquals( - 'Magento', - $modulePathExtractor->getExtensionPath( - self::MAGENTO_PATH - ) - ); + $mockPath = '/base/path/dev/tests/acceptance/tests/functional/VendorD/ModuleDTest/Test/SomeTest.xml'; + + $resolverMock = new MockModuleResolverBuilder(); + $resolverMock->setup($this->mockTestModulePaths); + $extractor = new ModulePathExtractor(); + $this->assertEquals('VendorD', $extractor->getExtensionPath($mockPath)); } /** - * Validate correct extension path is returned + * Validate module with no _ + * * @throws \Exception */ - public function testExtensionModulePath() + public function testGetModule() { - $modulePathExtractor = new ModulePathExtractor(); - $this->assertEquals( - 'TestExtension', - $modulePathExtractor->getExtensionPath( - self::EXTENSION_PATH - ) - ); + $mockPath = '/base/path/dev/tests/acceptance/tests/functional/FunctionalTest/SomeModuleE/Test/SomeTest.xml'; + + $resolverMock = new MockModuleResolverBuilder(); + $resolverMock->setup($this->mockTestModulePaths); + $extractor = new ModulePathExtractor(); + $this->assertEquals('NO MODULE DETECTED', $extractor->extractModuleName($mockPath)); + } + + /** + * Validate module for vendor/tests path + * + * @throws \Exception + */ + public function testGetModuleVendorDir() + { + $mockPath = '/base/path/vendor/magento/module-modulef/Test/Mftf/Test/SomeTest.xml'; + + $resolverMock = new MockModuleResolverBuilder(); + $resolverMock->setup($this->mockTestModulePaths); + $extractor = new ModulePathExtractor(); + $this->assertEquals('ModuleF', $extractor->extractModuleName($mockPath)); + } + + /** + * Validate vendor for vendor path + * + * @throws \Exception + */ + public function testGetVendorVendorDir() + { + $mockPath = '/base/path/vendor/vendorg/module-moduleg-test/Test/SomeTest.xml'; + + $resolverMock = new MockModuleResolverBuilder(); + $resolverMock->setup($this->mockTestModulePaths); + $extractor = new ModulePathExtractor(); + $this->assertEquals('VendorG', $extractor->getExtensionPath($mockPath)); } } diff --git a/dev/tests/unit/Magento/FunctionalTestFramework/Util/ModuleResolverTest.php b/dev/tests/unit/Magento/FunctionalTestFramework/Util/ModuleResolverTest.php index 248704adc..6acf81b53 100644 --- a/dev/tests/unit/Magento/FunctionalTestFramework/Util/ModuleResolverTest.php +++ b/dev/tests/unit/Magento/FunctionalTestFramework/Util/ModuleResolverTest.php @@ -68,6 +68,10 @@ public function testGetModulePathsAggregate() $this->setMockResolverProperties($resolver, null, [0 => "Magento_example"]); $this->assertEquals( [ + "example" . DIRECTORY_SEPARATOR . "paths", + "example" . DIRECTORY_SEPARATOR . "paths", + "example" . DIRECTORY_SEPARATOR . "paths", + "example" . DIRECTORY_SEPARATOR . "paths", "example" . DIRECTORY_SEPARATOR . "paths", "example" . DIRECTORY_SEPARATOR . "paths", "example" . DIRECTORY_SEPARATOR . "paths" @@ -82,6 +86,11 @@ public function testGetModulePathsAggregate() */ public function testGetModulePathsLocations() { + // clear test object handler value to inject parsed content + $property = new \ReflectionProperty(ModuleResolver::class, 'instance'); + $property->setAccessible(true); + $property->setValue(null); + $this->mockForceGenerate(false); $mockResolver = $this->setMockResolverClass( true, @@ -94,6 +103,10 @@ public function testGetModulePathsLocations() $this->setMockResolverProperties($resolver, null, null); $this->assertEquals( [ + "example" . DIRECTORY_SEPARATOR . "paths", + "example" . DIRECTORY_SEPARATOR . "paths", + "example" . DIRECTORY_SEPARATOR . "paths", + "example" . DIRECTORY_SEPARATOR . "paths", "example" . DIRECTORY_SEPARATOR . "paths", "example" . DIRECTORY_SEPARATOR . "paths", "example" . DIRECTORY_SEPARATOR . "paths" @@ -107,18 +120,19 @@ public function testGetModulePathsLocations() // Define the Module paths from default TESTS_MODULE_PATH $modulePath = defined('TESTS_MODULE_PATH') ? TESTS_MODULE_PATH : TESTS_BP; - $mockResolver->verifyInvoked('globRelevantPaths', [$modulePath, '']); + $mockResolver->verifyInvoked('globRelevantPaths', [$magentoBaseCodePath . '/vendor', 'Test/Mftf', null]); + $mockResolver->verifyInvoked('globRelevantPaths', [$magentoBaseCodePath . '/vendor', '*-test', 1]); + $mockResolver->verifyInvoked('globRelevantPaths', [$magentoBaseCodePath . '/app/code', 'Test/Mftf', null]); $mockResolver->verifyInvoked( 'globRelevantPaths', - [$magentoBaseCodePath . DIRECTORY_SEPARATOR . "vendor" , 'Test' . DIRECTORY_SEPARATOR .'Mftf'] + [$magentoBaseCodePath . '/dev/tests/acceptance/tests/functional', '*Test', 1] ); $mockResolver->verifyInvoked( 'globRelevantPaths', - [ - $magentoBaseCodePath . DIRECTORY_SEPARATOR . "app" . DIRECTORY_SEPARATOR . "code", - 'Test' . DIRECTORY_SEPARATOR .'Mftf' - ] + [$magentoBaseCodePath . '/dev/tests/acceptance/tests/functional', 'FunctionalTest/*', 1] ); + $mockResolver->verifyInvoked('globRelevantPaths', [$modulePath, 'Test/Mftf', null]); + $mockResolver->verifyInvoked('globRelevantPaths', [$modulePath, '*', 0]); } /** @@ -149,8 +163,8 @@ public function testGetModulePathsBlacklist() null, null, null, - function ($arg1, $arg2) { - if ($arg2 === "") { + function ($arg1, $arg2, $arg3) { + if ($arg3 === null) { $mockValue = ["somePath" => "somePath"]; } else { $mockValue = ["lastPath" => "lastPath"]; @@ -160,7 +174,10 @@ function ($arg1, $arg2) { ); $resolver = ModuleResolver::getInstance(); $this->setMockResolverProperties($resolver, null, null, ["somePath"]); - $this->assertEquals(["lastPath", "lastPath"], $resolver->getModulesPath()); + $this->assertEquals( + ["lastPath", "lastPath", "lastPath", "lastPath"], + $resolver->getModulesPath() + ); TestLoggingUtil::getInstance()->validateMockLogStatement( 'info', 'excluding module', diff --git a/dev/tests/unit/Util/MockModuleResolverBuilder.php b/dev/tests/unit/Util/MockModuleResolverBuilder.php new file mode 100644 index 000000000..706067f88 --- /dev/null +++ b/dev/tests/unit/Util/MockModuleResolverBuilder.php @@ -0,0 +1,57 @@ + ['/base/path/app/code/Magento/Module/Test/Mftf']]; + + /** + * Mock ModuleResolver builder + * + * @param array $paths + * @return void + * @throws \Exception + */ + public function setup($paths = null) + { + if (!empty($path)) { + $paths = $this->defaultPaths; + } + + $mockConfig = AspectMock::double(MftfApplicationConfig::class, ['forceGenerateEnabled' => false]); + $instance = AspectMock::double(ObjectManager::class, ['create' => $mockConfig->make(), 'get' => null])->make(); + AspectMock::double(ObjectManagerFactory::class, ['getObjectManager' => $instance]); + + $property = new \ReflectionProperty(ModuleResolver::class, 'instance'); + $property->setAccessible(true); + $property->setValue(null); + + $mockResolver = AspectMock::double( + ModuleResolver::class, + ['getAdminToken' => false, 'globRelevantPaths' => [], 'getEnabledModules' => []] + ); + $instance = AspectMock::double(ObjectManager::class, ['create' => $mockResolver->make(), 'get' => null]) + ->make(); + AspectMock::double(ObjectManagerFactory::class, ['getObjectManager' => $instance]); + + $resolver = ModuleResolver::getInstance(); + $property = new \ReflectionProperty(ModuleResolver::class, 'enabledModulePathsNoFlatten'); + $property->setAccessible(true); + $property->setValue($resolver, $paths); + } +} diff --git a/dev/tests/verification/TestModule/Page/SamplePage.xml b/dev/tests/verification/TestModule/Page/SamplePage.xml index bf8f99615..07494d88f 100644 --- a/dev/tests/verification/TestModule/Page/SamplePage.xml +++ b/dev/tests/verification/TestModule/Page/SamplePage.xml @@ -8,25 +8,25 @@ - +
- +
- +
- +
- +
- +
- +
diff --git a/src/Magento/FunctionalTestingFramework/Page/Config/Dom.php b/src/Magento/FunctionalTestingFramework/Page/Config/Dom.php index bb6d08f58..a67cb1f1e 100644 --- a/src/Magento/FunctionalTestingFramework/Page/Config/Dom.php +++ b/src/Magento/FunctionalTestingFramework/Page/Config/Dom.php @@ -87,13 +87,13 @@ public function initDom($xml, $filename = null) ); $pageNodes = $dom->getElementsByTagName('page'); $currentModule = - $this->modulePathExtractor->extractModuleName($filename) . - '_' . - $this->modulePathExtractor->getExtensionPath($filename); + $this->modulePathExtractor->getExtensionPath($filename) + . '_' + . $this->modulePathExtractor->extractModuleName($filename); foreach ($pageNodes as $pageNode) { $pageModule = $pageNode->getAttribute("module"); $pageName = $pageNode->getAttribute("name"); - if ($pageModule !== $currentModule) { + if ($pageModule != $currentModule) { if (MftfApplicationConfig::getConfig()->verboseEnabled()) { print( "Page Module does not match path Module. " . diff --git a/src/Magento/FunctionalTestingFramework/Test/Util/TestObjectExtractor.php b/src/Magento/FunctionalTestingFramework/Test/Util/TestObjectExtractor.php index acad18572..f4eb7b5a7 100644 --- a/src/Magento/FunctionalTestingFramework/Test/Util/TestObjectExtractor.php +++ b/src/Magento/FunctionalTestingFramework/Test/Util/TestObjectExtractor.php @@ -11,6 +11,7 @@ use Magento\FunctionalTestingFramework\Test\Objects\ActionObject; use Magento\FunctionalTestingFramework\Test\Objects\TestObject; use Magento\FunctionalTestingFramework\Util\ModulePathExtractor; +use Magento\FunctionalTestingFramework\Util\ModuleResolver; use Magento\FunctionalTestingFramework\Util\Validation\NameValidationUtil; /** @@ -94,7 +95,8 @@ public function extractTestData($testData) $filename = $testData['filename'] ?? null; $fileNames = explode(",", $filename); $baseFileName = $fileNames[0]; - $module = $this->modulePathExtractor->extractModuleName($baseFileName); + $module = ModuleResolver::getInstance() + ->trimTestModuleSuffix($this->modulePathExtractor->extractModuleName($baseFileName)); $testReference = $testData['extends'] ?? null; $testActions = $this->stripDescriptorTags( $testData, diff --git a/src/Magento/FunctionalTestingFramework/Util/ModulePathExtractor.php b/src/Magento/FunctionalTestingFramework/Util/ModulePathExtractor.php index 33e559ea8..c962782b1 100644 --- a/src/Magento/FunctionalTestingFramework/Util/ModulePathExtractor.php +++ b/src/Magento/FunctionalTestingFramework/Util/ModulePathExtractor.php @@ -11,41 +11,91 @@ */ class ModulePathExtractor { - const MAGENTO = 'Magento'; + const SPLIT_DELIMITER = '_'; + + /** + * Test module paths + * + * @var array + */ + private $testModulePaths = []; + + /** + * ModulePathExtractor constructor + */ + public function __construct() + { + if (empty($this->testModulePaths)) { + $this->testModulePaths = ModuleResolver::getInstance()->getModulesPath(false); + } + } /** * Extracts module name from the path given + * * @param string $path * @return string */ public function extractModuleName($path) { - if (empty($path)) { - return "NO MODULE DETECTED"; - } - $paths = explode(DIRECTORY_SEPARATOR, $path); - if (count($paths) < 3) { + $key = $this->extractKeyByPath($path); + if (empty($key)) { return "NO MODULE DETECTED"; - } elseif ($paths[count($paths)-3] == "Mftf") { - // app/code/Magento/[Analytics]/Test/Mftf/Test/SomeText.xml - return $paths[count($paths)-5]; } - // dev/tests/acceptance/tests/functional/Magento/FunctionalTest/[Analytics]/Test/SomeText.xml - return $paths[count($paths)-3]; + $parts = $this->splitKeyForParts($key); + return isset($parts[1]) ? $parts[1] : "NO MODULE DETECTED"; } /** - * Extracts the extension form the path, Magento for dev/tests/acceptance, [name] before module otherwise + * Extracts vendor name for module from the path given + * * @param string $path * @return string */ public function getExtensionPath($path) { + $key = $this->extractKeyByPath($path); + if (empty($key)) { + return "NO VENDOR DETECTED"; + } + $parts = $this->splitKeyForParts($key); + return isset($parts[0]) ? $parts[0] : "NO VENDOR DETECTED"; + } + + /** + * Split key by SPLIT_DELIMITER and return parts array + * + * @param string $key + * @return array + */ + private function splitKeyForParts($key) + { + $parts = explode(self::SPLIT_DELIMITER, $key); + return count($parts) == 2 ? $parts : []; + } + + /** + * Extract module name key by path + * + * @param string $path + * @return string + */ + private function extractKeyByPath($path) + { + if (empty($path)) { + return ''; + } $paths = explode(DIRECTORY_SEPARATOR, $path); - if ($paths[count($paths)-3] == "Mftf") { - // app/code/[Magento]/Analytics/Test/Mftf/Test/SomeText.xml - return $paths[count($paths)-6]; + if (count($paths) < 3) { + return ''; + } + $paths = array_slice($paths, 0, count($paths)-2); + $shortenedPath = implode(DIRECTORY_SEPARATOR, $paths); + foreach ($this->testModulePaths as $key => $pathArr) { + if (isset($pathArr[0]) && $pathArr[0] == $shortenedPath) { + return $key; + } } - return self::MAGENTO; + return ''; } } diff --git a/src/Magento/FunctionalTestingFramework/Util/ModuleResolver.php b/src/Magento/FunctionalTestingFramework/Util/ModuleResolver.php index 16e6037fa..d7b4829cf 100644 --- a/src/Magento/FunctionalTestingFramework/Util/ModuleResolver.php +++ b/src/Magento/FunctionalTestingFramework/Util/ModuleResolver.php @@ -9,6 +9,7 @@ use Magento\FunctionalTestingFramework\Config\MftfApplicationConfig; use Magento\FunctionalTestingFramework\Exceptions\TestFrameworkException; use Magento\FunctionalTestingFramework\Util\Logger\LoggingUtil; +use Symfony\Component\Config\Resource\DirectoryResource; use Symfony\Component\HttpFoundation\Response; /** @@ -38,6 +39,97 @@ class ModuleResolver */ const REGISTRAR_CLASS = "\Magento\Framework\Component\ComponentRegistrar"; + /** + * const for vendor + */ + const VENDOR = 'vendor'; + + /** + * const for app/code + */ + const APP_CODE = 'app' . DIRECTORY_SEPARATOR . "code"; + + /** + * const for dev/tests/acceptance/tests/functional + */ + const DEV_TEST = 'dev' + . DIRECTORY_SEPARATOR + . 'tests' + . DIRECTORY_SEPARATOR + . 'acceptance' + . DIRECTORY_SEPARATOR + . 'tests' + . DIRECTORY_SEPARATOR + . 'functional'; + + /** + * Vendor code path + */ + const VENDOR_CODE_PATH = DIRECTORY_SEPARATOR . self::VENDOR; + + /** + * App code path + */ + const APP_CODE_PATH = DIRECTORY_SEPARATOR . self::APP_CODE; + + /** + * Dev test code path + */ + const DEV_TEST_CODE_PATH = DIRECTORY_SEPARATOR . self::DEV_TEST; + + /** + * Pattern for Mftf directories + */ + const MFTF_DIR_PATTERN = 'Test' . DIRECTORY_SEPARATOR . 'Mftf'; + + /** + * Regex to match an invalid dev test code path + */ + const INVALID_DEV_TEST_CODE_PATH_REGEX = "~^[^:*\\?\"<>|']+" + . self::DEV_TEST_CODE_PATH + . "/[^/:*\\?\"<>|']+/" + . self::DEPRECATED_DEV_TEST_SHORT_NAME + . "$~"; + + /** + * Short directory name for deprecated path + */ + const DEPRECATED_DEV_TEST_SHORT_NAME = 'FunctionalTest'; + + /** + * Regex to match deprecated dev test code path + */ + const DEPRECATED_DEV_TEST_CODE_PATH_REGEX = "~^[^:*\\?\"<>|']+" + . self::DEV_TEST_CODE_PATH + . "/[^/:*\\?\"<>|']+/" + . self::DEPRECATED_DEV_TEST_SHORT_NAME + . "/[^:*\\?\"<>|']+~"; + + /** + * Test module name suffix + */ + const TEST_MODULE_NAME_SUFFIX = 'Test'; + + /** + * Regex to match a test module name with suffix defined in TEST_MODULE_NAME_SUFFIX + */ + const TEST_MODULE_NAME_REGEX = "~\S+" . self::TEST_MODULE_NAME_SUFFIX . "$~"; + + /** + * Regex to grab vendor name in vendor + */ + const VENDOR_NAME_REGEX_V = "~.+\\/" . self::VENDOR . "\/(?<" . self::VENDOR . ">[^\/]+)\/.+~"; + + /** + * Regex to grab vendor name in app/code + */ + const VENDOR_NAME_REGEX_A = "~.+\\/" . self::APP_CODE . "\/(?<" . self::VENDOR . ">[^\/]+)\/.+~"; + + /** + * Regex to grab vendor name dev/tests + */ + const VENDOR_NAME_REGEX_D = "~.+\\/" . self::DEV_TEST . "\/(?<" . self::VENDOR . ">[^\/]+)\/.+~"; + /** * Enabled modules. * @@ -52,6 +144,13 @@ class ModuleResolver */ protected $enabledModulePaths = null; + /** + * Paths for non flattened enabled modules. + * + * @var array|null + */ + protected $enabledModulePathsNoFlatten = null; + /** * Configuration instance. * @@ -110,6 +209,13 @@ class ModuleResolver 'SampleTests', 'SampleTemplates' ]; + /** + * Registered module list in magento instance being tested + * + * @var array + */ + private $registeredModuleList = []; + /** * Get ModuleResolver instance. * @@ -138,6 +244,7 @@ private function __construct() * Return an array of enabled modules of target Magento instance. * * @return array + * @throws TestFrameworkException */ public function getEnabledModules() { @@ -179,32 +286,22 @@ public function getEnabledModules() return $this->enabledModules; } - /** - * Return an array of module whitelist that not exist in target Magento instance. - * - * @return array - */ - protected function getModuleWhitelist() - { - $moduleWhitelist = getenv(self::MODULE_WHITELIST); - - if (empty($moduleWhitelist)) { - return []; - } - return array_map('trim', explode(',', $moduleWhitelist)); - } - /** * Return the modules path based on which modules are enabled in the target Magento instance. * + * @param boolean $flat * @return array */ - public function getModulesPath() + public function getModulesPath($flat = true) { - if (isset($this->enabledModulePaths)) { + if (isset($this->enabledModulePaths) && $flat) { return $this->enabledModulePaths; } + if (isset($this->enabledModulePathsNoFlatten) && !$flat) { + return $this->enabledModulePathsNoFlatten; + } + $allModulePaths = $this->aggregateTestModulePaths(); if (MftfApplicationConfig::getConfig()->forceGenerateEnabled()) { @@ -219,10 +316,49 @@ public function getModulesPath() return $this->enabledModulePaths; } + /** + * Sort files according module sequence. + * + * @param array $files + * @return array + */ + public function sortFilesByModuleSequence(array $files) + { + return $this->sequenceSorter->sort($files); + } + + /** + * Trim test module suffix from module name + * + * @param string $moduleName + * @return string + */ + public function trimTestModuleSuffix($moduleName) + { + preg_match(self::TEST_MODULE_NAME_REGEX, $moduleName, $match); + return empty($match) ? $moduleName : substr($moduleName, 0, -strlen(self::TEST_MODULE_NAME_SUFFIX)); + } + + /** + * Return an array of module whitelist that not exist in target Magento instance. + * + * @return array + */ + protected function getModuleWhitelist() + { + $moduleWhitelist = getenv(self::MODULE_WHITELIST); + + if (empty($moduleWhitelist)) { + return []; + } + return array_map('trim', explode(',', $moduleWhitelist)); + } + /** * Retrieves all module directories which might contain pertinent test code. * * @return array + * @throws TestFrameworkException */ private function aggregateTestModulePaths() { @@ -235,19 +371,71 @@ private function aggregateTestModulePaths() $modulePath = defined('TESTS_MODULE_PATH') ? TESTS_MODULE_PATH : TESTS_BP; $modulePath = rtrim($modulePath, DIRECTORY_SEPARATOR); - $vendorCodePath = DIRECTORY_SEPARATOR . "vendor"; - $appCodePath = DIRECTORY_SEPARATOR . "app" . DIRECTORY_SEPARATOR . "code"; - + // Add known paths $codePathsToPattern = [ - $modulePath => '', - $magentoBaseCodePath . $vendorCodePath => 'Test' . DIRECTORY_SEPARATOR . 'Mftf', - $magentoBaseCodePath . $appCodePath => 'Test' . DIRECTORY_SEPARATOR . 'Mftf' + $magentoBaseCodePath . self::VENDOR_CODE_PATH => [ + [ + 'pattern' => 'Test' . DIRECTORY_SEPARATOR . 'Mftf', + 'level' => null + ], + [ + 'pattern' => '*-test', + 'level' => 1 + ] + ], + $magentoBaseCodePath . self::APP_CODE_PATH => [ + [ + 'pattern' => 'Test' . DIRECTORY_SEPARATOR . 'Mftf', + 'level' => null + ] + ], + $magentoBaseCodePath . self::DEV_TEST_CODE_PATH => [ + [ + 'pattern' => '*' . self::TEST_MODULE_NAME_SUFFIX, + 'level' => 1 + ], + [ + 'pattern' => self::DEPRECATED_DEV_TEST_SHORT_NAME . DIRECTORY_SEPARATOR . '*', + 'level' => 1 + ] + ] ]; - foreach ($codePathsToPattern as $codePath => $pattern) { - $allModulePaths = array_merge_recursive($allModulePaths, $this->globRelevantPaths($codePath, $pattern)); + // Check if module path is a known path + $newPath = true; + foreach (array_keys($codePathsToPattern) as $key) { + if (strpos($modulePath, $key) !== false) { + $newPath = false; + } + } + + // Add module path if it's a new path + if ($newPath) { + $codePathsToPattern[$modulePath] = [ + [ + 'pattern' => '*', + 'level' => 0 + ], + [ + 'pattern' => 'Test' . DIRECTORY_SEPARATOR . 'Mftf', + 'level' => null + ] + ]; + } + + // Glob pattern for relevant paths + foreach ($codePathsToPattern as $codePath => $patterns) { + foreach ($patterns as $pattern) { + $allModulePaths = array_merge_recursive( + $allModulePaths, + $this->globRelevantPaths($codePath, $pattern['pattern'], $pattern['level']) + ); + } } + // Validate module paths + $this->validateModulePaths($allModulePaths); + return $allModulePaths; } @@ -256,37 +444,50 @@ private function aggregateTestModulePaths() * are returned as an associative array keyed by basename (the last dir excluding pattern) to an array containing * the matching path. * - * @param string $testPath - * @param string $pattern + * @param string $testPath + * @param string $pattern + * @param integer $level * @return array */ - private function globRelevantPaths($testPath, $pattern) + private function globRelevantPaths($testPath, $pattern, $level) { $modulePaths = []; $relevantPaths = []; if (file_exists($testPath)) { - $relevantPaths = $this->globRelevantWrapper($testPath, $pattern); + $relevantPaths = $this->globRelevantWrapper($testPath, $pattern, $level); } $allComponents = $this->getRegisteredModuleList(); foreach ($relevantPaths as $codePath) { - // Reduce magento/app/code/Magento/AdminGws/ to magento/app/code/Magento/AdminGws to read symlink + $possibleVendorName = $this->getPossibleVendorName($codePath); + + // Reduce magento/app/code/Magento/AdminGws/Test/MFTF to magento/app/code/Magento/AdminGws to read symlink // Symlinks must be resolved otherwise they will not match Magento's filepath to the module - $potentialSymlink = str_replace(DIRECTORY_SEPARATOR . $pattern, "", $codePath); - if (is_link($potentialSymlink)) { - $codePath = realpath($potentialSymlink) . DIRECTORY_SEPARATOR . $pattern; + if ($pattern == self::MFTF_DIR_PATTERN) { + $codePath = str_replace(DIRECTORY_SEPARATOR . self::MFTF_DIR_PATTERN, "", $codePath); + } + if (is_link($codePath)) { + $codePath = realpath($codePath); } - $mainModName = array_search($codePath, $allComponents) ?: basename(str_replace($pattern, '', $codePath)); - $modulePaths[$mainModName][] = $codePath; - - if (MftfApplicationConfig::getConfig()->verboseEnabled()) { - LoggingUtil::getInstance()->getLogger(ModuleResolver::class)->debug( - "including module", - ['module' => $mainModName, 'path' => $codePath] - ); + $mainModName = array_search($codePath, $allComponents) ?: $possibleVendorName . '_' . basename($codePath); + + preg_match(self::INVALID_DEV_TEST_CODE_PATH_REGEX, $codePath, $match); + if (empty($match)) { + if ($pattern == self::MFTF_DIR_PATTERN) { + $modulePaths[$mainModName][] = $codePath . DIRECTORY_SEPARATOR . self::MFTF_DIR_PATTERN; + } else { + $modulePaths[$mainModName][] = $codePath; + } + + if (MftfApplicationConfig::getConfig()->verboseEnabled()) { + LoggingUtil::getInstance()->getLogger(ModuleResolver::class)->debug( + "including module", + ['module' => $mainModName, 'path' => $codePath] + ); + } } } @@ -295,18 +496,25 @@ private function globRelevantPaths($testPath, $pattern) /** * Glob wrapper for globRelevantPaths function + * When $level = null, it's recursion * - * @param string $testPath - * @param string $pattern + * @param string $testPath + * @param string $pattern + * @param integer $level * @return array */ - private static function globRelevantWrapper($testPath, $pattern) + private static function globRelevantWrapper($testPath, $pattern, $level = null) { - if ($pattern == "") { - return glob($testPath . '*' . DIRECTORY_SEPARATOR . '*' . $pattern); + $subDirectory = DIRECTORY_SEPARATOR . "*"; + if ($level !== null) { + $subDirectories = ''; + for ($i = 0; $i < $level; $i++) { + $subDirectories .= $subDirectory; + } + return glob($testPath . $subDirectories . DIRECTORY_SEPARATOR . $pattern, GLOB_ONLYDIR); } - $subDirectory = "*" . DIRECTORY_SEPARATOR; - $directories = glob($testPath . $subDirectory . $pattern, GLOB_ONLYDIR); + + $directories = glob($testPath . $subDirectory . DIRECTORY_SEPARATOR . $pattern, GLOB_ONLYDIR); foreach (glob($testPath . $subDirectory, GLOB_ONLYDIR) as $dir) { $directories = array_merge_recursive($directories, self::globRelevantWrapper($dir, $pattern)); } @@ -341,16 +549,19 @@ private function getEnabledDirectoryPaths($enabledModules, $allModulePaths) { $enabledDirectoryPaths = []; foreach ($enabledModules as $magentoModuleName) { - if (!isset($this->knownDirectories[$magentoModuleName]) && !isset($allModulePaths[$magentoModuleName])) { - continue; - } elseif (isset($this->knownDirectories[$magentoModuleName]) - && !isset($allModulePaths[$magentoModuleName])) { + $magentoTestModuleName = $magentoModuleName . self::TEST_MODULE_NAME_SUFFIX; + if (isset($this->knownDirectories[$magentoModuleName]) && !isset($allModulePaths[$magentoModuleName])) { LoggingUtil::getInstance()->getLogger(ModuleResolver::class)->warn( "Known directory could not match to an existing path.", ['knownDirectory' => $magentoModuleName] ); } else { - $enabledDirectoryPaths[$magentoModuleName] = $allModulePaths[$magentoModuleName]; + if (isset($allModulePaths[$magentoModuleName])) { + $enabledDirectoryPaths[$magentoModuleName] = $allModulePaths[$magentoModuleName]; + } + if (isset($allModulePaths[$magentoTestModuleName])) { + $enabledDirectoryPaths[$magentoTestModuleName] = $allModulePaths[$magentoTestModuleName]; + } } } return $enabledDirectoryPaths; @@ -447,17 +658,6 @@ protected function getAdminToken() return json_decode($response); } - /** - * Sort files according module sequence. - * - * @param array $files - * @return array - */ - public function sortFilesByModuleSequence(array $files) - { - return $this->sequenceSorter->sort($files); - } - /** * A wrapping method for any custom logic which needs to be applied to the module list * @@ -476,6 +676,9 @@ protected function applyCustomModuleMethods($modulesPath) ); }, $customModulePaths); + if (!isset($this->enabledModulePathsNoFlatten)) { + $this->enabledModulePathsNoFlatten = array_merge($modulePathsResult, $customModulePaths); + } return $this->flattenAllModulePaths(array_merge($modulePathsResult, $customModulePaths)); } @@ -489,12 +692,23 @@ private function removeBlacklistModules($modulePaths) { $modulePathsResult = $modulePaths; foreach ($modulePathsResult as $moduleName => $modulePath) { + // Remove module if it is in blacklist if (in_array($moduleName, $this->getModuleBlacklist())) { unset($modulePathsResult[$moduleName]); LoggingUtil::getInstance()->getLogger(ModuleResolver::class)->info( "excluding module", ['module' => $moduleName] ); + } else { + // Remove test module if its magento module is in blacklist + $relatedModuleName = $this->trimTestModuleSuffix($moduleName); + if (($relatedModuleName != $moduleName) && in_array($relatedModuleName, $this->getModuleBlacklist())) { + unset($modulePathsResult[$moduleName]); + LoggingUtil::getInstance()->getLogger(ModuleResolver::class)->info( + "excluding module", + ['module' => $moduleName] + ); + } } } @@ -534,6 +748,10 @@ private function getModuleBlacklist() */ private function getRegisteredModuleList() { + if (!empty($this->registeredModuleList)) { + return $this->registeredModuleList; + } + if (array_key_exists('MAGENTO_BP', $_ENV)) { $autoloadPath = realpath(MAGENTO_BP . "/app/autoload.php"); if ($autoloadPath) { @@ -545,20 +763,22 @@ private function getRegisteredModuleList() } try { - $allComponents = []; + $this->registeredModuleList = []; if (!class_exists(self::REGISTRAR_CLASS)) { throw new TestFrameworkException("Magento Installation not found when loading registered modules.\n"); } $components = new \Magento\Framework\Component\ComponentRegistrar(); foreach (self::PATHS as $componentType) { - $allComponents = array_merge($allComponents, $components->getPaths($componentType)); + $this->registeredModuleList = array_merge( + $this->registeredModuleList, + $components->getPaths($componentType) + ); } - array_walk($allComponents, function (&$value) { + array_walk($this->registeredModuleList, function (&$value) { // Magento stores component paths with unix DIRECTORY_SEPARATOR, need to stay uniform and convert $value = realpath($value); - $value .= '/Test/Mftf'; }); - return $allComponents; + return $this->registeredModuleList; } catch (TestFrameworkException $e) { LoggingUtil::getInstance()->getLogger(ModuleResolver::class)->warning( "$e" @@ -575,4 +795,86 @@ private function getBackendUrl() { return getenv('MAGENTO_BACKEND_BASE_URL') ?: getenv('MAGENTO_BASE_URL'); } + + /** + * Validate module paths for violation and deprecations + * + * @param array $modulePaths + * @return void + * @throws TestFrameworkException + */ + private function validateModulePaths($modulePaths) + { + $foundDeprecate = false; + // Check tests should be in one location per module + foreach ($modulePaths as $moduleName => $modulePath) { + // Check tests in deprecated path + if (!$foundDeprecate) { + preg_match(self::DEPRECATED_DEV_TEST_CODE_PATH_REGEX, $modulePath[0], $match); + if (!empty($match)) { + $foundDeprecate = true; + } + } + $relatedModuleName = $this->trimTestModuleSuffix($moduleName); + if (($relatedModuleName != $moduleName) && isset($modulePaths[$relatedModuleName])) { + $message = "Mftf tests cannot be in both $moduleName and $relatedModuleName modules. " + . "Please move all mftf tests to $relatedModuleName."; + throw new TestFrameworkException($message); + } + } + + if ($foundDeprecate) { + $deprecaedPath = ltrim( + self::DEV_TEST_CODE_PATH + . DIRECTORY_SEPARATOR + . '' + . DIRECTORY_SEPARATOR + . self::DEPRECATED_DEV_TEST_SHORT_NAME + . DIRECTORY_SEPARATOR, + '/' + ); + + $suggestedPath = ltrim( + self::DEV_TEST_CODE_PATH + . DIRECTORY_SEPARATOR + . '' + . DIRECTORY_SEPARATOR, + '/' + ); + + // Suppress print during unit testing + if (MftfApplicationConfig::getConfig()->getPhase() !== MftfApplicationConfig::UNIT_TEST_PHASE) { + LoggingUtil::getInstance()->getLogger(ModuleResolver::class)->warning( + "DEPRECATION: $deprecaedPath is deprecated! Please move mftf test modules to $suggestedPath" + ); + print ("\nDEPRECATION: $deprecaedPath is deprecated! Please move mftf tests to $suggestedPath\n\n"); + } + } + } + + /** + * Return possible vendor name from a path given + * + * @param string $path + * @return string + */ + private function getPossibleVendorName($path) + { + $possibleVendorName = 'Unknown'; + $regexs = [ + self::VENDOR_NAME_REGEX_A, + self::VENDOR_NAME_REGEX_D, + self::VENDOR_NAME_REGEX_V + ]; + + foreach ($regexs as $regex) { + $match = []; + preg_match($regex, $path, $match); + if (isset($match[self::VENDOR])) { + $possibleVendorName = ucfirst($match[self::VENDOR]); + return $possibleVendorName; + } + } + return $possibleVendorName; + } }