diff --git a/.gitignore b/.gitignore index da2119a16..d39929c6f 100755 --- a/.gitignore +++ b/.gitignore @@ -13,4 +13,6 @@ coverage/ .vscode codeception.yml dev/tests/functional/MFTF.suite.yml -dev/tests/functional/_output \ No newline at end of file +dev/tests/functional/_output +dev/mftf.log +dev/tests/mftf.log \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 8d403f911..768cad7d3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,59 @@ Magento Functional Testing Framework Changelog ================================================ +2.3.0 +----- +### Enhancements +* Traceability + * MFTF now outputs generation run-time information, warnings, and errors to an `mftf.log` file. + * Overall error messages for various generation errors have been improved. Usage of the `--debug` flag provides file-specific errors for all XML-related errors. + * Allure Reports now require a unique `story` and `title` combination, to prevent collisions in Allure Report generation. + * The `features` annotation now ignores user input and defaults to the module the test lives under (for clear Allure organization). + * The `` annotation has been replaced with a `` annotation, allowing for nested `IssueId` elements. + * Tests now require the following annotations: `stories`, `title`, `description`, `severity`. + * This will be enforced in a future major release. +* Modularity + * MFTF has been decoupled from MagentoCE: + * MFTF can now generate and run tests by itself via `bin/mftf` commands. + * It is now a top level MagentoCE dependency, and no longer relies on supporting files in MagentoCE. + * It can be used as an isolated dependency for Magento projects such as extensions. + * `generate:tests` now warns the user if any declared `` has an inconsistent `module` (`Backend` vs `Magento_Backend`) + * The `--force` flag now completely ignores checking of the Magento Installation, allowing generation of tests without a Magento Instance to be running. +* Customizability + * Various test materials can now be extended via an `extends="ExistingMaterial"` attribute. This allows for creation of simple copies of any `entity`, `actionGroup`, or `test`, with small modifications. + * `test` and `actionGroup` deltas can now be provided in bulk via a `before/after` attribute on the `test` or `actionGroup` element. Deltas provided this way do not need individual `before/after` attributes, and are inserted sequentially. + * Secure and sensitive test data can now be stored and used via a new `.credentials` file, with declaration and usage syntax similar to `.env` file references. + * A new `` action has been added to allow users to create and use dates according to the given `date` and `format`. + * See DevDocs for more information on all above `Customizability` features. +* Maintainability + * New `bin/mftf` commands have been introduced with parity to existing `robo` commands. + * `robo` commands are still supported, but will be deprecated in a future major release. + * The `mftf upgrade:tests` command has been introduced, which runs all test upgrade scripts against the provided path. + * A new upgrade script was created to replace all test material schema paths to instead use a URN path. + * The `mftf generate:urn-catalog` command has been introduced to create a URN catalog in PHPStorm to support the above upgrade. + * A warning is now shown on generation if a page's url is referenced without specifying the url (`{{page}}` vs `{{page.url}}`). + * An error is now thrown if any test materials contain any overriding element (eg different ``s in a `
` with the same `name`) + * This previously would cause the last read element to override the previous, causing a silent but potentially incorrect test addition. + * Test distribution algorithm for `--config parallel` has been enhanced to take average step length into account. + +### Fixes +* `_after` hook of tests now executes if a non test-related failure causes the test to error. +* Fixed periods in Allure Report showing up as `•`. +* Fixed Windows incompatibility of relative paths in various files. +* Suites will no longer generate if they do not contain any tests. +* Fixed an issue in generation where users could not use javascript variables in `executeJS` actions. +* Fixed an issue in generation where entity replacement in action-groups replaced all entities with the first reference found. +* Fixed an issue in generation where `createData` actions inside `actionGroups` could not properly reference the given `createDataKey`. +* Fixed an issue where `suites` could not generate if they included an `actionGroup` with two arguments. +* Fixed an issue in generation where calling the same entity twice (with different parameters) would replace both calls with the first resolved value. +* The `magentoCLI` action now correctly executes the given command if the `MAGENTO_BASE_URL` contains `index.php` after the domain (ex `https://magento.instance/index.php`) +* The `stepKey` attribute can no longer be an empty. +* Variable substitution has been enabled for `regex` and `command` attributes in test actions. + +### GitHub Issues/Pull requests: +* [#161](https://github.com/magento/magento2-functional-testing-framework/pull/161) -- MAGETWO-46837: Implementing extension to wait for readiness metrics. +* [#72](https://github.com/magento/magento2-functional-testing-framework/issues/72) -- declare(strict_types=1) causes static code check failure (fixed in [#154](https://github.com/magento/magento2-functional-testing-framework/pull/154)) + 2.2.0 ----- ### Enhancements diff --git a/RoboFile.php b/RoboFile.php deleted file mode 100644 index c0af4c72c..000000000 --- a/RoboFile.php +++ /dev/null @@ -1,214 +0,0 @@ -_exec('cp -vn .env.example .env'); - $this->_exec('cp -vf codeception.dist.yml codeception.yml'); - $this->_exec('cp -vf dev' . DIRECTORY_SEPARATOR . 'tests'. DIRECTORY_SEPARATOR . 'functional' . DIRECTORY_SEPARATOR .'MFTF.suite.dist.yml dev' . DIRECTORY_SEPARATOR . 'tests'. DIRECTORY_SEPARATOR . 'functional' . DIRECTORY_SEPARATOR .'MFTF.suite.yml'); - } - - /** - * Duplicate the Example configuration files for the Project. - * Build the Codeception project. - * - * @return void - */ - function buildProject() - { - $this->writeln("This command will be removed in MFTF v3.0.0. Please use bin/mftf build:project instead.\n"); - $this->cloneFiles(); - $this->_exec('vendor'. DIRECTORY_SEPARATOR .'bin'. DIRECTORY_SEPARATOR .'codecept build'); - } - - /** - * Generate all Tests in PHP. - * - * @param array $tests - * @param array $opts - * @return void - */ - function generateTests(array $tests, $opts = ['config' => null, 'force' => true, 'nodes' => null, 'debug' => false]) - { - require 'dev' . DIRECTORY_SEPARATOR . 'tests'. DIRECTORY_SEPARATOR . 'functional' . DIRECTORY_SEPARATOR . '_bootstrap.php'; - $GLOBALS['GENERATE_TESTS'] = true; - if (!$this->isProjectBuilt()) { - $this->say("Please run bin/mftf build:project and configure your environment (.env) first."); - exit(\Robo\Result::EXITCODE_ERROR); - } - $testsObjects = []; - foreach ($tests as $test) { - $testsObjects[] = Magento\FunctionalTestingFramework\Test\Handlers\TestObjectHandler::getInstance()->getObject($test); - } - if ($opts['force']) { - $GLOBALS['FORCE_PHP_GENERATE'] = true; - } - $testsReferencedInSuites = \Magento\FunctionalTestingFramework\Suite\SuiteGenerator::getInstance()->generateAllSuites($opts['config']); - \Magento\FunctionalTestingFramework\Util\TestGenerator::getInstance(null, $testsObjects, $opts['debug'])->createAllTestFiles($opts['config'], $opts['nodes'], $testsReferencedInSuites); - $this->say("Generate Tests Command Run"); - } - - /** - * Check if MFTF has been properly configured - * @return bool - */ - private function isProjectBuilt() - { - $actorFile = __DIR__ . DIRECTORY_SEPARATOR . 'src' . DIRECTORY_SEPARATOR . 'Magento' . DIRECTORY_SEPARATOR . 'FunctionalTestingFramework' . DIRECTORY_SEPARATOR . '_generated' . DIRECTORY_SEPARATOR . 'AcceptanceTesterActions.php'; - - $login = !empty(getenv('MAGENTO_ADMIN_USERNAME')); - $password = !empty(getenv('MAGENTO_ADMIN_PASSWORD')); - $baseUrl = !empty(getenv('MAGENTO_BASE_URL')); - $backendName = !empty(getenv('MAGENTO_BACKEND_NAME')); - $test = (file_exists($actorFile) && $login && $password && $baseUrl && $backendName); - return $test; - } - - /** - * Generate a suite based on name(s) passed in as args. - * - * @param array $args - * @throws Exception - * @return void - */ - function generateSuite(array $args) - { - if (empty($args)) { - throw new Exception("Please provide suite name(s) after generate:suite command"); - } - - require 'dev' . DIRECTORY_SEPARATOR . 'tests'. DIRECTORY_SEPARATOR . 'functional' . DIRECTORY_SEPARATOR . '_bootstrap.php'; - $sg = \Magento\FunctionalTestingFramework\Suite\SuiteGenerator::getInstance(); - - foreach ($args as $arg) { - $sg->generateSuite($arg); - } - } - - /** - * Run all MFTF tests. - * - * @return void - */ - function mftf() - { - $this->_exec('.' . DIRECTORY_SEPARATOR . 'vendor' . DIRECTORY_SEPARATOR . 'bin' . DIRECTORY_SEPARATOR . 'codecept run MFTF --skip-group skip'); - } - - /** - * Run all Tests with the specified @group tag, excluding @group 'skip'. - * - * @param string $args - * @return void - */ - function group($args = '') - { - $this->taskExec('.' . DIRECTORY_SEPARATOR . 'vendor' . DIRECTORY_SEPARATOR . 'bin' . DIRECTORY_SEPARATOR . 'codecept run --verbose --steps --skip-group skip --group')->args($args)->run(); - } - - /** - * Run all Functional tests located under the Directory Path provided. - * - * @param string $args - * @return void - */ - function folder($args = '') - { - $this->taskExec('.' . DIRECTORY_SEPARATOR . 'vendor' . DIRECTORY_SEPARATOR . 'bin' . DIRECTORY_SEPARATOR . 'codecept run ')->args($args)->run(); - } - - /** - * Run all Tests marked with the @group tag 'example'. - * - * @return void - */ - function example() - { - $this->_exec('.' . DIRECTORY_SEPARATOR . 'vendor' . DIRECTORY_SEPARATOR . 'bin' . DIRECTORY_SEPARATOR . 'codecept run --group example --skip-group skip'); - } - - /** - * Generate the HTML for the Allure report based on the Test XML output - Allure v1.4.X - * - * @return \Robo\Result - */ - function allure1Generate() - { - return $this->_exec('allure generate tests'. DIRECTORY_SEPARATOR .'_output'. DIRECTORY_SEPARATOR .'allure-results'. DIRECTORY_SEPARATOR .' -o tests'. DIRECTORY_SEPARATOR .'_output'. DIRECTORY_SEPARATOR .'allure-report'. DIRECTORY_SEPARATOR .''); - } - - /** - * Generate the HTML for the Allure report based on the Test XML output - Allure v2.3.X - * - * @return \Robo\Result - */ - function allure2Generate() - { - return $this->_exec('allure generate tests'. DIRECTORY_SEPARATOR .'_output'. DIRECTORY_SEPARATOR .'allure-results'. DIRECTORY_SEPARATOR .' --output tests'. DIRECTORY_SEPARATOR .'_output'. DIRECTORY_SEPARATOR .'allure-report'. DIRECTORY_SEPARATOR .' --clean'); - } - - /** - * Open the HTML Allure report - Allure v1.4.X - * - * @return void - */ - function allure1Open() - { - $this->_exec('allure report open --report-dir tests'. DIRECTORY_SEPARATOR .'_output'. DIRECTORY_SEPARATOR .'allure-report'. DIRECTORY_SEPARATOR .''); - } - - /** - * Open the HTML Allure report - Allure v2.3.X - * - * @return void - */ - function allure2Open() - { - $this->_exec('allure open --port 0 tests'. DIRECTORY_SEPARATOR .'_output'. DIRECTORY_SEPARATOR .'allure-report'. DIRECTORY_SEPARATOR .''); - } - - /** - * Generate and open the HTML Allure report - Allure v1.4.X - * - * @return void - */ - function allure1Report() - { - $result1 = $this->allure1Generate(); - - if ($result1->wasSuccessful()) { - $this->allure1Open(); - } - } - - /** - * Generate and open the HTML Allure report - Allure v2.3.X - * - * @return void - */ - function allure2Report() - { - $result1 = $this->allure2Generate(); - - if ($result1->wasSuccessful()) { - $this->allure2Open(); - } - } -} diff --git a/bin/all-checks.bat b/bin/all-checks.bat new file mode 100644 index 000000000..d910923ce --- /dev/null +++ b/bin/all-checks.bat @@ -0,0 +1,8 @@ +:: Copyright © Magento, Inc. All rights reserved. +:: See COPYING.txt for license details. + +@echo off +call bin\static-checks.bat + +@echo off +call bin\phpunit-checks.bat diff --git a/bin/blacklist.txt b/bin/blacklist.txt index ea81ed858..39b3a6700 100644 --- a/bin/blacklist.txt +++ b/bin/blacklist.txt @@ -4,7 +4,6 @@ # THIS FILE CANNOT CONTAIN BLANK LINES # ################################################################### bin/blacklist.txt -dev/tests/static/Magento/Sniffs/Annotations/Helper.php -dev/tests/static/Magento/Sniffs/Annotations/RequireAnnotatedAttributesSniff.php -dev/tests/static/Magento/Sniffs/Annotations/RequireAnnotatedMethodsSniff.php +dev/tests/static/Magento/Sniffs/Commenting/FunctionCommentSniff.php +dev/tests/static/Magento/Sniffs/Commenting/VariableCommentSniff.php dev/tests/verification/_generated diff --git a/bin/copyright-check.bat b/bin/copyright-check.bat new file mode 100644 index 000000000..9f2e23bfb --- /dev/null +++ b/bin/copyright-check.bat @@ -0,0 +1,41 @@ +:: Copyright © Magento, Inc. All rights reserved. +:: See COPYING.txt for license details. + +@echo off +SETLOCAL EnableDelayedExpansion +SET BLACKLIST_FILE=bin/blacklist.txt +SET i=0 + +FOR /F %%x IN ('git ls-tree --full-tree -r --name-only HEAD') DO ( + SET GOOD_EXT= + if "%%~xx"==".php" set GOOD_EXT=1 + if "%%~xx"==".xml" set GOOD_EXT=1 + if "%%~xx"==".xsd" set GOOD_EXT=1 + IF DEFINED GOOD_EXT ( + SET BLACKLISTED= + FOR /F "tokens=* skip=5" %%f IN (%BLACKLIST_FILE%) DO ( + SET LINE=%%x + IF NOT "!LINE!"=="!LINE:%%f=!" ( + SET BLACKLISTED=1 + ) + ) + IF NOT DEFINED BLACKLISTED ( + FIND "Copyright © Magento, Inc. All rights reserved." %%x >nul + IF ERRORLEVEL 1 ( + SET /A i+=1 + SET NO_COPYRIGHT_LIST[!i!]=%%x + ) + ) + ) +) + +IF DEFINED NO_COPYRIGHT_LIST[1] ( + ECHO THE FOLLOWING FILES ARE MISSING THE MAGENTO COPYRIGHT: + ECHO. + ECHO Copyright © Magento, Inc. All rights reserved. + ECHO See COPYING.txt for license details. + ECHO. + FOR /L %%a IN (1,1,%i%) DO ( + ECHO !NO_COPYRIGHT_LIST[%%a]! + ) +) \ No newline at end of file diff --git a/bin/mftf b/bin/mftf index 623e3bc61..d70ee75c3 100755 --- a/bin/mftf +++ b/bin/mftf @@ -11,13 +11,30 @@ if (PHP_SAPI !== 'cli') { exit(1); } +$autoloadPath = realpath(__DIR__ . '/../../../autoload.php'); +$testBootstrapPath = realpath(__DIR__ . '/../dev/tests/functional/_bootstrap.php'); + +try { + if (file_exists($autoloadPath)) { + require_once $autoloadPath; + } else { + require_once $testBootstrapPath; + } +} catch (\Exception $e) { + echo 'Autoload error: ' . $e->getMessage(); + exit(1); +} + + try { - require_once __DIR__ . '/../bootstrap.php'; $application = new Symfony\Component\Console\Application(); $application->setName('Magento Functional Testing Framework CLI'); - $application->setVersion('1.0.0'); - $application->add(new Magento\FunctionalTestingFramework\Console\SetupEnvCommand()); - $application->add(new Magento\FunctionalTestingFramework\Console\BuildProjectCommand()); + $application->setVersion('2.3.0'); + /** @var \Magento\FunctionalTestingFramework\Console\CommandListInterface $commandList */ + $commandList = new \Magento\FunctionalTestingFramework\Console\CommandList; + foreach ($commandList->getCommands() as $command) { + $application->add($command); + } $application->run(); } catch (\Exception $e) { while ($e) { diff --git a/bin/static-checks.bat b/bin/static-checks.bat index 0b9402094..5e14dd289 100644 --- a/bin/static-checks.bat +++ b/bin/static-checks.bat @@ -5,17 +5,15 @@ @echo off @echo ===============================PHP CODE SNIFFER REPORT=============================== -call vendor\bin\phpcs .\src --standard=.\dev\tests\static\Magento --ignore=src\Magento\FunctionalTestingFramework\Group,src\Magento\FunctionalTestingFramework\AcceptanceTester.php -call vendor\bin\phpcs .\dev\tests\unit --standard=.\dev\tests\static\Magento -call vendor\bin\phpcs .\dev\tests\verification --standard=.\dev\tests\static\Magento --ignore=dev\tests\verification\_generated +call vendor\bin\phpcs --standard=.\dev\tests\static\Magento --ignore=src/Magento/FunctionalTestingFramework/Group,src/Magento/FunctionalTestingFramework/AcceptanceTester.php .\src +call vendor\bin\phpcs --standard=.\dev\tests\static\Magento .\dev\tests\unit +call vendor\bin\phpcs --standard=.\dev\tests\static\Magento --ignore=dev/tests/verification/_generated .\dev\tests\verification @echo ===============================COPY PASTE DETECTOR REPORT=============================== call vendor\bin\phpcpd .\src -@echo "===============================PHP MESS DETECTOR REPORT=============================== -vendor\bin\phpmd .\src text \dev\tests\static\Magento\CodeMessDetector\ruleset.xml --exclude _generated,src\Magento\FunctionalTestingFramework\Group,src\Magento\FunctionalTestingFramework\AcceptanceTester.php +@echo ===============================PHP MESS DETECTOR REPORT=============================== +call vendor\bin\phpmd --exclude _generated,src\Magento\FunctionalTestingFramework\Group,src\Magento\FunctionalTestingFramework\AcceptanceTester.php .\src text \dev\tests\static\Magento\CodeMessDetector\ruleset.xml @echo ===============================MAGENTO COPYRIGHT REPORT=============================== -echo msgbox "INFO:Copyright check currently not run as part of .bat implementation" > "%temp%\popup.vbs" -wscript.exe "%temp%\popup.vbs" -::bin\copyright-check +call bin\copyright-check.bat diff --git a/bootstrap.php b/bootstrap.php deleted file mode 100644 index 7a708e8a8..000000000 --- a/bootstrap.php +++ /dev/null @@ -1,8 +0,0 @@ -0.2", "fzaninotto/faker": "^1.6", + "monolog/monolog": "^1.0", "mustache/mustache": "~2.5", "symfony/process": "^2.8 || ^3.1 || ^4.0", "vlucas/phpdotenv": "^2.4" }, "require-dev": { - "squizlabs/php_codesniffer": "1.5.3", - "sebastian/phpcpd": "~3.0", + "squizlabs/php_codesniffer": "~3.2", + "sebastian/phpcpd": "~3.0 || ~4.0", "brainmaestro/composer-git-hooks": "^2.3", - "codeception/aspect-mock": "^2.0", - "goaop/framework": "2.1.2", + "doctrine/cache": "<1.7.0", + "codeception/aspect-mock": "^3.0", + "goaop/framework": "2.2.0", "codacy/coverage": "^1.4", "phpmd/phpmd": "^2.6.0", "rregeer/phpunit-coverage-check": "^0.1.4", @@ -33,6 +35,7 @@ "symfony/stopwatch": "~3.4.6" }, "autoload": { + "files": ["src/Magento/FunctionalTestingFramework/_bootstrap.php"], "psr-4": { "Magento\\FunctionalTestingFramework\\": "src/Magento/FunctionalTestingFramework", "MFTF\\": "dev/tests/functional/MFTF" diff --git a/composer.lock b/composer.lock index de8728da1..2f6dfca45 100644 --- a/composer.lock +++ b/composer.lock @@ -1,10 +1,10 @@ { "_readme": [ "This file locks the dependencies of your project to a known state", - "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "d394ebb149854c00f790da70a8f0a3f3", + "content-hash": "d44cd018c6bc05634270f2b317727ad7", "packages": [ { "name": "allure-framework/allure-codeception", @@ -170,20 +170,21 @@ }, { "name": "codeception/codeception", - "version": "2.3.9", + "version": "2.4.1", "source": { "type": "git", "url": "https://github.com/Codeception/Codeception.git", - "reference": "104f46fa0bde339f1bcc3a375aac21eb36e65a1e" + "reference": "bca3547632556875f1cdd567d6057cc14fe472b8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Codeception/Codeception/zipball/104f46fa0bde339f1bcc3a375aac21eb36e65a1e", - "reference": "104f46fa0bde339f1bcc3a375aac21eb36e65a1e", + "url": "https://api.github.com/repos/Codeception/Codeception/zipball/bca3547632556875f1cdd567d6057cc14fe472b8", + "reference": "bca3547632556875f1cdd567d6057cc14fe472b8", "shasum": "" }, "require": { - "behat/gherkin": "~4.4.0", + "behat/gherkin": "^4.4.0", + "codeception/phpunit-wrapper": "^6.0.9|^7.0.6", "codeception/stub": "^1.0", "ext-json": "*", "ext-mbstring": "*", @@ -191,10 +192,6 @@ "guzzlehttp/guzzle": ">=4.1.4 <7.0", "guzzlehttp/psr7": "~1.0", "php": ">=5.4.0 <8.0", - "phpunit/php-code-coverage": ">=2.2.4 <6.0", - "phpunit/phpunit": ">=4.8.28 <5.0.0 || >=5.6.3 <7.0", - "sebastian/comparator": ">1.1 <3.0", - "sebastian/diff": ">=1.4 <3.0", "symfony/browser-kit": ">=2.7 <5.0", "symfony/console": ">=2.7 <5.0", "symfony/css-selector": ">=2.7 <5.0", @@ -260,7 +257,53 @@ "functional testing", "unit testing" ], - "time": "2018-02-26T23:29:41+00:00" + "time": "2018-03-31T22:30:43+00:00" + }, + { + "name": "codeception/phpunit-wrapper", + "version": "6.0.10", + "source": { + "type": "git", + "url": "https://github.com/Codeception/phpunit-wrapper.git", + "reference": "7057e599d97b02b4efb009681a43b327dbce138a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Codeception/phpunit-wrapper/zipball/7057e599d97b02b4efb009681a43b327dbce138a", + "reference": "7057e599d97b02b4efb009681a43b327dbce138a", + "shasum": "" + }, + "require": { + "phpunit/php-code-coverage": ">=2.2.4 <6.0", + "phpunit/phpunit": ">=4.8.28 <5.0.0 || >=5.6.3 <7.0", + "sebastian/comparator": ">1.1 <3.0", + "sebastian/diff": ">=1.4 <4.0" + }, + "replace": { + "codeception/phpunit-wrapper": "*" + }, + "require-dev": { + "codeception/specify": "*", + "vlucas/phpdotenv": "^2.4" + }, + "type": "library", + "autoload": { + "psr-4": { + "Codeception\\PHPUnit\\": "src\\" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Davert", + "email": "davert.php@resend.cc" + } + ], + "description": "PHPUnit classes used by Codeception", + "time": "2018-06-20T20:08:14+00:00" }, { "name": "codeception/stub", @@ -1618,6 +1661,84 @@ ], "time": "2017-05-10T09:20:27+00:00" }, + { + "name": "monolog/monolog", + "version": "1.23.0", + "source": { + "type": "git", + "url": "https://github.com/Seldaek/monolog.git", + "reference": "fd8c787753b3a2ad11bc60c063cff1358a32a3b4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Seldaek/monolog/zipball/fd8c787753b3a2ad11bc60c063cff1358a32a3b4", + "reference": "fd8c787753b3a2ad11bc60c063cff1358a32a3b4", + "shasum": "" + }, + "require": { + "php": ">=5.3.0", + "psr/log": "~1.0" + }, + "provide": { + "psr/log-implementation": "1.0.0" + }, + "require-dev": { + "aws/aws-sdk-php": "^2.4.9 || ^3.0", + "doctrine/couchdb": "~1.0@dev", + "graylog2/gelf-php": "~1.0", + "jakub-onderka/php-parallel-lint": "0.9", + "php-amqplib/php-amqplib": "~2.4", + "php-console/php-console": "^3.1.3", + "phpunit/phpunit": "~4.5", + "phpunit/phpunit-mock-objects": "2.3.0", + "ruflin/elastica": ">=0.90 <3.0", + "sentry/sentry": "^0.13", + "swiftmailer/swiftmailer": "^5.3|^6.0" + }, + "suggest": { + "aws/aws-sdk-php": "Allow sending log messages to AWS services like DynamoDB", + "doctrine/couchdb": "Allow sending log messages to a CouchDB server", + "ext-amqp": "Allow sending log messages to an AMQP server (1.0+ required)", + "ext-mongo": "Allow sending log messages to a MongoDB server", + "graylog2/gelf-php": "Allow sending log messages to a GrayLog2 server", + "mongodb/mongodb": "Allow sending log messages to a MongoDB server via PHP Driver", + "php-amqplib/php-amqplib": "Allow sending log messages to an AMQP server using php-amqplib", + "php-console/php-console": "Allow sending log messages to Google Chrome", + "rollbar/rollbar": "Allow sending log messages to Rollbar", + "ruflin/elastica": "Allow sending log messages to an Elastic Search server", + "sentry/sentry": "Allow sending log messages to a Sentry server" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Monolog\\": "src/Monolog" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + } + ], + "description": "Sends your logs to files, sockets, inboxes, databases and various web services", + "homepage": "http://github.com/Seldaek/monolog", + "keywords": [ + "log", + "logging", + "psr-3" + ], + "time": "2017-06-19T01:22:40+00:00" + }, { "name": "moontoast/math", "version": "1.1.2", @@ -2472,16 +2593,16 @@ }, { "name": "phpunit/phpunit", - "version": "6.5.7", + "version": "6.5.8", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "6bd77b57707c236833d2b57b968e403df060c9d9" + "reference": "4f21a3c6b97c42952fd5c2837bb354ec0199b97b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/6bd77b57707c236833d2b57b968e403df060c9d9", - "reference": "6bd77b57707c236833d2b57b968e403df060c9d9", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/4f21a3c6b97c42952fd5c2837bb354ec0199b97b", + "reference": "4f21a3c6b97c42952fd5c2837bb354ec0199b97b", "shasum": "" }, "require": { @@ -2552,7 +2673,7 @@ "testing", "xunit" ], - "time": "2018-02-26T07:01:09+00:00" + "time": "2018-04-10T11:38:34+00:00" }, { "name": "phpunit/phpunit-mock-objects", @@ -4365,25 +4486,26 @@ }, { "name": "codeception/aspect-mock", - "version": "2.1.1", + "version": "3.0.0", "source": { "type": "git", "url": "https://github.com/Codeception/AspectMock.git", - "reference": "bf3c000599c0dc75ecb52e19dee2b8ed294cf7ba" + "reference": "793aad0a4e9f238ffc5a107337f57c98aa29d2cf" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Codeception/AspectMock/zipball/bf3c000599c0dc75ecb52e19dee2b8ed294cf7ba", - "reference": "bf3c000599c0dc75ecb52e19dee2b8ed294cf7ba", + "url": "https://api.github.com/repos/Codeception/AspectMock/zipball/793aad0a4e9f238ffc5a107337f57c98aa29d2cf", + "reference": "793aad0a4e9f238ffc5a107337f57c98aa29d2cf", "shasum": "" }, "require": { - "goaop/framework": "^2.0.0", - "php": ">=5.6.0", - "symfony/finder": "~2.4|~3.0" + "goaop/framework": "^2.2.0", + "php": ">=7.0.0", + "phpunit/phpunit": "> 6.0.0", + "symfony/finder": "~2.4|~3.0|~4.0" }, "require-dev": { - "codeception/base": "~2.1", + "codeception/base": "^2.4", "codeception/specify": "~0.3", "codeception/verify": "~0.2" }, @@ -4404,7 +4526,77 @@ } ], "description": "Experimental Mocking Framework powered by Aspects", - "time": "2017-10-24T10:20:17+00:00" + "time": "2018-03-20T08:44:00+00:00" + }, + { + "name": "doctrine/cache", + "version": "v1.6.2", + "source": { + "type": "git", + "url": "https://github.com/doctrine/cache.git", + "reference": "eb152c5100571c7a45470ff2a35095ab3f3b900b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/cache/zipball/eb152c5100571c7a45470ff2a35095ab3f3b900b", + "reference": "eb152c5100571c7a45470ff2a35095ab3f3b900b", + "shasum": "" + }, + "require": { + "php": "~5.5|~7.0" + }, + "conflict": { + "doctrine/common": ">2.2,<2.4" + }, + "require-dev": { + "phpunit/phpunit": "~4.8|~5.0", + "predis/predis": "~1.0", + "satooshi/php-coveralls": "~0.6" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.6.x-dev" + } + }, + "autoload": { + "psr-4": { + "Doctrine\\Common\\Cache\\": "lib/Doctrine/Common/Cache" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "Caching library offering an object-oriented API for many cache backends", + "homepage": "http://www.doctrine-project.org", + "keywords": [ + "cache", + "caching" + ], + "time": "2017-07-22T12:49:21+00:00" }, { "name": "gitonomy/gitlib", @@ -4464,29 +4656,32 @@ }, { "name": "goaop/framework", - "version": "2.1.2", + "version": "2.2.0", "source": { "type": "git", "url": "https://github.com/goaop/framework.git", - "reference": "6e2a0fe13c1943db02a67588cfd27692bddaffa5" + "reference": "152abbffffcba72d2d159b892deb40b0829d0f28" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/goaop/framework/zipball/6e2a0fe13c1943db02a67588cfd27692bddaffa5", - "reference": "6e2a0fe13c1943db02a67588cfd27692bddaffa5", + "url": "https://api.github.com/repos/goaop/framework/zipball/152abbffffcba72d2d159b892deb40b0829d0f28", + "reference": "152abbffffcba72d2d159b892deb40b0829d0f28", "shasum": "" }, "require": { - "doctrine/annotations": "~1.0", - "goaop/parser-reflection": "~1.2", + "doctrine/annotations": "^1.2.3", + "doctrine/cache": "^1.5", + "goaop/parser-reflection": "~1.4", "jakubledl/dissect": "~1.0", "php": ">=5.6.0" }, "require-dev": { "adlawson/vfs": "^0.12", "doctrine/orm": "^2.5", - "phpunit/phpunit": "^4.8", - "symfony/console": "^2.7|^3.0" + "phpunit/phpunit": "^5.7", + "symfony/console": "^2.7|^3.0", + "symfony/filesystem": "^3.3", + "symfony/process": "^3.3" }, "suggest": { "symfony/console": "Enables the usage of the command-line tool." @@ -4523,7 +4718,7 @@ "library", "php" ], - "time": "2017-07-12T11:46:25+00:00" + "time": "2018-01-05T23:07:51+00:00" }, { "name": "goaop/parser-reflection", @@ -5075,61 +5270,37 @@ }, { "name": "squizlabs/php_codesniffer", - "version": "1.5.3", + "version": "3.3.0", "source": { "type": "git", "url": "https://github.com/squizlabs/PHP_CodeSniffer.git", - "reference": "396178ada8499ec492363587f037125bf7b07fcc" + "reference": "d86873af43b4aa9d1f39a3601cc0cfcf02b25266" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/396178ada8499ec492363587f037125bf7b07fcc", - "reference": "396178ada8499ec492363587f037125bf7b07fcc", + "url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/d86873af43b4aa9d1f39a3601cc0cfcf02b25266", + "reference": "d86873af43b4aa9d1f39a3601cc0cfcf02b25266", "shasum": "" }, "require": { + "ext-simplexml": "*", "ext-tokenizer": "*", - "php": ">=5.1.2" + "ext-xmlwriter": "*", + "php": ">=5.4.0" }, - "suggest": { - "phpunit/php-timer": "dev-master" + "require-dev": { + "phpunit/phpunit": "^4.0 || ^5.0 || ^6.0 || ^7.0" }, "bin": [ - "scripts/phpcs" + "bin/phpcs", + "bin/phpcbf" ], "type": "library", "extra": { "branch-alias": { - "dev-phpcs-fixer": "2.0.x-dev" + "dev-master": "3.x-dev" } }, - "autoload": { - "classmap": [ - "CodeSniffer.php", - "CodeSniffer/CLI.php", - "CodeSniffer/Exception.php", - "CodeSniffer/File.php", - "CodeSniffer/Report.php", - "CodeSniffer/Reporting.php", - "CodeSniffer/Sniff.php", - "CodeSniffer/Tokens.php", - "CodeSniffer/Reports/", - "CodeSniffer/CommentParser/", - "CodeSniffer/Tokenizers/", - "CodeSniffer/DocGenerators/", - "CodeSniffer/Standards/AbstractPatternSniff.php", - "CodeSniffer/Standards/AbstractScopeSniff.php", - "CodeSniffer/Standards/AbstractVariableSniff.php", - "CodeSniffer/Standards/IncorrectPatternException.php", - "CodeSniffer/Standards/Generic/Sniffs/", - "CodeSniffer/Standards/MySource/Sniffs/", - "CodeSniffer/Standards/PEAR/Sniffs/", - "CodeSniffer/Standards/PSR1/Sniffs/", - "CodeSniffer/Standards/PSR2/Sniffs/", - "CodeSniffer/Standards/Squiz/Sniffs/", - "CodeSniffer/Standards/Zend/Sniffs/" - ] - }, "notification-url": "https://packagist.org/downloads/", "license": [ "BSD-3-Clause" @@ -5140,13 +5311,13 @@ "role": "lead" } ], - "description": "PHP_CodeSniffer tokenises PHP, JavaScript and CSS files and detects violations of a defined set of coding standards.", + "description": "PHP_CodeSniffer tokenizes PHP, JavaScript and CSS files and detects violations of a defined set of coding standards.", "homepage": "http://www.squizlabs.com/php-codesniffer", "keywords": [ "phpcs", "standards" ], - "time": "2014-05-01T03:07:07+00:00" + "time": "2018-06-06T23:58:19+00:00" }, { "name": "symfony/config", diff --git a/dev/tests/.cache/.gitignore b/dev/tests/.cache/.gitignore new file mode 100644 index 000000000..86d0cb272 --- /dev/null +++ b/dev/tests/.cache/.gitignore @@ -0,0 +1,4 @@ +# Ignore everything in this directory +* +# Except this file +!.gitignore \ No newline at end of file diff --git a/dev/tests/_bootstrap.php b/dev/tests/_bootstrap.php index 655e33910..4364b7f9a 100644 --- a/dev/tests/_bootstrap.php +++ b/dev/tests/_bootstrap.php @@ -6,21 +6,33 @@ error_reporting(~E_USER_NOTICE); define('PROJECT_ROOT', dirname(dirname(__DIR__))); -require_once PROJECT_ROOT . '/vendor/autoload.php'; -require_once 'util/MftfTestCase.php'; + +$vendorAutoloadPath = realpath(PROJECT_ROOT . '/vendor/autoload.php'); +$mftfTestCasePath = realpath(PROJECT_ROOT . '/dev/tests/util/MftfTestCase.php'); + +require_once $vendorAutoloadPath; +require_once $mftfTestCasePath; // Set up AspectMock $kernel = \AspectMock\Kernel::getInstance(); $kernel->init([ 'debug' => true, - 'includePaths' => [PROJECT_ROOT . '/src'] + 'includePaths' => [PROJECT_ROOT . DIRECTORY_SEPARATOR . 'src'], + 'cacheDir' => PROJECT_ROOT . + DIRECTORY_SEPARATOR . + 'dev' . + DIRECTORY_SEPARATOR . + 'tests' . + DIRECTORY_SEPARATOR . + '.cache' ]); // set mftf appplication context \Magento\FunctionalTestingFramework\Config\MftfApplicationConfig::create( true, - \Magento\FunctionalTestingFramework\Config\MftfApplicationConfig::GENERATION_PHASE, - true + \Magento\FunctionalTestingFramework\Config\MftfApplicationConfig::UNIT_TEST_PHASE, + true, + false ); // Load needed framework env params @@ -62,6 +74,27 @@ require($unitUtilFile); } + +// Mocks suite files location getter return to get files in verification/_suite Directory +// This mocks the paths of the suite files but still parses the xml files +$suiteDirectory = TESTS_BP . DIRECTORY_SEPARATOR . "verification" . DIRECTORY_SEPARATOR . "_suite"; + +$paths = [ + $suiteDirectory . DIRECTORY_SEPARATOR . 'functionalSuite.xml', + $suiteDirectory . DIRECTORY_SEPARATOR . 'functionalSuiteHooks.xml' +]; + +// create and return the iterator for these file paths +$iterator = new Magento\FunctionalTestingFramework\Util\Iterator\File($paths); +try { + AspectMock\Test::double( + Magento\FunctionalTestingFramework\Config\FileResolver\Root::class, + ['get' => $iterator] + )->make(); +} catch (Exception $e) { + echo "Suite directory not mocked."; +} + function sortInterfaces($files) { $bottom = []; diff --git a/dev/tests/functional/MFTF/_bootstrap.php b/dev/tests/functional/MFTF/_bootstrap.php deleted file mode 100755 index 6a524e867..000000000 --- a/dev/tests/functional/MFTF/_bootstrap.php +++ /dev/null @@ -1,7 +0,0 @@ -load(); +//Do not continue running this bootstrap if PHPUnit is calling it +$fullTrace = debug_backtrace(); +$rootFile = array_values(array_slice($fullTrace, -1))[0]['file']; +if (strpos($rootFile, "phpunit") !== false) { + return; +} - if (array_key_exists('TESTS_MODULE_PATH', $_ENV) xor array_key_exists('TESTS_BP', $_ENV)) { - throw new Exception('You must define both parameters TESTS_BP and TESTS_MODULE_PATH or neither parameter'); - } +defined('PROJECT_ROOT') || define('PROJECT_ROOT', dirname(dirname(dirname(__DIR__)))); +require_once realpath(PROJECT_ROOT . '/vendor/autoload.php'); - foreach ($_ENV as $key => $var) { - defined($key) || define($key, $var); - } -} +//Load constants from .env file defined('FW_BP') || define('FW_BP', PROJECT_ROOT); // add the debug flag here @@ -29,7 +23,26 @@ xdebug_disable(); } -$RELATIVE_TESTS_MODULE_PATH = '/MFTF/FunctionalTest'; +$RELATIVE_TESTS_MODULE_PATH = '/tests/functional/tests/MFTF'; -defined('TESTS_BP') || define('TESTS_BP', __DIR__); -defined('TESTS_MODULE_PATH') || define('TESTS_MODULE_PATH', TESTS_BP . $RELATIVE_TESTS_MODULE_PATH); +defined('MAGENTO_BP') || define('MAGENTO_BP', PROJECT_ROOT); +defined('TESTS_BP') || define('TESTS_BP', dirname(dirname(__DIR__))); +defined('TESTS_MODULE_PATH') || define('TESTS_MODULE_PATH', realpath(TESTS_BP . $RELATIVE_TESTS_MODULE_PATH)); + +if (file_exists(TESTS_BP . DIRECTORY_SEPARATOR . '.env')) { + $env = new \Dotenv\Loader(TESTS_BP . DIRECTORY_SEPARATOR . '.env'); + $env->load(); + + foreach ($_ENV as $key => $var) { + defined($key) || define($key, $var); + } + + defined('MAGENTO_CLI_COMMAND_PATH') || define( + 'MAGENTO_CLI_COMMAND_PATH', + 'dev/tests/acceptance/utils/command.php' + ); + $env->setEnvironmentVariable('MAGENTO_CLI_COMMAND_PATH', MAGENTO_CLI_COMMAND_PATH); + + defined('MAGENTO_CLI_COMMAND_PARAMETER') || define('MAGENTO_CLI_COMMAND_PARAMETER', 'command'); + $env->setEnvironmentVariable('MAGENTO_CLI_COMMAND_PARAMETER', MAGENTO_CLI_COMMAND_PARAMETER); +} diff --git a/dev/tests/functional/MFTF/FunctionalTest/DevDocs/Page/MFTFDocPage.xml b/dev/tests/functional/tests/MFTF/DevDocs/Page/MFTFDocPage.xml similarity index 100% rename from dev/tests/functional/MFTF/FunctionalTest/DevDocs/Page/MFTFDocPage.xml rename to dev/tests/functional/tests/MFTF/DevDocs/Page/MFTFDocPage.xml diff --git a/dev/tests/functional/MFTF/FunctionalTest/DevDocs/Section/ContentSection.xml b/dev/tests/functional/tests/MFTF/DevDocs/Section/ContentSection.xml similarity index 100% rename from dev/tests/functional/MFTF/FunctionalTest/DevDocs/Section/ContentSection.xml rename to dev/tests/functional/tests/MFTF/DevDocs/Section/ContentSection.xml diff --git a/dev/tests/functional/MFTF/FunctionalTest/DevDocs/Test/DevDocsTest.xml b/dev/tests/functional/tests/MFTF/DevDocs/Test/DevDocsTest.xml similarity index 100% rename from dev/tests/functional/MFTF/FunctionalTest/DevDocs/Test/DevDocsTest.xml rename to dev/tests/functional/tests/MFTF/DevDocs/Test/DevDocsTest.xml diff --git a/dev/tests/static/Magento/Sniffs/Annotations/Helper.php b/dev/tests/static/Magento/Sniffs/Annotations/Helper.php deleted file mode 100644 index 53a03b574..000000000 --- a/dev/tests/static/Magento/Sniffs/Annotations/Helper.php +++ /dev/null @@ -1,576 +0,0 @@ - - * @author Marc McIntyre - * @copyright 2006-2012 Squiz Pty Ltd (ABN 77 084 670 600) - * @license https://github.com/squizlabs/PHP_CodeSniffer/blob/master/licence.txt BSD Licence - * @link http://pear.php.net/package/PHP_CodeSniffer - * - * @SuppressWarnings(PHPMD) - */ -class Helper -{ - const ERROR_PARSING = 'ErrorParsing'; - - const AMBIGUOUS_TYPE = 'AmbiguousType'; - - const MISSING = 'Missing'; - - const WRONG_STYLE = 'WrongStyle'; - - const WRONG_END = 'WrongEnd'; - - const FAILED_PARSE = 'FailedParse'; - - const CONTENT_AFTER_OPEN = 'ContentAfterOpen'; - - const MISSING_SHORT = 'MissingShort'; - - const EMPTY_DOC = 'Empty'; - - const SPACING_BETWEEN = 'SpacingBetween'; - - const SPACING_BEFORE_SHORT = 'SpacingBeforeShort'; - - const SPACING_BEFORE_TAGS = 'SpacingBeforeTags'; - - const SHORT_SINGLE_LINE = 'ShortSingleLine'; - - const SHORT_NOT_CAPITAL = 'ShortNotCapital'; - - const SHORT_FULL_STOP = 'ShortFullStop'; - - const SPACING_AFTER = 'SpacingAfter'; - - const SEE_ORDER = 'SeeOrder'; - - const EMPTY_SEE = 'EmptySee'; - - const SEE_INDENT = 'SeeIndent'; - - const DUPLICATE_RETURN = 'DuplicateReturn'; - - const MISSING_PARAM_TAG = 'MissingParamTag'; - - const SPACING_AFTER_LONG_NAME = 'SpacingAfterLongName'; - - const SPACING_AFTER_LONG_TYPE = 'SpacingAfterLongType'; - - const MISSING_PARAM_TYPE = 'MissingParamType'; - - const MISSING_PARAM_NAME = 'MissingParamName'; - - const EXTRA_PARAM_COMMENT = 'ExtraParamComment'; - - const PARAM_NAME_NO_MATCH = 'ParamNameNoMatch'; - - const PARAM_NAME_NO_CASE_MATCH = 'ParamNameNoCaseMatch'; - - const INVALID_TYPE_HINT = 'InvalidTypeHint'; - - const INCORRECT_TYPE_HINT = 'IncorrectTypeHint'; - - const TYPE_HINT_MISSING = 'TypeHintMissing'; - - const INCORRECT_PARAM_VAR_NAME = 'IncorrectParamVarName'; - - const RETURN_ORDER = 'ReturnOrder'; - - const MISSING_RETURN_TYPE = 'MissingReturnType'; - - const INVALID_RETURN = 'InvalidReturn'; - - const INVALID_RETURN_VOID = 'InvalidReturnVoid'; - - const INVALID_NO_RETURN = 'InvalidNoReturn'; - - const INVALID_RETURN_NOT_VOID = 'InvalidReturnNotVoid'; - - const INCORRECT_INHERIT_DOC = 'IncorrectInheritDoc'; - - const RETURN_INDENT = 'ReturnIndent'; - - const MISSING_RETURN = 'MissingReturn'; - - const RETURN_NOT_REQUIRED = 'ReturnNotRequired'; - - const INVALID_THROWS = 'InvalidThrows'; - - const THROWS_NOT_CAPITAL = 'ThrowsNotCapital'; - - const THROWS_ORDER = 'ThrowsOrder'; - - const EMPTY_THROWS = 'EmptyThrows'; - - const THROWS_NO_FULL_STOP = 'ThrowsNoFullStop'; - - const SPACING_AFTER_PARAMS = 'SpacingAfterParams'; - - const SPACING_BEFORE_PARAMS = 'SpacingBeforeParams'; - - const SPACING_BEFORE_PARAM_TYPE = 'SpacingBeforeParamType'; - - const LONG_NOT_CAPITAL = 'LongNotCapital'; - - const TAG_NOT_ALLOWED = 'TagNotAllowed'; - - const DUPLICATE_VAR = 'DuplicateVar'; - - const VAR_ORDER = 'VarOrder'; - - const MISSING_VAR_TYPE = 'MissingVarType'; - - const INCORRECT_VAR_TYPE = 'IncorrectVarType'; - - const VAR_INDENT = 'VarIndent'; - - const MISSING_VAR = 'MissingVar'; - - const MISSING_PARAM_COMMENT = 'MissingParamComment'; - - const PARAM_COMMENT_NOT_CAPITAL = 'ParamCommentNotCapital'; - - const PARAM_COMMENT_FULL_STOP = 'ParamCommentFullStop'; - - // tells phpcs to use the default level - const ERROR = 0; - - // default level of warnings is 5 - const WARNING = 6; - - const INFO = 2; - - // Lowest possible level. - const OFF = 1; - - const LEVEL = 'level'; - - const MESSAGE = 'message'; - - /** - * Map of Error Type to Error Severity - * - * @var array - */ - protected static $reportingLevel = [ - self::ERROR_PARSING => [self::LEVEL => self::ERROR, self::MESSAGE => '%s'], - self::FAILED_PARSE => [self::LEVEL => self::ERROR, self::MESSAGE => '%s'], - self::AMBIGUOUS_TYPE => [ - self::LEVEL => self::WARNING, - self::MESSAGE => 'Ambiguous type "%s" for %s is NOT recommended', - ], - self::MISSING => [self::LEVEL => self::ERROR, self::MESSAGE => 'Missing %s doc comment'], - self::WRONG_STYLE => [ - self::LEVEL => self::WARNING, - self::MESSAGE => 'You must use "/**" style comments for a %s comment', - ], - self::WRONG_END => [ - self::LEVEL => self::WARNING, - self::MESSAGE => 'You must use "*/" to end a function comment; found "%s"', - ], - self::EMPTY_DOC => [self::LEVEL => self::WARNING, self::MESSAGE => '%s doc comment is empty'], - self::CONTENT_AFTER_OPEN => [ - self::LEVEL => self::WARNING, - self::MESSAGE => 'The open comment tag must be the only content on the line', - ], - self::MISSING_SHORT => [ - self::LEVEL => self::WARNING, - self::MESSAGE => 'Missing short description in %s doc comment', - ], - self::SPACING_BETWEEN => [ - self::LEVEL => self::OFF, - self::MESSAGE => 'There must be exactly one blank line between descriptions in %s comment', - ], - self::SPACING_BEFORE_SHORT => [ - self::LEVEL => self::WARNING, - self::MESSAGE => 'Extra newline(s) found before %s comment short description', - ], - self::SPACING_BEFORE_TAGS => [ - self::LEVEL => self::INFO, - self::MESSAGE => 'There must be exactly one blank line before the tags in %s comment', - ], - self::SHORT_SINGLE_LINE => [ - self::LEVEL => self::OFF, - self::MESSAGE => '%s comment short description must be on a single line', - ], - self::SHORT_NOT_CAPITAL => [ - self::LEVEL => self::WARNING, - self::MESSAGE => '%s comment short description must start with a capital letter', - ], - self::SHORT_FULL_STOP => [ - self::LEVEL => self::OFF, - self::MESSAGE => '%s comment short description must end with a full stop', - ], - self::SPACING_AFTER => [ - self::LEVEL => self::WARNING, - self::MESSAGE => 'Additional blank lines found at end of %s comment', - ], - self::SEE_ORDER => [ - self::LEVEL => self::WARNING, - self::MESSAGE => 'The @see tag is in the wrong order; the tag precedes @return', - ], - self::EMPTY_SEE => [ - self::LEVEL => self::WARNING, - self::MESSAGE => 'Content missing for @see tag in %s comment', - ], - self::SEE_INDENT => [ - self::LEVEL => self::OFF, - self::MESSAGE => '@see tag indented incorrectly; expected 1 spaces but found %s', - ], - self::DUPLICATE_RETURN => [ - self::LEVEL => self::ERROR, - self::MESSAGE => 'Only 1 @return tag is allowed in function comment', - ], - self::MISSING_PARAM_TAG => [self::LEVEL => self::ERROR, self::MESSAGE => 'Doc comment for "%s" missing'], - self::SPACING_AFTER_LONG_NAME => [ - self::LEVEL => self::OFF, - self::MESSAGE => 'Expected 1 space after the longest variable name', - ], - self::SPACING_AFTER_LONG_TYPE => [ - self::LEVEL => self::OFF, - self::MESSAGE => 'Expected 1 space after the longest type', - ], - self::MISSING_PARAM_TYPE => [self::LEVEL => self::ERROR, self::MESSAGE => 'Missing type at position %s'], - self::MISSING_PARAM_NAME => [ - self::LEVEL => self::ERROR, - self::MESSAGE => 'Missing parameter name at position %s', - ], - self::EXTRA_PARAM_COMMENT => [ - self::LEVEL => self::WARNING, - self::MESSAGE => 'Superfluous doc comment at position %s', - ], - self::PARAM_NAME_NO_MATCH => [ - self::LEVEL => self::WARNING, - self::MESSAGE => 'Doc comment for var %s does not match actual variable name %s at position %s', - ], - self::PARAM_NAME_NO_CASE_MATCH => [ - self::LEVEL => self::WARNING, - self::MESSAGE => 'Doc comment for var %s does not match case of actual variable name %s at position %s', - ], - self::INVALID_TYPE_HINT => [ - self::LEVEL => self::WARNING, - self::MESSAGE => 'Unknown type hint "%s" found for %s at position %s', - ], - self::INCORRECT_TYPE_HINT => [ - self::LEVEL => self::WARNING, - self::MESSAGE => 'Expected type hint "%s"; found "%s" for %s at position %s', - ], - self::TYPE_HINT_MISSING => [ - self::LEVEL => self::INFO, - self::MESSAGE => 'Type hint "%s" missing for %s at position %s', - ], - self::INCORRECT_PARAM_VAR_NAME => [ - self::LEVEL => self::WARNING, - self::MESSAGE => 'Expected "%s"; found "%s" for %s at position %s', - ], - self::RETURN_ORDER => [ - self::LEVEL => self::WARNING, - self::MESSAGE => 'The @return tag is in the wrong order; the tag follows @see (if used)', - ], - self::MISSING_RETURN_TYPE => [ - self::LEVEL => self::ERROR, - self::MESSAGE => 'Return type missing for @return tag in function comment', - ], - self::INVALID_RETURN => [ - self::LEVEL => self::ERROR, - self::MESSAGE => 'Function return type "%s" is invalid', - ], - self::INVALID_RETURN_VOID => [ - self::LEVEL => self::WARNING, - self::MESSAGE => 'Function return type is void, but function contains return statement', - ], - self::INVALID_NO_RETURN => [ - self::LEVEL => self::WARNING, - self::MESSAGE => 'Function return type is not void, but function has no return statement', - ], - self::INVALID_RETURN_NOT_VOID => [ - self::LEVEL => self::WARNING, - self::MESSAGE => 'Function return type is not void, but function is returning void here', - ], - self::INCORRECT_INHERIT_DOC => [ - self::LEVEL => self::WARNING, - self::MESSAGE => 'The incorrect inherit doc tag usage. Should be {@inheritdoc}', - ], - self::RETURN_INDENT => [ - self::LEVEL => self::OFF, - self::MESSAGE => '@return tag indented incorrectly; expected 1 space but found %s', - ], - self::MISSING_RETURN => [ - self::LEVEL => self::ERROR, - self::MESSAGE => 'Missing @return tag in function comment', - ], - self::RETURN_NOT_REQUIRED => [ - self::LEVEL => self::WARNING, - self::MESSAGE => '@return tag is not required for constructor and destructor', - ], - self::INVALID_THROWS => [ - self::LEVEL => self::WARNING, - self::MESSAGE => 'Exception type and comment missing for @throws tag in function comment', - ], - self::THROWS_NOT_CAPITAL => [ - self::LEVEL => self::WARNING, - self::MESSAGE => '@throws tag comment must start with a capital letter', - ], - self::THROWS_ORDER => [ - self::LEVEL => self::WARNING, - self::MESSAGE => 'The @throws tag is in the wrong order; the tag follows @return', - ], - self::EMPTY_THROWS => [ - self::LEVEL => self::OFF, - self::MESSAGE => 'Comment missing for @throws tag in function comment', - ], - self::THROWS_NO_FULL_STOP => [ - self::LEVEL => self::OFF, - self::MESSAGE => '@throws tag comment must end with a full stop', - ], - self::SPACING_AFTER_PARAMS => [ - self::LEVEL => self::OFF, - self::MESSAGE => 'Last parameter comment requires a blank newline after it', - ], - self::SPACING_BEFORE_PARAMS => [ - self::LEVEL => self::WARNING, - self::MESSAGE => 'Parameters must appear immediately after the comment', - ], - self::SPACING_BEFORE_PARAM_TYPE => [ - self::LEVEL => self::OFF, - self::MESSAGE => 'Expected 1 space before variable type', - ], - self::LONG_NOT_CAPITAL => [ - self::LEVEL => self::WARNING, - self::MESSAGE => '%s comment long description must start with a capital letter', - ], - self::TAG_NOT_ALLOWED => [ - self::LEVEL => self::WARNING, - self::MESSAGE => '@%s tag is not allowed in variable comment', - ], - self::DUPLICATE_VAR => [ - self::LEVEL => self::ERROR, - self::MESSAGE => 'Only 1 @var tag is allowed in variable comment', - ], - self::VAR_ORDER => [ - self::LEVEL => self::WARNING, - self::MESSAGE => 'The @var tag must be the first tag in a variable comment', - ], - self::MISSING_VAR_TYPE => [ - self::LEVEL => self::ERROR, - self::MESSAGE => 'Var type missing for @var tag in variable comment', - ], - self::INCORRECT_VAR_TYPE => [ - self::LEVEL => self::ERROR, - self::MESSAGE => 'Expected "%s"; found "%s" for @var tag in variable comment', - ], - self::VAR_INDENT => [ - self::LEVEL => self::OFF, - self::MESSAGE => '@var tag indented incorrectly; expected 1 space but found %s', - ], - self::MISSING_VAR => [ - self::LEVEL => self::WARNING, - self::MESSAGE => 'Missing @var tag in variable comment', - ], - self::MISSING_PARAM_COMMENT => [ - self::LEVEL => self::OFF, - self::MESSAGE => 'Missing comment for param "%s" at position %s', - ], - self::PARAM_COMMENT_NOT_CAPITAL => [ - self::LEVEL => self::OFF, - self::MESSAGE => 'Param comment must start with a capital letter', - ], - self::PARAM_COMMENT_FULL_STOP => [ - self::LEVEL => self::OFF, - self::MESSAGE => 'Param comment must end with a full stop', - ], - ]; - - /** - * List of allowed types - * - * @var string[] - */ - protected static $allowedTypes = [ - 'array', - 'boolean', - 'bool', - 'float', - 'integer', - 'int', - 'object', - 'string', - 'resource', - 'callable', - 'true', - 'false', - ]; - - /** - * The current PHP_CodeSniffer_File object we are processing. - * - * @var PHP_CodeSniffer_File - */ - protected $currentFile = null; - - /** - * Constructor for class. - * - * @param PHP_CodeSniffer_File $phpcsFile - */ - public function __construct(PHP_CodeSniffer_File $phpcsFile) - { - $this->currentFile = $phpcsFile; - } - - /** - * Returns the current file object - * - * @return PHP_CodeSniffer_File - */ - public function getCurrentFile() - { - return $this->currentFile; - } - - /** - * Returns the eol character used in the file - * - * @return string - */ - public function getEolChar() - { - return $this->currentFile->eolChar; - } - - /** - * Returns the array of allowed types for magento standard - * - * @return string[] - */ - public function getAllowedTypes() - { - return self::$allowedTypes; - } - - /** - * This method will add the message as an error or warning depending on the configuration - * - * @param int $stackPtr The stack position where the error occurred. - * @param string $code A violation code unique to the sniff message. - * @param string[] $data Replacements for the error message. - * @param int $severity The severity level for this error. A value of 0 - * @return void - */ - public function addMessage($stackPtr, $code, $data = [], $severity = 0) - { - // Does the $code key exist in the report level - if (array_key_exists($code, self::$reportingLevel)) { - $message = self::$reportingLevel[$code][self::MESSAGE]; - $level = self::$reportingLevel[$code][self::LEVEL]; - if ($level === self::WARNING || $level === self::INFO || $level === self::OFF) { - $s = $level; - if ($severity !== 0) { - $s = $severity; - } - $this->currentFile->addWarning($message, $stackPtr, $code, $data, $s); - } else { - $this->currentFile->addError($message, $stackPtr, $code, $data, $severity); - } - } - } - - /** - * Returns if we should filter a particular file - * - * @return bool - */ - public function shouldFilter() - { - $shouldFilter = false; - $filename = $this->getCurrentFile()->getFilename(); - if (preg_match('#(?:/|\\\\)dev(?:/|\\\\)tests(?:/|\\\\)#', $filename)) { - // TODO: Temporarily blacklist anything in dev/tests until a sweep of dev/tests can be made. - // This block of the if should be removed leaving only the phtml condition when dev/tests is swept. - // Skip all dev tests files - $shouldFilter = true; - } elseif (preg_match('#(?:/|\\\\)Test(?:/|\\\\)Unit(?:/|\\\\)#', $filename)) { - $shouldFilter = true; - } elseif (preg_match('/\\.phtml$/', $filename)) { - // Skip all phtml files - $shouldFilter = true; - } - - return $shouldFilter; - } - - /** - * Determine if text is a class name - * - * @param string $class - * @return bool - */ - protected function isClassName($class) - { - $return = false; - if (preg_match('/^\\\\?[A-Z]\\w+(?:\\\\\\w+)*?$/', $class)) { - $return = true; - } - return $return; - } - - /** - * Determine if the text has an ambiguous type - * - * @param string $text - * @param array &$matches Type that was detected as ambiguous is in result. - * @return bool - */ - public function isAmbiguous($text, &$matches = []) - { - return preg_match('/(mixed)/', $text, $matches); - } - - /** - * Take the type and suggest the correct one. - * - * @param string $type - * @return string - */ - public function suggestType($type) - { - $suggestedName = null; - // First check to see if this type is a list of types. If so we break it up and check each - if (preg_match('/^.*?(?:\|.*)+$/', $type)) { - // Return list of all types in this string. - $types = explode('|', $type); - if (is_array($types)) { - // Loop over all types and call this method on each. - $suggestions = []; - foreach ($types as $t) { - $suggestions[] = $this->suggestType($t); - } - // Now that we have suggestions put them back together. - $suggestedName = implode('|', $suggestions); - } else { - $suggestedName = 'Unknown'; - } - } elseif ($this->isClassName($type)) { - // If this looks like a class name. - $suggestedName = $type; - } else { - // Only one type First check if that type is a base one. - $lowerVarType = strtolower($type); - if (in_array($lowerVarType, self::$allowedTypes)) { - $suggestedName = $lowerVarType; - } - // If no name suggested yet then call the phpcs version of this method. - if (empty($suggestedName)) { - $suggestedName = PHP_CodeSniffer::suggestType($type); - } - } - return $suggestedName; - } -} diff --git a/dev/tests/static/Magento/Sniffs/Annotations/RequireAnnotatedAttributesSniff.php b/dev/tests/static/Magento/Sniffs/Annotations/RequireAnnotatedAttributesSniff.php deleted file mode 100644 index 98fc99a0e..000000000 --- a/dev/tests/static/Magento/Sniffs/Annotations/RequireAnnotatedAttributesSniff.php +++ /dev/null @@ -1,357 +0,0 @@ - - *
  • A variable doc comment exists.
  • - *
  • Short description ends with a full stop.
  • - *
  • There is a blank line after the short description.
  • - *
  • There is a blank line between the description and the tags.
  • - *
  • Check the order, indentation and content of each tag.
  • - * - * - * @author Greg Sherwood - * @author Marc McIntyre - * @copyright 2006-2012 Squiz Pty Ltd (ABN 77 084 670 600) - * @license https://github.com/squizlabs/PHP_CodeSniffer/blob/master/licence.txt BSD Licence - * @version Release: @package_version@ - * @link http://pear.php.net/package/PHP_CodeSniffer - * - * @SuppressWarnings(PHPMD) - */ -class RequireAnnotatedAttributesSniff extends PHP_CodeSniffer_Standards_AbstractVariableSniff -{ - /** - * The header comment parser for the current file. - * - * @var PHP_CodeSniffer_CommentParser_ClassCommentParser - */ - protected $commentParser = null; - - /** - * The sniff helper for stuff shared between the annotations sniffs - * - * @var Helper - */ - protected $helper = null; - - /** - * Extract the var comment docblock - * - * @param array $tokens - * @param string $commentToken - * @param int $stackPtr The position of the current token in the stack passed in $tokens. - * @return int|false - */ - protected function extractVarDocBlock($tokens, $commentToken, $stackPtr) - { - $commentEnd = $this->helper->getCurrentFile()->findPrevious($commentToken, $stackPtr - 3); - $break = false; - if ($commentEnd !== false && $tokens[$commentEnd]['code'] === T_COMMENT) { - $this->helper->addMessage($stackPtr, Helper::WRONG_STYLE, ['variable']); - $break = true; - } elseif ($commentEnd === false || $tokens[$commentEnd]['code'] !== T_DOC_COMMENT) { - $this->helper->addMessage($stackPtr, Helper::MISSING, ['variable']); - $break = true; - } else { - // Make sure the comment we have found belongs to us. - $commentFor = $this->helper->getCurrentFile()->findNext( - [T_VARIABLE, T_CLASS, T_INTERFACE], - $commentEnd + 1 - ); - if ($commentFor !== $stackPtr) { - $this->helper->addMessage($stackPtr, Helper::MISSING, ['variable']); - $break = true; - } - } - return $break ? false : $commentEnd; - } - - /** - * Checks for short and long descriptions on variable definitions - * - * @param PHP_CodeSniffer_CommentParser_CommentElement $comment - * @param int $commentStart - * @return void - */ - protected function checkForDescription($comment, $commentStart) - { - $short = $comment->getShortComment(); - $long = ''; - $newlineCount = 0; - if (trim($short) === '') { - $this->helper->addMessage($commentStart, Helper::MISSING_SHORT, ['variable']); - $newlineCount = 1; - } else { - // No extra newline before short description. - $newlineSpan = strspn($short, $this->helper->getEolChar()); - if ($short !== '' && $newlineSpan > 0) { - $this->helper->addMessage($commentStart + 1, Helper::SPACING_BEFORE_SHORT, ['variable']); - } - - $newlineCount = substr_count($short, $this->helper->getEolChar()) + 1; - - // Exactly one blank line between short and long description. - $long = $comment->getLongComment(); - if (empty($long) === false) { - $between = $comment->getWhiteSpaceBetween(); - $newlineBetween = substr_count($between, $this->helper->getEolChar()); - if ($newlineBetween !== 2) { - $this->helper->addMessage( - $commentStart + $newlineCount + 1, - Helper::SPACING_BETWEEN, - ['variable'] - ); - } - - $newlineCount += $newlineBetween; - - $testLong = trim($long); - if (preg_match('|\p{Lu}|u', $testLong[0]) === 0) { - $this->helper->addMessage( - $commentStart + $newlineCount, - Helper::LONG_NOT_CAPITAL, - ['Variable'] - ); - } - } - - // Short description must be single line and end with a full stop. - $testShort = trim($short); - $lastChar = $testShort[strlen($testShort) - 1]; - if (substr_count($testShort, $this->helper->getEolChar()) !== 0) { - $this->helper->addMessage($commentStart + 1, Helper::SHORT_SINGLE_LINE, ['Variable']); - } - - if (preg_match('|\p{Lu}|u', $testShort[0]) === 0) { - $this->helper->addMessage($commentStart + 1, Helper::SHORT_NOT_CAPITAL, ['Variable']); - } - - if ($lastChar !== '.') { - $this->helper->addMessage($commentStart + 1, Helper::SHORT_FULL_STOP, ['Variable']); - } - } - // Exactly one blank line before tags. - $tags = $this->commentParser->getTagOrders(); - if (count($tags) > 1) { - $newlineSpan = $comment->getNewlineAfter(); - if ($newlineSpan !== 2) { - if ($long !== '') { - $newlineCount += substr_count($long, $this->helper->getEolChar()) - $newlineSpan + 1; - } - - $this->helper->addMessage( - $commentStart + $newlineCount, - Helper::SPACING_BEFORE_TAGS, - ['variable'] - ); - $short = rtrim($short, $this->helper->getEolChar() . ' '); - } - } - } - - /** - * Called to process class member vars. - * - * @param PHP_CodeSniffer_File $phpcsFile The file being scanned. - * @param int $stackPtr The position of the current token - * in the stack passed in $tokens. - * - * @return void - */ - public function processMemberVar(PHP_CodeSniffer_File $phpcsFile, $stackPtr) - { - $this->helper = new Helper($phpcsFile); - // if we should skip this type we should do that - if ($this->helper->shouldFilter()) { - return; - } - $tokens = $phpcsFile->getTokens(); - $commentToken = [T_COMMENT, T_DOC_COMMENT]; - - // Extract the var comment docblock. - $commentEnd = $this->extractVarDocBlock($tokens, $commentToken, $stackPtr); - if ($commentEnd === false) { - return; - } - - $commentStart = $phpcsFile->findPrevious(T_DOC_COMMENT, $commentEnd - 1, null, true) + 1; - $commentString = $phpcsFile->getTokensAsString($commentStart, $commentEnd - $commentStart + 1); - - // Parse the header comment docblock. - try { - $this->commentParser = new PHP_CodeSniffer_CommentParser_MemberCommentParser($commentString, $phpcsFile); - $this->commentParser->parse(); - } catch (PHP_CodeSniffer_CommentParser_ParserException $e) { - $line = $e->getLineWithinComment() + $commentStart; - $data = [$e->getMessage()]; - $this->helper->addMessage($line, Helper::ERROR_PARSING, $data); - return; - } - - $comment = $this->commentParser->getComment(); - if (($comment === null) === true) { - $this->helper->addMessage($commentStart, Helper::EMPTY_DOC, ['Variable']); - return; - } - - // The first line of the comment should just be the /** code. - $eolPos = strpos($commentString, $phpcsFile->eolChar); - $firstLine = substr($commentString, 0, $eolPos); - if ($firstLine !== '/**') { - $this->helper->addMessage($commentStart, Helper::CONTENT_AFTER_OPEN); - } - - // Check for a comment description. - $this->checkForDescription($comment, $commentStart); - - // Check for unknown/deprecated tags. - $unknownTags = $this->commentParser->getUnknown(); - foreach ($unknownTags as $errorTag) { - // Unknown tags are not parsed, do not process further. - $data = [$errorTag['tag']]; - $this->helper->addMessage($commentStart + $errorTag['line'], Helper::TAG_NOT_ALLOWED, $data); - } - - // Check each tag. - $this->processVar($commentStart, $commentEnd); - $this->processSees($commentStart); - - // The last content should be a newline and the content before - // that should not be blank. If there is more blank space - // then they have additional blank lines at the end of the comment. - $words = $this->commentParser->getWords(); - $lastPos = count($words) - 1; - if (trim( - $words[$lastPos - 1] - ) !== '' || strpos( - $words[$lastPos - 1], - $this->currentFile->eolChar - ) === false || trim( - $words[$lastPos - 2] - ) === '' - ) { - $this->helper->addMessage($commentEnd, Helper::SPACING_AFTER, ['variable']); - } - } - - /** - * Process the var tag. - * - * @param int $commentStart The position in the stack where the comment started. - * @param int $commentEnd The position in the stack where the comment ended. - * - * @return void - */ - protected function processVar($commentStart, $commentEnd) - { - $var = $this->commentParser->getVar(); - - if ($var !== null) { - $errorPos = $commentStart + $var->getLine(); - $index = array_keys($this->commentParser->getTagOrders(), 'var'); - - if (count($index) > 1) { - $this->helper->addMessage($errorPos, Helper::DUPLICATE_VAR); - return; - } - - if ($index[0] !== 1) { - $this->helper->addMessage($errorPos, Helper::VAR_ORDER); - } - - $content = $var->getContent(); - if (empty($content) === true) { - $this->helper->addMessage($errorPos, Helper::MISSING_VAR_TYPE); - return; - } else { - $suggestedType = $this->helper->suggestType($content); - if ($content !== $suggestedType) { - $data = [$suggestedType, $content]; - $this->helper->addMessage($errorPos, Helper::INCORRECT_VAR_TYPE, $data); - } elseif ($this->helper->isAmbiguous($content, $matches)) { - // Warn about ambiguous types ie array or mixed - $data = [$matches[1], '@var']; - $this->helper->addMessage($errorPos, Helper::AMBIGUOUS_TYPE, $data); - } - } - - $spacing = substr_count($var->getWhitespaceBeforeContent(), ' '); - if ($spacing !== 1) { - $data = [$spacing]; - $this->helper->addMessage($errorPos, Helper::VAR_INDENT, $data); - } - } else { - $this->helper->addMessage($commentEnd, Helper::MISSING_VAR); - } - } - - /** - * Process the see tags. - * - * @param int $commentStart The position in the stack where the comment started. - * - * @return void - */ - protected function processSees($commentStart) - { - $sees = $this->commentParser->getSees(); - if (empty($sees) === false) { - foreach ($sees as $see) { - $errorPos = $commentStart + $see->getLine(); - $content = $see->getContent(); - if (empty($content) === true) { - $this->helper->addMessage($errorPos, Helper::EMPTY_SEE, ['variable']); - continue; - } - - $spacing = substr_count($see->getWhitespaceBeforeContent(), ' '); - if ($spacing !== 1) { - $data = [$spacing]; - $this->helper->addMessage($errorPos, Helper::SEE_INDENT, $data); - } - } - } - } - - /** - * Called to process a normal variable. - * - * Not required for this sniff. - * - * @param PHP_CodeSniffer_File $phpcsFile The PHP_CodeSniffer file where this token was found. - * @param int $stackPtr The position where the double quoted - * string was found. - * - * @return void - */ - protected function processVariable(PHP_CodeSniffer_File $phpcsFile, $stackPtr) - { - } - - /** - * Called to process variables found in double quoted strings. - * - * Not required for this sniff. - * - * @param PHP_CodeSniffer_File $phpcsFile The PHP_CodeSniffer file where this token was found. - * @param int $stackPtr The position where the double quoted - * string was found. - * - * @return void - */ - protected function processVariableInString(PHP_CodeSniffer_File $phpcsFile, $stackPtr) - { - } -} diff --git a/dev/tests/static/Magento/Sniffs/Annotations/RequireAnnotatedMethodsSniff.php b/dev/tests/static/Magento/Sniffs/Annotations/RequireAnnotatedMethodsSniff.php deleted file mode 100644 index 63dc220b6..000000000 --- a/dev/tests/static/Magento/Sniffs/Annotations/RequireAnnotatedMethodsSniff.php +++ /dev/null @@ -1,694 +0,0 @@ - - * @author Marc McIntyre - * @copyright 2006-2012 Squiz Pty Ltd (ABN 77 084 670 600) - * @license https://github.com/squizlabs/PHP_CodeSniffer/blob/master/licence.txt BSD Licence - * @link http://pear.php.net/package/PHP_CodeSniffer - * - * @SuppressWarnings(PHPMD) - */ -class RequireAnnotatedMethodsSniff implements PHP_CodeSniffer_Sniff -{ - /** - * The name of the method that we are currently processing. - * - * @var string - */ - private $_methodName = ''; - - /** - * The position in the stack where the function token was found. - * - * @var int - */ - private $_functionToken = null; - - /** - * The position in the stack where the class token was found. - * - * @var int - */ - private $_classToken = null; - - /** - * The index of the current tag we are processing. - * - * @var int - */ - private $_tagIndex = 0; - - /** - * The function comment parser for the current method. - * - * @var PHP_CodeSniffer_CommentParser_FunctionCommentParser - */ - protected $commentParser = null; - - /** - * The sniff helper for stuff shared between the annotations sniffs - * - * @var Helper - */ - protected $helper = null; - - /** - * Returns an array of tokens this test wants to listen for. - * - * @return array - */ - public function register() - { - return [T_FUNCTION]; - } - - /** - * Processes this test, when one of its tokens is encountered. - * - * @param PHP_CodeSniffer_File $phpcsFile The file being scanned. - * @param int $stackPtr The position of the current token - * in the stack passed in $tokens. - * - * @return void - */ - public function process(PHP_CodeSniffer_File $phpcsFile, $stackPtr) - { - $this->helper = new Helper($phpcsFile); - // if we should skip this type we should do that - if ($this->helper->shouldFilter()) { - return; - } - - $tokens = $phpcsFile->getTokens(); - - $find = [T_COMMENT, T_DOC_COMMENT, T_CLASS, T_FUNCTION, T_OPEN_TAG]; - - $commentEnd = $phpcsFile->findPrevious($find, $stackPtr - 1); - - if ($commentEnd === false) { - return; - } - - // If the token that we found was a class or a function, then this - // function has no doc comment. - $code = $tokens[$commentEnd]['code']; - - if ($code === T_COMMENT) { - // The function might actually be missing a comment, and this last comment - // found is just commenting a bit of code on a line. So if it is not the - // only thing on the line, assume we found nothing. - $prevContent = $phpcsFile->findPrevious(PHP_CodeSniffer_Tokens::$emptyTokens, $commentEnd); - if ($tokens[$commentEnd]['line'] === $tokens[$commentEnd]['line']) { - $this->helper->addMessage($stackPtr, Helper::MISSING, ['function']); - } else { - $this->helper->addMessage($stackPtr, Helper::WRONG_STYLE, ['function']); - } - return; - } elseif ($code !== T_DOC_COMMENT) { - $this->helper->addMessage($stackPtr, Helper::MISSING, ['function']); - return; - } elseif (trim($tokens[$commentEnd]['content']) !== '*/') { - $this->helper->addMessage($commentEnd, Helper::WRONG_END, [trim($tokens[$commentEnd]['content'])]); - return; - } - - // If there is any code between the function keyword and the doc block - // then the doc block is not for us. - $ignore = PHP_CodeSniffer_Tokens::$scopeModifiers; - $ignore[] = T_STATIC; - $ignore[] = T_WHITESPACE; - $ignore[] = T_ABSTRACT; - $ignore[] = T_FINAL; - $prevToken = $phpcsFile->findPrevious($ignore, $stackPtr - 1, null, true); - if ($prevToken !== $commentEnd) { - $this->helper->addMessage($stackPtr, Helper::MISSING, ['function']); - return; - } - - $this->_functionToken = $stackPtr; - - $this->_classToken = null; - foreach ($tokens[$stackPtr]['conditions'] as $condPtr => $condition) { - if ($condition === T_CLASS || $condition === T_INTERFACE) { - $this->_classToken = $condPtr; - break; - } - } - - // Find the first doc comment. - $commentStart = $phpcsFile->findPrevious(T_DOC_COMMENT, $commentEnd - 1, null, true) + 1; - $commentString = $phpcsFile->getTokensAsString($commentStart, $commentEnd - $commentStart + 1); - $this->_methodName = $phpcsFile->getDeclarationName($stackPtr); - - try { - $this->commentParser = new PHP_CodeSniffer_CommentParser_FunctionCommentParser($commentString, $phpcsFile); - $this->commentParser->parse(); - } catch (PHP_CodeSniffer_CommentParser_ParserException $e) { - $line = $e->getLineWithinComment() + $commentStart; - $this->helper->addMessage($line, Helper::FAILED_PARSE, [$e->getMessage()]); - return; - } - - $comment = $this->commentParser->getComment(); - if (($comment === null) === true) { - $this->helper->addMessage($commentStart, Helper::EMPTY_DOC, ['Function']); - return; - } - - // The first line of the comment should just be the /** code. - $eolPos = strpos($commentString, $phpcsFile->eolChar); - $firstLine = substr($commentString, 0, $eolPos); - if ($firstLine !== '/**') { - $this->helper->addMessage($commentStart, Helper::CONTENT_AFTER_OPEN); - } - - // If the comment has an inherit doc note just move on - if (preg_match('/\{\@inheritdoc\}/', $commentString)) { - return; - } elseif (preg_match('/\{?\@?inherit[dD]oc\}?/', $commentString)) { - $this->helper->addMessage($commentStart, Helper::INCORRECT_INHERIT_DOC); - return; - } - - $this->processParams($commentStart, $commentEnd); - $this->processSees($commentStart); - $this->processReturn($commentStart, $commentEnd); - $this->processThrows($commentStart); - - // Check for a comment description. - $short = $comment->getShortComment(); - if (trim($short) === '') { - $this->helper->addMessage($commentStart, Helper::MISSING_SHORT, ['function']); - return; - } - - // No extra newline before short description. - $newlineCount = 0; - $newlineSpan = strspn($short, $phpcsFile->eolChar); - if ($short !== '' && $newlineSpan > 0) { - $this->helper->addMessage($commentStart + 1, Helper::SPACING_BEFORE_SHORT, ['function']); - } - - $newlineCount = substr_count($short, $phpcsFile->eolChar) + 1; - - // Exactly one blank line between short and long description. - $long = $comment->getLongComment(); - if (empty($long) === false) { - $between = $comment->getWhiteSpaceBetween(); - $newlineBetween = substr_count($between, $phpcsFile->eolChar); - if ($newlineBetween !== 2) { - $this->helper->addMessage( - $commentStart + $newlineCount + 1, - Helper::SPACING_BETWEEN, - ['function'] - ); - } - $newlineCount += $newlineBetween; - $testLong = trim($long); - if (preg_match('|\p{Lu}|u', $testLong[0]) === 0) { - $this->helper->addMessage($commentStart + $newlineCount, Helper::LONG_NOT_CAPITAL, ['Function']); - } - } - - // Exactly one blank line before tags. - $params = $this->commentParser->getTagOrders(); - if (count($params) > 1) { - $newlineSpan = $comment->getNewlineAfter(); - if ($newlineSpan !== 2) { - if ($long !== '') { - $newlineCount += substr_count($long, $phpcsFile->eolChar) - $newlineSpan + 1; - } - - $this->helper->addMessage( - $commentStart + $newlineCount, - Helper::SPACING_BEFORE_TAGS, - ['function'] - ); - $short = rtrim($short, $phpcsFile->eolChar . ' '); - } - } - - // Short description must be single line and end with a full stop. - $testShort = trim($short); - $lastChar = $testShort[strlen($testShort) - 1]; - if (substr_count($testShort, $phpcsFile->eolChar) !== 0) { - $this->helper->addMessage($commentStart + 1, Helper::SHORT_SINGLE_LINE, ['Function']); - } - - if (preg_match('|\p{Lu}|u', $testShort[0]) === 0) { - $this->helper->addMessage($commentStart + 1, Helper::SHORT_NOT_CAPITAL, ['Function']); - } - - if ($lastChar !== '.') { - $this->helper->addMessage($commentStart + 1, Helper::SHORT_FULL_STOP, ['Function']); - } - - // Check for unknown/deprecated tags. - // For example call: $this->processUnknownTags($commentStart, $commentEnd); - - // The last content should be a newline and the content before - // that should not be blank. If there is more blank space - // then they have additional blank lines at the end of the comment. - $words = $this->commentParser->getWords(); - $lastPos = count($words) - 1; - if (trim( - $words[$lastPos - 1] - ) !== '' || strpos( - $words[$lastPos - 1], - $this->helper->getCurrentFile()->eolChar - ) === false || trim( - $words[$lastPos - 2] - ) === '' - ) { - $this->helper->addMessage($commentEnd, Helper::SPACING_AFTER, ['function']); - } - } - - /** - * Process the see tags. - * - * @param int $commentStart The position in the stack where the comment started. - * - * @return void - */ - protected function processSees($commentStart) - { - $sees = $this->commentParser->getSees(); - if (empty($sees) === false) { - $tagOrder = $this->commentParser->getTagOrders(); - $index = array_keys($this->commentParser->getTagOrders(), 'see'); - foreach ($sees as $i => $see) { - $errorPos = $commentStart + $see->getLine(); - $since = array_keys($tagOrder, 'since'); - if (count($since) === 1 && $this->_tagIndex !== 0) { - $this->_tagIndex++; - if ($index[$i] !== $this->_tagIndex) { - $this->helper->addMessage($errorPos, Helper::SEE_ORDER); - } - } - - $content = $see->getContent(); - if (empty($content) === true) { - $this->helper->addMessage($errorPos, Helper::EMPTY_SEE, ['function']); - continue; - } - } - } - } - - /** - * Process the return comment of this function comment. - * - * @param int $commentStart The position in the stack where the comment started. - * @param int $commentEnd The position in the stack where the comment ended. - * - * @return void - */ - protected function processReturn($commentStart, $commentEnd) - { - // Skip constructor and destructor. - $className = ''; - if ($this->_classToken !== null) { - $className = $this->helper->getCurrentFile()->getDeclarationName($this->_classToken); - $className = strtolower(ltrim($className, '_')); - } - - $methodName = strtolower(ltrim($this->_methodName, '_')); - $return = $this->commentParser->getReturn(); - - if ($this->_methodName !== '__construct' && $this->_methodName !== '__destruct') { - if ($return !== null) { - $tagOrder = $this->commentParser->getTagOrders(); - $index = array_keys($tagOrder, 'return'); - $errorPos = $commentStart + $return->getLine(); - $content = trim($return->getRawContent()); - - if (count($index) > 1) { - $this->helper->addMessage($errorPos, Helper::DUPLICATE_RETURN); - return; - } - - $since = array_keys($tagOrder, 'since'); - if (count($since) === 1 && $this->_tagIndex !== 0) { - $this->_tagIndex++; - if ($index[0] !== $this->_tagIndex) { - $this->helper->addMessage($errorPos, Helper::RETURN_ORDER); - } - } - - if (empty($content) === true) { - $this->helper->addMessage($errorPos, Helper::MISSING_RETURN_TYPE); - } else { - // Strip off any comments attached to our content - $parts = explode(' ', $content); - $content = $parts[0]; - // Check return type (can be multiple, separated by '|'). - $typeNames = explode('|', $content); - $suggestedNames = []; - foreach ($typeNames as $i => $typeName) { - $suggestedName = $this->helper->suggestType($typeName); - if (in_array($suggestedName, $suggestedNames) === false) { - $suggestedNames[] = $suggestedName; - } - } - - $suggestedType = implode('|', $suggestedNames); - if ($content !== $suggestedType) { - $data = [$content]; - $this->helper->addMessage($errorPos, Helper::INVALID_RETURN, $data); - } elseif ($this->helper->isAmbiguous($typeName, $matches)) { - // Warn about ambiguous types ie array or mixed - $data = [$matches[1], '@return']; - $this->helper->addMessage($errorPos, Helper::AMBIGUOUS_TYPE, $data); - } - - $tokens = $this->helper->getCurrentFile()->getTokens(); - - // If the return type is void, make sure there is - // no return statement in the function. - if ($content === 'void') { - if (isset($tokens[$this->_functionToken]['scope_closer']) === true) { - $endToken = $tokens[$this->_functionToken]['scope_closer']; - - $tokens = $this->helper->getCurrentFile()->getTokens(); - for ($returnToken = $this->_functionToken; $returnToken < $endToken; $returnToken++) { - if ($tokens[$returnToken]['code'] === T_CLOSURE) { - $returnToken = $tokens[$returnToken]['scope_closer']; - continue; - } - - if ($tokens[$returnToken]['code'] === T_RETURN) { - break; - } - } - - if ($returnToken !== $endToken) { - // If the function is not returning anything, just - // exiting, then there is no problem. - $semicolon = $this->helper->getCurrentFile()->findNext( - T_WHITESPACE, - $returnToken + 1, - null, - true - ); - if ($tokens[$semicolon]['code'] !== T_SEMICOLON) { - $this->helper->addMessage($errorPos, Helper::INVALID_RETURN_VOID); - } - } - } - } elseif ($content !== 'mixed') { - // If return type is not void, there needs to be a - // returns statement somewhere in the function that - // returns something. - if (isset($tokens[$this->_functionToken]['scope_closer']) === true) { - $endToken = $tokens[$this->_functionToken]['scope_closer']; - $returnToken = $this->helper->getCurrentFile()->findNext( - T_RETURN, - $this->_functionToken, - $endToken - ); - if ($returnToken === false) { - $this->helper->addMessage($errorPos, Helper::INVALID_NO_RETURN); - } else { - $semicolon = $this->helper->getCurrentFile()->findNext( - T_WHITESPACE, - $returnToken + 1, - null, - true - ); - if ($tokens[$semicolon]['code'] === T_SEMICOLON) { - $this->helper->addMessage($returnToken, Helper::INVALID_RETURN_NOT_VOID); - } - } - } - } - - $spacing = substr_count($return->getWhitespaceBeforeValue(), ' '); - if ($spacing !== 1) { - $data = [$spacing]; - $this->helper->addMessage($errorPos, Helper::RETURN_INDENT, $data); - } - } - } else { - $this->helper->addMessage($commentEnd, Helper::MISSING_RETURN); - } - } elseif ($return !== null) { - // No return tag for constructor and destructor. - $errorPos = $commentStart + $return->getLine(); - $this->helper->addMessage($errorPos, Helper::RETURN_NOT_REQUIRED); - } - } - - /** - * Process any throw tags that this function comment has. - * - * @param int $commentStart The position in the stack where the comment started. - * - * @return void - */ - protected function processThrows($commentStart) - { - if (count($this->commentParser->getThrows()) === 0) { - return; - } - - $tagOrder = $this->commentParser->getTagOrders(); - $index = array_keys($this->commentParser->getTagOrders(), 'throws'); - - foreach ($this->commentParser->getThrows() as $i => $throw) { - $exception = $throw->getValue(); - $content = trim($throw->getComment()); - $errorPos = $commentStart + $throw->getLine(); - if (empty($exception) === true) { - $this->helper->addMessage($errorPos, Helper::INVALID_THROWS); - } elseif (empty($content) === true) { - $this->helper->addMessage($errorPos, Helper::EMPTY_THROWS); - } else { - // Assumes that $content is not empty. - // Starts with a capital letter and ends with a fullstop. - $firstChar = $content[0]; - if (strtoupper($firstChar) !== $firstChar) { - $this->helper->addMessage($errorPos, Helper::THROWS_NOT_CAPITAL); - } - - $lastChar = $content[strlen($content) - 1]; - if ($lastChar !== '.') { - $this->helper->addMessage($errorPos, Helper::THROWS_NO_FULL_STOP); - } - } - - $since = array_keys($tagOrder, 'since'); - if (count($since) === 1 && $this->_tagIndex !== 0) { - $this->_tagIndex++; - if ($index[$i] !== $this->_tagIndex) { - $this->helper->addMessage($errorPos, Helper::THROWS_ORDER); - } - } - } - } - - /** - * Process the function parameter comments. - * - * @param int $commentStart The position in the stack where - * the comment started. - * @param int $commentEnd The position in the stack where - * the comment ended. - * - * @return void - */ - protected function processParams($commentStart, $commentEnd) - { - $realParams = $this->helper->getCurrentFile()->getMethodParameters($this->_functionToken); - $params = $this->commentParser->getParams(); - $foundParams = []; - - if (empty($params) === false) { - $subStrCount = substr_count( - $params[count($params) - 1]->getWhitespaceAfter(), - $this->helper->getCurrentFile()->eolChar - ); - if ($subStrCount !== 2) { - $errorPos = $params[count($params) - 1]->getLine() + $commentStart; - $this->helper->addMessage($errorPos, Helper::SPACING_AFTER_PARAMS); - } - - // Parameters must appear immediately after the comment. - if ($params[0]->getOrder() !== 2) { - $errorPos = $params[0]->getLine() + $commentStart; - $this->helper->addMessage($errorPos, Helper::SPACING_BEFORE_PARAMS); - } - - $previousParam = null; - $spaceBeforeVar = 10000; - $spaceBeforeComment = 10000; - $longestType = 0; - $longestVar = 0; - - foreach ($params as $param) { - $paramComment = trim($param->getComment()); - $errorPos = $param->getLine() + $commentStart; - - // Make sure that there is only one space before the var type. - if ($param->getWhitespaceBeforeType() !== ' ') { - $this->helper->addMessage($errorPos, Helper::SPACING_BEFORE_PARAM_TYPE); - } - - $spaceCount = substr_count($param->getWhitespaceBeforeVarName(), ' '); - if ($spaceCount < $spaceBeforeVar) { - $spaceBeforeVar = $spaceCount; - $longestType = $errorPos; - } - - $spaceCount = substr_count($param->getWhitespaceBeforeComment(), ' '); - - if ($spaceCount < $spaceBeforeComment && $paramComment !== '') { - $spaceBeforeComment = $spaceCount; - $longestVar = $errorPos; - } - - // Make sure they are in the correct order, and have the correct name. - $pos = $param->getPosition(); - $paramName = $param->getVarName() !== '' ? $param->getVarName() : '[ UNKNOWN ]'; - - if ($previousParam !== null) { - $previousName = $previousParam->getVarName() !== '' ? $previousParam->getVarName() : 'UNKNOWN'; - } - - // Variable must be one of the supported standard type. - $typeNames = explode('|', $param->getType()); - foreach ($typeNames as $typeName) { - $suggestedName = $this->helper->suggestType($typeName); - if ($typeName !== $suggestedName) { - $data = [$suggestedName, $typeName, $paramName, $pos]; - $this->helper->addMessage($errorPos, Helper::INCORRECT_PARAM_VAR_NAME, $data); - } elseif ($this->helper->isAmbiguous($typeName, $matches)) { - // Warn about ambiguous types ie array or mixed - $data = [$matches[1], $paramName, ' at position ' . $pos . ' is NOT recommended']; - $this->helper->addMessage($commentEnd + 2, Helper::AMBIGUOUS_TYPE, $data); - } elseif (count($typeNames) === 1) { - // Check type hint for array and custom type. - $suggestedTypeHint = ''; - if (strpos($suggestedName, 'array') !== false) { - $suggestedTypeHint = 'array'; - } elseif (strpos($suggestedName, 'callable') !== false) { - $suggestedTypeHint = 'callable'; - } elseif (in_array($typeName, $this->helper->getAllowedTypes()) === false) { - $suggestedTypeHint = $suggestedName; - } else { - $suggestedTypeHint = $this->helper->suggestType($typeName); - } - - if ($suggestedTypeHint !== '' && isset($realParams[$pos - 1]) === true) { - $typeHint = $realParams[$pos - 1]['type_hint']; - if ($typeHint === '') { - $data = [$suggestedTypeHint, $paramName, $pos]; - $this->helper->addMessage($commentEnd + 2, Helper::TYPE_HINT_MISSING, $data); - } elseif ($typeHint !== $suggestedTypeHint) { - $data = [$suggestedTypeHint, $typeHint, $paramName, $pos]; - $this->helper->addMessage($commentEnd + 2, Helper::INCORRECT_TYPE_HINT, $data); - } - } elseif ($suggestedTypeHint === '' && isset($realParams[$pos - 1]) === true) { - $typeHint = $realParams[$pos - 1]['type_hint']; - if ($typeHint !== '') { - $data = [$typeHint, $paramName, $pos]; - $this->helper->addMessage($commentEnd + 2, Helper::INVALID_TYPE_HINT, $data); - } - } - } - } - - // Make sure the names of the parameter comment matches the - // actual parameter. - if (isset($realParams[$pos - 1]) === true) { - $realName = $realParams[$pos - 1]['name']; - $foundParams[] = $realName; - - // Append ampersand to name if passing by reference. - if ($realParams[$pos - 1]['pass_by_reference'] === true) { - $realName = '&' . $realName; - } - - if ($realName !== $paramName) { - $code = Helper::PARAM_NAME_NO_MATCH; - $data = [$paramName, $realName, $pos]; - - if (strtolower($paramName) === strtolower($realName)) { - $code = Helper::PARAM_NAME_NO_CASE_MATCH; - } - - $this->helper->addMessage($errorPos, $code, $data); - } - } elseif (substr($paramName, -4) !== ',...') { - // We must have an extra parameter comment. - $this->helper->addMessage($errorPos, Helper::EXTRA_PARAM_COMMENT, [$pos]); - } - - if ($param->getVarName() === '') { - $this->helper->addMessage($errorPos, Helper::MISSING_PARAM_NAME, [$pos]); - } - - if ($param->getType() === '') { - $this->helper->addMessage($errorPos, Helper::MISSING_PARAM_TYPE, [$pos]); - } - - if ($paramComment === '') { - $data = [$paramName, $pos]; - $this->helper->addMessage($errorPos, Helper::MISSING_PARAM_COMMENT, $data); - } else { - // Param comments must start with a capital letter and - // end with the full stop. - $firstChar = $paramComment[0]; - if (preg_match('|\p{Lu}|u', $firstChar) === 0) { - $this->helper->addMessage($errorPos, Helper::PARAM_COMMENT_NOT_CAPITAL); - } - $lastChar = $paramComment[strlen($paramComment) - 1]; - if ($lastChar !== '.') { - $this->helper->addMessage($errorPos, Helper::PARAM_COMMENT_FULL_STOP); - } - } - - $previousParam = $param; - } - - if ($spaceBeforeVar !== 1 && $spaceBeforeVar !== 10000 && $spaceBeforeComment !== 10000) { - $this->helper->addMessage($longestType, Helper::SPACING_AFTER_LONG_TYPE); - } - - if ($spaceBeforeComment !== 1 && $spaceBeforeComment !== 10000) { - $this->helper->addMessage($longestVar, Helper::SPACING_AFTER_LONG_NAME); - } - } - - $realNames = []; - foreach ($realParams as $realParam) { - $realNames[] = $realParam['name']; - } - - // Report missing comments. - $diff = array_diff($realNames, $foundParams); - foreach ($diff as $neededParam) { - if (count($params) !== 0) { - $errorPos = $params[count($params) - 1]->getLine() + $commentStart; - } else { - $errorPos = $commentStart; - } - - $data = [$neededParam]; - $this->helper->addMessage($errorPos, Helper::MISSING_PARAM_TAG, $data); - } - } -} diff --git a/dev/tests/static/Magento/Sniffs/Arrays/ShortArraySyntaxSniff.php b/dev/tests/static/Magento/Sniffs/Arrays/ShortArraySyntaxSniff.php index e8e850706..9013f12b6 100644 --- a/dev/tests/static/Magento/Sniffs/Arrays/ShortArraySyntaxSniff.php +++ b/dev/tests/static/Magento/Sniffs/Arrays/ShortArraySyntaxSniff.php @@ -5,10 +5,10 @@ */ namespace Magento\Sniffs\Arrays; -use PHP_CodeSniffer_File; -use PHP_CodeSniffer_Sniff; +use PHP_CodeSniffer\Sniffs\Sniff; +use PHP_CodeSniffer\Files\File; -class ShortArraySyntaxSniff implements PHP_CodeSniffer_Sniff +class ShortArraySyntaxSniff implements Sniff { /** * {@inheritdoc} @@ -21,8 +21,12 @@ public function register() /** * {@inheritdoc} */ - public function process(PHP_CodeSniffer_File $sourceFile, $stackPtr) + public function process(File $sourceFile, $stackPtr) { - $sourceFile->addError('Short array syntax must be used; expected "[]" but found "array()"', $stackPtr); + $sourceFile->addError( + 'Short array syntax must be used; expected "[]" but found "array()"', + $stackPtr, + 'ShortArraySyntax' + ); } } diff --git a/dev/tests/static/Magento/Sniffs/Commenting/FunctionCommentSniff.php b/dev/tests/static/Magento/Sniffs/Commenting/FunctionCommentSniff.php new file mode 100644 index 000000000..2bd8be194 --- /dev/null +++ b/dev/tests/static/Magento/Sniffs/Commenting/FunctionCommentSniff.php @@ -0,0 +1,663 @@ + + * @copyright 2006-2015 Squiz Pty Ltd (ABN 77 084 670 600) + * @license https://github.com/squizlabs/PHP_CodeSniffer/blob/master/licence.txt BSD Licence + */ + +namespace Magento\Sniffs\Commenting; + +use PHP_CodeSniffer\Standards\PEAR\Sniffs\Commenting\FunctionCommentSniff as PEARFunctionCommentSniff; +use PHP_CodeSniffer\Files\File; +use PHP_CodeSniffer\Config; +use PHP_CodeSniffer\Util\Common; + +class FunctionCommentSniff extends PEARFunctionCommentSniff +{ + + /** + * The current PHP version. + * + * @var integer + */ + private $phpVersion = null; + + + /** + * Process the return comment of this function comment. + * + * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned. + * @param int $stackPtr The position of the current token + * in the stack passed in $tokens. + * @param int $commentStart The position in the stack where the comment started. + * + * @return void + */ + protected function processReturn(File $phpcsFile, $stackPtr, $commentStart) + { + $tokens = $phpcsFile->getTokens(); + + // Skip constructor and destructor. + $methodName = $phpcsFile->getDeclarationName($stackPtr); + $isSpecialMethod = ($methodName === '__construct' || $methodName === '__destruct'); + + $return = null; + foreach ($tokens[$commentStart]['comment_tags'] as $tag) { + if ($tokens[$tag]['content'] === '@return') { + if ($return !== null) { + $error = 'Only 1 @return tag is allowed in a function comment'; + $phpcsFile->addError($error, $tag, 'DuplicateReturn'); + return; + } + + $return = $tag; + } + } + + + if ($isSpecialMethod === true) { + return; + } + + if ($return !== null) { + $content = $tokens[($return + 2)]['content']; + if (empty($content) === true || $tokens[($return + 2)]['code'] !== T_DOC_COMMENT_STRING) { + $error = 'Return type missing for @return tag in function comment'; + $phpcsFile->addError($error, $return, 'MissingReturnType'); + } else { + // Support both a return type and a description. + $split = preg_match('`^((?:\|?(?:array\([^\)]*\)|[\\\\a-z0-9\[\]]+))*)( .*)?`i', $content, $returnParts); + if (isset($returnParts[1]) === false) { + return; + } + + $returnType = $returnParts[1]; + + // Check return type (can be multiple, separated by '|'). + $typeNames = explode('|', $returnType); + $suggestedNames = array(); + foreach ($typeNames as $i => $typeName) { + $suggestedName = Common::suggestType($typeName); + if (in_array($suggestedName, $suggestedNames) === false) { + $suggestedNames[] = $suggestedName; + } + } + + $suggestedType = implode('|', $suggestedNames); + if ($returnType !== $suggestedType) { + $error = 'Expected "%s" but found "%s" for function return type'; + $data = array( + $suggestedType, + $returnType, + ); + $fix = $phpcsFile->addFixableError($error, $return, 'InvalidReturn', $data); + if ($fix === true) { + $replacement = $suggestedType; + if (empty($returnParts[2]) === false) { + $replacement .= $returnParts[2]; + } + + $phpcsFile->fixer->replaceToken(($return + 2), $replacement); + unset($replacement); + } + } + + // If the return type is void, make sure there is + // no return statement in the function. + if ($returnType === 'void') { + if (isset($tokens[$stackPtr]['scope_closer']) === true) { + $endToken = $tokens[$stackPtr]['scope_closer']; + for ($returnToken = $stackPtr; $returnToken < $endToken; $returnToken++) { + if ($tokens[$returnToken]['code'] === T_CLOSURE + || $tokens[$returnToken]['code'] === T_ANON_CLASS + ) { + $returnToken = $tokens[$returnToken]['scope_closer']; + continue; + } + + if ($tokens[$returnToken]['code'] === T_RETURN + || $tokens[$returnToken]['code'] === T_YIELD + || $tokens[$returnToken]['code'] === T_YIELD_FROM + ) { + break; + } + } + + if ($returnToken !== $endToken) { + // If the function is not returning anything, just + // exiting, then there is no problem. + $semicolon = $phpcsFile->findNext(T_WHITESPACE, ($returnToken + 1), null, true); + if ($tokens[$semicolon]['code'] !== T_SEMICOLON) { + $error = 'Function return type is void, but function contains return statement'; + $phpcsFile->addError($error, $return, 'InvalidReturnVoid'); + } + } + }//end if + } else if ($returnType !== 'mixed' && in_array('void', $typeNames, true) === false) { + // If return type is not void, there needs to be a return statement + // somewhere in the function that returns something. + if (isset($tokens[$stackPtr]['scope_closer']) === true) { + $endToken = $tokens[$stackPtr]['scope_closer']; + $returnToken = $phpcsFile->findNext(array(T_RETURN, T_YIELD, T_YIELD_FROM), $stackPtr, $endToken); + if ($returnToken === false) { + $error = 'Function return type is not void, but function has no return statement'; + $phpcsFile->addError($error, $return, 'InvalidNoReturn'); + } else { + $semicolon = $phpcsFile->findNext(T_WHITESPACE, ($returnToken + 1), null, true); + if ($tokens[$semicolon]['code'] === T_SEMICOLON) { + $error = 'Function return type is not void, but function is returning void here'; + $phpcsFile->addError($error, $returnToken, 'InvalidReturnNotVoid'); + } + } + } + }//end if + }//end if + } else { + $error = 'Missing @return tag in function comment'; + $phpcsFile->addError($error, $tokens[$commentStart]['comment_closer'], 'MissingReturn'); + }//end if + + }//end processReturn() + + + /** + * Process any throw tags that this function comment has. + * + * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned. + * @param int $stackPtr The position of the current token + * in the stack passed in $tokens. + * @param int $commentStart The position in the stack where the comment started. + * + * @return void + */ + protected function processThrows(File $phpcsFile, $stackPtr, $commentStart) + { + $tokens = $phpcsFile->getTokens(); + + $throws = array(); + foreach ($tokens[$commentStart]['comment_tags'] as $pos => $tag) { + if ($tokens[$tag]['content'] !== '@throws') { + continue; + } + + $exception = null; + $comment = null; + if ($tokens[($tag + 2)]['code'] === T_DOC_COMMENT_STRING) { + $matches = array(); + preg_match('/([^\s]+)(?:\s+(.*))?/', $tokens[($tag + 2)]['content'], $matches); + $exception = $matches[1]; + if (isset($matches[2]) === true && trim($matches[2]) !== '') { + $comment = $matches[2]; + } + } + + if ($exception === null) { + $error = 'Exception type and comment missing for @throws tag in function comment'; + $phpcsFile->addError($error, $tag, 'InvalidThrows'); + } else if ($comment === null) { + $error = 'Comment missing for @throws tag in function comment'; +// $phpcsFile->addError($error, $tag, 'EmptyThrows'); + } else { + // Any strings until the next tag belong to this comment. + if (isset($tokens[$commentStart]['comment_tags'][($pos + 1)]) === true) { + $end = $tokens[$commentStart]['comment_tags'][($pos + 1)]; + } else { + $end = $tokens[$commentStart]['comment_closer']; + } + + for ($i = ($tag + 3); $i < $end; $i++) { + if ($tokens[$i]['code'] === T_DOC_COMMENT_STRING) { + $comment .= ' '.$tokens[$i]['content']; + } + } + + // Starts with a capital letter and ends with a fullstop. + $firstChar = $comment{0}; + if (strtoupper($firstChar) !== $firstChar) { + $error = '@throws tag comment must start with a capital letter'; + $phpcsFile->addError($error, ($tag + 2), 'ThrowsNotCapital'); + } + + $lastChar = substr($comment, -1); + if ($lastChar !== '.') { + $error = '@throws tag comment must end with a full stop'; + $phpcsFile->addError($error, ($tag + 2), 'ThrowsNoFullStop'); + } + }//end if + }//end foreach + + }//end processThrows() + + + /** + * Process the function parameter comments. + * + * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned. + * @param int $stackPtr The position of the current token + * in the stack passed in $tokens. + * @param int $commentStart The position in the stack where the comment started. + * + * @return void + */ + protected function processParams(File $phpcsFile, $stackPtr, $commentStart) + { + if ($this->phpVersion === null) { + $this->phpVersion = Config::getConfigData('php_version'); + if ($this->phpVersion === null) { + $this->phpVersion = PHP_VERSION_ID; + } + } + + $tokens = $phpcsFile->getTokens(); + + $params = array(); + $maxType = 0; + $maxVar = 0; + foreach ($tokens[$commentStart]['comment_tags'] as $pos => $tag) { + if ($tokens[$tag]['content'] !== '@param') { + continue; + } + + $type = ''; + $typeSpace = 0; + $var = ''; + $varSpace = 0; + $comment = ''; + $commentLines = array(); + if ($tokens[($tag + 2)]['code'] === T_DOC_COMMENT_STRING) { + $matches = array(); + preg_match('/([^$&.]+)(?:((?:\.\.\.)?(?:\$|&)[^\s]+)(?:(\s+)(.*))?)?/', $tokens[($tag + 2)]['content'], $matches); + + if (empty($matches) === false) { + $typeLen = strlen($matches[1]); + $type = trim($matches[1]); + $typeSpace = ($typeLen - strlen($type)); + $typeLen = strlen($type); + if ($typeLen > $maxType) { + $maxType = $typeLen; + } + } + + if (isset($matches[2]) === true) { + $var = $matches[2]; + $varLen = strlen($var); + if ($varLen > $maxVar) { + $maxVar = $varLen; + } + + if (isset($matches[4]) === true) { + $varSpace = strlen($matches[3]); + $comment = $matches[4]; + $commentLines[] = array( + 'comment' => $comment, + 'token' => ($tag + 2), + 'indent' => $varSpace, + ); + + // Any strings until the next tag belong to this comment. + if (isset($tokens[$commentStart]['comment_tags'][($pos + 1)]) === true) { + $end = $tokens[$commentStart]['comment_tags'][($pos + 1)]; + } else { + $end = $tokens[$commentStart]['comment_closer']; + } + + for ($i = ($tag + 3); $i < $end; $i++) { + if ($tokens[$i]['code'] === T_DOC_COMMENT_STRING) { + $indent = 0; + if ($tokens[($i - 1)]['code'] === T_DOC_COMMENT_WHITESPACE) { + $indent = strlen($tokens[($i - 1)]['content']); + } + + $comment .= ' '.$tokens[$i]['content']; + $commentLines[] = array( + 'comment' => $tokens[$i]['content'], + 'token' => $i, + 'indent' => $indent, + ); + } + } + } else { + $error = 'Missing parameter comment'; +// $phpcsFile->addError($error, $tag, 'MissingParamComment'); + $commentLines[] = array('comment' => ''); + }//end if + } else { + $error = 'Missing parameter name'; + $phpcsFile->addError($error, $tag, 'MissingParamName'); + }//end if + } else { + $error = 'Missing parameter type'; + $phpcsFile->addError($error, $tag, 'MissingParamType'); + }//end if + + $params[] = array( + 'tag' => $tag, + 'type' => $type, + 'var' => $var, + 'comment' => $comment, + 'commentLines' => $commentLines, + 'type_space' => $typeSpace, + 'var_space' => $varSpace, + ); + }//end foreach + + $realParams = $phpcsFile->getMethodParameters($stackPtr); + $foundParams = array(); + + // We want to use ... for all variable length arguments, so added + // this prefix to the variable name so comparisons are easier. + foreach ($realParams as $pos => $param) { + if ($param['variable_length'] === true) { + $realParams[$pos]['name'] = '...'.$realParams[$pos]['name']; + } + } + + foreach ($params as $pos => $param) { + // If the type is empty, the whole line is empty. + if ($param['type'] === '') { + continue; + } + + // Check the param type value. + $typeNames = explode('|', $param['type']); + $suggestedTypeNames = array(); + + foreach ($typeNames as $typeName) { + $suggestedName = Common::suggestType($typeName); + $suggestedTypeNames[] = $suggestedName; + + if (count($typeNames) > 1) { + continue; + } + + // Check type hint for array and custom type. + $suggestedTypeHint = ''; + if (strpos($suggestedName, 'array') !== false || substr($suggestedName, -2) === '[]') { + $suggestedTypeHint = 'array'; + } else if (strpos($suggestedName, 'callable') !== false) { + $suggestedTypeHint = 'callable'; + } else if (strpos($suggestedName, 'callback') !== false) { + $suggestedTypeHint = 'callable'; + } else if (in_array($suggestedName, Common::$allowedTypes) === false) { + $suggestedTypeHint = $suggestedName; + } + + if ($this->phpVersion >= 70000) { + if ($suggestedName === 'string') { + $suggestedTypeHint = 'string'; + } else if ($suggestedName === 'int' || $suggestedName === 'integer') { + $suggestedTypeHint = 'int'; + } else if ($suggestedName === 'float') { + $suggestedTypeHint = 'float'; + } else if ($suggestedName === 'bool' || $suggestedName === 'boolean') { + $suggestedTypeHint = 'bool'; + } + } + + if ($suggestedTypeHint !== '' && isset($realParams[$pos]) === true) { + $typeHint = $realParams[$pos]['type_hint']; + if ($typeHint === '') { + $error = 'Type hint "%s" missing for %s'; + $data = array( + $suggestedTypeHint, + $param['var'], + ); + + $errorCode = 'TypeHintMissing'; + if ($suggestedTypeHint === 'string' + || $suggestedTypeHint === 'int' + || $suggestedTypeHint === 'float' + || $suggestedTypeHint === 'bool' + ) { + $errorCode = 'Scalar'.$errorCode; + } + +// $phpcsFile->addError($error, $stackPtr, $errorCode, $data); + } else if ($typeHint !== substr($suggestedTypeHint, (strlen($typeHint) * -1))) { + $error = 'Expected type hint "%s"; found "%s" for %s'; + $data = array( + $suggestedTypeHint, + $typeHint, + $param['var'], + ); + $phpcsFile->addError($error, $stackPtr, 'IncorrectTypeHint', $data); + }//end if + } else if ($suggestedTypeHint === '' && isset($realParams[$pos]) === true) { + $typeHint = $realParams[$pos]['type_hint']; + if ($typeHint !== '') { + $error = 'Unknown type hint "%s" found for %s'; + $data = array( + $typeHint, + $param['var'], + ); + $phpcsFile->addError($error, $stackPtr, 'InvalidTypeHint', $data); + } + }//end if + }//end foreach + + $suggestedType = implode($suggestedTypeNames, '|'); + if ($param['type'] !== $suggestedType) { + $error = 'Expected "%s" but found "%s" for parameter type'; + $data = array( + $suggestedType, + $param['type'], + ); + + $fix = $phpcsFile->addFixableError($error, $param['tag'], 'IncorrectParamVarName', $data); + if ($fix === true) { + $phpcsFile->fixer->beginChangeset(); + + $content = $suggestedType; + $content .= str_repeat(' ', $param['type_space']); + $content .= $param['var']; + $content .= str_repeat(' ', $param['var_space']); + if (isset($param['commentLines'][0]) === true) { + $content .= $param['commentLines'][0]['comment']; + } + + $phpcsFile->fixer->replaceToken(($param['tag'] + 2), $content); + + // Fix up the indent of additional comment lines. + foreach ($param['commentLines'] as $lineNum => $line) { + if ($lineNum === 0 + || $param['commentLines'][$lineNum]['indent'] === 0 + ) { + continue; + } + + $diff = (strlen($param['type']) - strlen($suggestedType)); + $newIndent = ($param['commentLines'][$lineNum]['indent'] - $diff); + $phpcsFile->fixer->replaceToken( + ($param['commentLines'][$lineNum]['token'] - 1), + str_repeat(' ', $newIndent) + ); + } + + $phpcsFile->fixer->endChangeset(); + }//end if + }//end if + + if ($param['var'] === '') { + continue; + } + + $foundParams[] = $param['var']; + + // Check number of spaces after the type. + $this->checkSpacingAfterParamType($phpcsFile, $param, $maxType); + + // Make sure the param name is correct. + if (isset($realParams[$pos]) === true) { + $realName = $realParams[$pos]['name']; + if ($realName !== $param['var']) { + $code = 'ParamNameNoMatch'; + $data = array( + $param['var'], + $realName, + ); + + $error = 'Doc comment for parameter %s does not match '; + if (strtolower($param['var']) === strtolower($realName)) { + $error .= 'case of '; + $code = 'ParamNameNoCaseMatch'; + } + + $error .= 'actual variable name %s'; + + $phpcsFile->addError($error, $param['tag'], $code, $data); + } + } else if (substr($param['var'], -4) !== ',...') { + // We must have an extra parameter comment. + $error = 'Superfluous parameter comment'; + $phpcsFile->addError($error, $param['tag'], 'ExtraParamComment'); + }//end if + + if ($param['comment'] === '') { + continue; + } + + // Check number of spaces after the var name. + $this->checkSpacingAfterParamName($phpcsFile, $param, $maxVar); + + // Param comments must start with a capital letter and end with the full stop. + if (preg_match('/^(\p{Ll}|\P{L})/u', $param['comment']) === 1) { + $error = 'Parameter comment must start with a capital letter'; + $phpcsFile->addError($error, $param['tag'], 'ParamCommentNotCapital'); + } + + $lastChar = substr($param['comment'], -1); + if ($lastChar !== '.') { + $error = 'Parameter comment must end with a full stop'; + $phpcsFile->addError($error, $param['tag'], 'ParamCommentFullStop'); + } + }//end foreach + + $realNames = array(); + foreach ($realParams as $realParam) { + $realNames[] = $realParam['name']; + } + + // Report missing comments. + $diff = array_diff($realNames, $foundParams); + foreach ($diff as $neededParam) { + $error = 'Doc comment for parameter "%s" missing'; + $data = array($neededParam); +// $phpcsFile->addError($error, $commentStart, 'MissingParamTag', $data); + } + + }//end processParams() + + + /** + * Check the spacing after the type of a parameter. + * + * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned. + * @param array $param The parameter to be checked. + * @param int $maxType The maxlength of the longest parameter type. + * @param int $spacing The number of spaces to add after the type. + * + * @return void + */ + protected function checkSpacingAfterParamType(File $phpcsFile, $param, $maxType, $spacing=1) + { + // Check number of spaces after the type. + $spaces = ($maxType - strlen($param['type']) + $spacing); + if ($param['type_space'] !== $spaces) { + $error = 'Expected %s spaces after parameter type; %s found'; + $data = array( + $spaces, + $param['type_space'], + ); + + $fix = $phpcsFile->addFixableError($error, $param['tag'], 'SpacingAfterParamType', $data); + if ($fix === true) { + $phpcsFile->fixer->beginChangeset(); + + $content = $param['type']; + $content .= str_repeat(' ', $spaces); + $content .= $param['var']; + $content .= str_repeat(' ', $param['var_space']); + $content .= $param['commentLines'][0]['comment']; + $phpcsFile->fixer->replaceToken(($param['tag'] + 2), $content); + + // Fix up the indent of additional comment lines. + foreach ($param['commentLines'] as $lineNum => $line) { + if ($lineNum === 0 + || $param['commentLines'][$lineNum]['indent'] === 0 + ) { + continue; + } + + $diff = ($param['type_space'] - $spaces); + $newIndent = ($param['commentLines'][$lineNum]['indent'] - $diff); + $phpcsFile->fixer->replaceToken( + ($param['commentLines'][$lineNum]['token'] - 1), + str_repeat(' ', $newIndent) + ); + } + + $phpcsFile->fixer->endChangeset(); + }//end if + }//end if + + }//end checkSpacingAfterParamType() + + + /** + * Check the spacing after the name of a parameter. + * + * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned. + * @param array $param The parameter to be checked. + * @param int $maxVar The maxlength of the longest parameter name. + * @param int $spacing The number of spaces to add after the type. + * + * @return void + */ + protected function checkSpacingAfterParamName(File $phpcsFile, $param, $maxVar, $spacing=1) + { + // Check number of spaces after the var name. + $spaces = ($maxVar - strlen($param['var']) + $spacing); + if ($param['var_space'] !== $spaces) { + $error = 'Expected %s spaces after parameter name; %s found'; + $data = array( + $spaces, + $param['var_space'], + ); + + $fix = $phpcsFile->addFixableError($error, $param['tag'], 'SpacingAfterParamName', $data); + if ($fix === true) { + $phpcsFile->fixer->beginChangeset(); + + $content = $param['type']; + $content .= str_repeat(' ', $param['type_space']); + $content .= $param['var']; + $content .= str_repeat(' ', $spaces); + $content .= $param['commentLines'][0]['comment']; + $phpcsFile->fixer->replaceToken(($param['tag'] + 2), $content); + + // Fix up the indent of additional comment lines. + foreach ($param['commentLines'] as $lineNum => $line) { + if ($lineNum === 0 + || $param['commentLines'][$lineNum]['indent'] === 0 + ) { + continue; + } + + $diff = ($param['var_space'] - $spaces); + $newIndent = ($param['commentLines'][$lineNum]['indent'] - $diff); + $phpcsFile->fixer->replaceToken( + ($param['commentLines'][$lineNum]['token'] - 1), + str_repeat(' ', $newIndent) + ); + } + + $phpcsFile->fixer->endChangeset(); + }//end if + }//end if + + }//end checkSpacingAfterParamName() + + +}//end class diff --git a/dev/tests/static/Magento/Sniffs/Commenting/VariableCommentSniff.php b/dev/tests/static/Magento/Sniffs/Commenting/VariableCommentSniff.php new file mode 100644 index 000000000..2a3cb92b2 --- /dev/null +++ b/dev/tests/static/Magento/Sniffs/Commenting/VariableCommentSniff.php @@ -0,0 +1,153 @@ + + * @copyright 2006-2015 Squiz Pty Ltd (ABN 77 084 670 600) + * @license https://github.com/squizlabs/PHP_CodeSniffer/blob/master/licence.txt BSD Licence + */ + +namespace Magento\Sniffs\Commenting; + +use PHP_CodeSniffer\Sniffs\AbstractVariableSniff; +use PHP_CodeSniffer\Files\File; +use PHP_CodeSniffer\Util\Common; + +class VariableCommentSniff extends AbstractVariableSniff +{ + + + /** + * Called to process class member vars. + * + * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned. + * @param int $stackPtr The position of the current token + * in the stack passed in $tokens. + * + * @return void + */ + public function processMemberVar(File $phpcsFile, $stackPtr) + { + $tokens = $phpcsFile->getTokens(); + $ignore = array( + T_PUBLIC, + T_PRIVATE, + T_PROTECTED, + T_VAR, + T_STATIC, + T_WHITESPACE, + ); + + $commentEnd = $phpcsFile->findPrevious($ignore, ($stackPtr - 1), null, true); + if ($commentEnd === false + || ($tokens[$commentEnd]['code'] !== T_DOC_COMMENT_CLOSE_TAG + && $tokens[$commentEnd]['code'] !== T_COMMENT) + ) { + $phpcsFile->addError('Missing member variable doc comment', $stackPtr, 'Missing'); + return; + } + + if ($tokens[$commentEnd]['code'] === T_COMMENT) { + $phpcsFile->addError('You must use "/**" style comments for a member variable comment', $stackPtr, 'WrongStyle'); + return; + } + + $commentStart = $tokens[$commentEnd]['comment_opener']; + + $foundVar = null; + foreach ($tokens[$commentStart]['comment_tags'] as $tag) { + if ($tokens[$tag]['content'] === '@var') { + if ($foundVar !== null) { + $error = 'Only one @var tag is allowed in a member variable comment'; + $phpcsFile->addError($error, $tag, 'DuplicateVar'); + } else { + $foundVar = $tag; + } + } else if ($tokens[$tag]['content'] === '@see') { + // Make sure the tag isn't empty. + $string = $phpcsFile->findNext(T_DOC_COMMENT_STRING, $tag, $commentEnd); + if ($string === false || $tokens[$string]['line'] !== $tokens[$tag]['line']) { + $error = 'Content missing for @see tag in member variable comment'; + $phpcsFile->addError($error, $tag, 'EmptySees'); + } + } else { + $error = '%s tag is not allowed in member variable comment'; + $data = array($tokens[$tag]['content']); + $phpcsFile->addWarning($error, $tag, 'TagNotAllowed', $data); + }//end if + }//end foreach + + // The @var tag is the only one we require. + if ($foundVar === null) { + $error = 'Missing @var tag in member variable comment'; + $phpcsFile->addError($error, $commentEnd, 'MissingVar'); + return; + } + + $firstTag = $tokens[$commentStart]['comment_tags'][0]; + if ($foundVar !== null && $tokens[$firstTag]['content'] !== '@var') { + $error = 'The @var tag must be the first tag in a member variable comment'; + $phpcsFile->addError($error, $foundVar, 'VarOrder'); + } + + // Make sure the tag isn't empty and has the correct padding. + $string = $phpcsFile->findNext(T_DOC_COMMENT_STRING, $foundVar, $commentEnd); + if ($string === false || $tokens[$string]['line'] !== $tokens[$foundVar]['line']) { + $error = 'Content missing for @var tag in member variable comment'; + $phpcsFile->addError($error, $foundVar, 'EmptyVar'); + return; + } + + $varType = $tokens[($foundVar + 2)]['content']; + $suggestedType = Common::suggestType($varType); + if ($varType !== $suggestedType) { + $error = 'Expected "%s" but found "%s" for @var tag in member variable comment'; + $data = array( + $suggestedType, + $varType, + ); + + $fix = $phpcsFile->addFixableError($error, ($foundVar + 2), 'IncorrectVarType', $data); + if ($fix === true) { + $phpcsFile->fixer->replaceToken(($foundVar + 2), $suggestedType); + } + } + + }//end processMemberVar() + + + /** + * Called to process a normal variable. + * + * Not required for this sniff. + * + * @param \PHP_CodeSniffer\Files\File $phpcsFile The PHP_CodeSniffer file where this token was found. + * @param int $stackPtr The position where the double quoted + * string was found. + * + * @return void + */ + protected function processVariable(File $phpcsFile, $stackPtr) + { + + }//end processVariable() + + + /** + * Called to process variables found in double quoted strings. + * + * Not required for this sniff. + * + * @param \PHP_CodeSniffer\Files\File $phpcsFile The PHP_CodeSniffer file where this token was found. + * @param int $stackPtr The position where the double quoted + * string was found. + * + * @return void + */ + protected function processVariableInString(File $phpcsFile, $stackPtr) + { + + }//end processVariableInString() + + +}//end class diff --git a/dev/tests/static/Magento/Sniffs/Files/LineLengthSniff.php b/dev/tests/static/Magento/Sniffs/Files/LineLengthSniff.php index 50020cb29..2abcf0531 100644 --- a/dev/tests/static/Magento/Sniffs/Files/LineLengthSniff.php +++ b/dev/tests/static/Magento/Sniffs/Files/LineLengthSniff.php @@ -5,10 +5,12 @@ */ namespace Magento\Sniffs\Files; +use PHP_CodeSniffer\Standards\Generic\Sniffs\Files\LineLengthSniff as FilesLineLengthSniff; + /** * Line length sniff which ignores long lines in case they contain strings intended for translation. */ -class LineLengthSniff extends \Generic_Sniffs_Files_LineLengthSniff +class LineLengthSniff extends FilesLineLengthSniff { /** * Having previous line content allows to ignore long lines in case of multi-line declaration. @@ -20,7 +22,7 @@ class LineLengthSniff extends \Generic_Sniffs_Files_LineLengthSniff /** * {@inheritdoc} */ - protected function checkLineLength(\PHP_CodeSniffer_File $phpcsFile, $stackPtr, $lineContent) + protected function checkLineLength($phpcsFile, $stackPtr, $lineContent) { $previousLineRegexp = '~__\($|\bPhrase\($~'; $currentLineRegexp = '~__\(.+\)|\bPhrase\(.+\)~'; diff --git a/dev/tests/static/Magento/Sniffs/LiteralNamespaces/LiteralNamespacesSniff.php b/dev/tests/static/Magento/Sniffs/LiteralNamespaces/LiteralNamespacesSniff.php index 31796ec4e..b6af3f37c 100644 --- a/dev/tests/static/Magento/Sniffs/LiteralNamespaces/LiteralNamespacesSniff.php +++ b/dev/tests/static/Magento/Sniffs/LiteralNamespaces/LiteralNamespacesSniff.php @@ -5,13 +5,13 @@ */ namespace Magento\Sniffs\LiteralNamespaces; -use PHP_CodeSniffer_File; -use PHP_CodeSniffer_Sniff; +use PHP_CodeSniffer\Sniffs\Sniff; +use PHP_CodeSniffer\Files\File; /** * Custom phpcs sniff to detect usages of literal class and interface names. */ -class LiteralNamespacesSniff implements PHP_CodeSniffer_Sniff +class LiteralNamespacesSniff implements Sniff { /** * @var string @@ -37,7 +37,7 @@ public function register() /** * @inheritdoc */ - public function process(PHP_CodeSniffer_File $sourceFile, $stackPtr) + public function process(File $sourceFile, $stackPtr) { $tokens = $sourceFile->getTokens(); if ($sourceFile->findPrevious(T_STRING_CONCAT, $stackPtr, $stackPtr - 3) || @@ -47,8 +47,17 @@ public function process(PHP_CodeSniffer_File $sourceFile, $stackPtr) } $content = trim($tokens[$stackPtr]['content'], "\"'"); + // replace double slashes from class name for avoiding problems with class autoload + if (strpos($content, '\\') !== false) { + $content = preg_replace('|\\\{2,}|', '\\', $content); + } + if (preg_match($this->literalNamespacePattern, $content) === 1 && $this->classExists($content)) { - $sourceFile->addError("Use ::class notation instead.", $stackPtr); + $sourceFile->addError( + "Use ::class notation instead.", + $stackPtr, + 'LiteralClassUsage' + ); } } diff --git a/dev/tests/static/Magento/Sniffs/MicroOptimizations/IsNullSniff.php b/dev/tests/static/Magento/Sniffs/MicroOptimizations/IsNullSniff.php index 095025a3d..928fc3a0d 100644 --- a/dev/tests/static/Magento/Sniffs/MicroOptimizations/IsNullSniff.php +++ b/dev/tests/static/Magento/Sniffs/MicroOptimizations/IsNullSniff.php @@ -5,10 +5,10 @@ */ namespace Magento\Sniffs\MicroOptimizations; -use PHP_CodeSniffer_File; -use PHP_CodeSniffer_Sniff; +use PHP_CodeSniffer\Sniffs\Sniff; +use PHP_CodeSniffer\Files\File; -class IsNullSniff implements PHP_CodeSniffer_Sniff +class IsNullSniff implements Sniff { /** * @var string @@ -26,11 +26,15 @@ public function register() /** * @inheritdoc */ - public function process(PHP_CodeSniffer_File $sourceFile, $stackPtr) + public function process(File $sourceFile, $stackPtr) { $tokens = $sourceFile->getTokens(); if ($tokens[$stackPtr]['content'] === $this->blacklist) { - $sourceFile->addError("is_null must be avoided. Use strict comparison instead.", $stackPtr); + $sourceFile->addError( + "is_null must be avoided. Use strict comparison instead.", + $stackPtr, + 'IsNullUsage' + ); } } } diff --git a/dev/tests/static/Magento/Sniffs/NamingConventions/InterfaceNameSniff.php b/dev/tests/static/Magento/Sniffs/NamingConventions/InterfaceNameSniff.php index 2e3e4db2a..1618beb66 100644 --- a/dev/tests/static/Magento/Sniffs/NamingConventions/InterfaceNameSniff.php +++ b/dev/tests/static/Magento/Sniffs/NamingConventions/InterfaceNameSniff.php @@ -5,10 +5,10 @@ */ namespace Magento\Sniffs\NamingConventions; -use PHP_CodeSniffer_File; -use PHP_CodeSniffer_Sniff; +use PHP_CodeSniffer\Sniffs\Sniff; +use PHP_CodeSniffer\Files\File; -class InterfaceNameSniff implements PHP_CodeSniffer_Sniff +class InterfaceNameSniff implements Sniff { const INTERFACE_SUFFIX = 'Interface'; @@ -23,7 +23,7 @@ public function register() /** * {@inheritdoc} */ - public function process(PHP_CodeSniffer_File $sourceFile, $stackPtr) + public function process(File $sourceFile, $stackPtr) { $tokens = $sourceFile->getTokens(); $declarationLine = $tokens[$stackPtr]['line']; @@ -32,7 +32,11 @@ public function process(PHP_CodeSniffer_File $sourceFile, $stackPtr) while ($tokens[$stackPtr]['line'] == $declarationLine) { if ($tokens[$stackPtr]['type'] == 'T_STRING') { if (substr($tokens[$stackPtr]['content'], 0 - $suffixLength) != self::INTERFACE_SUFFIX) { - $sourceFile->addError('Interface should have name that ends with "Interface" suffix.', $stackPtr); + $sourceFile->addError( + 'Interface should have name that ends with "Interface" suffix.', + $stackPtr, + 'WrongInterfaceName' + ); } break; } diff --git a/dev/tests/static/Magento/Sniffs/NamingConventions/ReservedWordsSniff.php b/dev/tests/static/Magento/Sniffs/NamingConventions/ReservedWordsSniff.php index 3d2d979c3..f41c235a6 100644 --- a/dev/tests/static/Magento/Sniffs/NamingConventions/ReservedWordsSniff.php +++ b/dev/tests/static/Magento/Sniffs/NamingConventions/ReservedWordsSniff.php @@ -5,10 +5,10 @@ */ namespace Magento\Sniffs\NamingConventions; -use PHP_CodeSniffer_File; -use PHP_CodeSniffer_Sniff; +use PHP_CodeSniffer\Sniffs\Sniff; +use PHP_CodeSniffer\Files\File; -class ReservedWordsSniff implements PHP_CodeSniffer_Sniff +class ReservedWordsSniff implements Sniff { /** * The following words cannot be used to name a class, interface or trait, @@ -45,11 +45,11 @@ public function register() /** * Check all namespace parts * - * @param PHP_CodeSniffer_File $sourceFile + * @param File $sourceFile * @param int $stackPtr * @return void */ - protected function validateNamespace(PHP_CodeSniffer_File $sourceFile, $stackPtr) + protected function validateNamespace(File $sourceFile, $stackPtr) { $stackPtr += 2; $tokens = $sourceFile->getTokens(); @@ -74,11 +74,11 @@ protected function validateNamespace(PHP_CodeSniffer_File $sourceFile, $stackPtr /** * Check class name not having reserved words * - * @param PHP_CodeSniffer_File $sourceFile + * @param File $sourceFile * @param int $stackPtr * @return void */ - protected function validateClass(PHP_CodeSniffer_File $sourceFile, $stackPtr) + protected function validateClass(File $sourceFile, $stackPtr) { $tokens = $sourceFile->getTokens(); $stackPtr += 2; //skip "class" and whitespace @@ -96,7 +96,7 @@ protected function validateClass(PHP_CodeSniffer_File $sourceFile, $stackPtr) /** * {@inheritdoc} */ - public function process(PHP_CodeSniffer_File $sourceFile, $stackPtr) + public function process(File $sourceFile, $stackPtr) { $tokens = $sourceFile->getTokens(); switch ($tokens[$stackPtr]['code']) { diff --git a/dev/tests/static/Magento/Sniffs/Whitespace/EmptyLineMissedSniff.php b/dev/tests/static/Magento/Sniffs/Whitespace/EmptyLineMissedSniff.php index 41782b1e2..de3cfc50b 100644 --- a/dev/tests/static/Magento/Sniffs/Whitespace/EmptyLineMissedSniff.php +++ b/dev/tests/static/Magento/Sniffs/Whitespace/EmptyLineMissedSniff.php @@ -5,13 +5,13 @@ */ namespace Magento\Sniffs\Whitespace; -use PHP_CodeSniffer_File; -use PHP_CodeSniffer_Sniff; +use PHP_CodeSniffer\Sniffs\Sniff; +use PHP_CodeSniffer\Files\File; /** * Class EmptyLineMissedSniff */ -class EmptyLineMissedSniff implements PHP_CodeSniffer_Sniff +class EmptyLineMissedSniff implements Sniff { /** * {@inheritdoc} @@ -24,7 +24,7 @@ public function register() /** * {@inheritdoc} */ - public function process(PHP_CodeSniffer_File $phpcsFile, $stackPtr) + public function process(File $phpcsFile, $stackPtr) { $tokens = $phpcsFile->getTokens(); if ($this->doCheck($phpcsFile, $stackPtr, $tokens)) { @@ -37,12 +37,12 @@ public function process(PHP_CodeSniffer_File $phpcsFile, $stackPtr) } /** - * @param PHP_CodeSniffer_File $phpcsFile + * @param File $phpcsFile * @param int $stackPtr * @param array $tokens * @return bool */ - private function doCheck(PHP_CodeSniffer_File $phpcsFile, $stackPtr, $tokens) + private function doCheck(File $phpcsFile, $stackPtr, $tokens) { $result = false; if ($phpcsFile->hasCondition($stackPtr, T_CLASS) || $phpcsFile->hasCondition($stackPtr, T_INTERFACE)) { diff --git a/dev/tests/static/Magento/Sniffs/Whitespace/MultipleEmptyLinesSniff.php b/dev/tests/static/Magento/Sniffs/Whitespace/MultipleEmptyLinesSniff.php index 794f316de..f276426ef 100644 --- a/dev/tests/static/Magento/Sniffs/Whitespace/MultipleEmptyLinesSniff.php +++ b/dev/tests/static/Magento/Sniffs/Whitespace/MultipleEmptyLinesSniff.php @@ -5,13 +5,13 @@ */ namespace Magento\Sniffs\Whitespace; -use PHP_CodeSniffer_File; -use PHP_CodeSniffer_Sniff; +use PHP_CodeSniffer\Sniffs\Sniff; +use PHP_CodeSniffer\Files\File; /** * Class MultipleEmptyLinesSniff */ -class MultipleEmptyLinesSniff implements PHP_CodeSniffer_Sniff +class MultipleEmptyLinesSniff implements Sniff { /** * {@inheritdoc} @@ -24,7 +24,7 @@ public function register() /** * {@inheritdoc} */ - public function process(PHP_CodeSniffer_File $phpcsFile, $stackPtr) + public function process(File $phpcsFile, $stackPtr) { $tokens = $phpcsFile->getTokens(); if ($phpcsFile->hasCondition($stackPtr, T_FUNCTION) diff --git a/dev/tests/static/Magento/ruleset.xml b/dev/tests/static/Magento/ruleset.xml index 794b5a8ff..98b4d4241 100644 --- a/dev/tests/static/Magento/ruleset.xml +++ b/dev/tests/static/Magento/ruleset.xml @@ -19,6 +19,10 @@ */_files/* + + */dev/tests* + + @@ -26,4 +30,9 @@ + + + + 0 + diff --git a/dev/tests/unit/Magento/FunctionalTestFramework/Config/Reader/FilesystemTest.php b/dev/tests/unit/Magento/FunctionalTestFramework/Config/Reader/FilesystemTest.php new file mode 100644 index 000000000..b59858e7f --- /dev/null +++ b/dev/tests/unit/Magento/FunctionalTestFramework/Config/Reader/FilesystemTest.php @@ -0,0 +1,108 @@ +setMockLoggingUtil(); + } + + /** + * Test Reading Empty Files + * @throws \Exception + */ + public function testEmptyXmlFile() + { + // create mocked items and read the file + $someFile = $this->setMockFile("somepath.xml", ""); + $filesystem = $this->createPseudoFileSystem($someFile); + $filesystem->read(); + + // validate log statement + TestLoggingUtil::getInstance()->validateMockLogStatement( + "warning", + "XML File is empty.", + ["File" => "somepath.xml"] + ); + } + + /** + * Function used to set mock for File created in test + * + * @param string $fileName + * @param string $content + * @return object + * @throws \Exception + */ + public function setMockFile($fileName, $content) + { + $file = AspectMock::double( + File::class, + [ + 'current' => "", + 'count' => 1, + 'getFilename' => $fileName + ] + )->make(); + + //set mocked data property for File + $property = new \ReflectionProperty(File::class, 'data'); + $property->setAccessible(true); + $property->setValue($file, [$fileName => $content]); + + return $file; + } + + /** + * Function used to set mock for filesystem class during test + * + * @param string $fileList + * @return object + * @throws \Exception + */ + public function createPseudoFileSystem($fileList) + { + $filesystem = AspectMock::double(Filesystem::class)->make(); + + //set resolver to use mocked resolver + $mockFileResolver = AspectMock::double(Module::class, ['get' => $fileList])->make(); + $property = new \ReflectionProperty(Filesystem::class, 'fileResolver'); + $property->setAccessible(true); + $property->setValue($filesystem, $mockFileResolver); + + //set validator to use mocked validator + $mockValidation = AspectMock::double(ValidationState::class, ['isValidationRequired' => false])->make(); + $property = new \ReflectionProperty(Filesystem::class, 'validationState'); + $property->setAccessible(true); + $property->setValue($filesystem, $mockValidation); + + return $filesystem; + } + + /** + * After class functionality + * @return void + */ + public static function tearDownAfterClass() + { + TestLoggingUtil::getInstance()->clearMockLoggingUtil(); + parent::tearDownAfterClass(); + } +} diff --git a/dev/tests/unit/Magento/FunctionalTestFramework/DataGenerator/Handlers/CredentialStoreTest.php b/dev/tests/unit/Magento/FunctionalTestFramework/DataGenerator/Handlers/CredentialStoreTest.php new file mode 100644 index 000000000..a451f8dc9 --- /dev/null +++ b/dev/tests/unit/Magento/FunctionalTestFramework/DataGenerator/Handlers/CredentialStoreTest.php @@ -0,0 +1,38 @@ + ["$testKey=$testValue"] + ]); + + $encryptedCred = CredentialStore::getInstance()->getSecret($testKey); + + // assert the value we've gotten is in fact not identical to our test value + $this->assertNotEquals($testValue, $encryptedCred); + + $actualValue = CredentialStore::getInstance()->decryptSecretValue($encryptedCred); + + // assert that we are able to successfully decrypt our secret value + $this->assertEquals($testValue, $actualValue); + } +} diff --git a/dev/tests/unit/Magento/FunctionalTestFramework/DataGenerator/Handlers/DataObjectHandlerTest.php b/dev/tests/unit/Magento/FunctionalTestFramework/DataGenerator/Handlers/DataObjectHandlerTest.php index c09e47478..b77659aa2 100644 --- a/dev/tests/unit/Magento/FunctionalTestFramework/DataGenerator/Handlers/DataObjectHandlerTest.php +++ b/dev/tests/unit/Magento/FunctionalTestFramework/DataGenerator/Handlers/DataObjectHandlerTest.php @@ -12,12 +12,12 @@ use Magento\FunctionalTestingFramework\DataGenerator\Parsers\DataProfileSchemaParser; use Magento\FunctionalTestingFramework\ObjectManager; use Magento\FunctionalTestingFramework\ObjectManagerFactory; -use PHPUnit\Framework\TestCase; +use Magento\FunctionalTestingFramework\Util\MagentoTestCase; /** * Class DataObjectHandlerTest */ -class DataObjectHandlerTest extends TestCase +class DataObjectHandlerTest extends MagentoTestCase { // All tests share this array, feel free to add but be careful modifying or removing const PARSER_OUTPUT = [ diff --git a/dev/tests/unit/Magento/FunctionalTestFramework/DataGenerator/Handlers/OperationDefinitionObjectHandlerTest.php b/dev/tests/unit/Magento/FunctionalTestFramework/DataGenerator/Handlers/OperationDefinitionObjectHandlerTest.php index 3050b3830..b54980314 100644 --- a/dev/tests/unit/Magento/FunctionalTestFramework/DataGenerator/Handlers/OperationDefinitionObjectHandlerTest.php +++ b/dev/tests/unit/Magento/FunctionalTestFramework/DataGenerator/Handlers/OperationDefinitionObjectHandlerTest.php @@ -13,12 +13,12 @@ use Magento\FunctionalTestingFramework\ObjectManagerFactory; use Magento\FunctionalTestingFramework\DataGenerator\Handlers\OperationDefinitionObjectHandler; use Magento\FunctionalTestingFramework\DataGenerator\Parsers\OperationDefinitionParser; -use PHPUnit\Framework\TestCase; +use Magento\FunctionalTestingFramework\Util\MagentoTestCase; /** * Class OperationDefinitionObjectHandlerTest */ -class OperationDefinitionObjectHandlerTest extends TestCase +class OperationDefinitionObjectHandlerTest extends MagentoTestCase { public function testGetMultipleObjects() { @@ -62,7 +62,7 @@ public function testGetMultipleObjects() OperationDefinitionObjectHandler::ENTITY_OPERATION_ENTRY_VALUE => "integer" ], ] - ]]]; + ]]]; $this->setMockParserOutput($mockData); //Perform Assertions @@ -70,7 +70,6 @@ public function testGetMultipleObjects() $operations = $operationDefinitionManager->getAllObjects(); $this->assertArrayHasKey($operationType1 . $dataType1, $operations); $this->assertArrayHasKey($operationType2 . $dataType1, $operations); - } public function testObjectCreation() diff --git a/dev/tests/unit/Magento/FunctionalTestFramework/DataGenerator/Objects/EntityDataObjectTest.php b/dev/tests/unit/Magento/FunctionalTestFramework/DataGenerator/Objects/EntityDataObjectTest.php index 1b611dba6..5b63f0c94 100644 --- a/dev/tests/unit/Magento/FunctionalTestFramework/DataGenerator/Objects/EntityDataObjectTest.php +++ b/dev/tests/unit/Magento/FunctionalTestFramework/DataGenerator/Objects/EntityDataObjectTest.php @@ -6,7 +6,9 @@ namespace Magento\FunctionalTestingFramework\DataGenerator\Objects; -use PHPUnit\Framework\TestCase; +use Magento\FunctionalTestingFramework\Util\MagentoTestCase; +use Magento\FunctionalTestingFramework\Exceptions\TestFrameworkException; +use tests\unit\Util\TestLoggingUtil; /** * The following function declarations override the global function_exists and declare msq/msqs for use @@ -32,8 +34,17 @@ function msqs($id = null) /** * Class EntityDataObjectTest */ -class EntityDataObjectTest extends TestCase +class EntityDataObjectTest extends MagentoTestCase { + /** + * Before test functionality + * @return void + */ + public function setUp() + { + TestLoggingUtil::getInstance()->setMockLoggingUtil(); + } + public function testBasicGetters() { $data = ["datakey1" => "value1"]; @@ -77,12 +88,11 @@ public function testVarGetter() $dataObject = new EntityDataObject("name", "type", $data, null, null, $vars); // Perform Asserts $this->assertEquals("id", $dataObject->getVarReference("someOtherEntity")); - } public function testGetDataByNameInvalidUniquenessFormatValue() { - $this->expectException("Magento\FunctionalTestingFramework\Exceptions\TestFrameworkException"); + $this->expectException(TestFrameworkException::class); $data = ["datakey1" => "value1", "datakey2" => "value2", "datakey3" => "value3"]; $dataObject = new EntityDataObject("name", "type", $data, null, null, null); // Trigger Exception @@ -92,7 +102,7 @@ public function testGetDataByNameInvalidUniquenessFormatValue() public function testUniquenessFunctionsDontExist() { $this->markTestIncomplete('Test fails, as msqMock is always declared in test runs.'); - $this->expectException("Magento\FunctionalTestingFramework\Exceptions\TestFrameworkException"); + $this->expectException(TestFrameworkException::class); $data = ["datakey1" => "value1", "datakey2" => "value2", "datakey3" => "value3"]; $uniquenessKeys = ["datakey1" => "suffix"]; $dataObject = new EntityDataObject("name", "type", $data, null, $uniquenessKeys, null); @@ -109,4 +119,13 @@ public function testGetLinkedEntities() $this->assertEquals("linkedEntity1", $dataObject->getLinkedEntitiesOfType("linkedEntityType")[0]); $this->assertEquals("linkedEntity2", $dataObject->getLinkedEntitiesOfType("otherEntityType")[0]); } + + /** + * After class functionality + * @return void + */ + public static function tearDownAfterClass() + { + TestLoggingUtil::getInstance()->clearMockLoggingUtil(); + } } diff --git a/dev/tests/unit/Magento/FunctionalTestFramework/DataGenerator/Persist/OperationDataArrayResolverTest.php b/dev/tests/unit/Magento/FunctionalTestFramework/DataGenerator/Persist/OperationDataArrayResolverTest.php index 4b57a2f73..9e425e020 100644 --- a/dev/tests/unit/Magento/FunctionalTestFramework/DataGenerator/Persist/OperationDataArrayResolverTest.php +++ b/dev/tests/unit/Magento/FunctionalTestFramework/DataGenerator/Persist/OperationDataArrayResolverTest.php @@ -9,12 +9,13 @@ use Magento\FunctionalTestingFramework\DataGenerator\Handlers\DataObjectHandler; use Magento\FunctionalTestingFramework\DataGenerator\Handlers\OperationDefinitionObjectHandler; use Magento\FunctionalTestingFramework\DataGenerator\Persist\OperationDataArrayResolver; -use PHPUnit\Framework\TestCase; +use Magento\FunctionalTestingFramework\Util\MagentoTestCase; use tests\unit\Util\EntityDataObjectBuilder; use tests\unit\Util\OperationDefinitionBuilder; use tests\unit\Util\OperationElementBuilder; +use tests\unit\Util\TestLoggingUtil; -class OperationDataArrayResolverTest extends TestCase +class OperationDataArrayResolverTest extends MagentoTestCase { const NESTED_METADATA_EXPECTED_RESULT = ["parentType" => [ "name" => "Hopper", @@ -35,6 +36,15 @@ class OperationDataArrayResolverTest extends TestCase ] ]]; + /** + * Before test functionality + * @return void + */ + public function setUp() + { + TestLoggingUtil::getInstance()->setMockLoggingUtil(); + } + /** * Test a basic metadata resolve between primitive values and a primitive data set * @@ -344,4 +354,13 @@ public function testNestedMetadataArrayOfValue() // Do assert on result here $this->assertEquals(self::NESTED_METADATA_ARRAY_RESULT, $result); } + + /** + * After class functionality + * @return void + */ + public static function tearDownAfterClass() + { + TestLoggingUtil::getInstance()->clearMockLoggingUtil(); + } } diff --git a/dev/tests/unit/Magento/FunctionalTestFramework/DataGenerator/Util/DataExtensionUtilTest.php b/dev/tests/unit/Magento/FunctionalTestFramework/DataGenerator/Util/DataExtensionUtilTest.php new file mode 100644 index 000000000..e72c15b31 --- /dev/null +++ b/dev/tests/unit/Magento/FunctionalTestFramework/DataGenerator/Util/DataExtensionUtilTest.php @@ -0,0 +1,155 @@ + [ + 'extended' => [ + 'type' => 'testType', + 'extends' => "parent", + 'data' => [ + 0 => [ + 'key' => 'testKey', + 'value' => 'testValue' + ] + ] + ] + ] + ]; + + $this->setMockEntities($extendedDataObject); + + $this->expectExceptionMessage("Parent Entity parent not defined for Entity extended."); + DataObjectHandler::getInstance()->getObject("extended"); + } + + public function testAlreadyExtendedParentData() + { + $extendedDataObjects = [ + 'entity' => [ + 'extended' => [ + 'type' => 'testType', + 'extends' => "parent" + ], + 'parent' => [ + 'type' => 'type', + 'extends' => "grandparent" + ], + 'grandparent' => [ + 'type' => 'grand' + ] + ] + ]; + + $this->setMockEntities($extendedDataObjects); + + $this->expectExceptionMessage( + "Cannot extend an entity that already extends another entity. Entity: parent." . PHP_EOL + ); + DataObjectHandler::getInstance()->getObject("extended"); + } + + public function testExtendedVarGetter() + { + $extendedDataObjects = [ + 'entity' => [ + 'extended' => [ + 'type' => 'testType', + 'extends' => "parent" + ], + 'parent' => [ + 'type' => 'type', + 'var' => [ + 'someOtherEntity' => [ + 'entityType' => 'someOtherEntity', + 'entityKey' => 'id', + 'key' => 'someOtherEntity' + ] + ] + ] + ] + ]; + + $this->setMockEntities($extendedDataObjects); + $resultextendedDataObject = DataObjectHandler::getInstance()->getObject("extended"); + // Perform Asserts + $this->assertEquals("someOtherEntity->id", $resultextendedDataObject->getVarReference("someOtherEntity")); + } + + public function testGetLinkedEntities() + { + $extendedDataObjects = [ + 'entity' => [ + 'extended' => [ + 'type' => 'testType', + 'extends' => "parent" + ], + 'parent' => [ + 'type' => 'type', + 'requiredEntity' => [ + 'linkedEntity1' => [ + 'type' => 'linkedEntityType', + 'value' => 'linkedEntity1' + ], + 'linkedEntity2' => [ + 'type' => 'otherEntityType', + 'value' => 'linkedEntity2' + ], + ] + ] + ] + ]; + + $this->setMockEntities($extendedDataObjects); + // Perform Asserts + $resultextendedDataObject = DataObjectHandler::getInstance()->getObject("extended"); + $this->assertEquals("linkedEntity1", $resultextendedDataObject->getLinkedEntitiesOfType("linkedEntityType")[0]); + $this->assertEquals("linkedEntity2", $resultextendedDataObject->getLinkedEntitiesOfType("otherEntityType")[0]); + } + + private function setMockEntities($mockEntityData) + { + $property = new \ReflectionProperty(DataObjectHandler::class, 'INSTANCE'); + $property->setAccessible(true); + $property->setValue(null); + + $mockDataProfileSchemaParser = AspectMock::double(DataProfileSchemaParser::class, [ + 'readDataProfiles' => $mockEntityData + ])->make(); + + $mockObjectManager = AspectMock::double(ObjectManager::class, [ + 'create' => $mockDataProfileSchemaParser + ])->make(); + + AspectMock::double(ObjectManagerFactory::class, [ + 'getObjectManager' => $mockObjectManager + ]); + } +} diff --git a/dev/tests/unit/Magento/FunctionalTestFramework/Page/Handlers/PageObjectHandlerTest.php b/dev/tests/unit/Magento/FunctionalTestFramework/Page/Handlers/PageObjectHandlerTest.php index 62e87022d..28ffcd9e1 100644 --- a/dev/tests/unit/Magento/FunctionalTestFramework/Page/Handlers/PageObjectHandlerTest.php +++ b/dev/tests/unit/Magento/FunctionalTestFramework/Page/Handlers/PageObjectHandlerTest.php @@ -11,9 +11,9 @@ use Magento\FunctionalTestingFramework\ObjectManagerFactory; use Magento\FunctionalTestingFramework\Page\Handlers\PageObjectHandler; use Magento\FunctionalTestingFramework\XmlParser\PageParser; -use PHPUnit\Framework\TestCase; +use Magento\FunctionalTestingFramework\Util\MagentoTestCase; -class PageObjectHandlerTest extends TestCase +class PageObjectHandlerTest extends MagentoTestCase { public function testGetPageObject() { diff --git a/dev/tests/unit/Magento/FunctionalTestFramework/Page/Handlers/SectionObjectHandlerTest.php b/dev/tests/unit/Magento/FunctionalTestFramework/Page/Handlers/SectionObjectHandlerTest.php index 2f79b3680..4a939dbad 100644 --- a/dev/tests/unit/Magento/FunctionalTestFramework/Page/Handlers/SectionObjectHandlerTest.php +++ b/dev/tests/unit/Magento/FunctionalTestFramework/Page/Handlers/SectionObjectHandlerTest.php @@ -11,9 +11,9 @@ use Magento\FunctionalTestingFramework\ObjectManagerFactory; use Magento\FunctionalTestingFramework\Page\Handlers\SectionObjectHandler; use Magento\FunctionalTestingFramework\XmlParser\SectionParser; -use PHPUnit\Framework\TestCase; +use Magento\FunctionalTestingFramework\Util\MagentoTestCase; -class SectionObjectHandlerTest extends TestCase +class SectionObjectHandlerTest extends MagentoTestCase { public function testGetSectionObject() { diff --git a/dev/tests/unit/Magento/FunctionalTestFramework/Page/Objects/ElementObjectTest.php b/dev/tests/unit/Magento/FunctionalTestFramework/Page/Objects/ElementObjectTest.php index 00d5e9479..d585f4085 100644 --- a/dev/tests/unit/Magento/FunctionalTestFramework/Page/Objects/ElementObjectTest.php +++ b/dev/tests/unit/Magento/FunctionalTestFramework/Page/Objects/ElementObjectTest.php @@ -8,12 +8,12 @@ use Magento\FunctionalTestingFramework\Exceptions\XmlException; use Magento\FunctionalTestingFramework\Page\Objects\ElementObject; -use PHPUnit\Framework\TestCase; +use Magento\FunctionalTestingFramework\Util\MagentoTestCase; /** * Class ElementObjectTest */ -class ElementObjectTest extends TestCase +class ElementObjectTest extends MagentoTestCase { /** * Timeout should be null when instantiated with '-' diff --git a/dev/tests/unit/Magento/FunctionalTestFramework/Page/Objects/PageObjectTest.php b/dev/tests/unit/Magento/FunctionalTestFramework/Page/Objects/PageObjectTest.php index 32973217a..7f8053e77 100644 --- a/dev/tests/unit/Magento/FunctionalTestFramework/Page/Objects/PageObjectTest.php +++ b/dev/tests/unit/Magento/FunctionalTestFramework/Page/Objects/PageObjectTest.php @@ -7,12 +7,12 @@ namespace tests\unit\Magento\FunctionalTestFramework\Page\Objects; use Magento\FunctionalTestingFramework\Page\Objects\PageObject; -use PHPUnit\Framework\TestCase; +use Magento\FunctionalTestingFramework\Util\MagentoTestCase; /** * Class PageObjectTest */ -class PageObjectTest extends TestCase +class PageObjectTest extends MagentoTestCase { /** * Assert that the page object has a section diff --git a/dev/tests/unit/Magento/FunctionalTestFramework/Page/Objects/SectionObjectTest.php b/dev/tests/unit/Magento/FunctionalTestFramework/Page/Objects/SectionObjectTest.php index 6257ee0db..5ed1f557f 100644 --- a/dev/tests/unit/Magento/FunctionalTestFramework/Page/Objects/SectionObjectTest.php +++ b/dev/tests/unit/Magento/FunctionalTestFramework/Page/Objects/SectionObjectTest.php @@ -8,12 +8,12 @@ use Magento\FunctionalTestingFramework\Page\Objects\ElementObject; use Magento\FunctionalTestingFramework\Page\Objects\SectionObject; -use PHPUnit\Framework\TestCase; +use Magento\FunctionalTestingFramework\Util\MagentoTestCase; /** * Class SectionObjectTest */ -class SectionObjectTest extends TestCase +class SectionObjectTest extends MagentoTestCase { /** * Assert that the section object has an element diff --git a/dev/tests/unit/Magento/FunctionalTestFramework/Suite/Handlers/SuiteObjectHandlerTest.php b/dev/tests/unit/Magento/FunctionalTestFramework/Suite/Handlers/SuiteObjectHandlerTest.php index ede99bf34..3f0d2c5a8 100644 --- a/dev/tests/unit/Magento/FunctionalTestFramework/Suite/Handlers/SuiteObjectHandlerTest.php +++ b/dev/tests/unit/Magento/FunctionalTestFramework/Suite/Handlers/SuiteObjectHandlerTest.php @@ -12,11 +12,11 @@ use Magento\FunctionalTestingFramework\Suite\Parsers\SuiteDataParser; use Magento\FunctionalTestingFramework\Test\Handlers\TestObjectHandler; use Magento\FunctionalTestingFramework\Test\Parsers\TestDataParser; -use PHPUnit\Framework\TestCase; +use Magento\FunctionalTestingFramework\Util\MagentoTestCase; use tests\unit\Util\SuiteDataArrayBuilder; use tests\unit\Util\TestDataArrayBuilder; -class SuiteObjectHandlerTest extends TestCase +class SuiteObjectHandlerTest extends MagentoTestCase { /** * 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 new file mode 100644 index 000000000..7bef700ab --- /dev/null +++ b/dev/tests/unit/Magento/FunctionalTestFramework/Suite/SuiteGeneratorTest.php @@ -0,0 +1,211 @@ + null, + 'appendEntriesToConfig' => null + ]); + } + + /** + * Before test functionality + * @return void + */ + public function setUp() + { + TestLoggingUtil::getInstance()->setMockLoggingUtil(); + } + + /** + * Tests generating a single suite given a set of parsed test data + * @throws \Exception + */ + public function testGenerateSuite() + { + $suiteDataArrayBuilder = new SuiteDataArrayBuilder(); + $mockData = $suiteDataArrayBuilder + ->withName('basicTestSuite') + ->withAfterHook() + ->withBeforeHook() + ->includeTests(['simpleTest']) + ->includeGroups(['group1']) + ->build(); + + $testDataArrayBuilder = new TestDataArrayBuilder(); + $mockSimpleTest = $testDataArrayBuilder + ->withName('simpleTest') + ->withTestActions() + ->build(); + + $mockTestData = ['tests' => array_merge($mockSimpleTest)]; + $this->setMockTestAndSuiteParserOutput($mockTestData, $mockData); + + // parse and generate suite object with mocked data + $mockSuiteGenerator = SuiteGenerator::getInstance(); + $mockSuiteGenerator->generateSuite("basicTestSuite"); + + // assert that expected suite is generated + TestLoggingUtil::getInstance()->validateMockLogStatement( + 'info', + "suite generated", + ['suite' => 'basicTestSuite', 'relative_path' => "_generated" . DIRECTORY_SEPARATOR . "basicTestSuite"] + ); + } + + /** + * Tests generating all suites given a set of parsed test data + * @throws \Exception + */ + public function testGenerateAllSuites() + { + $suiteDataArrayBuilder = new SuiteDataArrayBuilder(); + $mockData = $suiteDataArrayBuilder + ->withName('basicTestSuite') + ->withAfterHook() + ->withBeforeHook() + ->includeTests(['simpleTest']) + ->includeGroups(['group1']) + ->build(); + + $testDataArrayBuilder = new TestDataArrayBuilder(); + $mockSimpleTest = $testDataArrayBuilder + ->withName('simpleTest') + ->withTestActions() + ->build(); + + $mockTestData = ['tests' => array_merge($mockSimpleTest)]; + $this->setMockTestAndSuiteParserOutput($mockTestData, $mockData); + + // parse and retrieve suite object with mocked data + $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"] + ); + } + + /** + * Tests attempting to generate a suite with no included/excluded tests and no hooks + * @throws \Exception + */ + public function testGenerateEmptySuite() + { + $suiteDataArrayBuilder = new SuiteDataArrayBuilder(); + $mockData = $suiteDataArrayBuilder + ->withName('basicTestSuite') + ->build(); + unset($mockData['suites']['basicTestSuite'][TestObjectExtractor::TEST_BEFORE_HOOK]); + unset($mockData['suites']['basicTestSuite'][TestObjectExtractor::TEST_AFTER_HOOK]); + + $mockTestData = null; + $this->setMockTestAndSuiteParserOutput($mockTestData, $mockData); + + // set expected error message + $this->expectExceptionMessage("Suites must not be empty. Suite: \"basicTestSuite\""); + + // parse and generate suite object with mocked data + $mockSuiteGenerator = SuiteGenerator::getInstance(); + $mockSuiteGenerator->generateSuite("basicTestSuite"); + } + + /** + * Function used to set mock for parser return and force init method to run between tests. + * + * @param array $testData + * @throws \Exception + */ + private function setMockTestAndSuiteParserOutput($testData, $suiteData) + { + $property = new \ReflectionProperty(SuiteGenerator::class, 'SUITE_GENERATOR_INSTANCE'); + $property->setAccessible(true); + $property->setValue(null); + + // clear test object handler value to inject parsed content + $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, 'SUITE_OBJECT_HANLDER_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 all tests + */ + public static function tearDownAfterClass() + { + TestLoggingUtil::getInstance()->clearMockLoggingUtil(); + parent::tearDownAfterClass(); + } +} diff --git a/dev/tests/unit/Magento/FunctionalTestFramework/Test/Config/ActionGroupDomTest.php b/dev/tests/unit/Magento/FunctionalTestFramework/Test/Config/ActionGroupDomTest.php index b7ea66f65..8d0bc19ca 100644 --- a/dev/tests/unit/Magento/FunctionalTestFramework/Test/Config/ActionGroupDomTest.php +++ b/dev/tests/unit/Magento/FunctionalTestFramework/Test/Config/ActionGroupDomTest.php @@ -7,9 +7,9 @@ use Magento\FunctionalTestingFramework\Exceptions\Collector\ExceptionCollector; use Magento\FunctionalTestingFramework\Test\Config\ActionGroupDom; -use PHPUnit\Framework\TestCase; +use Magento\FunctionalTestingFramework\Util\MagentoTestCase; -class ActionGroupDomTest extends TestCase +class ActionGroupDomTest extends MagentoTestCase { /** * Test Action Group duplicate step key validation diff --git a/dev/tests/unit/Magento/FunctionalTestFramework/Test/Handlers/TestObjectHandlerTest.php b/dev/tests/unit/Magento/FunctionalTestFramework/Test/Handlers/TestObjectHandlerTest.php index a03fb4bc1..d368d5fa1 100644 --- a/dev/tests/unit/Magento/FunctionalTestFramework/Test/Handlers/TestObjectHandlerTest.php +++ b/dev/tests/unit/Magento/FunctionalTestFramework/Test/Handlers/TestObjectHandlerTest.php @@ -18,10 +18,10 @@ use Magento\FunctionalTestingFramework\Test\Parsers\TestDataParser; use Magento\FunctionalTestingFramework\Test\Util\ActionObjectExtractor; use Magento\FunctionalTestingFramework\Test\Util\TestObjectExtractor; -use PHPUnit\Framework\TestCase; +use Magento\FunctionalTestingFramework\Util\MagentoTestCase; use tests\unit\Util\TestDataArrayBuilder; -class TestObjectHandlerTest extends TestCase +class TestObjectHandlerTest extends MagentoTestCase { /** * Basic test to validate array => test object conversion. @@ -66,12 +66,12 @@ public function testGetTestObject() $expectedBeforeHookObject = new TestHookObject( TestObjectExtractor::TEST_BEFORE_HOOK, $testDataArrayBuilder->testName, - [$expectedBeforeActionObject] + ["testActionBefore" => $expectedBeforeActionObject] ); $expectedAfterHookObject = new TestHookObject( TestObjectExtractor::TEST_AFTER_HOOK, $testDataArrayBuilder->testName, - [$expectedAfterActionObject] + ["testActionAfter" => $expectedAfterActionObject] ); $expectedFailedHookObject = new TestHookObject( TestObjectExtractor::TEST_FAILED_HOOK, @@ -86,8 +86,9 @@ public function testGetTestObject() ); $expectedTestObject = new TestObject( $testDataArrayBuilder->testName, - [$expectedTestActionObject], + ["testActionInTest" => $expectedTestActionObject], [ + 'features' => ['NO MODULE DETECTED'], 'group' => ['test'] ], [ @@ -101,6 +102,15 @@ public function testGetTestObject() $this->assertEquals($expectedTestObject, $actualTestObject); } + /** + * Tests basic getting of a test that has a fileName + */ + public function testGetTestWithFileName() + { + $this->markTestIncomplete(); + //TODO + } + /** * Tests the function used to get a series of relevant tests by group. * @@ -131,6 +141,44 @@ public function testGetTestsByGroup() $this->assertArrayNotHasKey('excludeTest', $tests); } + /** + * Tests the function used to parse and determine a test's Module (used in allure Features annotation) + * + * @throws \Exception + */ + public function testGetTestWithModuleName() + { + // set up Test Data + $moduleExpected = "SomeTestModule"; + $filepath = DIRECTORY_SEPARATOR . + "user" . + "magento2ce" . DIRECTORY_SEPARATOR . + "dev" . DIRECTORY_SEPARATOR . + "tests" . DIRECTORY_SEPARATOR . + "acceptance" . DIRECTORY_SEPARATOR . + "tests" . DIRECTORY_SEPARATOR . + $moduleExpected . DIRECTORY_SEPARATOR . + "Tests" . DIRECTORY_SEPARATOR . + "text.xml"; + // set up mock data + $testDataArrayBuilder = new TestDataArrayBuilder(); + $mockData = $testDataArrayBuilder + ->withAnnotations() + ->withFailedHook() + ->withAfterHook() + ->withBeforeHook() + ->withTestActions() + ->withFileName($filepath) + ->build(); + $this->setMockParserOutput(['tests' => $mockData]); + // Execute Test Method + $toh = TestObjectHandler::getInstance(); + $actualTestObject = $toh->getObject($testDataArrayBuilder->testName); + $moduleName = $actualTestObject->getAnnotations()["features"][0]; + //performAsserts + $this->assertEquals($moduleExpected, $moduleName); + } + /** * Function used to set mock for parser return and force init method to run between tests. * diff --git a/dev/tests/unit/Magento/FunctionalTestFramework/Test/Objects/ActionGroupObjectTest.php b/dev/tests/unit/Magento/FunctionalTestFramework/Test/Objects/ActionGroupObjectTest.php index 8a5ba7100..4844ca684 100644 --- a/dev/tests/unit/Magento/FunctionalTestFramework/Test/Objects/ActionGroupObjectTest.php +++ b/dev/tests/unit/Magento/FunctionalTestFramework/Test/Objects/ActionGroupObjectTest.php @@ -15,14 +15,24 @@ use Magento\FunctionalTestingFramework\Test\Objects\ActionGroupObject; use Magento\FunctionalTestingFramework\Test\Objects\ActionObject; use Magento\FunctionalTestingFramework\Test\Objects\ArgumentObject; -use PHPUnit\Framework\TestCase; +use Magento\FunctionalTestingFramework\Util\MagentoTestCase; use tests\unit\Util\ActionGroupObjectBuilder; use tests\unit\Util\EntityDataObjectBuilder; +use tests\unit\Util\TestLoggingUtil; -class ActionGroupObjectTest extends TestCase +class ActionGroupObjectTest extends MagentoTestCase { const ACTION_GROUP_MERGE_KEY = 'TestKey'; + /** + * Before test functionality + * @return void + */ + public function setUp() + { + TestLoggingUtil::getInstance()->setMockLoggingUtil(); + } + /** * Tests a string literal in an action group */ @@ -284,4 +294,13 @@ private function assertOnMergeKeyAndActionValue($actions, $expectedValue, $expec $this->assertEquals($expectedMergeKey, $action->getStepKey()); $this->assertEquals($expectedValue, $action->getCustomActionAttributes()); } + + /** + * After class functionality + * @return void + */ + public static function tearDownAfterClass() + { + TestLoggingUtil::getInstance()->clearMockLoggingUtil(); + } } diff --git a/dev/tests/unit/Magento/FunctionalTestFramework/Test/Objects/ActionObjectTest.php b/dev/tests/unit/Magento/FunctionalTestFramework/Test/Objects/ActionObjectTest.php index b5aa8c4b1..6163b4d38 100644 --- a/dev/tests/unit/Magento/FunctionalTestFramework/Test/Objects/ActionObjectTest.php +++ b/dev/tests/unit/Magento/FunctionalTestFramework/Test/Objects/ActionObjectTest.php @@ -15,13 +15,24 @@ use Magento\FunctionalTestingFramework\Test\Objects\ActionObject; use Magento\FunctionalTestingFramework\Page\Handlers\SectionObjectHandler; use Magento\FunctionalTestingFramework\Page\Objects\SectionObject; -use PHPUnit\Framework\TestCase; +use Magento\FunctionalTestingFramework\Exceptions\TestReferenceException; +use tests\unit\Util\TestLoggingUtil; +use Magento\FunctionalTestingFramework\Util\MagentoTestCase; /** * Class ActionObjectTest */ -class ActionObjectTest extends TestCase +class ActionObjectTest extends MagentoTestCase { + /** + * Before test functionality + * @return void + */ + public function setUp() + { + TestLoggingUtil::getInstance()->setMockLoggingUtil(); + } + /** * The order offset should be 0 when the action is instantiated with 'before' */ @@ -191,6 +202,8 @@ public function testTimeoutFromElement() /** * {{PageObject.url}} should be replaced with someUrl.html + * + * @throws /Exception */ public function testResolveUrl() { @@ -213,6 +226,42 @@ public function testResolveUrl() $this->assertEquals($expected, $actionObject->getCustomActionAttributes()); } + /** + * {{PageObject}} should not be replaced and should elicit a warning in console + * + * @throws /Exception + */ + public function testResolveUrlWithNoAttribute() + { + // Set up mocks + $actionObject = new ActionObject('merge123', 'amOnPage', [ + 'url' => '{{PageObject}}' + ]); + $pageObject = new PageObject('PageObject', '/replacement/url.html', 'Test', [], false, "test"); + $pageObjectList = ["PageObject" => $pageObject]; + $instance = AspectMock::double( + PageObjectHandler::class, + ['getObject' => $pageObject, 'getAllObjects' => $pageObjectList] + )->make(); // bypass the private constructor + AspectMock::double(PageObjectHandler::class, ['getInstance' => $instance]); + + // Call the method under test + $actionObject->resolveReferences(); + + // Expect this warning to get generated + TestLoggingUtil::getInstance()->validateMockLogStatement( + "warning", + "page url attribute not found and is required", + ['action' => $actionObject->getType(), 'url' => '{{PageObject}}', 'stepKey' => $actionObject->getStepKey()] + ); + + // Verify + $expected = [ + 'url' => '{{PageObject}}' + ]; + $this->assertEquals($expected, $actionObject->getCustomActionAttributes()); + } + /** * {{PageObject.url(param)}} should be replaced */ @@ -290,7 +339,7 @@ public function testResolveArrayData() */ public function testTooFewArgumentException() { - $this->expectException('Magento\FunctionalTestingFramework\Exceptions\TestReferenceException'); + $this->expectException(TestReferenceException::class); $actionObject = new ActionObject('key123', 'fillField', [ 'selector' => "{{SectionObject.elementObject('arg1')}}", @@ -308,7 +357,7 @@ public function testTooFewArgumentException() */ public function testTooManyArgumentException() { - $this->expectException('Magento\FunctionalTestingFramework\Exceptions\TestReferenceException'); + $this->expectException(TestReferenceException::class); $actionObject = new ActionObject('key123', 'fillField', [ 'selector' => "{{SectionObject.elementObject('arg1', 'arg2', 'arg3')}}", @@ -335,4 +384,13 @@ private function mockDataHandlerWithData($dataObject) ->make(); AspectMock::double(DataObjectHandler::class, ['getInstance' => $dataInstance]); } + + /** + * After class functionality + * @return void + */ + public static function tearDownAfterClass() + { + TestLoggingUtil::getInstance()->clearMockLoggingUtil(); + } } diff --git a/dev/tests/unit/Magento/FunctionalTestFramework/Test/Util/ActionGroupObjectExtractorTest.php b/dev/tests/unit/Magento/FunctionalTestFramework/Test/Util/ActionGroupObjectExtractorTest.php new file mode 100644 index 000000000..47b797c1c --- /dev/null +++ b/dev/tests/unit/Magento/FunctionalTestFramework/Test/Util/ActionGroupObjectExtractorTest.php @@ -0,0 +1,70 @@ +testActionGroupObjectExtractor = new ActionGroupObjectExtractor(); + TestLoggingUtil::getInstance()->setMockLoggingUtil(); + } + + /** + * Tests basic action object extraction with an empty stepKey + */ + public function testEmptyStepKey() + { + $this->expectExceptionMessage( + "StepKeys cannot be empty. Action='sampleAction' in Action Group filename.xml" + ); + $this->testActionGroupObjectExtractor->extractActionGroup($this->createBasicActionObjectArray("")); + } + + /** + * Utility function to return mock parser output for testing extraction into ActionObjects. + * + * @param string $stepKey + * @param string $actionGroup + * @param string $filename + * @return array + */ + private function createBasicActionObjectArray( + $stepKey = 'testAction1', + $actionGroup = "actionGroup", + $filename = "filename.xml" + ) { + $baseArray = [ + 'nodeName' => 'actionGroup', + 'name' => $actionGroup, + 'filename' => $filename, + $stepKey => [ + "nodeName" => "sampleAction", + "stepKey" => $stepKey, + "someAttribute" => "someAttributeValue" + ] + ]; + return $baseArray; + } + + /** + * clean up function runs after all tests + */ + public static function tearDownAfterClass() + { + TestLoggingUtil::getInstance()->clearMockLoggingUtil(); + } +} diff --git a/dev/tests/unit/Magento/FunctionalTestFramework/Test/Util/ActionMergeUtilTest.php b/dev/tests/unit/Magento/FunctionalTestFramework/Test/Util/ActionMergeUtilTest.php index bae9965dd..0f367c226 100644 --- a/dev/tests/unit/Magento/FunctionalTestFramework/Test/Util/ActionMergeUtilTest.php +++ b/dev/tests/unit/Magento/FunctionalTestFramework/Test/Util/ActionMergeUtilTest.php @@ -11,11 +11,21 @@ use Magento\FunctionalTestingFramework\Test\Objects\ActionObject; use Magento\FunctionalTestingFramework\Test\Util\ActionMergeUtil; use Magento\FunctionalTestingFramework\Test\Util\ActionObjectExtractor; -use PHPUnit\Framework\TestCase; +use Magento\FunctionalTestingFramework\Util\MagentoTestCase; use tests\unit\Util\DataObjectHandlerReflectionUtil; +use tests\unit\Util\TestLoggingUtil; -class ActionMergeUtilTest extends TestCase +class ActionMergeUtilTest extends MagentoTestCase { + /** + * Before test functionality + * @return void + */ + public function setUp() + { + TestLoggingUtil::getInstance()->setMockLoggingUtil(); + } + /** * Test to validate actions are properly ordered during a merge. * @@ -132,7 +142,7 @@ public function testNoActionException() ActionObject::MERGE_ACTION_ORDER_BEFORE ); - $this->expectException("\Magento\FunctionalTestingFramework\Exceptions\XmlException"); + $this->expectException(\Magento\FunctionalTestingFramework\Exceptions\XmlException::class); $actionMergeUtil = new ActionMergeUtil("actionMergeUtilTest", "TestCase"); $actionMergeUtil->resolveActionSteps($actionObjects); @@ -161,6 +171,14 @@ public function testInsertWait() 0 ); $this->assertEquals($expected, $actual); + } + /** + * After class functionality + * @return void + */ + public static function tearDownAfterClass() + { + TestLoggingUtil::getInstance()->clearMockLoggingUtil(); } } diff --git a/dev/tests/unit/Magento/FunctionalTestFramework/Test/Util/ActionObjectExtractorTest.php b/dev/tests/unit/Magento/FunctionalTestFramework/Test/Util/ActionObjectExtractorTest.php index 873e622f7..f584adea9 100644 --- a/dev/tests/unit/Magento/FunctionalTestFramework/Test/Util/ActionObjectExtractorTest.php +++ b/dev/tests/unit/Magento/FunctionalTestFramework/Test/Util/ActionObjectExtractorTest.php @@ -7,9 +7,10 @@ use Magento\FunctionalTestingFramework\Test\Objects\ActionObject; use Magento\FunctionalTestingFramework\Test\Util\ActionObjectExtractor; -use PHPUnit\Framework\TestCase; +use Magento\FunctionalTestingFramework\Util\MagentoTestCase; +use tests\unit\Util\TestLoggingUtil; -class ActionObjectExtractorTest extends TestCase +class ActionObjectExtractorTest extends MagentoTestCase { /** @var ActionObjectExtractor */ private $testActionObjectExtractor; @@ -20,6 +21,7 @@ class ActionObjectExtractorTest extends TestCase public function setUp() { $this->testActionObjectExtractor = new ActionObjectExtractor(); + TestLoggingUtil::getInstance()->setMockLoggingUtil(); } /** @@ -42,19 +44,27 @@ public function testBasicActionObjectExtration() public function testInvalidMergeOrderReference() { $invalidArray = $this->createBasicActionObjectArray('invalidTestAction1', 'invalidTestAction1'); - - $this->expectException('\Magento\FunctionalTestingFramework\Exceptions\TestReferenceException'); - $expectedExceptionMessage = "Invalid ordering configuration in test TestWithSelfReferencingStepKey with step" . - " key(s):\n\tinvalidTestAction1\n"; - $this->expectExceptionMessage($expectedExceptionMessage); - - $this->testActionObjectExtractor->extractActions($invalidArray, 'TestWithSelfReferencingStepKey'); + $this->expectException(\Magento\FunctionalTestingFramework\Exceptions\TestReferenceException::class); + try { + $this->testActionObjectExtractor->extractActions($invalidArray, 'TestWithSelfReferencingStepKey'); + } catch (\Exception $e) { + TestLoggingUtil::getInstance()->validateMockLogStatmentRegex( + 'error', + '/Line \d*: Invalid ordering configuration in test/', + [ + 'test' => 'TestWithSelfReferencingStepKey', + 'stepKey' => ['invalidTestAction1'] + ] + ); + + throw $e; + } } /** * Validates a warning is printed to the console when multiple actions reference the same actions for merging. */ - public function testAmbiguousMergeOrderRefernece() + public function testAmbiguousMergeOrderReference() { $ambiguousArray = $this->createBasicActionObjectArray('testAction1'); $ambiguousArray = array_merge( @@ -67,12 +77,25 @@ public function testAmbiguousMergeOrderRefernece() $this->createBasicActionObjectArray('testAction3', null, 'testAction1') ); - $outputString = "multiple actions referencing step key testAction1 in test AmbiguousRefTest:\n" . - "\ttestAction2\n" . - "\ttestAction3\n"; - - $this->expectOutputString($outputString); $this->testActionObjectExtractor->extractActions($ambiguousArray, 'AmbiguousRefTest'); + TestLoggingUtil::getInstance()->validateMockLogStatement( + 'warning', + 'multiple actions referencing step key', + [ + 'test' => 'AmbiguousRefTest', + 'stepKey' => 'testAction1', + 'ref' => ['testAction2', 'testAction3'] + ] + ); + } + + /** + * Tests basic action object extraction with an empty stepKey + */ + public function testEmptyStepKey() + { + $this->expectExceptionMessage("StepKeys cannot be empty. Action='sampleAction'"); + $this->testActionObjectExtractor->extractActions($this->createBasicActionObjectArray("")); } /** @@ -103,4 +126,12 @@ private function createBasicActionObjectArray($stepKey = 'testAction1', $before return $baseArray; } + + /** + * clean up function runs after all tests + */ + public static function tearDownAfterClass() + { + TestLoggingUtil::getInstance()->clearMockLoggingUtil(); + } } diff --git a/dev/tests/unit/Magento/FunctionalTestFramework/Test/Util/AnnotationExtractorTest.php b/dev/tests/unit/Magento/FunctionalTestFramework/Test/Util/AnnotationExtractorTest.php new file mode 100644 index 000000000..fcdad2e40 --- /dev/null +++ b/dev/tests/unit/Magento/FunctionalTestFramework/Test/Util/AnnotationExtractorTest.php @@ -0,0 +1,133 @@ +setMockLoggingUtil(); + } + + /** + * Annotation extractor takes in raw array and condenses it to expected format + * + * @throws \Exception + */ + public function testExtractAnnotations() + { + // Test Data + $testAnnotations = [ + "nodeName" => "annotations", + "features" => [ + [ + "nodeName" => "features", + "value" => "TestFeatures" + ] + ], + "stories" => [ + [ + "nodeName" => "stories", + "value" => "TestStories" + ] + ], + "description" => [ + [ + "nodeName" => "description", + "value" => "TestDescription" + ] + ], + "severity" => [ + [ + "nodeName" => "severity", + "value" => "CRITICAL" + ] + ], + "group" => [ + [ + "nodeName" => "group", + "value" => "TestGroup" + ] + ], + ]; + // Perform Test + $extractor = new AnnotationExtractor(); + $returnedAnnotations = $extractor->extractAnnotations($testAnnotations, "testFileName"); + + // Asserts + + $this->assertEquals("TestFeatures", $returnedAnnotations['features'][0]); + $this->assertEquals("TestStories", $returnedAnnotations['stories'][0]); + $this->assertEquals("TestDescription", $returnedAnnotations['description'][0]); + $this->assertEquals("CRITICAL", $returnedAnnotations['severity'][0]); + $this->assertEquals("TestGroup", $returnedAnnotations['group'][0]); + } + + /** + * Annotation extractor should throw warning when required annotations are missing + * + * @throws \Exception + */ + public function testMissingAnnotations() + { + // Test Data, missing title, description, and severity + $testAnnotations = [ + "nodeName" => "annotations", + "features" => [ + [ + "nodeName" => "features", + "value" => "TestFeatures" + ] + ], + "stories" => [ + [ + "nodeName" => "stories", + "value" => "TestStories" + ] + ], + "group" => [ + [ + "nodeName" => "group", + "value" => "TestGroup" + ] + ], + ]; + // Perform Test + $extractor = new AnnotationExtractor(); + $returnedAnnotations = $extractor->extractAnnotations($testAnnotations, "testFileName"); + + // Asserts + TestLoggingUtil::getInstance()->validateMockLogStatement( + 'warning', + 'DEPRECATION: Test testFileName is missing required annotations.', + [ + 'testName' => 'testFileName', + 'missingAnnotations' => "title, description, severity" + ] + ); + } + + /** + * After class functionality + * @return void + */ + public static function tearDownAfterClass() + { + TestLoggingUtil::getInstance()->clearMockLoggingUtil(); + } +} diff --git a/dev/tests/unit/Magento/FunctionalTestFramework/Test/Util/ObjectExtensionUtilTest.php b/dev/tests/unit/Magento/FunctionalTestFramework/Test/Util/ObjectExtensionUtilTest.php new file mode 100644 index 000000000..4b350e5a6 --- /dev/null +++ b/dev/tests/unit/Magento/FunctionalTestFramework/Test/Util/ObjectExtensionUtilTest.php @@ -0,0 +1,379 @@ +setMockLoggingUtil(); + } + + /** + * Tests generating a test that extends another test + * @throws \Exception + */ + public function testGenerateExtendedTest() + { + $mockActions = [ + "mockStep" => ["nodeName" => "mockNode", "stepKey" => "mockStep"] + ]; + + $testDataArrayBuilder = new TestDataArrayBuilder(); + $mockSimpleTest = $testDataArrayBuilder + ->withName('simpleTest') + ->withTestActions($mockActions) + ->build(); + + $mockExtendedTest = $testDataArrayBuilder + ->withName('extendedTest') + ->withTestReference("simpleTest") + ->build(); + + $mockTestData = ['tests' => array_merge($mockSimpleTest, $mockExtendedTest)]; + $this->setMockTestOutput($mockTestData); + + // parse and generate test object with mocked data + $testObject = TestObjectHandler::getInstance()->getObject('extendedTest'); + + // assert log statement is correct + TestLoggingUtil::getInstance()->validateMockLogStatement( + 'debug', + 'extending test', + ['parent' => 'simpleTest', 'test' => 'extendedTest'] + ); + + // assert that expected test is generated + $this->assertEquals($testObject->getParentName(), "simpleTest"); + $this->assertArrayHasKey("mockStep", $testObject->getOrderedActions()); + } + + /** + * Tests generating a test that extends another test + * @throws \Exception + */ + public function testGenerateExtendedWithHooks() + { + $mockBeforeHooks = [ + "beforeHookAction" => ["nodeName" => "mockNodeBefore", "stepKey" => "mockStepBefore"] + ]; + $mockAfterHooks = [ + "afterHookAction" => ["nodeName" => "mockNodeAfter", "stepKey" => "mockStepAfter"] + ]; + + $testDataArrayBuilder = new TestDataArrayBuilder(); + $mockSimpleTest = $testDataArrayBuilder + ->withName('simpleTest') + ->withBeforeHook($mockBeforeHooks) + ->withAfterHook($mockAfterHooks) + ->build(); + + $mockExtendedTest = $testDataArrayBuilder + ->withName('extendedTest') + ->withTestReference("simpleTest") + ->build(); + + $mockTestData = ['tests' => array_merge($mockSimpleTest, $mockExtendedTest)]; + $this->setMockTestOutput($mockTestData); + + // parse and generate test object with mocked data + $testObject = TestObjectHandler::getInstance()->getObject('extendedTest'); + + // assert log statement is correct + TestLoggingUtil::getInstance()->validateMockLogStatement( + 'debug', + 'extending test', + ['parent' => 'simpleTest', 'test' => 'extendedTest'] + ); + + // assert that expected test is generated + $this->assertEquals($testObject->getParentName(), "simpleTest"); + $this->assertArrayHasKey("mockStepBefore", $testObject->getHooks()['before']->getActions()); + $this->assertArrayHasKey("mockStepAfter", $testObject->getHooks()['after']->getActions()); + } + + /** + * Tests generating a test that extends another test + * @throws \Exception + */ + public function testExtendedTestNoParent() + { + $testDataArrayBuilder = new TestDataArrayBuilder(); + $mockExtendedTest = $testDataArrayBuilder + ->withName('extendedTest') + ->withTestReference("simpleTest") + ->build(); + + $mockTestData = ['tests' => array_merge($mockExtendedTest)]; + $this->setMockTestOutput($mockTestData); + + // parse and generate test object with mocked data + TestObjectHandler::getInstance()->getObject('extendedTest'); + + // validate log statement + TestLoggingUtil::getInstance()->validateMockLogStatement( + 'debug', + "parent test not defined. test will be skipped", + ['parent' => 'simpleTest', 'test' => 'extendedTest'] + ); + } + + /** + * Tests generating a test that extends another test + * @throws \Exception + */ + public function testExtendingExtendedTest() + { + $testDataArrayBuilder = new TestDataArrayBuilder(); + $mockParentTest = $testDataArrayBuilder + ->withName('anotherTest') + ->withTestActions() + ->build(); + + $mockSimpleTest = $testDataArrayBuilder + ->withName('simpleTest') + ->withTestActions() + ->withTestReference("anotherTest") + ->build(); + + $mockExtendedTest = $testDataArrayBuilder + ->withName('extendedTest') + ->withTestReference("simpleTest") + ->build(); + + $mockTestData = ['tests' => array_merge($mockParentTest, $mockSimpleTest, $mockExtendedTest)]; + $this->setMockTestOutput($mockTestData); + + $this->expectExceptionMessage("Cannot extend a test that already extends another test. Test: simpleTest"); + + // parse and generate test object with mocked data + TestObjectHandler::getInstance()->getObject('extendedTest'); + + // validate log statement + TestLoggingUtil::getInstance()->validateMockLogStatement( + 'debug', + "parent test not defined. test will be skipped", + ['parent' => 'simpleTest', 'test' => 'extendedTest'] + ); + $this->expectOutputString("Extending Test: anotherTest => simpleTest" . PHP_EOL); + } + + /** + * Tests generating an action group that extends another action group + * @throws \Exception + */ + public function testGenerateExtendedActionGroup() + { + $mockSimpleActionGroup = [ + "nodeName" => "actionGroup", + "name" => "mockSimpleActionGroup", + "filename" => "someFile", + "commentHere" => [ + "nodeName" => "comment", + "selector" => "selector", + "stepKey" => "commentHere" + ], + "parentComment" => [ + "nodeName" => "comment", + "selector" => "parentSelector", + "stepKey" => "parentComment" + ], + ]; + + $mockExtendedActionGroup = [ + "nodeName" => "actionGroup", + "name" => "mockExtendedActionGroup", + "filename" => "someFile", + "extends" => "mockSimpleActionGroup", + "commentHere" => [ + "nodeName" => "comment", + "selector" => "otherSelector", + "stepKey" => "commentHere" + ], + ]; + + $mockActionGroupData = [ + 'actionGroups' => [ + 'mockSimpleActionGroup' => $mockSimpleActionGroup, + 'mockExtendedActionGroup' => $mockExtendedActionGroup + ] + ]; + $this->setMockTestOutput(null, $mockActionGroupData); + + // parse and generate test object with mocked data + $actionGroupObject = ActionGroupObjectHandler::getInstance()->getObject('mockExtendedActionGroup'); + + // validate log statement + TestLoggingUtil::getInstance()->validateMockLogStatement( + 'debug', + 'extending action group:', + ['parent' => $mockSimpleActionGroup['name'], 'actionGroup' => $mockExtendedActionGroup['name']] + ); + + // assert that expected test is generated + $this->assertEquals("mockSimpleActionGroup", $actionGroupObject->getParentName()); + $actions = $actionGroupObject->getActions(); + $this->assertEquals("otherSelector", $actions["commentHere"]->getCustomActionAttributes()["selector"]); + $this->assertEquals("parentSelector", $actions["parentComment"]->getCustomActionAttributes()["selector"]); + } + + /** + * Tests generating an action group that extends an action group that does not exist + * @throws \Exception + */ + public function testGenerateExtendedActionGroupNoParent() + { + $mockExtendedActionGroup = [ + "nodeName" => "actionGroup", + "name" => "mockSimpleActionGroup", + "filename" => "someFile", + "extends" => "mockSimpleActionGroup", + "commentHere" => [ + "nodeName" => "comment", + "selector" => "otherSelector", + "stepKey" => "commentHere" + ], + ]; + + $mockActionGroupData = [ + 'actionGroups' => [ + 'mockExtendedActionGroup' => $mockExtendedActionGroup + ] + ]; + $this->setMockTestOutput(null, $mockActionGroupData); + + $this->expectExceptionMessage( + "Parent Action Group mockSimpleActionGroup not defined for Test " . $mockExtendedActionGroup['extends'] + ); + + // parse and generate test object with mocked data + ActionGroupObjectHandler::getInstance()->getObject('mockExtendedActionGroup'); + } + + /** + * Tests generating an action group that extends another action group that is already extended + * @throws \Exception + */ + public function testExtendingExtendedActionGroup() + { + $mockParentActionGroup = [ + "nodeName" => "actionGroup", + "name" => "mockParentActionGroup", + "filename" => "someFile" + ]; + + $mockSimpleActionGroup = [ + "nodeName" => "actionGroup", + "name" => "mockSimpleActionGroup", + "filename" => "someFile", + "extends" => "mockParentActionGroup", + ]; + + $mockExtendedActionGroup = [ + "nodeName" => "actionGroup", + "name" => "mockSimpleActionGroup", + "filename" => "someFile", + "extends" => "mockSimpleActionGroup", + ]; + + $mockActionGroupData = [ + 'actionGroups' => [ + 'mockParentActionGroup' => $mockParentActionGroup, + 'mockSimpleActionGroup' => $mockSimpleActionGroup, + 'mockExtendedActionGroup' => $mockExtendedActionGroup + ] + ]; + $this->setMockTestOutput(null, $mockActionGroupData); + + $this->expectExceptionMessage( + "Cannot extend an action group that already extends another action group. " . $mockSimpleActionGroup['name'] + ); + + // parse and generate test object with mocked data + try { + ActionGroupObjectHandler::getInstance()->getObject('mockExtendedActionGroup'); + } catch (\Exception $e) { + // validate log statement + TestLoggingUtil::getInstance()->validateMockLogStatement( + 'error', + "Cannot extend an action group that already extends another action group. " . + $mockSimpleActionGroup['name'], + ['parent' => $mockSimpleActionGroup['name'], 'actionGroup' => $mockSimpleActionGroup['name']] + ); + + throw $e; + } + } + + /** + * Function used to set mock for parser return and force init method to run between tests. + * + * @param array $testData + * @throws \Exception + */ + private function setMockTestOutput($testData = null, $actionGroupData = null) + { + // clear test object handler value to inject parsed content + $property = new \ReflectionProperty(TestObjectHandler::class, 'testObjectHandler'); + $property->setAccessible(true); + $property->setValue(null); + + // clear test object handler value to inject parsed content + $property = new \ReflectionProperty(ActionGroupObjectHandler::class, 'ACTION_GROUP_OBJECT_HANDLER'); + $property->setAccessible(true); + $property->setValue(null); + + $mockDataParser = AspectMock::double(TestDataParser::class, ['readTestData' => $testData])->make(); + $mockActionGroupParser = AspectMock::double( + ActionGroupDataParser::class, + ['readActionGroupData' => $actionGroupData] + )->make(); + $instance = AspectMock::double( + ObjectManager::class, + ['create' => function ($clazz) use ( + $mockDataParser, + $mockActionGroupParser + ) { + if ($clazz == TestDataParser::class) { + return $mockDataParser; + } + if ($clazz == ActionGroupDataParser::class) { + return $mockActionGroupParser; + } + }] + )->make(); + // bypass the private constructor + AspectMock::double(ObjectManagerFactory::class, ['getObjectManager' => $instance]); + } + + /** + * After class functionality + * @return void + */ + public static function tearDownAfterClass() + { + TestLoggingUtil::getInstance()->clearMockLoggingUtil(); + } +} diff --git a/dev/tests/unit/Magento/FunctionalTestFramework/Util/ModulePathExtractorTest.php b/dev/tests/unit/Magento/FunctionalTestFramework/Util/ModulePathExtractorTest.php new file mode 100644 index 000000000..5efa6384b --- /dev/null +++ b/dev/tests/unit/Magento/FunctionalTestFramework/Util/ModulePathExtractorTest.php @@ -0,0 +1,109 @@ +assertEquals( + '[Analytics]', + $modulePathExtractor->extractModuleName( + self::MAGENTO_PATH + ) + ); + } + + /** + * Validate correct module is returned for extension path + * @throws \Exception + */ + public function testGetExtensionModule() + { + $modulePathExtractor = new ModulePathExtractor(); + $this->assertEquals( + '[Analytics]', + $modulePathExtractor->extractModuleName( + self::EXTENSION_PATH + ) + ); + } + + /** + * Validate Magento is returned for dev/tests/acceptance + * @throws \Exception + */ + public function testMagentoModulePath() + { + $modulePathExtractor = new ModulePathExtractor(); + $this->assertEquals( + 'Magento', + $modulePathExtractor->getExtensionPath( + self::MAGENTO_PATH + ) + ); + } + + /** + * Validate correct extension path is returned + * @throws \Exception + */ + public function testExtensionModulePath() + { + $modulePathExtractor = new ModulePathExtractor(); + $this->assertEquals( + 'TestExtension', + $modulePathExtractor->getExtensionPath( + self::EXTENSION_PATH + ) + ); + } +} diff --git a/dev/tests/unit/Magento/FunctionalTestFramework/Util/ModuleResolverTest.php b/dev/tests/unit/Magento/FunctionalTestFramework/Util/ModuleResolverTest.php new file mode 100644 index 000000000..15aa4ad9d --- /dev/null +++ b/dev/tests/unit/Magento/FunctionalTestFramework/Util/ModuleResolverTest.php @@ -0,0 +1,362 @@ +setMockLoggingUtil(); + } + + /** + * After class functionality + * @return void + */ + public static function tearDownAfterClass() + { + TestLoggingUtil::getInstance()->clearMockLoggingUtil(); + } + + /** + * Validate that Paths that are already set are returned + * @throws \Exception + */ + public function testGetModulePathsAlreadySet() + { + $this->setMockResolverClass(); + $resolver = ModuleResolver::getInstance(); + $this->setMockResolverProperties($resolver, ["example" . DIRECTORY_SEPARATOR . "paths"]); + $this->assertEquals(["example" . DIRECTORY_SEPARATOR . "paths"], $resolver->getModulesPath()); + } + + /** + * Validate paths are aggregated correctly + * @throws \Exception + */ + public function testGetModulePathsAggregate() + { + $this->mockForceGenerate(false); + $this->setMockResolverClass(false, null, null, null, ["example" => "example" . DIRECTORY_SEPARATOR . "paths"]); + $resolver = ModuleResolver::getInstance(); + $this->setMockResolverProperties($resolver, null, [0 => "Magento_example"]); + $this->assertEquals( + [ + "example" . DIRECTORY_SEPARATOR . "paths", + "example" . DIRECTORY_SEPARATOR . "paths", + "example" . DIRECTORY_SEPARATOR . "paths" + ], + $resolver->getModulesPath() + ); + } + + /** + * Validate correct path locations are fed into globRelevantPaths + * @throws \Exception + */ + public function testGetModulePathsLocations() + { + $this->mockForceGenerate(false); + $mockResolver = $this->setMockResolverClass( + true, + [0 => "magento_example"], + null, + null, + ["example" => "example" . DIRECTORY_SEPARATOR . "paths"] + ); + $resolver = ModuleResolver::getInstance(); + $this->setMockResolverProperties($resolver, null, null); + $this->assertEquals( + [ + "example" . DIRECTORY_SEPARATOR . "paths", + "example" . DIRECTORY_SEPARATOR . "paths", + "example" . DIRECTORY_SEPARATOR . "paths" + ], + $resolver->getModulesPath() + ); + + // Define the Module paths from app/code + $appCodePath = MAGENTO_BP + . DIRECTORY_SEPARATOR + . 'app' . DIRECTORY_SEPARATOR + . 'code' . DIRECTORY_SEPARATOR; + + // Define the Module paths from default TESTS_MODULE_PATH + $modulePath = defined('TESTS_MODULE_PATH') ? TESTS_MODULE_PATH : TESTS_BP; + + // Define the Module paths from vendor modules + $vendorCodePath = PROJECT_ROOT + . DIRECTORY_SEPARATOR + . 'vendor' . DIRECTORY_SEPARATOR; + + $mockResolver->verifyInvoked('globRelevantPaths', [$modulePath, '']); + $mockResolver->verifyInvoked( + 'globRelevantPaths', + [$appCodePath, DIRECTORY_SEPARATOR . 'Test' . DIRECTORY_SEPARATOR .'Mftf'] + ); + $mockResolver->verifyInvoked( + 'globRelevantPaths', + [$vendorCodePath, DIRECTORY_SEPARATOR . 'Test' . DIRECTORY_SEPARATOR .'Mftf'] + ); + } + + /** + * Validate custom modules are added + * @throws \Exception + */ + public function testGetCustomModulePath() + { + $this->setMockResolverClass(false, ["Magento_TestModule"], null, null, [], ['otherPath']); + $resolver = ModuleResolver::getInstance(); + $this->setMockResolverProperties($resolver, null, null, null); + $this->assertEquals(['otherPath'], $resolver->getModulesPath()); + TestLoggingUtil::getInstance()->validateMockLogStatement( + 'info', + 'including custom module', + ['module' => 'otherPath'] + ); + } + + /** + * Validate blacklisted modules are removed + * @throws \Exception + */ + public function testGetModulePathsBlacklist() + { + $this->setMockResolverClass( + false, + null, + null, + null, + function ($arg1, $arg2) { + if ($arg2 === "") { + $mockValue = ["somePath" => "somePath"]; + } elseif (strpos($arg1, "app")) { + $mockValue = ["otherPath" => "otherPath"]; + } else { + $mockValue = ["lastPath" => "lastPath"]; + } + return $mockValue; + } + ); + $resolver = ModuleResolver::getInstance(); + $this->setMockResolverProperties($resolver, null, null, ["somePath"]); + $this->assertEquals(["otherPath", "lastPath"], $resolver->getModulesPath()); + TestLoggingUtil::getInstance()->validateMockLogStatement( + 'info', + 'excluding module', + ['module' => 'somePath'] + ); + } + + /** + * Validate that getEnabledModules errors out when no Admin Token is returned and --force is false + * @throws \Exception + */ + public function testGetModulePathsNoAdminToken() + { + // Set --force to false + $this->mockForceGenerate(false); + + // Mock ModuleResolver and $enabledModulesPath + $this->setMockResolverClass(false, null, ["example" . DIRECTORY_SEPARATOR . "paths"], []); + $resolver = ModuleResolver::getInstance(); + $this->setMockResolverProperties($resolver, null, null); + + // Cannot Generate if no --force was passed in and no Admin Token is returned succesfully + $this->expectException(TestFrameworkException::class); + $resolver->getModulesPath(); + } + + /** + * Validates that getAdminToken is not called when --force is enabled + */ + public function testGetAdminTokenNotCalledWhenForce() + { + // Set --force to true + $this->mockForceGenerate(true); + + // Mock ModuleResolver and applyCustomModuleMethods() + $mockResolver = $this->setMockResolverClass(); + $resolver = ModuleResolver::getInstance(); + $this->setMockResolverProperties($resolver, null, null); + $resolver->getModulesPath(); + $mockResolver->verifyNeverInvoked("getAdminToken"); + + // verifyNeverInvoked does not add to assertion count + $this->addToAssertionCount(1); + } + + /** + * Verify the getAdminToken method returns throws an exception if ENV is not fully loaded. + */ + public function testGetAdminTokenWithMissingEnv() + { + // Set --force to true + $this->mockForceGenerate(false); + + // Unset env + unset($_ENV['MAGENTO_ADMIN_USERNAME']); + + // Mock ModuleResolver and applyCustomModuleMethods() + $mockResolver = $this->setMockResolverClass(); + $resolver = ModuleResolver::getInstance(); + + // Expect exception + $this->expectException(TestFrameworkException::class); + $resolver->getModulesPath(); + } + + /** + * Verify the getAdminToken method returns throws an exception if Token was bad. + */ + public function testGetAdminTokenWithBadResponse() + { + // Set --force to true + $this->mockForceGenerate(false); + + // Mock ModuleResolver and applyCustomModuleMethods() + $mockResolver = $this->setMockResolverClass(); + $resolver = ModuleResolver::getInstance(); + + // Expect exception + $this->expectException(TestFrameworkException::class); + $resolver->getModulesPath(); + } + + /** + * Function used to set mock for parser return and force init method to run between tests. + * + * @param string $mockToken + * @param array $mockGetModules + * @param string[] $mockCustomMethods + * @param string[] $mockGlob + * @param string[] $mockRelativePaths + * @param string[] $mockCustomModules + * @throws \Exception + * @return Verifier ModuleResolver double + */ + private function setMockResolverClass( + $mockToken = null, + $mockGetModules = null, + $mockCustomMethods = null, + $mockGlob = null, + $mockRelativePaths = null, + $mockCustomModules = null + ) { + $property = new \ReflectionProperty(ModuleResolver::class, 'instance'); + $property->setAccessible(true); + $property->setValue(null); + + $mockMethods = []; + if (isset($mockToken)) { + $mockMethods['getAdminToken'] = $mockToken; + } + if (isset($mockGetModules)) { + $mockMethods['getEnabledModules'] = $mockGetModules; + } + if (isset($mockCustomMethods)) { + $mockMethods['applyCustomModuleMethods'] = $mockCustomMethods; + } + if (isset($mockGlob)) { + $mockMethods['globRelevantWrapper'] = $mockGlob; + } + if (isset($mockRelativePaths)) { + $mockMethods['globRelevantPaths'] = $mockRelativePaths; + } + if (isset($mockCustomModules)) { + $mockMethods['getCustomModulePaths'] = $mockCustomModules; + } +// $mockMethods['printMagentoVersionInfo'] = null; + + $mockResolver = AspectMock::double( + ModuleResolver::class, + $mockMethods + ); + $instance = AspectMock::double( + ObjectManager::class, + ['create' => $mockResolver->make(), 'get' => null] + )->make(); + // bypass the private constructor + AspectMock::double(ObjectManagerFactory::class, ['getObjectManager' => $instance]); + + return $mockResolver; + } + + /** + * Function used to set mock for Resolver properties + * + * @param ModuleResolver $instance + * @param array $mockPaths + * @param array $mockModules + * @param array $mockBlacklist + * @throws \Exception + */ + private function setMockResolverProperties($instance, $mockPaths = null, $mockModules = null, $mockBlacklist = []) + { + $property = new \ReflectionProperty(ModuleResolver::class, 'enabledModulePaths'); + $property->setAccessible(true); + $property->setValue($instance, $mockPaths); + + $property = new \ReflectionProperty(ModuleResolver::class, 'enabledModules'); + $property->setAccessible(true); + $property->setValue($instance, $mockModules); + + $property = new \ReflectionProperty(ModuleResolver::class, 'moduleBlacklist'); + $property->setAccessible(true); + $property->setValue($instance, $mockBlacklist); + } + + /** + * Mocks MftfApplicationConfig->forceGenerateEnabled() + * @param $forceGenerate + * @throws \Exception + * @return void + */ + private function mockForceGenerate($forceGenerate) + { + $mockConfig = AspectMock::double( + MftfApplicationConfig::class, + ['forceGenerateEnabled' => $forceGenerate] + ); + $instance = AspectMock::double( + ObjectManager::class, + ['create' => $mockConfig->make(), 'get' => null] + )->make(); + AspectMock::double(ObjectManagerFactory::class, ['getObjectManager' => $instance]); + } + + /** + * After method functionality + * @return void + */ + protected function tearDown() + { + // re set env + if (!isset($_ENV['MAGENTO_ADMIN_USERNAME'])) { + $_ENV['MAGENTO_ADMIN_USERNAME'] = "admin"; + } + + AspectMock::clean(); + } +} diff --git a/dev/tests/unit/Magento/FunctionalTestFramework/Util/Sorter/ParallelGroupSorterTest.php b/dev/tests/unit/Magento/FunctionalTestFramework/Util/Sorter/ParallelGroupSorterTest.php index 1acd31522..c4a04d361 100644 --- a/dev/tests/unit/Magento/FunctionalTestFramework/Util/Sorter/ParallelGroupSorterTest.php +++ b/dev/tests/unit/Magento/FunctionalTestFramework/Util/Sorter/ParallelGroupSorterTest.php @@ -11,9 +11,9 @@ use Magento\FunctionalTestingFramework\Test\Handlers\TestObjectHandler; use Magento\FunctionalTestingFramework\Test\Objects\TestObject; use Magento\FunctionalTestingFramework\Util\Sorter\ParallelGroupSorter; -use PHPUnit\Framework\TestCase; +use Magento\FunctionalTestingFramework\Util\MagentoTestCase; -class ParallelGroupSorterTest extends TestCase +class ParallelGroupSorterTest extends MagentoTestCase { /** * Test a basic sort of available tests based on size @@ -36,9 +36,9 @@ public function testBasicTestGroupSort() $expectedResult = [ 1 => ['test2'], 2 => ['test7'], - 3 => ['test6', 'test9'], - 4 => ['test1', 'test4', 'test3'], - 5 => ['test5', 'test10', 'test8'] + 3 => ['test6', 'test4', 'test8'], + 4 => ['test1', 'test9'], + 5 => ['test3', 'test5', 'test10'] ]; $testSorter = new ParallelGroupSorter(); @@ -59,13 +59,16 @@ public function testSortWithSuites() { // mock tests for test object handler. $numberOfCalls = 0; - $mockTest1 = AspectMock::double(TestObject::class, ['getTestActionCount' => function () use (&$numberOfCalls) { - $actionCount = [200, 275]; - $result = $actionCount[$numberOfCalls]; - $numberOfCalls++; - - return $result; - }])->make(); + $mockTest1 = AspectMock::double( + TestObject::class, + ['getEstimatedDuration' => function () use (&$numberOfCalls) { + $actionCount = [300, 275]; + $result = $actionCount[$numberOfCalls]; + $numberOfCalls++; + + return $result; + }] + )->make(); $mockHandler = AspectMock::double( TestObjectHandler::class, @@ -92,17 +95,16 @@ public function testSortWithSuites() // perform sort $testSorter = new ParallelGroupSorter(); - $actualResult = $testSorter->getTestsGroupedBySize($sampleSuiteArray, $sampleTestArray, 200); + $actualResult = $testSorter->getTestsGroupedBySize($sampleSuiteArray, $sampleTestArray, 500); // verify the resulting groups - $this->assertCount(5, $actualResult); + $this->assertCount(4, $actualResult); $expectedResults = [ 1 => ['test3'], - 2 => ['test2'], - 3 => ['mockSuite1_0'], - 4 => ['mockSuite1_1'], - 5 => ['test5', 'test4', 'test1'] + 2 => ['test2','test5', 'test4'], + 3 => ['mockSuite1_0', 'test1'], + 4 => ['mockSuite1_1'] ]; foreach ($actualResult as $groupNum => $group) { diff --git a/dev/tests/unit/Magento/FunctionalTestFramework/Util/Validation/DuplicateNodeValidationUtilTest.php b/dev/tests/unit/Magento/FunctionalTestFramework/Util/Validation/DuplicateNodeValidationUtilTest.php new file mode 100644 index 000000000..ad47b73ba --- /dev/null +++ b/dev/tests/unit/Magento/FunctionalTestFramework/Util/Validation/DuplicateNodeValidationUtilTest.php @@ -0,0 +1,49 @@ + + + + + + + '; + $uniqueIdentifier = "stepKey"; + $filename = "file"; + + // Perform Test + $dom = new \DOMDocument(); + $dom->loadXML($xml); + $testNode = $dom->getElementsByTagName('test')->item(0); + + $exceptionCollector = new ExceptionCollector(); + $validator = new DuplicateNodeValidationUtil($uniqueIdentifier, $exceptionCollector); + $validator->validateChildUniqueness( + $testNode, + $filename, + $uniqueIdentifier, + $exceptionCollector + ); + $this->expectException(\Exception::class); + $exceptionCollector->throwException(); + } +} diff --git a/dev/tests/unit/Magento/FunctionalTestFramework/Util/Validation/NameValidationUtilTest.php b/dev/tests/unit/Magento/FunctionalTestFramework/Util/Validation/NameValidationUtilTest.php index 595033012..28d75b1ad 100644 --- a/dev/tests/unit/Magento/FunctionalTestFramework/Util/Validation/NameValidationUtilTest.php +++ b/dev/tests/unit/Magento/FunctionalTestFramework/Util/Validation/NameValidationUtilTest.php @@ -8,9 +8,9 @@ use Magento\FunctionalTestingFramework\Exceptions\XmlException; use Magento\FunctionalTestingFramework\Util\Validation\NameValidationUtil; -use PHPUnit\Framework\TestCase; +use Magento\FunctionalTestingFramework\Util\MagentoTestCase; -class NameValidationUtilTest extends TestCase +class NameValidationUtilTest extends MagentoTestCase { /** * Validate name with curly braces throws exception diff --git a/dev/tests/unit/Util/ActionGroupObjectBuilder.php b/dev/tests/unit/Util/ActionGroupObjectBuilder.php index 4844776d5..f97d0465d 100644 --- a/dev/tests/unit/Util/ActionGroupObjectBuilder.php +++ b/dev/tests/unit/Util/ActionGroupObjectBuilder.php @@ -34,6 +34,13 @@ class ActionGroupObjectBuilder */ private $arguments = []; + /** + * Action Group Object Builder default name + * + * @var string + */ + private $extends = null; + /** * Setter for the Action Group Object name * @@ -70,6 +77,18 @@ public function withActionObjects($actionObjs) return $this; } + /** + * Setter for the Action Group Object extended objects + * + * @param string $extendedActionGroup + * @return ActionGroupObjectBuilder + */ + public function withExtendedAction($extendedActionGroup) + { + $this->extends = $extendedActionGroup; + return $this; + } + /** * ActionGroupObjectBuilder constructor. */ @@ -90,7 +109,8 @@ public function build() return new ActionGroupObject( $this->name, $this->arguments, - $this->actionObjects + $this->actionObjects, + $this->extends ); } } diff --git a/dev/tests/unit/Util/MagentoTestCase.php b/dev/tests/unit/Util/MagentoTestCase.php new file mode 100644 index 000000000..0aa153106 --- /dev/null +++ b/dev/tests/unit/Util/MagentoTestCase.php @@ -0,0 +1,25 @@ + 'boolean' ]; + /** + * Array of nested metadata, merged to main object via addElement() + * + * @var array + */ private $nestedMetadata = []; /** @@ -144,7 +149,7 @@ public function addElements($elementsToAdd) /** * Adds a new set of fields (value => type) into an object parameter to be converted to Operation Elements. * - * @param $fieldsToAdd + * @param array $fieldsToAdd * @return OperationElementBuilder */ public function addFields($fieldsToAdd) diff --git a/dev/tests/unit/Util/TestDataArrayBuilder.php b/dev/tests/unit/Util/TestDataArrayBuilder.php index 2cbc5ccf4..aeeeae850 100644 --- a/dev/tests/unit/Util/TestDataArrayBuilder.php +++ b/dev/tests/unit/Util/TestDataArrayBuilder.php @@ -7,6 +7,7 @@ namespace tests\unit\Util; use Magento\FunctionalTestingFramework\Test\Util\ActionObjectExtractor; +use Magento\FunctionalTestingFramework\Test\Util\AnnotationExtractor; use Magento\FunctionalTestingFramework\Test\Util\TestObjectExtractor; class TestDataArrayBuilder @@ -18,6 +19,13 @@ class TestDataArrayBuilder */ public $testName = 'testTest'; + /** + * Mock file name + * + * @var string + */ + public $filename = null; + /** * Mock before action name * @@ -79,7 +87,12 @@ class TestDataArrayBuilder private $testActions = []; /** - * @param $name + * @var array + */ + private $testReference = null; + + /** + * @param string $name * @return $this */ public function withName($name) @@ -187,6 +200,38 @@ public function withTestActions($actions = null) return $this; } + /** + * Add file name passe in by arg (or default if no arg) + * @param string $filename + * @return $this + */ + public function withFileName($filename = null) + { + if ($filename == null) { + $this->filename = + "/magento2-functional-testing-framework/dev/tests/verification/TestModule/Test/BasicFunctionalTest.xml"; + } else { + $this->filename = $filename; + } + + return $this; + } + + /** + * Add test reference passed in by arg (or default if no arg) + * + * @param string $reference + * @return $this + */ + public function withTestReference($reference = null) + { + if ($reference != null) { + $this->testReference = $reference; + } + + return $this; + } + /** * Output the resulting test data array based on parameters set in the object * @@ -201,7 +246,9 @@ public function build() TestObjectExtractor::TEST_ANNOTATIONS => $this->annotations, TestObjectExtractor::TEST_BEFORE_HOOK => $this->beforeHook, TestObjectExtractor::TEST_AFTER_HOOK => $this->afterHook, - TestObjectExtractor::TEST_FAILED_HOOK => $this->failedHook + TestObjectExtractor::TEST_FAILED_HOOK => $this->failedHook, + "filename" => $this->filename, + "extends" => $this->testReference ], $this->testActions )]; diff --git a/dev/tests/unit/Util/TestLoggingUtil.php b/dev/tests/unit/Util/TestLoggingUtil.php new file mode 100644 index 000000000..655048552 --- /dev/null +++ b/dev/tests/unit/Util/TestLoggingUtil.php @@ -0,0 +1,105 @@ +testLogHandler = new TestHandler(); + $testLogger = new MftfLogger('testLogger'); + $testLogger->pushHandler($this->testLogHandler); + $mockLoggingUtil = AspectMock::double( + LoggingUtil::class, + ['getLogger' => $testLogger] + )->make(); + $property = new \ReflectionProperty(LoggingUtil::class, 'INSTANCE'); + $property->setAccessible(true); + $property->setValue($mockLoggingUtil); + } + + /** + * Function which validates messages have been logged as intended during test execution. + * + * @param string $type + * @param string $message + * @param array $context + * @return void + */ + public function validateMockLogStatement($type, $message, $context) + { + $records = $this->testLogHandler->getRecords(); + $record = $records[count($records)-1]; // we assume the latest record is what requires validation + $this->assertEquals(strtoupper($type), $record['level_name']); + $this->assertEquals($message, $record['message']); + $this->assertEquals($context, $record['context']); + } + + public function validateMockLogStatmentRegex($type, $regex, $context) + { + $records = $this->testLogHandler->getRecords(); + $record = $records[count($records)-1]; // we assume the latest record is what requires validation + $this->assertEquals(strtoupper($type), $record['level_name']); + $this->assertRegExp($regex, $record['message']); + $this->assertEquals($context, $record['context']); + } + + /** + * Function which clears the test logger context from the LogginUtil class. Should be run after a test class has + * executed. + * + * @return void + */ + public function clearMockLoggingUtil() + { + AspectMock::clean(LoggingUtil::class); + } +} diff --git a/dev/tests/util/MftfTestCase.php b/dev/tests/util/MftfTestCase.php index e96c75fde..8bfa1009f 100644 --- a/dev/tests/util/MftfTestCase.php +++ b/dev/tests/util/MftfTestCase.php @@ -5,13 +5,20 @@ */ namespace tests\util; +use Magento\FunctionalTestingFramework\ObjectManager; use Magento\FunctionalTestingFramework\Test\Handlers\TestObjectHandler; use Magento\FunctionalTestingFramework\Util\TestGenerator; use PHPUnit\Framework\TestCase; abstract class MftfTestCase extends TestCase { - const RESOURCES_PATH = __DIR__ . '/../verification/Resources'; + const RESOURCES_PATH = __DIR__ . + DIRECTORY_SEPARATOR . + '..' . + DIRECTORY_SEPARATOR . + 'verification' . + DIRECTORY_SEPARATOR . + 'Resources'; /** * Private function which takes a test name, generates the test and compares with a correspondingly named txt file @@ -37,4 +44,57 @@ public function generateAndCompareTest($testName) $cestFile ); } -} \ No newline at end of file + + /** + * Private function which attempts to generate tests given an invalid shcema of a various type + * + * @param string[] $fileContents + * @param string $objectType + * @param string $expectedError + * @throws \Exception + */ + public function validateSchemaErrorWithTest($fileContents, $objectType ,$expectedError) + { + $this->clearHandler(); + $fullTestModulePath = TESTS_MODULE_PATH . + DIRECTORY_SEPARATOR . + 'TestModule' . + DIRECTORY_SEPARATOR . + $objectType . + DIRECTORY_SEPARATOR; + + foreach ($fileContents as $fileName => $fileContent) { + $tempFile = $fullTestModulePath . $fileName; + $handle = fopen($tempFile, 'w') or die('Cannot open file: ' . $tempFile); + fwrite($handle, $fileContent); + fclose($handle); + } + try { + $this->expectExceptionMessage($expectedError); + TestObjectHandler::getInstance()->getObject("someTest"); + } finally { + foreach (array_keys($fileContents) as $fileName) { + unlink($fullTestModulePath . $fileName); + } + $this->clearHandler(); + } + } + + /** + * Clears test handler and object manager to force recollection of test data + * + * @throws \Exception + */ + private function clearHandler() + { + // clear test object handler to force recollection of test data + $property = new \ReflectionProperty(TestObjectHandler::class, 'testObjectHandler'); + $property->setAccessible(true); + $property->setValue(null); + + // clear test object handler to force recollection of test data + $property = new \ReflectionProperty(ObjectManager::class, 'instance'); + $property->setAccessible(true); + $property->setValue(null); + } +} diff --git a/dev/tests/verification/Resources/ActionGroupContainsStepKeyInArgText.txt b/dev/tests/verification/Resources/ActionGroupContainsStepKeyInArgText.txt new file mode 100644 index 000000000..60b645cfa --- /dev/null +++ b/dev/tests/verification/Resources/ActionGroupContainsStepKeyInArgText.txt @@ -0,0 +1,43 @@ +see("arg1", ".selector"); + } + + /** + * @Features({"TestModule"}) + * @Parameter(name = "AcceptanceTester", value="$I") + * @param AcceptanceTester $I + * @return void + * @throws \Exception + */ + public function ActionGroupContainsStepKeyInArgText(AcceptanceTester $I) + { + $I->see("arg1", ".selector"); + } +} diff --git a/dev/tests/verification/Resources/ActionGroupMergedViaInsertAfter.txt b/dev/tests/verification/Resources/ActionGroupMergedViaInsertAfter.txt new file mode 100644 index 000000000..ddee76f82 --- /dev/null +++ b/dev/tests/verification/Resources/ActionGroupMergedViaInsertAfter.txt @@ -0,0 +1,39 @@ +fillField("#foo", "foo"); + $I->fillField("#bar", "bar"); + $I->click("#foo2"); + $I->click("#bar2"); + $I->click("#baz2"); + $I->fillField("#baz", "baz"); + } +} diff --git a/dev/tests/verification/Resources/ActionGroupMergedViaInsertBefore.txt b/dev/tests/verification/Resources/ActionGroupMergedViaInsertBefore.txt new file mode 100644 index 000000000..4121a6d48 --- /dev/null +++ b/dev/tests/verification/Resources/ActionGroupMergedViaInsertBefore.txt @@ -0,0 +1,39 @@ +fillField("#foo", "foo"); + $I->click("#foo2"); + $I->click("#bar2"); + $I->click("#baz2"); + $I->fillField("#bar", "bar"); + $I->fillField("#baz", "baz"); + } +} diff --git a/dev/tests/verification/Resources/ActionGroupToExtend.txt b/dev/tests/verification/Resources/ActionGroupToExtend.txt new file mode 100644 index 000000000..fb8a9fd60 --- /dev/null +++ b/dev/tests/verification/Resources/ActionGroupToExtend.txt @@ -0,0 +1,35 @@ +grabMultiple("selector"); + $I->assertCount(99, $grabProductsActionGroup); + } +} diff --git a/dev/tests/verification/Resources/ActionGroupUsingCreateData.txt b/dev/tests/verification/Resources/ActionGroupUsingCreateData.txt new file mode 100644 index 000000000..41953aa3f --- /dev/null +++ b/dev/tests/verification/Resources/ActionGroupUsingCreateData.txt @@ -0,0 +1,59 @@ +amGoingTo("create entity that has the stepKey: createCategoryKey1"); + $ApiCategory = DataObjectHandler::getInstance()->getObject("ApiCategory"); + $this->createCategoryKey1 = new DataPersistenceHandler($ApiCategory, []); + $this->createCategoryKey1->createEntity(); + $I->amGoingTo("create entity that has the stepKey: createConfigProductKey1"); + $ApiConfigurableProduct = DataObjectHandler::getInstance()->getObject("ApiConfigurableProduct"); + $this->createConfigProductKey1 = new DataPersistenceHandler($ApiConfigurableProduct, [$this->createCategoryKey1]); + $this->createConfigProductKey1->createEntity(); + } + + /** + * @Features({"TestModule"}) + * @Parameter(name = "AcceptanceTester", value="$I") + * @param AcceptanceTester $I + * @return void + * @throws \Exception + */ + public function ActionGroupUsingCreateData(AcceptanceTester $I) + { + } +} diff --git a/dev/tests/verification/Resources/ActionGroupUsingNestedArgument.txt b/dev/tests/verification/Resources/ActionGroupUsingNestedArgument.txt index b978a3188..674de1afc 100644 --- a/dev/tests/verification/Resources/ActionGroupUsingNestedArgument.txt +++ b/dev/tests/verification/Resources/ActionGroupUsingNestedArgument.txt @@ -5,6 +5,7 @@ use Magento\FunctionalTestingFramework\AcceptanceTester; use Magento\FunctionalTestingFramework\DataGenerator\Handlers\DataObjectHandler; use Magento\FunctionalTestingFramework\DataGenerator\Persist\DataPersistenceHandler; use Magento\FunctionalTestingFramework\DataGenerator\Objects\EntityDataObject; +use Magento\FunctionalTestingFramework\DataGenerator\Handlers\CredentialStore; use \Codeception\Util\Locator; use Yandex\Allure\Adapter\Annotation\Features; use Yandex\Allure\Adapter\Annotation\Stories; @@ -20,6 +21,7 @@ use Yandex\Allure\Adapter\Annotation\TestCaseId; class ActionGroupUsingNestedArgumentCest { /** + * @Features({"TestModule"}) * @Parameter(name = "AcceptanceTester", value="$I") * @param AcceptanceTester $I * @return void diff --git a/dev/tests/verification/Resources/ActionGroupWithDataOverrideTest.txt b/dev/tests/verification/Resources/ActionGroupWithDataOverrideTest.txt index 0809289b3..6815d318b 100644 --- a/dev/tests/verification/Resources/ActionGroupWithDataOverrideTest.txt +++ b/dev/tests/verification/Resources/ActionGroupWithDataOverrideTest.txt @@ -5,6 +5,7 @@ use Magento\FunctionalTestingFramework\AcceptanceTester; use Magento\FunctionalTestingFramework\DataGenerator\Handlers\DataObjectHandler; use Magento\FunctionalTestingFramework\DataGenerator\Persist\DataPersistenceHandler; use Magento\FunctionalTestingFramework\DataGenerator\Objects\EntityDataObject; +use Magento\FunctionalTestingFramework\DataGenerator\Handlers\CredentialStore; use \Codeception\Util\Locator; use Yandex\Allure\Adapter\Annotation\Features; use Yandex\Allure\Adapter\Annotation\Stories; @@ -16,7 +17,6 @@ use Yandex\Allure\Adapter\Model\SeverityLevel; use Yandex\Allure\Adapter\Annotation\TestCaseId; /** - * @Title("A Functional Cest") * @group functional */ class ActionGroupWithDataOverrideTestCest @@ -61,7 +61,7 @@ class ActionGroupWithDataOverrideTestCest /** * @Severity(level = SeverityLevel::CRITICAL) - * @Features({"Action Group Functional Cest"}) + * @Features({"TestModule"}) * @Stories({"MQE-433"}) * @Parameter(name = "AcceptanceTester", value="$I") * @param AcceptanceTester $I diff --git a/dev/tests/verification/Resources/ActionGroupWithDataTest.txt b/dev/tests/verification/Resources/ActionGroupWithDataTest.txt index fe3cf57c1..e4b79cf73 100644 --- a/dev/tests/verification/Resources/ActionGroupWithDataTest.txt +++ b/dev/tests/verification/Resources/ActionGroupWithDataTest.txt @@ -5,6 +5,7 @@ use Magento\FunctionalTestingFramework\AcceptanceTester; use Magento\FunctionalTestingFramework\DataGenerator\Handlers\DataObjectHandler; use Magento\FunctionalTestingFramework\DataGenerator\Persist\DataPersistenceHandler; use Magento\FunctionalTestingFramework\DataGenerator\Objects\EntityDataObject; +use Magento\FunctionalTestingFramework\DataGenerator\Handlers\CredentialStore; use \Codeception\Util\Locator; use Yandex\Allure\Adapter\Annotation\Features; use Yandex\Allure\Adapter\Annotation\Stories; @@ -16,7 +17,6 @@ use Yandex\Allure\Adapter\Model\SeverityLevel; use Yandex\Allure\Adapter\Annotation\TestCaseId; /** - * @Title("A Functional Cest") * @group functional */ class ActionGroupWithDataTestCest @@ -61,7 +61,7 @@ class ActionGroupWithDataTestCest /** * @Severity(level = SeverityLevel::CRITICAL) - * @Features({"Action Group Functional Cest"}) + * @Features({"TestModule"}) * @Stories({"MQE-433"}) * @Parameter(name = "AcceptanceTester", value="$I") * @param AcceptanceTester $I diff --git a/dev/tests/verification/Resources/ActionGroupWithDefaultArgumentAndStringSelectorParam.txt b/dev/tests/verification/Resources/ActionGroupWithDefaultArgumentAndStringSelectorParam.txt index 9c56afff4..79c95b0df 100644 --- a/dev/tests/verification/Resources/ActionGroupWithDefaultArgumentAndStringSelectorParam.txt +++ b/dev/tests/verification/Resources/ActionGroupWithDefaultArgumentAndStringSelectorParam.txt @@ -5,6 +5,7 @@ use Magento\FunctionalTestingFramework\AcceptanceTester; use Magento\FunctionalTestingFramework\DataGenerator\Handlers\DataObjectHandler; use Magento\FunctionalTestingFramework\DataGenerator\Persist\DataPersistenceHandler; use Magento\FunctionalTestingFramework\DataGenerator\Objects\EntityDataObject; +use Magento\FunctionalTestingFramework\DataGenerator\Handlers\CredentialStore; use \Codeception\Util\Locator; use Yandex\Allure\Adapter\Annotation\Features; use Yandex\Allure\Adapter\Annotation\Stories; @@ -22,6 +23,7 @@ class ActionGroupWithDefaultArgumentAndStringSelectorParamCest { /** * @Severity(level = SeverityLevel::BLOCKER) + * @Features({"TestModule"}) * @Parameter(name = "AcceptanceTester", value="$I") * @param AcceptanceTester $I * @return void diff --git a/dev/tests/verification/Resources/ActionGroupWithMultipleParameterSelectorsFromDefaultArgument.txt b/dev/tests/verification/Resources/ActionGroupWithMultipleParameterSelectorsFromDefaultArgument.txt index aa213c80f..620bbf5f5 100644 --- a/dev/tests/verification/Resources/ActionGroupWithMultipleParameterSelectorsFromDefaultArgument.txt +++ b/dev/tests/verification/Resources/ActionGroupWithMultipleParameterSelectorsFromDefaultArgument.txt @@ -5,6 +5,7 @@ use Magento\FunctionalTestingFramework\AcceptanceTester; use Magento\FunctionalTestingFramework\DataGenerator\Handlers\DataObjectHandler; use Magento\FunctionalTestingFramework\DataGenerator\Persist\DataPersistenceHandler; use Magento\FunctionalTestingFramework\DataGenerator\Objects\EntityDataObject; +use Magento\FunctionalTestingFramework\DataGenerator\Handlers\CredentialStore; use \Codeception\Util\Locator; use Yandex\Allure\Adapter\Annotation\Features; use Yandex\Allure\Adapter\Annotation\Stories; @@ -22,6 +23,7 @@ class ActionGroupWithMultipleParameterSelectorsFromDefaultArgumentCest { /** * @Severity(level = SeverityLevel::BLOCKER) + * @Features({"TestModule"}) * @Parameter(name = "AcceptanceTester", value="$I") * @param AcceptanceTester $I * @return void diff --git a/dev/tests/verification/Resources/ActionGroupWithNoArguments.txt b/dev/tests/verification/Resources/ActionGroupWithNoArguments.txt index 45807663a..b0f709ac9 100644 --- a/dev/tests/verification/Resources/ActionGroupWithNoArguments.txt +++ b/dev/tests/verification/Resources/ActionGroupWithNoArguments.txt @@ -5,6 +5,7 @@ use Magento\FunctionalTestingFramework\AcceptanceTester; use Magento\FunctionalTestingFramework\DataGenerator\Handlers\DataObjectHandler; use Magento\FunctionalTestingFramework\DataGenerator\Persist\DataPersistenceHandler; use Magento\FunctionalTestingFramework\DataGenerator\Objects\EntityDataObject; +use Magento\FunctionalTestingFramework\DataGenerator\Handlers\CredentialStore; use \Codeception\Util\Locator; use Yandex\Allure\Adapter\Annotation\Features; use Yandex\Allure\Adapter\Annotation\Stories; @@ -22,6 +23,7 @@ class ActionGroupWithNoArgumentsCest { /** * @Severity(level = SeverityLevel::BLOCKER) + * @Features({"TestModule"}) * @Parameter(name = "AcceptanceTester", value="$I") * @param AcceptanceTester $I * @return void diff --git a/dev/tests/verification/Resources/ActionGroupWithNoDefaultTest.txt b/dev/tests/verification/Resources/ActionGroupWithNoDefaultTest.txt index 5cc1d7f39..a5119c368 100644 --- a/dev/tests/verification/Resources/ActionGroupWithNoDefaultTest.txt +++ b/dev/tests/verification/Resources/ActionGroupWithNoDefaultTest.txt @@ -5,6 +5,7 @@ use Magento\FunctionalTestingFramework\AcceptanceTester; use Magento\FunctionalTestingFramework\DataGenerator\Handlers\DataObjectHandler; use Magento\FunctionalTestingFramework\DataGenerator\Persist\DataPersistenceHandler; use Magento\FunctionalTestingFramework\DataGenerator\Objects\EntityDataObject; +use Magento\FunctionalTestingFramework\DataGenerator\Handlers\CredentialStore; use \Codeception\Util\Locator; use Yandex\Allure\Adapter\Annotation\Features; use Yandex\Allure\Adapter\Annotation\Stories; @@ -16,7 +17,6 @@ use Yandex\Allure\Adapter\Model\SeverityLevel; use Yandex\Allure\Adapter\Annotation\TestCaseId; /** - * @Title("A Functional Cest") * @group functional */ class ActionGroupWithNoDefaultTestCest @@ -61,7 +61,7 @@ class ActionGroupWithNoDefaultTestCest /** * @Severity(level = SeverityLevel::CRITICAL) - * @Features({"Action Group Functional Cest"}) + * @Features({"TestModule"}) * @Stories({"MQE-433"}) * @Parameter(name = "AcceptanceTester", value="$I") * @param AcceptanceTester $I diff --git a/dev/tests/verification/Resources/ActionGroupWithPassedArgumentAndStringSelectorParam.txt b/dev/tests/verification/Resources/ActionGroupWithPassedArgumentAndStringSelectorParam.txt index 043a71c94..33d3d3d13 100644 --- a/dev/tests/verification/Resources/ActionGroupWithPassedArgumentAndStringSelectorParam.txt +++ b/dev/tests/verification/Resources/ActionGroupWithPassedArgumentAndStringSelectorParam.txt @@ -5,6 +5,7 @@ use Magento\FunctionalTestingFramework\AcceptanceTester; use Magento\FunctionalTestingFramework\DataGenerator\Handlers\DataObjectHandler; use Magento\FunctionalTestingFramework\DataGenerator\Persist\DataPersistenceHandler; use Magento\FunctionalTestingFramework\DataGenerator\Objects\EntityDataObject; +use Magento\FunctionalTestingFramework\DataGenerator\Handlers\CredentialStore; use \Codeception\Util\Locator; use Yandex\Allure\Adapter\Annotation\Features; use Yandex\Allure\Adapter\Annotation\Stories; @@ -22,6 +23,7 @@ class ActionGroupWithPassedArgumentAndStringSelectorParamCest { /** * @Severity(level = SeverityLevel::BLOCKER) + * @Features({"TestModule"}) * @Parameter(name = "AcceptanceTester", value="$I") * @param AcceptanceTester $I * @return void diff --git a/dev/tests/verification/Resources/ActionGroupWithPersistedData.txt b/dev/tests/verification/Resources/ActionGroupWithPersistedData.txt index 2707770d5..133553573 100644 --- a/dev/tests/verification/Resources/ActionGroupWithPersistedData.txt +++ b/dev/tests/verification/Resources/ActionGroupWithPersistedData.txt @@ -5,6 +5,7 @@ use Magento\FunctionalTestingFramework\AcceptanceTester; use Magento\FunctionalTestingFramework\DataGenerator\Handlers\DataObjectHandler; use Magento\FunctionalTestingFramework\DataGenerator\Persist\DataPersistenceHandler; use Magento\FunctionalTestingFramework\DataGenerator\Objects\EntityDataObject; +use Magento\FunctionalTestingFramework\DataGenerator\Handlers\CredentialStore; use \Codeception\Util\Locator; use Yandex\Allure\Adapter\Annotation\Features; use Yandex\Allure\Adapter\Annotation\Stories; @@ -16,7 +17,6 @@ use Yandex\Allure\Adapter\Model\SeverityLevel; use Yandex\Allure\Adapter\Annotation\TestCaseId; /** - * @Title("A Functional Cest") * @group functional */ class ActionGroupWithPersistedDataCest @@ -61,7 +61,7 @@ class ActionGroupWithPersistedDataCest /** * @Severity(level = SeverityLevel::CRITICAL) - * @Features({"Action Group Functional Cest"}) + * @Features({"TestModule"}) * @Stories({"MQE-433"}) * @Parameter(name = "AcceptanceTester", value="$I") * @param AcceptanceTester $I diff --git a/dev/tests/verification/Resources/ActionGroupWithSimpleDataUsageFromDefaultArgument.txt b/dev/tests/verification/Resources/ActionGroupWithSimpleDataUsageFromDefaultArgument.txt index 3774e639d..b5c871a0d 100644 --- a/dev/tests/verification/Resources/ActionGroupWithSimpleDataUsageFromDefaultArgument.txt +++ b/dev/tests/verification/Resources/ActionGroupWithSimpleDataUsageFromDefaultArgument.txt @@ -5,6 +5,7 @@ use Magento\FunctionalTestingFramework\AcceptanceTester; use Magento\FunctionalTestingFramework\DataGenerator\Handlers\DataObjectHandler; use Magento\FunctionalTestingFramework\DataGenerator\Persist\DataPersistenceHandler; use Magento\FunctionalTestingFramework\DataGenerator\Objects\EntityDataObject; +use Magento\FunctionalTestingFramework\DataGenerator\Handlers\CredentialStore; use \Codeception\Util\Locator; use Yandex\Allure\Adapter\Annotation\Features; use Yandex\Allure\Adapter\Annotation\Stories; @@ -22,6 +23,7 @@ class ActionGroupWithSimpleDataUsageFromDefaultArgumentCest { /** * @Severity(level = SeverityLevel::CRITICAL) + * @Features({"TestModule"}) * @Parameter(name = "AcceptanceTester", value="$I") * @param AcceptanceTester $I * @return void diff --git a/dev/tests/verification/Resources/ActionGroupWithSimpleDataUsageFromPassedArgument.txt b/dev/tests/verification/Resources/ActionGroupWithSimpleDataUsageFromPassedArgument.txt index a1e5a316b..d3b910c0a 100644 --- a/dev/tests/verification/Resources/ActionGroupWithSimpleDataUsageFromPassedArgument.txt +++ b/dev/tests/verification/Resources/ActionGroupWithSimpleDataUsageFromPassedArgument.txt @@ -5,6 +5,7 @@ use Magento\FunctionalTestingFramework\AcceptanceTester; use Magento\FunctionalTestingFramework\DataGenerator\Handlers\DataObjectHandler; use Magento\FunctionalTestingFramework\DataGenerator\Persist\DataPersistenceHandler; use Magento\FunctionalTestingFramework\DataGenerator\Objects\EntityDataObject; +use Magento\FunctionalTestingFramework\DataGenerator\Handlers\CredentialStore; use \Codeception\Util\Locator; use Yandex\Allure\Adapter\Annotation\Features; use Yandex\Allure\Adapter\Annotation\Stories; @@ -22,6 +23,7 @@ class ActionGroupWithSimpleDataUsageFromPassedArgumentCest { /** * @Severity(level = SeverityLevel::CRITICAL) + * @Features({"TestModule"}) * @Parameter(name = "AcceptanceTester", value="$I") * @param AcceptanceTester $I * @return void diff --git a/dev/tests/verification/Resources/ActionGroupWithSingleParameterSelectorFromDefaultArgument.txt b/dev/tests/verification/Resources/ActionGroupWithSingleParameterSelectorFromDefaultArgument.txt index d04e2ba80..027370779 100644 --- a/dev/tests/verification/Resources/ActionGroupWithSingleParameterSelectorFromDefaultArgument.txt +++ b/dev/tests/verification/Resources/ActionGroupWithSingleParameterSelectorFromDefaultArgument.txt @@ -5,6 +5,7 @@ use Magento\FunctionalTestingFramework\AcceptanceTester; use Magento\FunctionalTestingFramework\DataGenerator\Handlers\DataObjectHandler; use Magento\FunctionalTestingFramework\DataGenerator\Persist\DataPersistenceHandler; use Magento\FunctionalTestingFramework\DataGenerator\Objects\EntityDataObject; +use Magento\FunctionalTestingFramework\DataGenerator\Handlers\CredentialStore; use \Codeception\Util\Locator; use Yandex\Allure\Adapter\Annotation\Features; use Yandex\Allure\Adapter\Annotation\Stories; @@ -22,6 +23,7 @@ class ActionGroupWithSingleParameterSelectorFromDefaultArgumentCest { /** * @Severity(level = SeverityLevel::BLOCKER) + * @Features({"TestModule"}) * @Parameter(name = "AcceptanceTester", value="$I") * @param AcceptanceTester $I * @return void diff --git a/dev/tests/verification/Resources/ActionGroupWithSingleParameterSelectorFromPassedArgument.txt b/dev/tests/verification/Resources/ActionGroupWithSingleParameterSelectorFromPassedArgument.txt index fb10e7847..7900385db 100644 --- a/dev/tests/verification/Resources/ActionGroupWithSingleParameterSelectorFromPassedArgument.txt +++ b/dev/tests/verification/Resources/ActionGroupWithSingleParameterSelectorFromPassedArgument.txt @@ -5,6 +5,7 @@ use Magento\FunctionalTestingFramework\AcceptanceTester; use Magento\FunctionalTestingFramework\DataGenerator\Handlers\DataObjectHandler; use Magento\FunctionalTestingFramework\DataGenerator\Persist\DataPersistenceHandler; use Magento\FunctionalTestingFramework\DataGenerator\Objects\EntityDataObject; +use Magento\FunctionalTestingFramework\DataGenerator\Handlers\CredentialStore; use \Codeception\Util\Locator; use Yandex\Allure\Adapter\Annotation\Features; use Yandex\Allure\Adapter\Annotation\Stories; @@ -22,6 +23,7 @@ class ActionGroupWithSingleParameterSelectorFromPassedArgumentCest { /** * @Severity(level = SeverityLevel::BLOCKER) + * @Features({"TestModule"}) * @Parameter(name = "AcceptanceTester", value="$I") * @param AcceptanceTester $I * @return void diff --git a/dev/tests/verification/Resources/ActionGroupWithStepKeyReferences.txt b/dev/tests/verification/Resources/ActionGroupWithStepKeyReferences.txt index ac97746b8..28bb00d4d 100644 --- a/dev/tests/verification/Resources/ActionGroupWithStepKeyReferences.txt +++ b/dev/tests/verification/Resources/ActionGroupWithStepKeyReferences.txt @@ -5,6 +5,7 @@ use Magento\FunctionalTestingFramework\AcceptanceTester; use Magento\FunctionalTestingFramework\DataGenerator\Handlers\DataObjectHandler; use Magento\FunctionalTestingFramework\DataGenerator\Persist\DataPersistenceHandler; use Magento\FunctionalTestingFramework\DataGenerator\Objects\EntityDataObject; +use Magento\FunctionalTestingFramework\DataGenerator\Handlers\CredentialStore; use \Codeception\Util\Locator; use Yandex\Allure\Adapter\Annotation\Features; use Yandex\Allure\Adapter\Annotation\Stories; @@ -20,6 +21,7 @@ use Yandex\Allure\Adapter\Annotation\TestCaseId; class ActionGroupWithStepKeyReferencesCest { /** + * @Features({"TestModule"}) * @Parameter(name = "AcceptanceTester", value="$I") * @param AcceptanceTester $I * @return void diff --git a/dev/tests/verification/Resources/ActionGroupWithTopLevelPersistedData.txt b/dev/tests/verification/Resources/ActionGroupWithTopLevelPersistedData.txt index 743864b79..4ec6520d9 100644 --- a/dev/tests/verification/Resources/ActionGroupWithTopLevelPersistedData.txt +++ b/dev/tests/verification/Resources/ActionGroupWithTopLevelPersistedData.txt @@ -5,6 +5,7 @@ use Magento\FunctionalTestingFramework\AcceptanceTester; use Magento\FunctionalTestingFramework\DataGenerator\Handlers\DataObjectHandler; use Magento\FunctionalTestingFramework\DataGenerator\Persist\DataPersistenceHandler; use Magento\FunctionalTestingFramework\DataGenerator\Objects\EntityDataObject; +use Magento\FunctionalTestingFramework\DataGenerator\Handlers\CredentialStore; use \Codeception\Util\Locator; use Yandex\Allure\Adapter\Annotation\Features; use Yandex\Allure\Adapter\Annotation\Stories; @@ -16,7 +17,6 @@ use Yandex\Allure\Adapter\Model\SeverityLevel; use Yandex\Allure\Adapter\Annotation\TestCaseId; /** - * @Title("A Functional Cest") * @group functional */ class ActionGroupWithTopLevelPersistedDataCest @@ -61,7 +61,7 @@ class ActionGroupWithTopLevelPersistedDataCest /** * @Severity(level = SeverityLevel::CRITICAL) - * @Features({"Action Group Functional Cest"}) + * @Features({"TestModule"}) * @Stories({"MQE-433"}) * @Parameter(name = "AcceptanceTester", value="$I") * @param AcceptanceTester $I diff --git a/dev/tests/verification/Resources/ArgumentWithSameNameAsElement.txt b/dev/tests/verification/Resources/ArgumentWithSameNameAsElement.txt index bb59a6758..a97fd116e 100644 --- a/dev/tests/verification/Resources/ArgumentWithSameNameAsElement.txt +++ b/dev/tests/verification/Resources/ArgumentWithSameNameAsElement.txt @@ -5,6 +5,7 @@ use Magento\FunctionalTestingFramework\AcceptanceTester; use Magento\FunctionalTestingFramework\DataGenerator\Handlers\DataObjectHandler; use Magento\FunctionalTestingFramework\DataGenerator\Persist\DataPersistenceHandler; use Magento\FunctionalTestingFramework\DataGenerator\Objects\EntityDataObject; +use Magento\FunctionalTestingFramework\DataGenerator\Handlers\CredentialStore; use \Codeception\Util\Locator; use Yandex\Allure\Adapter\Annotation\Features; use Yandex\Allure\Adapter\Annotation\Stories; @@ -16,7 +17,6 @@ use Yandex\Allure\Adapter\Model\SeverityLevel; use Yandex\Allure\Adapter\Annotation\TestCaseId; /** - * @Title("A Functional Cest") * @group functional */ class ArgumentWithSameNameAsElementCest @@ -61,7 +61,7 @@ class ArgumentWithSameNameAsElementCest /** * @Severity(level = SeverityLevel::CRITICAL) - * @Features({"Action Group Functional Cest"}) + * @Features({"TestModule"}) * @Stories({"MQE-433"}) * @Parameter(name = "AcceptanceTester", value="$I") * @param AcceptanceTester $I diff --git a/dev/tests/verification/Resources/AssertTest.txt b/dev/tests/verification/Resources/AssertTest.txt index 60eed6a8d..beb78eed1 100644 --- a/dev/tests/verification/Resources/AssertTest.txt +++ b/dev/tests/verification/Resources/AssertTest.txt @@ -5,6 +5,7 @@ use Magento\FunctionalTestingFramework\AcceptanceTester; use Magento\FunctionalTestingFramework\DataGenerator\Handlers\DataObjectHandler; use Magento\FunctionalTestingFramework\DataGenerator\Persist\DataPersistenceHandler; use Magento\FunctionalTestingFramework\DataGenerator\Objects\EntityDataObject; +use Magento\FunctionalTestingFramework\DataGenerator\Handlers\CredentialStore; use \Codeception\Util\Locator; use Yandex\Allure\Adapter\Annotation\Features; use Yandex\Allure\Adapter\Annotation\Stories; @@ -37,6 +38,7 @@ class AssertTestCest } /** + * @Features({"TestModule"}) * @Parameter(name = "AcceptanceTester", value="$I") * @param AcceptanceTester $I * @return void diff --git a/dev/tests/verification/Resources/BasicActionGroupTest.txt b/dev/tests/verification/Resources/BasicActionGroupTest.txt index 86835364e..b8b491ef4 100644 --- a/dev/tests/verification/Resources/BasicActionGroupTest.txt +++ b/dev/tests/verification/Resources/BasicActionGroupTest.txt @@ -5,6 +5,7 @@ use Magento\FunctionalTestingFramework\AcceptanceTester; use Magento\FunctionalTestingFramework\DataGenerator\Handlers\DataObjectHandler; use Magento\FunctionalTestingFramework\DataGenerator\Persist\DataPersistenceHandler; use Magento\FunctionalTestingFramework\DataGenerator\Objects\EntityDataObject; +use Magento\FunctionalTestingFramework\DataGenerator\Handlers\CredentialStore; use \Codeception\Util\Locator; use Yandex\Allure\Adapter\Annotation\Features; use Yandex\Allure\Adapter\Annotation\Stories; @@ -16,7 +17,6 @@ use Yandex\Allure\Adapter\Model\SeverityLevel; use Yandex\Allure\Adapter\Annotation\TestCaseId; /** - * @Title("A Functional Cest") * @group functional */ class BasicActionGroupTestCest @@ -42,7 +42,7 @@ class BasicActionGroupTestCest /** * @Severity(level = SeverityLevel::CRITICAL) - * @Features({"Action Group Functional Cest"}) + * @Features({"TestModule"}) * @Stories({"MQE-433"}) * @Parameter(name = "AcceptanceTester", value="$I") * @param AcceptanceTester $I diff --git a/dev/tests/verification/Resources/BasicFunctionalTest.txt b/dev/tests/verification/Resources/BasicFunctionalTest.txt index 57bd9dd01..7fed6db42 100644 --- a/dev/tests/verification/Resources/BasicFunctionalTest.txt +++ b/dev/tests/verification/Resources/BasicFunctionalTest.txt @@ -5,6 +5,7 @@ use Magento\FunctionalTestingFramework\AcceptanceTester; use Magento\FunctionalTestingFramework\DataGenerator\Handlers\DataObjectHandler; use Magento\FunctionalTestingFramework\DataGenerator\Persist\DataPersistenceHandler; use Magento\FunctionalTestingFramework\DataGenerator\Objects\EntityDataObject; +use Magento\FunctionalTestingFramework\DataGenerator\Handlers\CredentialStore; use \Codeception\Util\Locator; use Yandex\Allure\Adapter\Annotation\Features; use Yandex\Allure\Adapter\Annotation\Stories; @@ -50,7 +51,7 @@ class BasicFunctionalTestCest /** * @Severity(level = SeverityLevel::CRITICAL) - * @Features({"Basic Functional Cest"}) + * @Features({"TestModule"}) * @Stories({"MQE-305"}) * @Parameter(name = "AcceptanceTester", value="$I") * @param AcceptanceTester $I @@ -98,13 +99,17 @@ class BasicFunctionalTestCest $executeJSKey1 = $I->executeJS("someJSFunction"); $I->fillField(".functionalTestSelector", "someInput"); $I->fillField(".functionalTestSelector", "0"); + $date = new \DateTime(); + $date->setTimestamp(strtotime("Now")); + $date->setTimezone(new \DateTimeZone("America/Los_Angeles")); + $generateDateKey = $date->format("H:i:s"); $grabAttributeFromKey1 = $I->grabAttributeFrom(".functionalTestSelector", "someInput"); $grabCookieKey1 = $I->grabCookie("grabCookieInput", ['domain' => 'www.google.com']); $grabFromCurrentUrlKey1 = $I->grabFromCurrentUrl("/grabCurrentUrl"); $grabMultipleKey1 = $I->grabMultiple(".functionalTestSelector"); $grabTextFromKey1 = $I->grabTextFrom(".functionalTestSelector"); $grabValueFromKey1 = $I->grabValueFrom(".functionalTestSelector"); - $magentoCli1 = $I->magentoCLI("maintenance:enable"); + $magentoCli1 = $I->magentoCLI("maintenance:enable", "\"stuffHere\""); $I->comment($magentoCli1); $I->makeScreenshot("screenShotInput"); $I->maximizeWindow(); diff --git a/dev/tests/verification/Resources/BasicMergeTest.txt b/dev/tests/verification/Resources/BasicMergeTest.txt index 2cecb803f..280630b6f 100644 --- a/dev/tests/verification/Resources/BasicMergeTest.txt +++ b/dev/tests/verification/Resources/BasicMergeTest.txt @@ -5,6 +5,7 @@ use Magento\FunctionalTestingFramework\AcceptanceTester; use Magento\FunctionalTestingFramework\DataGenerator\Handlers\DataObjectHandler; use Magento\FunctionalTestingFramework\DataGenerator\Persist\DataPersistenceHandler; use Magento\FunctionalTestingFramework\DataGenerator\Objects\EntityDataObject; +use Magento\FunctionalTestingFramework\DataGenerator\Handlers\CredentialStore; use \Codeception\Util\Locator; use Yandex\Allure\Adapter\Annotation\Features; use Yandex\Allure\Adapter\Annotation\Stories; @@ -16,7 +17,7 @@ use Yandex\Allure\Adapter\Model\SeverityLevel; use Yandex\Allure\Adapter\Annotation\TestCaseId; /** - * @Title("A Functional Cest") + * @Title("BasicMergeTest") * @group functional * @group mergeTest */ @@ -52,7 +53,7 @@ class BasicMergeTestCest /** * @Severity(level = SeverityLevel::CRITICAL) - * @Features({"Merge Functional Cest"}) + * @Features({"TestModule"}) * @Stories({"MQE-433"}) * @Parameter(name = "AcceptanceTester", value="$I") * @param AcceptanceTester $I diff --git a/dev/tests/verification/Resources/CharacterReplacementTest.txt b/dev/tests/verification/Resources/CharacterReplacementTest.txt index 4ae325fba..844b08974 100644 --- a/dev/tests/verification/Resources/CharacterReplacementTest.txt +++ b/dev/tests/verification/Resources/CharacterReplacementTest.txt @@ -5,6 +5,7 @@ use Magento\FunctionalTestingFramework\AcceptanceTester; use Magento\FunctionalTestingFramework\DataGenerator\Handlers\DataObjectHandler; use Magento\FunctionalTestingFramework\DataGenerator\Persist\DataPersistenceHandler; use Magento\FunctionalTestingFramework\DataGenerator\Objects\EntityDataObject; +use Magento\FunctionalTestingFramework\DataGenerator\Handlers\CredentialStore; use \Codeception\Util\Locator; use Yandex\Allure\Adapter\Annotation\Features; use Yandex\Allure\Adapter\Annotation\Stories; @@ -20,6 +21,7 @@ use Yandex\Allure\Adapter\Annotation\TestCaseId; class CharacterReplacementTestCest { /** + * @Features({"TestModule"}) * @Parameter(name = "AcceptanceTester", value="$I") * @param AcceptanceTester $I * @return void diff --git a/dev/tests/verification/Resources/ChildExtendedTestAddHooks.txt b/dev/tests/verification/Resources/ChildExtendedTestAddHooks.txt new file mode 100644 index 000000000..d8417f5cb --- /dev/null +++ b/dev/tests/verification/Resources/ChildExtendedTestAddHooks.txt @@ -0,0 +1,64 @@ +amOnPage("/beforeUrl"); + } + + /** + * @param AcceptanceTester $I + * @throws \Exception + */ + public function _after(AcceptanceTester $I) + { + $I->amOnPage("/afterUrl"); + } + + /** + * @param AcceptanceTester $I + * @throws \Exception + */ + public function _failed(AcceptanceTester $I) + { + $I->saveScreenshot(); + } + + /** + * @Severity(level = SeverityLevel::MINOR) + * @Features({"TestModule"}) + * @Stories({"Parent"}) + * @Parameter(name = "AcceptanceTester", value="$I") + * @param AcceptanceTester $I + * @return void + * @throws \Exception + */ + public function ChildExtendedTestAddHooks(AcceptanceTester $I) + { + } +} diff --git a/dev/tests/verification/Resources/ChildExtendedTestMerging.txt b/dev/tests/verification/Resources/ChildExtendedTestMerging.txt new file mode 100644 index 000000000..caf382718 --- /dev/null +++ b/dev/tests/verification/Resources/ChildExtendedTestMerging.txt @@ -0,0 +1,70 @@ +amOnPage("/firstUrl"); + $I->amOnPage("/beforeUrl"); + $I->amOnPage("/lastUrl"); + } + + /** + * @param AcceptanceTester $I + * @throws \Exception + */ + public function _after(AcceptanceTester $I) + { + $I->amOnPage("/afterUrl"); + } + + /** + * @param AcceptanceTester $I + * @throws \Exception + */ + public function _failed(AcceptanceTester $I) + { + $I->saveScreenshot(); + } + + /** + * @Severity(level = SeverityLevel::TRIVIAL) + * @Features({"TestModule"}) + * @Stories({"Child"}) + * @Parameter(name = "AcceptanceTester", value="$I") + * @param AcceptanceTester $I + * @return void + * @throws \Exception + */ + public function ChildExtendedTestMerging(AcceptanceTester $I) + { + $I->comment("Before Comment"); + $I->comment("Parent Comment"); + $I->comment("After Comment"); + $I->comment("Last Comment"); + } +} diff --git a/dev/tests/verification/Resources/ChildExtendedTestNoParent.txt b/dev/tests/verification/Resources/ChildExtendedTestNoParent.txt new file mode 100644 index 000000000..3ab774b3d --- /dev/null +++ b/dev/tests/verification/Resources/ChildExtendedTestNoParent.txt @@ -0,0 +1,39 @@ +skip("This test is skipped due to the following issues:\nNo issues have been specified."); + } +} diff --git a/dev/tests/verification/Resources/ChildExtendedTestRemoveAction.txt b/dev/tests/verification/Resources/ChildExtendedTestRemoveAction.txt new file mode 100644 index 000000000..4fefcd657 --- /dev/null +++ b/dev/tests/verification/Resources/ChildExtendedTestRemoveAction.txt @@ -0,0 +1,64 @@ +amOnPage("/beforeUrl"); + } + + /** + * @param AcceptanceTester $I + * @throws \Exception + */ + public function _after(AcceptanceTester $I) + { + $I->amOnPage("/afterUrl"); + } + + /** + * @param AcceptanceTester $I + * @throws \Exception + */ + public function _failed(AcceptanceTester $I) + { + $I->saveScreenshot(); + } + + /** + * @Severity(level = SeverityLevel::CRITICAL) + * @Features({"TestModule"}) + * @Stories({"Child"}) + * @Parameter(name = "AcceptanceTester", value="$I") + * @param AcceptanceTester $I + * @return void + * @throws \Exception + */ + public function ChildExtendedTestRemoveAction(AcceptanceTester $I) + { + } +} diff --git a/dev/tests/verification/Resources/ChildExtendedTestRemoveHookAction.txt b/dev/tests/verification/Resources/ChildExtendedTestRemoveHookAction.txt new file mode 100644 index 000000000..5f135c6d7 --- /dev/null +++ b/dev/tests/verification/Resources/ChildExtendedTestRemoveHookAction.txt @@ -0,0 +1,64 @@ +amOnPage("/afterUrl"); + } + + /** + * @param AcceptanceTester $I + * @throws \Exception + */ + public function _failed(AcceptanceTester $I) + { + $I->saveScreenshot(); + } + + /** + * @Severity(level = SeverityLevel::CRITICAL) + * @Features({"TestModule"}) + * @Stories({"Child"}) + * @Parameter(name = "AcceptanceTester", value="$I") + * @param AcceptanceTester $I + * @return void + * @throws \Exception + */ + public function ChildExtendedTestRemoveHookAction(AcceptanceTester $I) + { + $I->comment("Parent Comment"); + } +} diff --git a/dev/tests/verification/Resources/ChildExtendedTestReplace.txt b/dev/tests/verification/Resources/ChildExtendedTestReplace.txt new file mode 100644 index 000000000..650544131 --- /dev/null +++ b/dev/tests/verification/Resources/ChildExtendedTestReplace.txt @@ -0,0 +1,65 @@ +amOnPage("/beforeUrl"); + } + + /** + * @param AcceptanceTester $I + * @throws \Exception + */ + public function _after(AcceptanceTester $I) + { + $I->amOnPage("/afterUrl"); + } + + /** + * @param AcceptanceTester $I + * @throws \Exception + */ + public function _failed(AcceptanceTester $I) + { + $I->saveScreenshot(); + } + + /** + * @Severity(level = SeverityLevel::TRIVIAL) + * @Features({"TestModule"}) + * @Stories({"Child"}) + * @Parameter(name = "AcceptanceTester", value="$I") + * @param AcceptanceTester $I + * @return void + * @throws \Exception + */ + public function ChildExtendedTestReplace(AcceptanceTester $I) + { + $I->comment("Different Input"); + } +} diff --git a/dev/tests/verification/Resources/ChildExtendedTestReplaceHook.txt b/dev/tests/verification/Resources/ChildExtendedTestReplaceHook.txt new file mode 100644 index 000000000..9c05eb88e --- /dev/null +++ b/dev/tests/verification/Resources/ChildExtendedTestReplaceHook.txt @@ -0,0 +1,65 @@ +amOnPage("/slightlyDifferentBeforeUrl"); + } + + /** + * @param AcceptanceTester $I + * @throws \Exception + */ + public function _after(AcceptanceTester $I) + { + $I->amOnPage("/afterUrl"); + } + + /** + * @param AcceptanceTester $I + * @throws \Exception + */ + public function _failed(AcceptanceTester $I) + { + $I->saveScreenshot(); + } + + /** + * @Severity(level = SeverityLevel::TRIVIAL) + * @Features({"TestModule"}) + * @Stories({"Child"}) + * @Parameter(name = "AcceptanceTester", value="$I") + * @param AcceptanceTester $I + * @return void + * @throws \Exception + */ + public function ChildExtendedTestReplaceHook(AcceptanceTester $I) + { + $I->comment("Parent Comment"); + } +} diff --git a/dev/tests/verification/Resources/DataReplacementTest.txt b/dev/tests/verification/Resources/DataReplacementTest.txt index d5121931c..8a95eb1e8 100644 --- a/dev/tests/verification/Resources/DataReplacementTest.txt +++ b/dev/tests/verification/Resources/DataReplacementTest.txt @@ -5,6 +5,7 @@ use Magento\FunctionalTestingFramework\AcceptanceTester; use Magento\FunctionalTestingFramework\DataGenerator\Handlers\DataObjectHandler; use Magento\FunctionalTestingFramework\DataGenerator\Persist\DataPersistenceHandler; use Magento\FunctionalTestingFramework\DataGenerator\Objects\EntityDataObject; +use Magento\FunctionalTestingFramework\DataGenerator\Handlers\CredentialStore; use \Codeception\Util\Locator; use Yandex\Allure\Adapter\Annotation\Features; use Yandex\Allure\Adapter\Annotation\Stories; @@ -20,6 +21,7 @@ use Yandex\Allure\Adapter\Annotation\TestCaseId; class DataReplacementTestCest { /** + * @Features({"TestModule"}) * @Parameter(name = "AcceptanceTester", value="$I") * @param AcceptanceTester $I * @return void @@ -28,6 +30,7 @@ class DataReplacementTestCest public function DataReplacementTest(AcceptanceTester $I) { $I->fillField("#selector", "StringBefore John StringAfter"); + $I->seeCurrentUrlMatches("~\/John~i"); $I->fillField("#John", "input"); $I->dragAndDrop("#John", "Doe"); $I->conditionalClick("Doe", "#John", true); @@ -50,5 +53,7 @@ class DataReplacementTestCest $I->searchAndMultiSelectOption("#selector", [msq("uniqueData") . "John", "Doe" . msq("uniqueData")]); $I->selectMultipleOptions("#Doe" . msq("uniqueData"), "#element", [msq("uniqueData") . "John", "Doe" . msq("uniqueData")]); $I->fillField(".selector", "0"); + $insertCommand = $I->magentoCLI("do something Doe" . msq("uniqueData") . " with uniqueness"); + $I->comment($insertCommand); } } diff --git a/dev/tests/verification/Resources/ExecuteJsEscapingTest.txt b/dev/tests/verification/Resources/ExecuteJsEscapingTest.txt new file mode 100644 index 000000000..94dc2cbdb --- /dev/null +++ b/dev/tests/verification/Resources/ExecuteJsEscapingTest.txt @@ -0,0 +1,35 @@ +executeJS("return \$javascriptVariable"); + $mftfVariableNotEscaped = $I->executeJS("return {$doNotEscape}"); + } +} diff --git a/dev/tests/verification/Resources/ExtendParentDataTest.txt b/dev/tests/verification/Resources/ExtendParentDataTest.txt new file mode 100644 index 000000000..a681733f0 --- /dev/null +++ b/dev/tests/verification/Resources/ExtendParentDataTest.txt @@ -0,0 +1,42 @@ +amGoingTo("create entity that has the stepKey: simpleDataKey"); + $extendParentData = DataObjectHandler::getInstance()->getObject("extendParentData"); + $simpleDataKey = new DataPersistenceHandler($extendParentData, []); + $simpleDataKey->createEntity(); + $I->searchAndMultiSelectOption("#selector", ["otherName"]); + $I->searchAndMultiSelectOption("#selector", ["extendName"]); + $I->searchAndMultiSelectOption("#selector", ["item"]); + $I->searchAndMultiSelectOption("#selector", [msq("extendParentData") . "prename"]); + $I->searchAndMultiSelectOption("#selector", ["postnameExtend" . msq("extendParentData")]); + } +} diff --git a/dev/tests/verification/Resources/ExtendedActionGroup.txt b/dev/tests/verification/Resources/ExtendedActionGroup.txt new file mode 100644 index 000000000..93acc8910 --- /dev/null +++ b/dev/tests/verification/Resources/ExtendedActionGroup.txt @@ -0,0 +1,38 @@ +comment("New Input Before"); + $grabProductsActionGroup = $I->grabMultiple("notASelector"); + $I->comment("New Input After"); + $I->assertCount(99, $grabProductsActionGroup); + $I->assertCount(8000, $grabProductsActionGroup); + } +} diff --git a/dev/tests/verification/Resources/ExtendedParameterArrayTest.txt b/dev/tests/verification/Resources/ExtendedParameterArrayTest.txt new file mode 100644 index 000000000..afdbdec3e --- /dev/null +++ b/dev/tests/verification/Resources/ExtendedParameterArrayTest.txt @@ -0,0 +1,39 @@ +amGoingTo("create entity that has the stepKey: simpleDataKey"); + $testExtendSimpleParamData = DataObjectHandler::getInstance()->getObject("testExtendSimpleParamData"); + $simpleDataKey = new DataPersistenceHandler($testExtendSimpleParamData, []); + $simpleDataKey->createEntity(); + $I->searchAndMultiSelectOption("#selector", ["otherName"]); + $I->searchAndMultiSelectOption("#selector", ["extendName"]); + } +} diff --git a/dev/tests/verification/Resources/ExtendedRemoveActionGroup.txt b/dev/tests/verification/Resources/ExtendedRemoveActionGroup.txt new file mode 100644 index 000000000..2bfd08a91 --- /dev/null +++ b/dev/tests/verification/Resources/ExtendedRemoveActionGroup.txt @@ -0,0 +1,33 @@ +fillField("#foo", "foo"); + $I->fillField("#bar", "bar"); + $I->click("#mergeOne"); + $I->click("#mergeTwo"); + $I->click("#mergeThree"); + $I->fillField("#baz", "baz"); + } +} diff --git a/dev/tests/verification/Resources/MergeMassViaInsertBefore.txt b/dev/tests/verification/Resources/MergeMassViaInsertBefore.txt new file mode 100644 index 000000000..82e2c4d6d --- /dev/null +++ b/dev/tests/verification/Resources/MergeMassViaInsertBefore.txt @@ -0,0 +1,39 @@ +fillField("#foo", "foo"); + $I->click("#mergeOne"); + $I->click("#mergeTwo"); + $I->click("#mergeThree"); + $I->fillField("#bar", "bar"); + $I->fillField("#baz", "baz"); + } +} diff --git a/dev/tests/verification/Resources/MergeSkip.txt b/dev/tests/verification/Resources/MergeSkip.txt new file mode 100644 index 000000000..d6ecff5db --- /dev/null +++ b/dev/tests/verification/Resources/MergeSkip.txt @@ -0,0 +1,34 @@ +skip("This test is skipped due to the following issues:\nIssue5"); + } +} diff --git a/dev/tests/verification/Resources/MergedActionGroupTest.txt b/dev/tests/verification/Resources/MergedActionGroupTest.txt index ab9b46f8d..7862b0036 100644 --- a/dev/tests/verification/Resources/MergedActionGroupTest.txt +++ b/dev/tests/verification/Resources/MergedActionGroupTest.txt @@ -5,6 +5,7 @@ use Magento\FunctionalTestingFramework\AcceptanceTester; use Magento\FunctionalTestingFramework\DataGenerator\Handlers\DataObjectHandler; use Magento\FunctionalTestingFramework\DataGenerator\Persist\DataPersistenceHandler; use Magento\FunctionalTestingFramework\DataGenerator\Objects\EntityDataObject; +use Magento\FunctionalTestingFramework\DataGenerator\Handlers\CredentialStore; use \Codeception\Util\Locator; use Yandex\Allure\Adapter\Annotation\Features; use Yandex\Allure\Adapter\Annotation\Stories; @@ -16,7 +17,6 @@ use Yandex\Allure\Adapter\Model\SeverityLevel; use Yandex\Allure\Adapter\Annotation\TestCaseId; /** - * @Title("A Functional Cest") * @group functional */ class MergedActionGroupTestCest @@ -61,7 +61,7 @@ class MergedActionGroupTestCest /** * @Severity(level = SeverityLevel::CRITICAL) - * @Features({"Action Group Functional Cest"}) + * @Features({"TestModule"}) * @Stories({"MQE-433"}) * @Parameter(name = "AcceptanceTester", value="$I") * @param AcceptanceTester $I @@ -70,9 +70,9 @@ class MergedActionGroupTestCest */ public function MergedActionGroupTest(AcceptanceTester $I) { - $I->see("#element .Jane"); $I->see(".merge .Jane"); - $I->click(".merge .Dane"); + $I->see("#element .Jane"); $I->amOnPage("/Jane/Dane.html"); + $I->click(".merge .Dane"); } } diff --git a/dev/tests/verification/Resources/MergedReferencesTest.txt b/dev/tests/verification/Resources/MergedReferencesTest.txt index 519db4314..39111ad40 100644 --- a/dev/tests/verification/Resources/MergedReferencesTest.txt +++ b/dev/tests/verification/Resources/MergedReferencesTest.txt @@ -5,6 +5,7 @@ use Magento\FunctionalTestingFramework\AcceptanceTester; use Magento\FunctionalTestingFramework\DataGenerator\Handlers\DataObjectHandler; use Magento\FunctionalTestingFramework\DataGenerator\Persist\DataPersistenceHandler; use Magento\FunctionalTestingFramework\DataGenerator\Objects\EntityDataObject; +use Magento\FunctionalTestingFramework\DataGenerator\Handlers\CredentialStore; use \Codeception\Util\Locator; use Yandex\Allure\Adapter\Annotation\Features; use Yandex\Allure\Adapter\Annotation\Stories; @@ -16,7 +17,7 @@ use Yandex\Allure\Adapter\Model\SeverityLevel; use Yandex\Allure\Adapter\Annotation\TestCaseId; /** - * @Title("A Functional Cest") + * @Title("MergedReferencesTest") * @group functional */ class MergedReferencesTestCest @@ -50,7 +51,7 @@ class MergedReferencesTestCest /** * @Severity(level = SeverityLevel::CRITICAL) - * @Features({"Merge Functional Cest"}) + * @Features({"TestModule"}) * @Stories({"MQE-433"}) * @Parameter(name = "AcceptanceTester", value="$I") * @param AcceptanceTester $I diff --git a/dev/tests/verification/Resources/MultipleActionGroupsTest.txt b/dev/tests/verification/Resources/MultipleActionGroupsTest.txt index d9b32bfce..1b5a74c94 100644 --- a/dev/tests/verification/Resources/MultipleActionGroupsTest.txt +++ b/dev/tests/verification/Resources/MultipleActionGroupsTest.txt @@ -5,6 +5,7 @@ use Magento\FunctionalTestingFramework\AcceptanceTester; use Magento\FunctionalTestingFramework\DataGenerator\Handlers\DataObjectHandler; use Magento\FunctionalTestingFramework\DataGenerator\Persist\DataPersistenceHandler; use Magento\FunctionalTestingFramework\DataGenerator\Objects\EntityDataObject; +use Magento\FunctionalTestingFramework\DataGenerator\Handlers\CredentialStore; use \Codeception\Util\Locator; use Yandex\Allure\Adapter\Annotation\Features; use Yandex\Allure\Adapter\Annotation\Stories; @@ -16,7 +17,6 @@ use Yandex\Allure\Adapter\Model\SeverityLevel; use Yandex\Allure\Adapter\Annotation\TestCaseId; /** - * @Title("A Functional Cest") * @group functional */ class MultipleActionGroupsTestCest @@ -61,7 +61,7 @@ class MultipleActionGroupsTestCest /** * @Severity(level = SeverityLevel::CRITICAL) - * @Features({"Action Group Functional Cest"}) + * @Features({"TestModule"}) * @Stories({"MQE-433"}) * @Parameter(name = "AcceptanceTester", value="$I") * @param AcceptanceTester $I diff --git a/dev/tests/verification/Resources/PageReplacementTest.txt b/dev/tests/verification/Resources/PageReplacementTest.txt index 5621e5c7a..1422a24ba 100644 --- a/dev/tests/verification/Resources/PageReplacementTest.txt +++ b/dev/tests/verification/Resources/PageReplacementTest.txt @@ -5,6 +5,7 @@ use Magento\FunctionalTestingFramework\AcceptanceTester; use Magento\FunctionalTestingFramework\DataGenerator\Handlers\DataObjectHandler; use Magento\FunctionalTestingFramework\DataGenerator\Persist\DataPersistenceHandler; use Magento\FunctionalTestingFramework\DataGenerator\Objects\EntityDataObject; +use Magento\FunctionalTestingFramework\DataGenerator\Handlers\CredentialStore; use \Codeception\Util\Locator; use Yandex\Allure\Adapter\Annotation\Features; use Yandex\Allure\Adapter\Annotation\Stories; @@ -20,6 +21,7 @@ use Yandex\Allure\Adapter\Annotation\TestCaseId; class PageReplacementTestCest { /** + * @Features({"TestModule"}) * @Parameter(name = "AcceptanceTester", value="$I") * @param AcceptanceTester $I * @return void diff --git a/dev/tests/verification/Resources/ParameterArrayTest.txt b/dev/tests/verification/Resources/ParameterArrayTest.txt index f60478037..4b56eb15c 100644 --- a/dev/tests/verification/Resources/ParameterArrayTest.txt +++ b/dev/tests/verification/Resources/ParameterArrayTest.txt @@ -5,6 +5,7 @@ use Magento\FunctionalTestingFramework\AcceptanceTester; use Magento\FunctionalTestingFramework\DataGenerator\Handlers\DataObjectHandler; use Magento\FunctionalTestingFramework\DataGenerator\Persist\DataPersistenceHandler; use Magento\FunctionalTestingFramework\DataGenerator\Objects\EntityDataObject; +use Magento\FunctionalTestingFramework\DataGenerator\Handlers\CredentialStore; use \Codeception\Util\Locator; use Yandex\Allure\Adapter\Annotation\Features; use Yandex\Allure\Adapter\Annotation\Stories; @@ -20,6 +21,7 @@ use Yandex\Allure\Adapter\Annotation\TestCaseId; class ParameterArrayTestCest { /** + * @Features({"TestModule"}) * @Parameter(name = "AcceptanceTester", value="$I") * @param AcceptanceTester $I * @return void diff --git a/dev/tests/verification/Resources/ParentExtendedTest.txt b/dev/tests/verification/Resources/ParentExtendedTest.txt new file mode 100644 index 000000000..a8e74e493 --- /dev/null +++ b/dev/tests/verification/Resources/ParentExtendedTest.txt @@ -0,0 +1,65 @@ +amOnPage("/beforeUrl"); + } + + /** + * @param AcceptanceTester $I + * @throws \Exception + */ + public function _after(AcceptanceTester $I) + { + $I->amOnPage("/afterUrl"); + } + + /** + * @param AcceptanceTester $I + * @throws \Exception + */ + public function _failed(AcceptanceTester $I) + { + $I->saveScreenshot(); + } + + /** + * @Severity(level = SeverityLevel::MINOR) + * @Features({"TestModule"}) + * @Stories({"Parent"}) + * @Parameter(name = "AcceptanceTester", value="$I") + * @param AcceptanceTester $I + * @return void + * @throws \Exception + */ + public function ParentExtendedTest(AcceptanceTester $I) + { + $I->comment("Parent Comment"); + } +} diff --git a/dev/tests/verification/Resources/PersistedAndXmlEntityArguments.txt b/dev/tests/verification/Resources/PersistedAndXmlEntityArguments.txt new file mode 100644 index 000000000..8ff0d58bd --- /dev/null +++ b/dev/tests/verification/Resources/PersistedAndXmlEntityArguments.txt @@ -0,0 +1,34 @@ +seeInCurrentUrl("/" . $persistedInTest->getCreatedDataByName('urlKey') . ".html?___store=" . msq("uniqueData") . "John"); + } +} diff --git a/dev/tests/verification/Resources/PersistedReplacementTest.txt b/dev/tests/verification/Resources/PersistedReplacementTest.txt index 2c335c7f0..78cdd5029 100644 --- a/dev/tests/verification/Resources/PersistedReplacementTest.txt +++ b/dev/tests/verification/Resources/PersistedReplacementTest.txt @@ -5,6 +5,7 @@ use Magento\FunctionalTestingFramework\AcceptanceTester; use Magento\FunctionalTestingFramework\DataGenerator\Handlers\DataObjectHandler; use Magento\FunctionalTestingFramework\DataGenerator\Persist\DataPersistenceHandler; use Magento\FunctionalTestingFramework\DataGenerator\Objects\EntityDataObject; +use Magento\FunctionalTestingFramework\DataGenerator\Handlers\CredentialStore; use \Codeception\Util\Locator; use Yandex\Allure\Adapter\Annotation\Features; use Yandex\Allure\Adapter\Annotation\Stories; @@ -37,6 +38,7 @@ class PersistedReplacementTestCest } /** + * @Features({"TestModule"}) * @Parameter(name = "AcceptanceTester", value="$I") * @param AcceptanceTester $I * @return void @@ -51,6 +53,7 @@ class PersistedReplacementTestCest $I->fillField("#selector", "StringBefore " . $createdData->getCreatedDataByName('firstname') . " StringAfter"); $I->fillField("#" . $createdData->getCreatedDataByName('firstname'), "input"); $I->fillField("#" . getenv("MAGENTO_BASE_URL") . "#" . $createdData->getCreatedDataByName('firstname'), "input"); + $I->fillSecretField("#" . CredentialStore::getInstance()->getSecret("SECRET_PARAM") . "#" . $createdData->getCreatedDataByName('firstname'), "input"); $I->dragAndDrop("#" . $createdData->getCreatedDataByName('firstname'), $createdData->getCreatedDataByName('lastname')); $I->conditionalClick($createdData->getCreatedDataByName('lastname'), "#" . $createdData->getCreatedDataByName('firstname'), true); $I->amOnUrl($createdData->getCreatedDataByName('firstname') . ".html"); diff --git a/dev/tests/verification/Resources/PersistenceCustomFieldsTest.txt b/dev/tests/verification/Resources/PersistenceCustomFieldsTest.txt index 53db9bcf8..eb9f2d234 100644 --- a/dev/tests/verification/Resources/PersistenceCustomFieldsTest.txt +++ b/dev/tests/verification/Resources/PersistenceCustomFieldsTest.txt @@ -5,6 +5,7 @@ use Magento\FunctionalTestingFramework\AcceptanceTester; use Magento\FunctionalTestingFramework\DataGenerator\Handlers\DataObjectHandler; use Magento\FunctionalTestingFramework\DataGenerator\Persist\DataPersistenceHandler; use Magento\FunctionalTestingFramework\DataGenerator\Objects\EntityDataObject; +use Magento\FunctionalTestingFramework\DataGenerator\Handlers\CredentialStore; use \Codeception\Util\Locator; use Yandex\Allure\Adapter\Annotation\Features; use Yandex\Allure\Adapter\Annotation\Stories; @@ -49,6 +50,7 @@ class PersistenceCustomFieldsTestCest } /** + * @Features({"TestModule"}) * @Parameter(name = "AcceptanceTester", value="$I") * @param AcceptanceTester $I * @return void diff --git a/dev/tests/verification/Resources/SectionReplacementTest.txt b/dev/tests/verification/Resources/SectionReplacementTest.txt index 07a8e7400..6fdef9083 100644 --- a/dev/tests/verification/Resources/SectionReplacementTest.txt +++ b/dev/tests/verification/Resources/SectionReplacementTest.txt @@ -5,6 +5,7 @@ use Magento\FunctionalTestingFramework\AcceptanceTester; use Magento\FunctionalTestingFramework\DataGenerator\Handlers\DataObjectHandler; use Magento\FunctionalTestingFramework\DataGenerator\Persist\DataPersistenceHandler; use Magento\FunctionalTestingFramework\DataGenerator\Objects\EntityDataObject; +use Magento\FunctionalTestingFramework\DataGenerator\Handlers\CredentialStore; use \Codeception\Util\Locator; use Yandex\Allure\Adapter\Annotation\Features; use Yandex\Allure\Adapter\Annotation\Stories; @@ -20,6 +21,7 @@ use Yandex\Allure\Adapter\Annotation\TestCaseId; class SectionReplacementTestCest { /** + * @Features({"TestModule"}) * @Parameter(name = "AcceptanceTester", value="$I") * @param AcceptanceTester $I * @return void @@ -62,5 +64,7 @@ class SectionReplacementTestCest $I->click("#stringLiteral1-" . $createdData->getCreatedDataByName('firstname') . " .{$data}"); $I->click("#stringLiteral1-" . $createdData->getCreatedDataByName('firstname') . " ." . msq("uniqueData") . "John"); $I->click("#stringLiteral1-" . $createdData->getCreatedDataByName('firstname') . " .Doe" . msq("uniqueData")); + $I->click("#element .1#element .2"); + $I->click("#element .1#element .{$data}"); } } diff --git a/dev/tests/verification/Resources/SkippedTest.txt b/dev/tests/verification/Resources/SkippedTest.txt new file mode 100644 index 000000000..b00fda574 --- /dev/null +++ b/dev/tests/verification/Resources/SkippedTest.txt @@ -0,0 +1,38 @@ +skip("This test is skipped due to the following issues:\nSkippedValue"); + } +} diff --git a/dev/tests/verification/Resources/SkippedTestNoIssues.txt b/dev/tests/verification/Resources/SkippedTestNoIssues.txt new file mode 100644 index 000000000..8d22ca415 --- /dev/null +++ b/dev/tests/verification/Resources/SkippedTestNoIssues.txt @@ -0,0 +1,39 @@ +skip("This test is skipped due to the following issues:\nNo issues have been specified."); + } +} diff --git a/dev/tests/verification/Resources/SkippedTestTwoIssues.txt b/dev/tests/verification/Resources/SkippedTestTwoIssues.txt new file mode 100644 index 000000000..cd140e440 --- /dev/null +++ b/dev/tests/verification/Resources/SkippedTestTwoIssues.txt @@ -0,0 +1,38 @@ +skip("This test is skipped due to the following issues:\nSkippedValue\nSecondSkippedValue"); + } +} diff --git a/dev/tests/verification/Resources/functionalSuiteHooks.txt b/dev/tests/verification/Resources/functionalSuiteHooks.txt new file mode 100644 index 000000000..8ec37451c --- /dev/null +++ b/dev/tests/verification/Resources/functionalSuiteHooks.txt @@ -0,0 +1,112 @@ +currentTestRun++; + $this->executePreConditions(); + + if ($this->preconditionFailure != null) { + //if our preconditions fail, we need to mark all the tests as incomplete. + $e->getTest()->getMetadata()->setIncomplete($this->preconditionFailure); + } + } + + + private function executePreConditions() + { + if ($this->currentTestRun == 1) { + print sprintf(self::$HOOK_EXECUTION_INIT, "before"); + + try { + $webDriver = $this->getModule('\Magento\FunctionalTestingFramework\Module\MagentoWebDriver'); + + // close any open sessions + if ($webDriver->webDriver != null) { + $webDriver->webDriver->close(); + $webDriver->webDriver = null; + } + + // initialize the webdriver session + $webDriver->_initializeSession(); + $webDriver->amOnPage("some.url"); + $createFields['someKey'] = "dataHere"; + $createThis = DataObjectHandler::getInstance()->getObject("createThis"); + $this->create = new DataPersistenceHandler($createThis, [], $createFields); + $this->create->createEntity(); + $webDriver->see("John", msq("uniqueData") . "John"); + + // reset configuration and close session + $this->getModule('\Magento\FunctionalTestingFramework\Module\MagentoWebDriver')->_resetConfig(); + $webDriver->webDriver->close(); + $webDriver->webDriver = null; + } catch (\Exception $exception) { + $this->preconditionFailure = $exception->getMessage(); + } + + print sprintf(self::$HOOK_EXECUTION_END, "before"); + } + } + + public function _after(\Codeception\Event\TestEvent $e) + { + $this->executePostConditions(); + } + + + private function executePostConditions() + { + if ($this->currentTestRun == $this->testCount) { + print sprintf(self::$HOOK_EXECUTION_INIT, "after"); + + try { + $webDriver = $this->getModule('\Magento\FunctionalTestingFramework\Module\MagentoWebDriver'); + + // close any open sessions + if ($webDriver->webDriver != null) { + $webDriver->webDriver->close(); + $webDriver->webDriver = null; + } + + // initialize the webdriver session + $webDriver->_initializeSession(); + $webDriver->amOnPage("some.url"); + $webDriver->deleteEntityByUrl("deleteThis"); + $webDriver->see("John", msq("uniqueData") . "John"); + + // reset configuration and close session + $this->getModule('\Magento\FunctionalTestingFramework\Module\MagentoWebDriver')->_resetConfig(); + $webDriver->webDriver->close(); + $webDriver->webDriver = null; + } catch (\Exception $exception) { + print $exception->getMessage(); + } + + print sprintf(self::$HOOK_EXECUTION_END, "after"); + } + } +} diff --git a/dev/tests/verification/TestModule/ActionGroup/BasicActionGroup.xml b/dev/tests/verification/TestModule/ActionGroup/BasicActionGroup.xml index 94e7a6b60..5b5ddf775 100644 --- a/dev/tests/verification/TestModule/ActionGroup/BasicActionGroup.xml +++ b/dev/tests/verification/TestModule/ActionGroup/BasicActionGroup.xml @@ -19,6 +19,15 @@ + + + + + + + + + @@ -59,4 +68,50 @@ grabProducts + + + + + + + + {{count}} + grabProducts + + + + + + + + + + + + {{otherCount}} + grabProducts + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/dev/tests/verification/TestModule/ActionGroup/FunctionalActionGroup.xml b/dev/tests/verification/TestModule/ActionGroup/FunctionalActionGroup.xml index 843c66739..5e1a20f96 100644 --- a/dev/tests/verification/TestModule/ActionGroup/FunctionalActionGroup.xml +++ b/dev/tests/verification/TestModule/ActionGroup/FunctionalActionGroup.xml @@ -50,4 +50,21 @@ + + + + + + + + + + + + + + + + + diff --git a/dev/tests/verification/TestModule/ActionGroup/MergeFunctionalActionGroup.xml b/dev/tests/verification/TestModule/ActionGroup/MergeFunctionalActionGroup.xml index bd478e645..7d8585772 100644 --- a/dev/tests/verification/TestModule/ActionGroup/MergeFunctionalActionGroup.xml +++ b/dev/tests/verification/TestModule/ActionGroup/MergeFunctionalActionGroup.xml @@ -13,4 +13,16 @@ + + + + + + + + + + + + diff --git a/dev/tests/verification/TestModule/Data/ExtendedData.xml b/dev/tests/verification/TestModule/Data/ExtendedData.xml new file mode 100644 index 000000000..74fa921d0 --- /dev/null +++ b/dev/tests/verification/TestModule/Data/ExtendedData.xml @@ -0,0 +1,23 @@ + + + + + + name + prename + postname + + + otherName + extendName + item + postnameExtend + value + + diff --git a/dev/tests/verification/TestModule/Page/SamplePage.xml b/dev/tests/verification/TestModule/Page/SamplePage.xml index b4bf97896..bf8f99615 100644 --- a/dev/tests/verification/TestModule/Page/SamplePage.xml +++ b/dev/tests/verification/TestModule/Page/SamplePage.xml @@ -8,25 +8,25 @@ - +
    - +
    - +
    - +
    - +
    - +
    - +
    diff --git a/dev/tests/verification/TestModule/Test/ActionGroupFunctionalTest.xml b/dev/tests/verification/TestModule/Test/ActionGroupFunctionalTest.xml index ad372b838..8c24794e1 100644 --- a/dev/tests/verification/TestModule/Test/ActionGroupFunctionalTest.xml +++ b/dev/tests/verification/TestModule/Test/ActionGroupFunctionalTest.xml @@ -11,7 +11,6 @@ - <group value="functional"/> <features value="Action Group Functional Cest"/> <stories value="MQE-433"/> @@ -27,7 +26,6 @@ <test name="ActionGroupWithDataTest"> <annotations> <severity value="CRITICAL"/> - <title value="A Functional Cest"/> <group value="functional"/> <features value="Action Group Functional Cest"/> <stories value="MQE-433"/> @@ -46,7 +44,6 @@ <test name="ActionGroupWithDataOverrideTest"> <annotations> <severity value="CRITICAL"/> - <title value="A Functional Cest"/> <group value="functional"/> <features value="Action Group Functional Cest"/> <stories value="MQE-433"/> @@ -67,7 +64,6 @@ <test name="ActionGroupWithNoDefaultTest"> <annotations> <severity value="CRITICAL"/> - <title value="A Functional Cest"/> <group value="functional"/> <features value="Action Group Functional Cest"/> <stories value="MQE-433"/> @@ -88,7 +84,6 @@ <test name="ActionGroupWithPersistedData"> <annotations> <severity value="CRITICAL"/> - <title value="A Functional Cest"/> <group value="functional"/> <features value="Action Group Functional Cest"/> <stories value="MQE-433"/> @@ -108,7 +103,6 @@ <test name="ActionGroupWithTopLevelPersistedData"> <annotations> <severity value="CRITICAL"/> - <title value="A Functional Cest"/> <group value="functional"/> <features value="Action Group Functional Cest"/> <stories value="MQE-433"/> @@ -127,7 +121,6 @@ <test name="MultipleActionGroupsTest"> <annotations> <severity value="CRITICAL"/> - <title value="A Functional Cest"/> <group value="functional"/> <features value="Action Group Functional Cest"/> <stories value="MQE-433"/> @@ -149,7 +142,6 @@ <test name="MergedActionGroupTest"> <annotations> <severity value="CRITICAL"/> - <title value="A Functional Cest"/> <group value="functional"/> <features value="Action Group Functional Cest"/> <stories value="MQE-433"/> @@ -168,7 +160,6 @@ <test name="ArgumentWithSameNameAsElement"> <annotations> <severity value="CRITICAL"/> - <title value="A Functional Cest"/> <group value="functional"/> <features value="Action Group Functional Cest"/> <stories value="MQE-433"/> @@ -182,4 +173,15 @@ <actionGroup ref="FunctionalActionGroup" stepKey="afterGroup"/> </after> </test> + <test name="ActionGroupMergedViaInsertBefore"> + <actionGroup ref="FunctionalActionGroupForMassMergeBefore" stepKey="keyone"/> + </test> + <test name="ActionGroupMergedViaInsertAfter"> + <actionGroup ref="FunctionalActionGroupForMassMergeAfter" stepKey="keyone"/> + </test> + <test name="PersistedAndXmlEntityArguments"> + <actionGroup ref="FunctionalActionGroupWithXmlAndPersistedData" stepKey="afterGroup"> + <argument name="persistedData" value="$persistedInTest$"/> + </actionGroup> + </test> </tests> diff --git a/dev/tests/verification/TestModule/Test/ActionGroupTest.xml b/dev/tests/verification/TestModule/Test/ActionGroupTest.xml index 94b9e74dd..ccace672b 100644 --- a/dev/tests/verification/TestModule/Test/ActionGroupTest.xml +++ b/dev/tests/verification/TestModule/Test/ActionGroupTest.xml @@ -116,8 +116,42 @@ </test> <test name="ActionGroupUsingNestedArgument"> - <actionGroup ref="actionGroupWithNestedArgument" stepKey="actionGroup"> + <actionGroup ref="ActionGroupToExtend" stepKey="actionGroup"> <argument name="count" value="99"/> </actionGroup> </test> + + <test name="ActionGroupToExtend"> + <actionGroup ref="ActionGroupToExtend" stepKey="actionGroup"> + <argument name="count" value="99"/> + </actionGroup> + </test> + + <test name="ExtendedActionGroup"> + <actionGroup ref="extendTestActionGroup" stepKey="actionGroup"> + <argument name="count" value="99"/> + <argument name="otherCount" value="8000"/> + </actionGroup> + </test> + + <test name="ExtendedRemoveActionGroup"> + <actionGroup ref="extendRemoveTestActionGroup" stepKey="actionGroup"/> + </test> + + <test name="ActionGroupUsingCreateData"> + <before> + <actionGroup ref="actionGroupWithCreateData" stepKey="Key1"/> + </before> + </test> + + <test name="ActionGroupContainsStepKeyInArgText"> + <before> + <actionGroup ref="actionGroupContainsStepKeyInArgValue" stepKey="actionGroup"> + <argument name="sameStepKeyAsArg" value="arg1"/> + </actionGroup> + </before> + <actionGroup ref="actionGroupContainsStepKeyInArgValue" stepKey="actionGroup"> + <argument name="sameStepKeyAsArg" value="arg1"/> + </actionGroup> + </test> </tests> diff --git a/dev/tests/verification/TestModule/Test/BasicFunctionalTest.xml b/dev/tests/verification/TestModule/Test/BasicFunctionalTest.xml index bd7c23522..0a2676ef6 100644 --- a/dev/tests/verification/TestModule/Test/BasicFunctionalTest.xml +++ b/dev/tests/verification/TestModule/Test/BasicFunctionalTest.xml @@ -60,13 +60,14 @@ <executeJS function="someJSFunction" stepKey="executeJSKey1"/> <fillField selector=".functionalTestSelector" userInput="someInput" stepKey="fillFieldKey1" /> <fillField selector=".functionalTestSelector" userInput="0" stepKey="fillFieldKey2" /> + <generateDate date="Now" format="H:i:s" stepKey="generateDateKey"/> <grabAttributeFrom selector=".functionalTestSelector" userInput="someInput" stepKey="grabAttributeFromKey1" /> <grabCookie userInput="grabCookieInput" parameterArray="['domain' => 'www.google.com']" stepKey="grabCookieKey1" /> <grabFromCurrentUrl regex="/grabCurrentUrl" stepKey="grabFromCurrentUrlKey1" /> <grabMultiple selector=".functionalTestSelector" stepKey="grabMultipleKey1" /> <grabTextFrom selector=".functionalTestSelector" stepKey="grabTextFromKey1" /> <grabValueFrom selector=".functionalTestSelector" stepKey="grabValueFromKey1" /> - <magentoCLI command="maintenance:enable" stepKey="magentoCli1"/> + <magentoCLI command="maintenance:enable" arguments=""stuffHere"" stepKey="magentoCli1"/> <makeScreenshot userInput="screenShotInput" stepKey="makeScreenshotKey1"/> <maximizeWindow stepKey="maximizeWindowKey1"/> <moveBack stepKey="moveBackKey1"/> @@ -119,4 +120,14 @@ <waitForJS function="someJsFunction" time="30" stepKey="waitForJSKey1" /> <waitForText selector=".functionalTestSelector" userInput="someInput" time="30" stepKey="waitForText1"/> </test> + <test name="MergeMassViaInsertBefore"> + <fillField selector="#foo" userInput="foo" stepKey="fillField1"/> + <fillField selector="#bar" userInput="bar" stepKey="fillField2"/> + <fillField selector="#baz" userInput="baz" stepKey="fillField3"/> + </test> + <test name="MergeMassViaInsertAfter"> + <fillField selector="#foo" userInput="foo" stepKey="fillField1"/> + <fillField selector="#bar" userInput="bar" stepKey="fillField2"/> + <fillField selector="#baz" userInput="baz" stepKey="fillField3"/> + </test> </tests> \ No newline at end of file diff --git a/dev/tests/verification/TestModule/Test/DataReplacementTest.xml b/dev/tests/verification/TestModule/Test/DataReplacementTest.xml index f12c70b26..ca3577373 100644 --- a/dev/tests/verification/TestModule/Test/DataReplacementTest.xml +++ b/dev/tests/verification/TestModule/Test/DataReplacementTest.xml @@ -10,6 +10,7 @@ xsi:noNamespaceSchemaLocation="../../../../../src/Magento/FunctionalTestingFramework/Test/etc/testSchema.xsd"> <test name="DataReplacementTest"> <fillField stepKey="inputReplace" selector="#selector" userInput="StringBefore {{simpleData.firstname}} StringAfter"/> + <seeCurrentUrlMatches stepKey="seeInRegex" regex="~\/{{simpleData.firstname}}~i"/> <fillField stepKey="selectorReplace" selector="#{{simpleData.firstname}}" userInput="input"/> <dragAndDrop stepKey="selector12Replace" selector1="#{{simpleData.firstname}}" selector2="{{simpleData.lastname}}"/> <conditionalClick stepKey="dependentSelectorReplace" dependentSelector="#{{simpleData.firstname}}" selector="{{simpleData.lastname}}" visible="true"/> @@ -37,5 +38,6 @@ </selectMultipleOptions> <fillField stepKey="insertZero" selector=".selector" userInput="{{simpleData.favoriteIndex}}"/> + <magentoCLI stepKey="insertCommand" command="do something {{uniqueData.lastname}} with uniqueness"/> </test> </tests> \ No newline at end of file diff --git a/dev/tests/verification/TestModule/Test/ExecuteJsTest.xml b/dev/tests/verification/TestModule/Test/ExecuteJsTest.xml new file mode 100644 index 000000000..2429fa484 --- /dev/null +++ b/dev/tests/verification/TestModule/Test/ExecuteJsTest.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../src/Magento/FunctionalTestingFramework/Test/etc/testSchema.xsd"> + <test name="ExecuteJsEscapingTest"> + <executeJS function="return $javascriptVariable" stepKey="javaVariableEscape"/> + <executeJS function="return {$doNotEscape}" stepKey="mftfVariableNotEscaped"/> + </test> +</tests> diff --git a/dev/tests/verification/TestModule/Test/ExtendedDataTest.xml b/dev/tests/verification/TestModule/Test/ExtendedDataTest.xml new file mode 100644 index 000000000..21f06eb3a --- /dev/null +++ b/dev/tests/verification/TestModule/Test/ExtendedDataTest.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../src/Magento/FunctionalTestingFramework/Test/etc/testSchema.xsd"> + <test name="ExtendParentDataTest"> + <createData entity="extendParentData" stepKey="simpleDataKey"/> + <searchAndMultiSelectOption selector="#selector" parameterArray="[{{extendParentData.name}}]" stepKey="getName"/> + <searchAndMultiSelectOption selector="#selector" parameterArray="[{{extendParentData.nameExtend}}]" stepKey="getNameExtend"/> + <searchAndMultiSelectOption selector="#selector" parameterArray="[{{extendParentData.uniqueNamePost}}]" stepKey="emptyPost"/> + <searchAndMultiSelectOption selector="#selector" parameterArray="[{{extendParentData.uniqueNamePre}}]" stepKey="originalPre"/> + <searchAndMultiSelectOption selector="#selector" parameterArray="[{{extendParentData.anotherUniqueNamePre}}]" stepKey="secondUniquePre"/> + </test> +</tests> diff --git a/dev/tests/verification/TestModule/Test/ExtendedFunctionalTest.xml b/dev/tests/verification/TestModule/Test/ExtendedFunctionalTest.xml new file mode 100644 index 000000000..602b9a02c --- /dev/null +++ b/dev/tests/verification/TestModule/Test/ExtendedFunctionalTest.xml @@ -0,0 +1,132 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../src/Magento/FunctionalTestingFramework/Test/etc/testSchema.xsd"> + <test name="ParentExtendedTest"> + <annotations> + <severity value="AVERAGE"/> + <title value="ParentExtendedTest"/> + <group value="Parent"/> + <features value="Parent"/> + <stories value="Parent"/> + </annotations> + <before> + <amOnPage url="/beforeUrl" stepKey="beforeAmOnPageKey"/> + </before> + <after> + <amOnPage url="/afterUrl" stepKey="afterAmOnPageKey"/> + </after> + <comment stepKey="basicCommentWithNoData" userInput="Parent Comment"/> + </test> + + <test name="ChildExtendedTestReplace" extends="ParentExtendedTest"> + <annotations> + <severity value="MINOR"/> + <title value="ChildExtendedTestReplace"/> + <group value="Child"/> + <features value="Child"/> + <stories value="Child"/> + </annotations> + <comment stepKey="basicCommentWithNoData" userInput="Different Input"/> + </test> + + <test name="ChildExtendedTestReplaceHook" extends="ParentExtendedTest"> + <annotations> + <severity value="MINOR"/> + <title value="ChildExtendedTestReplaceHook"/> + <group value="Child"/> + <features value="Child"/> + <stories value="Child"/> + </annotations> + <before> + <amOnPage url="/slightlyDifferentBeforeUrl" stepKey="beforeAmOnPageKey"/> + </before> + </test> + + <test name="ChildExtendedTestMerging" extends="ParentExtendedTest"> + <annotations> + <severity value="MINOR"/> + <title value="ChildExtendedTestMerging"/> + <group value="Child"/> + <features value="Child"/> + <stories value="Child"/> + </annotations> + <before> + <amOnPage url="/firstUrl" stepKey="firstBeforeAmOnPageKey" before="beforeAmOnPageKey"/> + <amOnPage url="/lastUrl" stepKey="lastBefore" after="beforeAmOnPageKey"/> + </before> + <comment stepKey="lastStepKey" userInput="Last Comment"/> + <comment stepKey="beforeBasicCommentWithNoData" userInput="Before Comment" before="basicCommentWithNoData"/> + <comment stepKey="afterBasicCommentWithNoData" userInput="After Comment" after="basicCommentWithNoData"/> + </test> + + <test name="ChildExtendedTestRemoveAction" extends="ParentExtendedTest"> + <annotations> + <severity value="CRITICAL"/> + <title value="ChildExtendedTestRemoveAction"/> + <group value="Child"/> + <features value="Child"/> + <stories value="Child"/> + </annotations> + <remove keyForRemoval="basicCommentWithNoData"/> + </test> + + <test name="ParentExtendedTestNoHooks"> + <annotations> + <severity value="AVERAGE"/> + <title value="ParentExtendedTestNoHooks"/> + <group value="Parent"/> + <features value="Parent"/> + <stories value="Parent"/> + </annotations> + <comment stepKey="basicCommentWithNoData" userInput="Parent Comment"/> + </test> + + <test name="ChildExtendedTestAddHooks"> + <annotations> + <severity value="AVERAGE"/> + <title value="ChildExtendedTestAddHooks"/> + <group value="Parent"/> + <features value="Parent"/> + <stories value="Parent"/> + </annotations> + <before> + <amOnPage url="/beforeUrl" stepKey="beforeAmOnPageKey"/> + </before> + <after> + <amOnPage url="/afterUrl" stepKey="afterAmOnPageKey"/> + </after> + </test> + + <test name="ChildExtendedTestRemoveHookAction" extends="ParentExtendedTest"> + <annotations> + <severity value="CRITICAL"/> + <title value="ChildExtendedTestRemoveHookAction"/> + <group value="Child"/> + <features value="Child"/> + <stories value="Child"/> + </annotations> + <before> + <remove keyForRemoval="beforeAmOnPageKey"/> + </before> + </test> + + <test name="ChildExtendedTestNoParent" extends="ThisTestDoesNotExist"> + <annotations> + <severity value="CRITICAL"/> + <title value="ChildExtendedTestNoParent"/> + <group value="Child"/> + <features value="Child"/> + <stories value="Child"/> + </annotations> + <before> + <remove keyForRemoval="beforeAmOnPageKey"/> + </before> + </test> +</tests> \ No newline at end of file diff --git a/dev/tests/verification/TestModule/Test/MergeFunctionalTest.xml b/dev/tests/verification/TestModule/Test/MergeFunctionalTest.xml index 75c62b36f..5189b8cf4 100644 --- a/dev/tests/verification/TestModule/Test/MergeFunctionalTest.xml +++ b/dev/tests/verification/TestModule/Test/MergeFunctionalTest.xml @@ -11,7 +11,7 @@ <test name="BasicMergeTest"> <annotations> <severity value="CRITICAL"/> - <title value="A Functional Cest"/> + <title value="BasicMergeTest"/> <group value="functional"/> <features value="Merge Functional Cest"/> <stories value="MQE-433"/> @@ -31,7 +31,7 @@ <test name="MergedReferencesTest"> <annotations> <severity value="CRITICAL"/> - <title value="A Functional Cest"/> + <title value="MergedReferencesTest"/> <group value="functional"/> <features value="Merge Functional Cest"/> <stories value="MQE-433"/> @@ -45,4 +45,17 @@ <fillField stepKey="fillField1" selector="{{SampleSection.mergeElement}}" userInput="{{DefaultPerson.mergedField}}"/> <fillField stepKey="fillField2" selector="{{SampleSection.newElement}}" userInput="{{DefaultPerson.newField}}" /> </test> + <test name="MergeMassViaInsertBefore" insertBefore="fillField2"> + <click stepKey="clickOne" selector="#mergeOne"/> + <click stepKey="clickTwo" selector="#mergeTwo"/> + <click stepKey="clickThree" selector="#mergeThree"/> + </test> + <test name="MergeMassViaInsertAfter" insertAfter="fillField2"> + <click stepKey="clickOne" selector="#mergeOne"/> + <click stepKey="clickTwo" selector="#mergeTwo"/> + <click stepKey="clickThree" selector="#mergeThree"/> + </test> + <test name="MergeSkip"> + <comment userInput="ThisTestShouldBeSkipped" stepKey="skipComment"/> + </test> </tests> diff --git a/dev/tests/verification/TestModule/Test/PageReplacementTest.xml b/dev/tests/verification/TestModule/Test/PageReplacementTest.xml index acdbbb798..b6adee5d7 100644 --- a/dev/tests/verification/TestModule/Test/PageReplacementTest.xml +++ b/dev/tests/verification/TestModule/Test/PageReplacementTest.xml @@ -22,7 +22,4 @@ <amOnPage stepKey="oneParamAdminPageString" url="{{AdminOneParamPage.url('StringLiteral')}}"/> <amOnUrl stepKey="onExternalPage" url="{{ExternalPage.url}}"/> </test> - <test name="ExternalPageTestBadReference"> - <amOnPage stepKey="onExternalPage" url="{{ExternalPage.url}}"/> - </test> </tests> \ No newline at end of file diff --git a/dev/tests/verification/TestModule/Test/PersistedReplacementTest.xml b/dev/tests/verification/TestModule/Test/PersistedReplacementTest.xml index 26885fda4..8fc8fd19a 100644 --- a/dev/tests/verification/TestModule/Test/PersistedReplacementTest.xml +++ b/dev/tests/verification/TestModule/Test/PersistedReplacementTest.xml @@ -17,6 +17,7 @@ <fillField stepKey="inputReplace" selector="#selector" userInput="StringBefore $createdData.firstname$ StringAfter"/> <fillField stepKey="selectorReplace" selector="#$createdData.firstname$" userInput="input"/> <fillField stepKey="selectorReplace2" selector="#{{_ENV.MAGENTO_BASE_URL}}#$createdData.firstname$" userInput="input"/> + <fillField stepKey="selectorReplace3" selector="#{{_CREDS.SECRET_PARAM}}#$createdData.firstname$" userInput="input"/> <dragAndDrop stepKey="selector12Replace" selector1="#$createdData.firstname$" selector2="$createdData.lastname$"/> <conditionalClick stepKey="dependentSelectorReplace" dependentSelector="#$createdData.firstname$" selector="$createdData.lastname$" visible="true"/> <amOnUrl stepKey="urlReplace" url="$createdData.firstname$.html"/> diff --git a/dev/tests/verification/TestModule/Test/SectionReplacementTest.xml b/dev/tests/verification/TestModule/Test/SectionReplacementTest.xml index 93f7e477e..9daef72cd 100644 --- a/dev/tests/verification/TestModule/Test/SectionReplacementTest.xml +++ b/dev/tests/verification/TestModule/Test/SectionReplacementTest.xml @@ -43,10 +43,12 @@ <click stepKey="selectorReplaceThreeParamVariable" selector="{{SampleSection.threeParamElement({$data1}, {$data2}, {$data3})}}"/> <click stepKey="selectorReplaceThreeParamVariableOneDupe" selector="{{SampleSection.threeOneDuplicateParamElement(simpleData.firstname, simpleData.lastname, simpleData.middlename)}}"/> - <click stepKey="selectorReplaceThreeParamMixed1" selector="{{SampleSection.threeParamElement('stringLiteral1', $createdData.firstname$, simpleData.firstname)}}"/> <click stepKey="selectorReplaceThreeParamMixed2" selector="{{SampleSection.threeParamElement('stringLiteral1', $createdData.firstname$, {$data})}}"/> <click stepKey="selectorReplaceThreeParamMixedMSQPrefix" selector="{{SampleSection.threeParamElement('stringLiteral1', $createdData.firstname$, uniqueData.firstname)}}"/> <click stepKey="selectorReplaceThreeParamMixedMSQSuffix" selector="{{SampleSection.threeParamElement('stringLiteral1', $createdData.firstname$, uniqueData.lastname)}}"/> + + <click stepKey="selectorReplaceTwoParamElements" selector="{{SampleSection.oneParamElement('1')}}{{SampleSection.oneParamElement('2')}}"/> + <click stepKey="selectorReplaceTwoParamMixedTypes" selector="{{SampleSection.oneParamElement('1')}}{{SampleSection.oneParamElement({$data})}}"/> </test> </tests> diff --git a/dev/tests/verification/TestModule/Test/SkippedTest.xml b/dev/tests/verification/TestModule/Test/SkippedTest.xml new file mode 100644 index 000000000..bceb619f8 --- /dev/null +++ b/dev/tests/verification/TestModule/Test/SkippedTest.xml @@ -0,0 +1,43 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../src/Magento/FunctionalTestingFramework/Test/etc/testSchema.xsd"> + <test name="SkippedTest"> + <annotations> + <stories value="skipped"/> + <title value="skippedTest"/> + <description value=""/> + <severity value="AVERAGE"/> + <skip> + <issueId value="SkippedValue"/> + </skip> + </annotations> + </test> + <test name="SkippedTestTwoIssues"> + <annotations> + <stories value="skippedMultiple"/> + <title value="skippedMultipleIssuesTest"/> + <description value=""/> + <severity value="AVERAGE"/> + <skip> + <issueId value="SkippedValue"/> + <issueId value="SecondSkippedValue"/> + </skip> + </annotations> + </test> + <test name="SkippedTestNoIssues"> + <annotations> + <stories value="skippedNo"/> + <title value="skippedNoIssuesTest"/> + <description value=""/> + <severity value="AVERAGE"/> + <group value="skip"/> + </annotations> + </test> +</tests> diff --git a/dev/tests/verification/TestModuleMerged/Test/MergeFunctionalTest.xml b/dev/tests/verification/TestModuleMerged/Test/MergeFunctionalTest.xml index aa8cb375d..912854cc4 100644 --- a/dev/tests/verification/TestModuleMerged/Test/MergeFunctionalTest.xml +++ b/dev/tests/verification/TestModuleMerged/Test/MergeFunctionalTest.xml @@ -26,4 +26,11 @@ <click stepKey="step10" selector="#step10MergedInResult"/> <actionGroup ref="FunctionalActionGroupWithData" stepKey="step8Merge" after="step7Merge"/> </test> + <test name="MergeSkip"> + <annotations> + <skip> + <issueId value="Issue5"/> + </skip> + </annotations> + </test> </tests> diff --git a/dev/tests/verification/Tests/ActionGroupGenerationTest.php b/dev/tests/verification/Tests/ActionGroupGenerationTest.php index a26e9ad87..d7b57baed 100644 --- a/dev/tests/verification/Tests/ActionGroupGenerationTest.php +++ b/dev/tests/verification/Tests/ActionGroupGenerationTest.php @@ -118,4 +118,70 @@ public function testActionGroupWithNestedArgument() { $this->generateAndCompareTest('ActionGroupUsingNestedArgument'); } + + /** + * Test generation of a test referencing an action group that uses stepKey references (grabFrom/CreateData) + * + * @throws \Exception + * @throws \Magento\FunctionalTestingFramework\Exceptions\TestReferenceException + */ + public function testActionGroupWithPersistedAndXmlEntityArguments() + { + $this->generateAndCompareTest('PersistedAndXmlEntityArguments'); + } + + /** + * Test generation of a test referencing an action group which is referenced by another action group + * + * @throws \Exception + * @throws \Magento\FunctionalTestingFramework\Exceptions\TestReferenceException + */ + public function testActionGroupToExtend() + { + $this->generateAndCompareTest('ActionGroupToExtend'); + } + + /** + * Test generation of a test referencing an action group that references another action group + * + * @throws \Exception + * @throws \Magento\FunctionalTestingFramework\Exceptions\TestReferenceException + */ + public function testExtendedActionGroup() + { + $this->generateAndCompareTest('ExtendedActionGroup'); + } + + /** + * Test generation of a test referencing an action group that references another action group but removes an action + * + * @throws \Exception + * @throws \Magento\FunctionalTestingFramework\Exceptions\TestReferenceException + */ + public function testExtendedRemoveActionGroup() + { + $this->generateAndCompareTest('ExtendedRemoveActionGroup'); + } + + /** + * Test generation of a test referencing an action group that uses stepKey references within the action group + * + * @throws \Exception + * @throws \Magento\FunctionalTestingFramework\Exceptions\TestReferenceException + */ + public function testActionGroupWithCreateData() + { + $this->generateAndCompareTest('ActionGroupUsingCreateData'); + } + + /** + * Test an action group with an arg containing stepKey text + * + * @throws \Exception + * @throws \Magento\FunctionalTestingFramework\Exceptions\TestReferenceException + */ + public function testActionGroupWithArgContainingStepKey() + { + $this->generateAndCompareTest('ActionGroupContainsStepKeyInArgText'); + } } diff --git a/dev/tests/verification/Tests/ActionGroupMergeGenerationTest.php b/dev/tests/verification/Tests/ActionGroupMergeGenerationTest.php index 9176f3955..0361bec75 100644 --- a/dev/tests/verification/Tests/ActionGroupMergeGenerationTest.php +++ b/dev/tests/verification/Tests/ActionGroupMergeGenerationTest.php @@ -108,4 +108,26 @@ public function testArgumentWithSameNameAsElement() { $this->generateAndCompareTest('ArgumentWithSameNameAsElement'); } + + /** + * Test an action group with a merge counterpart that's merged via insertBefore + * + * @throws \Exception + * @throws \Magento\FunctionalTestingFramework\Exceptions\TestReferenceException + */ + public function testMergedActionGroupViaInsertBefore() + { + $this->generateAndCompareTest('ActionGroupMergedViaInsertBefore'); + } + + /** + * Test an action group with a merge counterpart that's merged via insertAfter + * + * @throws \Exception + * @throws \Magento\FunctionalTestingFramework\Exceptions\TestReferenceException + */ + public function testMergedActionGroupViaInsertAfter() + { + $this->generateAndCompareTest('ActionGroupMergedViaInsertAfter'); + } } diff --git a/dev/tests/verification/Tests/ExecuteJsTest.php b/dev/tests/verification/Tests/ExecuteJsTest.php new file mode 100644 index 000000000..67a5f3c6f --- /dev/null +++ b/dev/tests/verification/Tests/ExecuteJsTest.php @@ -0,0 +1,22 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace tests\verification\Tests; + +use tests\util\MftfTestCase; + +class ExecuteJsTest extends MftfTestCase +{ + /** + * Tests escaping of $javascriptVariable => \$javascriptVariable in the executeJs function + * + * @throws \Exception + * @throws \Magento\FunctionalTestingFramework\Exceptions\TestReferenceException + */ + public function testExecuteJsTest() + { + $this->generateAndCompareTest('ExecuteJsEscapingTest'); + } +} diff --git a/dev/tests/verification/Tests/ExtendedDataTest.php b/dev/tests/verification/Tests/ExtendedDataTest.php new file mode 100644 index 000000000..291b6cb7f --- /dev/null +++ b/dev/tests/verification/Tests/ExtendedDataTest.php @@ -0,0 +1,22 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace tests\verification\Tests; + +use tests\util\MftfTestCase; + +class ExtendedDataTest extends MftfTestCase +{ + /** + * Tests flat generation of a hardcoded test file with no external references. + * + * @throws \Exception + * @throws \Magento\FunctionalTestingFramework\Exceptions\TestReferenceException + */ + public function testExtendedParameterArrayGeneration() + { + $this->generateAndCompareTest('ExtendParentDataTest'); + } +} diff --git a/dev/tests/verification/Tests/ExtendedGenerationTest.php b/dev/tests/verification/Tests/ExtendedGenerationTest.php new file mode 100644 index 000000000..d0c9926d0 --- /dev/null +++ b/dev/tests/verification/Tests/ExtendedGenerationTest.php @@ -0,0 +1,99 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace tests\verification\Tests; + +use tests\util\MftfTestCase; + +class ExtendedGenerationTest extends MftfTestCase +{ + /** + * Tests flat generation of a test that is referenced by another test + * + * @throws \Exception + * @throws \Magento\FunctionalTestingFramework\Exceptions\TestReferenceException + */ + public function testExtendedParentTestGeneration() + { + $this->generateAndCompareTest('ParentExtendedTest'); + } + + /** + * Tests generation of test that extends based on another test when replacing actions + * + * @throws \Exception + * @throws \Magento\FunctionalTestingFramework\Exceptions\TestReferenceException + */ + public function testExtendedTestGenerationReplaceStepKey() + { + $this->generateAndCompareTest('ChildExtendedTestReplace'); + } + + /** + * Tests generation of test that extends based on another test when replacing actions in hooks + * + * @throws \Exception + * @throws \Magento\FunctionalTestingFramework\Exceptions\TestReferenceException + */ + public function testExtendedTestGenerationReplaceHook() + { + $this->generateAndCompareTest('ChildExtendedTestReplaceHook'); + } + + /** + * Tests generation of test that extends based on another test when merging actions + * + * @throws \Exception + * @throws \Magento\FunctionalTestingFramework\Exceptions\TestReferenceException + */ + public function testExtendedTestGenerationMergeActions() + { + $this->generateAndCompareTest('ChildExtendedTestMerging'); + } + + /** + * Tests generation of test that extends based on another test when adding hooks + * + * @throws \Exception + * @throws \Magento\FunctionalTestingFramework\Exceptions\TestReferenceException + */ + public function testExtendedTestGenerationAddHooks() + { + $this->generateAndCompareTest('ChildExtendedTestAddHooks'); + } + + /** + * Tests generation of test that extends based on another test when removing an action + * + * @throws \Exception + * @throws \Magento\FunctionalTestingFramework\Exceptions\TestReferenceException + */ + public function testExtendedTestGenerationRemoveAction() + { + $this->generateAndCompareTest('ChildExtendedTestRemoveAction'); + } + + /** + * Tests generation of test that extends based on another test when removing an action + * + * @throws \Exception + * @throws \Magento\FunctionalTestingFramework\Exceptions\TestReferenceException + */ + public function testExtendedTestGenerationRemoveHookAction() + { + $this->generateAndCompareTest('ChildExtendedTestRemoveHookAction'); + } + + /** + * Tests generation of test that attemps to extend a test that doesn't exist + * + * @throws \Exception + * @throws \Magento\FunctionalTestingFramework\Exceptions\TestReferenceException + */ + public function testExtendedTestGenerationNoParent() + { + $this->generateAndCompareTest('ChildExtendedTestNoParent'); + } +} diff --git a/dev/tests/verification/Tests/MergedGenerationTest.php b/dev/tests/verification/Tests/MergedGenerationTest.php index fe2990b76..c336cd8d8 100644 --- a/dev/tests/verification/Tests/MergedGenerationTest.php +++ b/dev/tests/verification/Tests/MergedGenerationTest.php @@ -40,4 +40,37 @@ public function testParsedArray() $entity = DataObjectHandler::getInstance()->getObject('testEntity'); $this->assertCount(3, $entity->getLinkedEntities()); } + + /** + * Tests generation of a test merge file via insertBefore + * + * @throws \Exception + * @throws \Magento\FunctionalTestingFramework\Exceptions\TestReferenceException + */ + public function testMergeMassViaInsertBefore() + { + $this->generateAndCompareTest('MergeMassViaInsertBefore'); + } + + /** + * Tests generation of a test merge file via insertBefore + * + * @throws \Exception + * @throws \Magento\FunctionalTestingFramework\Exceptions\TestReferenceException + */ + public function testMergeMassViaInsertAfter() + { + $this->generateAndCompareTest('MergeMassViaInsertAfter'); + } + + /** + * Tests generation of a test skipped in merge. + * + * @throws \Exception + * @throws \Magento\FunctionalTestingFramework\Exceptions\TestReferenceException + */ + public function testMergeSkipGeneration() + { + $this->generateAndCompareTest('MergeSkip'); + } } diff --git a/dev/tests/verification/Tests/SchemaValidationTest.php b/dev/tests/verification/Tests/SchemaValidationTest.php new file mode 100644 index 000000000..86828e9a5 --- /dev/null +++ b/dev/tests/verification/Tests/SchemaValidationTest.php @@ -0,0 +1,42 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace tests\verification\Tests; + +use Magento\FunctionalTestingFramework\Config\MftfApplicationConfig; +use tests\util\MftfTestCase; +use AspectMock\Test as AspectMock; + +class SchemaValidationTest extends MftfTestCase +{ + /** + * Test generation of a test referencing an action group with no arguments + * + * @throws \Exception + * @throws \Magento\FunctionalTestingFramework\Exceptions\TestReferenceException + */ + public function testInvalidTestSchema() + { + AspectMock::double(MftfApplicationConfig::class, ['debugEnabled' => true]); + $testFile = ['testFile.xml' => "<tests><test name='testName'><annotations>a</annotations></test></tests>"]; + $expectedError = TESTS_MODULE_PATH . + DIRECTORY_SEPARATOR . + "TestModule" . + DIRECTORY_SEPARATOR . + "Test" . + DIRECTORY_SEPARATOR . + "testFile.xml"; + $this->validateSchemaErrorWithTest($testFile, 'Test', $expectedError); + } + + /** + * After method functionality + * @return void + */ + protected function tearDown() + { + AspectMock::clean(); + } +} diff --git a/dev/tests/verification/Tests/SkippedGenerationTest.php b/dev/tests/verification/Tests/SkippedGenerationTest.php new file mode 100644 index 000000000..6a65df8e3 --- /dev/null +++ b/dev/tests/verification/Tests/SkippedGenerationTest.php @@ -0,0 +1,44 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace tests\verification\Tests; + +use tests\util\MftfTestCase; + +class SkippedGenerationTest extends MftfTestCase +{ + /** + * Tests skipped test generation. + * + * @throws \Exception + * @throws \Magento\FunctionalTestingFramework\Exceptions\TestReferenceException + */ + public function testSkippedGeneration() + { + $this->generateAndCompareTest('SkippedTest'); + } + + /** + * Tests skipped test with multiple issues generation. + * + * @throws \Exception + * @throws \Magento\FunctionalTestingFramework\Exceptions\TestReferenceException + */ + public function testMultipleSkippedIssuesGeneration() + { + $this->generateAndCompareTest('SkippedTestTwoIssues'); + } + + /** + * Tests skipped test generation with no specified issues. Will be deprecated after MFTF 3.0.0 + * + * @throws \Exception + * @throws \Magento\FunctionalTestingFramework\Exceptions\TestReferenceException + */ + public function testSkippedNoIssueGeneration() + { + $this->generateAndCompareTest('SkippedTestNoIssues'); + } +} diff --git a/dev/tests/verification/Tests/SuiteGenerationTest.php b/dev/tests/verification/Tests/SuiteGenerationTest.php index b1790ac81..52b34b3ef 100644 --- a/dev/tests/verification/Tests/SuiteGenerationTest.php +++ b/dev/tests/verification/Tests/SuiteGenerationTest.php @@ -6,23 +6,26 @@ namespace tests\verification\Tests; -use Magento\Framework\Module\Dir; use Magento\FunctionalTestingFramework\Suite\SuiteGenerator; -use Magento\FunctionalTestingFramework\Util\TestManifest; +use Magento\FunctionalTestingFramework\Util\Filesystem\DirSetupUtil; +use Magento\FunctionalTestingFramework\Util\Manifest\DefaultTestManifest; +use Magento\FunctionalTestingFramework\Util\Manifest\ParallelTestManifest; +use Magento\FunctionalTestingFramework\Util\Manifest\TestManifestFactory; +use PHPUnit\Util\Filesystem; use Symfony\Component\Yaml\Yaml; +use tests\unit\Util\TestLoggingUtil; use tests\util\MftfTestCase; class SuiteGenerationTest extends MftfTestCase { const RESOURCES_DIR = TESTS_BP . DIRECTORY_SEPARATOR . 'verification' . DIRECTORY_SEPARATOR . 'Resources'; - const CONFIG_YML_FILE = FW_BP . DIRECTORY_SEPARATOR . SuiteGenerator::YAML_CODECEPTION_CONFIG_FILENAME; - - /** - * Flag to track existence of config.yml file - * - * @var bool - */ - private static $YML_EXISTS_FLAG = false; + const CONFIG_YML_FILE = TESTS_BP . DIRECTORY_SEPARATOR . SuiteGenerator::YAML_CODECEPTION_CONFIG_FILENAME; + const GENERATE_RESULT_DIR = TESTS_BP . + DIRECTORY_SEPARATOR . + "verification" . + DIRECTORY_SEPARATOR . + "_generated" . + DIRECTORY_SEPARATOR; /** * Array which stores state of any existing config.yml groups @@ -36,18 +39,30 @@ class SuiteGenerationTest extends MftfTestCase */ public static function setUpBeforeClass() { - if (file_exists(self::CONFIG_YML_FILE)) { - self::$YML_EXISTS_FLAG = true; - return; + // destroy _generated if it exists + if (file_exists(self::GENERATE_RESULT_DIR)) { + DirSetupUtil::rmdirRecursive(self::GENERATE_RESULT_DIR); } + } - // destroy manifest file if it exists - if (file_exists(self::getManifestFilePath())) { - unlink(self::getManifestFilePath()); - } + public function setUp() + { + // copy config yml file to test dir + $fileSystem = new \Symfony\Component\Filesystem\Filesystem(); + $fileSystem->copy( + realpath( + FW_BP + . DIRECTORY_SEPARATOR + . 'etc' + . DIRECTORY_SEPARATOR + . 'config' + . DIRECTORY_SEPARATOR + . 'codeception.dist.yml' + ), + self::CONFIG_YML_FILE + ); - $configYml = fopen(self::CONFIG_YML_FILE, "w"); - fclose($configYml); + TestLoggingUtil::getInstance()->setMockLoggingUtil(); } /** @@ -55,7 +70,7 @@ public static function setUpBeforeClass() */ public function testSuiteGeneration1() { - $groupName = 'functionalSuite1'; + $groupName = 'functionalSuite1'; $expectedContents = [ 'additionalTestCest.php', @@ -67,19 +82,185 @@ public function testSuiteGeneration1() // Generate the Suite SuiteGenerator::getInstance()->generateSuite($groupName); - // Validate console message and add group name for later deletion - $this->expectOutputRegex('/Suite .* generated to .*/'); + // Validate log message and add group name for later deletion + TestLoggingUtil::getInstance()->validateMockLogStatement( + 'info', + "suite generated", + ['suite' => $groupName, 'relative_path' => "_generated" . DIRECTORY_SEPARATOR . $groupName] + ); + self::$TEST_GROUPS[] = $groupName; // Validate Yaml file updated $yml = Yaml::parse(file_get_contents(self::CONFIG_YML_FILE)); $this->assertArrayHasKey($groupName, $yml['groups']); - $suiteResultBaseDir = TESTS_BP . + $suiteResultBaseDir = self::GENERATE_RESULT_DIR . DIRECTORY_SEPARATOR . - "verification" . + $groupName . + DIRECTORY_SEPARATOR; + + // Validate tests have been generated + $dirContents = array_diff(scandir($suiteResultBaseDir), ['..', '.']); + + foreach ($expectedContents as $expectedFile) { + $this->assertTrue(in_array($expectedFile, $dirContents)); + } + } + + /** + * Test generation of parallel suite groups + */ + public function testSuiteGenerationParallel() + { + $groupName = 'functionalSuite1'; + + $expectedGroups = [ + 'functionalSuite1_0', + 'functionalSuite1_1', + 'functionalSuite1_2', + 'functionalSuite1_3' + ]; + + $expectedContents = [ + 'additionalTestCest.php', + 'additionalIncludeTest2Cest.php', + 'IncludeTest2Cest.php', + 'IncludeTestCest.php' + ]; + + //createParallelManifest + /** @var ParallelTestManifest $parallelManifest */ + $parallelManifest = TestManifestFactory::makeManifest("parallel", ["functionalSuite1" => []]); + + // Generate the Suite + $parallelManifest->createTestGroups(1); + SuiteGenerator::getInstance()->generateAllSuites($parallelManifest); + + // Validate log message (for final group) and add group name for later deletion + $expectedGroup = $expectedGroups[count($expectedGroups)-1] ; + TestLoggingUtil::getInstance()->validateMockLogStatement( + 'info', + "suite generated", + ['suite' => $expectedGroup, 'relative_path' => "_generated" . DIRECTORY_SEPARATOR . $expectedGroup] + ); + + self::$TEST_GROUPS[] = $groupName; + + // Validate Yaml file updated + $yml = Yaml::parse(file_get_contents(self::CONFIG_YML_FILE)); + $this->assertEquals(array_intersect($expectedGroups, array_keys($yml['groups'])), $expectedGroups); + + foreach ($expectedGroups as $expectedFolder) { + $suiteResultBaseDir = self::GENERATE_RESULT_DIR . + DIRECTORY_SEPARATOR . + $expectedFolder . + DIRECTORY_SEPARATOR; + + // Validate tests have been generated + $dirContents = array_diff(scandir($suiteResultBaseDir), ['..', '.']); + + //Validate only one test has been added to each group since lines are set to 1 + $this->assertEquals(1, count($dirContents)); + $this->assertContains(array_values($dirContents)[0], $expectedContents); + } + } + + /** + * Test hook groups generated during suite generation + */ + public function testSuiteGenerationHooks() + { + $groupName = 'functionalSuiteHooks'; + + $expectedContents = [ + 'IncludeTestCest.php' + ]; + + // Generate the Suite + SuiteGenerator::getInstance()->generateSuite($groupName); + + // Validate log message and add group name for later deletion + TestLoggingUtil::getInstance()->validateMockLogStatement( + 'info', + "suite generated", + ['suite' => $groupName, 'relative_path' => "_generated" . DIRECTORY_SEPARATOR . $groupName] + ); + self::$TEST_GROUPS[] = $groupName; + + // Validate Yaml file updated + $yml = Yaml::parse(file_get_contents(self::CONFIG_YML_FILE)); + $this->assertArrayHasKey($groupName, $yml['groups']); + + $suiteResultBaseDir = self::GENERATE_RESULT_DIR . DIRECTORY_SEPARATOR . - "_generated" . + $groupName . + DIRECTORY_SEPARATOR; + + // Validate tests have been generated + $dirContents = array_diff(scandir($suiteResultBaseDir), ['..', '.']); + + foreach ($expectedContents as $expectedFile) { + $this->assertTrue(in_array($expectedFile, $dirContents)); + } + + //assert group file created and contains correct contents + $groupFile = PROJECT_ROOT . + DIRECTORY_SEPARATOR . + "src" . + DIRECTORY_SEPARATOR . + "Magento" . + DIRECTORY_SEPARATOR . + "FunctionalTestingFramework" . + DIRECTORY_SEPARATOR . + "Group" . + DIRECTORY_SEPARATOR . + $groupName . + ".php"; + + $this->assertTrue(file_exists($groupFile)); + $this->assertFileEquals( + self::RESOURCES_PATH . DIRECTORY_SEPARATOR . $groupName . ".txt", + $groupFile + ); + } + + /** + * Test generation of parallel suite groups + */ + public function testSuiteGenerationSingleRun() + { + //using functionalSuite2 to avoid directory caching + $groupName = 'functionalSuite2'; + + $expectedContents = [ + 'additionalTestCest.php', + 'additionalIncludeTest2Cest.php', + 'IncludeTest2Cest.php', + 'IncludeTestCest.php' + ]; + + //createParallelManifest + /** @var DefaultTestManifest $parallelManifest */ + $singleRunManifest = TestManifestFactory::makeManifest("singleRun", ["functionalSuite2" => []]); + + // Generate the Suite + SuiteGenerator::getInstance()->generateAllSuites($singleRunManifest); + $singleRunManifest->generate(); + + // Validate log message and add group name for later deletion + TestLoggingUtil::getInstance()->validateMockLogStatement( + 'info', + "suite generated", + ['suite' => $groupName, 'relative_path' => "_generated" . DIRECTORY_SEPARATOR . $groupName] + ); + self::$TEST_GROUPS[] = $groupName; + + // Validate Yaml file updated + $yml = Yaml::parse(file_get_contents(self::CONFIG_YML_FILE)); + $this->assertArrayHasKey($groupName, $yml['groups']); + + $suiteResultBaseDir = self::GENERATE_RESULT_DIR . DIRECTORY_SEPARATOR . $groupName . DIRECTORY_SEPARATOR; @@ -90,25 +271,41 @@ public function testSuiteGeneration1() foreach ($expectedContents as $expectedFile) { $this->assertTrue(in_array($expectedFile, $dirContents)); } + + $expectedManifest = "verification" + . DIRECTORY_SEPARATOR + . "_generated" + . DIRECTORY_SEPARATOR + . "default" + . DIRECTORY_SEPARATOR + . PHP_EOL + . "-g functionalSuite2" + . PHP_EOL; + + $this->assertEquals($expectedManifest, file_get_contents(self::getManifestFilePath())); } /** * revert any changes made to config.yml + * remove _generated directory */ - public static function tearDownAfterClass() + public function tearDown() { - // restore config if we see there was an original codeception.yml file - if (self::$YML_EXISTS_FLAG) { - $yml = Yaml::parse(file_get_contents(self::CONFIG_YML_FILE)); - foreach (self::$TEST_GROUPS as $testGroup) { - unset($yml['group'][$testGroup]); - } - - file_put_contents(self::CONFIG_YML_FILE, Yaml::dump($yml, 10)); - return; - } + DirSetupUtil::rmdirRecursive(self::GENERATE_RESULT_DIR); - unlink(self::CONFIG_YML_FILE); + // delete config yml file from test dir + $fileSystem = new \Symfony\Component\Filesystem\Filesystem(); + $fileSystem->remove( + self::CONFIG_YML_FILE + ); + } + + /** + * Remove yml if created during tests and did not exist before + */ + public static function tearDownAfterClass() + { + TestLoggingUtil::getInstance()->clearMockLoggingUtil(); } /** diff --git a/dev/_suite/functionalSuite.xml b/dev/tests/verification/_suite/functionalSuite.xml similarity index 55% rename from dev/_suite/functionalSuite.xml rename to dev/tests/verification/_suite/functionalSuite.xml index ab7816e4c..439445c24 100644 --- a/dev/_suite/functionalSuite.xml +++ b/dev/tests/verification/_suite/functionalSuite.xml @@ -6,7 +6,7 @@ */ --> -<suites xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../src/Magento/FunctionalTestingFramework/Suite/etc/suiteSchema.xsd"> +<suites xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../src/Magento/FunctionalTestingFramework/Suite/etc/suiteSchema.xsd"> <suite name="functionalSuite1"> <include> <group name="include"/> @@ -18,4 +18,15 @@ <test name="ExcludeTest2"/> </exclude> </suite> + <suite name="functionalSuite2"> + <include> + <group name="include"/> + <test name="IncludeTest"/> + <module name="TestModule" file="SampleSuite2Test.xml"/> + </include> + <exclude> + <group name="exclude"/> + <test name="ExcludeTest2"/> + </exclude> + </suite> </suites> diff --git a/dev/tests/verification/_suite/functionalSuiteHooks.xml b/dev/tests/verification/_suite/functionalSuiteHooks.xml new file mode 100644 index 000000000..e86ea9590 --- /dev/null +++ b/dev/tests/verification/_suite/functionalSuiteHooks.xml @@ -0,0 +1,33 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<suites xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../src/Magento/FunctionalTestingFramework/Suite/etc/suiteSchema.xsd"> + <suite name="functionalSuiteHooks"> + <include> + <test name="IncludeTest"/> + </include> + <before> + <amOnPage url="some.url" stepKey="before"/> + <createData entity="createThis" stepKey="create"> + <field key="someKey">dataHere</field> + </createData> + <actionGroup ref="actionGroupWithTwoArguments" stepKey="AC"> + <argument name="somePerson" value="simpleData"/> + <argument name="anotherPerson" value="uniqueData"/> + </actionGroup> + </before> + <after> + <amOnPage url="some.url" stepKey="after"/> + <deleteData url="deleteThis" stepKey="delete"/> + <actionGroup ref="actionGroupWithTwoArguments" stepKey="AC"> + <argument name="somePerson" value="simpleData"/> + <argument name="anotherPerson" value="uniqueData"/> + </actionGroup> + </after> + </suite> +</suites> diff --git a/etc/config/.credentials.example b/etc/config/.credentials.example new file mode 100644 index 000000000..ea8b03480 --- /dev/null +++ b/etc/config/.credentials.example @@ -0,0 +1,75 @@ +#carriers/fedex/account= +#carriers/fedex/meter_number= +#carriers/fedex/key= +#carriers/fedex/password= + +#carriers/ups/password= +#carriers/ups/username= +#carriers/ups/access_license_number= +#carriers/ups/shipper_number= + +#carriers/usps/userid= +#carriers/usps/password= + +#carriers_dhl_id_us= +#carriers_dhl_password_us= +#carriers_dhl_account_us= + +#carriers_dhl_id_eu= +#carriers_dhl_password_eu= +#carriers_dhl_account_eu= + + +#payment_authorizenet_login= +#payment_authorizenet_trans_key= +#payment_authorizenet_trans_md5= + +#authorizenet_fraud_review_login= +#authorizenet_fraud_review_trans_key= +#authorizenet_fraud_review_md5= + +#braintree_enabled_fraud_merchant_account_id= +#braintree_enabled_fraud_merchant_id= +#braintree_enabled_fraud_public_key= +#braintree_enabled_fraud_private_key= + +#braintree_disabled_fraud_merchant_account_id= +#braintree_disabled_fraud_merchant_id= +#braintree_disabled_fraud_public_key= +#braintree_disabled_fraud_private_key= + +#payment/paypal_group_all_in_one/wpp_usuk/wpp_required_settings/wpp_and_express_checkout/business_account= +#payment/paypal_group_all_in_one/wpp_usuk/wpp_required_settings/wpp_and_express_checkout/api_username= +#payment/paypal_group_all_in_one/wpp_usuk/wpp_required_settings/wpp_and_express_checkout/api_password= +#payment/paypal_group_all_in_one/wpp_usuk/wpp_required_settings/wpp_and_express_checkout/api_signature= +#payment/paypal_express/merchant_id= + +#payflow_pro_fraud_protection_enabled_business_account= +#payflow_pro_fraud_protection_enabled_partner= +#payflow_pro_fraud_protection_enabled_user= +#payflow_pro_fraud_protection_enabled_pwd= +#payflow_pro_fraud_protection_enabled_vendor= + +#payflow_pro_business_account= +#payflow_pro_partner= +#payflow_pro_user= +#payflow_pro_pwd= +#payflow_pro_vendor= + +#payflow_link_business_account_email= +#payflow_link_partner= +#payflow_link_user= +#payflow_link_password= +#payflow_link_vendor= + +#payment/paypal_group_all_in_one/payments_pro_hosted_solution_with_express_checkout/pphs_required_settings/pphs_required_settings_pphs/business_account= +#payment/paypal_group_all_in_one/payments_pro_hosted_solution_with_express_checkout/pphs_required_settings/pphs_required_settings_pphs/api_username= +#payment/paypal_group_all_in_one/payments_pro_hosted_solution_with_express_checkout/pphs_required_settings/pphs_required_settings_pphs/api_password= +#payment/paypal_group_all_in_one/payments_pro_hosted_solution_with_express_checkout/pphs_required_settings/pphs_required_settings_pphs/api_signature= + +#payment/paypal_alternative_payment_methods/express_checkout_us/express_checkout_required/express_checkout_required_express_checkout/business_account= +#payment/paypal_alternative_payment_methods/express_checkout_us/express_checkout_required/express_checkout_required_express_checkout/api_username= +#payment/paypal_alternative_payment_methods/express_checkout_us/express_checkout_required/express_checkout_required_express_checkout/api_password= +#payment/paypal_alternative_payment_methods/express_checkout_us/express_checkout_required/express_checkout_required_express_checkout/api_signature= + +#fraud_protection/signifyd/api_key= \ No newline at end of file diff --git a/.env.example b/etc/config/.env.example similarity index 100% rename from .env.example rename to etc/config/.env.example diff --git a/etc/config/codeception.dist.yml b/etc/config/codeception.dist.yml new file mode 100755 index 000000000..57582f1bd --- /dev/null +++ b/etc/config/codeception.dist.yml @@ -0,0 +1,38 @@ +# Copyright © Magento, Inc. All rights reserved. +# See COPYING.txt for license details. +actor: Tester +paths: + tests: tests + log: tests/_output + data: tests/_data + support: src/Magento/FunctionalTestingFramework + envs: etc/_envs +settings: + colors: true + memory_limit: 1024M +extensions: + enabled: + - Codeception\Extension\RunFailed + - Magento\FunctionalTestingFramework\Extension\TestContextExtension + - Magento\FunctionalTestingFramework\Allure\Adapter\MagentoAllureAdapter + - Magento\FunctionalTestingFramework\Extension\PageReadinessExtension + config: + Yandex\Allure\Adapter\AllureAdapter: + deletePreviousResults: true + outputDirectory: allure-results + ignoredAnnotations: + - env + - zephyrId + - useCaseId + Magento\FunctionalTestingFramework\Extension\PageReadinessExtension: + driver: \Magento\FunctionalTestingFramework\Module\MagentoWebDriver + timeout: 30 + resetFailureThreshold: 3 + readinessMetrics: + - \Magento\FunctionalTestingFramework\Extension\ReadinessMetrics\DocumentReadyState + - \Magento\FunctionalTestingFramework\Extension\ReadinessMetrics\JQueryAjaxRequests + - \Magento\FunctionalTestingFramework\Extension\ReadinessMetrics\PrototypeAjaxRequests + - \Magento\FunctionalTestingFramework\Extension\ReadinessMetrics\RequireJsDefinitions + - \Magento\FunctionalTestingFramework\Extension\ReadinessMetrics\MagentoLoadingMasks +params: + - .env \ No newline at end of file diff --git a/dev/tests/functional/MFTF.suite.dist.yml b/etc/config/functional.suite.dist.yml similarity index 84% rename from dev/tests/functional/MFTF.suite.dist.yml rename to etc/config/functional.suite.dist.yml index a54620385..bfd093e41 100644 --- a/dev/tests/functional/MFTF.suite.dist.yml +++ b/etc/config/functional.suite.dist.yml @@ -14,14 +14,15 @@ modules: - \Magento\FunctionalTestingFramework\Module\MagentoWebDriver - \Magento\FunctionalTestingFramework\Helper\Acceptance - \Magento\FunctionalTestingFramework\Helper\MagentoFakerData + - \Magento\FunctionalTestingFramework\Module\MagentoSequence - \Magento\FunctionalTestingFramework\Module\MagentoAssert - Asserts config: \Magento\FunctionalTestingFramework\Module\MagentoWebDriver: url: "%MAGENTO_BASE_URL%" backend_name: "%MAGENTO_BACKEND_NAME%" - browser: '%BROWSER%' - window_size: maximize + browser: 'chrome' + window_size: 1280x1024 username: "%MAGENTO_ADMIN_USERNAME%" password: "%MAGENTO_ADMIN_PASSWORD%" pageload_timeout: 30 @@ -31,5 +32,5 @@ modules: path: %SELENIUM_PATH% capabilities: chromeOptions: - args: ["--start-maximized", "--disable-extensions", "--enable-automation"] + args: ["--incognito", "--disable-extensions", "--enable-automation"] diff --git a/etc/di.xml b/etc/di.xml index d03ed2267..60faed3a0 100644 --- a/etc/di.xml +++ b/etc/di.xml @@ -8,7 +8,7 @@ <!-- Entity value gets replaced in Dom.php before reading $xml --> <!DOCTYPE config [ - <!ENTITY commonTestActions "acceptPopup|actionGroup|amOnPage|amOnUrl|amOnSubdomain|appendField|assertArrayIsSorted|assertArraySubset|assertElementContainsAttribute|attachFile|cancelPopup|checkOption|clearField|click|clickWithLeftButton|clickWithRightButton|closeAdminNotification|closeTab|comment|conditionalClick|createData|deleteData|updateData|getData|dontSee|dontSeeJsError|dontSeeCheckboxIsChecked|dontSeeCookie|dontSeeCurrentUrlEquals|dontSeeCurrentUrlMatches|dontSeeElement|dontSeeElementInDOM|dontSeeInCurrentUrl|dontSeeInField|dontSeeInFormFields|dontSeeInPageSource|dontSeeInSource|dontSeeInTitle|dontSeeLink|dontSeeOptionIsSelected|doubleClick|dragAndDrop|entity|executeJS|executeInSelenium|fillField|formatMoney|grabAttributeFrom|grabCookie|grabFromCurrentUrl|grabMultiple|grabPageSource|grabTextFrom|grabValueFrom|loadSessionSnapshot|loginAsAdmin|magentoCLI|makeScreenshot|maximizeWindow|moveBack|moveForward|moveMouseOver|mSetLocale|mResetLocale|openNewTab|pauseExecution|parseFloat|performOn|pressKey|reloadPage|resetCookie|submitForm|resizeWindow|saveSessionSnapshot|scrollTo|scrollToTopOfPage|searchAndMultiSelectOption|see|seeCheckboxIsChecked|seeCookie|seeCurrentUrlEquals|seeCurrentUrlMatches|seeElement|seeElementInDOM|seeInCurrentUrl|seeInField|seeInFormFields|seeInPageSource|seeInPopup|seeInSource|seeInTitle|seeLink|seeNumberOfElements|seeOptionIsSelected|selectOption|setCookie|submitForm|switchToIFrame|switchToNextTab|switchToPreviousTab|switchToWindow|typeInPopup|uncheckOption|unselectOption|wait|waitForAjaxLoad|waitForElement|waitForElementChange|waitForElementNotVisible|waitForElementVisible|waitForJS|waitForLoadingMaskToDisappear|waitForPageLoad|waitForText|assertArrayHasKey|assertArrayNotHasKey|assertArraySubset|assertContains|assertCount|assertEmpty|assertEquals|assertFalse|assertFileExists|assertFileNotExists|assertGreaterOrEquals|assertGreaterThan|assertGreaterThanOrEqual|assertInstanceOf|assertInternalType|assertIsEmpty|assertLessOrEquals|assertLessThan|assertLessThanOrEqual|assertNotContains|assertNotEmpty|assertNotEquals|assertNotInstanceOf|assertNotNull|assertNotRegExp|assertNotSame|assertNull|assertRegExp|assertSame|assertStringStartsNotWith|assertStringStartsWith|assertTrue|expectException|fail|dontSeeFullUrlEquals|dontSee|dontSeeFullUrlMatches|dontSeeInFullUrl|seeFullUrlEquals|seeFullUrlMatches|seeInFullUrl|grabFromFullUrl"> + <!ENTITY commonTestActions "acceptPopup|actionGroup|amOnPage|amOnUrl|amOnSubdomain|appendField|assertArrayIsSorted|assertArraySubset|assertElementContainsAttribute|attachFile|cancelPopup|checkOption|clearField|click|clickWithLeftButton|clickWithRightButton|closeAdminNotification|closeTab|comment|conditionalClick|createData|deleteData|updateData|getData|dontSee|dontSeeJsError|dontSeeCheckboxIsChecked|dontSeeCookie|dontSeeCurrentUrlEquals|dontSeeCurrentUrlMatches|dontSeeElement|dontSeeElementInDOM|dontSeeInCurrentUrl|dontSeeInField|dontSeeInFormFields|dontSeeInPageSource|dontSeeInSource|dontSeeInTitle|dontSeeLink|dontSeeOptionIsSelected|doubleClick|dragAndDrop|entity|executeJS|executeInSelenium|fillField|formatMoney|generateDate|grabAttributeFrom|grabCookie|grabFromCurrentUrl|grabMultiple|grabPageSource|grabTextFrom|grabValueFrom|loadSessionSnapshot|loginAsAdmin|magentoCLI|makeScreenshot|maximizeWindow|moveBack|moveForward|moveMouseOver|mSetLocale|mResetLocale|openNewTab|pauseExecution|parseFloat|performOn|pressKey|reloadPage|resetCookie|submitForm|resizeWindow|saveSessionSnapshot|scrollTo|scrollToTopOfPage|searchAndMultiSelectOption|see|seeCheckboxIsChecked|seeCookie|seeCurrentUrlEquals|seeCurrentUrlMatches|seeElement|seeElementInDOM|seeInCurrentUrl|seeInField|seeInFormFields|seeInPageSource|seeInPopup|seeInSource|seeInTitle|seeLink|seeNumberOfElements|seeOptionIsSelected|selectOption|setCookie|submitForm|switchToIFrame|switchToNextTab|switchToPreviousTab|switchToWindow|typeInPopup|uncheckOption|unselectOption|wait|waitForAjaxLoad|waitForElement|waitForElementChange|waitForElementNotVisible|waitForElementVisible|waitForJS|waitForLoadingMaskToDisappear|waitForPageLoad|waitForText|assertArrayHasKey|assertArrayNotHasKey|assertArraySubset|assertContains|assertCount|assertEmpty|assertEquals|assertFalse|assertFileExists|assertFileNotExists|assertGreaterOrEquals|assertGreaterThan|assertGreaterThanOrEqual|assertInstanceOf|assertInternalType|assertIsEmpty|assertLessOrEquals|assertLessThan|assertLessThanOrEqual|assertNotContains|assertNotEmpty|assertNotEquals|assertNotInstanceOf|assertNotNull|assertNotRegExp|assertNotSame|assertNull|assertRegExp|assertSame|assertStringStartsNotWith|assertStringStartsWith|assertTrue|expectException|fail|dontSeeFullUrlEquals|dontSee|dontSeeFullUrlMatches|dontSeeInFullUrl|seeFullUrlEquals|seeFullUrlMatches|seeInFullUrl|grabFromFullUrl"> ]> <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../src/Magento/FunctionalTestingFramework/ObjectManager/etc/config.xsd"> @@ -78,11 +78,12 @@ <argument name="schemaPath" xsi:type="string">Magento/FunctionalTestingFramework/Page/etc/SectionObject.xsd</argument> </arguments> </virtualType> - <virtualType name="Magento\FunctionalTestingFramework\Config\Reader\Page" type="Magento\FunctionalTestingFramework\Config\Reader\Filesystem"> + <virtualType name="Magento\FunctionalTestingFramework\Config\Reader\Page" type="Magento\FunctionalTestingFramework\Config\Reader\MftfFilesystem"> <arguments> <argument name="fileResolver" xsi:type="object">Magento\FunctionalTestingFramework\Config\FileResolver\Module</argument> <argument name="converter" xsi:type="object">Magento\FunctionalTestingFramework\Config\Converter</argument> <argument name="schemaLocator" xsi:type="object">Magento\FunctionalTestingFramework\Config\SchemaLocator\Page</argument> + <argument name="domDocumentClass" xsi:type="string">Magento\FunctionalTestingFramework\Page\Config\Dom</argument> <argument name="idAttributes" xsi:type="array"> <item name="/pages/page" xsi:type="string">name</item> <item name="/pages/page/section" xsi:type="string">name</item> @@ -91,11 +92,12 @@ <argument name="defaultScope" xsi:type="string">Page</argument> </arguments> </virtualType> - <virtualType name="Magento\FunctionalTestingFramework\Config\Reader\Section" type="Magento\FunctionalTestingFramework\Config\Reader\Filesystem"> + <virtualType name="Magento\FunctionalTestingFramework\Config\Reader\Section" type="Magento\FunctionalTestingFramework\Config\Reader\MftfFilesystem"> <arguments> <argument name="fileResolver" xsi:type="object">Magento\FunctionalTestingFramework\Config\FileResolver\Module</argument> <argument name="converter" xsi:type="object">Magento\FunctionalTestingFramework\Config\Converter</argument> <argument name="schemaLocator" xsi:type="object">Magento\FunctionalTestingFramework\Config\SchemaLocator\Section</argument> + <argument name="domDocumentClass" xsi:type="string">Magento\FunctionalTestingFramework\Page\Config\SectionDom</argument> <argument name="idAttributes" xsi:type="array"> <item name="/sections/section" xsi:type="string">name</item> <item name="/sections/section/element" xsi:type="string">name</item> @@ -146,7 +148,7 @@ <argument name="schemaPath" xsi:type="string">Magento/FunctionalTestingFramework/DataGenerator/etc/dataProfileSchema.xsd</argument> </arguments> </virtualType> - <virtualType name="Magento\FunctionalTestingFramework\Config\Reader\DataProfile" type="Magento\FunctionalTestingFramework\DataGenerator\Config\Reader\Filesystem"> + <virtualType name="Magento\FunctionalTestingFramework\Config\Reader\DataProfile" type="Magento\FunctionalTestingFramework\Config\Reader\MftfFilesystem"> <arguments> <argument name="fileResolver" xsi:type="object">Magento\FunctionalTestingFramework\Config\FileResolver\Module</argument> <argument name="converter" xsi:type="object">Magento\FunctionalTestingFramework\Config\Converter</argument> @@ -155,6 +157,7 @@ <argument name="idAttributes" xsi:type="array"> <item name="/entities/entity" xsi:type="string">name</item> <item name="/entities/entity/(data|array)" xsi:type="string">key</item> + <item name="/entities/entity/requiredEntity" xsi:type="string">type</item> </argument> <argument name="mergeablePaths" xsi:type="array"> <item name="/entities/entity/requiredEntity" xsi:type="string"/> @@ -182,11 +185,11 @@ <argument name="schemaPath" xsi:type="string">Magento/FunctionalTestingFramework/DataGenerator/etc/dataOperation.xsd</argument> </arguments> </virtualType> - <virtualType name="Magento\FunctionalTestingFramework\Config\Reader\Metadata" type="Magento\FunctionalTestingFramework\DataGenerator\Config\Reader\Filesystem"> + <virtualType name="Magento\FunctionalTestingFramework\Config\Reader\Metadata" type="Magento\FunctionalTestingFramework\Config\Reader\MftfFilesystem"> <arguments> <argument name="fileResolver" xsi:type="object">Magento\FunctionalTestingFramework\Config\FileResolver\Module</argument> <argument name="converter" xsi:type="object">Magento\FunctionalTestingFramework\Config\Converter</argument> - <argument name="domDocumentClass" xsi:type="string">Magento\FunctionalTestingFramework\DataGenerator\Config\Dom</argument> + <argument name="domDocumentClass" xsi:type="string">Magento\FunctionalTestingFramework\DataGenerator\Config\OperationDom</argument> <argument name="schemaLocator" xsi:type="object">Magento\FunctionalTestingFramework\Config\SchemaLocator\Metadata</argument> <argument name="idAttributes" xsi:type="array"> <item name="/operations/operation" xsi:type="string">name</item> @@ -207,7 +210,7 @@ <argument name="schemaPath" xsi:type="string">Magento/FunctionalTestingFramework/Test/etc/mergedTestSchema.xsd</argument> </arguments> </virtualType> - <virtualType name="Magento\FunctionalTestingFramework\Config\Reader\TestData" type="Magento\FunctionalTestingFramework\Test\Config\Reader\Filesystem"> + <virtualType name="Magento\FunctionalTestingFramework\Config\Reader\TestData" type="Magento\FunctionalTestingFramework\Config\Reader\MftfFilesystem"> <arguments> <argument name="fileResolver" xsi:type="object">Magento\FunctionalTestingFramework\Config\FileResolver\Module</argument> <argument name="converter" xsi:type="object">Magento\FunctionalTestingFramework\Config\TestDataConverter</argument> @@ -257,6 +260,7 @@ <item name="/tests/test/annotations/group" xsi:type="string">/tests/test/annotations/group</item> <item name="/tests/test/annotations/env" xsi:type="string">/tests/test/annotations/env</item> <item name="/tests/test/annotations/return" xsi:type="string">/tests/test/annotations/return</item> + <item name="/tests/test/annotations/skip/issueId" xsi:type="string">/tests/test/annotations/skip/issueId</item> </argument> </arguments> </virtualType> @@ -286,7 +290,7 @@ </arguments> </virtualType> - <virtualType name="Magento\FunctionalTestingFramework\Config\Reader\ActionGroupData" type="Magento\FunctionalTestingFramework\Test\Config\Reader\Filesystem"> + <virtualType name="Magento\FunctionalTestingFramework\Config\Reader\ActionGroupData" type="Magento\FunctionalTestingFramework\Config\Reader\MftfFilesystem"> <arguments> <argument name="fileResolver" xsi:type="object">Magento\FunctionalTestingFramework\Config\FileResolver\Module</argument> <argument name="converter" xsi:type="object">Magento\FunctionalTestingFramework\Config\ActionGroupDataConverter</argument> @@ -360,6 +364,7 @@ <argument name="idAttributes" xsi:type="array"> <item name="/suites/suite" xsi:type="string">name</item> <item name="/suites/suite/(before|after)/remove" xsi:type="string">keyForRemoval</item> + <item name="/suites/suite/(before|after)/actionGroup/argument" xsi:type="string">name</item> <item name="/suites/suite/(before|after)/(actionGroup|&commonTestActions;)" xsi:type="string">stepKey</item> <item name="/suites/suite/(before|after)/createData/requiredEntity" xsi:type="string">createDataKey</item> <item name="/suites/suite/(before|after)/createData/field" xsi:type="string">key</item> @@ -376,6 +381,7 @@ <argument name="assocArrayAttributes" xsi:type="array"> <item name="/suites/suite" xsi:type="string">name</item> <item name="/suites/suite/(before|after)/remove" xsi:type="string">keyForRemoval</item> + <item name="/suites/suite/(before|after)/actionGroup/argument" xsi:type="string">name</item> <item name="/suites/suite/(before|after)/(actionGroup|&commonTestActions;)" xsi:type="string">stepKey</item> <item name="/suites/suite/(before|after)/createData/requiredEntity" xsi:type="string">createDataKey</item> <item name="/suites/suite/(before|after)/createData/field" xsi:type="string">key</item> diff --git a/src/Magento/FunctionalTestingFramework/Allure/Adapter/MagentoAllureAdapter.php b/src/Magento/FunctionalTestingFramework/Allure/Adapter/MagentoAllureAdapter.php index 393f29008..d23af52da 100644 --- a/src/Magento/FunctionalTestingFramework/Allure/Adapter/MagentoAllureAdapter.php +++ b/src/Magento/FunctionalTestingFramework/Allure/Adapter/MagentoAllureAdapter.php @@ -8,7 +8,9 @@ use Magento\FunctionalTestingFramework\Data\Argument\Interpreter\NullType; use Magento\FunctionalTestingFramework\Suite\Handlers\SuiteObjectHandler; use Yandex\Allure\Adapter\AllureAdapter; +use Yandex\Allure\Adapter\Event\StepStartedEvent; use Codeception\Event\SuiteEvent; +use Codeception\Event\StepEvent; /** * Class MagentoAllureAdapter @@ -23,7 +25,7 @@ class MagentoAllureAdapter extends AllureAdapter /** * Array of group values passed to test runner command * - * @return String + * @return string */ private function getGroup() { @@ -91,4 +93,28 @@ private function sanitizeGroupName($group) $originalName = in_array($originalName, $suiteNames) ? $originalName : $group; return $originalName; } + + /** + * Override of parent method, only different to prevent replacing of . to • + * + * @param StepEvent $stepEvent + * @return void + */ + public function stepBefore(StepEvent $stepEvent) + { + //Hard set to 200; we don't expose this config in MFTF + $argumentsLength = 200; + $stepAction = $stepEvent->getStep()->getHumanizedActionWithoutArguments(); + $stepArgs = $stepEvent->getStep()->getArgumentsAsString($argumentsLength); + + if (!trim($stepAction)) { + $stepAction = $stepEvent->getStep()->getMetaStep()->getHumanizedActionWithoutArguments(); + $stepArgs = $stepEvent->getStep()->getMetaStep()->getArgumentsAsString($argumentsLength); + } + + $stepName = $stepAction . ' ' . $stepArgs; + + $this->emptyStep = false; + $this->getLifecycle()->fire(new StepStartedEvent($stepName)); + } } diff --git a/src/Magento/FunctionalTestingFramework/Config/Converter.php b/src/Magento/FunctionalTestingFramework/Config/Converter.php index 2b046fe5d..dcc67e5cf 100644 --- a/src/Magento/FunctionalTestingFramework/Config/Converter.php +++ b/src/Magento/FunctionalTestingFramework/Config/Converter.php @@ -49,10 +49,10 @@ class Converter implements \Magento\FunctionalTestingFramework\Config\ConverterI /** * Constructor for Converter object. * - * @param ArgumentParser $argumentParser + * @param ArgumentParser $argumentParser * @param InterpreterInterface $argumentInterpreter - * @param string $argumentNodeName - * @param array $idAttributes + * @param string $argumentNodeName + * @param array $idAttributes */ public function __construct( ArgumentParser $argumentParser, @@ -130,7 +130,7 @@ protected function convertXml($elements) * Get key for DOM element * * @param \DOMElement $element - * @return bool|string + * @return boolean|string */ protected function getElementKey(\DOMElement $element) { @@ -149,8 +149,8 @@ protected function getElementKey(\DOMElement $element) * Verify attribute is main key for element. * * @param \DOMElement $element - * @param \DOMAttr $attribute - * @return bool + * @param \DOMAttr $attribute + * @return boolean */ protected function isKeyAttribute(\DOMElement $element, \DOMAttr $attribute) { @@ -200,7 +200,7 @@ protected function getChildNodes(\DOMElement $element) * Cast nodeValue to int or double. * * @param string $nodeValue - * @return float|int + * @return float|integer */ protected function castNumeric($nodeValue) { diff --git a/src/Magento/FunctionalTestingFramework/Config/Converter/Dom/Flat.php b/src/Magento/FunctionalTestingFramework/Config/Converter/Dom/Flat.php index 23f3b3e7c..4350633e7 100644 --- a/src/Magento/FunctionalTestingFramework/Config/Converter/Dom/Flat.php +++ b/src/Magento/FunctionalTestingFramework/Config/Converter/Dom/Flat.php @@ -66,7 +66,7 @@ protected function getNodeAttributes(\DOMNode $node) * ) * * @param \DOMNode $source - * @param string $basePath + * @param string $basePath * @return string|array * @throws \UnexpectedValueException * @SuppressWarnings(PHPMD.CyclomaticComplexity) diff --git a/src/Magento/FunctionalTestingFramework/Config/Dom.php b/src/Magento/FunctionalTestingFramework/Config/Dom.php index 4bdfda289..1305574cc 100644 --- a/src/Magento/FunctionalTestingFramework/Config/Dom.php +++ b/src/Magento/FunctionalTestingFramework/Config/Dom.php @@ -70,7 +70,7 @@ class Dom * The path to ID attribute name should not include any attribute notations or modifiers -- only node names * * @param string $xml - * @param array $idAttributes + * @param array $idAttributes * @param string $typeAttributeName * @param string $schemaFile * @param string $errorFormat @@ -93,12 +93,14 @@ public function __construct( /** * Merge $xml into DOM document * - * @param string $xml + * @param string $xml + * @param string $filename + * @param ExceptionCollector $exceptionCollector * @return void */ - public function merge($xml) + public function merge($xml, $filename = null, $exceptionCollector = null) { - $dom = $this->initDom($xml); + $dom = $this->initDom($xml, $filename, $exceptionCollector); $this->mergeNode($dom->documentElement, ''); } @@ -111,7 +113,7 @@ public function merge($xml) * 3. Append new node if original document doesn't have the same node * * @param \DOMElement $node - * @param string $parentPath path to parent node + * @param string $parentPath Path to parent node. * @return void */ protected function mergeNode(\DOMElement $node, $parentPath) @@ -135,9 +137,9 @@ protected function mergeNode(\DOMElement $node, $parentPath) * Function to process matching node merges. Broken into shared logic for extending classes. * * @param \DomElement $node - * @param string $parentPath + * @param string $parentPath * @param |DomElement $matchedNode - * @param string $path + * @param string $path * @return void * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.NPathComplexity) @@ -181,7 +183,7 @@ protected function mergeMatchingNode(\DomElement $node, $parentPath, $matchedNod /** * Replace node value. * - * @param string $parentPath + * @param string $parentPath * @param \DOMElement $node * @param \DOMElement $matchedNode * @@ -198,7 +200,7 @@ protected function replaceNodeValue($parentPath, \DOMElement $node, \DOMElement * Check if the node content is text * * @param \DOMElement $node - * @return bool + * @return boolean */ protected function isTextNode($node) { @@ -209,12 +211,20 @@ protected function isTextNode($node) * Merges attributes of the merge node to the base node * * @param \DOMElement $baseNode - * @param \DOMNode $mergeNode + * @param \DOMNode $mergeNode * @return void */ protected function mergeAttributes($baseNode, $mergeNode) { foreach ($mergeNode->attributes as $attribute) { + // Do not overwrite filename of base node + if ($attribute->name === "filename") { + $baseNode->setAttribute( + $this->getAttributeName($attribute), + $baseNode->getAttribute("filename") . "," . $attribute->value + ); + continue; + } $baseNode->setAttribute($this->getAttributeName($attribute), $attribute->value); } } @@ -223,7 +233,7 @@ protected function mergeAttributes($baseNode, $mergeNode) * Identify node path based on parent path and node attributes * * @param \DOMElement $node - * @param string $parentPath + * @param string $parentPath * @return string */ protected function getNodePathByParent(\DOMElement $node, $parentPath) @@ -270,8 +280,8 @@ protected function getMatchedNode($nodePath) * Validate dom document * * @param \DOMDocument $dom - * @param string $schemaFileName - * @param string $errorFormat + * @param string $schemaFileName + * @param string $errorFormat * @return array of errors * @throws \Exception */ @@ -312,7 +322,7 @@ public static function validateDomDocument( * Render error message string by replacing placeholders '%field%' with properties of \LibXMLError * * @param \LibXMLError $errorInfo - * @param string $format + * @param string $format * @return string * @throws \InvalidArgumentException */ @@ -363,9 +373,9 @@ protected function initDom($xml) /** * Validate self contents towards to specified schema * - * @param string $schemaFileName absolute path to schema file - * @param array &$errors - * @return bool + * @param string $schemaFileName Absolute path to schema file. + * @param array $errors + * @return boolean */ public function validate($schemaFileName, &$errors = []) { diff --git a/src/Magento/FunctionalTestingFramework/Config/Dom/ArrayNodeConfig.php b/src/Magento/FunctionalTestingFramework/Config/Dom/ArrayNodeConfig.php index eaa216649..5f03450dc 100644 --- a/src/Magento/FunctionalTestingFramework/Config/Dom/ArrayNodeConfig.php +++ b/src/Magento/FunctionalTestingFramework/Config/Dom/ArrayNodeConfig.php @@ -24,6 +24,13 @@ class ArrayNodeConfig */ private $assocArrays = []; + /** + * Flat array of expanded patterns for matching xpath + * + * @var array + */ + private $flatAssocArray = []; + /** * Format: array('/numeric/array/path', ...) * @@ -34,8 +41,8 @@ class ArrayNodeConfig /** * ArrayNodeConfig constructor. * @param NodePathMatcher $nodePathMatcher - * @param array $assocArrayAttributes - * @param array $numericArrays + * @param array $assocArrayAttributes + * @param array $numericArrays */ public function __construct( NodePathMatcher $nodePathMatcher, @@ -44,6 +51,7 @@ public function __construct( ) { $this->nodePathMatcher = $nodePathMatcher; $this->assocArrays = $assocArrayAttributes; + $this->flatAssocArray = $this->flattenToAssocKeyAttributes($assocArrayAttributes); $this->numericArrays = $numericArrays; } @@ -51,7 +59,7 @@ public function __construct( * Whether a node is a numeric array or not * * @param string $nodeXpath - * @return bool + * @return boolean */ public function isNumericArray($nodeXpath) { @@ -71,6 +79,10 @@ public function isNumericArray($nodeXpath) */ public function getAssocArrayKeyAttribute($nodeXpath) { + if (array_key_exists($nodeXpath, $this->flatAssocArray)) { + return $this->flatAssocArray[$nodeXpath]; + } + foreach ($this->assocArrays as $pathPattern => $keyAttribute) { if ($this->nodePathMatcher->match($pathPattern, $nodeXpath)) { return $keyAttribute; @@ -78,4 +90,57 @@ public function getAssocArrayKeyAttribute($nodeXpath) } return null; } + + /** + * Function which takes a patterned list of xpath matchers and flattens to a single level array for + * performance improvement + * + * @param array $assocArrayAttributes + * @return array + */ + private function flattenToAssocKeyAttributes($assocArrayAttributes) + { + $finalPatterns = []; + foreach ($assocArrayAttributes as $pattern => $key) { + $vars = explode("/", ltrim($pattern, "/")); + $stringPatterns = [""]; + foreach ($vars as $var) { + if (strstr($var, "|")) { + $repOpen = str_replace("(", "", $var); + $repClosed = str_replace(")", "", $repOpen); + $nestedPatterns = explode("|", $repClosed); + $stringPatterns = $this->mergeStrings($stringPatterns, $nestedPatterns); + continue; + } + + // append this path to all of the paths that currently exist + array_walk($stringPatterns, function (&$value, $key) use ($var) { + $value .= "/" . $var; + }); + } + + $finalPatterns = array_merge($finalPatterns, array_fill_keys($stringPatterns, $key)); + } + + return $finalPatterns; + } + + /** + * Takes 2 arrays and appends all string in the second array to each entry in the first. + * + * @param string[] $parentStrings + * @param string[] $childStrings + * @return array + */ + private function mergeStrings($parentStrings, $childStrings) + { + $result = []; + foreach ($parentStrings as $pString) { + foreach ($childStrings as $cString) { + $result[] = $pString . "/" . $cString; + } + } + + return $result; + } } diff --git a/src/Magento/FunctionalTestingFramework/Config/Dom/NodeMergingConfig.php b/src/Magento/FunctionalTestingFramework/Config/Dom/NodeMergingConfig.php index 8990a43f7..e4231c8ec 100644 --- a/src/Magento/FunctionalTestingFramework/Config/Dom/NodeMergingConfig.php +++ b/src/Magento/FunctionalTestingFramework/Config/Dom/NodeMergingConfig.php @@ -27,7 +27,7 @@ class NodeMergingConfig /** * NodeMergingConfig constructor. * @param NodePathMatcher $nodePathMatcher - * @param array $idAttributes + * @param array $idAttributes */ public function __construct(NodePathMatcher $nodePathMatcher, array $idAttributes) { diff --git a/src/Magento/FunctionalTestingFramework/Config/Dom/NodePathMatcher.php b/src/Magento/FunctionalTestingFramework/Config/Dom/NodePathMatcher.php index 15f2c4160..2857bbea6 100644 --- a/src/Magento/FunctionalTestingFramework/Config/Dom/NodePathMatcher.php +++ b/src/Magento/FunctionalTestingFramework/Config/Dom/NodePathMatcher.php @@ -13,9 +13,9 @@ class NodePathMatcher /** * Whether a subject XPath matches to a given path pattern * - * @param string $pathPattern Example: '/some/static/path' or '/some/regexp/path(/item)+' - * @param string $xpathSubject Example: '/some[@attr="value"]/static/ns:path' - * @return bool + * @param string $pathPattern Example: '/some/static/path' or '/some/regexp/path(/item)+'. + * @param string $xpathSubject Example: '/some[@attr="value"]/static/ns:path'. + * @return boolean */ public function match($pathPattern, $xpathSubject) { diff --git a/src/Magento/FunctionalTestingFramework/Config/FileResolver/Root.php b/src/Magento/FunctionalTestingFramework/Config/FileResolver/Root.php index 93e74a82c..3b0940b28 100644 --- a/src/Magento/FunctionalTestingFramework/Config/FileResolver/Root.php +++ b/src/Magento/FunctionalTestingFramework/Config/FileResolver/Root.php @@ -11,7 +11,7 @@ class Root extends Module { - const ROOT_SUITE_DIR = "_suite"; + const ROOT_SUITE_DIR = "tests/_suite"; /** * Retrieve the list of configuration files with given name that relate to specified scope at the root level as well @@ -25,7 +25,7 @@ public function get($filename, $scope) { // first pick up the root level test suite dir $paths = glob( - dirname(TESTS_BP) . DIRECTORY_SEPARATOR . self::ROOT_SUITE_DIR + TESTS_BP . DIRECTORY_SEPARATOR . self::ROOT_SUITE_DIR . DIRECTORY_SEPARATOR . $filename ); diff --git a/src/Magento/FunctionalTestingFramework/Config/MftfApplicationConfig.php b/src/Magento/FunctionalTestingFramework/Config/MftfApplicationConfig.php index 4d4500b1e..eeabdf117 100644 --- a/src/Magento/FunctionalTestingFramework/Config/MftfApplicationConfig.php +++ b/src/Magento/FunctionalTestingFramework/Config/MftfApplicationConfig.php @@ -11,12 +11,13 @@ class MftfApplicationConfig { const GENERATION_PHASE = "generation"; const EXECUTION_PHASE = "execution"; - const MFTF_PHASES = [self::GENERATION_PHASE, self::EXECUTION_PHASE]; + const UNIT_TEST_PHASE = "testing"; + const MFTF_PHASES = [self::GENERATION_PHASE, self::EXECUTION_PHASE, self::UNIT_TEST_PHASE]; /** * Determines whether the user has specified a force option for generation * - * @var bool + * @var boolean */ private $forceGenerate; @@ -30,10 +31,17 @@ class MftfApplicationConfig /** * Determines whether the user would like to execute mftf in a verbose run. * - * @var bool + * @var boolean */ private $verboseEnabled; + /** + * Determines whether the user would like to execute mftf in a verbose run. + * + * @var boolean + */ + private $debugEnabled; + /** * MftfApplicationConfig Singelton Instance * @@ -44,13 +52,18 @@ class MftfApplicationConfig /** * MftfApplicationConfig constructor. * - * @param bool $forceGenerate - * @param string $phase - * @param bool $verboseEnabled + * @param boolean $forceGenerate + * @param string $phase + * @param boolean $verboseEnabled + * @param boolean $debugEnabled * @throws TestFrameworkException */ - private function __construct($forceGenerate = false, $phase = self::EXECUTION_PHASE, $verboseEnabled = null) - { + private function __construct( + $forceGenerate = false, + $phase = self::EXECUTION_PHASE, + $verboseEnabled = null, + $debugEnabled = null + ) { $this->forceGenerate = $forceGenerate; if (!in_array($phase, self::MFTF_PHASES)) { @@ -59,21 +72,24 @@ private function __construct($forceGenerate = false, $phase = self::EXECUTION_PH $this->phase = $phase; $this->verboseEnabled = $verboseEnabled; + $this->debugEnabled = $debugEnabled; } /** * Creates an instance of the configuration instance for reference once application has started. This function * returns void and is only run once during the lifetime of the application. * - * @param bool $forceGenerate - * @param string $phase - * @param bool $verboseEnabled + * @param boolean $forceGenerate + * @param string $phase + * @param boolean $verboseEnabled + * @param boolean $debugEnabled * @return void */ - public static function create($forceGenerate, $phase, $verboseEnabled) + public static function create($forceGenerate, $phase, $verboseEnabled, $debugEnabled) { if (self::$MFTF_APPLICATION_CONTEXT == null) { - self::$MFTF_APPLICATION_CONTEXT = new MftfApplicationConfig($forceGenerate, $phase, $verboseEnabled); + self::$MFTF_APPLICATION_CONTEXT = + new MftfApplicationConfig($forceGenerate, $phase, $verboseEnabled, $debugEnabled); } } @@ -97,7 +113,7 @@ public static function getConfig() /** * Returns a booelan indiciating whether or not the user has indicated a forced generation. * - * @return bool + * @return boolean */ public function forceGenerateEnabled() { @@ -108,13 +124,24 @@ public function forceGenerateEnabled() * Returns a boolean indicating whether the user has indicated a verbose run, which will cause all applicable * text to print to the console. * - * @return bool + * @return boolean */ public function verboseEnabled() { return $this->verboseEnabled ?? getenv('MFTF_DEBUG'); } + /** + * Returns a boolean indicating whether the user has indicated a debug run, which will lengthy validation + * with some extra error messaging to be run + * + * @return boolean + */ + public function debugEnabled() + { + return $this->debugEnabled ?? getenv('MFTF_DEBUG'); + } + /** * Returns a string which indicates the phase of mftf execution. * diff --git a/src/Magento/FunctionalTestingFramework/Config/MftfDom.php b/src/Magento/FunctionalTestingFramework/Config/MftfDom.php new file mode 100644 index 000000000..2a4c5f8b6 --- /dev/null +++ b/src/Magento/FunctionalTestingFramework/Config/MftfDom.php @@ -0,0 +1,62 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\FunctionalTestingFramework\Config; + +use Magento\FunctionalTestingFramework\Exceptions\Collector\ExceptionCollector; +use Magento\FunctionalTestingFramework\Exceptions\XmlException; +use Magento\FunctionalTestingFramework\Config\Dom\NodeMergingConfig; +use Magento\FunctionalTestingFramework\Config\Dom\NodePathMatcher; +use Magento\FunctionalTestingFramework\Test\Objects\ActionObject; +use Magento\FunctionalTestingFramework\Util\Validation\DuplicateNodeValidationUtil; + +/** + * Class MftfDom + * @package Magento\FunctionalTestingFramework\Config + */ +class MftfDom extends \Magento\FunctionalTestingFramework\Config\Dom +{ + /** + * MftfDom constructor. + * @param string $xml + * @param string $filename + * @param ExceptionCollector $exceptionCollector + * @param array $idAttributes + * @param string $typeAttributeName + * @param string $schemaFile + * @param string $errorFormat + */ + public function __construct( + $xml, + $filename, + $exceptionCollector, + array $idAttributes = [], + $typeAttributeName = null, + $schemaFile = null, + $errorFormat = self::ERROR_FORMAT_DEFAULT + ) { + $this->schemaFile = $schemaFile; + $this->nodeMergingConfig = new NodeMergingConfig(new NodePathMatcher(), $idAttributes); + $this->typeAttributeName = $typeAttributeName; + $this->errorFormat = $errorFormat; + $this->dom = $this->initDom($xml, $filename, $exceptionCollector); + $this->rootNamespace = $this->dom->lookupNamespaceUri($this->dom->namespaceURI); + } + + /** + * Redirects any merges into the init method for appending xml filename + * + * @param string $xml + * @param string|null $filename + * @param ExceptionCollector $exceptionCollector + * @return void + */ + public function merge($xml, $filename = null, $exceptionCollector = null) + { + $dom = $this->initDom($xml, $filename, $exceptionCollector); + $this->mergeNode($dom->documentElement, ''); + } +} diff --git a/src/Magento/FunctionalTestingFramework/Config/Reader.php b/src/Magento/FunctionalTestingFramework/Config/Reader.php index 9ceebb2f1..0c996f14c 100644 --- a/src/Magento/FunctionalTestingFramework/Config/Reader.php +++ b/src/Magento/FunctionalTestingFramework/Config/Reader.php @@ -24,14 +24,14 @@ class Reader extends \Magento\FunctionalTestingFramework\Config\Reader\Filesyste /** * Reader constructor. - * @param FileResolverInterface $fileResolver - * @param ConverterInterface $converter - * @param SchemaLocatorInterface $schemaLocator + * @param FileResolverInterface $fileResolver + * @param ConverterInterface $converter + * @param SchemaLocatorInterface $schemaLocator * @param ValidationStateInterface $validationState - * @param string $fileName - * @param array $idAttributes - * @param string $domDocumentClass - * @param string $defaultScope + * @param string $fileName + * @param array $idAttributes + * @param string $domDocumentClass + * @param string $defaultScope */ public function __construct( FileResolverInterface $fileResolver, @@ -40,7 +40,7 @@ public function __construct( ValidationStateInterface $validationState, $fileName = 'scenario.xml', $idAttributes = [], - $domDocumentClass = 'Magento\FunctionalTestingFramework\Config\Dom', + $domDocumentClass = Magento\FunctionalTestingFramework\Config\Dom::class, $defaultScope = 'etc' ) { $this->fileResolver = $fileResolver; diff --git a/src/Magento/FunctionalTestingFramework/Config/Reader/Filesystem.php b/src/Magento/FunctionalTestingFramework/Config/Reader/Filesystem.php index adcb330ba..4c3aa581b 100644 --- a/src/Magento/FunctionalTestingFramework/Config/Reader/Filesystem.php +++ b/src/Magento/FunctionalTestingFramework/Config/Reader/Filesystem.php @@ -5,6 +5,9 @@ */ namespace Magento\FunctionalTestingFramework\Config\Reader; +use Magento\FunctionalTestingFramework\Config\MftfApplicationConfig; +use Magento\FunctionalTestingFramework\Util\Logger\LoggingUtil; + /** * Filesystem configuration loader. Loads configuration from XML files, split by scopes. */ @@ -83,14 +86,14 @@ class Filesystem implements \Magento\FunctionalTestingFramework\Config\ReaderInt /** * Constructor * - * @param \Magento\FunctionalTestingFramework\Config\FileResolverInterface $fileResolver - * @param \Magento\FunctionalTestingFramework\Config\ConverterInterface $converter - * @param \Magento\FunctionalTestingFramework\Config\SchemaLocatorInterface $schemaLocator + * @param \Magento\FunctionalTestingFramework\Config\FileResolverInterface $fileResolver + * @param \Magento\FunctionalTestingFramework\Config\ConverterInterface $converter + * @param \Magento\FunctionalTestingFramework\Config\SchemaLocatorInterface $schemaLocator * @param \Magento\FunctionalTestingFramework\Config\ValidationStateInterface $validationState - * @param string $fileName - * @param array $idAttributes - * @param string $domDocumentClass - * @param string $defaultScope + * @param string $fileName + * @param array $idAttributes + * @param string $domDocumentClass + * @param string $defaultScope */ public function __construct( \Magento\FunctionalTestingFramework\Config\FileResolverInterface $fileResolver, @@ -144,23 +147,24 @@ protected function readFiles($fileList) /** @var \Magento\FunctionalTestingFramework\Config\Dom $configMerger */ $configMerger = null; foreach ($fileList as $key => $content) { + //check if file is empty and continue to next if it is + if (!$this->verifyFileEmpty($content, $fileList->getFilename())) { + continue; + } try { if (!$configMerger) { $configMerger = $this->createConfigMerger($this->domDocumentClass, $content); } else { $configMerger->merge($content); } + if (MftfApplicationConfig::getConfig()->debugEnabled()) { + $this->validateSchema($configMerger, $fileList->getFilename()); + } } catch (\Magento\FunctionalTestingFramework\Config\Dom\ValidationException $e) { throw new \Exception("Invalid XML in file " . $fileList->getFilename() . ":\n" . $e->getMessage()); } } - if ($this->validationState->isValidationRequired()) { - $errors = []; - if ($configMerger && !$configMerger->validate($this->schemaFile, $errors)) { - $message = "Invalid Document \n"; - throw new \Exception($message . implode("\n", $errors)); - } - } + $this->validateSchema($configMerger); $output = []; if ($configMerger) { @@ -192,4 +196,44 @@ protected function createConfigMerger($mergerClass, $initialContents) } return $result; } + + /** + * Checks if content is empty and logs warning, returns false if file is empty + * + * @param string $content + * @param string $fileName + * @return boolean + */ + protected function verifyFileEmpty($content, $fileName) + { + if (empty($content)) { + if (MftfApplicationConfig::getConfig()->verboseEnabled()) { + LoggingUtil::getInstance()->getLogger(Filesystem::class)->warn( + "XML File is empty.", + ["File" => $fileName] + ); + } + return false; + } + return true; + } + + /** + * Validate read xml against expected schema + * + * @param string $configMerger + * @param string $filename + * @throws \Exception + * @return void + */ + protected function validateSchema($configMerger, $filename = null) + { + if ($this->validationState->isValidationRequired()) { + $errors = []; + if ($configMerger && !$configMerger->validate($this->schemaFile, $errors)) { + $message = $filename ? $filename . PHP_EOL . "Invalid Document \n" : PHP_EOL . "Invalid Document \n"; + throw new \Exception($message . implode("\n", $errors)); + } + } + } } diff --git a/src/Magento/FunctionalTestingFramework/Test/Config/Reader/Filesystem.php b/src/Magento/FunctionalTestingFramework/Config/Reader/MftfFilesystem.php similarity index 74% rename from src/Magento/FunctionalTestingFramework/Test/Config/Reader/Filesystem.php rename to src/Magento/FunctionalTestingFramework/Config/Reader/MftfFilesystem.php index d96a926c5..728c4cb3a 100644 --- a/src/Magento/FunctionalTestingFramework/Test/Config/Reader/Filesystem.php +++ b/src/Magento/FunctionalTestingFramework/Config/Reader/MftfFilesystem.php @@ -4,12 +4,13 @@ * See COPYING.txt for license details. */ -namespace Magento\FunctionalTestingFramework\Test\Config\Reader; +namespace Magento\FunctionalTestingFramework\Config\Reader; +use Magento\FunctionalTestingFramework\Config\MftfApplicationConfig; use Magento\FunctionalTestingFramework\Exceptions\Collector\ExceptionCollector; use Magento\FunctionalTestingFramework\Util\Iterator\File; -class Filesystem extends \Magento\FunctionalTestingFramework\Config\Reader\Filesystem +class MftfFilesystem extends \Magento\FunctionalTestingFramework\Config\Reader\Filesystem { /** * Method to redirect file name passing into Dom class @@ -21,10 +22,13 @@ class Filesystem extends \Magento\FunctionalTestingFramework\Config\Reader\Files public function readFiles($fileList) { $exceptionCollector = new ExceptionCollector(); - $errors = []; /** @var \Magento\FunctionalTestingFramework\Test\Config\Dom $configMerger */ $configMerger = null; foreach ($fileList as $key => $content) { + //check if file is empty and continue to next if it is + if (!parent::verifyFileEmpty($content, $fileList->getFilename())) { + continue; + } try { if (!$configMerger) { $configMerger = $this->createConfigMerger( @@ -36,16 +40,16 @@ public function readFiles($fileList) } else { $configMerger->merge($content, $fileList->getFilename(), $exceptionCollector); } + if (MftfApplicationConfig::getConfig()->debugEnabled()) { + $this->validateSchema($configMerger, $fileList->getFilename()); + } } catch (\Magento\FunctionalTestingFramework\Config\Dom\ValidationException $e) { throw new \Exception("Invalid XML in file " . $key . ":\n" . $e->getMessage()); } } $exceptionCollector->throwException(); - if ($this->validationState->isValidationRequired()) { - if ($configMerger && !$configMerger->validate($this->schemaFile, $errors)) { - $message = "Invalid Document \n"; - throw new \Exception($message . implode("\n", $errors)); - } + if ($fileList->valid()) { + $this->validateSchema($configMerger, $fileList->getFilename()); } $output = []; @@ -58,9 +62,9 @@ public function readFiles($fileList) /** * Return newly created instance of a config merger * - * @param string $mergerClass - * @param string $initialContents - * @param string $filename + * @param string $mergerClass + * @param string $initialContents + * @param string $filename * @param ExceptionCollector $exceptionCollector * @return \Magento\FunctionalTestingFramework\Config\Dom * @throws \UnexpectedValueException diff --git a/src/Magento/FunctionalTestingFramework/Config/ReplacerInterface.php b/src/Magento/FunctionalTestingFramework/Config/ReplacerInterface.php index 6652f9bc8..ce77a6c42 100644 --- a/src/Magento/FunctionalTestingFramework/Config/ReplacerInterface.php +++ b/src/Magento/FunctionalTestingFramework/Config/ReplacerInterface.php @@ -14,7 +14,7 @@ interface ReplacerInterface /** * Apply specified node in 'replace' attribute instead of original. * - * @param array &$output + * @param array $output * @return array */ public function apply(array &$output); diff --git a/src/Magento/FunctionalTestingFramework/Config/SchemaLocator.php b/src/Magento/FunctionalTestingFramework/Config/SchemaLocator.php index fe88e39e6..a92f536a3 100644 --- a/src/Magento/FunctionalTestingFramework/Config/SchemaLocator.php +++ b/src/Magento/FunctionalTestingFramework/Config/SchemaLocator.php @@ -28,7 +28,7 @@ class SchemaLocator implements \Magento\FunctionalTestingFramework\Config\Schema /** * Class constructor * - * @param string $schemaPath + * @param string $schemaPath * @param string|null $perFileSchema */ public function __construct($schemaPath, $perFileSchema = null) diff --git a/src/Magento/FunctionalTestingFramework/Console/BuildProjectCommand.php b/src/Magento/FunctionalTestingFramework/Console/BuildProjectCommand.php index 6e29ac071..350a004f3 100644 --- a/src/Magento/FunctionalTestingFramework/Console/BuildProjectCommand.php +++ b/src/Magento/FunctionalTestingFramework/Console/BuildProjectCommand.php @@ -1,5 +1,4 @@ <?php -// @codingStandardsIgnoreFile /** * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. @@ -8,6 +7,7 @@ namespace Magento\FunctionalTestingFramework\Console; +use Magento\FunctionalTestingFramework\Util\Logger\LoggingUtil; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\ArrayInput; use Symfony\Component\Filesystem\Filesystem; @@ -16,9 +16,19 @@ use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Process\Process; use Magento\FunctionalTestingFramework\Util\Env\EnvProcessor; +use Symfony\Component\Yaml\Yaml; +/** + * Class BuildProjectCommand + * @package Magento\FunctionalTestingFramework\Console + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ class BuildProjectCommand extends Command { + const DEFAULT_YAML_INLINE_DEPTH = 10; + const CREDENTIALS_FILE_PATH = TESTS_BP . DIRECTORY_SEPARATOR . '.credentials.example'; + /** * Env processor manages .env files. * @@ -33,9 +43,15 @@ class BuildProjectCommand extends Command */ protected function configure() { - $this->setName('build:project'); - $this->setDescription('Generate configuration files for the project. Build the Codeception project.'); - $this->envProcessor = new EnvProcessor(BP . DIRECTORY_SEPARATOR . '.env'); + $this->setName('build:project') + ->setDescription('Generate configuration files for the project. Build the Codeception project.') + ->addOption( + "upgrade", + 'u', + InputOption::VALUE_NONE, + 'upgrade existing MFTF tests according to last major release requiements' + ); + $this->envProcessor = new EnvProcessor(TESTS_BP . DIRECTORY_SEPARATOR . '.env'); $env = $this->envProcessor->getEnv(); foreach ($env as $key => $value) { $this->addOption($key, null, InputOption::VALUE_REQUIRED, '', $value); @@ -45,26 +61,20 @@ protected function configure() /** * Executes the current command. * - * @param InputInterface $input + * @param InputInterface $input * @param OutputInterface $output * @return void * @throws \Symfony\Component\Console\Exception\LogicException + * + * @SuppressWarnings(PHPMD.UnusedLocalVariable) */ protected function execute(InputInterface $input, OutputInterface $output) { - $fileSystem = new Filesystem(); - $fileSystem->copy( - BP . DIRECTORY_SEPARATOR . 'codeception.dist.yml', - BP . DIRECTORY_SEPARATOR . 'codeception.yml' - ); - $output->writeln("codeception.yml configuration successfully applied.\n"); - $fileSystem->copy( - BP . DIRECTORY_SEPARATOR . 'dev' . DIRECTORY_SEPARATOR . 'tests' . DIRECTORY_SEPARATOR . - 'functional' . DIRECTORY_SEPARATOR . 'MFTF.suite.dist.yml', - BP . DIRECTORY_SEPARATOR . 'dev' . DIRECTORY_SEPARATOR . 'tests' . DIRECTORY_SEPARATOR . - 'functional' . DIRECTORY_SEPARATOR . 'MFTF.suite.yml' - ); - $output->writeln("MFTF.suite.yml configuration successfully applied.\n"); + $resetCommand = new CleanProjectCommand(); + $resetOptions = new ArrayInput([]); + $resetCommand->run($resetOptions, $output); + + $this->generateConfigFiles($output); $setupEnvCommand = new SetupEnvCommand(); $commandInput = []; @@ -78,12 +88,78 @@ protected function execute(InputInterface $input, OutputInterface $output) $commandInput = new ArrayInput($commandInput); $setupEnvCommand->run($commandInput, $output); - $process = new Process('vendor/bin/codecept build'); - $process->run(); - if ($process->isSuccessful()) { - $output->writeln("Codeception build run successfully.\n"); + // TODO can we just import the codecept symfony command? + $codeceptBuildCommand = realpath(PROJECT_ROOT . '/vendor/bin/codecept') . ' build'; + $process = new Process($codeceptBuildCommand); + $process->setWorkingDirectory(TESTS_BP); + $process->setIdleTimeout(600); + $process->setTimeout(0); + $process->run( + function ($type, $buffer) use ($output) { + if ($output->isVerbose()) { + $output->write($buffer); + } + } + ); + + if ($input->getOption('upgrade')) { + $upgradeCommand = new UpgradeTestsCommand(); + $upgradeOptions = new ArrayInput(['path' => TESTS_MODULE_PATH]); + $upgradeCommand->run($upgradeOptions, $output); + } + } + + /** + * Generates needed codeception configuration files to the TEST_BP directory + * + * @param OutputInterface $output + * @return void + */ + private function generateConfigFiles(OutputInterface $output) + { + $fileSystem = new Filesystem(); + //Find travel path from codeception.yml to FW_BP + $relativePath = $fileSystem->makePathRelative(FW_BP, TESTS_BP); + + if (!$fileSystem->exists(TESTS_BP . DIRECTORY_SEPARATOR . 'codeception.yml')) { + // read in the codeception.yml file + $configDistYml = Yaml::parse(file_get_contents(realpath(FW_BP . "/etc/config/codeception.dist.yml"))); + $configDistYml['paths']['support'] = $relativePath . 'src/Magento/FunctionalTestingFramework'; + $configDistYml['paths']['envs'] = $relativePath . 'etc/_envs'; + $configYmlText = Yaml::dump($configDistYml, self::DEFAULT_YAML_INLINE_DEPTH); + + // dump output to new codeception.yml file + file_put_contents(TESTS_BP . DIRECTORY_SEPARATOR . 'codeception.yml', $configYmlText); + $output->writeln("codeception.yml configuration successfully applied."); + } + + if ($output->isVerbose()) { + $output->writeln("codeception.yml applied to " . TESTS_BP . DIRECTORY_SEPARATOR . 'codeception.yml'); } - $output->writeln('<info>The project built successfully.</info>'); + // copy the functional suite yml, will only copy if there are differences between the template the destination + $fileSystem->copy( + realpath(FW_BP . '/etc/config/functional.suite.dist.yml'), + TESTS_BP . DIRECTORY_SEPARATOR . 'tests' . DIRECTORY_SEPARATOR . 'functional.suite.yml' + ); + $output->writeln('functional.suite.yml configuration successfully applied.'); + + if ($output->isVerbose()) { + $output->writeln("functional.suite.yml applied to " . + TESTS_BP . DIRECTORY_SEPARATOR . 'tests' . DIRECTORY_SEPARATOR . 'functional.suite.yml'); + } + + $fileSystem->copy( + FW_BP . '/etc/config/.credentials.example', + self::CREDENTIALS_FILE_PATH + ); + + // Remove and Create Log File + $logPath = LoggingUtil::getInstance()->getLoggingPath(); + $fileSystem->remove($logPath); + $fileSystem->touch($logPath); + $fileSystem->chmod($logPath, 0777); + + $output->writeln('.credentials.example successfully applied.'); } } diff --git a/src/Magento/FunctionalTestingFramework/Console/CleanProjectCommand.php b/src/Magento/FunctionalTestingFramework/Console/CleanProjectCommand.php new file mode 100644 index 000000000..b2b2c01f3 --- /dev/null +++ b/src/Magento/FunctionalTestingFramework/Console/CleanProjectCommand.php @@ -0,0 +1,96 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types = 1); + +namespace Magento\FunctionalTestingFramework\Console; + +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Filesystem\Filesystem; +use Symfony\Component\Finder\Finder; + +class CleanProjectCommand extends Command +{ + const CONFIGURATION_FILES = [ + // codeception.yml file for top level config + TESTS_BP . DIRECTORY_SEPARATOR . 'codeception.yml', + // functional.suite.yml for test execution config + TESTS_BP . DIRECTORY_SEPARATOR . 'tests' . DIRECTORY_SEPARATOR . 'functional.suite.yml', + // Acceptance Tester Actions generated by codeception + FW_BP . '/src/Magento/FunctionalTestingFramework/_generated', + // AcceptanceTester Class generated by codeception + FW_BP . '/src/Magento/FunctionalTestingFramework/AcceptanceTester.php' + ]; + + const GENERATED_FILES = [ + TESTS_MODULE_PATH . '/_generated' + ]; + + /** + * Configures the current command. + * + * @return void + */ + protected function configure() + { + $this->setName('reset') + ->setDescription( + 'This command will clean any configuration files from the environment (not including .env), as well as generated artifacts.' // phpcs:ignore + ) + ->addOption('hard', null, InputOption::VALUE_NONE, "parameter to force reset of configuration files."); + } + + /** + * Executes the current command. + * + * @param InputInterface $input + * @param OutputInterface $output + * @return void + * @throws \Symfony\Component\Console\Exception\LogicException + */ + protected function execute(InputInterface $input, OutputInterface $output) + { + $isHardReset = $input->getOption('hard'); + $fileSystem = new Filesystem(); + $finder = new Finder(); + $finder->files()->name('*.php')->in(realpath(FW_BP . '/src/Magento/FunctionalTestingFramework/Group/')); + $filesForRemoval = []; + + // include config files if user specifies a hard reset for deletion + if ($isHardReset) { + $filesForRemoval = array_merge($filesForRemoval, self::CONFIGURATION_FILES); + } + + // include the files mftf generates during test execution in TESTS_BP for deletion + $filesForRemoval = array_merge($filesForRemoval, self::GENERATED_FILES); + + if ($output->isVerbose()) { + $output->writeln('Deleting Files:'); + } + + // delete any suite based group files + foreach ($finder->files() as $file) { + if ($output->isVerbose()) { + $output->writeln($file->getRealPath()); + } + + $fileSystem->remove($file); + } + + // delete files specified for removal + foreach ($filesForRemoval as $fileForRemoval) { + if ($fileSystem->exists($fileForRemoval) && $output->isVerbose()) { + $output->writeln($fileForRemoval); + } + + $fileSystem->remove($fileForRemoval); + } + + $output->writeln('mftf files removed from filesystem.'); + } +} diff --git a/src/Magento/FunctionalTestingFramework/Console/CommandList.php b/src/Magento/FunctionalTestingFramework/Console/CommandList.php new file mode 100644 index 000000000..362b29296 --- /dev/null +++ b/src/Magento/FunctionalTestingFramework/Console/CommandList.php @@ -0,0 +1,50 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\FunctionalTestingFramework\Console; + +/** + * Class CommandList has a list of commands. + * @codingStandardsIgnoreFile + */ +class CommandList implements CommandListInterface +{ + /** + * List of Commands + * @var \Symfony\Component\Console\Command\Command[] + */ + private $commands; + + /** + * Constructor + * + * @param array $commands + */ + public function __construct(array $commands = []) + { + $this->commands = [ + 'build:project' => new BuildProjectCommand(), + 'reset' => new CleanProjectCommand(), + 'generate:urn-catalog' => new GenerateDevUrnCommand(), + 'generate:suite' => new GenerateSuiteCommand(), + 'generate:tests' => new GenerateTestsCommand(), + 'run:test' => new RunTestCommand(), + 'run:group' => new RunTestGroupCommand(), + 'setup:env' => new SetupEnvCommand(), + 'upgrade:tests' => new UpgradeTestsCommand(), + ] + $commands; + } + + /** + * {@inheritdoc} + */ + public function getCommands() + { + return $this->commands; + } +} diff --git a/src/Magento/FunctionalTestingFramework/Console/CommandListInterface.php b/src/Magento/FunctionalTestingFramework/Console/CommandListInterface.php new file mode 100644 index 000000000..050f7e3c0 --- /dev/null +++ b/src/Magento/FunctionalTestingFramework/Console/CommandListInterface.php @@ -0,0 +1,20 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\FunctionalTestingFramework\Console; + +/** + * Contains a list of Console commands + * @api + */ +interface CommandListInterface +{ + /** + * Gets list of command instances + * + * @return \Symfony\Component\Console\Command\Command[] + */ + public function getCommands(); +} diff --git a/src/Magento/FunctionalTestingFramework/Console/GenerateDevUrnCommand.php b/src/Magento/FunctionalTestingFramework/Console/GenerateDevUrnCommand.php new file mode 100644 index 000000000..1147704c0 --- /dev/null +++ b/src/Magento/FunctionalTestingFramework/Console/GenerateDevUrnCommand.php @@ -0,0 +1,134 @@ +<?php +// @codingStandardsIgnoreFile +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types = 1); + +namespace Magento\FunctionalTestingFramework\Console; + +use Magento\FunctionalTestingFramework\Exceptions\TestFrameworkException; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Input\InputOption; +use Magento\FunctionalTestingFramework\Util\Logger\LoggingUtil; + +class GenerateDevUrnCommand extends Command +{ + /** + * Configures the current command. + * + * @return void + */ + protected function configure() + { + $this->setName('generate:urn-catalog') + ->setDescription('This command generates an URN catalog to enable PHPStorm to recognize and highlight URNs.') + ->addArgument('path', InputArgument::REQUIRED, 'path to PHPStorm misc.xml file (typically located in [ProjectRoot]/.idea/misc.xml)') + ->addOption( + "force", + 'f', + InputOption::VALUE_NONE, + 'forces creation of misc.xml file if not found in the path given.' + ); + } + + /** + * Executes the current command. + * + * @param InputInterface $input + * @param OutputInterface $output + * @return void + * @throws \Magento\FunctionalTestingFramework\Exceptions\TestFrameworkException + */ + protected function execute(InputInterface $input, OutputInterface $output) + { + $miscXmlFilePath = $input->getArgument('path') . DIRECTORY_SEPARATOR . "misc.xml"; + $miscXmlFile = realpath($miscXmlFilePath); + $force = $input->getOption('force'); + + if ($miscXmlFile === false) { + if ($force == true) { + // create file and refresh realpath + $xml = "<project version=\"4\"/>"; + file_put_contents($miscXmlFilePath, $xml); + $miscXmlFile = realpath($miscXmlFilePath); + } else { + $exceptionMessage = "misc.xml not found in given path '{$miscXmlFilePath}'"; + LoggingUtil::getInstance()->getLogger(GenerateDevUrnCommand::class) + ->error($exceptionMessage); + throw new TestFrameworkException($exceptionMessage); + } + } + $dom = new \DOMDocument('1.0'); + $dom->preserveWhiteSpace = false; + $dom->formatOutput = true; + $dom->loadXML(file_get_contents($miscXmlFile)); + + //Locate ProjectResources node, create one if none are found. + $nodeForWork = null; + foreach($dom->getElementsByTagName('component') as $child) { + if ($child->getAttribute('name') === 'ProjectResources') { + $nodeForWork = $child; + } + } + if ($nodeForWork === null) { + $project = $dom->getElementsByTagName('project')->item(0); + $nodeForWork = $dom->createElement('component'); + $nodeForWork->setAttribute('name', 'ProjectResources'); + $project->appendChild($nodeForWork); + } + + //Extract url=>location mappings that already exist, add MFTF URNs and reappend + $resources = []; + $resourceNodes = $nodeForWork->getElementsByTagName('resource'); + $resourceCount = $resourceNodes->length; + for ($i = 0; $i < $resourceCount; $i++) { + $child = $resourceNodes[0]; + $resources[$child->getAttribute('url')] = $child->getAttribute('location'); + $child->parentNode->removeChild($child); + } + + $resources = array_merge($resources, $this->generateResourcesArray()); + + foreach ($resources as $url => $location) { + $resourceNode = $dom->createElement('resource'); + $resourceNode->setAttribute('url', $url); + $resourceNode->setAttribute('location', $location); + $nodeForWork->appendChild($resourceNode); + } + + //Save output + $dom->save($miscXmlFile); + $output->writeln("MFTF URN mapping successfully added to {$miscXmlFile}."); + } + + /** + * Generates urn => location array for all MFTF schema. + * @return array + */ + private function generateResourcesArray() + { + $resourcesArray = [ + 'urn:magento:mftf:DataGenerator/etc/dataOperation.xsd' => + realpath(FW_BP . '/src/Magento/FunctionalTestingFramework/DataGenerator/etc/dataOperation.xsd'), + 'urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd' => + realpath(FW_BP . '/src/Magento/FunctionalTestingFramework/DataGenerator/etc/dataProfileSchema.xsd'), + 'urn:magento:mftf:Page/etc/PageObject.xsd' => + realpath(FW_BP . '/src/Magento/FunctionalTestingFramework/Page/etc/PageObject.xsd'), + 'urn:magento:mftf:Page/etc/SectionObject.xsd' => + realpath(FW_BP . '/src/Magento/FunctionalTestingFramework/Page/etc/SectionObject.xsd'), + 'urn:magento:mftf:Test/etc/actionGroupSchema.xsd' => + realpath(FW_BP . '/src/Magento/FunctionalTestingFramework/Test/etc/actionGroupSchema.xsd'), + 'urn:magento:mftf:Test/etc/testSchema.xsd' => + realpath(FW_BP . '/src/Magento/FunctionalTestingFramework/Test/etc/testSchema.xsd'), + 'urn:magento:mftf:Suite/etc/suiteSchema.xsd' => + realpath(FW_BP . '/src/Magento/FunctionalTestingFramework/Suite/etc/suiteSchema.xsd') + ]; + return $resourcesArray; + } + +} diff --git a/src/Magento/FunctionalTestingFramework/Console/GenerateSuiteCommand.php b/src/Magento/FunctionalTestingFramework/Console/GenerateSuiteCommand.php new file mode 100644 index 000000000..ca9be1fac --- /dev/null +++ b/src/Magento/FunctionalTestingFramework/Console/GenerateSuiteCommand.php @@ -0,0 +1,55 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types = 1); + +namespace Magento\FunctionalTestingFramework\Console; + +use Magento\FunctionalTestingFramework\Suite\SuiteGenerator; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; + +class GenerateSuiteCommand extends Command +{ + /** + * Configures the current command. + * + * @return void + */ + protected function configure() + { + $this->setName('generate:suite') + ->setDescription('This command generates a single suite based on declaration in xml') + ->addArgument( + 'suites', + InputArgument::IS_ARRAY | InputArgument::REQUIRED, + 'argument which indicates suite names for generation (separated by space)' + ); + } + + /** + * Executes the current command. + * + * @param InputInterface $input + * @param OutputInterface $output + * @return integer|null|void + * @throws \Exception + */ + protected function execute(InputInterface $input, OutputInterface $output) + { + $suites = $input->getArgument('suites'); + + foreach ($suites as $suite) { + SuiteGenerator::getInstance()->generateSuite($suite); + if ($output->isVerbose()) { + $output->writeLn("suite $suite generated"); + } + } + + $output->writeLn("Suites Generated"); + } +} diff --git a/src/Magento/FunctionalTestingFramework/Console/GenerateTestsCommand.php b/src/Magento/FunctionalTestingFramework/Console/GenerateTestsCommand.php new file mode 100644 index 000000000..845fb5151 --- /dev/null +++ b/src/Magento/FunctionalTestingFramework/Console/GenerateTestsCommand.php @@ -0,0 +1,173 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types = 1); + +namespace Magento\FunctionalTestingFramework\Console; + +use Magento\FunctionalTestingFramework\Config\MftfApplicationConfig; +use Magento\FunctionalTestingFramework\Exceptions\TestFrameworkException; +use Magento\FunctionalTestingFramework\Suite\SuiteGenerator; +use Magento\FunctionalTestingFramework\Test\Handlers\TestObjectHandler; +use Magento\FunctionalTestingFramework\Util\Manifest\ParallelTestManifest; +use Magento\FunctionalTestingFramework\Util\Manifest\TestManifestFactory; +use Magento\FunctionalTestingFramework\Util\TestGenerator; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Output\OutputInterface; + +class GenerateTestsCommand extends Command +{ + /** + * Configures the current command. + * + * @return void + */ + protected function configure() + { + $this->setName('generate:tests') + ->setDescription('This command generates all test files and suites based on xml declarations') + ->addArgument( + 'name', + InputArgument::OPTIONAL | InputArgument::IS_ARRAY, + 'name(s) of specific tests to generate' + )->addOption("config", 'c', InputOption::VALUE_REQUIRED, 'default, singleRun, or parallel', 'default') + ->addOption( + "force", + 'f', + InputOption::VALUE_NONE, + 'force generation of tests regardless of Magento Instance Configuration' + )->addOption( + 'time', + 'i', + InputOption::VALUE_REQUIRED, + 'Used in combination with a parallel configuration, determines desired group size (in minutes)', + 10 + )->addOption( + 'tests', + 't', + InputOption::VALUE_REQUIRED, + 'A parameter accepting a JSON string used to determine the test configuration' + )->addOption('debug', 'd', InputOption::VALUE_NONE, 'run extra validation when generating tests'); + } + + /** + * Executes the current command. + * + * @param InputInterface $input + * @param OutputInterface $output + * @return void + * @throws TestFrameworkException + * @throws \Magento\FunctionalTestingFramework\Exceptions\TestReferenceException + * @throws \Magento\FunctionalTestingFramework\Exceptions\XmlException + */ + protected function execute(InputInterface $input, OutputInterface $output) + { + $tests = $input->getArgument('name'); + $config = $input->getOption('config'); + $json = $input->getOption('tests'); + $force = $input->getOption('force'); + $time = $input->getOption('time') * 60 * 1000; // convert from minutes to milliseconds + $debug = $input->getOption('debug'); + $verbose = $output->isVerbose(); + + if ($json !== null && !json_decode($json)) { + // stop execution if we have failed to properly parse any json passed in by the user + throw new TestFrameworkException("JSON could not be parsed: " . json_last_error_msg()); + } + + if ($config === 'parallel' && $time <= 0) { + // stop execution if the user has given us an invalid argument for time argument during parallel generation + throw new TestFrameworkException("time option cannot be less than or equal to 0"); + } + + $testConfiguration = $this->createTestConfiguration($json, $tests, $force, $debug, $verbose); + + // create our manifest file here + $testManifest = TestManifestFactory::makeManifest($config, $testConfiguration['suites']); + TestGenerator::getInstance(null, $testConfiguration['tests'])->createAllTestFiles($testManifest); + + if ($config == 'parallel') { + /** @var ParallelTestManifest $testManifest */ + $testManifest->createTestGroups($time); + } + + if (empty($tests)) { + SuiteGenerator::getInstance()->generateAllSuites($testManifest); + } + + $testManifest->generate(); + + $output->writeln("Generate Tests Command Run"); + } + + /** + * Function which builds up a configuration including test and suites for consumption of Magento generation methods. + * + * @param string $json + * @param array $tests + * @param boolean $force + * @param boolean $debug + * @param boolean $verbose + * @return array + * @throws \Magento\FunctionalTestingFramework\Exceptions\TestReferenceException + * @throws \Magento\FunctionalTestingFramework\Exceptions\XmlException + */ + private function createTestConfiguration($json, array $tests, bool $force, bool $debug, bool $verbose) + { + // set our application configuration so we can references the user options in our framework + MftfApplicationConfig::create( + $force, + MftfApplicationConfig::GENERATION_PHASE, + $verbose, + $debug + ); + + $testConfiguration = []; + $testConfiguration['tests'] = $tests; + $testConfiguration['suites'] = []; + + $testConfiguration = $this->parseTestsConfigJson($json, $testConfiguration); + + // if we have references to specific tests, we resolve the test objects and pass them to the config + if (!empty($testConfiguration['tests'])) { + $testObjects = []; + + foreach ($testConfiguration['tests'] as $test) { + $testObjects[$test] = TestObjectHandler::getInstance()->getObject($test); + } + + $testConfiguration['tests'] = $testObjects; + } + + return $testConfiguration; + } + + /** + * Function which takes a json string of potential custom configuration and parses/validates the resulting json + * passed in by the user. The result is a testConfiguration array. + * + * @param string $json + * @param array $testConfiguration + * @throws TestFrameworkException + * @return array + */ + private function parseTestsConfigJson($json, array $testConfiguration) + { + if ($json === null) { + return $testConfiguration; + } + + $jsonTestConfiguration = []; + $testConfigArray = json_decode($json, true); + + $jsonTestConfiguration['tests'] = $testConfigArray['tests'] ?? null; + ; + $jsonTestConfiguration['suites'] = $testConfigArray['suites'] ?? null; + return $jsonTestConfiguration; + } +} diff --git a/src/Magento/FunctionalTestingFramework/Console/RunTestCommand.php b/src/Magento/FunctionalTestingFramework/Console/RunTestCommand.php new file mode 100644 index 000000000..9bf250ed0 --- /dev/null +++ b/src/Magento/FunctionalTestingFramework/Console/RunTestCommand.php @@ -0,0 +1,84 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types = 1); + +namespace Magento\FunctionalTestingFramework\Console; + +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\ArrayInput; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Debug\Debug; +use Symfony\Component\Process\Process; + +class RunTestCommand extends Command +{ + /** + * Configures the current command. + * + * @return void + */ + protected function configure() + { + $this->setName("run:test") + ->setDescription("generation and execution of test(s) defined in xml") + ->addArgument( + 'name', + InputArgument::REQUIRED | InputArgument::IS_ARRAY, + "name of tests to generate and execute" + )->addOption('skip-generate', 'k', InputOption::VALUE_NONE, "skip generation and execute existing test") + ->addOption( + "force", + 'f', + InputOption::VALUE_NONE, + 'force generation of tests regardless of Magento Instance Configuration' + ); + } + + /** + * Executes the current command. + * + * @param InputInterface $input + * @param OutputInterface $output + * @return integer|null|void + * @throws \Exception + * + * @SuppressWarnings(PHPMD.UnusedLocalVariable) + */ + protected function execute(InputInterface $input, OutputInterface $output) + { + $tests = $input->getArgument('name'); + $skipGeneration = $input->getOption('skip-generate'); + $force = $input->getOption('force'); + + if (!$skipGeneration) { + $command = $this->getApplication()->find('generate:tests'); + $args = [ + '--tests' => json_encode([ + 'tests' => $tests, + 'suites' => null + ]), + '--force' => $force + ]; + $command->run(new ArrayInput($args), $output); + } + + // we only generate relevant tests here so we can execute "all tests" + $codeceptionCommand = realpath(PROJECT_ROOT . '/vendor/bin/codecept') . " run functional --verbose --steps"; + + $process = new Process($codeceptionCommand); + $process->setWorkingDirectory(TESTS_BP); + $process->setIdleTimeout(600); + $process->setTimeout(0); + $process->run( + function ($type, $buffer) use ($output) { + $output->write($buffer); + } + ); + } +} diff --git a/src/Magento/FunctionalTestingFramework/Console/RunTestGroupCommand.php b/src/Magento/FunctionalTestingFramework/Console/RunTestGroupCommand.php new file mode 100644 index 000000000..110b86565 --- /dev/null +++ b/src/Magento/FunctionalTestingFramework/Console/RunTestGroupCommand.php @@ -0,0 +1,128 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types = 1); + +namespace Magento\FunctionalTestingFramework\Console; + +use Magento\FunctionalTestingFramework\Suite\Handlers\SuiteObjectHandler; +use Magento\FunctionalTestingFramework\Config\MftfApplicationConfig; +use Magento\FunctionalTestingFramework\Test\Handlers\TestObjectHandler; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\ArrayInput; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Process\Process; + +class RunTestGroupCommand extends Command +{ + /** + * Configures the current command. + * + * @return void + */ + protected function configure() + { + $this->setName('run:group') + ->setDescription('Execute a set of tests referenced via group annotations') + ->addOption( + 'skip-generate', + 'k', + InputOption::VALUE_NONE, + "only execute a group of tests without generating from source xml" + )->addOption( + "force", + 'f', + InputOption::VALUE_NONE, + 'force generation of tests regardless of Magento Instance Configuration' + )->addArgument( + 'groups', + InputArgument::IS_ARRAY | InputArgument::REQUIRED, + 'group names to be executed via codeception' + ); + } + + /** + * Executes the current command. + * + * @param InputInterface $input + * @param OutputInterface $output + * @return integer|null|void + * @throws \Exception + * + * @SuppressWarnings(PHPMD.UnusedLocalVariable) + */ + protected function execute(InputInterface $input, OutputInterface $output) + { + $skipGeneration = $input->getOption('skip-generate'); + $force = $input->getOption('force'); + $groups = $input->getArgument('groups'); + + // Create Mftf Configuration + MftfApplicationConfig::create( + $force, + MftfApplicationConfig::GENERATION_PHASE, + false, + false + ); + + if (!$skipGeneration) { + $testConfiguration = $this->getGroupAndSuiteConfiguration($groups); + $command = $this->getApplication()->find('generate:tests'); + $args = [ + '--tests' => $testConfiguration, + '--force' => $force + ]; + + $command->run(new ArrayInput($args), $output); + } + + $codeceptionCommand = realpath(PROJECT_ROOT . '/vendor/bin/codecept') . ' run functional --verbose --steps'; + + foreach ($groups as $group) { + $codeceptionCommand .= " -g {$group}"; + } + + $process = new Process($codeceptionCommand); + $process->setWorkingDirectory(TESTS_BP); + $process->setIdleTimeout(600); + $process->setTimeout(0); + $process->run( + function ($type, $buffer) use ($output) { + $output->write($buffer); + } + ); + } + + /** + * Returns a json string to be used as an argument for generation of a group or suite + * + * @param array $groups + * @return string + * @throws \Magento\FunctionalTestingFramework\Exceptions\XmlException + */ + private function getGroupAndSuiteConfiguration(array $groups) + { + $testConfiguration['tests'] = []; + $testConfiguration['suites'] = null; + $availableSuites = SuiteObjectHandler::getInstance()->getAllObjects(); + + foreach ($groups as $group) { + if (array_key_exists($group, $availableSuites)) { + $testConfiguration['suites'][$group] = []; + } + + $testConfiguration['tests'] = array_merge( + $testConfiguration['tests'], + array_keys(TestObjectHandler::getInstance()->getTestsByGroup($group)) + ); + } + + $testConfigurationJson = json_encode($testConfiguration); + return $testConfigurationJson; + } +} diff --git a/src/Magento/FunctionalTestingFramework/Console/SetupEnvCommand.php b/src/Magento/FunctionalTestingFramework/Console/SetupEnvCommand.php index e0ef6053a..4d1ce0334 100644 --- a/src/Magento/FunctionalTestingFramework/Console/SetupEnvCommand.php +++ b/src/Magento/FunctionalTestingFramework/Console/SetupEnvCommand.php @@ -1,5 +1,4 @@ <?php -// @codingStandardsIgnoreFile /** * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. @@ -31,9 +30,9 @@ class SetupEnvCommand extends Command */ protected function configure() { - $this->setName('setup:env'); - $this->setDescription("Generate .env file."); - $this->envProcessor = new EnvProcessor(BP . DIRECTORY_SEPARATOR . '.env'); + $this->setName('setup:env') + ->setDescription("Generate .env file."); + $this->envProcessor = new EnvProcessor(TESTS_BP . DIRECTORY_SEPARATOR . '.env'); $env = $this->envProcessor->getEnv(); foreach ($env as $key => $value) { $this->addOption($key, null, InputOption::VALUE_REQUIRED, '', $value); @@ -43,7 +42,7 @@ protected function configure() /** * Executes the current command. * - * @param InputInterface $input + * @param InputInterface $input * @param OutputInterface $output * @return void * @throws \Symfony\Component\Console\Exception\LogicException @@ -59,6 +58,6 @@ protected function execute(InputInterface $input, OutputInterface $output) $userEnv[$key] = $input->getOption($key); } $this->envProcessor->putEnvFile($userEnv); - $output->writeln(".env configuration successfully applied.\n"); + $output->writeln(".env configuration successfully applied."); } } diff --git a/src/Magento/FunctionalTestingFramework/Console/UpgradeTestsCommand.php b/src/Magento/FunctionalTestingFramework/Console/UpgradeTestsCommand.php new file mode 100644 index 000000000..8e24290b5 --- /dev/null +++ b/src/Magento/FunctionalTestingFramework/Console/UpgradeTestsCommand.php @@ -0,0 +1,58 @@ +<?php +// @codingStandardsIgnoreFile +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types = 1); + +namespace Magento\FunctionalTestingFramework\Console; + +use Magento\FunctionalTestingFramework\Upgrade\UpgradeScriptList; +use Magento\FunctionalTestingFramework\Util\Logger\LoggingUtil; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Output\OutputInterface; + +class UpgradeTestsCommand extends Command +{ + /** + * Pool of upgrade scripts to run + * + * @var \Magento\FunctionalTestingFramework\Upgrade\UpgradeScriptListInterface + */ + private $upgradeScriptsList; + + /** + * Configures the current command. + * + * @return void + */ + protected function configure() + { + $this->setName('upgrade:tests') + ->setDescription('This command will upgrade all tests in the provided path according to new MFTF Major version requirements.') + ->addArgument('path', InputArgument::REQUIRED, 'path to MFTF tests to upgrade'); + $this->upgradeScriptsList = new UpgradeScriptList(); + } + + /** + * + * + * @param InputInterface $input + * @param OutputInterface $output + * @return int|null|void + * @throws \Exception + */ + protected function execute(InputInterface $input, OutputInterface $output) + { + /** @var \Magento\FunctionalTestingFramework\Upgrade\UpgradeInterface[] $upgradeScriptObjects */ + $upgradeScriptObjects = $this->upgradeScriptsList->getUpgradeScripts(); + foreach ($upgradeScriptObjects as $upgradeScriptObject) { + $upgradeOutput = $upgradeScriptObject->execute($input); + LoggingUtil::getInstance()->getLogger(get_class($upgradeScriptObject))->info($upgradeOutput); + $output->writeln($upgradeOutput); + } + } +} diff --git a/src/Magento/FunctionalTestingFramework/Data/Argument/Interpreter/ArrayType.php b/src/Magento/FunctionalTestingFramework/Data/Argument/Interpreter/ArrayType.php index 056a4f9d2..566c09912 100644 --- a/src/Magento/FunctionalTestingFramework/Data/Argument/Interpreter/ArrayType.php +++ b/src/Magento/FunctionalTestingFramework/Data/Argument/Interpreter/ArrayType.php @@ -79,10 +79,10 @@ function ($firstItemKey, $secondItemKey) use ($indexedItems) { /** * Compare sortOrder of item * - * @param string|int $firstItemKey - * @param string|int $secondItemKey - * @param array $indexedItems - * @return int + * @param string|integer $firstItemKey + * @param string|integer $secondItemKey + * @param array $indexedItems + * @return integer */ private function compareItems($firstItemKey, $secondItemKey, $indexedItems) { @@ -110,7 +110,7 @@ private function compareItems($firstItemKey, $secondItemKey, $indexedItems) * Determine if a sort order exists for any of the items. * * @param array $items - * @return bool + * @return boolean */ private function isSortOrderDefined($items) { diff --git a/src/Magento/FunctionalTestingFramework/Data/Argument/Interpreter/Boolean.php b/src/Magento/FunctionalTestingFramework/Data/Argument/Interpreter/Boolean.php index f545c2aa4..8e4e5a899 100644 --- a/src/Magento/FunctionalTestingFramework/Data/Argument/Interpreter/Boolean.php +++ b/src/Magento/FunctionalTestingFramework/Data/Argument/Interpreter/Boolean.php @@ -31,7 +31,7 @@ public function __construct(BooleanUtils $booleanUtils) /** * {@inheritdoc} - * @return bool + * @return boolean * @throws \InvalidArgumentException */ public function evaluate(array $data) diff --git a/src/Magento/FunctionalTestingFramework/Data/Argument/Interpreter/Composite.php b/src/Magento/FunctionalTestingFramework/Data/Argument/Interpreter/Composite.php index 2d3ea0e0e..8c5981a78 100644 --- a/src/Magento/FunctionalTestingFramework/Data/Argument/Interpreter/Composite.php +++ b/src/Magento/FunctionalTestingFramework/Data/Argument/Interpreter/Composite.php @@ -28,7 +28,7 @@ class Composite implements InterpreterInterface /** * Composite constructor. - * @param array $interpreters + * @param array $interpreters * @param string $discriminator * @throws \InvalidArgumentException */ @@ -47,6 +47,7 @@ public function __construct(array $interpreters, $discriminator) /** * {@inheritdoc} + * @return mixed * @throws \InvalidArgumentException */ public function evaluate(array $data) @@ -65,7 +66,7 @@ public function evaluate(array $data) /** * Register interpreter instance under a given unique name * - * @param string $name + * @param string $name * @param InterpreterInterface $instance * @return void * @throws \InvalidArgumentException diff --git a/src/Magento/FunctionalTestingFramework/Data/Argument/Interpreter/Number.php b/src/Magento/FunctionalTestingFramework/Data/Argument/Interpreter/Number.php index 3583896c4..3742a67ad 100644 --- a/src/Magento/FunctionalTestingFramework/Data/Argument/Interpreter/Number.php +++ b/src/Magento/FunctionalTestingFramework/Data/Argument/Interpreter/Number.php @@ -14,7 +14,7 @@ class Number implements InterpreterInterface { /** * {@inheritdoc} - * @return string|int|float + * @return string|integer|float * @throws \InvalidArgumentException */ public function evaluate(array $data) diff --git a/src/Magento/FunctionalTestingFramework/Data/Argument/InterpreterInterface.php b/src/Magento/FunctionalTestingFramework/Data/Argument/InterpreterInterface.php index 6ff11b082..6aa835e3e 100644 --- a/src/Magento/FunctionalTestingFramework/Data/Argument/InterpreterInterface.php +++ b/src/Magento/FunctionalTestingFramework/Data/Argument/InterpreterInterface.php @@ -8,7 +8,6 @@ /** * Interface that encapsulates complexity of expression computation */ -// @codingStandardsIgnoreFile interface InterpreterInterface { /** diff --git a/src/Magento/FunctionalTestingFramework/Data/Argument/InterpreterInterface/Proxy.php b/src/Magento/FunctionalTestingFramework/Data/Argument/InterpreterInterface/Proxy.php index 1741952a6..543682b4f 100644 --- a/src/Magento/FunctionalTestingFramework/Data/Argument/InterpreterInterface/Proxy.php +++ b/src/Magento/FunctionalTestingFramework/Data/Argument/InterpreterInterface/Proxy.php @@ -34,7 +34,7 @@ class Proxy implements \Magento\FunctionalTestingFramework\Data\Argument\Interpr /** * Instance shareability flag * - * @var bool + * @var boolean */ protected $isShared = null; @@ -42,8 +42,8 @@ class Proxy implements \Magento\FunctionalTestingFramework\Data\Argument\Interpr * Proxy constructor * * @param \Magento\FunctionalTestingFramework\ObjectManagerInterface $objectManager - * @param string $instanceName - * @param bool $shared + * @param string $instanceName + * @param boolean $shared */ public function __construct( \Magento\FunctionalTestingFramework\ObjectManagerInterface $objectManager, @@ -100,6 +100,7 @@ protected function getSubject() /** * {@inheritdoc} + * @return mixed */ public function evaluate(array $data) { diff --git a/src/Magento/FunctionalTestingFramework/DataGenerator/Config/Dom.php b/src/Magento/FunctionalTestingFramework/DataGenerator/Config/Dom.php index ab794d5ef..1da3e9c0e 100644 --- a/src/Magento/FunctionalTestingFramework/DataGenerator/Config/Dom.php +++ b/src/Magento/FunctionalTestingFramework/DataGenerator/Config/Dom.php @@ -7,117 +7,78 @@ use Magento\FunctionalTestingFramework\Config\Dom\NodeMergingConfig; use Magento\FunctionalTestingFramework\Config\Dom\NodePathMatcher; +use Magento\FunctionalTestingFramework\Exceptions\Collector\ExceptionCollector; +use Magento\FunctionalTestingFramework\Util\Validation\DuplicateNodeValidationUtil; /** - * Magento configuration XML DOM utility + * MFTF actionGroup.xml configuration XML DOM utility + * @package Magento\FunctionalTestingFramework\DataGenerator\Config */ -class Dom extends \Magento\FunctionalTestingFramework\Config\Dom +class Dom extends \Magento\FunctionalTestingFramework\Config\MftfDom { + const DATA_FILE_NAME_ENDING = "Data"; + const DATA_META_FILENAME_ATTRIBUTE = "filename"; /** - * Array of non keyed mergeable paths - * - * @var array + * NodeValidationUtil + * @var DuplicateNodeValidationUtil */ - private $mergeablePaths; + private $validationUtil; /** - * Build DOM with initial XML contents and specifying identifier attributes for merging. Overridden to include new - * mergeablePaths argument which can be matched for non keyed mergeable xml elements. - * - * Format of $idAttributes: array('/xpath/to/some/node' => 'id_attribute_name') - * The path to ID attribute name should not include any attribute notations or modifiers -- only node names - * - * @param string $xml - * @param array $idAttributes - * @param array $mergeablePaths - * @param string $typeAttributeName - * @param string $schemaFile - * @param string $errorFormat + * Entity Dom constructor. + * @param string $xml + * @param string $filename + * @param ExceptionCollector $exceptionCollector + * @param array $idAttributes + * @param string $typeAttributeName + * @param string $schemaFile + * @param string $errorFormat */ public function __construct( $xml, + $filename, + $exceptionCollector, array $idAttributes = [], - array $mergeablePaths = [], $typeAttributeName = null, $schemaFile = null, $errorFormat = self::ERROR_FORMAT_DEFAULT ) { - $this->schemaFile = $schemaFile; - $this->nodeMergingConfig = new NodeMergingConfig(new NodePathMatcher(), $idAttributes); - $this->mergeablePaths = $mergeablePaths; - $this->typeAttributeName = $typeAttributeName; - $this->errorFormat = $errorFormat; - $this->dom = $this->initDom($xml); - $this->rootNamespace = $this->dom->lookupNamespaceUri($this->dom->namespaceURI); + $this->validationUtil = new DuplicateNodeValidationUtil('key', $exceptionCollector); + parent::__construct( + $xml, + $filename, + $exceptionCollector, + $idAttributes, + $typeAttributeName, + $schemaFile, + $errorFormat + ); } /** - * Recursive merging of the \DOMElement into the original document. Overridden to include a call to + * Takes a dom element from xml and appends the filename based on location * - * Algorithm: - * 1. Find the same node in original document - * 2. Extend and override original document node attributes and scalar value if found - * 3. Append new node if original document doesn't have the same node - * - * @param \DOMElement $node - * @param string $parentPath path to parent node - * @return void + * @param string $xml + * @param string|null $filename + * @return \DOMDocument */ - public function mergeNode(\DOMElement $node, $parentPath) + public function initDom($xml, $filename = null) { - $path = $this->getNodePathByParent($node, $parentPath); - $isMergeablePath = $this->validateIsPathMergeable($path); - - $matchedNode = $this->getMatchedNode($path, $isMergeablePath); + $dom = parent::initDom($xml); - /* Update matched node attributes and value */ - if ($matchedNode && !$isMergeablePath) { - //different node type - $this->mergeMatchingNode($node, $parentPath, $matchedNode, $path); - } else { - /* Add node as is to the document under the same parent element */ - $parentMatchedNode = $this->getMatchedNode($parentPath); - $newNode = $this->dom->importNode($node, true); - $parentMatchedNode->appendChild($newNode); + if (strpos($filename, self::DATA_FILE_NAME_ENDING)) { + $entityNodes = $dom->getElementsByTagName('entity'); + foreach ($entityNodes as $entityNode) { + /** @var \DOMElement $entityNode */ + $entityNode->setAttribute(self::DATA_META_FILENAME_ATTRIBUTE, $filename); + $this->validationUtil->validateChildUniqueness( + $entityNode, + $filename + ); + } } - } - /** - * Getter for node by path, overridden to include validation flag for mergeable entries - * An exception is possible if original document contains multiple nodes for identifier - * - * @param string $nodePath - * @param boolean $isMergeablePath - * @throws \Exception - * @return \DOMElement|null - */ - public function getMatchedNode($nodePath, $isMergeablePath = false) - { - $xPath = new \DOMXPath($this->dom); - if ($this->rootNamespace) { - $xPath->registerNamespace(self::ROOT_NAMESPACE_PREFIX, $this->rootNamespace); - } - $matchedNodes = $xPath->query($nodePath); - $node = null; - - if ($matchedNodes->length > 1 && !$isMergeablePath) { - throw new \Exception("More than one node matching the query: {$nodePath}"); - } elseif ($matchedNodes->length == 1) { - $node = $matchedNodes->item(0); - } - return $node; - } - - /** - * Function which simplifies and xpath match in dom and compares with listed known mergeable paths - * - * @param string $path - * @return boolean - */ - private function validateIsPathMergeable($path) - { - $simplifiedPath = $this->nodeMergingConfig->getNodePathMatcher()->simplifyXpath($path); - return array_key_exists($simplifiedPath, $this->mergeablePaths); + return $dom; } } diff --git a/src/Magento/FunctionalTestingFramework/DataGenerator/Config/OperationDom.php b/src/Magento/FunctionalTestingFramework/DataGenerator/Config/OperationDom.php new file mode 100644 index 000000000..f4617c61e --- /dev/null +++ b/src/Magento/FunctionalTestingFramework/DataGenerator/Config/OperationDom.php @@ -0,0 +1,110 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\FunctionalTestingFramework\DataGenerator\Config; + +use Magento\FunctionalTestingFramework\Config\Dom\NodeMergingConfig; +use Magento\FunctionalTestingFramework\Config\Dom\NodePathMatcher; +use Magento\FunctionalTestingFramework\Exceptions\Collector\ExceptionCollector; +use Magento\FunctionalTestingFramework\Util\Validation\DuplicateNodeValidationUtil; + +/** + * MFTF metadata.xml configuration XML DOM utility + * @package Magento\FunctionalTestingFramework\DataGenerator\Config + */ +class OperationDom extends \Magento\FunctionalTestingFramework\Config\MftfDom +{ + const METADATA_FILE_NAME_ENDING = "meta"; + const METADATA_META_FILENAME_ATTRIBUTE = "filename"; + + /** + * NodeValidationUtil + * @var DuplicateNodeValidationUtil + */ + private $validationUtil; + + /** + * Metadata Dom constructor. + * @param string $xml + * @param string $filename + * @param ExceptionCollector $exceptionCollector + * @param array $idAttributes + * @param string $typeAttributeName + * @param string $schemaFile + * @param string $errorFormat + */ + public function __construct( + $xml, + $filename, + $exceptionCollector, + array $idAttributes = [], + $typeAttributeName = null, + $schemaFile = null, + $errorFormat = self::ERROR_FORMAT_DEFAULT + ) { + $this->validationUtil = new DuplicateNodeValidationUtil('key', $exceptionCollector); + parent::__construct( + $xml, + $filename, + $exceptionCollector, + $idAttributes, + $typeAttributeName, + $schemaFile, + $errorFormat + ); + } + + /** + * Takes a dom element from xml and appends the filename based on location + * + * @param string $xml + * @param string|null $filename + * @return \DOMDocument + */ + public function initDom($xml, $filename = null) + { + $dom = parent::initDom($xml); + + if (strpos($filename, self::METADATA_FILE_NAME_ENDING)) { + $operationNodes = $dom->getElementsByTagName('operation'); + foreach ($operationNodes as $operationNode) { + /** @var \DOMElement $operationNode */ + $operationNode->setAttribute(self::METADATA_META_FILENAME_ATTRIBUTE, $filename); + $this->validateOperationElements( + $operationNode, + $filename + ); + } + } + + return $dom; + } + + /** + * Recurse through child elements and validate uniqueKeys + * @param \DOMElement $parentNode + * @param string $filename + * @return void + */ + public function validateOperationElements(\DOMElement $parentNode, $filename) + { + $this->validationUtil->validateChildUniqueness( + $parentNode, + $filename + ); + $childNodes = $parentNode->childNodes; + + for ($i = 0; $i < $childNodes->length; $i++) { + $currentNode = $childNodes->item($i); + if (!is_a($currentNode, \DOMElement::class)) { + continue; + } + $this->validateOperationElements( + $currentNode, + $filename + ); + } + } +} diff --git a/src/Magento/FunctionalTestingFramework/DataGenerator/Config/Reader/Filesystem.php b/src/Magento/FunctionalTestingFramework/DataGenerator/Config/Reader/Filesystem.php index 4af2f8cf4..4f21fabea 100644 --- a/src/Magento/FunctionalTestingFramework/DataGenerator/Config/Reader/Filesystem.php +++ b/src/Magento/FunctionalTestingFramework/DataGenerator/Config/Reader/Filesystem.php @@ -20,15 +20,15 @@ class Filesystem extends \Magento\FunctionalTestingFramework\Config\Reader\Files /** * Constructor * - * @param \Magento\FunctionalTestingFramework\Config\FileResolverInterface $fileResolver - * @param \Magento\FunctionalTestingFramework\Config\ConverterInterface $converter - * @param \Magento\FunctionalTestingFramework\Config\SchemaLocatorInterface $schemaLocator + * @param \Magento\FunctionalTestingFramework\Config\FileResolverInterface $fileResolver + * @param \Magento\FunctionalTestingFramework\Config\ConverterInterface $converter + * @param \Magento\FunctionalTestingFramework\Config\SchemaLocatorInterface $schemaLocator * @param \Magento\FunctionalTestingFramework\Config\ValidationStateInterface $validationState - * @param string $fileName - * @param array $idAttributes - * @param array $mergeablePaths - * @param string $domDocumentClass - * @param string $defaultScope + * @param string $fileName + * @param array $idAttributes + * @param array $mergeablePaths + * @param string $domDocumentClass + * @param string $defaultScope */ public function __construct( \Magento\FunctionalTestingFramework\Config\FileResolverInterface $fileResolver, diff --git a/src/Magento/FunctionalTestingFramework/DataGenerator/Handlers/CredentialStore.php b/src/Magento/FunctionalTestingFramework/DataGenerator/Handlers/CredentialStore.php new file mode 100644 index 000000000..a28547d18 --- /dev/null +++ b/src/Magento/FunctionalTestingFramework/DataGenerator/Handlers/CredentialStore.php @@ -0,0 +1,159 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\FunctionalTestingFramework\DataGenerator\Handlers; + +use Magento\FunctionalTestingFramework\Config\MftfApplicationConfig; +use Magento\FunctionalTestingFramework\Console\BuildProjectCommand; +use Magento\FunctionalTestingFramework\Exceptions\TestFrameworkException; +use Magento\FunctionalTestingFramework\Util\Logger\LoggingUtil; + +class CredentialStore +{ + const ENCRYPTION_ALGO = "AES-256-CBC"; + + /** + * Singleton instance + * + * @var CredentialStore + */ + private static $INSTANCE = null; + + /** + * Initial vector for open_ssl encryption. + * + * @var string + */ + private $iv = null; + + /** + * Key for open_ssl encryption/decryption + * + * @var string + */ + private $encodedKey = null; + + /** + * Key/Value paris of credential names and their corresponding values + * + * @var array + */ + private $credentials = []; + + /** + * Static singleton getter for CredentialStore Instance + * + * @return CredentialStore + */ + public static function getInstance() + { + if (self::$INSTANCE == null) { + self::$INSTANCE = new CredentialStore(); + } + + return self::$INSTANCE; + } + + /** + * CredentialStore constructor. + */ + private function __construct() + { + $this->encodedKey = base64_encode(openssl_random_pseudo_bytes(16)); + $this->iv = substr(hash('sha256', $this->encodedKey), 0, 16); + $creds = $this->readInCredentialsFile(); + $this->credentials = $this->encryptCredFileContents($creds); + } + + /** + * Returns the value of a secret based on corresponding key + * + * @param string $key + * @return string|null + * @throws TestFrameworkException + */ + public function getSecret($key) + { + if (!array_key_exists($key, $this->credentials)) { + throw new TestFrameworkException( + "{$key} not defined in .credentials, please provide a value in order to use this secret in a test." + ); + } + + // log here for verbose config + if (MftfApplicationConfig::getConfig()->verboseEnabled()) { + LoggingUtil::getInstance()->getLogger(CredentialStore::class)->debug( + "retrieving secret for key name {$key}" + ); + } + + return $this->credentials[$key] ?? null; + } + + /** + * Private function which reads in secret key/values from .credentials file and stores in memory as key/value pair. + * + * @return array + * @throws TestFrameworkException + */ + private function readInCredentialsFile() + { + $credsFilePath = str_replace( + '.credentials.example', + '.credentials', + BuildProjectCommand::CREDENTIALS_FILE_PATH + ); + + if (!file_exists($credsFilePath)) { + throw new TestFrameworkException( + "Cannot find .credentials file, please create in " + . TESTS_BP . " in order to reference sensitive information" + ); + } + + return file($credsFilePath, FILE_IGNORE_NEW_LINES); + } + + /** + * Function which takes the contents of the credentials file and encrypts the entries. + * + * @param array $credContents + * @return array + */ + private function encryptCredFileContents($credContents) + { + $encryptedCreds = []; + foreach ($credContents as $credValue) { + if (substr($credValue, 0, 1) === '#' || empty($credValue)) { + continue; + } + + list($key, $value) = explode("=", $credValue); + if (!empty($value)) { + $encryptedCreds[$key] = openssl_encrypt( + $value, + self::ENCRYPTION_ALGO, + $this->encodedKey, + 0, + $this->iv + ); + } + } + + return $encryptedCreds; + } + + /** + * Takes a value encrypted at runtime and descrypts using the object's initial vector. + * + * @param string $value + * @return string + */ + public function decryptSecretValue($value) + { + return openssl_decrypt($value, self::ENCRYPTION_ALGO, $this->encodedKey, 0, $this->iv); + } +} diff --git a/src/Magento/FunctionalTestingFramework/DataGenerator/Handlers/DataObjectHandler.php b/src/Magento/FunctionalTestingFramework/DataGenerator/Handlers/DataObjectHandler.php index 95fb6139c..7305fdc37 100644 --- a/src/Magento/FunctionalTestingFramework/DataGenerator/Handlers/DataObjectHandler.php +++ b/src/Magento/FunctionalTestingFramework/DataGenerator/Handlers/DataObjectHandler.php @@ -8,14 +8,17 @@ use Magento\FunctionalTestingFramework\DataGenerator\Objects\EntityDataObject; use Magento\FunctionalTestingFramework\DataGenerator\Parsers\DataProfileSchemaParser; +use Magento\FunctionalTestingFramework\Exceptions\XmlException; use Magento\FunctionalTestingFramework\ObjectManager\ObjectHandlerInterface; use Magento\FunctionalTestingFramework\ObjectManagerFactory; +use Magento\FunctionalTestingFramework\DataGenerator\Util\DataExtensionUtil; class DataObjectHandler implements ObjectHandlerInterface { const _ENTITY = 'entity'; const _NAME = 'name'; const _TYPE = 'type'; + const _EXTENDS = 'extends'; const _DATA = 'data'; const _KEY = 'key'; const _VALUE = 'value'; @@ -45,6 +48,13 @@ class DataObjectHandler implements ObjectHandlerInterface */ private $entityDataObjects = []; + /** + * Instance of DataExtensionUtil class + * + * @var DataExtensionUtil + */ + private $extendUtil; + /** * Constructor */ @@ -56,6 +66,7 @@ private function __construct() return; } $this->entityDataObjects = $this->processParserOutput($parserOutput); + $this->extendUtil = new DataExtensionUtil(); } /** @@ -80,10 +91,8 @@ public static function getInstance() */ public function getObject($name) { - $allObjects = $this->getAllObjects(); - - if (array_key_exists($name, $allObjects)) { - return $allObjects[$name]; + if (array_key_exists($name, $this->entityDataObjects)) { + return $this->extendDataObject($this->entityDataObjects[$name]); } return null; @@ -96,14 +105,18 @@ public function getObject($name) */ public function getAllObjects() { + foreach ($this->entityDataObjects as $entityName => $entityObject) { + $this->entityDataObjects[$entityName] = $this->extendDataObject($entityObject); + } return $this->entityDataObjects; } /** * Convert the parser output into a collection of EntityDataObjects * - * @param string[] $parserOutput primitive array output from the Magento parser + * @param string[] $parserOutput Primitive array output from the Magento parser. * @return EntityDataObject[] + * @throws XmlException */ private function processParserOutput($parserOutput) { @@ -115,11 +128,12 @@ private function processParserOutput($parserOutput) throw new XmlException(sprintf(self::DATA_NAME_ERROR_MSG, $name)); } - $type = $rawEntity[self::_TYPE]; + $type = $rawEntity[self::_TYPE] ?? null; $data = []; $linkedEntities = []; $uniquenessData = []; $vars = []; + $parentEntity = null; if (array_key_exists(self::_DATA, $rawEntity)) { $data = $this->processDataElements($rawEntity); @@ -142,7 +156,19 @@ private function processParserOutput($parserOutput) $vars = $this->processVarElements($rawEntity); } - $entityDataObject = new EntityDataObject($name, $type, $data, $linkedEntities, $uniquenessData, $vars); + if (array_key_exists(self::_EXTENDS, $rawEntity)) { + $parentEntity = $rawEntity[self::_EXTENDS]; + } + + $entityDataObject = new EntityDataObject( + $name, + $type, + $data, + $linkedEntities, + $uniquenessData, + $vars, + $parentEntity + ); $entityDataObjects[$entityDataObject->getName()] = $entityDataObject; } @@ -153,8 +179,8 @@ private function processParserOutput($parserOutput) /** * Takes an array of items and a top level entity data array and merges in elements from parsed entity definitions. * - * @param array $arrayItems - * @param array $data + * @param array $arrayItems + * @param array $data * @param string $key * @return array */ @@ -237,4 +263,18 @@ private function processVarElements($entityData) } return $vars; } + + /** + * This method checks if the data object is extended and creates a new data object accordingly + * + * @param EntityDataObject $dataObject + * @return EntityDataObject + */ + private function extendDataObject($dataObject) + { + if ($dataObject->getParentName() != null) { + return $this->extendUtil->extendEntity($dataObject); + } + return $dataObject; + } } diff --git a/src/Magento/FunctionalTestingFramework/DataGenerator/Handlers/OperationDefinitionObjectHandler.php b/src/Magento/FunctionalTestingFramework/DataGenerator/Handlers/OperationDefinitionObjectHandler.php index c79c711ff..66ca3f594 100644 --- a/src/Magento/FunctionalTestingFramework/DataGenerator/Handlers/OperationDefinitionObjectHandler.php +++ b/src/Magento/FunctionalTestingFramework/DataGenerator/Handlers/OperationDefinitionObjectHandler.php @@ -9,6 +9,7 @@ use Magento\FunctionalTestingFramework\DataGenerator\Objects\OperationElement; use Magento\FunctionalTestingFramework\DataGenerator\Parsers\OperationDefinitionParser; use Magento\FunctionalTestingFramework\DataGenerator\Util\OperationElementExtractor; +use Magento\FunctionalTestingFramework\Exceptions\XmlException; use Magento\FunctionalTestingFramework\ObjectManager\ObjectHandlerInterface; use Magento\FunctionalTestingFramework\ObjectManagerFactory; @@ -125,6 +126,8 @@ public function getOperationDefinition($operation, $dataType) * into an array of objects. * * @return void + * @throws \Exception + * * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.NPathComplexity) * @SuppressWarnings(PHPMD.UnusedPrivateMethod) diff --git a/src/Magento/FunctionalTestingFramework/DataGenerator/Objects/EntityDataObject.php b/src/Magento/FunctionalTestingFramework/DataGenerator/Objects/EntityDataObject.php index 74cef78f2..1ba126e5f 100644 --- a/src/Magento/FunctionalTestingFramework/DataGenerator/Objects/EntityDataObject.php +++ b/src/Magento/FunctionalTestingFramework/DataGenerator/Objects/EntityDataObject.php @@ -6,7 +6,9 @@ namespace Magento\FunctionalTestingFramework\DataGenerator\Objects; +use Magento\FunctionalTestingFramework\Config\MftfApplicationConfig; use Magento\FunctionalTestingFramework\Exceptions\TestFrameworkException; +use Magento\FunctionalTestingFramework\Util\Logger\LoggingUtil; /** * Class EntityDataObject @@ -63,17 +65,25 @@ class EntityDataObject */ private $uniquenessData = []; + /** + * String of parent Entity + * + * @var string + */ + private $parentEntity; + /** * Constructor * - * @param string $name - * @param string $type + * @param string $name + * @param string $type * @param string[] $data * @param string[] $linkedEntities * @param string[] $uniquenessData * @param string[] $vars + * @param string $parentEntity */ - public function __construct($name, $type, $data, $linkedEntities, $uniquenessData, $vars = []) + public function __construct($name, $type, $data, $linkedEntities, $uniquenessData, $vars = [], $parentEntity = null) { $this->name = $name; $this->type = $type; @@ -84,6 +94,7 @@ public function __construct($name, $type, $data, $linkedEntities, $uniquenessDat } $this->vars = $vars; + $this->parentEntity = $parentEntity; } /** @@ -119,17 +130,23 @@ public function getAllData() /** * Get a piece of data by name and the desired uniqueness format. * - * @param string $name - * @param int $uniquenessFormat + * @param string $name + * @param integer $uniquenessFormat * @return string|null * @throws TestFrameworkException */ public function getDataByName($name, $uniquenessFormat) { + if (MftfApplicationConfig::getConfig()->verboseEnabled()) { + LoggingUtil::getInstance()->getLogger(EntityDataObject::class) + ->debug("Fetching data field from entity", ["entity" => $this->getName(), "field" => $name]); + } + if (!$this->isValidUniqueDataFormat($uniquenessFormat)) { - throw new TestFrameworkException( - sprintf('Invalid unique data format value: %s \n', $uniquenessFormat) - ); + $exceptionMessage = sprintf("Invalid unique data format value: %s \n", $uniquenessFormat); + LoggingUtil::getInstance()->getLogger(EntityDataObject::class) + ->error($exceptionMessage, ["entity" => $this->getName(), "field" => $name]); + throw new TestFrameworkException($exceptionMessage); } $name_lower = strtolower($name); @@ -145,6 +162,16 @@ public function getDataByName($name, $uniquenessFormat) return null; } + /** + * Getter for data parent + * + * @return \string + */ + public function getParentName() + { + return $this->parentEntity; + } + /** * Formats and returns data based on given uniqueDataFormat and prefix/suffix. * @@ -152,6 +179,7 @@ public function getDataByName($name, $uniquenessFormat) * @param string $uniqueData * @param string $uniqueDataFormat * @return null|string + * @throws TestFrameworkException */ private function formatUniqueData($name, $uniqueData, $uniqueDataFormat) { @@ -203,12 +231,12 @@ private function formatUniqueData($name, $uniqueData, $uniqueDataFormat) private function checkUniquenessFunctionExists($function, $uniqueDataFormat) { if (!function_exists($function)) { - throw new TestFrameworkException( - sprintf( - 'Unique data format value: %s can only be used when running cests.\n', - $uniqueDataFormat - ) + $exceptionMessage = sprintf( + 'Unique data format value: %s can only be used when running cests.\n', + $uniqueDataFormat ); + + throw new TestFrameworkException($exceptionMessage); } } @@ -298,8 +326,8 @@ public function getUniquenessData() /** * Validate if input value is a valid unique data format. * - * @param int $uniDataFormat - * @return bool + * @param integer $uniDataFormat + * @return boolean */ private function isValidUniqueDataFormat($uniDataFormat) { diff --git a/src/Magento/FunctionalTestingFramework/DataGenerator/Objects/OperationDefinitionObject.php b/src/Magento/FunctionalTestingFramework/DataGenerator/Objects/OperationDefinitionObject.php index abedf14e3..d6237a437 100644 --- a/src/Magento/FunctionalTestingFramework/DataGenerator/Objects/OperationDefinitionObject.php +++ b/src/Magento/FunctionalTestingFramework/DataGenerator/Objects/OperationDefinitionObject.php @@ -106,25 +106,25 @@ class OperationDefinitionObject /** * Determines if operation should remove backend_name from URL. - * @var bool + * @var boolean */ private $removeBackend; /** * OperationDefinitionObject constructor. - * @param string $name - * @param string $operation - * @param string $dataType - * @param string $apiMethod - * @param string $apiUri - * @param string $auth - * @param array $headers - * @param array $params - * @param array $metaData - * @param string $contentType - * @param bool $removeBackend - * @param string $successRegex - * @param string $returnRegex + * @param string $name + * @param string $operation + * @param string $dataType + * @param string $apiMethod + * @param string $apiUri + * @param string $auth + * @param array $headers + * @param array $params + * @param array $metaData + * @param string $contentType + * @param boolean $removeBackend + * @param string $successRegex + * @param string $returnRegex * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -237,7 +237,7 @@ public function getHeaders() /** * Getter for removeBackend * - * @return bool + * @return boolean */ public function removeUrlBackend() { diff --git a/src/Magento/FunctionalTestingFramework/DataGenerator/Objects/OperationElement.php b/src/Magento/FunctionalTestingFramework/DataGenerator/Objects/OperationElement.php index 5980e2f0a..7338e52f3 100644 --- a/src/Magento/FunctionalTestingFramework/DataGenerator/Objects/OperationElement.php +++ b/src/Magento/FunctionalTestingFramework/DataGenerator/Objects/OperationElement.php @@ -45,17 +45,17 @@ class OperationElement /** * Required attribute, used to determine if values need to be cast before insertion. - * @var bool + * @var boolean */ private $required; /** * OperationElement constructor. - * @param string $key - * @param string $value - * @param string $type - * @param bool $required - * @param array $nestedElements + * @param string $key + * @param string $value + * @param string $type + * @param boolean $required + * @param array $nestedElements * @param null|array $nestedMetadata */ public function __construct($key, $value, $type, $required, $nestedElements = [], $nestedMetadata = null) @@ -105,7 +105,7 @@ public function getType() /** * Accessor for required attribute * - * @return bool + * @return boolean */ public function isRequired() { diff --git a/src/Magento/FunctionalTestingFramework/DataGenerator/Persist/Curl/AdminExecutor.php b/src/Magento/FunctionalTestingFramework/DataGenerator/Persist/Curl/AdminExecutor.php index fb30704a7..cff7c04fb 100644 --- a/src/Magento/FunctionalTestingFramework/DataGenerator/Persist/Curl/AdminExecutor.php +++ b/src/Magento/FunctionalTestingFramework/DataGenerator/Persist/Curl/AdminExecutor.php @@ -38,7 +38,7 @@ class AdminExecutor extends AbstractExecutor implements CurlInterface /** * Should executor remove backend_name from api url - * @var bool + * @var boolean */ private $removeBackend; @@ -51,9 +51,10 @@ class AdminExecutor extends AbstractExecutor implements CurlInterface /** * Constructor. - * @param bool $removeBackend + * @param boolean $removeBackend * * @constructor + * @throws TestFrameworkException */ public function __construct($removeBackend) { @@ -109,9 +110,9 @@ private function setFormKey() * Send request to the remote server. * * @param string $url - * @param array $data + * @param array $data * @param string $method - * @param array $headers + * @param array $headers * @return void * @throws TestFrameworkException */ @@ -167,8 +168,8 @@ public function read($successRegex = null, $returnRegex = null) /** * Add additional option to cURL. * - * @param int $option the CURLOPT_* constants - * @param int|string|bool|array $value + * @param integer $option CURLOPT_* constants. + * @param integer|string|boolean|array $value * @return void */ public function addOption($option, $value) diff --git a/src/Magento/FunctionalTestingFramework/DataGenerator/Persist/Curl/FrontendExecutor.php b/src/Magento/FunctionalTestingFramework/DataGenerator/Persist/Curl/FrontendExecutor.php index b6f448dc0..aa1706ef3 100644 --- a/src/Magento/FunctionalTestingFramework/DataGenerator/Persist/Curl/FrontendExecutor.php +++ b/src/Magento/FunctionalTestingFramework/DataGenerator/Persist/Curl/FrontendExecutor.php @@ -62,6 +62,8 @@ class FrontendExecutor extends AbstractExecutor implements CurlInterface * * @param string $customerEmail * @param string $customerPassWord + * + * @throws TestFrameworkException */ public function __construct($customerEmail, $customerPassWord) { @@ -130,9 +132,9 @@ protected function setCookies() * Send request to the remote server. * * @param string $url - * @param array $data + * @param array $data * @param string $method - * @param array $headers + * @param array $headers * @return void * @throws TestFrameworkException */ @@ -189,8 +191,8 @@ public function read($successRegex = null, $returnRegex = null) /** * Add additional option to cURL. * - * @param int $option the CURLOPT_* constants - * @param int|string|bool|array $value + * @param integer $option CURLOPT_* constants. + * @param integer|string|boolean|array $value * @return void */ public function addOption($option, $value) diff --git a/src/Magento/FunctionalTestingFramework/DataGenerator/Persist/Curl/WebapiExecutor.php b/src/Magento/FunctionalTestingFramework/DataGenerator/Persist/Curl/WebapiExecutor.php index 9a93cded3..16fef75f2 100644 --- a/src/Magento/FunctionalTestingFramework/DataGenerator/Persist/Curl/WebapiExecutor.php +++ b/src/Magento/FunctionalTestingFramework/DataGenerator/Persist/Curl/WebapiExecutor.php @@ -6,6 +6,7 @@ namespace Magento\FunctionalTestingFramework\DataGenerator\Persist\Curl; +use Magento\FunctionalTestingFramework\Exceptions\TestFrameworkException; use Magento\FunctionalTestingFramework\Util\Protocol\CurlInterface; use Magento\FunctionalTestingFramework\Util\Protocol\CurlTransport; @@ -54,6 +55,7 @@ class WebapiExecutor extends AbstractExecutor implements CurlInterface * WebapiExecutor Constructor. * * @param string $storeCode + * @throws TestFrameworkException */ public function __construct($storeCode = null) { @@ -70,6 +72,7 @@ public function __construct($storeCode = null) * Returns the authorization token needed for some requests via REST call. * * @return void + * @throws TestFrameworkException */ protected function authorize() { @@ -90,10 +93,11 @@ protected function authorize() * Send request to the remote server. * * @param string $url - * @param array $data + * @param array $data * @param string $method - * @param array $headers + * @param array $headers * @return void + * @throws TestFrameworkException */ public function write($url, $data = [], $method = CurlInterface::POST, $headers = []) { @@ -111,6 +115,7 @@ public function write($url, $data = [], $method = CurlInterface::POST, $headers * @param string $successRegex * @param string $returnRegex * @return string + * @throws TestFrameworkException */ public function read($successRegex = null, $returnRegex = null) { @@ -121,8 +126,8 @@ public function read($successRegex = null, $returnRegex = null) /** * Add additional option to cURL. * - * @param int $option the CURLOPT_* constants - * @param int|string|bool|array $value + * @param integer $option CURLOPT_* constants. + * @param integer|string|boolean|array $value * @return void */ public function addOption($option, $value) diff --git a/src/Magento/FunctionalTestingFramework/DataGenerator/Persist/CurlHandler.php b/src/Magento/FunctionalTestingFramework/DataGenerator/Persist/CurlHandler.php index 63144a9ff..ee61d8c12 100644 --- a/src/Magento/FunctionalTestingFramework/DataGenerator/Persist/CurlHandler.php +++ b/src/Magento/FunctionalTestingFramework/DataGenerator/Persist/CurlHandler.php @@ -58,7 +58,7 @@ class CurlHandler /** * If the content type is Json. * - * @var bool + * @var boolean */ private $isJson; @@ -77,9 +77,9 @@ class CurlHandler /** * ApiSubObject constructor. * - * @param string $operation + * @param string $operation * @param EntityDataObject $entityObject - * @param string $storeCode + * @param string $storeCode */ public function __construct($operation, $entityObject, $storeCode = null) { @@ -100,6 +100,7 @@ public function __construct($operation, $entityObject, $storeCode = null) * @param array $dependentEntities * @return array | null * @throws TestFrameworkException + * @throws \Exception */ public function executeRequest($dependentEntities) { @@ -178,7 +179,7 @@ public function getRequestDataArray() /** * If content type of a request is Json. * - * @return bool + * @return boolean */ public function isContentTypeJson() { @@ -189,7 +190,7 @@ public function isContentTypeJson() * Resolve rul reference from entity objects. * * @param string $urlIn - * @param array $entityObjects + * @param array $entityObjects * @return string */ private function resolveUrlReference($urlIn, $entityObjects) diff --git a/src/Magento/FunctionalTestingFramework/DataGenerator/Persist/DataPersistenceHandler.php b/src/Magento/FunctionalTestingFramework/DataGenerator/Persist/DataPersistenceHandler.php index 290a14c4e..dcb9d158a 100644 --- a/src/Magento/FunctionalTestingFramework/DataGenerator/Persist/DataPersistenceHandler.php +++ b/src/Magento/FunctionalTestingFramework/DataGenerator/Persist/DataPersistenceHandler.php @@ -8,6 +8,7 @@ use Magento\FunctionalTestingFramework\DataGenerator\Objects\EntityDataObject; use Magento\FunctionalTestingFramework\DataGenerator\Handlers\DataObjectHandler; +use Magento\FunctionalTestingFramework\Exceptions\TestFrameworkException; /** * Class DataPersistenceHandler @@ -45,8 +46,8 @@ class DataPersistenceHandler * DataPersistenceHandler constructor. * * @param EntityDataObject $entityObject - * @param array $dependentObjects - * @param array $customFields + * @param array $dependentObjects + * @param array $customFields */ public function __construct($entityObject, $dependentObjects = [], $customFields = []) { @@ -75,6 +76,7 @@ public function __construct($entityObject, $dependentObjects = [], $customFields * * @param string $storeCode * @return void + * @throws TestFrameworkException */ public function createEntity($storeCode = null) { @@ -95,10 +97,11 @@ public function createEntity($storeCode = null) * Function which executes a put request based on specific operation metadata. * * @param string $updateDataName - * @param array $updateDependentObjects + * @param array $updateDependentObjects * @return void + * @throws TestFrameworkException + * @throws \Exception */ - public function updateEntity($updateDataName, $updateDependentObjects = []) { foreach ($updateDependentObjects as $dependentObject) { @@ -119,10 +122,10 @@ public function updateEntity($updateDataName, $updateDependentObjects = []) * Function which executes a get request on specific operation metadata. * * @param integer|null $index - * @param string $storeCode + * @param string $storeCode * @return void + * @throws TestFrameworkException */ - public function getEntity($index = null, $storeCode = null) { if (!empty($storeCode)) { @@ -142,6 +145,7 @@ public function getEntity($index = null, $storeCode = null) * Function which executes a delete request based on specific operation metadata * * @return void + * @throws TestFrameworkException */ public function deleteEntity() { @@ -163,6 +167,7 @@ public function getCreatedObject() * Returns a specific data value based on the CreatedObject's definition. * @param string $dataName * @return string + * @throws TestFrameworkException */ public function getCreatedDataByName($dataName) { @@ -174,8 +179,8 @@ public function getCreatedDataByName($dataName) * * @param string|array $response * @param integer|null $index - * @param array $requestDataArray - * @param bool $isJson + * @param array $requestDataArray + * @param boolean $isJson * @return void */ private function setCreatedObject($response, $index, $requestDataArray, $isJson) @@ -209,7 +214,7 @@ private function setCreatedObject($response, $index, $requestDataArray, $isJson) /** * Convert an multi-dimensional array to flat array. * - * @param array $arrayIn + * @param array $arrayIn * @param string $rootKey * @return array */ diff --git a/src/Magento/FunctionalTestingFramework/DataGenerator/Persist/OperationDataArrayResolver.php b/src/Magento/FunctionalTestingFramework/DataGenerator/Persist/OperationDataArrayResolver.php index 6097f64da..a14d384a8 100644 --- a/src/Magento/FunctionalTestingFramework/DataGenerator/Persist/OperationDataArrayResolver.php +++ b/src/Magento/FunctionalTestingFramework/DataGenerator/Persist/OperationDataArrayResolver.php @@ -11,6 +11,7 @@ use Magento\FunctionalTestingFramework\DataGenerator\Objects\EntityDataObject; use Magento\FunctionalTestingFramework\DataGenerator\Objects\OperationElement; use Magento\FunctionalTestingFramework\DataGenerator\Util\OperationElementExtractor; +use Magento\FunctionalTestingFramework\Exceptions\TestFrameworkException; class OperationDataArrayResolver { @@ -59,9 +60,9 @@ public function __construct($dependentEntities = null) * structure for the request of the desired entity type. * * @param EntityDataObject $entityObject - * @param array $operationMetadata - * @param string $operation - * @param bool $fromArray + * @param array $operationMetadata + * @param string $operation + * @param boolean $fromArray * @return array * @throws \Exception * @SuppressWarnings(PHPMD.CyclomaticComplexity) @@ -124,7 +125,6 @@ public function resolveOperationDataArray($entityObject, $operationMetadata, $op $operationElementType, $elementData ); - } elseif ($operationElement->isRequired()) { throw new \Exception(sprintf( self::EXCEPTION_REQUIRED_DATA, @@ -171,9 +171,10 @@ public function resolveOperationDataArray($entityObject, $operationMetadata, $op * entities. * * @param EntityDataObject $entityObject - * @param string $operationKey - * @param string $operationElementType + * @param string $operationKey + * @param string $operationElementType * @return array|string + * @throws TestFrameworkException */ private function resolvePrimitiveReference($entityObject, $operationKey, $operationElementType) { @@ -196,7 +197,6 @@ private function resolvePrimitiveReference($entityObject, $operationKey, $operat } return $elementDatas; - } $entity = $this->getDependentEntitiesOfType($type)[0]; @@ -232,8 +232,9 @@ private function getDependentEntitiesOfType($type) * the object. * * @param EntityDataObject $entityObject - * @param string $operationElementValue + * @param string $operationElementValue * @return EntityDataObject|null + * @throws \Exception */ private function resolveOperationObjectAndEntityData($entityObject, $operationElementValue) { @@ -253,11 +254,12 @@ private function resolveOperationObjectAndEntityData($entityObject, $operationEl /** * Resolves DataObjects and pre-defined metadata (in other operation.xml file) referenced by the operation * - * @param string $entityName + * @param string $entityName * @param OperationElement $operationElement - * @param string $operation - * @param bool $fromArray + * @param string $operation + * @param boolean $fromArray * @return array + * @throws \Exception */ private function resolveNonPrimitiveElement($entityName, $operationElement, $operation, $fromArray = false) { @@ -290,6 +292,7 @@ private function resolveNonPrimitiveElement($entityName, $operationElement, $ope * * @param string $entityName * @return EntityDataObject + * @throws \Exception */ private function resolveLinkedEntityObject($entityName) { @@ -320,7 +323,7 @@ private static function incrementSequence($entityName) * Get the current sequence number for an entity. * * @param string $entityName - * @return int + * @return integer */ private static function getSequence($entityName) { diff --git a/src/Magento/FunctionalTestingFramework/DataGenerator/Util/DataExtensionUtil.php b/src/Magento/FunctionalTestingFramework/DataGenerator/Util/DataExtensionUtil.php new file mode 100644 index 000000000..1123531ed --- /dev/null +++ b/src/Magento/FunctionalTestingFramework/DataGenerator/Util/DataExtensionUtil.php @@ -0,0 +1,99 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\FunctionalTestingFramework\DataGenerator\Util; + +use Magento\FunctionalTestingFramework\Config\MftfApplicationConfig; +use Magento\FunctionalTestingFramework\DataGenerator\Handlers\DataObjectHandler; +use Magento\FunctionalTestingFramework\DataGenerator\Objects\EntityDataObject; +use Magento\FunctionalTestingFramework\Exceptions\XmlException; + +class DataExtensionUtil +{ + /** + * ObjectExtensionUtil constructor. + */ + public function __construct() + { + // empty + } + + /** + * Resolves test references for extending test objects + * + * @param EntityDataObject $entityObject + * @return EntityDataObject + * @throws XmlException + */ + public function extendEntity($entityObject) + { + // Check to see if the parent entity exists + $parentEntity = DataObjectHandler::getInstance()->getObject($entityObject->getParentName()); + if ($parentEntity == null) { + throw new XmlException( + "Parent Entity " . + $entityObject->getParentName() . + " not defined for Entity " . + $entityObject->getName() . + "." . + PHP_EOL + ); + } + + // Check to see if the parent entity is already an extended entity + if ($parentEntity->getParentName() !== null) { + throw new XmlException( + "Cannot extend an entity that already extends another entity. Entity: " . + $parentEntity->getName() . + "." . + PHP_EOL + ); + } + if (MftfApplicationConfig::getConfig()->verboseEnabled() && + MftfApplicationConfig::getConfig()->getPhase() !== MftfApplicationConfig::UNIT_TEST_PHASE) { + print("Extending Data: " . $parentEntity->getName() . " => " . $entityObject->getName() . PHP_EOL); + } + + //get parent entity type if child does not have a type + $newType = $entityObject->getType() ?? $parentEntity->getType(); + + // Get all data for both parent and child and merge + $referencedData = $parentEntity->getAllData(); + $newData = array_merge($referencedData, $entityObject->getAllData()); + + // Get all linked references for both parent and child and merge + $referencedLinks = $parentEntity->getLinkedEntities(); + $newLinkedReferences = array_merge($referencedLinks, $entityObject->getLinkedEntities()); + + // Get all unique references for both parent and child and merge + $referencedUniqueData = $parentEntity->getUniquenessData(); + $newUniqueReferences = array_merge($referencedUniqueData, $entityObject->getUniquenessData()); + + // Get all var references for both parent and child and merge + $referencedVars = $parentEntity->getVarReferences(); + $newVarReferences = array_merge($referencedVars, $entityObject->getVarReferences()); + + // Remove unique references for objects that are replaced without such reference + $unmatchedUniqueReferences = array_diff_key($referencedUniqueData, $entityObject->getUniquenessData()); + foreach ($unmatchedUniqueReferences as $uniqueKey => $uniqueData) { + if (array_key_exists($uniqueKey, $entityObject->getAllData())) { + unset($newUniqueReferences[$uniqueKey]); + } + } + + // Create new entity object to return + $extendedEntity = new EntityDataObject( + $entityObject->getName(), + $newType, + $newData, + $newLinkedReferences, + $newUniqueReferences, + $newVarReferences, + $entityObject->getParentName() + ); + return $extendedEntity; + } +} diff --git a/src/Magento/FunctionalTestingFramework/DataGenerator/Util/OperationElementExtractor.php b/src/Magento/FunctionalTestingFramework/DataGenerator/Util/OperationElementExtractor.php index 6f2e3e447..35d188f5b 100644 --- a/src/Magento/FunctionalTestingFramework/DataGenerator/Util/OperationElementExtractor.php +++ b/src/Magento/FunctionalTestingFramework/DataGenerator/Util/OperationElementExtractor.php @@ -86,7 +86,7 @@ public function extractOperationElement($operationElementArray) /** * Creates and Adds relevant DataElements from data entries defined within dataObject array * - * @param array &$operationElements + * @param array $operationElements * @param array $operationFieldArray * @return void */ @@ -105,7 +105,7 @@ private function extractOperationField(&$operationElements, $operationFieldArray /** * Creates and Adds relevant DataElements from data arrays defined within dataObject array * - * @param array &$operationArrayData + * @param array $operationArrayData * @param array $operationArrayArray * @return void */ diff --git a/src/Magento/FunctionalTestingFramework/DataGenerator/etc/dataOperation.xsd b/src/Magento/FunctionalTestingFramework/DataGenerator/etc/dataOperation.xsd index 4d2a93c8f..7a455dc2b 100644 --- a/src/Magento/FunctionalTestingFramework/DataGenerator/etc/dataOperation.xsd +++ b/src/Magento/FunctionalTestingFramework/DataGenerator/etc/dataOperation.xsd @@ -29,6 +29,7 @@ <xs:attribute type="xs:string" name="method"/> <xs:attribute type="xs:string" name="successRegex"/> <xs:attribute type="xs:string" name="returnRegex"/> + <xs:attribute type="xs:string" name="filename"/> </xs:complexType> </xs:element> </xs:sequence> diff --git a/src/Magento/FunctionalTestingFramework/DataGenerator/etc/dataProfileSchema.xsd b/src/Magento/FunctionalTestingFramework/DataGenerator/etc/dataProfileSchema.xsd index b956e718b..aec157309 100644 --- a/src/Magento/FunctionalTestingFramework/DataGenerator/etc/dataProfileSchema.xsd +++ b/src/Magento/FunctionalTestingFramework/DataGenerator/etc/dataProfileSchema.xsd @@ -77,6 +77,14 @@ </xs:documentation> </xs:annotation> </xs:attribute> + <xs:attribute type="xs:string" name="extends"> + <xs:annotation> + <xs:documentation> + Name of the entity that is extended. + </xs:documentation> + </xs:annotation> + </xs:attribute> + <xs:attribute type="xs:string" name="filename"/> </xs:complexType> <xs:complexType name="dataType"> diff --git a/src/Magento/FunctionalTestingFramework/Exceptions/TestFrameworkException.php b/src/Magento/FunctionalTestingFramework/Exceptions/TestFrameworkException.php index 9374feebe..5e9e594c0 100644 --- a/src/Magento/FunctionalTestingFramework/Exceptions/TestFrameworkException.php +++ b/src/Magento/FunctionalTestingFramework/Exceptions/TestFrameworkException.php @@ -6,6 +6,8 @@ namespace Magento\FunctionalTestingFramework\Exceptions; +use Magento\FunctionalTestingFramework\Util\Logger\LoggingUtil; + /** * Class TestFrameworkException */ @@ -14,9 +16,17 @@ class TestFrameworkException extends \Exception /** * TestFrameworkException constructor. * @param string $message + * @param array $context + * @SuppressWarnings(PHPMD.UnusedLocalVariable) */ - public function __construct($message) + public function __construct($message, $context = []) { + list($childClass, $callingClass) = debug_backtrace(false, 2); + LoggingUtil::getInstance()->getLogger($callingClass['class'])->error( + $message, + $context + ); + parent::__construct($message); } } diff --git a/src/Magento/FunctionalTestingFramework/Exceptions/TestReferenceException.php b/src/Magento/FunctionalTestingFramework/Exceptions/TestReferenceException.php index 1fd2e4f67..0546c4a22 100644 --- a/src/Magento/FunctionalTestingFramework/Exceptions/TestReferenceException.php +++ b/src/Magento/FunctionalTestingFramework/Exceptions/TestReferenceException.php @@ -6,6 +6,8 @@ namespace Magento\FunctionalTestingFramework\Exceptions; +use Magento\FunctionalTestingFramework\Util\Logger\LoggingUtil; + /** * Class TestReferenceException */ @@ -14,9 +16,17 @@ class TestReferenceException extends \Exception /** * TestReferenceException constructor. * @param string $message + * @param array $context + * @SuppressWarnings(PHPMD.UnusedLocalVariable) */ - public function __construct($message) + public function __construct($message, $context = []) { + list($childClass, $callingClass) = debug_backtrace(false, 2); + LoggingUtil::getInstance()->getLogger($callingClass['class'])->error( + "Line {$callingClass['line']}: $message", + $context + ); + parent::__construct($message); } } diff --git a/src/Magento/FunctionalTestingFramework/Exceptions/XmlException.php b/src/Magento/FunctionalTestingFramework/Exceptions/XmlException.php index 2ee008b79..929ffd937 100644 --- a/src/Magento/FunctionalTestingFramework/Exceptions/XmlException.php +++ b/src/Magento/FunctionalTestingFramework/Exceptions/XmlException.php @@ -6,6 +6,8 @@ namespace Magento\FunctionalTestingFramework\Exceptions; +use Magento\FunctionalTestingFramework\Util\Logger\LoggingUtil; + /** * Class XmlException */ @@ -14,9 +16,17 @@ class XmlException extends \Exception /** * XmlException constructor. * @param string $message + * @param array $context + * @SuppressWarnings(PHPMD.UnusedLocalVariable) */ - public function __construct($message) + public function __construct($message, $context = []) { + list($childClass, $callingClass) = debug_backtrace(false, 2); + LoggingUtil::getInstance()->getLogger($callingClass['class'])->error( + $message, + $context + ); + parent::__construct($message); } } diff --git a/src/Magento/FunctionalTestingFramework/Extension/ErrorLogger.php b/src/Magento/FunctionalTestingFramework/Extension/ErrorLogger.php index 822395921..bc29fcd64 100644 --- a/src/Magento/FunctionalTestingFramework/Extension/ErrorLogger.php +++ b/src/Magento/FunctionalTestingFramework/Extension/ErrorLogger.php @@ -42,7 +42,7 @@ private function __construct() /** * Loops through stepEvent for browser log entries * @param \Facebook\WebDriver\Remote\RemoteWebDriver $webDriver - * @param \Codeception\Event\StepEvent $stepEvent + * @param \Codeception\Event\StepEvent $stepEvent * @return void */ public function logErrors($webDriver, $stepEvent) @@ -58,9 +58,9 @@ public function logErrors($webDriver, $stepEvent) /** * Logs errors to console/report. - * @param string $type + * @param string $type * @param \Codeception\Event\StepEvent $stepEvent - * @param array $entry + * @param array $entry * @return void */ private function logError($type, $stepEvent, $entry) diff --git a/src/Magento/FunctionalTestingFramework/Extension/PageReadinessExtension.php b/src/Magento/FunctionalTestingFramework/Extension/PageReadinessExtension.php new file mode 100644 index 000000000..9b9163a0a --- /dev/null +++ b/src/Magento/FunctionalTestingFramework/Extension/PageReadinessExtension.php @@ -0,0 +1,272 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\FunctionalTestingFramework\Extension; + +use Codeception\Event\StepEvent; +use Codeception\Event\TestEvent; +use Codeception\Events; +use Codeception\Exception\ModuleRequireException; +use Codeception\Extension; +use Codeception\Module\WebDriver; +use Codeception\Step; +use Facebook\WebDriver\Exception\UnexpectedAlertOpenException; +use Magento\FunctionalTestingFramework\Extension\ReadinessMetrics\AbstractMetricCheck; +use Facebook\WebDriver\Exception\TimeOutException; +use Magento\FunctionalTestingFramework\Util\Logger\LoggingUtil; +use Magento\FunctionalTestingFramework\Config\MftfApplicationConfig; +use Monolog\Logger; + +/** + * Class PageReadinessExtension + */ +class PageReadinessExtension extends Extension +{ + /** + * Codeception Events Mapping to methods + * + * @var array + */ + public static $events = [ + Events::TEST_BEFORE => 'beforeTest', + Events::STEP_BEFORE => 'beforeStep' + ]; + + /** + * List of action types that should bypass metric checks + * shouldSkipCheck() also checks for the 'Comment' step type, which doesn't follow the $step->getAction() pattern + * + * @var array + */ + private $ignoredActions = [ + 'saveScreenshot', + 'wait' + ]; + + /** + * @var Logger + */ + private $logger; + + /** + * Logger verbosity + * + * @var boolean + */ + private $verbose; + + /** + * Array of readiness metrics, initialized during beforeTest event + * + * @var AbstractMetricCheck[] + */ + private $readinessMetrics; + + /** + * The name of the active test + * + * @var string + */ + private $testName; + + /** + * The current URI of the active page + * + * @var string + */ + private $uri; + + /** + * Initialize local vars + * + * @return void + * @throws \Exception + */ + public function _initialize() + { + $this->logger = LoggingUtil::getInstance()->getLogger(get_class($this)); + $this->verbose = MftfApplicationConfig::getConfig()->verboseEnabled(); + } + + /** + * WebDriver instance to use to execute readiness metric checks + * + * @return WebDriver + * @throws ModuleRequireException + */ + public function getDriver() + { + return $this->getModule($this->config['driver']); + } + + /** + * Initialize the readiness metrics for the test + * + * @param \Codeception\Event\TestEvent $e + * @return void + */ + public function beforeTest(TestEvent $e) + { + if (isset($this->config['resetFailureThreshold'])) { + $failThreshold = intval($this->config['resetFailureThreshold']); + } else { + $failThreshold = 3; + } + + $this->testName = $e->getTest()->getMetadata()->getName(); + $this->uri = null; + + $metrics = []; + foreach ($this->config['readinessMetrics'] as $metricClass) { + $metrics[] = new $metricClass($this, $failThreshold); + } + + $this->readinessMetrics = $metrics; + } + + /** + * Waits for busy page flags to disappear before executing a step + * + * @param StepEvent $e + * @return void + * @throws \Exception + */ + public function beforeStep(StepEvent $e) + { + $step = $e->getStep(); + if ($this->shouldSkipCheck($step)) { + return; + } + + $this->checkForNewPage($step); + + // todo: Implement step parameter to override global timeout configuration + if (isset($this->config['timeout'])) { + $timeout = intval($this->config['timeout']); + } else { + $timeout = $this->getDriver()->_getConfig()['pageload_timeout']; + } + + $metrics = $this->readinessMetrics; + + try { + $this->getDriver()->webDriver->wait($timeout)->until( + function () use ($metrics) { + $passing = true; + + /** @var AbstractMetricCheck $metric */ + foreach ($metrics as $metric) { + try { + if (!$metric->runCheck()) { + $passing = false; + } + } catch (UnexpectedAlertOpenException $exception) { + } + } + return $passing; + } + ); + } catch (TimeoutException $exception) { + } + + /** @var AbstractMetricCheck $metric */ + foreach ($metrics as $metric) { + $metric->finalizeForStep($step); + } + } + + /** + * Check if the URI has changed and reset metric tracking if so + * + * @param Step $step + * @return void + */ + private function checkForNewPage($step) + { + try { + $currentUri = $this->getDriver()->_getCurrentUri(); + + if ($this->uri !== $currentUri) { + $this->logDebug( + 'Page URI changed; resetting readiness metric failure tracking', + [ + 'step' => $step->__toString(), + 'newUri' => $currentUri + ] + ); + + /** @var AbstractMetricCheck $metric */ + foreach ($this->readinessMetrics as $metric) { + $metric->resetTracker(); + } + + $this->uri = $currentUri; + } + } catch (\Exception $e) { + $this->logDebug('Could not retrieve current URI', ['step' => $step->__toString()]); + } + } + + /** + * Gets the active page URI from the start of the most recent step + * + * @return string + */ + public function getUri() + { + return $this->uri; + } + + /** + * Gets the name of the active test + * + * @return string + */ + public function getTestName() + { + return $this->testName; + } + + /** + * Should the given step bypass the readiness checks + * todo: Implement step parameter to bypass specific metrics (or all) instead of basing on action type + * + * @param Step $step + * @return boolean + */ + private function shouldSkipCheck($step) + { + if ($step instanceof Step\Comment || in_array($step->getAction(), $this->ignoredActions)) { + return true; + } + return false; + } + + /** + * If verbose, log the given message to logger->debug including test context information + * + * @param string $message + * @param array $context + * @return void + * @SuppressWarnings(PHPMD) + */ + private function logDebug($message, $context = []) + { + if ($this->verbose) { + $logContext = [ + 'test' => $this->testName, + 'uri' => $this->uri + ]; + foreach ($this->readinessMetrics as $metric) { + $logContext[$metric->getName()] = $metric->getStoredValue(); + $logContext[$metric->getName() . '.failCount'] = $metric->getFailureCount(); + } + $context = array_merge($logContext, $context); + //TODO REMOVE THIS LINE, UNCOMMENT LOGGER + //$this->logger->info($message, $context); + } + } +} diff --git a/src/Magento/FunctionalTestingFramework/Extension/ReadinessMetrics/AbstractMetricCheck.php b/src/Magento/FunctionalTestingFramework/Extension/ReadinessMetrics/AbstractMetricCheck.php new file mode 100644 index 000000000..417ae336f --- /dev/null +++ b/src/Magento/FunctionalTestingFramework/Extension/ReadinessMetrics/AbstractMetricCheck.php @@ -0,0 +1,365 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\FunctionalTestingFramework\Extension\ReadinessMetrics; + +use Codeception\Exception\ModuleRequireException; +use Codeception\Module\WebDriver; +use Codeception\Step; +use Facebook\WebDriver\Exception\UnexpectedAlertOpenException; +use Magento\FunctionalTestingFramework\Extension\PageReadinessExtension; +use Magento\FunctionalTestingFramework\Util\Logger\LoggingUtil; +use Magento\FunctionalTestingFramework\Config\MftfApplicationConfig; +use Monolog\Logger; + +/** + * Class AbstractMetricCheck + */ +abstract class AbstractMetricCheck +{ + /** + * Extension being used to verify this metric passes before test metrics + * + * @var PageReadinessExtension + */ + protected $extension; + + /** + * Current state of the value the metric tracks + * + * @var mixed; + */ + protected $currentValue; + + /** + * Most recent saved state of the value the metric tracks + * Updated when the metric passes or is finalized + * + * @var mixed; + */ + protected $storedValue; + + /** + * Current count of sequential identical failures + * + * @var integer; + */ + protected $failCount; + + /** + * Number of sequential identical failures before force-resetting the metric + * + * @var integer + */ + protected $resetFailureThreshold; + + /** + * @var Logger + */ + protected $logger; + + /** + * @var boolean + */ + protected $verbose; + + /** + * Constructor, called from the beforeTest event + * + * @param PageReadinessExtension $extension + * @param integer $resetFailureThreshold + * @throws \Exception + */ + public function __construct($extension, $resetFailureThreshold) + { + $this->extension = $extension; + $this->logger = LoggingUtil::getInstance()->getLogger(get_class($this)); + $this->verbose = MftfApplicationConfig::getConfig()->verboseEnabled(); + + // If the clearFailureOnPage() method is overridden, use the configured failure threshold + // If not, the default clearFailureOnPage() method does nothing so don't worry about resetting failures + $reflector = new \ReflectionMethod($this, 'clearFailureOnPage'); + if ($reflector->getDeclaringClass()->getName() === get_class($this)) { + $this->resetFailureThreshold = $resetFailureThreshold; + } else { + $this->resetFailureThreshold = -1; + } + + $this->resetTracker(); + } + + /** + * Does the given value pass the readiness metric + * + * @param mixed $value + * @return boolean + */ + abstract protected function doesMetricPass($value); + + /** + * Retrieve the active value for the metric to check from the page + * + * @return mixed + * @throws UnexpectedAlertOpenException + */ + abstract protected function fetchValueFromPage(); + + /** + * Override this method to reset the actual state of the page to make the metric pass + * This method is called when too many identical failures were encountered in a row + * + * @return void + */ + protected function clearFailureOnPage() + { + return; + } + + /** + * Get the base class name of the metric implementation + * + * @return string + */ + public function getName() + { + $clazz = get_class($this); + $namespaceBreak = strrpos($clazz, '\\'); + if ($namespaceBreak !== false) { + $clazz = substr($clazz, $namespaceBreak + 1); + } + return $clazz; + } + + /** + * Fetches a new value for the metric and checks if it passes, clearing the failure tracking if so + * + * Even on a success, the readiness check will continue to be run until all metrics pass at the same time in order + * to catch cases where a slow request of one metric can trigger calls for other metrics that were previously + * thought ready + * + * @return boolean + * @throws UnexpectedAlertOpenException + */ + public function runCheck() + { + if ($this->doesMetricPass($this->getCurrentValue(true))) { + $this->setTracker($this->getCurrentValue(), 0); + return true; + } + + return false; + } + + /** + * Update the state of the metric including tracked failure state and checking if a failing value is stuck and + * needs to be reset so future checks can be accurate + * + * Called when the readiness check is finished (either all metrics pass or the check has timed out) + * + * @param Step $step + * @return void + */ + public function finalizeForStep($step) + { + try { + $currentValue = $this->getCurrentValue(); + } catch (UnexpectedAlertOpenException $exception) { + $this->debugLog( + 'An alert is open, bypassing javascript-based metric check', + ['step' => $step->__toString()] + ); + return; + } + + if ($this->doesMetricPass($currentValue)) { + $this->setTracker($currentValue, 0); + } else { + // If failure happened on the same value as before, increment the fail count, otherwise set at 1 + if (!isset($this->storedValue) || $currentValue !== $this->getStoredValue()) { + $failCount = 1; + } else { + $failCount = $this->getFailureCount() + 1; + } + $this->setTracker($currentValue, $failCount); + + $this->errorLog('Failed readiness check', ['step' => $step->__toString()]); + + if ($this->resetFailureThreshold >= 0 && $failCount >= $this->resetFailureThreshold) { + $this->debugLog( + 'Too many failures, assuming metric is stuck and resetting state', + ['step' => $step->__toString()] + ); + $this->resetMetric(); + } + } + } + + /** + * Helper function to retrieve the driver being used to run the test + * + * @return WebDriver + * @throws ModuleRequireException + */ + protected function getDriver() + { + return $this->extension->getDriver(); + } + + /** + * Helper function to execute javascript code, see WebDriver::executeJs for more information + * + * @param string $script + * @param array $arguments + * @return mixed + * @throws UnexpectedAlertOpenException + * @throws ModuleRequireException + */ + protected function executeJs($script, $arguments = []) + { + return $this->extension->getDriver()->executeJS($script, $arguments); + } + + /** + * Gets the current state of the given variable + * Fetches an updated value if not known or $refresh is true + * + * @param boolean $refresh + * @return mixed + * @throws UnexpectedAlertOpenException + */ + private function getCurrentValue($refresh = false) + { + if ($refresh) { + unset($this->currentValue); + } + if (!isset($this->currentValue)) { + $this->currentValue = $this->fetchValueFromPage(); + } + return $this->currentValue; + } + + /** + * Returns the value of the given variable for the previous check + * + * @return mixed + */ + public function getStoredValue() + { + return $this->storedValue ?? null; + } + + /** + * The current count of sequential identical failures + * Used to detect potentially stuck metrics + * + * @return integer + */ + public function getFailureCount() + { + return $this->failCount; + } + + /** + * Update the state of the page to pass the metric and clear the saved failure state + * Called when a failure is found to be stuck + * + * @return void + */ + private function resetMetric() + { + $this->clearFailureOnPage(); + $this->resetTracker(); + } + + /** + * Tracks the most recent value and the number of identical failures in a row + * + * @param mixed $value + * @param integer $failCount + * @return void + */ + public function setTracker($value, $failCount) + { + unset($this->currentValue); + $this->storedValue = $value; + $this->failCount = $failCount; + } + + /** + * Resets the tracked metric values on a new page or stuck failure + * + * @return void + */ + public function resetTracker() + { + unset($this->currentValue); + unset($this->storedValue); + $this->failCount = 0; + } + + /** + * Log the given message to logger->error including context information + * + * @param string $message + * @param array $context + * @return void + * @SuppressWarnings(PHPMD) + */ + protected function errorLog($message, $context = []) + { + $context = array_merge($this->getLogContext(), $context); + //TODO REMOVE THIS LINE, UNCOMMENT LOGGER + //$this->logger->error($message, $context); + } + + /** + * Log the given message to logger->info including context information + * + * @param string $message + * @param array $context + * @return void + * @SuppressWarnings(PHPMD) + */ + protected function infoLog($message, $context = []) + { + $context = array_merge($this->getLogContext(), $context); + //TODO REMOVE THIS LINE, UNCOMMENT LOGGER + //$this->logger->info($message, $context); + } + + /** + * If verbose, log the given message to logger->debug including context information + * + * @param string $message + * @param array $context + * @return void + * @SuppressWarnings(PHPMD) + */ + protected function debugLog($message, $context = []) + { + if ($this->verbose) { + $context = array_merge($this->getLogContext(), $context); + //TODO REMOVE THIS LINE, UNCOMMENT LOGGER + //$this->logger->debug($message, $context); + } + } + + /** + * Base context information to include in all log messages: test name, current URI, metric state + * Reports most recent stored value, not current value, so call setTracker() first to update + * + * @return array + */ + private function getLogContext() + { + return [ + 'test' => $this->extension->getTestName(), + 'uri' => $this->extension->getUri(), + $this->getName() => $this->getStoredValue(), + $this->getName() . '.failCount' => $this->getFailureCount() + ]; + } +} diff --git a/src/Magento/FunctionalTestingFramework/Extension/ReadinessMetrics/DocumentReadyState.php b/src/Magento/FunctionalTestingFramework/Extension/ReadinessMetrics/DocumentReadyState.php new file mode 100644 index 000000000..26cf91aa7 --- /dev/null +++ b/src/Magento/FunctionalTestingFramework/Extension/ReadinessMetrics/DocumentReadyState.php @@ -0,0 +1,43 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\FunctionalTestingFramework\Extension\ReadinessMetrics; + +/** + * Class DocumentReadyState + */ + +use Facebook\WebDriver\Exception\UnexpectedAlertOpenException; + +/** + * Class DocumentReadyState + * + * Looks for document.readyState == 'complete' before passing the readiness check + */ +class DocumentReadyState extends AbstractMetricCheck +{ + /** + * Metric passes when document.readyState == 'complete' + * + * @param string $value + * @return boolean + */ + protected function doesMetricPass($value) + { + return $value === 'complete'; + } + + /** + * Retrieve document.readyState + * + * @return string + * @throws UnexpectedAlertOpenException + */ + protected function fetchValueFromPage() + { + return $this->executeJS('return document.readyState;'); + } +} diff --git a/src/Magento/FunctionalTestingFramework/Extension/ReadinessMetrics/JQueryAjaxRequests.php b/src/Magento/FunctionalTestingFramework/Extension/ReadinessMetrics/JQueryAjaxRequests.php new file mode 100644 index 000000000..c005923d3 --- /dev/null +++ b/src/Magento/FunctionalTestingFramework/Extension/ReadinessMetrics/JQueryAjaxRequests.php @@ -0,0 +1,58 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\FunctionalTestingFramework\Extension\ReadinessMetrics; + +use Facebook\WebDriver\Exception\UnexpectedAlertOpenException; + +/** + * Class JQueryAjaxRequests + * + * Looks for all active jQuery ajax requests to finish before passing the readiness check + */ +class JQueryAjaxRequests extends AbstractMetricCheck +{ + /** + * Metric passes once there are no remaining active requests + * + * @param integer $value + * @return boolean + */ + protected function doesMetricPass($value) + { + return $value == 0; + } + + /** + * Grabs the number of active jQuery ajax requests if available + * + * @return integer + * @throws UnexpectedAlertOpenException + */ + protected function fetchValueFromPage() + { + return intval( + $this->executeJS( + 'if (!!window.jQuery) { + return window.jQuery.active; + } + return 0;' + ) + ); + } + + /** + * Active request count can get stuck above zero if an exception is thrown during a callback, causing the + * ajax handler method to fail before decrementing the request count + * + * @return void + * @throws UnexpectedAlertOpenException + */ + protected function clearFailureOnPage() + { + $this->executeJS('if (!!window.jQuery) { window.jQuery.active = 0; };'); + } +} diff --git a/src/Magento/FunctionalTestingFramework/Extension/ReadinessMetrics/MagentoLoadingMasks.php b/src/Magento/FunctionalTestingFramework/Extension/ReadinessMetrics/MagentoLoadingMasks.php new file mode 100644 index 000000000..4f15524ba --- /dev/null +++ b/src/Magento/FunctionalTestingFramework/Extension/ReadinessMetrics/MagentoLoadingMasks.php @@ -0,0 +1,54 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\FunctionalTestingFramework\Extension\ReadinessMetrics; + +use Facebook\WebDriver\Exception\NoSuchElementException; +use Facebook\WebDriver\Exception\StaleElementReferenceException; +use Magento\FunctionalTestingFramework\Module\MagentoWebDriver; +use WebDriverBy; + +/** + * Class MagentoLoadingMasks + * + * Looks for all loading masks to disappear before passing the readiness check + */ +class MagentoLoadingMasks extends AbstractMetricCheck +{ + /** + * Metric passes once all loading masks are absent or invisible + * + * @param string|null $value + * @return boolean + */ + protected function doesMetricPass($value) + { + return $value === null; + } + + /** + * Get the locator and ID for the first active loading mask or null if there are none visible + * + * @return string|null + */ + protected function fetchValueFromPage() + { + foreach (MagentoWebDriver::$loadingMasksLocators as $maskLocator) { + $driverLocator = WebDriverBy::xpath($maskLocator); + $maskElements = $this->getDriver()->webDriver->findElements($driverLocator); + foreach ($maskElements as $element) { + try { + if ($element->isDisplayed()) { + return "$maskLocator : " . $element ->getID(); + } + } catch (NoSuchElementException $e) { + } catch (StaleElementReferenceException $e) { + } + } + } + return null; + } +} diff --git a/src/Magento/FunctionalTestingFramework/Extension/ReadinessMetrics/PrototypeAjaxRequests.php b/src/Magento/FunctionalTestingFramework/Extension/ReadinessMetrics/PrototypeAjaxRequests.php new file mode 100644 index 000000000..2fc8f70cb --- /dev/null +++ b/src/Magento/FunctionalTestingFramework/Extension/ReadinessMetrics/PrototypeAjaxRequests.php @@ -0,0 +1,58 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\FunctionalTestingFramework\Extension\ReadinessMetrics; + +use Facebook\WebDriver\Exception\UnexpectedAlertOpenException; + +/** + * Class PrototypeAjaxRequests + * + * Looks for all active prototype ajax requests to finish before passing the readiness check + */ +class PrototypeAjaxRequests extends AbstractMetricCheck +{ + /** + * Metric passes once there are no remaining active requests + * + * @param integer $value + * @return boolean + */ + protected function doesMetricPass($value) + { + return $value == 0; + } + + /** + * Grabs the number of active prototype ajax requests if available + * + * @return integer + * @throws UnexpectedAlertOpenException + */ + protected function fetchValueFromPage() + { + return intval( + $this->executeJS( + 'if (!!window.Prototype) { + return window.Ajax.activeRequestCount; + } + return 0;' + ) + ); + } + + /** + * Active request count can get stuck above zero if an exception is thrown during a callback, causing the + * ajax handler method to fail before decrementing the request count + * + * @return void + * @throws UnexpectedAlertOpenException + */ + protected function clearFailureOnPage() + { + $this->executeJS('if (!!window.Prototype) { window.Ajax.activeRequestCount = 0; };'); + } +} diff --git a/src/Magento/FunctionalTestingFramework/Extension/ReadinessMetrics/RequireJsDefinitions.php b/src/Magento/FunctionalTestingFramework/Extension/ReadinessMetrics/RequireJsDefinitions.php new file mode 100644 index 000000000..6df470123 --- /dev/null +++ b/src/Magento/FunctionalTestingFramework/Extension/ReadinessMetrics/RequireJsDefinitions.php @@ -0,0 +1,60 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\FunctionalTestingFramework\Extension\ReadinessMetrics; + +use Facebook\WebDriver\Exception\UnexpectedAlertOpenException; + +/** + * Class RequireJsDefinitions + * + * Looks for all active require.js module definitions to complete before passing the readiness check + */ +class RequireJsDefinitions extends AbstractMetricCheck +{ + /** + * Metric passes once there are no enabled modules waiting in the registry queue + * + * @param string|null $value + * @return boolean + */ + protected function doesMetricPass($value) + { + return $value === null; + } + + /** + * Retrieve the name of the first enabled module still waiting in the require.js registry queue + * + * @return string|null + * @throws UnexpectedAlertOpenException + */ + protected function fetchValueFromPage() + { + $script = + 'if (!window.requirejs) { + return null; + } + var contexts = window.requirejs.s.contexts; + for (var label in contexts) { + if (contexts.hasOwnProperty(label)) { + var registry = contexts[label].registry; + for (var module in registry) { + if (registry.hasOwnProperty(module) && registry[module].enabled) { + return module; + } + } + } + } + return null;'; + + $moduleInProgress = $this->executeJS($script); + if ($moduleInProgress === 'null') { + $moduleInProgress = null; + } + return $moduleInProgress; + } +} diff --git a/src/Magento/FunctionalTestingFramework/Extension/TestContextExtension.php b/src/Magento/FunctionalTestingFramework/Extension/TestContextExtension.php index c03743dc3..c0bf78f19 100644 --- a/src/Magento/FunctionalTestingFramework/Extension/TestContextExtension.php +++ b/src/Magento/FunctionalTestingFramework/Extension/TestContextExtension.php @@ -23,7 +23,8 @@ class TestContextExtension extends \Codeception\Extension */ public static $events = [ Events::TEST_FAIL => 'testFail', - Events::STEP_AFTER => 'afterStep' + Events::STEP_AFTER => 'afterStep', + Events::TEST_END => 'testError' ]; /** @@ -38,25 +39,64 @@ public function testFail(\Codeception\Event\FailEvent $e) // Do not attempt to run _after if failure was in the _after block // Try to run _after but catch exceptions to prevent them from overwriting original failure. if ($context != TestContextExtension::TEST_PHASE_AFTER) { - try { - $actorClass = $e->getTest()->getMetadata()->getCurrent('actor'); - $I = new $actorClass($cest->getScenario()); - call_user_func(\Closure::bind( - function () use ($cest, $I) { - $cest->executeHook($I, 'after'); - }, - null, - $cest - )); - } catch (\Exception $e) { - // Do not rethrow Exception + $this->runAfterBlock($e, $cest); + } + } + + /** + * Codeception event listener function, triggered on test error. + * @param \Codeception\Event\TestEvent $e + * @return void + */ + public function testError(\Codeception\Event\TestEvent $e) + { + $cest = $e->getTest(); + + //Access private TestResultObject to find stack and if there are any errors (as opposed to failures) + $testResultObject = call_user_func(\Closure::bind( + function () use ($cest) { + return $cest->getTestResultObject(); + }, + $cest + )); + $errors = $testResultObject->errors(); + if (!empty($errors)) { + $stack = $errors[0]->thrownException()->getTrace(); + $context = $this->extractContext($stack, $cest->getTestMethod()); + // Do not attempt to run _after if failure was in the _after block + // Try to run _after but catch exceptions to prevent them from overwriting original failure. + if ($context != TestContextExtension::TEST_PHASE_AFTER) { + $this->runAfterBlock($e, $cest); } } } + /** + * Runs cest's after block, if necessary. + * @param Symfony\Component\EventDispatcher\Event $e + * @param \Codeception\TestInterface $cest + * @return void + */ + private function runAfterBlock($e, $cest) + { + try { + $actorClass = $e->getTest()->getMetadata()->getCurrent('actor'); + $I = new $actorClass($cest->getScenario()); + call_user_func(\Closure::bind( + function () use ($cest, $I) { + $cest->executeHook($I, 'after'); + }, + null, + $cest + )); + } catch (\Exception $e) { + // Do not rethrow Exception + } + } + /** * Extracts hook method from trace, looking specifically for the cest class given. - * @param array $trace + * @param array $trace * @param string $class * @return string */ @@ -79,7 +119,9 @@ public function extractContext($trace, $class) */ public function afterStep(\Codeception\Event\StepEvent $e) { + // @codingStandardsIgnoreStart $webDriver = $this->getModule("\Magento\FunctionalTestingFramework\Module\MagentoWebDriver")->webDriver; + // @codingStandardsIgnoreEnd ErrorLogger::getInstance()->logErrors($webDriver, $e); } } diff --git a/src/Magento/FunctionalTestingFramework/Helper/EntityRESTApiHelper.php b/src/Magento/FunctionalTestingFramework/Helper/EntityRESTApiHelper.php index 5358b4461..521ddc9ee 100644 --- a/src/Magento/FunctionalTestingFramework/Helper/EntityRESTApiHelper.php +++ b/src/Magento/FunctionalTestingFramework/Helper/EntityRESTApiHelper.php @@ -50,7 +50,7 @@ public function __construct($host, $port) * @param string $apiMethod * @param string $requestURI * @param string $jsonBody - * @param array $headers + * @param array $headers * @return \Psr\Http\Message\ResponseInterface */ public function submitAuthAPIRequest($apiMethod, $requestURI, $jsonBody, $headers) @@ -93,7 +93,7 @@ private function getAuthToken() * @param string $apiMethod * @param string $requestURI * @param string $jsonBody - * @param array $headers + * @param array $headers * @return \Psr\Http\Message\ResponseInterface */ private function submitAPIRequest($apiMethod, $requestURI, $jsonBody, $headers) diff --git a/src/Magento/FunctionalTestingFramework/Module/MagentoAssert.php b/src/Magento/FunctionalTestingFramework/Module/MagentoAssert.php index 494b4a6ed..3c6c08173 100644 --- a/src/Magento/FunctionalTestingFramework/Module/MagentoAssert.php +++ b/src/Magento/FunctionalTestingFramework/Module/MagentoAssert.php @@ -19,7 +19,7 @@ class MagentoAssert extends \Codeception\Module * Asserts that all items in the array are sorted by given direction. Can be given int, string, double, dates. * Converts given date strings to epoch for comparison. * - * @param array $data + * @param array $data * @param string $sortOrder * @return void */ diff --git a/src/Magento/FunctionalTestingFramework/Module/MagentoRestDriver.php b/src/Magento/FunctionalTestingFramework/Module/MagentoRestDriver.php index 419e16aa9..513d92dfb 100644 --- a/src/Magento/FunctionalTestingFramework/Module/MagentoRestDriver.php +++ b/src/Magento/FunctionalTestingFramework/Module/MagentoRestDriver.php @@ -49,7 +49,6 @@ * Conflicts with SOAP module * */ -// @codingStandardsIgnoreFile class MagentoRestDriver extends REST { /** @@ -60,14 +59,6 @@ class MagentoRestDriver extends REST const HTTP_METHOD_PUT = 'PUT'; const HTTP_METHOD_POST = 'POST'; - protected static $categoryEndpoint = 'categories'; - protected static $productEndpoint = 'products'; - protected static $productAttributesEndpoint = 'products/attributes'; - protected static $productAttributesOptionsEndpoint = 'products/attributes/%s/options'; - protected static $productAttributeSetEndpoint = 'products/attribute-sets/attributes'; - protected static $configurableProductEndpoint = 'configurable-products/%s/options'; - protected static $customersEndpoint = 'customers'; - /** * Module required fields. * @@ -101,6 +92,7 @@ class MagentoRestDriver extends REST * Before suite. * * @param array $settings + * @return void */ public function _beforeSuite($settings = []) { @@ -120,11 +112,14 @@ public function _beforeSuite($settings = []) $this->seeResponseCodeIs(\Codeception\Util\HttpCode::OK); $this->haveHttpHeader('Authorization', 'Bearer ' . $token); self::$adminTokens[$this->config['username']] = $token; + // @codingStandardsIgnoreStart $this->getModule('\Magento\FunctionalTestingFramework\Module\MagentoSequence')->_initialize(); + // @codingStandardsIgnoreEnd } /** * After suite. + * @return void */ public function _afterSuite() { @@ -135,17 +130,17 @@ public function _afterSuite() /** * Get admin auth token by username and password. * - * @param string $username - * @param string $password - * @param bool $newToken + * @param string $username + * @param string $password + * @param boolean $newToken * @return string * @part json * @part xml */ public function getAdminAuthToken($username = null, $password = null, $newToken = false) { - $username = !is_null($username) ? $username : $this->config['username']; - $password = !is_null($password) ? $password : $this->config['password']; + $username = $username !== null ? $username : $this->config['username']; + $password = $password !== null ? $password : $this->config['password']; // Use existing token if it exists if (!$newToken @@ -163,16 +158,17 @@ public function getAdminAuthToken($username = null, $password = null, $newToken /** * Admin token authentication for a given user. * - * @param string $username - * @param string $password - * @param bool $newToken + * @param string $username + * @param string $password + * @param boolean $newToken * @part json * @part xml + * @return void */ public function amAdminTokenAuthenticated($username = null, $password = null, $newToken = false) { - $username = !is_null($username) ? $username : $this->config['username']; - $password = !is_null($password) ? $password : $this->config['password']; + $username = $username !== null ? $username : $this->config['username']; + $password = $password !== null ? $password : $this->config['password']; $this->haveHttpHeader('Content-Type', 'application/json'); if ($newToken || !isset(self::$adminTokens[$username])) { @@ -187,11 +183,11 @@ public function amAdminTokenAuthenticated($username = null, $password = null, $n /** * Send REST API request. * - * @param string $endpoint - * @param string $httpMethod - * @param array $params - * @param string $grabByJsonPath - * @param bool $decode + * @param string $endpoint + * @param string $httpMethod + * @param array $params + * @param string $grabByJsonPath + * @param boolean $decode * @return mixed * @throws \LogicException * @part json @@ -218,7 +214,7 @@ public function sendRestRequest($endpoint, $httpMethod, $params = [], $grabByJso } $this->seeResponseCodeIs(\Codeception\Util\HttpCode::OK); - if (!$decode && is_null($grabByJsonPath)) { + if (!$decode && $grabByJsonPath === null) { return $this->grabResponse(); } elseif (!$decode) { return $this->grabDataFromResponseByJsonPath($grabByJsonPath); @@ -251,8 +247,8 @@ public function requireCategory($categoryData = []) /** * Create a simple product in Magento. * - * @param int $categoryId - * @param array $simpleProductData + * @param integer $categoryId + * @param array $simpleProductData * @return array|mixed * @part json * @part xml @@ -273,8 +269,8 @@ public function requireSimpleProduct($categoryId = 0, $simpleProductData = []) /** * Create a configurable product in Magento. * - * @param int $categoryId - * @param array $configurableProductData + * @param integer $categoryId + * @param array $configurableProductData * @return array|mixed * @part json * @part xml @@ -382,7 +378,7 @@ public function requireProductAttribute($code = 'attribute') /** * Create a customer in Magento. * - * @param array $customerData + * @param array $customerData * @param string $password * @return array|mixed * @part json @@ -445,9 +441,9 @@ public function getCategoryApiData($categoryData = []) /** * Get simple product api data. * - * @param string $type + * @param string $type * @param integer $categoryId - * @param array $productData + * @param array $productData * @return array * @part json * @part xml @@ -513,7 +509,7 @@ public function getCustomerApiData($customerData = []) /** * Get customer data including password. * - * @param array $customerData + * @param array $customerData * @param string $password * @return array * @part json @@ -526,7 +522,7 @@ public function getCustomerApiDataWithPassword($customerData = [], $password = ' /** * @param string $code - * @param array $attributeData + * @param array $attributeData * @return array * @part json * @part xml @@ -625,10 +621,10 @@ public function getConfigurableProductOptionsApiData($attributes, $optionIds) } /** - * @param array $configurableProductOptions - * @param array $childProductIds - * @param array $configurableProduct - * @param int $categoryId + * @param array $configurableProductOptions + * @param array $childProductIds + * @param array $configurableProduct + * @param integer $categoryId * @return array * @part json * @part xml @@ -655,9 +651,9 @@ public function getConfigurableProductApiData( } /** - * @param $attributeCode - * @param int $attributeSetId - * @param int $attributeGroupId + * @param string $attributeCode + * @param integer $attributeSetId + * @param integer $attributeGroupId * @return array * @part json * @part xml diff --git a/src/Magento/FunctionalTestingFramework/Module/MagentoSequence.php b/src/Magento/FunctionalTestingFramework/Module/MagentoSequence.php index df59a62ec..6273965dd 100644 --- a/src/Magento/FunctionalTestingFramework/Module/MagentoSequence.php +++ b/src/Magento/FunctionalTestingFramework/Module/MagentoSequence.php @@ -3,7 +3,8 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - + +// @codingStandardsIgnoreFile namespace Magento\FunctionalTestingFramework\Module; use Codeception\Module\Sequence; @@ -12,13 +13,11 @@ /** * MagentoSequence module. * - * @codingStandardsIgnoreFile */ class MagentoSequence extends Sequence { protected $config = ['prefix' => '']; } - if (!function_exists('msq') && !function_exists('msqs')) { require_once __DIR__ . DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR . 'Util' . DIRECTORY_SEPARATOR . 'msq.php'; } else { diff --git a/src/Magento/FunctionalTestingFramework/Module/MagentoWebDriver.php b/src/Magento/FunctionalTestingFramework/Module/MagentoWebDriver.php index 1917e8bab..62907fca4 100644 --- a/src/Magento/FunctionalTestingFramework/Module/MagentoWebDriver.php +++ b/src/Magento/FunctionalTestingFramework/Module/MagentoWebDriver.php @@ -6,25 +6,18 @@ namespace Magento\FunctionalTestingFramework\Module; -use Codeception\Events; use Codeception\Module\WebDriver; use Codeception\Test\Descriptor; use Codeception\TestInterface; -use Facebook\WebDriver\WebDriverSelect; -use Facebook\WebDriver\WebDriverBy; -use Facebook\WebDriver\Exception\NoSuchElementException; use Facebook\WebDriver\Interactions\WebDriverActions; -use Codeception\Exception\ElementNotFound; use Codeception\Exception\ModuleConfigException; use Codeception\Exception\ModuleException; use Codeception\Util\Uri; -use Codeception\Util\ActionSequence; +use Magento\FunctionalTestingFramework\DataGenerator\Handlers\CredentialStore; use Magento\FunctionalTestingFramework\DataGenerator\Persist\Curl\WebapiExecutor; use Magento\FunctionalTestingFramework\Util\Protocol\CurlTransport; use Magento\FunctionalTestingFramework\Util\Protocol\CurlInterface; -use Magento\Setup\Exception; use Magento\FunctionalTestingFramework\Util\ConfigSanitizerUtil; -use Yandex\Allure\Adapter\Event\TestCaseFinishedEvent; use Yandex\Allure\Adapter\Support\AttachmentSupport; /** @@ -44,11 +37,17 @@ * password: admin_password * browser: chrome * ``` + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ -// @codingStandardsIgnoreFile class MagentoWebDriver extends WebDriver { use AttachmentSupport; + + /** + * List of known magento loading masks by selector + * @var array + */ public static $loadingMasksLocators = [ '//div[contains(@class, "loading-mask")]', '//div[contains(@class, "admin_data-grid-loading-mask")]', @@ -105,12 +104,21 @@ class MagentoWebDriver extends WebDriver */ private $htmlReport; + /** + * Sanitizes config, then initializes using parent. + * @return void + */ public function _initialize() { $this->config = ConfigSanitizerUtil::sanitizeWebDriverConfig($this->config); parent::_initialize(); } + /** + * Calls parent reset, then re-sanitizes config + * + * @return void + */ public function _resetConfig() { parent::_resetConfig(); @@ -250,18 +258,19 @@ public function closeAdminNotification() // Cheating here for the minute. Still working on the best method to deal with this issue. try { $this->executeJS("jQuery('.modal-popup').remove(); jQuery('.modals-overlay').remove();"); - } catch (\Exception $e) {} + } catch (\Exception $e) { + } } - /** * Search for and Select multiple options from a Magento Multi-Select drop down menu. * e.g. The drop down menu you use to assign Products to Categories. * - * @param $select - * @param array $options - * @param bool $requireAction + * @param string $select + * @param array $options + * @param boolean $requireAction * @throws \Exception + * @return void */ public function searchAndMultiSelectOption($select, array $options, $requireAction = false) { @@ -286,8 +295,8 @@ public function searchAndMultiSelectOption($select, array $options, $requireActi /** * Select multiple options from a drop down using a filter and text field to narrow results. * - * @param string $selectSearchTextField - * @param string $selectSearchResult + * @param string $selectSearchTextField + * @param string $selectSearchResult * @param string[] $options * @return void */ @@ -306,7 +315,8 @@ public function selectMultipleOptions($selectSearchTextField, $selectSearchResul /** * Wait for all Ajax calls to finish. * - * @param int $timeout + * @param integer $timeout + * @return void */ public function waitForAjaxLoad($timeout = null) { @@ -324,8 +334,9 @@ public function waitForAjaxLoad($timeout = null) /** * Wait for all JavaScript to finish executing. * - * @param int $timeout + * @param integer $timeout * @throws \Exception + * @return void */ public function waitForPageLoad($timeout = null) { @@ -340,10 +351,11 @@ public function waitForPageLoad($timeout = null) * Wait for all visible loading masks to disappear. Gets all elements by mask selector, then loops over them. * * @throws \Exception + * @return void */ public function waitForLoadingMaskToDisappear() { - foreach( self::$loadingMasksLocators as $maskLocator) { + foreach (self::$loadingMasksLocators as $maskLocator) { // Get count of elements found for looping. // Elements are NOT useful for interaction, as they cannot be fed to codeception actions. $loadingMaskElements = $this->_findElements($maskLocator); @@ -359,6 +371,7 @@ public function waitForLoadingMaskToDisappear() * Verify that there are no JavaScript errors in the console. * * @throws ModuleException + * @return void */ public function dontSeeJsError() { @@ -371,7 +384,7 @@ public function dontSeeJsError() } /** - * @param float $money + * @param float $money * @param string $locale * @return array */ @@ -391,14 +404,16 @@ public function formatMoney(float $money, $locale = 'en_US.UTF-8') * @param string $floatString * @return float */ - public function parseFloat($floatString){ + public function parseFloat($floatString) + { $floatString = str_replace(',', '', $floatString); return floatval($floatString); } /** - * @param int $category - * @param string $locale + * @param integer $category + * @param string $locale + * @return void */ public function mSetLocale(int $category, $locale) { @@ -413,11 +428,12 @@ public function mSetLocale(int $category, $locale) /** * Reset Locale setting. + * @return void */ public function mResetLocale() { foreach (self::$localeAll as $c => $l) { - if (!is_null($l)) { + if ($l !== null) { setlocale($c, $l); self::$localeAll[$c] = null; } @@ -426,6 +442,7 @@ public function mResetLocale() /** * Scroll to the Top of the Page. + * @return void */ public function scrollToTopOfPage() { @@ -435,13 +452,26 @@ public function scrollToTopOfPage() /** * Takes given $command and executes it against exposed MTF CLI entry point. Returns response from server. * @param string $command - * @returns string + * @param string $arguments + * @return string */ - public function magentoCLI($command) + public function magentoCLI($command, $arguments = null) { - $apiURL = $this->config['url'] . getenv('MAGENTO_CLI_COMMAND_PATH'); + // trim everything after first '/' in URL after (ex http://magento.instance/<index.php>) + preg_match("/.+\/\/[^\/]+\/?/", $this->config['url'], $trimmed); + $trimmedUrl = $trimmed[0]; + $apiURL = $trimmedUrl . ltrim(getenv('MAGENTO_CLI_COMMAND_PATH'), '/'); + $executor = new CurlTransport(); - $executor->write($apiURL, [getenv('MAGENTO_CLI_COMMAND_PARAMETER') => $command], CurlInterface::POST, []); + $executor->write( + $apiURL, + [ + getenv('MAGENTO_CLI_COMMAND_PARAMETER') => $command, + 'arguments' => $arguments + ], + CurlInterface::POST, + [] + ); $response = $executor->read(); $executor->close(); return $response; @@ -464,10 +494,11 @@ public function deleteEntityByUrl($url) /** * Conditional click for an area that should be visible * - * @param string $selector - * @param string $dependentSelector - * @param bool $visible + * @param string $selector + * @param string $dependentSelector + * @param boolean $visible * @throws \Exception + * @return void */ public function conditionalClick($selector, $dependentSelector, $visible) { @@ -492,6 +523,7 @@ public function conditionalClick($selector, $dependentSelector, $visible) * Clear the given Text Field or Textarea * * @param string $selector + * @return void */ public function clearField($selector) { @@ -503,7 +535,8 @@ public function clearField($selector) * * @param string $selector * @param string $attribute - * @param $value + * @param string $value + * @return void */ public function assertElementContainsAttribute($selector, $attribute, $value) { @@ -518,6 +551,11 @@ public function assertElementContainsAttribute($selector, $attribute, $value) } } + /** + * Sets current test to the given test, and resets test failure artifacts to null + * @param TestInterface $test + * @return void + */ public function _before(TestInterface $test) { $this->current_test = $test; @@ -529,10 +567,10 @@ public function _before(TestInterface $test) /** * Override for codeception's default dragAndDrop to include offset options. - * @param string $source - * @param string $target - * @param int $xOffset - * @param int $yOffset + * @param string $source + * @param string $target + * @param integer $xOffset + * @param integer $yOffset * @return void */ public function dragAndDrop($source, $target, $xOffset = null, $yOffset = null) @@ -557,12 +595,30 @@ public function dragAndDrop($source, $target, $xOffset = null, $yOffset = null) } } + /** + * Function used to fill sensitive crednetials with user data, data is decrypted immediately prior to fill to avoid + * exposure in console or log. + * + * @param string $field + * @param string $value + * @return void + */ + public function fillSecretField($field, $value) + { + // to protect any secrets from being printed to console the values are executed only at the webdriver level as a + // decrypted value + + $decryptedValue = CredentialStore::getInstance()->decryptSecretValue($value); + $this->fillField($field, $decryptedValue); + } + /** * Override for _failed method in Codeception method. Adds png and html attachments to allure report * following parent execution of test failure processing. * * @param TestInterface $test - * @param \Exception $fail + * @param \Exception $fail + * @return void */ public function _failed(TestInterface $test, $fail) { @@ -586,6 +642,7 @@ public function _failed(TestInterface $test, $fail) /** * Function which saves a screenshot of the current stat of the browser + * @return void */ public function saveScreenshot() { @@ -599,4 +656,17 @@ public function saveScreenshot() $this->_saveScreenshot($this->pngReport = $outputDir . mb_strcut($filename, 0, 245, 'utf-8') . '.fail.png'); $this->_savePageSource($this->htmlReport = $outputDir . mb_strcut($filename, 0, 244, 'utf-8') . '.fail.html'); } + + /** + * Go to a page and wait for ajax requests to finish + * + * @param string $page + * @throws \Exception + * @return void + */ + public function amOnPage($page) + { + parent::amOnPage($page); + $this->waitForPageLoad(); + } } diff --git a/src/Magento/FunctionalTestingFramework/ObjectManager.php b/src/Magento/FunctionalTestingFramework/ObjectManager.php index 51b64dd29..8b0f34537 100644 --- a/src/Magento/FunctionalTestingFramework/ObjectManager.php +++ b/src/Magento/FunctionalTestingFramework/ObjectManager.php @@ -34,9 +34,9 @@ class ObjectManager extends \Magento\FunctionalTestingFramework\ObjectManager\Ob /** * ObjectManager constructor. - * @param ObjectManager\Factory|null $factory + * @param ObjectManager\Factory|null $factory * @param ObjectManager\ConfigInterface|null $config - * @param array $sharedInstances + * @param array $sharedInstances */ public function __construct( \Magento\FunctionalTestingFramework\ObjectManager\Factory $factory = null, @@ -64,7 +64,7 @@ public function getParameters($type, $method) * * @param object $object * @param string $method - * @param array $arguments + * @param array $arguments * @return array */ public function prepareArguments($object, $method, array $arguments = []) @@ -101,7 +101,7 @@ public static function setInstance(ObjectManager $objectManager) /** * Retrieve object manager * - * @return ObjectManager|bool + * @return ObjectManager|boolean * @throws \RuntimeException */ public static function getInstance() diff --git a/src/Magento/FunctionalTestingFramework/ObjectManager/Config.php b/src/Magento/FunctionalTestingFramework/ObjectManager/Config.php index fdc8ca373..3133de1ef 100644 --- a/src/Magento/FunctionalTestingFramework/ObjectManager/Config.php +++ b/src/Magento/FunctionalTestingFramework/ObjectManager/Config.php @@ -27,7 +27,7 @@ class Config extends ObjectManagerConfig * Check whether type is shared * * @param string $type - * @return bool + * @return boolean * @SuppressWarnings(PHPMD.UnusedLocalVariable) */ public function isShared($type) diff --git a/src/Magento/FunctionalTestingFramework/ObjectManager/Config/Config.php b/src/Magento/FunctionalTestingFramework/ObjectManager/Config/Config.php index 502dba0c0..c8e6c8292 100644 --- a/src/Magento/FunctionalTestingFramework/ObjectManager/Config/Config.php +++ b/src/Magento/FunctionalTestingFramework/ObjectManager/Config/Config.php @@ -73,7 +73,7 @@ class Config implements \Magento\FunctionalTestingFramework\ObjectManager\Config /** * Config constructor. - * @param RelationsInterface|null $relations + * @param RelationsInterface|null $relations * @param DefinitionInterface|null $definitions */ public function __construct(RelationsInterface $relations = null, DefinitionInterface $definitions = null) @@ -99,7 +99,7 @@ public function getArguments($type) * Check whether type is shared * * @param string $type - * @return bool + * @return boolean */ public function isShared($type) { diff --git a/src/Magento/FunctionalTestingFramework/ObjectManager/Config/Reader/DomFactory.php b/src/Magento/FunctionalTestingFramework/ObjectManager/Config/Reader/DomFactory.php index 78e977625..27d5518ad 100644 --- a/src/Magento/FunctionalTestingFramework/ObjectManager/Config/Reader/DomFactory.php +++ b/src/Magento/FunctionalTestingFramework/ObjectManager/Config/Reader/DomFactory.php @@ -30,7 +30,7 @@ class DomFactory * Factory constructor * * @param \Magento\FunctionalTestingFramework\ObjectManagerInterface $objectManager - * @param string $instanceName + * @param string $instanceName */ public function __construct( \Magento\FunctionalTestingFramework\ObjectManagerInterface $objectManager, diff --git a/src/Magento/FunctionalTestingFramework/ObjectManager/ConfigInterface.php b/src/Magento/FunctionalTestingFramework/ObjectManager/ConfigInterface.php index bec418fc0..53f3ba95a 100644 --- a/src/Magento/FunctionalTestingFramework/ObjectManager/ConfigInterface.php +++ b/src/Magento/FunctionalTestingFramework/ObjectManager/ConfigInterface.php @@ -22,7 +22,7 @@ public function getArguments($type); * Check whether type is shared * * @param string $type - * @return bool + * @return boolean */ public function isShared($type); diff --git a/src/Magento/FunctionalTestingFramework/ObjectManager/Factory.php b/src/Magento/FunctionalTestingFramework/ObjectManager/Factory.php index b377139e2..9b795d58f 100644 --- a/src/Magento/FunctionalTestingFramework/ObjectManager/Factory.php +++ b/src/Magento/FunctionalTestingFramework/ObjectManager/Factory.php @@ -24,10 +24,10 @@ class Factory extends \Magento\FunctionalTestingFramework\ObjectManager\Factory\ /** * Factory constructor. - * @param ConfigInterface $config + * @param ConfigInterface $config * @param \Magento\FunctionalTestingFramework\ObjectManagerInterface|null $objectManager - * @param DefinitionInterface|null $definitions - * @param array $globalArguments + * @param DefinitionInterface|null $definitions + * @param array $globalArguments */ public function __construct( ConfigInterface $config, @@ -77,7 +77,7 @@ public function getParameters($type, $method) * * @param object $object * @param string $method - * @param array $arguments + * @param array $arguments * @return array */ public function prepareArguments($object, $method, array $arguments = []) @@ -95,8 +95,8 @@ public function prepareArguments($object, $method, array $arguments = []) * Resolve constructor arguments * * @param string $requestedType - * @param array $parameters - * @param array $arguments + * @param array $parameters + * @param array $arguments * @return array * @throws \UnexpectedValueException * @throws \BadMethodCallException @@ -177,7 +177,7 @@ protected function resolveArguments($requestedType, array $parameters, array $ar /** * Parse array argument * - * @param array &$array + * @param array $array * @return void * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.NPathComplexity) @@ -216,7 +216,7 @@ protected function parseArray(&$array) * Create instance with call time arguments * * @param string $requestedType - * @param array $arguments + * @param array $arguments * @return object * @throws \Exception * diff --git a/src/Magento/FunctionalTestingFramework/ObjectManager/Factory/Dynamic/Developer.php b/src/Magento/FunctionalTestingFramework/ObjectManager/Factory/Dynamic/Developer.php index 5e452e55f..dbe6aa497 100644 --- a/src/Magento/FunctionalTestingFramework/ObjectManager/Factory/Dynamic/Developer.php +++ b/src/Magento/FunctionalTestingFramework/ObjectManager/Factory/Dynamic/Developer.php @@ -47,10 +47,10 @@ class Developer implements \Magento\FunctionalTestingFramework\ObjectManager\Fac /** * Developer constructor. - * @param \Magento\FunctionalTestingFramework\ObjectManager\ConfigInterface $config - * @param \Magento\FunctionalTestingFramework\ObjectManagerInterface|null $objectManager + * @param \Magento\FunctionalTestingFramework\ObjectManager\ConfigInterface $config + * @param \Magento\FunctionalTestingFramework\ObjectManagerInterface|null $objectManager * @param \Magento\FunctionalTestingFramework\ObjectManager\DefinitionInterface|null $definitions - * @param array $globalArguments + * @param array $globalArguments */ public function __construct( \Magento\FunctionalTestingFramework\ObjectManager\ConfigInterface $config, @@ -79,8 +79,8 @@ public function setObjectManager(\Magento\FunctionalTestingFramework\ObjectManag * Resolve constructor arguments * * @param string $requestedType - * @param array $parameters - * @param array $arguments + * @param array $parameters + * @param array $arguments * @return array * @throws \UnexpectedValueException * @throws \BadMethodCallException @@ -139,7 +139,7 @@ protected function resolveArguments($requestedType, array $parameters, array $ar /** * Parse array argument * - * @param array &$array + * @param array $array * @return void */ protected function parseArray(&$array) @@ -167,7 +167,7 @@ protected function parseArray(&$array) * Create instance with call time arguments * * @param string $requestedType - * @param array $arguments + * @param array $arguments * @return object * @throws \Exception * diff --git a/src/Magento/FunctionalTestingFramework/ObjectManager/FactoryInterface.php b/src/Magento/FunctionalTestingFramework/ObjectManager/FactoryInterface.php index eca6f5bb1..b1ad87597 100644 --- a/src/Magento/FunctionalTestingFramework/ObjectManager/FactoryInterface.php +++ b/src/Magento/FunctionalTestingFramework/ObjectManager/FactoryInterface.php @@ -14,7 +14,7 @@ interface FactoryInterface * Create instance with call time arguments * * @param string $requestedType - * @param array $arguments + * @param array $arguments * @return object * @throws \LogicException * @throws \BadMethodCallException diff --git a/src/Magento/FunctionalTestingFramework/ObjectManager/ObjectManager.php b/src/Magento/FunctionalTestingFramework/ObjectManager/ObjectManager.php index ff177e87a..57c5cc39c 100644 --- a/src/Magento/FunctionalTestingFramework/ObjectManager/ObjectManager.php +++ b/src/Magento/FunctionalTestingFramework/ObjectManager/ObjectManager.php @@ -13,7 +13,7 @@ class ObjectManager implements \Magento\FunctionalTestingFramework\ObjectManager { /** * Create instance with call time arguments. - * + * * @var \Magento\FunctionalTestingFramework\ObjectManager\FactoryInterface */ protected $factory; @@ -35,8 +35,8 @@ class ObjectManager implements \Magento\FunctionalTestingFramework\ObjectManager /** * ObjectManager constructor. * @param FactoryInterface $factory - * @param ConfigInterface $config - * @param array $sharedInstances + * @param ConfigInterface $config + * @param array $sharedInstances */ public function __construct(FactoryInterface $factory, ConfigInterface $config, array $sharedInstances = []) { @@ -50,7 +50,7 @@ public function __construct(FactoryInterface $factory, ConfigInterface $config, * Create new object instance * * @param string $type - * @param array $arguments + * @param array $arguments * @return object */ public function create($type, array $arguments = []) diff --git a/src/Magento/FunctionalTestingFramework/ObjectManager/Relations/Runtime.php b/src/Magento/FunctionalTestingFramework/ObjectManager/Relations/Runtime.php index 4da8b6de0..192758220 100644 --- a/src/Magento/FunctionalTestingFramework/ObjectManager/Relations/Runtime.php +++ b/src/Magento/FunctionalTestingFramework/ObjectManager/Relations/Runtime.php @@ -37,7 +37,7 @@ public function __construct(\Magento\FunctionalTestingFramework\Code\Reader\Clas * Check whether requested type is available for read * * @param string $type - * @return bool + * @return boolean */ public function has($type) { diff --git a/src/Magento/FunctionalTestingFramework/ObjectManager/RelationsInterface.php b/src/Magento/FunctionalTestingFramework/ObjectManager/RelationsInterface.php index 51245710f..b1ff6b72b 100644 --- a/src/Magento/FunctionalTestingFramework/ObjectManager/RelationsInterface.php +++ b/src/Magento/FunctionalTestingFramework/ObjectManager/RelationsInterface.php @@ -14,7 +14,7 @@ interface RelationsInterface * Check whether requested type is available for read * * @param string $type - * @return bool + * @return boolean */ public function has($type); diff --git a/src/Magento/FunctionalTestingFramework/ObjectManagerInterface.php b/src/Magento/FunctionalTestingFramework/ObjectManagerInterface.php index 4e7c9da1d..53cfc8d8e 100644 --- a/src/Magento/FunctionalTestingFramework/ObjectManagerInterface.php +++ b/src/Magento/FunctionalTestingFramework/ObjectManagerInterface.php @@ -15,7 +15,7 @@ interface ObjectManagerInterface * Create new object instance * * @param string $type - * @param array $arguments + * @param array $arguments * @return object */ public function create($type, array $arguments = []); diff --git a/src/Magento/FunctionalTestingFramework/Page/Config/Dom.php b/src/Magento/FunctionalTestingFramework/Page/Config/Dom.php new file mode 100644 index 000000000..0226a286a --- /dev/null +++ b/src/Magento/FunctionalTestingFramework/Page/Config/Dom.php @@ -0,0 +1,104 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\FunctionalTestingFramework\Page\Config; + +use Magento\FunctionalTestingFramework\Config\MftfApplicationConfig; +use Magento\FunctionalTestingFramework\Exceptions\Collector\ExceptionCollector; +use Magento\FunctionalTestingFramework\Config\Dom\NodeMergingConfig; +use Magento\FunctionalTestingFramework\Config\Dom\NodePathMatcher; +use Magento\FunctionalTestingFramework\Util\ModulePathExtractor; +use Magento\FunctionalTestingFramework\Util\Validation\DuplicateNodeValidationUtil; + +/** + * MFTF page.xml configuration XML DOM utility + * @package Magento\FunctionalTestingFramework\Page\Config + */ +class Dom extends \Magento\FunctionalTestingFramework\Config\MftfDom +{ + const PAGE_META_FILENAME_ATTRIBUTE = "filename"; + + /** + * Module Path extractor + * + * @var ModulePathExtractor + */ + private $modulePathExtractor; + + /** + * NodeValidationUtil + * + * @var DuplicateNodeValidationUtil + */ + private $validationUtil; + + /** + * Page Dom constructor. + * @param string $xml + * @param string $filename + * @param ExceptionCollector $exceptionCollector + * @param array $idAttributes + * @param string $typeAttributeName + * @param string $schemaFile + * @param string $errorFormat + */ + public function __construct( + $xml, + $filename, + $exceptionCollector, + array $idAttributes = [], + $typeAttributeName = null, + $schemaFile = null, + $errorFormat = self::ERROR_FORMAT_DEFAULT + ) { + $this->modulePathExtractor = new ModulePathExtractor(); + $this->validationUtil = new DuplicateNodeValidationUtil('name', $exceptionCollector); + parent::__construct( + $xml, + $filename, + $exceptionCollector, + $idAttributes, + $typeAttributeName, + $schemaFile, + $errorFormat + ); + } + + /** + * Takes a dom element from xml and appends the filename based on location + * + * @param string $xml + * @param string|null $filename + * @return \DOMDocument + */ + public function initDom($xml, $filename = null) + { + $dom = parent::initDom($xml); + + $pagesNode = $dom->getElementsByTagName('pages')->item(0); + $this->validationUtil->validateChildUniqueness($pagesNode, $filename); + $pageNodes = $dom->getElementsByTagName('page'); + $currentModule = + $this->modulePathExtractor->extractModuleName($filename) . + '_' . + $this->modulePathExtractor->getExtensionPath($filename); + foreach ($pageNodes as $pageNode) { + $pageModule = $pageNode->getAttribute("module"); + $pageName = $pageNode->getAttribute("name"); + if ($pageModule !== $currentModule) { + if (MftfApplicationConfig::getConfig()->verboseEnabled()) { + print( + "Page Module does not match path Module. " . + "(Page, Module): ($pageName, $pageModule) - Path Module: $currentModule" . + PHP_EOL + ); + } + } + $pageNode->setAttribute(self::PAGE_META_FILENAME_ATTRIBUTE, $filename); + } + return $dom; + } +} diff --git a/src/Magento/FunctionalTestingFramework/Page/Config/SectionDom.php b/src/Magento/FunctionalTestingFramework/Page/Config/SectionDom.php new file mode 100644 index 000000000..7115adb34 --- /dev/null +++ b/src/Magento/FunctionalTestingFramework/Page/Config/SectionDom.php @@ -0,0 +1,78 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\FunctionalTestingFramework\Page\Config; + +use Magento\FunctionalTestingFramework\Config\MftfApplicationConfig; +use Magento\FunctionalTestingFramework\Exceptions\Collector\ExceptionCollector; +use Magento\FunctionalTestingFramework\Config\Dom\NodeMergingConfig; +use Magento\FunctionalTestingFramework\Config\Dom\NodePathMatcher; +use Magento\FunctionalTestingFramework\Util\ModulePathExtractor; +use Magento\FunctionalTestingFramework\Util\Validation\DuplicateNodeValidationUtil; + +/** + * MFTF section.xml configuration XML DOM utility + * @package Magento\FunctionalTestingFramework\Page\Config + */ +class SectionDom extends \Magento\FunctionalTestingFramework\Config\MftfDom +{ + const SECTION_META_FILENAME_ATTRIBUTE = "filename"; + + /** + * NodeValidationUtil + * @var DuplicateNodeValidationUtil + */ + private $validationUtil; + + /** + * Entity Dom constructor. + * @param string $xml + * @param string $filename + * @param ExceptionCollector $exceptionCollector + * @param array $idAttributes + * @param string $typeAttributeName + * @param string $schemaFile + * @param string $errorFormat + */ + public function __construct( + $xml, + $filename, + $exceptionCollector, + array $idAttributes = [], + $typeAttributeName = null, + $schemaFile = null, + $errorFormat = self::ERROR_FORMAT_DEFAULT + ) { + $this->validationUtil = new DuplicateNodeValidationUtil('name', $exceptionCollector); + parent::__construct( + $xml, + $filename, + $exceptionCollector, + $idAttributes, + $typeAttributeName, + $schemaFile, + $errorFormat + ); + } + + /** + * Takes a dom element from xml and appends the filename based on location + * + * @param string $xml + * @param string|null $filename + * @return \DOMDocument + */ + public function initDom($xml, $filename = null) + { + $dom = parent::initDom($xml); + $sectionNodes = $dom->getElementsByTagName('section'); + foreach ($sectionNodes as $sectionNode) { + $sectionNode->setAttribute(self::SECTION_META_FILENAME_ATTRIBUTE, $filename); + $this->validationUtil->validateChildUniqueness($sectionNode, $filename); + } + return $dom; + } +} diff --git a/src/Magento/FunctionalTestingFramework/Page/Handlers/PageObjectHandler.php b/src/Magento/FunctionalTestingFramework/Page/Handlers/PageObjectHandler.php index 0c47ab7c3..caf2262fa 100644 --- a/src/Magento/FunctionalTestingFramework/Page/Handlers/PageObjectHandler.php +++ b/src/Magento/FunctionalTestingFramework/Page/Handlers/PageObjectHandler.php @@ -38,6 +38,8 @@ class PageObjectHandler implements ObjectHandlerInterface /** * Private constructor + * + * @throws XmlException */ private function __construct() { @@ -74,6 +76,7 @@ private function __construct() * Singleton method to return PageObjectHandler. * * @return PageObjectHandler + * @throws XmlException */ public static function getInstance() { diff --git a/src/Magento/FunctionalTestingFramework/Page/Handlers/SectionObjectHandler.php b/src/Magento/FunctionalTestingFramework/Page/Handlers/SectionObjectHandler.php index a361579e8..459f4a9ef 100644 --- a/src/Magento/FunctionalTestingFramework/Page/Handlers/SectionObjectHandler.php +++ b/src/Magento/FunctionalTestingFramework/Page/Handlers/SectionObjectHandler.php @@ -43,6 +43,7 @@ class SectionObjectHandler implements ObjectHandlerInterface * Constructor * * @constructor + * @throws XmlException */ private function __construct() { @@ -93,6 +94,7 @@ private function __construct() * Initialize and/or return the singleton instance of this class * * @return SectionObjectHandler + * @throws XmlException */ public static function getInstance() { @@ -106,7 +108,7 @@ public static function getInstance() /** * Get a SectionObject by name * - * @param string $name The section name + * @param string $name The section name. * @return SectionObject | null */ public function getObject($name) diff --git a/src/Magento/FunctionalTestingFramework/Page/Objects/ElementObject.php b/src/Magento/FunctionalTestingFramework/Page/Objects/ElementObject.php index 7a18c76dd..6b0626f10 100644 --- a/src/Magento/FunctionalTestingFramework/Page/Objects/ElementObject.php +++ b/src/Magento/FunctionalTestingFramework/Page/Objects/ElementObject.php @@ -58,12 +58,12 @@ class ElementObject /** * ElementObject constructor. - * @param string $name - * @param string $type - * @param string $selector - * @param string $locatorFunction - * @param string $timeout - * @param bool $parameterized + * @param string $name + * @param string $type + * @param string $selector + * @param string $locatorFunction + * @param string $timeout + * @param boolean $parameterized * @throws XmlException */ public function __construct($name, $type, $selector, $locatorFunction, $timeout, $parameterized) @@ -138,7 +138,7 @@ public function getPrioritizedSelector() /** * Returns an integer representing an element's timeout * - * @return int|null + * @return integer|null */ public function getTimeout() { @@ -152,9 +152,8 @@ public function getTimeout() /** * Determines if the element's selector is parameterized. Based on $parameterized property. * - * @return bool + * @return boolean */ - public function isParameterized() { return $this->parameterized; diff --git a/src/Magento/FunctionalTestingFramework/Page/Objects/PageObject.php b/src/Magento/FunctionalTestingFramework/Page/Objects/PageObject.php index 30cdc89ed..3533a13f3 100644 --- a/src/Magento/FunctionalTestingFramework/Page/Objects/PageObject.php +++ b/src/Magento/FunctionalTestingFramework/Page/Objects/PageObject.php @@ -6,7 +6,7 @@ namespace Magento\FunctionalTestingFramework\Page\Objects; - +use Magento\FunctionalTestingFramework\Exceptions\XmlException; use Magento\FunctionalTestingFramework\Page\Handlers\SectionObjectHandler; /** @@ -60,12 +60,12 @@ class PageObject /** * PageObject constructor. - * @param string $name - * @param string $url - * @param string $module - * @param array $sections - * @param bool $parameterized - * @param string $area + * @param string $name + * @param string $url + * @param string $module + * @param array $sections + * @param boolean $parameterized + * @param string $area */ public function __construct($name, $url, $module, $sections, $parameterized, $area) { @@ -143,6 +143,7 @@ public function hasSection($sectionName) * * @param string $sectionName * @return SectionObject | null + * @throws XmlException */ public function getSection($sectionName) { @@ -156,9 +157,8 @@ public function getSection($sectionName) /** * Determines if the page's url is parameterized. Based on $parameterized property. * - * @return bool + * @return boolean */ - public function isParameterized() { return $this->parameterized; diff --git a/src/Magento/FunctionalTestingFramework/Page/Objects/SectionObject.php b/src/Magento/FunctionalTestingFramework/Page/Objects/SectionObject.php index 121d3e6ca..9ac641bca 100644 --- a/src/Magento/FunctionalTestingFramework/Page/Objects/SectionObject.php +++ b/src/Magento/FunctionalTestingFramework/Page/Objects/SectionObject.php @@ -28,7 +28,7 @@ class SectionObject /** * SectionObject constructor. * @param string $name - * @param array $elements + * @param array $elements */ public function __construct($name, $elements) { @@ -59,7 +59,7 @@ public function getElements() /** * Checks to see if this section contains any element by the name of elementName * @param string $elementName - * @return bool + * @return boolean */ public function hasElement($elementName) { diff --git a/src/Magento/FunctionalTestingFramework/Page/etc/PageObject.xsd b/src/Magento/FunctionalTestingFramework/Page/etc/PageObject.xsd index 48688f9ac..c6f399a67 100644 --- a/src/Magento/FunctionalTestingFramework/Page/etc/PageObject.xsd +++ b/src/Magento/FunctionalTestingFramework/Page/etc/PageObject.xsd @@ -65,6 +65,7 @@ <xs:attribute type="xs:boolean" name="parameterized" use="optional"/> <xs:attribute type="pageArea" name="area" use="required"/> <xs:attributeGroup ref="removeAttribute"/> + <xs:attribute type="xs:string" name="filename"/> </xs:complexType> </xs:element> diff --git a/src/Magento/FunctionalTestingFramework/Page/etc/SectionObject.xsd b/src/Magento/FunctionalTestingFramework/Page/etc/SectionObject.xsd index a2457f4e4..44e057369 100644 --- a/src/Magento/FunctionalTestingFramework/Page/etc/SectionObject.xsd +++ b/src/Magento/FunctionalTestingFramework/Page/etc/SectionObject.xsd @@ -45,6 +45,7 @@ </xs:annotation> </xs:attribute> <xs:attributeGroup ref="removeAttribute"/> + <xs:attribute type="xs:string" name="filename"/> </xs:complexType> </xs:element> diff --git a/src/Magento/FunctionalTestingFramework/Suite/Generators/GroupClassGenerator.php b/src/Magento/FunctionalTestingFramework/Suite/Generators/GroupClassGenerator.php index e374d0e6e..2b9662a1a 100644 --- a/src/Magento/FunctionalTestingFramework/Suite/Generators/GroupClassGenerator.php +++ b/src/Magento/FunctionalTestingFramework/Suite/Generators/GroupClassGenerator.php @@ -6,6 +6,7 @@ namespace Magento\FunctionalTestingFramework\Suite\Generators; +use Magento\FunctionalTestingFramework\Exceptions\TestReferenceException; use Magento\FunctionalTestingFramework\Suite\Objects\SuiteObject; use Magento\FunctionalTestingFramework\Test\Objects\ActionObject; use Magento\FunctionalTestingFramework\Test\Objects\TestHookObject; @@ -68,6 +69,7 @@ public function __construct() * * @param SuiteObject $suiteObject * @return string + * @throws TestReferenceException */ public function generateGroupClass($suiteObject) { @@ -84,6 +86,7 @@ public function generateGroupClass($suiteObject) * * @param SuiteObject $suiteObject * @return string; + * @throws TestReferenceException */ private function createClassContent($suiteObject) { @@ -122,6 +125,7 @@ private function extractClassVar($beforeArray, $afterArray) * * @param TestHookObject $hookObj * @return array + * @throws TestReferenceException */ private function buildHookMustacheArray($hookObj) { @@ -169,8 +173,9 @@ private function buildHookMustacheArray($hookObj) * appends the entry to the given array. The result is returned by the function. * * @param ActionObject $action - * @param array $actionEntries + * @param array $actionEntries * @return array + * @throws TestReferenceException */ private function buildWebDriverActionsMustacheArray($action, $actionEntries) { @@ -189,7 +194,7 @@ private function buildWebDriverActionsMustacheArray($action, $actionEntries) * for the generated step. * * @param string $formattedStep - * @param array $actionEntries + * @param array $actionEntries * @param string $actor * @return array */ @@ -212,7 +217,7 @@ private function replaceReservedTesterFunctions($formattedStep, $actionEntries, * Takes an action object of persistence type and formats an array entiry for mustache template interpretation. * * @param ActionObject $action - * @param array $entityArray + * @param array $entityArray * @return array */ private function buildPersistenceMustacheArray($action, $entityArray) diff --git a/src/Magento/FunctionalTestingFramework/Suite/Handlers/SuiteObjectHandler.php b/src/Magento/FunctionalTestingFramework/Suite/Handlers/SuiteObjectHandler.php index 8930de98c..d4b044cf8 100644 --- a/src/Magento/FunctionalTestingFramework/Suite/Handlers/SuiteObjectHandler.php +++ b/src/Magento/FunctionalTestingFramework/Suite/Handlers/SuiteObjectHandler.php @@ -5,6 +5,7 @@ */ namespace Magento\FunctionalTestingFramework\Suite\Handlers; +use Magento\FunctionalTestingFramework\Exceptions\XmlException; use Magento\FunctionalTestingFramework\ObjectManager\ObjectHandlerInterface; use Magento\FunctionalTestingFramework\ObjectManagerFactory; use Magento\FunctionalTestingFramework\Suite\Objects\SuiteObject; @@ -43,6 +44,7 @@ private function __construct() * Function to enforce singleton design pattern * * @return ObjectHandlerInterface + * @throws XmlException */ public static function getInstance() { @@ -103,6 +105,7 @@ public function getAllTestReferences() * * @return void * @SuppressWarnings(PHPMD.UnusedPrivateMethod) + * @throws XmlException */ private function initSuiteData() { diff --git a/src/Magento/FunctionalTestingFramework/Suite/Objects/SuiteObject.php b/src/Magento/FunctionalTestingFramework/Suite/Objects/SuiteObject.php index bf560eac3..d7f6bd18a 100644 --- a/src/Magento/FunctionalTestingFramework/Suite/Objects/SuiteObject.php +++ b/src/Magento/FunctionalTestingFramework/Suite/Objects/SuiteObject.php @@ -43,9 +43,9 @@ class SuiteObject /** * SuiteObject constructor. - * @param string $name - * @param TestObject[] $includeTests - * @param TestObject[] $excludeTests + * @param string $name + * @param TestObject[] $includeTests + * @param TestObject[] $excludeTests * @param TestHookObject[] $hooks */ public function __construct($name, $includeTests, $excludeTests, $hooks) @@ -110,7 +110,7 @@ private function resolveTests($includeTests, $excludeTests) * Convenience method for determining if a Suite will require group file generation. * A group file will only be generated when the user specifies a before/after statement. * - * @return bool + * @return boolean */ public function requiresGroupFile() { diff --git a/src/Magento/FunctionalTestingFramework/Suite/SuiteGenerator.php b/src/Magento/FunctionalTestingFramework/Suite/SuiteGenerator.php index 9c96742b4..7f3efdbfc 100644 --- a/src/Magento/FunctionalTestingFramework/Suite/SuiteGenerator.php +++ b/src/Magento/FunctionalTestingFramework/Suite/SuiteGenerator.php @@ -7,11 +7,13 @@ namespace Magento\FunctionalTestingFramework\Suite; 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\Test\Handlers\TestObjectHandler; use Magento\FunctionalTestingFramework\Util\Filesystem\DirSetupUtil; +use Magento\FunctionalTestingFramework\Util\Logger\LoggingUtil; use Magento\FunctionalTestingFramework\Util\Manifest\BaseTestManifest; use Magento\FunctionalTestingFramework\Util\TestGenerator; use Symfony\Component\Yaml\Yaml; @@ -71,13 +73,11 @@ public static function getInstance() * * @param BaseTestManifest $testManifest * @return void + * @throws \Exception */ public function generateAllSuites($testManifest) { - $suites = array_keys(SuiteObjectHandler::getInstance()->getAllObjects()); - if ($testManifest != null) { - $suites = $testManifest->getSuiteConfig(); - } + $suites = $testManifest->getSuiteConfig(); foreach ($suites as $suiteName => $suiteContent) { $firstElement = array_values($suiteContent)[0]; @@ -94,43 +94,13 @@ public function generateAllSuites($testManifest) } } - /** - * Returns an array of tests contained within suites as keys pointed at the name of their corresponding suite. - * - * @return array - */ - public function getTestsReferencedInSuites() - { - $testsReferencedInSuites = []; - $suites = SuiteObjectHandler::getInstance()->getAllObjects(); - - // see if we have a specific suite configuration. - if (!empty($this->suiteReferences)) { - $suites = array_intersect_key($suites, $this->suiteReferences); - } - - foreach ($suites as $suite) { - /** @var SuiteObject $suite */ - $test_keys = array_keys($suite->getTests()); - - // see if we need to filter which tests we'll be generating. - if (array_key_exists($suite->getName(), $this->suiteReferences)) { - $test_keys = $this->suiteReferences[$suite->getName()] ?? $test_keys; - } - - $testToSuiteName = array_fill_keys($test_keys, [$suite->getName()]); - $testsReferencedInSuites = array_merge_recursive($testsReferencedInSuites, $testToSuiteName); - } - - return $testsReferencedInSuites; - } - /** * Function which takes a suite name and generates corresponding dir, test files, group class, and updates * yml configuration for group run. * * @param string $suiteName * @return void + * @throws \Exception */ public function generateSuite($suiteName) { @@ -144,9 +114,11 @@ public function generateSuite($suiteName) * run so that any pre/post conditions can be duplicated. * * @param string $suiteName - * @param array $tests + * @param array $tests * @param string $originalSuiteName * @return void + * @throws TestReferenceException + * @throws XmlException */ private function generateSuiteFromTest($suiteName, $tests = [], $originalSuiteName = null) { @@ -169,7 +141,10 @@ private function generateSuiteFromTest($suiteName, $tests = [], $originalSuiteNa $groupNamespace = $this->generateGroupFile($suiteName, $relevantTests, $originalSuiteName); $this->appendEntriesToConfig($suiteName, $fullPath, $groupNamespace); - print "Suite ${suiteName} generated to ${relativePath}.\n"; + LoggingUtil::getInstance()->getLogger(SuiteGenerator::class)->info( + "suite generated", + ['suite' => $suiteName, 'relative_path' => $relativePath] + ); } /** @@ -177,26 +152,22 @@ private function generateSuiteFromTest($suiteName, $tests = [], $originalSuiteNa * prevent possible invalid test configurations from executing. * * @param string $suiteName - * @param array $testsReferenced + * @param array $testsReferenced * @param string $originalSuiteName * @return void * @throws TestReferenceException + * @throws XmlException */ private function validateTestsReferencedInSuite($suiteName, $testsReferenced, $originalSuiteName) { $suiteRef = $originalSuiteName ?? $suiteName; $possibleTestRef = SuiteObjectHandler::getInstance()->getObject($suiteRef)->getTests(); - $invalidTestRef = null; - $errorMsg = "Cannot reference tests not declared as part of {$suiteRef}:\n "; + $errorMsg = "Cannot reference tests whcih are not declared as part of suite."; - array_walk($testsReferenced, function ($value) use (&$invalidTestRef, $possibleTestRef, &$errorMsg) { - if (!array_key_exists($value, $possibleTestRef)) { - $invalidTestRef.= "\t{$value}\n"; - } - }); + $invalidTestRef = array_diff($testsReferenced, array_keys($possibleTestRef)); - if ($invalidTestRef != null) { - throw new TestReferenceException($errorMsg . $invalidTestRef); + if (!empty($invalidTestRef)) { + throw new TestReferenceException($errorMsg, ['suite' => $suiteRef, 'test' => $invalidTestRef]); } } @@ -205,8 +176,9 @@ private function validateTestsReferencedInSuite($suiteName, $testsReferenced, $o * and generates applicable suites. * * @param string $suiteName - * @param array $suiteContent + * @param array $suiteContent * @return void + * @throws \Exception */ private function generateSplitSuiteFromTest($suiteName, $suiteContent) { @@ -220,9 +192,11 @@ private function generateSplitSuiteFromTest($suiteName, $suiteContent) * and generates a group file which captures suite level preconditions. * * @param string $suiteName - * @param array $tests + * @param array $tests * @param string $originalSuiteName * @return null|string + * @throws XmlException + * @throws TestReferenceException */ private function generateGroupFile($suiteName, $tests, $originalSuiteName) { @@ -266,7 +240,8 @@ private function generateGroupFile($suiteName, $tests, $originalSuiteName) */ private function appendEntriesToConfig($suiteName, $suitePath, $groupNamespace) { - $relativeSuitePath = substr($suitePath, strlen(dirname(dirname(TESTS_BP))) + 1); + $relativeSuitePath = substr($suitePath, strlen(TESTS_BP)); + $relativeSuitePath = ltrim($relativeSuitePath, DIRECTORY_SEPARATOR); $ymlArray = self::getYamlFileContents(); if (!array_key_exists(self::YAML_GROUPS_TAG, $ymlArray)) { @@ -298,13 +273,11 @@ private static function clearPreviousSessionConfigEntries() 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)) { @@ -321,8 +294,9 @@ private static function clearPreviousSessionConfigEntries() * generator which is then called to create all the test files for the suite. * * @param string $path - * @param array $tests + * @param array $tests * @return void + * @throws TestReferenceException */ private function generateRelevantGroupTests($path, $tests) { @@ -368,6 +342,6 @@ private static function getYamlFileContents() */ private static function getYamlConfigFilePath() { - return dirname(dirname(TESTS_BP)) . DIRECTORY_SEPARATOR; + return TESTS_BP . DIRECTORY_SEPARATOR; } } diff --git a/src/Magento/FunctionalTestingFramework/Suite/Util/SuiteObjectExtractor.php b/src/Magento/FunctionalTestingFramework/Suite/Util/SuiteObjectExtractor.php index 4db1abfc7..642671246 100644 --- a/src/Magento/FunctionalTestingFramework/Suite/Util/SuiteObjectExtractor.php +++ b/src/Magento/FunctionalTestingFramework/Suite/Util/SuiteObjectExtractor.php @@ -42,6 +42,7 @@ public function __construct() * @throws XmlException * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.NPathComplexity) + * @throws \Exception */ public function parseSuiteDataIntoObjects($parsedSuiteData) { @@ -88,11 +89,6 @@ public function parseSuiteDataIntoObjects($parsedSuiteData) $includeTests = $this->extractTestObjectsFromSuiteRef($groupTestsToInclude); $excludeTests = $this->extractTestObjectsFromSuiteRef($groupTestsToExclude); - // add all test if include tests is completely empty - if (empty($includeTests)) { - $includeTests = TestObjectHandler::getInstance()->getAllObjects(); - } - // parse any object hooks if (array_key_exists(TestObjectExtractor::TEST_BEFORE_HOOK, $parsedSuite)) { $suiteHooks[TestObjectExtractor::TEST_BEFORE_HOOK] = $testHookObjectExtractor->extractHook( @@ -108,12 +104,31 @@ public function parseSuiteDataIntoObjects($parsedSuiteData) $parsedSuite[TestObjectExtractor::TEST_AFTER_HOOK] ); } + if (count($suiteHooks) == 1) { throw new XmlException(sprintf( "Suites that contain hooks must contain both a 'before' and an 'after' hook. Suite: \"%s\"", $parsedSuite[self::NAME] )); } + // check if suite hooks are empty/not included and there are no included tests/groups/modules + $noHooks = count($suiteHooks) == 0 || + ( + empty($suiteHooks['before']->getActions()) && + empty($suiteHooks['after']->getActions()) + ); + // if suite body is empty throw error + if ($noHooks && empty($includeTests) && empty($excludeTests)) { + throw new XmlException(sprintf( + "Suites must not be empty. Suite: \"%s\"", + $parsedSuite[self::NAME] + )); + } + + // add all test if include tests is completely empty + if (empty($includeTests)) { + $includeTests = TestObjectHandler::getInstance()->getAllObjects(); + } // create the new suite object $suiteObjects[$parsedSuite[self::NAME]] = new SuiteObject( @@ -133,6 +148,7 @@ public function parseSuiteDataIntoObjects($parsedSuiteData) * * @param array $suiteReferences * @return array + * @throws \Exception */ private function extractTestObjectsFromSuiteRef($suiteReferences) { @@ -169,6 +185,7 @@ private function extractTestObjectsFromSuiteRef($suiteReferences) * @param string $moduleName * @param string $moduleFilePath * @return array + * @throws \Exception */ private function extractModuleAndFiles($moduleName, $moduleFilePath) { @@ -183,7 +200,7 @@ private function extractModuleAndFiles($moduleName, $moduleFilePath) * Takes a filepath (and optionally a module name) and resolves to a test object. * * @param string $filename - * @param null $moduleName + * @param null $moduleName * @return TestObject[] * @throws Exception */ @@ -219,6 +236,7 @@ private function resolveFilePathTestNames($filename, $moduleName = null) * * @param string $moduleName * @return array + * @throws \Exception */ private function resolveModulePathTestNames($moduleName) { diff --git a/src/Magento/FunctionalTestingFramework/Test/Config/ActionGroupDom.php b/src/Magento/FunctionalTestingFramework/Test/Config/ActionGroupDom.php index 794007dde..ab8bc66f5 100644 --- a/src/Magento/FunctionalTestingFramework/Test/Config/ActionGroupDom.php +++ b/src/Magento/FunctionalTestingFramework/Test/Config/ActionGroupDom.php @@ -6,7 +6,12 @@ namespace Magento\FunctionalTestingFramework\Test\Config; use Magento\FunctionalTestingFramework\Exceptions\Collector\ExceptionCollector; +use Magento\FunctionalTestingFramework\Util\Validation\DuplicateNodeValidationUtil; +/** + * MFTF actionGroup.xml configuration XML DOM utility + * @package Magento\FunctionalTestingFramework\Test\Config + */ class ActionGroupDom extends Dom { const ACTION_GROUP_FILE_NAME_ENDING = "ActionGroup.xml"; @@ -15,12 +20,11 @@ class ActionGroupDom extends Dom * Takes a dom element from xml and appends the filename based on location while also validating the action group * step key. * - * @param string $xml + * @param string $xml * @param string|null $filename - * @param ExceptionCollector $exceptionCollector * @return \DOMDocument */ - public function initDom($xml, $filename = null, $exceptionCollector = null) + public function initDom($xml, $filename = null) { $dom = parent::initDom($xml); @@ -29,10 +33,41 @@ public function initDom($xml, $filename = null, $exceptionCollector = null) foreach ($actionGroupNodes as $actionGroupNode) { /** @var \DOMElement $actionGroupNode */ $actionGroupNode->setAttribute(self::TEST_META_FILENAME_ATTRIBUTE, $filename); - $this->validateDomStepKeys($actionGroupNode, $filename, 'Action Group', $exceptionCollector); + $this->validationUtil->validateChildUniqueness( + $actionGroupNode, + $filename + ); + $beforeNode = $actionGroupNode->getElementsByTagName('before')->item(0); + $afterNode = $actionGroupNode->getElementsByTagName('after')->item(0); + if (isset($beforeNode)) { + $this->validationUtil->validateChildUniqueness( + $beforeNode, + $filename + ); + } + if (isset($afterNode)) { + $this->validationUtil->validateChildUniqueness( + $afterNode, + $filename + ); + } + if ($actionGroupNode->getAttribute(self::TEST_MERGE_POINTER_AFTER) !== "") { + $this->appendMergePointerToActions( + $actionGroupNode, + self::TEST_MERGE_POINTER_AFTER, + $actionGroupNode->getAttribute(self::TEST_MERGE_POINTER_AFTER), + $filename + ); + } elseif ($actionGroupNode->getAttribute(self::TEST_MERGE_POINTER_BEFORE) !== "") { + $this->appendMergePointerToActions( + $actionGroupNode, + self::TEST_MERGE_POINTER_BEFORE, + $actionGroupNode->getAttribute(self::TEST_MERGE_POINTER_BEFORE), + $filename + ); + } } } - return $dom; } } diff --git a/src/Magento/FunctionalTestingFramework/Test/Config/Converter/Dom/Flat.php b/src/Magento/FunctionalTestingFramework/Test/Config/Converter/Dom/Flat.php index 254cdcc69..dd72264b9 100644 --- a/src/Magento/FunctionalTestingFramework/Test/Config/Converter/Dom/Flat.php +++ b/src/Magento/FunctionalTestingFramework/Test/Config/Converter/Dom/Flat.php @@ -16,6 +16,8 @@ class Flat implements ConverterInterface { const REMOVE_ACTION = 'remove'; const REMOVE_KEY_ATTRIBUTE = 'keyForRemoval'; + const EXTENDS_ATTRIBUTE = 'extends'; + const TEST_HOOKS = ['before', 'after']; /** * Array node configuration. @@ -63,7 +65,7 @@ public function convert($source) * ) * * @param \DOMNode $source - * @param string $basePath + * @param string $basePath * @return string|array * @throws \UnexpectedValueException * @SuppressWarnings(PHPMD.CyclomaticComplexity) @@ -87,8 +89,19 @@ public function convertXml(\DOMNode $source, $basePath = '') } if ($nodeName == self::REMOVE_ACTION) { - unset($value[$node->getAttribute(self::REMOVE_KEY_ATTRIBUTE)]); - continue; + // Check to see if the test extends for this remove action + $parentHookExtends = in_array($node->parentNode->nodeName, self::TEST_HOOKS) + && !empty($node->parentNode->parentNode->getAttribute('extends')); + $test_extends = $parentHookExtends || !empty($node->parentNode->getAttribute('extends')); + + // If the test does extend, don't remove the remove action and set the stepkey + if ($test_extends) { + $keyForRemoval = $node->getAttribute('keyForRemoval'); + $node->setAttribute('stepKey', $keyForRemoval); + } else { + unset($value[$node->getAttribute(self::REMOVE_KEY_ATTRIBUTE)]); + continue; + } } $nodeData = $this->convertXml($node, $nodePath); diff --git a/src/Magento/FunctionalTestingFramework/Test/Config/Dom.php b/src/Magento/FunctionalTestingFramework/Test/Config/Dom.php index 24710243f..ae301b5b1 100644 --- a/src/Magento/FunctionalTestingFramework/Test/Config/Dom.php +++ b/src/Magento/FunctionalTestingFramework/Test/Config/Dom.php @@ -10,23 +10,43 @@ use Magento\FunctionalTestingFramework\Exceptions\XmlException; use Magento\FunctionalTestingFramework\Config\Dom\NodeMergingConfig; use Magento\FunctionalTestingFramework\Config\Dom\NodePathMatcher; +use Magento\FunctionalTestingFramework\Test\Objects\ActionObject; +use Magento\FunctionalTestingFramework\Util\Validation\DuplicateNodeValidationUtil; -class Dom extends \Magento\FunctionalTestingFramework\Config\Dom +/** + * MFTF test.xml configuration XML DOM utility + * @package Magento\FunctionalTestingFramework\Test\Config + */ +class Dom extends \Magento\FunctionalTestingFramework\Config\MftfDom { const TEST_FILE_NAME_ENDING = 'Test'; const TEST_META_FILENAME_ATTRIBUTE = 'filename'; const TEST_META_NAME_ATTRIBUTE = 'name'; const TEST_HOOK_NAMES = ["after", "before"]; + const TEST_MERGE_POINTER_BEFORE = "insertBefore"; + const TEST_MERGE_POINTER_AFTER = "insertAfter"; + + /** + * NodeValidationUtil + * @var DuplicateNodeValidationUtil + */ + protected $validationUtil; + + /** + * ExceptionCollector + * @var ExceptionCollector + */ + private $exceptionCollector; /** - * TestDom constructor. - * @param string $xml - * @param string $filename + * Metadata Dom constructor. + * @param string $xml + * @param string $filename * @param ExceptionCollector $exceptionCollector - * @param array $idAttributes - * @param string $typeAttributeName - * @param string $schemaFile - * @param string $errorFormat + * @param array $idAttributes + * @param string $typeAttributeName + * @param string $schemaFile + * @param string $errorFormat */ public function __construct( $xml, @@ -37,23 +57,27 @@ public function __construct( $schemaFile = null, $errorFormat = self::ERROR_FORMAT_DEFAULT ) { - $this->schemaFile = $schemaFile; - $this->nodeMergingConfig = new NodeMergingConfig(new NodePathMatcher(), $idAttributes); - $this->typeAttributeName = $typeAttributeName; - $this->errorFormat = $errorFormat; - $this->dom = $this->initDom($xml, $filename, $exceptionCollector); - $this->rootNamespace = $this->dom->lookupNamespaceUri($this->dom->namespaceURI); + $this->validationUtil = new DuplicateNodeValidationUtil('stepKey', $exceptionCollector); + $this->exceptionCollector = $exceptionCollector; + parent::__construct( + $xml, + $filename, + $exceptionCollector, + $idAttributes, + $typeAttributeName, + $schemaFile, + $errorFormat + ); } /** * Takes a dom element from xml and appends the filename based on location * - * @param string $xml + * @param string $xml * @param string|null $filename - * @param ExceptionCollector $exceptionCollector * @return \DOMDocument */ - public function initDom($xml, $filename = null, $exceptionCollector = null) + public function initDom($xml, $filename = null) { $dom = parent::initDom($xml); @@ -62,7 +86,41 @@ public function initDom($xml, $filename = null, $exceptionCollector = null) foreach ($testNodes as $testNode) { /** @var \DOMElement $testNode */ $testNode->setAttribute(self::TEST_META_FILENAME_ATTRIBUTE, $filename); - $this->validateDomStepKeys($testNode, $filename, 'Test', $exceptionCollector); + if ($testNode->getAttribute(self::TEST_MERGE_POINTER_AFTER) !== "") { + $this->appendMergePointerToActions( + $testNode, + self::TEST_MERGE_POINTER_AFTER, + $testNode->getAttribute(self::TEST_MERGE_POINTER_AFTER), + $filename + ); + } elseif ($testNode->getAttribute(self::TEST_MERGE_POINTER_BEFORE) !== "") { + $this->appendMergePointerToActions( + $testNode, + self::TEST_MERGE_POINTER_BEFORE, + $testNode->getAttribute(self::TEST_MERGE_POINTER_BEFORE), + $filename + ); + } + + $this->validationUtil->validateChildUniqueness( + $testNode, + $filename + ); + $beforeNode = $testNode->getElementsByTagName('before')->item(0); + $afterNode = $testNode->getElementsByTagName('after')->item(0); + + if (isset($beforeNode)) { + $this->validationUtil->validateChildUniqueness( + $beforeNode, + $filename + ); + } + if (isset($afterNode)) { + $this->validationUtil->validateChildUniqueness( + $afterNode, + $filename + ); + } } } @@ -70,61 +128,36 @@ public function initDom($xml, $filename = null, $exceptionCollector = null) } /** - * Redirects any merges into the init method for appending xml filename - * - * @param string $xml - * @param string|null $filename - * @param ExceptionCollector $exceptionCollector - * @return void - */ - public function merge($xml, $filename = null, $exceptionCollector = null) - { - $dom = $this->initDom($xml, $filename, $exceptionCollector); - $this->mergeNode($dom->documentElement, ''); - } - - /** - * Parses an individual DOM structure for repeated stepKey attributes + * Parses DOM Structure's actions and appends a before/after attribute along with the parent's stepkey reference. * * @param \DOMElement $testNode - * @param string $filename - * @param string $type - * @param ExceptionCollector $exceptionCollector + * @param string $insertType + * @param string $insertKey + * @param string $filename * @return void - * @throws XmlException */ - protected function validateDomStepKeys($testNode, $filename, $type, $exceptionCollector) + protected function appendMergePointerToActions($testNode, $insertType, $insertKey, $filename) { $childNodes = $testNode->childNodes; - - $keyValues = []; + $previousStepKey = $insertKey; + $actionInsertType = ActionObject::MERGE_ACTION_ORDER_AFTER; + if ($insertType == self::TEST_MERGE_POINTER_BEFORE) { + $actionInsertType = ActionObject::MERGE_ACTION_ORDER_BEFORE; + } for ($i = 0; $i < $childNodes->length; $i++) { $currentNode = $childNodes->item($i); - - if (!is_a($currentNode, \DOMElement::class)) { + if (!is_a($currentNode, \DOMElement::class) || !$currentNode->hasAttribute('stepKey')) { continue; } - - if (in_array($currentNode->nodeName, self::TEST_HOOK_NAMES)) { - $this->validateDomStepKeys($currentNode, $filename, $type, $exceptionCollector); + if ($currentNode->hasAttribute($insertType) && $testNode->hasAttribute($insertType)) { + $errorMsg = "Actions cannot have merge pointers if contained in tests that has a merge pointer."; + $errorMsg .= "\n\tstepKey: {$currentNode->getAttribute('stepKey')}\tin file: {$filename}"; + $this->exceptionCollector->addError($filename, $errorMsg); } - - if ($currentNode->hasAttribute('stepKey')) { - $keyValues[] = $currentNode->getAttribute('stepKey'); - } - } - - $withoutDuplicates = array_unique($keyValues); - $duplicates = array_diff_assoc($keyValues, $withoutDuplicates); - - if (count($duplicates) > 0) { - $stepKeyError = ""; - foreach ($duplicates as $duplicateKey => $duplicateValue) { - $stepKeyError .= "\tstepKey: {$duplicateValue} is used more than once.\n"; - } - - $errorMsg = "{$type}s cannot use stepKey more than once.\t\n{$stepKeyError}\tin file: {$filename}"; - $exceptionCollector->addError($filename, $errorMsg); + $currentNode->setAttribute($actionInsertType, $previousStepKey); + $previousStepKey = $currentNode->getAttribute('stepKey'); + // All actions after the first need to insert AFTER. + $actionInsertType = ActionObject::MERGE_ACTION_ORDER_AFTER; } } } diff --git a/src/Magento/FunctionalTestingFramework/Test/Handlers/ActionGroupObjectHandler.php b/src/Magento/FunctionalTestingFramework/Test/Handlers/ActionGroupObjectHandler.php index 6cb31770f..994467bd3 100644 --- a/src/Magento/FunctionalTestingFramework/Test/Handlers/ActionGroupObjectHandler.php +++ b/src/Magento/FunctionalTestingFramework/Test/Handlers/ActionGroupObjectHandler.php @@ -11,6 +11,7 @@ use Magento\FunctionalTestingFramework\Test\Objects\TestObject; use Magento\FunctionalTestingFramework\Test\Parsers\ActionGroupDataParser; use Magento\FunctionalTestingFramework\Test\Util\ActionGroupObjectExtractor; +use Magento\FunctionalTestingFramework\Test\Util\ObjectExtensionUtil; /** * Class ActionGroupObjectHandler @@ -35,6 +36,13 @@ class ActionGroupObjectHandler implements ObjectHandlerInterface */ private $actionGroups = []; + /** + * Instance of ObjectExtensionUtil class + * + * @var ObjectExtensionUtil + */ + private $extendUtil; + /** * Singleton getter for instance of ActionGroupObjectHandler * @@ -55,7 +63,7 @@ public static function getInstance() */ private function __construct() { - // private constructor + $this->extendUtil = new ObjectExtensionUtil(); } /** @@ -66,8 +74,9 @@ private function __construct() */ public function getObject($actionGroupName) { - if (array_key_exists($actionGroupName, $this->getAllObjects())) { - return $this->getAllObjects()[$actionGroupName]; + if (array_key_exists($actionGroupName, $this->actionGroups)) { + $actionGroupObject = $this->actionGroups[$actionGroupName]; + return $this->extendActionGroup($actionGroupObject); } return null; @@ -80,6 +89,9 @@ public function getObject($actionGroupName) */ public function getAllObjects() { + foreach ($this->actionGroups as $actionGroupName => $actionGroup) { + $this->actionGroups[$actionGroupName] = $this->extendActionGroup($actionGroup); + } return $this->actionGroups; } @@ -106,4 +118,18 @@ private function initActionGroups() $actionGroupObjectExtractor->extractActionGroup($actionGroupData); } } + + /** + * This method checks if the action group is extended and creates a new action group object accordingly + * + * @param ActionGroupObject $actionGroupObject + * @return ActionGroupObject + */ + private function extendActionGroup($actionGroupObject) + { + if ($actionGroupObject->getParentName() !== null) { + return $this->extendUtil->extendActionGroup($actionGroupObject); + } + return $actionGroupObject; + } } diff --git a/src/Magento/FunctionalTestingFramework/Test/Handlers/TestObjectHandler.php b/src/Magento/FunctionalTestingFramework/Test/Handlers/TestObjectHandler.php index b301c7aaa..8e97b27d8 100644 --- a/src/Magento/FunctionalTestingFramework/Test/Handlers/TestObjectHandler.php +++ b/src/Magento/FunctionalTestingFramework/Test/Handlers/TestObjectHandler.php @@ -5,11 +5,16 @@ */ namespace Magento\FunctionalTestingFramework\Test\Handlers; +use Magento\FunctionalTestingFramework\Exceptions\Collector\ExceptionCollector; +use Magento\FunctionalTestingFramework\Exceptions\TestReferenceException; +use Magento\FunctionalTestingFramework\Exceptions\XmlException; use Magento\FunctionalTestingFramework\ObjectManager\ObjectHandlerInterface; use Magento\FunctionalTestingFramework\ObjectManagerFactory; use Magento\FunctionalTestingFramework\Test\Objects\TestObject; use Magento\FunctionalTestingFramework\Test\Parsers\TestDataParser; +use Magento\FunctionalTestingFramework\Test\Util\ObjectExtensionUtil; use Magento\FunctionalTestingFramework\Test\Util\TestObjectExtractor; +use Magento\FunctionalTestingFramework\Test\Util\AnnotationExtractor; /** * Class TestObjectHandler @@ -32,10 +37,18 @@ class TestObjectHandler implements ObjectHandlerInterface */ private $tests = []; + /** + * Instance of ObjectExtensionUtil class + * + * @var ObjectExtensionUtil + */ + private $extendUtil; + /** * Singleton method to return TestObjectHandler. * * @return TestObjectHandler + * @throws XmlException */ public static function getInstance() { @@ -52,7 +65,7 @@ public static function getInstance() */ private function __construct() { - // private constructor + $this->extendUtil = new ObjectExtensionUtil(); } /** @@ -60,15 +73,16 @@ private function __construct() * * @param string $testName * @return TestObject + * @throws TestReferenceException */ public function getObject($testName) { if (!array_key_exists($testName, $this->tests)) { - trigger_error("Test ${testName} not defined in xml.", E_USER_ERROR); - return null; + throw new TestReferenceException("Test ${testName} not defined in xml."); } + $testObject = $this->tests[$testName]; - return $this->tests[$testName]; + return $this->extendTest($testObject); } /** @@ -78,6 +92,9 @@ public function getObject($testName) */ public function getAllObjects() { + foreach ($this->tests as $testName => $test) { + $this->tests[$testName] = $this->extendTest($test); + } return $this->tests; } @@ -106,6 +123,7 @@ public function getTestsByGroup($groupName) * * @return void * @SuppressWarnings(PHPMD.UnusedPrivateMethod) + * @throws XmlException */ private function initTestData() { @@ -119,12 +137,33 @@ private function initTestData() return; } + $exceptionCollector = new ExceptionCollector(); foreach ($parsedTestArray[TestObjectHandler::XML_ROOT] as $testName => $testData) { if (!is_array($testData)) { continue; } + try { + $this->tests[$testName] = $testObjectExtractor->extractTestData($testData); + } catch (XmlException $exception) { + $exceptionCollector->addError(self::class, $exception->getMessage()); + } + } + $exceptionCollector->throwException(); + + $testObjectExtractor->getAnnotationExtractor()->validateStoryTitleUniqueness(); + } - $this->tests[$testName] = $testObjectExtractor->extractTestData($testData); + /** + * This method checks if the test is extended and creates a new test object accordingly + * + * @param TestObject $testObject + * @return TestObject + */ + private function extendTest($testObject) + { + if ($testObject->getParentName() !== null) { + return $this->extendUtil->extendTest($testObject); } + return $testObject; } } diff --git a/src/Magento/FunctionalTestingFramework/Test/Objects/ActionGroupObject.php b/src/Magento/FunctionalTestingFramework/Test/Objects/ActionGroupObject.php index fdef1107e..659cc0a74 100644 --- a/src/Magento/FunctionalTestingFramework/Test/Objects/ActionGroupObject.php +++ b/src/Magento/FunctionalTestingFramework/Test/Objects/ActionGroupObject.php @@ -7,8 +7,10 @@ namespace Magento\FunctionalTestingFramework\Test\Objects; use Magento\FunctionalTestingFramework\Exceptions\TestReferenceException; +use Magento\FunctionalTestingFramework\Test\Handlers\ActionGroupObjectHandler; use Magento\FunctionalTestingFramework\Test\Util\ActionGroupObjectExtractor; use Magento\FunctionalTestingFramework\Test\Util\ActionMergeUtil; +use Magento\FunctionalTestingFramework\Test\Util\ObjectExtension; /** * Class ActionGroupObject @@ -45,14 +47,22 @@ class ActionGroupObject */ private $arguments; + /** + * String of parent Action Group + * + * @var string + */ + private $parentActionGroup; + /** * ActionGroupObject constructor. * - * @param string $name + * @param string $name * @param ArgumentObject[] $arguments - * @param array $actions + * @param array $actions + * @param string $parentActionGroup */ - public function __construct($name, $arguments, $actions) + public function __construct($name, $arguments, $actions, $parentActionGroup) { $this->varAttributes = array_merge( ActionObject::SELECTOR_ENABLED_ATTRIBUTES, @@ -62,12 +72,13 @@ public function __construct($name, $arguments, $actions) $this->name = $name; $this->arguments = $arguments; $this->parsedActions = $actions; + $this->parentActionGroup = $parentActionGroup; } /** * Gets the ordered steps including merged waits * - * @param array $arguments + * @param array $arguments * @param string $actionReferenceKey * @return array * @throws TestReferenceException @@ -85,6 +96,7 @@ public function getSteps($arguments, $actionReferenceKey) * Iterates through given $arguments and overrides ActionGroup's argument values, if any are found. * @param array $arguments * @return ArgumentObject[] + * @throws TestReferenceException */ private function resolveArguments($arguments) { @@ -117,33 +129,45 @@ private function resolveArguments($arguments) * Function which takes a set of arguments to be appended to an action objects fields returns resulting * action objects with proper argument.field references. * - * @param array $arguments + * @param array $arguments * @param string $actionReferenceKey * @return array */ private function getResolvedActionsWithArgs($arguments, $actionReferenceKey) { $resolvedActions = []; + $replacementStepKeys = []; foreach ($this->parsedActions as $action) { + $replacementStepKeys[$action->getStepKey()] = $action->getStepKey() . ucfirst($actionReferenceKey); $varAttributes = array_intersect($this->varAttributes, array_keys($action->getCustomActionAttributes())); + + // replace createDataKey attributes inside the action group + $resolvedActionAttributes = $this->replaceCreateDataKeys($action, $replacementStepKeys); + $newActionAttributes = []; if (!empty($varAttributes)) { $newActionAttributes = $this->resolveAttributesWithArguments( $arguments, - $action->getCustomActionAttributes() + $resolvedActionAttributes ); } + // translate 0/1 back to before/after + $orderOffset = ActionObject::MERGE_ACTION_ORDER_BEFORE; + if ($action->getOrderOffset() === 1) { + $orderOffset = ActionObject::MERGE_ACTION_ORDER_AFTER; + } + // we append the action reference key to any linked action and the action's merge key as the user might // use this action group multiple times in the same test. $resolvedActions[$action->getStepKey() . ucfirst($actionReferenceKey)] = new ActionObject( $action->getStepKey() . ucfirst($actionReferenceKey), $action->getType(), - array_replace_recursive($action->getCustomActionAttributes(), $newActionAttributes), + array_replace_recursive($resolvedActionAttributes, $newActionAttributes), $action->getLinkedAction() == null ? null : $action->getLinkedAction() . ucfirst($actionReferenceKey), - $action->getOrderOffset(), + $orderOffset, [self::ACTION_GROUP_ORIGIN_NAME => $this->name, self::ACTION_GROUP_ORIGIN_TEST_REF => $actionReferenceKey] ); @@ -167,7 +191,6 @@ private function resolveAttributesWithArguments($arguments, $attributes) $newActionAttributes = []; foreach ($attributes as $attributeKey => $attributeValue) { - if (is_array($attributeValue)) { // attributes with child elements are parsed as an array, need make recursive call to resolve children $newActionAttributes[$attributeKey] = $this->resolveAttributesWithArguments( @@ -193,16 +216,15 @@ private function resolveAttributesWithArguments($arguments, $attributes) ); } return $newActionAttributes; - } /** * Function that takes an array of replacement arguments, and matches them with args in an actionGroup's attribute. * Determines if the replacement arguments are persisted data, and replaces them accordingly. * - * @param array $arguments + * @param array $arguments * @param string $attributeValue - * @param array $matches + * @param array $matches * @return string */ private function replaceAttributeArguments($arguments, $attributeValue, $matches) @@ -233,10 +255,10 @@ private function replaceAttributeArguments($arguments, $attributeValue, $matches /** * Replace attribute arguments in variable. * - * @param string $variable - * @param array $arguments - * @param string $attributeValue - * @param bool $isInnerArgument + * @param string $variable + * @param array $arguments + * @param string $attributeValue + * @param boolean $isInnerArgument * @return string */ private function replaceAttributeArgumentInVariable( @@ -289,9 +311,9 @@ private function replaceAttributeArgumentInVariable( /** * Replaces any arguments that were declared as simpleData="true". * Takes in isInnerArgument to determine what kind of replacement to expect: {{data}} vs section.element(data) - * @param string $argumentValue - * @param string $variableName - * @param string $attributeValue + * @param string $argumentValue + * @param string $variableName + * @param string $attributeValue * @param boolean $isInnerArgument * @return string */ @@ -306,10 +328,10 @@ private function replaceSimpleArgument($argumentValue, $variableName, $attribute /** * Replaces args with replacements given, behavior is specific to persisted arguments. - * @param string $replacement - * @param string $attributeValue - * @param string $fullVariable - * @param string $variable + * @param string $replacement + * @param string $attributeValue + * @param string $fullVariable + * @param string $variable * @param boolean $isParameter * @return string */ @@ -331,8 +353,12 @@ private function replacePersistedArgument($replacement, $attributeValue, $fullVa $fullReplacement = str_replace($variable, trim($replacement, '$'), trim($fullVariable, "'")); $newAttributeValue = str_replace($fullVariable, $scope . $fullReplacement . $scope, $newAttributeValue); } else { - $newAttributeValue = str_replace('{{', $scope, str_replace('}}', $scope, $newAttributeValue)); - $newAttributeValue = str_replace($variable, trim($replacement, '$'), $newAttributeValue); + $fullReplacement = str_replace($variable, trim($replacement, '$'), $fullVariable); + $newAttributeValue = str_replace( + '{{' . $fullVariable . '}}', + $scope . $fullReplacement . $scope, + $newAttributeValue + ); } return $newAttributeValue; @@ -351,10 +377,50 @@ public function extractStepKeys() return $originalKeys; } + /** + * Getter for the Action Group Name + * + * @return string + */ + public function getName() + { + return $this->name; + } + + /** + * Getter for the Parent Action Group Name + * + * @return string + */ + public function getParentName() + { + return $this->parentActionGroup; + } + + /** + * Getter for the Action Group Actions + * + * @return ActionObject[] + */ + public function getActions() + { + return $this->parsedActions; + } + + /** + * Getter for the Action Group Arguments + * + * @return array + */ + public function getArguments() + { + return $this->arguments; + } + /** * Searches through ActionGroupObject's arguments and returns first argument wi * @param string $name - * @param array $argumentList + * @param array $argumentList * @return ArgumentObject|null */ private function findArgumentByName($name, $argumentList) @@ -370,4 +436,27 @@ function ($e) use ($name) { } return null; } + + /** + * Replaces references to step keys used earlier in an action group + * + * @param ActionObject $action + * @param array $replacementStepKeys + * @return ActionObject[] + */ + private function replaceCreateDataKeys($action, $replacementStepKeys) + { + $resolvedActionAttributes = []; + + foreach ($action->getCustomActionAttributes() as $actionAttribute => $actionAttributeDetails) { + if (is_array($actionAttributeDetails) && array_key_exists('createDataKey', $actionAttributeDetails)) { + $actionAttributeDetails['createDataKey'] = + $replacementStepKeys[$actionAttributeDetails['createDataKey']] ?? + $actionAttributeDetails['createDataKey']; + } + $resolvedActionAttributes[$actionAttribute] = $actionAttributeDetails; + } + + return $resolvedActionAttributes; + } } diff --git a/src/Magento/FunctionalTestingFramework/Test/Objects/ActionObject.php b/src/Magento/FunctionalTestingFramework/Test/Objects/ActionObject.php index a0436bc19..f229ccf8a 100644 --- a/src/Magento/FunctionalTestingFramework/Test/Objects/ActionObject.php +++ b/src/Magento/FunctionalTestingFramework/Test/Objects/ActionObject.php @@ -15,6 +15,7 @@ use Magento\FunctionalTestingFramework\Page\Handlers\PageObjectHandler; use Magento\FunctionalTestingFramework\Page\Handlers\SectionObjectHandler; use Magento\FunctionalTestingFramework\Exceptions\TestReferenceException; +use Magento\FunctionalTestingFramework\Util\Logger\LoggingUtil; /** * Class ActionObject @@ -22,6 +23,12 @@ class ActionObject { const __ENV = "_ENV"; + const __CREDS = "_CREDS"; + const RUNTIME_REFERENCES = [ + self::__ENV, + self::__CREDS + ]; + const DATA_ENABLED_ATTRIBUTES = [ "userInput", "parameterArray", @@ -30,7 +37,11 @@ class ActionObject "x", "y", "expectedResult", - "actualResult" + "actualResult", + "command", + "regex", + "date", + "format" ]; const SELECTOR_ENABLED_ATTRIBUTES = [ 'selector', @@ -39,7 +50,8 @@ class ActionObject "selector2", "function", 'filterSelector', - 'optionSelector' + 'optionSelector', + "command" ]; const OLD_ASSERTION_ATTRIBUTES = ["expected", "expectedType", "actual", "actualType"]; const ASSERTION_ATTRIBUTES = ["expectedResult" => "expected", "actualResult" => "actual"]; @@ -53,7 +65,7 @@ class ActionObject const ACTION_ATTRIBUTE_URL = 'url'; const ACTION_ATTRIBUTE_SELECTOR = 'selector'; const ACTION_ATTRIBUTE_VARIABLE_REGEX_PARAMETER = '/\(.+\)/'; - const ACTION_ATTRIBUTE_VARIABLE_REGEX_PATTERN = '/({{[\w]+\.[\w\[\]]+}})|({{[\w]+\.[\w]+\(.+\)}})/'; + const ACTION_ATTRIBUTE_VARIABLE_REGEX_PATTERN = '/({{[\w]+\.[\w\[\]]+}})|({{[\w]+\.[\w]+\((?(?!}}).)+\)}})/'; /** * The unique identifier for the action @@ -114,12 +126,12 @@ class ActionObject /** * ActionObject constructor. * - * @param string $stepKey - * @param string $type - * @param array $actionAttributes + * @param string $stepKey + * @param string $type + * @param array $actionAttributes * @param string|null $linkedAction - * @param string $order - * @param array $actionOrigin + * @param string $order + * @param array $actionOrigin */ public function __construct( $stepKey, @@ -198,7 +210,7 @@ public function getLinkedAction() /** * This function returns the int property orderOffset, describing before or after for a merge. * - * @return int + * @return integer */ public function getOrderOffset() { @@ -209,7 +221,7 @@ public function getOrderOffset() * This function returns the int property timeout, this can be set as a result of the use of a section element * requiring a wait. * - * @return int + * @return integer */ public function getTimeout() { @@ -219,7 +231,7 @@ public function getTimeout() /** * Set the timeout value. * - * @param int $timeout + * @param integer $timeout * @return void */ public function setTimeout($timeout) @@ -234,6 +246,8 @@ public function setTimeout($timeout) * userInput * * @return void + * @throws TestReferenceException + * @throws XmlException */ public function resolveReferences() { @@ -254,6 +268,7 @@ public function resolveReferences() * Warns user if they are using old Assertion syntax. * * @return void + * @throws TestReferenceException */ public function trimAssertionAttributes() { @@ -265,12 +280,13 @@ public function trimAssertionAttributes() */ $oldAttributes = array_intersect($actionAttributeKeys, ActionObject::OLD_ASSERTION_ATTRIBUTES); if (!empty($oldAttributes)) { - // @codingStandardsIgnoreStart $appConfig = MftfApplicationConfig::getConfig(); if ($appConfig->getPhase() == MftfApplicationConfig::GENERATION_PHASE && $appConfig->verboseEnabled()) { - echo("WARNING: Use of one line Assertion actions will be deprecated in MFTF 3.0.0, please use nested syntax (Action: {$this->type} StepKey: {$this->stepKey})" . PHP_EOL); + LoggingUtil::getInstance()->getLogger(ActionObject::class)->deprecation( + "use of one line Assertion actions will be deprecated in MFTF 3.0.0, please use nested syntax", + ["action" => $this->type, "stepKey" => $this->stepKey] + ); } - // @codingStandardsIgnoreEnd return; } @@ -314,9 +330,10 @@ private function validateAssertionSchema($attributes) if (!in_array($this->type, $singleChildTypes)) { if (!in_array('expectedResult', $attributes) || !in_array('actualResult', $attributes)) { - // @codingStandardsIgnoreStart - throw new TestReferenceException("{$this->type} must have both an expectedResult and actualResult defined (stepKey: {$this->stepKey})"); - // @codingStandardsIgnoreEnd + throw new TestReferenceException( + "{$this->type} must have both an expectedResult & actualResult defined (stepKey: {$this->stepKey})", + ["action" => $this->type, "stepKey" => $this->stepKey] + ); } } } @@ -327,6 +344,8 @@ private function validateAssertionSchema($attributes) * e.g. {{SomeSectionName.ElementName}} becomes #login-button * * @return void + * @throws XmlException + * @throws \Exception */ private function resolveSelectorReferenceAndTimeout() { @@ -353,6 +372,8 @@ private function resolveSelectorReferenceAndTimeout() * e.g. {{SomePageName}} becomes http://localhost:76543/some/url * * @return void + * @throws TestReferenceException + * @throws XmlException */ private function resolveUrlReference() { @@ -365,6 +386,14 @@ private function resolveUrlReference() $replacement = $this->findAndReplaceReferences(PageObjectHandler::getInstance(), $url); if ($replacement) { $this->resolvedCustomAttributes[ActionObject::ACTION_ATTRIBUTE_URL] = $replacement; + $allPages = PageObjectHandler::getInstance()->getAllObjects(); + if ($replacement === $url && array_key_exists(trim($url, "{}"), $allPages) + ) { + LoggingUtil::getInstance()->getLogger(ActionObject::class)->warning( + "page url attribute not found and is required", + ["action" => $this->type, "url" => $url, "stepKey" => $this->stepKey] + ); + } } } @@ -374,6 +403,8 @@ private function resolveUrlReference() * e.g. {{CustomerEntityFoo.FirstName}} becomes Jerry * * @return void + * @throws TestReferenceException + * @throws \Exception */ private function resolveDataInputReferences() { @@ -451,8 +482,9 @@ private function stripAndReturnParameters($reference) * Return a string based on a reference to a page, section, or data field (e.g. {{foo.ref}} resolves to 'data') * * @param ObjectHandlerInterface $objectHandler - * @param string $inputString + * @param string $inputString * @return string | null + * @throws TestReferenceException * @throws \Exception * * @SuppressWarnings(PHPMD.CyclomaticComplexity) @@ -473,8 +505,8 @@ private function findAndReplaceReferences($objectHandler, $inputString) $obj = $objectHandler->getObject($objName); - // Leave {{_ENV.VARIABLE}} references to be replaced in TestGenerator with getenv("VARIABLE") - if ($objName === ActionObject::__ENV) { + // Leave runtime references to be replaced in TestGenerator with getter function accessing "VARIABLE" + if (in_array($objName, ActionObject::RUNTIME_REFERENCES)) { continue; } @@ -489,7 +521,7 @@ private function findAndReplaceReferences($objectHandler, $inputString) } elseif (get_class($obj) == SectionObject::class) { list(,$objField) = $this->stripAndSplitReference($match); if ($obj->getElement($objField) == null) { - throw new TestReferenceException("Could not resolve entity reference " . $inputString); + throw new TestReferenceException("Could not resolve entity reference", ["input" => $inputString]); } $parameterized = $obj->getElement($objField)->isParameterized(); $replacement = $obj->getElement($objField)->getPrioritizedSelector(); @@ -506,7 +538,7 @@ private function findAndReplaceReferences($objectHandler, $inputString) if (get_class($objectHandler) != DataObjectHandler::class) { return $this->findAndReplaceReferences(DataObjectHandler::getInstance(), $outputString); } else { - throw new TestReferenceException("Could not resolve entity reference " . $inputString); + throw new TestReferenceException("Could not resolve entity reference", ["input" => $inputString]); } } @@ -529,12 +561,14 @@ private function validateMutuallyExclusiveAttributes(array $attributes) if (count($matches) > 1) { throw new TestReferenceException( "Actions of type '{$this->getType()}' must only contain one attribute of types '" - . implode("', '", $attributes) . "'" + . implode("', '", $attributes) . "'", + ["type" => $this->getType(), "attributes" => $attributes] ); } elseif (count($matches) == 0) { throw new TestReferenceException( "Actions of type '{$this->getType()}' must contain at least one attribute of types '" - . implode("', '", $attributes) . "'" + . implode("', '", $attributes) . "'", + ["type" => $this->getType(), "attributes" => $attributes] ); } } @@ -551,7 +585,8 @@ private function validateUrlAreaAgainstActionType($obj) if ($obj->getArea() == 'external' && in_array($this->getType(), self::EXTERNAL_URL_AREA_INVALID_ACTIONS)) { throw new TestReferenceException( - "Page of type 'external' is not compatible with action type '{$this->getType()}'" + "Page of type 'external' is not compatible with action type '{$this->getType()}'", + ["type" => $this->getType()] ); } } @@ -581,10 +616,11 @@ private function resolveEntityDataObjectReference($obj, $match) /** * Resolves $replacement parameterization with given conditional. * @param boolean $isParameterized - * @param string $replacement - * @param string $match - * @param object $object + * @param string $replacement + * @param string $match + * @param object $object * @return string + * @throws \Exception */ private function resolveParameterization($isParameterized, $replacement, $match, $object) { @@ -605,7 +641,7 @@ private function resolveParameterization($isParameterized, $replacement, $match, * Parameter list given is also resolved, attempting to match {{data.field}} references. * * @param string $reference - * @param array $parameters + * @param array $parameters * @return string * @throws \Exception */ @@ -623,12 +659,14 @@ private function matchParameterReferences($reference, $parameters) } throw new TestReferenceException( "Parameter Resolution Failed: Not enough parameters given for reference " . - $reference . ". Parameters Given: " . $parametersGiven + $reference . ". Parameters Given: " . $parametersGiven, + ["reference" => $reference, "parametersGiven" => $parametersGiven] ); } elseif (count($varMatches[0]) < count($parameters)) { throw new TestReferenceException( "Parameter Resolution Failed: Too many parameters given for reference " . - $reference . ". Parameters Given: " . implode(", ", $parameters) + $reference . ". Parameters Given: " . implode(", ", $parameters), + ["reference" => $reference, "parametersGiven" => $parameters] ); } diff --git a/src/Magento/FunctionalTestingFramework/Test/Objects/TestHookObject.php b/src/Magento/FunctionalTestingFramework/Test/Objects/TestHookObject.php index 2c51b84e1..379082632 100644 --- a/src/Magento/FunctionalTestingFramework/Test/Objects/TestHookObject.php +++ b/src/Magento/FunctionalTestingFramework/Test/Objects/TestHookObject.php @@ -36,9 +36,8 @@ class TestHookObject private $actions = []; /** - * Array of Hook-defined data. + * Array of Hook-defined data. Deprecated because no usage of property exist. Will be removed next major release. * @var array|null - * @deprecated because no usage of property exist. Will be removed next major release. */ private $customData = []; @@ -46,7 +45,7 @@ class TestHookObject * TestHookObject constructor. * @param string $type * @param string $parentName - * @param array $actions + * @param array $actions */ public function __construct($type, $parentName, $actions) { @@ -65,6 +64,16 @@ public function getType() return $this->type; } + /** + * Getter for hook parent name + * + * @return string + */ + public function getParentName() + { + return $this->parentName; + } + /** * Returns an array of action objects to be executed within the hook. * @@ -76,6 +85,16 @@ public function getActions() return $mergeUtil->resolveActionSteps($this->actions); } + /** + * Returns an array of unresolved actions + * + * @return array + */ + public function getUnresolvedActions() + { + return $this->actions; + } + /** * Returns an array of customData to be interperpreted by the generator. * @return array|null diff --git a/src/Magento/FunctionalTestingFramework/Test/Objects/TestObject.php b/src/Magento/FunctionalTestingFramework/Test/Objects/TestObject.php index 8d8700868..71f5ec36b 100644 --- a/src/Magento/FunctionalTestingFramework/Test/Objects/TestObject.php +++ b/src/Magento/FunctionalTestingFramework/Test/Objects/TestObject.php @@ -9,12 +9,26 @@ use Magento\FunctionalTestingFramework\Test\Handlers\ActionGroupObjectHandler; use Magento\FunctionalTestingFramework\Test\Util\ActionMergeUtil; use Magento\FunctionalTestingFramework\Test\Util\ActionObjectExtractor; +use Magento\FunctionalTestingFramework\Test\Util\TestHookObjectExtractor; +use Magento\FunctionalTestingFramework\Test\Util\TestObjectExtractor; /** * Class TestObject */ class TestObject { + const WAIT_TIME_ATTRIBUTE = 'time'; + + const TEST_ACTION_WEIGHT = [ + 'waitForPageLoad' => 1500, + 'amOnPage' => 1000, + 'waitForLoadingMaskToDisappear' => 500, + 'wait' => self::WAIT_TIME_ATTRIBUTE, + 'comment' => 5, + 'assertCount' => 5, + 'closeAdminNotification' => 10 + ]; + /** * Name of the test * @@ -46,26 +60,35 @@ class TestObject /** * String of filename of test * - * @var String + * @var string */ private $filename; + /** + * String of parent test + * + * @var string + */ + private $parentTest; + /** * TestObject constructor. * - * @param string $name - * @param ActionObject[] $parsedSteps - * @param array $annotations + * @param string $name + * @param ActionObject[] $parsedSteps + * @param array $annotations * @param TestHookObject[] $hooks - * @param String $filename + * @param string $filename + * @param string $parentTest */ - public function __construct($name, $parsedSteps, $annotations, $hooks, $filename = null) + public function __construct($name, $parsedSteps, $annotations, $hooks, $filename = null, $parentTest = null) { $this->name = $name; $this->parsedSteps = $parsedSteps; $this->annotations = $annotations; $this->hooks = $hooks; $this->filename = $filename; + $this->parentTest = $parentTest; } /** @@ -88,6 +111,16 @@ public function getFilename() return $this->filename; } + /** + * Getter for the Parent Test Name + * + * @return string + */ + public function getParentName() + { + return $this->parentTest; + } + /** * Getter for the skip_test boolean * @@ -95,7 +128,10 @@ public function getFilename() */ public function isSkipped() { - if (array_key_exists('group', $this->annotations) && (in_array("skip", $this->annotations['group']))) { + // TODO deprecation|deprecate MFTF 3.0.0 remove elseif when group skip is no longer allowed + if (array_key_exists('skip', $this->annotations)) { + return true; + } elseif (array_key_exists('group', $this->annotations) && (in_array("skip", $this->annotations['group']))) { return true; } return false; @@ -128,7 +164,7 @@ public function getAnnotations() /** * Returns hooks. * - * @return array + * @return TestHookObject[] */ public function getHooks() { @@ -140,28 +176,55 @@ public function getHooks() } /** - * Returns the number of a test actions contained within a single test (including before/after actions). + * Returns the estimated duration of a single test (including before/after actions). * - * @return int + * @return integer */ - public function getTestActionCount() + public function getEstimatedDuration() { // a skipped action results in a single skip being appended to the beginning of the test and no execution if ($this->isSkipped()) { return 1; } - $hookActions = 0; - if (array_key_exists('before', $this->hooks)) { - $hookActions += count($this->hooks['before']->getActions()); + $hookTime = 0; + foreach ([TestObjectExtractor::TEST_BEFORE_HOOK, TestObjectExtractor::TEST_AFTER_HOOK] as $hookName) { + if (array_key_exists($hookName, $this->hooks)) { + $hookTime += $this->calculateWeightedActionTimes($this->hooks[$hookName]->getActions()); + } } - if (array_key_exists('after', $this->hooks)) { - $hookActions += count($this->hooks['after']->getActions()); + $testTime = $this->calculateWeightedActionTimes($this->getOrderedActions()); + + return $hookTime + $testTime; + } + + /** + * Function which takes a set of actions and estimates time for completion based on action type. + * + * @param ActionObject[] $actions + * @return integer + */ + private function calculateWeightedActionTimes($actions) + { + $actionTime = 0; + // search for any actions of special type + foreach ($actions as $action) { + /** @var ActionObject $action */ + if (array_key_exists($action->getType(), self::TEST_ACTION_WEIGHT)) { + $weight = self::TEST_ACTION_WEIGHT[$action->getType()]; + if ($weight === self::WAIT_TIME_ATTRIBUTE) { + $weight = intval($action->getCustomActionAttributes()[$weight]) * 1000; + } + + $actionTime += $weight; + continue; + } + + $actionTime += 50; } - $testActions = count($this->getOrderedActions()); - return $hookActions + $testActions; + return $actionTime; } /** @@ -200,6 +263,16 @@ public function getOrderedActions() return $mergeUtil->resolveActionSteps($this->parsedSteps); } + /** + * This method returns currently parsed steps + * + * @return array + */ + public function getUnresolvedSteps() + { + return $this->parsedSteps; + } + /** * Get information about actions and steps in test. * diff --git a/src/Magento/FunctionalTestingFramework/Test/Util/ActionGroupObjectExtractor.php b/src/Magento/FunctionalTestingFramework/Test/Util/ActionGroupObjectExtractor.php index aeef21760..04f739af6 100644 --- a/src/Magento/FunctionalTestingFramework/Test/Util/ActionGroupObjectExtractor.php +++ b/src/Magento/FunctionalTestingFramework/Test/Util/ActionGroupObjectExtractor.php @@ -7,6 +7,7 @@ namespace Magento\FunctionalTestingFramework\Test\Util; use Magento\FunctionalTestingFramework\Data\Argument\Interpreter\Argument; +use Magento\FunctionalTestingFramework\Exceptions\XmlException; use Magento\FunctionalTestingFramework\Test\Objects\ActionGroupObject; use Magento\FunctionalTestingFramework\Test\Objects\ArgumentObject; @@ -18,6 +19,9 @@ class ActionGroupObjectExtractor extends BaseObjectExtractor const DEFAULT_VALUE = 'defaultValue'; const ACTION_GROUP_ARGUMENTS = 'arguments'; const FILENAME = 'filename'; + const ACTION_GROUP_INSERT_BEFORE = "insertBefore"; + const ACTION_GROUP_INSERT_AFTER = "insertAfter"; + const EXTENDS_ACTION_GROUP = 'extends'; /** * Action Object Extractor for converting actions into objects @@ -39,21 +43,30 @@ public function __construct() * * @param array $actionGroupData * @return ActionGroupObject + * @throws XmlException */ public function extractActionGroup($actionGroupData) { $arguments = []; + $actionGroupReference = $actionGroupData[self::EXTENDS_ACTION_GROUP] ?? null; $actionData = $this->stripDescriptorTags( $actionGroupData, self::NODE_NAME, self::ACTION_GROUP_ARGUMENTS, self::NAME, - self::FILENAME + self::FILENAME, + self::ACTION_GROUP_INSERT_BEFORE, + self::ACTION_GROUP_INSERT_AFTER, + self::EXTENDS_ACTION_GROUP ); // TODO filename is now available to the ActionGroupObject, integrate this into debug and error statements - $actions = $this->actionObjectExtractor->extractActions($actionData); + try { + $actions = $this->actionObjectExtractor->extractActions($actionData); + } catch (\Exception $error) { + throw new XmlException($error->getMessage() . " in Action Group " . $actionGroupData[self::FILENAME]); + } if (array_key_exists(self::ACTION_GROUP_ARGUMENTS, $actionGroupData)) { $arguments = $this->extractArguments($actionGroupData[self::ACTION_GROUP_ARGUMENTS]); @@ -62,7 +75,8 @@ public function extractActionGroup($actionGroupData) return new ActionGroupObject( $actionGroupData[self::NAME], $arguments, - $actions + $actions, + $actionGroupReference ); } diff --git a/src/Magento/FunctionalTestingFramework/Test/Util/ActionMergeUtil.php b/src/Magento/FunctionalTestingFramework/Test/Util/ActionMergeUtil.php index 8af5e3af0..fb562c626 100644 --- a/src/Magento/FunctionalTestingFramework/Test/Util/ActionMergeUtil.php +++ b/src/Magento/FunctionalTestingFramework/Test/Util/ActionMergeUtil.php @@ -18,7 +18,7 @@ class ActionMergeUtil { const STEP_MISSING_ERROR_MSG = "Merge Error - Step could not be found in either TestXML or DeltaXML. - \t%s = '%s'\tTestStep='%s'\tLinkedStep'%s'"; + \t%s: '%s'\tTestStep: '%s'\tLinkedStep: '%s'"; const WAIT_ATTR = 'timeout'; const WAIT_ACTION_NAME = 'waitForPageLoad'; @@ -68,9 +68,11 @@ public function __construct($contextName, $contextType) /** * Method to execute merge of steps and insert wait steps. * - * @param array $parsedSteps - * @param bool $skipActionGroupResolution + * @param array $parsedSteps + * @param boolean $skipActionGroupResolution * @return array + * @throws TestReferenceException + * @throws XmlException */ public function resolveActionSteps($parsedSteps, $skipActionGroupResolution = false) { @@ -81,7 +83,66 @@ public function resolveActionSteps($parsedSteps, $skipActionGroupResolution = fa return $this->orderedSteps; } - return $this->resolveActionGroups($this->orderedSteps); + $resolvedActions = $this->resolveActionGroups($this->orderedSteps); + return $this->resolveSecretFieldAccess($resolvedActions); + } + + /** + * Takes an array of actions and resolves any references to secret fields. The function then validates whether the + * refernece is valid and replaces the function name accordingly to hide arguments at runtime. + * + * @param ActionObject[] $resolvedActions + * @return ActionObject[] + * @throws TestReferenceException + */ + private function resolveSecretFieldAccess($resolvedActions) + { + $actions = []; + foreach ($resolvedActions as $resolvedAction) { + $action = $resolvedAction; + $hasSecretRef = $this->actionAttributeContainsSecretRef($resolvedAction->getCustomActionAttributes()); + + if ($resolvedAction->getType() !== 'fillField' && $hasSecretRef) { + throw new TestReferenceException("You cannot reference secret data outside of fill field actions"); + } + + if ($resolvedAction->getType() === 'fillField' && $hasSecretRef) { + $action = new ActionObject( + $action->getStepKey(), + 'fillSecretField', + $action->getCustomActionAttributes(), + $action->getLinkedAction(), + $action->getActionOrigin() + ); + } + + $actions[$action->getStepKey()] = $action; + } + + return $actions; + } + + /** + * Returns a boolean based on whether or not the action attributes contain a reference to a secret field. + * + * @param array $actionAttributes + * @return boolean + */ + private function actionAttributeContainsSecretRef($actionAttributes) + { + foreach ($actionAttributes as $actionAttribute) { + if (is_array($actionAttribute)) { + return $this->actionAttributeContainsSecretRef($actionAttribute); + } + + preg_match_all("/{{_CREDS\.([\w]+)}}/", $actionAttribute, $matches); + + if (!empty($matches[0])) { + return true; + } + } + + return false; } /** @@ -119,6 +180,7 @@ private function resolveActionGroups($mergedSteps) * * @param array $parsedSteps * @return void + * @throws XmlException */ private function mergeActions($parsedSteps) { diff --git a/src/Magento/FunctionalTestingFramework/Test/Util/ActionObjectExtractor.php b/src/Magento/FunctionalTestingFramework/Test/Util/ActionObjectExtractor.php index a3ce58a5e..5cd7b7ac5 100644 --- a/src/Magento/FunctionalTestingFramework/Test/Util/ActionObjectExtractor.php +++ b/src/Magento/FunctionalTestingFramework/Test/Util/ActionObjectExtractor.php @@ -11,6 +11,7 @@ use Magento\FunctionalTestingFramework\Exceptions\TestReferenceException; use Magento\FunctionalTestingFramework\Exceptions\XmlException; use Magento\FunctionalTestingFramework\Test\Objects\ActionObject; +use Magento\FunctionalTestingFramework\Util\Logger\LoggingUtil; /** * Class ActionObjectExtractor @@ -26,6 +27,7 @@ class ActionObjectExtractor extends BaseObjectExtractor const ACTION_GROUP_ARG_VALUE = 'value'; const BEFORE_AFTER_ERROR_MSG = "Merge Error - Steps cannot have both before and after attributes.\tStepKey='%s'"; const STEP_KEY_BLACKLIST_ERROR_MSG = "StepKeys cannot contain non alphanumeric characters.\tStepKey='%s'"; + const STEP_KEY_EMPTY_ERROR_MSG = "StepKeys cannot be empty.\tAction='%s'"; const DATA_PERSISTENCE_CUSTOM_FIELD = 'field'; const DATA_PERSISTENCE_CUSTOM_FIELD_KEY = 'key'; const ACTION_OBJECT_PERSISTENCE_FIELDS = 'customFields'; @@ -44,10 +46,11 @@ public function __construct() * This method takes an array of test actions read in from a TestHook or Test. The actions are stripped of * irrelevant tags and returned as an array of ActionObjects. * - * @param array $testActions + * @param array $testActions * @param string $testName * @return array * @throws XmlException + * @throws TestReferenceException */ public function extractActions($testActions, $testName = null) { @@ -56,6 +59,11 @@ public function extractActions($testActions, $testName = null) foreach ($testActions as $actionName => $actionData) { $stepKey = $actionData[self::TEST_STEP_MERGE_KEY]; + $actionType = $actionData[self::NODE_NAME]; + + if (empty($stepKey)) { + throw new XmlException(sprintf(self::STEP_KEY_EMPTY_ERROR_MSG, $actionData['nodeName'])); + } if (preg_match('/[^a-zA-Z0-9_]/', $stepKey)) { throw new XmlException(sprintf(self::STEP_KEY_BLACKLIST_ERROR_MSG, $actionName)); @@ -72,10 +80,7 @@ public function extractActions($testActions, $testName = null) $actionAttributes['parameterArray'] = $actionData['array']['value']; } - if ($actionData[self::NODE_NAME] === self::ACTION_GROUP_TAG) { - $actionAttributes = $this->processActionGroupArgs($actionAttributes); - } - + $actionAttributes = $this->processActionGroupArgs($actionType, $actionAttributes); $linkedAction = $this->processLinkedActions($actionName, $actionData); $actions = $this->extractFieldActions($actionData, $actions); $actionAttributes = $this->extractFieldReferences($actionData, $actionAttributes); @@ -89,9 +94,9 @@ public function extractActions($testActions, $testName = null) $returnVariable = $actionData[ActionGroupObjectHandler::TEST_ACTION_RETURN_VARIABLE]; }*/ - $actions[] = new ActionObject( + $actions[$stepKey] = new ActionObject( $stepKey, - $actionData[self::NODE_NAME], + $actionType, $actionAttributes, $linkedAction['stepKey'], $linkedAction['order'] @@ -108,7 +113,7 @@ public function extractActions($testActions, $testName = null) * Returns an array with keys corresponding to the linked action's stepKey and order. * * @param string $actionName - * @param array $actionData + * @param array $actionData * @return array * @throws XmlException */ @@ -135,11 +140,16 @@ private function processLinkedActions($actionName, $actionData) * Takes the action group reference and parses out arguments as an array that can be passed to override defaults * defined in the action group xml. * - * @param array $actionAttributeData + * @param string $actionType + * @param array $actionAttributeData * @return array */ - private function processActionGroupArgs($actionAttributeData) + private function processActionGroupArgs($actionType, $actionAttributeData) { + if ($actionType !== self::ACTION_GROUP_TAG) { + return $actionAttributeData; + } + $actionAttributeArgData = []; foreach ($actionAttributeData as $attributeDataKey => $attributeDataValues) { if ($attributeDataKey == self::ACTION_GROUP_REF) { @@ -162,6 +172,8 @@ private function processActionGroupArgs($actionAttributeData) * @param array $actionData * @param array $actions * @return array + * @throws XmlException + * @throws TestReferenceException */ private function extractFieldActions($actionData, $actions) { @@ -223,7 +235,7 @@ private function extractFieldReferences($actionData, $actionAttributes) /** * Function which validates stepKey references within mergeable actions * - * @param array $stepKeyRefs + * @param array $stepKeyRefs * @param string $testName * @return void * @throws TestReferenceException @@ -240,12 +252,10 @@ private function auditMergeSteps($stepKeyRefs, $testName) }, ARRAY_FILTER_USE_BOTH); if (!empty($invalidStepRef)) { - $errorMsg = "Invalid ordering configuration in test {$testName} with step key(s):\n"; - array_walk($invalidStepRef, function ($value, $key) use (&$errorMsg) { - $errorMsg.="\t{$key}\n"; - }); - - throw new TestReferenceException($errorMsg); + throw new TestReferenceException( + "Invalid ordering configuration in test", + ['test' => $testName, 'stepKey' => array_keys($invalidStepRef)] + ); } // check for ambiguous references to step keys (multiple refs across test merges). @@ -253,16 +263,11 @@ private function auditMergeSteps($stepKeyRefs, $testName) return count($value) > 1; }); - $multipleActionsError = ""; foreach ($atRiskStepRef as $stepKey => $stepRefs) { - $multipleActionsError.= "multiple actions referencing step key {$stepKey} in test {$testName}:\n"; - array_walk($stepRefs, function ($value) use (&$multipleActionsError) { - $multipleActionsError.= "\t{$value}\n"; - }); - } - - if (MftfApplicationConfig::getConfig()->verboseEnabled()) { - print $multipleActionsError; + LoggingUtil::getInstance()->getLogger(ActionObjectExtractor::class)->warn( + 'multiple actions referencing step key', + ['test' => $testName, 'stepKey' => $stepKey, 'ref' => $stepRefs] + ); } } } diff --git a/src/Magento/FunctionalTestingFramework/Test/Util/AnnotationExtractor.php b/src/Magento/FunctionalTestingFramework/Test/Util/AnnotationExtractor.php index 6e98c78f3..805a010c2 100644 --- a/src/Magento/FunctionalTestingFramework/Test/Util/AnnotationExtractor.php +++ b/src/Magento/FunctionalTestingFramework/Test/Util/AnnotationExtractor.php @@ -6,11 +6,22 @@ namespace Magento\FunctionalTestingFramework\Test\Util; +use Magento\FunctionalTestingFramework\Config\MftfApplicationConfig; +use Magento\FunctionalTestingFramework\Exceptions\XmlException; +use Magento\FunctionalTestingFramework\Util\Logger\LoggingUtil; + /** * Class AnnotationExtractor */ class AnnotationExtractor extends BaseObjectExtractor { + /** + * Mappings of all Test => title mappings, indexed by Story + * e.g. $storyToTitleMappings['storyAnnotation'] = ['testName' => 'titleAnnotation'] + * @var array + */ + private $storyToTitleMappings = []; + const ANNOTATION_VALUE = 'value'; const MAGENTO_TO_ALLURE_SEVERITY_MAP = [ "BLOCKER" => "BLOCKER", @@ -19,6 +30,12 @@ class AnnotationExtractor extends BaseObjectExtractor "AVERAGE" => "MINOR", "MINOR" => "TRIVIAL" ]; + const REQUIRED_ANNOTATIONS = [ + "stories", + "title", + "description", + "severity" + ]; /** * AnnotationExtractor constructor. @@ -32,12 +49,13 @@ public function __construct() * This method trims away irrelevant tags and returns annotations used in the array passed. The annotations * can be found in both Tests and their child element tests. * - * @param array $testAnnotations + * @param array $testAnnotations + * @param string $filename * @return array + * @throws XmlException */ - public function extractAnnotations($testAnnotations) + public function extractAnnotations($testAnnotations, $filename) { - $annotationObjects = []; $annotations = $this->stripDescriptorTags($testAnnotations, self::NODE_NAME); @@ -53,15 +71,114 @@ public function extractAnnotations($testAnnotations) continue; } + if ($annotationKey == "skip") { + $annotationData = $annotationData['issueId']; + $this->validateSkippedIssues($annotationData, $filename); + } + foreach ($annotationData as $annotationValue) { $annotationValues[] = $annotationValue[self::ANNOTATION_VALUE]; } + // TODO deprecation|deprecate MFTF 3.0.0 + if ($annotationKey == "group" && in_array("skip", $annotationValues)) { + LoggingUtil::getInstance()->getLogger(AnnotationExtractor::class)->warning( + "Use of group skip will be deprecated in MFTF 3.0.0. Please update tests to use skip tags.", + ["test" => $filename] + ); + } + $annotationObjects[$annotationKey] = $annotationValues; } + $this->validateMissingAnnotations($annotationObjects, $filename); + + $this->addStoryTitleToMap($annotationObjects, $filename); + return $annotationObjects; } + /** + * Adds story/title/filename combination to static map + * @param array $annotations + * @param string $filename + * @return void + */ + public function addStoryTitleToMap($annotations, $filename) + { + if (isset($annotations['stories']) && isset($annotations['title'])) { + $story = $annotations['stories'][0]; + $title = $annotations['title'][0]; + $this->storyToTitleMappings[$story . "/" . $title][] = $filename; + } + } + + /** + * Validates given annotations against list of required annotations. + * @param array $annotationObjects + * @return void + */ + private function validateMissingAnnotations($annotationObjects, $filename) + { + $missingAnnotations = []; + + foreach (self::REQUIRED_ANNOTATIONS as $REQUIRED_ANNOTATION) { + if (!array_key_exists($REQUIRED_ANNOTATION, $annotationObjects)) { + $missingAnnotations[] = $REQUIRED_ANNOTATION; + } + } + + if (!empty($missingAnnotations)) { + $message = "Test {$filename} is missing required annotations."; + LoggingUtil::getInstance()->getLogger(ActionObject::class)->deprecation( + $message, + ["testName" => $filename, "missingAnnotations" => implode(", ", $missingAnnotations)] + ); + } + } + + /** + * Validates that all Story/Title combinations are unique, builds list of violators if found. + * @throws XmlException + * @return void + */ + public function validateStoryTitleUniqueness() + { + $dupes = []; + + foreach ($this->storyToTitleMappings as $storyTitle => $files) { + if (count($files) > 1) { + $dupes[$storyTitle] = "'" . implode("', '", $files) . "'"; + } + } + if (!empty($dupes)) { + $message = "Story and Title annotation pairs must be unique:\n\n"; + foreach ($dupes as $storyTitle => $tests) { + $storyTitleArray = explode("/", $storyTitle); + $story = $storyTitleArray[0]; + $title = $storyTitleArray[1]; + $message .= "Story: '{$story}' Title: '{$title}' in Tests {$tests}\n\n"; + } + throw new XmlException($message); + } + } + + /** + * Validates that all issueId tags contain a non-empty value + * @param array $issues + * @param string $filename + * @throws XmlException + * @return void + */ + public function validateSkippedIssues($issues, $filename) + { + foreach ($issues as $issueId) { + if (empty($issueId['value'])) { + $message = "issueId for skipped tests cannot be empty. Test: $filename"; + throw new XmlException($message); + } + } + } + /** * This method transforms Magento severity values from Severity annotation * Returns Allure annotation value diff --git a/src/Magento/FunctionalTestingFramework/Test/Util/BaseObjectExtractor.php b/src/Magento/FunctionalTestingFramework/Test/Util/BaseObjectExtractor.php index 50dffc3c3..26644937d 100644 --- a/src/Magento/FunctionalTestingFramework/Test/Util/BaseObjectExtractor.php +++ b/src/Magento/FunctionalTestingFramework/Test/Util/BaseObjectExtractor.php @@ -22,13 +22,12 @@ public function __construct() // empty } - // @codingStandardsIgnoreStart /** * This method takes an array of data and an array representing irrelevant tags. The method strips * the data passed in of the irrelevant tags and returns the result. * * @param array $data - * @param array $tags + * @param array ...$tags * @return array */ protected function stripDescriptorTags($data, ...$tags) @@ -40,5 +39,4 @@ protected function stripDescriptorTags($data, ...$tags) return $results; } - // @codingStandardsIgnoreEnd } diff --git a/src/Magento/FunctionalTestingFramework/Test/Util/ObjectExtensionUtil.php b/src/Magento/FunctionalTestingFramework/Test/Util/ObjectExtensionUtil.php new file mode 100644 index 000000000..97f67004a --- /dev/null +++ b/src/Magento/FunctionalTestingFramework/Test/Util/ObjectExtensionUtil.php @@ -0,0 +1,230 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\FunctionalTestingFramework\Test\Util; + +use Magento\FunctionalTestingFramework\Config\MftfApplicationConfig; +use Magento\FunctionalTestingFramework\Exceptions\TestReferenceException; +use Magento\FunctionalTestingFramework\Exceptions\XmlException; +use Magento\FunctionalTestingFramework\Test\Handlers\ActionGroupObjectHandler; +use Magento\FunctionalTestingFramework\Test\Objects\ActionGroupObject; +use Magento\FunctionalTestingFramework\Test\Objects\ActionObject; +use Magento\FunctionalTestingFramework\Test\Objects\TestObject; +use Magento\FunctionalTestingFramework\Test\Objects\TestHookObject; +use Magento\FunctionalTestingFramework\Test\Handlers\TestObjectHandler; +use Magento\FunctionalTestingFramework\Util\Logger\LoggingUtil; + +class ObjectExtensionUtil +{ + /** + * ObjectExtensionUtil constructor. + */ + public function __construct() + { + // empty + } + + /** + * Resolves test references for extending test objects + * + * @param TestObject $testObject + * @return TestObject + * @throws TestReferenceException|XmlException + */ + public function extendTest($testObject) + { + // Check to see if the parent test exists + try { + $parentTest = TestObjectHandler::getInstance()->getObject($testObject->getParentName()); + } catch (TestReferenceException $error) { + if (MftfApplicationConfig::getConfig()->verboseEnabled()) { + LoggingUtil::getInstance()->getLogger(ObjectExtensionUtil::class)->debug( + "parent test not defined. test will be skipped", + ["parent" => $testObject->getParentName(), "test" => $testObject->getName()] + ); + } + $skippedTest = $this->skipTest($testObject); + return $skippedTest; + } + + // Check to see if the parent test is already an extended test + if ($parentTest->getParentName() !== null) { + throw new XmlException( + "Cannot extend a test that already extends another test. Test: " . $parentTest->getName(), + ["parent" => $parentTest->getName(), "actionGroup" => $testObject->getName()] + ); + } + if (MftfApplicationConfig::getConfig()->verboseEnabled()) { + LoggingUtil::getInstance()->getLogger(ObjectExtensionUtil::class) + ->debug("extending test", ["parent" => $parentTest->getName(), "test" => $testObject->getName()]); + } + + // Get steps for both the parent and the child tests + $referencedTestSteps = $parentTest->getUnresolvedSteps(); + $newSteps = $this->processRemoveActions(array_merge($referencedTestSteps, $testObject->getUnresolvedSteps())); + + $testHooks = $this->resolveExtendedHooks($testObject, $parentTest); + + // Create new Test object to return + $extendedTest = new TestObject( + $testObject->getName(), + $newSteps, + $testObject->getAnnotations(), + $testHooks, + $testObject->getFilename(), + $testObject->getParentName() + ); + return $extendedTest; + } + + /** + * Resolves test references for extending action group objects + * + * @param ActionGroupObject $actionGroupObject + * @return ActionGroupObject + * @throws XmlException + */ + public function extendActionGroup($actionGroupObject) + { + // Check to see if the parent action group exists + $parentActionGroup = ActionGroupObjectHandler::getInstance()->getObject($actionGroupObject->getParentName()); + if ($parentActionGroup == null) { + throw new XmlException( + "Parent Action Group " . + $actionGroupObject->getParentName() . + " not defined for Test " . + $actionGroupObject->getName() . + "." . + PHP_EOL + ); + } + + // Check to see if the parent action group is already an extended action group + if ($parentActionGroup->getParentName() !== null) { + throw new XmlException( + "Cannot extend an action group that already extends another action group. " . + $parentActionGroup->getName(), + ["parent" => $parentActionGroup->getName(), "actionGroup" => $actionGroupObject->getName()] + ); + } + if (MftfApplicationConfig::getConfig()->verboseEnabled()) { + LoggingUtil::getInstance()->getLogger(ObjectExtensionUtil::class)->debug( + "extending action group:", + ["parent" => $parentActionGroup->getName(), "actionGroup" => $actionGroupObject->getName()] + ); + } + + // Get steps for both the parent and the child action groups + $referencedActions = $parentActionGroup->getActions(); + $newActions = $this->processRemoveActions(array_merge($referencedActions, $actionGroupObject->getActions())); + + $extendedArguments = array_merge( + $actionGroupObject->getArguments(), + $parentActionGroup->getArguments() + ); + + // Create new Action Group object to return + $extendedActionGroup = new ActionGroupObject( + $actionGroupObject->getName(), + $extendedArguments, + $newActions, + $actionGroupObject->getParentName() + ); + return $extendedActionGroup; + } + + /** + * Resolves test references for extending test objects + * + * @param TestObject $testObject + * @param TestObject $parentTestObject + * @return TestHookObject[] $testHooks + */ + private function resolveExtendedHooks($testObject, $parentTestObject) + { + $testHooks = $testObject->getHooks(); + $parentHooks = $parentTestObject->getHooks(); + + // Get the hooks for each Test merge changes from the child hooks to the parent hooks into the child hooks + foreach ($testHooks as $key => $hook) { + if (array_key_exists($key, $parentHooks)) { + $testHookActions = array_merge( + $parentHooks[$key]->getUnresolvedActions(), + $testHooks[$key]->getUnresolvedActions() + ); + $cleanedTestHookActions = $this->processRemoveActions($testHookActions); + + $newTestHook = new TestHookObject( + $parentHooks[$key]->getType(), + $parentHooks[$key]->getParentName(), + $cleanedTestHookActions + ); + $testHooks[$key] = $newTestHook; + } else { + $testHooks[$key] = $hook; + } + } + + // Add parent hooks to child if they did not originally exist on the child + foreach ($parentHooks as $key => $hook) { + if (!array_key_exists($key, $testHooks)) { + $testHooks[$key] = $hook; + } + } + + return $testHooks; + } + + /** + * Resolves test references for removing actions in extended test + * + * @param ActionObject[] $actions + * @return ActionObject[] + * @throws XmlException + */ + private function processRemoveActions($actions) + { + $cleanedActions = []; + + // remove actions merged that are of type 'remove' + foreach ($actions as $actionName => $actionData) { + if ($actionData->getType() != "remove") { + $cleanedActions[$actionName] = $actionData; + } + } + + return $cleanedActions; + } + + /** + * This method returns a skipped form of the Test Object + * + * @param TestObject $testObject + * @return TestObject + */ + public function skipTest($testObject) + { + $annotations = $testObject->getAnnotations(); + + // Add skip to the group array if it doesn't already exist + if (array_key_exists('group', $annotations) && !in_array('skip', $annotations['group'])) { + array_push($annotations['group'], 'skip'); + } elseif (!array_key_exists('group', $annotations)) { + $annotations['group'] = ['skip']; + } + + $skippedTest = new TestObject( + $testObject->getName(), + [], + $annotations, + [], + $testObject->getFilename(), + $testObject->getParentName() + ); + + return $skippedTest; + } +} diff --git a/src/Magento/FunctionalTestingFramework/Test/Util/TestHookObjectExtractor.php b/src/Magento/FunctionalTestingFramework/Test/Util/TestHookObjectExtractor.php index 16ba69e0b..c11a6e512 100644 --- a/src/Magento/FunctionalTestingFramework/Test/Util/TestHookObjectExtractor.php +++ b/src/Magento/FunctionalTestingFramework/Test/Util/TestHookObjectExtractor.php @@ -35,8 +35,9 @@ public function __construct() * * @param string $parentName * @param string $hookType - * @param array $testHook + * @param array $testHook * @return TestHookObject + * @throws \Exception */ public function extractHook($parentName, $hookType, $testHook) { diff --git a/src/Magento/FunctionalTestingFramework/Test/Util/TestObjectExtractor.php b/src/Magento/FunctionalTestingFramework/Test/Util/TestObjectExtractor.php index be4e3c97c..acad18572 100644 --- a/src/Magento/FunctionalTestingFramework/Test/Util/TestObjectExtractor.php +++ b/src/Magento/FunctionalTestingFramework/Test/Util/TestObjectExtractor.php @@ -10,6 +10,7 @@ use Magento\FunctionalTestingFramework\Exceptions\XmlException; use Magento\FunctionalTestingFramework\Test\Objects\ActionObject; use Magento\FunctionalTestingFramework\Test\Objects\TestObject; +use Magento\FunctionalTestingFramework\Util\ModulePathExtractor; use Magento\FunctionalTestingFramework\Util\Validation\NameValidationUtil; /** @@ -21,6 +22,11 @@ class TestObjectExtractor extends BaseObjectExtractor const TEST_BEFORE_HOOK = 'before'; const TEST_AFTER_HOOK = 'after'; const TEST_FAILED_HOOK = 'failed'; + const TEST_BEFORE_ATTRIBUTE = 'before'; + const TEST_AFTER_ATTRIBUTE = 'after'; + const TEST_INSERT_BEFORE = 'insertBefore'; + const TEST_INSERT_AFTER = 'insertAfter'; + const TEST_FILENAME = 'filename'; /** * Action Object Extractor object @@ -43,6 +49,13 @@ class TestObjectExtractor extends BaseObjectExtractor */ private $testHookObjectExtractor; + /** + * Module Path extractor + * + * @var ModulePathExtractor + */ + private $modulePathExtractor; + /** * TestObjectExtractor constructor. */ @@ -51,6 +64,16 @@ public function __construct() $this->actionObjectExtractor = new ActionObjectExtractor(); $this->annotationExtractor = new AnnotationExtractor(); $this->testHookObjectExtractor = new TestHookObjectExtractor(); + $this->modulePathExtractor = new ModulePathExtractor(); + } + + /** + * Getter for AnnotationExtractor + * @return AnnotationExtractor + */ + public function getAnnotationExtractor() + { + return $this->annotationExtractor; } /** @@ -59,7 +82,7 @@ public function __construct() * * @param array $testData * @return TestObject - * @throws \Magento\FunctionalTestingFramework\Exceptions\XmlException + * @throws \Exception */ public function extractTestData($testData) { @@ -69,6 +92,10 @@ public function extractTestData($testData) $testAnnotations = []; $testHooks = []; $filename = $testData['filename'] ?? null; + $fileNames = explode(",", $filename); + $baseFileName = $fileNames[0]; + $module = $this->modulePathExtractor->extractModuleName($baseFileName); + $testReference = $testData['extends'] ?? null; $testActions = $this->stripDescriptorTags( $testData, self::NODE_NAME, @@ -77,15 +104,22 @@ public function extractTestData($testData) self::TEST_BEFORE_HOOK, self::TEST_AFTER_HOOK, self::TEST_FAILED_HOOK, - 'filename' + self::TEST_INSERT_BEFORE, + self::TEST_INSERT_AFTER, + self::TEST_FILENAME, + 'extends' ); - if (array_key_exists(self::TEST_ANNOTATIONS, $testData)) { - $testAnnotations = $this->annotationExtractor->extractAnnotations($testData[self::TEST_ANNOTATIONS]); - } + $testAnnotations = $this->annotationExtractor->extractAnnotations( + $testData[self::TEST_ANNOTATIONS] ?? [], + $testData[self::NAME] + ); + + //Override features with module name if present, populates it otherwise + $testAnnotations["features"] = [$module]; // extract before - if (array_key_exists(self::TEST_BEFORE_HOOK, $testData)) { + if (array_key_exists(self::TEST_BEFORE_HOOK, $testData) && is_array($testData[self::TEST_BEFORE_HOOK])) { $testHooks[self::TEST_BEFORE_HOOK] = $this->testHookObjectExtractor->extractHook( $testData[self::NAME], 'before', @@ -93,7 +127,7 @@ public function extractTestData($testData) ); } - if (array_key_exists(self::TEST_AFTER_HOOK, $testData)) { + if (array_key_exists(self::TEST_AFTER_HOOK, $testData) && is_array($testData[self::TEST_AFTER_HOOK])) { // extract after $testHooks[self::TEST_AFTER_HOOK] = $this->testHookObjectExtractor->extractHook( $testData[self::NAME], @@ -114,7 +148,8 @@ public function extractTestData($testData) $this->actionObjectExtractor->extractActions($testActions, $testData[self::NAME]), $testAnnotations, $testHooks, - $filename + $filename, + $testReference ); } catch (XmlException $exception) { throw new XmlException($exception->getMessage() . ' in Test ' . $filename); diff --git a/src/Magento/FunctionalTestingFramework/Test/etc/Actions/customActions.xsd b/src/Magento/FunctionalTestingFramework/Test/etc/Actions/customActions.xsd index 6a27dd4ce..3a002e126 100644 --- a/src/Magento/FunctionalTestingFramework/Test/etc/Actions/customActions.xsd +++ b/src/Magento/FunctionalTestingFramework/Test/etc/Actions/customActions.xsd @@ -22,6 +22,7 @@ <xs:element type="scrollToTopOfPageType" name="scrollToTopOfPage" minOccurs="0" maxOccurs="unbounded"/> <xs:element type="clearFieldType" name="clearField" minOccurs="0" maxOccurs="unbounded"/> <xs:element type="assertArrayIsSortedType" name="assertArrayIsSorted" minOccurs="0" maxOccurs="unbounded"/> + <xs:element type="generateDateType" name="generateDate" minOccurs="0" maxOccurs="unbounded"/> </xs:choice> </xs:group> @@ -42,6 +43,13 @@ </xs:documentation> </xs:annotation> </xs:attribute> + <xs:attribute type="xs:string" name="arguments"> + <xs:annotation> + <xs:documentation> + Arguments for Magento CLI command, will not be escaped. + </xs:documentation> + </xs:annotation> + </xs:attribute> <xs:attributeGroup ref="commonActionAttributes"/> </xs:extension> </xs:simpleContent> @@ -212,6 +220,41 @@ <xs:attributeGroup ref="commonActionAttributes"/> </xs:complexType> + <xs:complexType name="generateDateType"> + <xs:annotation> + <xs:documentation> + Generates a date according to input and format. + </xs:documentation> + </xs:annotation> + <xs:simpleContent> + <xs:extension base="xs:string"> + <xs:attribute name="date" use="required" type="xs:string"> + <xs:annotation> + <xs:documentation> + Date input to parse, uses same functionality as php strtotime() function. + </xs:documentation> + </xs:annotation> + </xs:attribute> + <xs:attribute name="format" use="required" type="xs:string"> + <xs:annotation> + <xs:documentation> + Format to save given date in, uses same functionality as php date() function. + </xs:documentation> + </xs:annotation> + </xs:attribute> + <xs:attribute name="timezone" use="optional" type="xs:string"> + <xs:annotation> + <xs:documentation> + Timezone to generate date in, defaults to "America/Los_Angeles". + </xs:documentation> + </xs:annotation> + </xs:attribute> + <xs:attributeGroup ref="commonActionAttributes"/> + </xs:extension> + </xs:simpleContent> + </xs:complexType> + + <xs:complexType name="arrayType"> <xs:simpleContent> <xs:extension base="xs:string"> diff --git a/src/Magento/FunctionalTestingFramework/Test/etc/actionGroupSchema.xsd b/src/Magento/FunctionalTestingFramework/Test/etc/actionGroupSchema.xsd index 74badd40c..619d21e9f 100644 --- a/src/Magento/FunctionalTestingFramework/Test/etc/actionGroupSchema.xsd +++ b/src/Magento/FunctionalTestingFramework/Test/etc/actionGroupSchema.xsd @@ -33,6 +33,9 @@ </xs:choice> <xs:attribute type="xs:string" name="name" use="required"/> <xs:attribute type="xs:string" name="filename"/> + <xs:attribute type="xs:string" name="insertBefore"/> + <xs:attribute type="xs:string" name="insertAfter"/> + <xs:attribute type="xs:string" name="extends"/> </xs:complexType> <xs:simpleType name="dataTypeEnum" final="restriction"> <xs:restriction base="xs:string"> diff --git a/src/Magento/FunctionalTestingFramework/Test/etc/actionTypeTags.xsd b/src/Magento/FunctionalTestingFramework/Test/etc/actionTypeTags.xsd index 9aa772858..90671c340 100644 --- a/src/Magento/FunctionalTestingFramework/Test/etc/actionTypeTags.xsd +++ b/src/Magento/FunctionalTestingFramework/Test/etc/actionTypeTags.xsd @@ -37,6 +37,7 @@ <xs:element type="executeInSeleniumType" name="executeInSelenium" minOccurs="0" maxOccurs="unbounded"/> <xs:element type="executeJSType" name="executeJS" minOccurs="0" maxOccurs="unbounded"/> <xs:element type="fillFieldType" name="fillField" minOccurs="0" maxOccurs="unbounded"/> + <xs:element type="fillFieldType" name="fillSecretField" minOccurs="0" maxOccurs="unbounded"/> <xs:element type="loadSessionSnapshotType" name="loadSessionSnapshot" minOccurs="0" maxOccurs="unbounded"/> <xs:element type="makeScreenshotType" name="makeScreenshot" minOccurs="0" maxOccurs="unbounded"/> <xs:element type="maximizeWindowType" name="maximizeWindow" minOccurs="0" maxOccurs="unbounded"/> diff --git a/src/Magento/FunctionalTestingFramework/Test/etc/mergedTestSchema.xsd b/src/Magento/FunctionalTestingFramework/Test/etc/mergedTestSchema.xsd index 012083ab8..b65843bae 100644 --- a/src/Magento/FunctionalTestingFramework/Test/etc/mergedTestSchema.xsd +++ b/src/Magento/FunctionalTestingFramework/Test/etc/mergedTestSchema.xsd @@ -29,6 +29,7 @@ <xs:element type="annotationType" name="testCaseId" minOccurs="0" maxOccurs="unbounded"/> <xs:element type="annotationType" name="useCaseId" minOccurs="0" maxOccurs="unbounded"/> <xs:element type="annotationType" name="group" minOccurs="0" maxOccurs="unbounded"/> + <xs:element type="skipType" name="skip" minOccurs="0" maxOccurs="unbounded"/> <xs:element type="annotationType" name="return" minOccurs="0" maxOccurs="unbounded"/> </xs:choice> </xs:complexType> @@ -55,6 +56,18 @@ </xs:extension> </xs:simpleContent> </xs:complexType> + <xs:complexType name="skipType"> + <xs:sequence maxOccurs="unbounded"> + <xs:element type="issueType" name="issueId" maxOccurs="unbounded"/> + </xs:sequence> + </xs:complexType> + <xs:complexType name="issueType"> + <xs:simpleContent> + <xs:extension base="xs:string"> + <xs:attribute type="xs:string" name="value" use="required"/> + </xs:extension> + </xs:simpleContent> + </xs:complexType> <xs:complexType name="hookType"> <xs:choice minOccurs="0" maxOccurs="unbounded"> <xs:group ref="testTypeTags"/> @@ -70,6 +83,9 @@ <xs:attribute type="xs:string" name="name"/> <xs:attribute type="xs:boolean" name="remove" default="false"/> <xs:attribute type="xs:string" name="filename"/> + <xs:attribute type="xs:string" name="insertBefore"/> + <xs:attribute type="xs:string" name="insertAfter"/> + <xs:attribute type="xs:string" name="extends"/> </xs:complexType> <xs:group name="testTypeTags"> <xs:choice> diff --git a/src/Magento/FunctionalTestingFramework/Upgrade/UpdateTestSchemaPaths.php b/src/Magento/FunctionalTestingFramework/Upgrade/UpdateTestSchemaPaths.php new file mode 100644 index 000000000..8b79b6019 --- /dev/null +++ b/src/Magento/FunctionalTestingFramework/Upgrade/UpdateTestSchemaPaths.php @@ -0,0 +1,73 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\FunctionalTestingFramework\Upgrade; + +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Filesystem\Filesystem; +use Symfony\Component\Finder\Finder; + +/** + * Class UpdateTestSchemaPaths + * @package Magento\FunctionalTestingFramework\Upgrade + */ +class UpdateTestSchemaPaths implements UpgradeInterface +{ + /** + * Upgrades all test xml files, replacing relative schema paths to URN. + * + * @param InputInterface $input + * @return string + */ + public function execute(InputInterface $input) + { + // @codingStandardsIgnoreStart + $relativeToUrn = [ + "dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/DataGenerator/etc/dataOperation.xsd" + => "urn:magento:mftf:DataGenerator/etc/dataOperation.xsd", + "dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/DataGenerator/etc/dataProfileSchema.xsd" + => "urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd", + "dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/PageObject.xsd" + => "urn:magento:mftf:Page/etc/PageObject.xsd", + "dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/SectionObject.xsd" + => "urn:magento:mftf:Page/etc/SectionObject.xsd", + "dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/actionGroupSchema.xsd" + => "urn:magento:mftf:Test/etc/actionGroupSchema.xsd", + "dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/testSchema.xsd" + => "urn:magento:mftf:Test/etc/testSchema.xsd", + "dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Suite/etc/suiteSchema.xsd" + => "urn:magento:mftf:Suite/etc/suiteSchema.xsd" + ]; + // @codingStandardsIgnoreEnd + + $relativePatterns = []; + $urns = []; + // Prepare array of patterns to URNs for preg_replace (replace / to escapes + foreach ($relativeToUrn as $relative => $urn) { + $relativeReplaced = str_replace('/', '\/', $relative); + $relativePatterns[] = '/[.\/]+' . $relativeReplaced . '/'; + $urns[] = $urn; + } + + $testsPath = $input->getArgument('path'); + $finder = new Finder(); + $finder->files()->in($testsPath)->name("*.xml"); + + $fileSystem = new Filesystem(); + $testsUpdated = 0; + foreach ($finder->files() as $file) { + $count = 0; + $contents = $file->getContents(); + $contents = preg_replace($relativePatterns, $urns, $contents, -1, $count); + $fileSystem->dumpFile($file->getRealPath(), $contents); + if ($count > 0) { + $testsUpdated++; + } + } + + return ("Schema Path updated to use MFTF URNs in {$testsUpdated} file(s)."); + } +} diff --git a/src/Magento/FunctionalTestingFramework/Upgrade/UpgradeInterface.php b/src/Magento/FunctionalTestingFramework/Upgrade/UpgradeInterface.php new file mode 100644 index 000000000..b6905845c --- /dev/null +++ b/src/Magento/FunctionalTestingFramework/Upgrade/UpgradeInterface.php @@ -0,0 +1,22 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\FunctionalTestingFramework\Upgrade; + +use Symfony\Component\Console\Input\InputInterface; + +/** + * Upgrade script interface + */ +interface UpgradeInterface +{ + /** + * Executes upgrade script, returns output. + * @param InputInterface $input + * @return string + */ + public function execute(InputInterface $input); +} diff --git a/src/Magento/FunctionalTestingFramework/Upgrade/UpgradeScriptList.php b/src/Magento/FunctionalTestingFramework/Upgrade/UpgradeScriptList.php new file mode 100644 index 000000000..245f95d82 --- /dev/null +++ b/src/Magento/FunctionalTestingFramework/Upgrade/UpgradeScriptList.php @@ -0,0 +1,42 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\FunctionalTestingFramework\Upgrade; + +/** + * Class UpgradeScriptList has a list of scripts. + * @codingStandardsIgnoreFile + */ +class UpgradeScriptList implements UpgradeScriptListInterface +{ + /** + * Property contains all upgrade scripts. + * + * @var \Magento\FunctionalTestingFramework\Upgrade\UpgradeInterface[] + */ + private $scripts; + + /** + * Constructor + * + * @param array $scripts + */ + public function __construct(array $scripts = []) + { + $this->scripts = [ + 'upgradeTestSchema' => new UpdateTestSchemaPaths(), + ] + $scripts; + } + + /** + * {@inheritdoc} + */ + public function getUpgradeScripts() + { + return $this->scripts; + } +} diff --git a/src/Magento/FunctionalTestingFramework/Upgrade/UpgradeScriptListInterface.php b/src/Magento/FunctionalTestingFramework/Upgrade/UpgradeScriptListInterface.php new file mode 100644 index 000000000..a96316797 --- /dev/null +++ b/src/Magento/FunctionalTestingFramework/Upgrade/UpgradeScriptListInterface.php @@ -0,0 +1,20 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\FunctionalTestingFramework\Upgrade; + +/** + * Contains a list of Upgrade Scripts + * @api + */ +interface UpgradeScriptListInterface +{ + /** + * Gets list of upgrade script instances + * + * @return \Magento\FunctionalTestingFramework\Upgrade\UpgradeInterface[] + */ + public function getUpgradeScripts(); +} diff --git a/src/Magento/FunctionalTestingFramework/Util/ConfigSanitizerUtil.php b/src/Magento/FunctionalTestingFramework/Util/ConfigSanitizerUtil.php index 802db4127..ce5740493 100644 --- a/src/Magento/FunctionalTestingFramework/Util/ConfigSanitizerUtil.php +++ b/src/Magento/FunctionalTestingFramework/Util/ConfigSanitizerUtil.php @@ -15,7 +15,7 @@ class ConfigSanitizerUtil { /** * Sanitizes the given Webdriver Config's url and selenium env params, can be selective based on second argument. - * @param array $config + * @param array $config * @param String[] $params * @return array */ diff --git a/src/Magento/FunctionalTestingFramework/Util/Env/EnvProcessor.php b/src/Magento/FunctionalTestingFramework/Util/Env/EnvProcessor.php index 7129ff091..f09ab63fa 100644 --- a/src/Magento/FunctionalTestingFramework/Util/Env/EnvProcessor.php +++ b/src/Magento/FunctionalTestingFramework/Util/Env/EnvProcessor.php @@ -1,5 +1,4 @@ <?php -// @codingStandardsIgnoreFile /** * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. @@ -36,6 +35,13 @@ class EnvProcessor */ private $env = []; + /** + * Boolean indicating existence of env file + * + * @var boolean + */ + private $envExists; + /** * EnvProcessor constructor. * @param string $envFile @@ -44,33 +50,55 @@ public function __construct( string $envFile = '' ) { $this->envFile = $envFile; - $this->envExampleFile = $envFile . '.example'; + $this->envExists = file_exists($envFile); + $this->envExampleFile = realpath(FW_BP . "/etc/config/.env.example"); } /** - * Serves for parsing '.env.example' file into associative array. + * Serves for parsing '.env' file into associative array. * * @return array */ - public function parseEnvFile(): array + private function parseEnvFile(): array { - $envLines = file( + $envExampleFile = file( $this->envExampleFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES ); - $env = []; - foreach ($envLines as $line) { + + $envContents = []; + if ($this->envExists) { + $envFile = file( + $this->envFile, + FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES + ); + + $envContents = $this->parseEnvFileLines($envFile); + } + + return array_merge($this->parseEnvFileLines($envExampleFile), $envContents); + } + + /** + * Iterates through env and returns array of file contents. + * @param array $file + * @return array + */ + private function parseEnvFileLines(array $file): array + { + $fileArray = []; + foreach ($file as $line) { // do not use commented out lines if (strpos($line, '#') !== 0) { list($key, $value) = explode('=', $line); - $env[$key] = $value; + $fileArray[$key] = $value; } } - return $env; + return $fileArray; } /** - * Serves for putting array with environment variables into .env file. + * Serves for putting array with environment variables into .env file or appending new variables we introduce * * @param array $config * @return void @@ -81,6 +109,7 @@ public function putEnvFile(array $config = []) foreach ($config as $key => $value) { $envData .= $key . '=' . $value . PHP_EOL; } + file_put_contents($this->envFile, $envData); } diff --git a/src/Magento/FunctionalTestingFramework/Util/Filesystem/DirSetupUtil.php b/src/Magento/FunctionalTestingFramework/Util/Filesystem/DirSetupUtil.php index 4207a2c8c..9f0036629 100644 --- a/src/Magento/FunctionalTestingFramework/Util/Filesystem/DirSetupUtil.php +++ b/src/Magento/FunctionalTestingFramework/Util/Filesystem/DirSetupUtil.php @@ -26,17 +26,19 @@ class DirSetupUtil */ public static function createGroupDir($fullPath) { + //prevent redundant calls to these directories + $sanitizedPath = rtrim($fullPath, DIRECTORY_SEPARATOR); // make sure we haven't already cleaned up this directory at any point before deletion - if (in_array($fullPath, self::$DIR_CONTEXT)) { + if (in_array($sanitizedPath, self::$DIR_CONTEXT)) { return; } - if (file_exists($fullPath)) { - self::rmDirRecursive($fullPath); + if (file_exists($sanitizedPath)) { + self::rmDirRecursive($sanitizedPath); } - mkdir($fullPath, 0777, true); - self::$DIR_CONTEXT[] = $fullPath; + mkdir($sanitizedPath, 0777, true); + self::$DIR_CONTEXT[] = $sanitizedPath; } /** diff --git a/src/Magento/FunctionalTestingFramework/Util/Iterator/AbstractIterator.php b/src/Magento/FunctionalTestingFramework/Util/Iterator/AbstractIterator.php index 0b95a4ca7..69e2527aa 100644 --- a/src/Magento/FunctionalTestingFramework/Util/Iterator/AbstractIterator.php +++ b/src/Magento/FunctionalTestingFramework/Util/Iterator/AbstractIterator.php @@ -99,7 +99,7 @@ public function valid() /** * Get data key of the current data element * - * @return int|string + * @return integer|string */ public function key() { @@ -109,7 +109,7 @@ public function key() /** * To make iterator countable * - * @return int + * @return integer */ public function count() { diff --git a/src/Magento/FunctionalTestingFramework/Util/Logger/LoggingUtil.php b/src/Magento/FunctionalTestingFramework/Util/Logger/LoggingUtil.php new file mode 100644 index 000000000..78b0c29ef --- /dev/null +++ b/src/Magento/FunctionalTestingFramework/Util/Logger/LoggingUtil.php @@ -0,0 +1,82 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\FunctionalTestingFramework\Util\Logger; + +use Monolog\Handler\StreamHandler; +use Monolog\Logger; + +class LoggingUtil +{ + /** + * Private Map of Logger instances, indexed by Class Name. + * + * @var array + */ + private $loggers = []; + + /** + * Singleton LogginUtil Instance + * + * @var LoggingUtil + */ + private static $INSTANCE; + + /** + * Singleton accessor for instance variable + * + * @return LoggingUtil + */ + public static function getInstance() + { + if (self::$INSTANCE == null) { + self::$INSTANCE = new LoggingUtil(); + } + + return self::$INSTANCE; + } + + /** + * Constructor for Logging Util + */ + private function __construct() + { + // private constructor + } + + /** + * Creates a new logger instances based on class name if it does not exist. If logger instance already exists, the + * existing instance is simply returned. + * + * @param string $clazz + * @return MftfLogger + * @throws \Exception + */ + public function getLogger($clazz) + { + if ($clazz == null) { + throw new \Exception("You must pass a class to receive a logger"); + } + + if (!array_key_exists($clazz, $this->loggers)) { + $logger = new MftfLogger($clazz); + $logger->pushHandler(new StreamHandler($this->getLoggingPath())); + $this->loggers[$clazz] = $logger; + } + + return $this->loggers[$clazz]; + } + + /** + * Function which returns a static path to the the log file. + * + * @return string + */ + public function getLoggingPath() + { + return TESTS_BP . DIRECTORY_SEPARATOR . "mftf.log"; + } +} diff --git a/src/Magento/FunctionalTestingFramework/Util/Logger/MftfLogger.php b/src/Magento/FunctionalTestingFramework/Util/Logger/MftfLogger.php new file mode 100644 index 000000000..5844858a0 --- /dev/null +++ b/src/Magento/FunctionalTestingFramework/Util/Logger/MftfLogger.php @@ -0,0 +1,31 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\FunctionalTestingFramework\Util\Logger; + +use Magento\FunctionalTestingFramework\Config\MftfApplicationConfig; +use Monolog\Handler\StreamHandler; +use Monolog\Logger; + +class MftfLogger extends Logger +{ + /** + * Prints a deprecation warning, as well as adding a log at the WARNING level. + * + * @param string $message The log message. + * @param array $context The log context. + * @return void + */ + public function deprecation($message, array $context = []) + { + $message = "DEPRECATION: " . $message; + // Suppress print during unit testing + if (MftfApplicationConfig::getConfig()->getPhase() !== MftfApplicationConfig::UNIT_TEST_PHASE) { + print ($message . json_encode($context) . "\n"); + } + parent::warning($message, $context); + } +} diff --git a/src/Magento/FunctionalTestingFramework/Util/Manifest/BaseTestManifest.php b/src/Magento/FunctionalTestingFramework/Util/Manifest/BaseTestManifest.php index 3c3eb6de9..f7ad8aea2 100644 --- a/src/Magento/FunctionalTestingFramework/Util/Manifest/BaseTestManifest.php +++ b/src/Magento/FunctionalTestingFramework/Util/Manifest/BaseTestManifest.php @@ -38,12 +38,13 @@ abstract class BaseTestManifest * * @param string $path * @param string $runConfig - * @param array $suiteConfiguration + * @param array $suiteConfiguration */ public function __construct($path, $runConfig, $suiteConfiguration) { $this->runTypeConfig = $runConfig; - $this->relativeDirPath = substr($path, strlen(dirname(dirname(TESTS_BP))) + 1); + $relativeDirPath = substr($path, strlen(TESTS_BP)); + $this->relativeDirPath = ltrim($relativeDirPath, DIRECTORY_SEPARATOR); $this->suiteConfiguration = $suiteConfiguration; } diff --git a/src/Magento/FunctionalTestingFramework/Util/Manifest/DefaultTestManifest.php b/src/Magento/FunctionalTestingFramework/Util/Manifest/DefaultTestManifest.php index f6af0fe88..eb4f79db2 100644 --- a/src/Magento/FunctionalTestingFramework/Util/Manifest/DefaultTestManifest.php +++ b/src/Magento/FunctionalTestingFramework/Util/Manifest/DefaultTestManifest.php @@ -37,7 +37,7 @@ class DefaultTestManifest extends BaseTestManifest /** * DefaultTestManifest constructor. * - * @param array $suiteConfiguration + * @param array $suiteConfiguration * @param string $testPath */ public function __construct($suiteConfiguration, $testPath) diff --git a/src/Magento/FunctionalTestingFramework/Util/Manifest/ParallelTestManifest.php b/src/Magento/FunctionalTestingFramework/Util/Manifest/ParallelTestManifest.php index 12ce5d701..9b12cfd00 100644 --- a/src/Magento/FunctionalTestingFramework/Util/Manifest/ParallelTestManifest.php +++ b/src/Magento/FunctionalTestingFramework/Util/Manifest/ParallelTestManifest.php @@ -52,7 +52,7 @@ class ParallelTestManifest extends BaseTestManifest /** * TestManifest constructor. * - * @param array $suiteConfiguration + * @param array $suiteConfiguration * @param string $testPath */ public function __construct($suiteConfiguration, $testPath) @@ -70,22 +70,22 @@ public function __construct($suiteConfiguration, $testPath) */ public function addTest($testObject) { - $this->testNameToSize[$testObject->getCodeceptionName()] = $testObject->getTestActionCount(); + $this->testNameToSize[$testObject->getCodeceptionName()] = $testObject->getEstimatedDuration(); } /** * Function which generates test groups based on arg passed. The function builds groups using the args as an upper * limit. * - * @param int $lines + * @param integer $time * @return void */ - public function createTestGroups($lines) + public function createTestGroups($time) { $this->testGroups = $this->parallelGroupSorter->getTestsGroupedBySize( $this->getSuiteConfig(), $this->testNameToSize, - $lines + $time ); $this->suiteConfiguration = $this->parallelGroupSorter->getResultingSuiteConfig(); @@ -121,9 +121,9 @@ public function getSorter() * for the entry in order to generate a txt file used by devops for parllel execution in Jenkins. The results * are checked against a flattened list of suites in order to generate proper entries. * - * @param array $testGroup - * @param int $nodeNumber - * @param array $suites + * @param array $testGroup + * @param integer $nodeNumber + * @param array $suites * @return void */ private function generateGroupFile($testGroup, $nodeNumber, $suites) diff --git a/src/Magento/FunctionalTestingFramework/Util/Manifest/SingleRunTestManifest.php b/src/Magento/FunctionalTestingFramework/Util/Manifest/SingleRunTestManifest.php index 94004e3be..417fca686 100644 --- a/src/Magento/FunctionalTestingFramework/Util/Manifest/SingleRunTestManifest.php +++ b/src/Magento/FunctionalTestingFramework/Util/Manifest/SingleRunTestManifest.php @@ -13,7 +13,7 @@ class SingleRunTestManifest extends DefaultTestManifest /** * SingleRunTestManifest constructor. * - * @param array $suiteConfiguration + * @param array $suiteConfiguration * @param string $testPath */ public function __construct($suiteConfiguration, $testPath) diff --git a/src/Magento/FunctionalTestingFramework/Util/Manifest/TestManifestFactory.php b/src/Magento/FunctionalTestingFramework/Util/Manifest/TestManifestFactory.php index 51b34bc80..1cfa6559e 100644 --- a/src/Magento/FunctionalTestingFramework/Util/Manifest/TestManifestFactory.php +++ b/src/Magento/FunctionalTestingFramework/Util/Manifest/TestManifestFactory.php @@ -22,9 +22,9 @@ private function __construct() /** * Static function which takes path and config to return the appropriate manifest output type. * - * @param String $runConfig - * @param array $suiteConfiguration - * @param String $testPath + * @param string $runConfig + * @param array $suiteConfiguration + * @param string $testPath * @return BaseTestManifest */ public static function makeManifest($runConfig, $suiteConfiguration, $testPath = TestGenerator::DEFAULT_DIR) @@ -44,7 +44,6 @@ public static function makeManifest($runConfig, $suiteConfiguration, $testPath = default: return new DefaultTestManifest($suiteConfiguration, $testDirFullPath); - } } } diff --git a/src/Magento/FunctionalTestingFramework/Util/MetadataGenerator/FormData/MetadataGenUtil.php b/src/Magento/FunctionalTestingFramework/Util/MetadataGenerator/FormData/MetadataGenUtil.php index 821f598a7..2cd3f7011 100644 --- a/src/Magento/FunctionalTestingFramework/Util/MetadataGenerator/FormData/MetadataGenUtil.php +++ b/src/Magento/FunctionalTestingFramework/Util/MetadataGenerator/FormData/MetadataGenUtil.php @@ -123,7 +123,7 @@ private function appendParentParams($data) * Function which is called recursively to generate the mustache array for the template enging. Makes decisions * about type and format based on parameter array. * - * @param array $results + * @param array $results * @param string $defaultDataType * @return array */ diff --git a/src/Magento/FunctionalTestingFramework/Util/MetadataGenerator/Swagger/MetadataGenerator.php b/src/Magento/FunctionalTestingFramework/Util/MetadataGenerator/Swagger/MetadataGenerator.php index 64c981fe1..10d75bf8e 100644 --- a/src/Magento/FunctionalTestingFramework/Util/MetadataGenerator/Swagger/MetadataGenerator.php +++ b/src/Magento/FunctionalTestingFramework/Util/MetadataGenerator/Swagger/MetadataGenerator.php @@ -140,8 +140,8 @@ public function generateMetadataFromSwagger() * Render swagger operations. * * @param Operation $operation - * @param string $path - * @param string $method + * @param string $path + * @param string $method * @return void */ private function renderOperation($operation, $path, $method) @@ -182,7 +182,7 @@ private function renderOperation($operation, $path, $method) /** * Render swagger definitions. * - * @param string $defKey + * @param string $defKey * @param ObjectSchema|ArraySchema $definition * @return void */ @@ -232,9 +232,9 @@ private function renderDefinition($defKey, $definition) * Parse schema and return an array that will be consumed by mustache template engine. * * @param SchemaInterface $schema - * @param string $name - * @param bool $forArray - * @param integer $depth + * @param string $name + * @param boolean $forArray + * @param integer $depth * @return array */ private function parseSchema($schema, $name, $forArray, $depth) @@ -285,7 +285,7 @@ private function parseSchema($schema, $name, $forArray, $depth) * Parse params for an operation. * * @param ArrayCollection $params - * @param string $path + * @param string $path * @return void */ private function parseParams($params, $path) @@ -348,7 +348,7 @@ private function setBodyParams($param) * Set path params for an operation. * * @param AbstractTypedParameter $param - * @param string $path + * @param string $path * @return void */ private function setPathParams($param, $path) @@ -406,7 +406,7 @@ private function initMustacheTemplates() * @param string $relativeDir * @param string $fileName * @param string $template - * @param array $data + * @param array $data * @return void */ private function generateMetaDataFile($relativeDir, $fileName, $template, $data) diff --git a/src/Magento/FunctionalTestingFramework/Util/ModulePathExtractor.php b/src/Magento/FunctionalTestingFramework/Util/ModulePathExtractor.php new file mode 100644 index 000000000..33e559ea8 --- /dev/null +++ b/src/Magento/FunctionalTestingFramework/Util/ModulePathExtractor.php @@ -0,0 +1,51 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\FunctionalTestingFramework\Util; + +/** + * Class ModulePathExtractor, resolve module reference based on path + */ +class ModulePathExtractor +{ + const MAGENTO = 'Magento'; + + /** + * 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) { + 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]; + } + + /** + * Extracts the extension form the path, Magento for dev/tests/acceptance, [name] before module otherwise + * @param string $path + * @return string + */ + public function getExtensionPath($path) + { + $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]; + } + return self::MAGENTO; + } +} diff --git a/src/Magento/FunctionalTestingFramework/Util/ModuleResolver.php b/src/Magento/FunctionalTestingFramework/Util/ModuleResolver.php index f3a6eb6e6..0a5974387 100644 --- a/src/Magento/FunctionalTestingFramework/Util/ModuleResolver.php +++ b/src/Magento/FunctionalTestingFramework/Util/ModuleResolver.php @@ -7,6 +7,9 @@ namespace Magento\FunctionalTestingFramework\Util; use Magento\FunctionalTestingFramework\Config\MftfApplicationConfig; +use Magento\FunctionalTestingFramework\Exceptions\TestFrameworkException; +use Magento\FunctionalTestingFramework\Util\Logger\LoggingUtil; +use Symfony\Component\HttpFoundation\Response; /** * Class ModuleResolver, resolve module path based on enabled modules of target Magento instance. @@ -137,10 +140,6 @@ public function getEnabledModules() } $token = $this->getAdminToken(); - if (!$token || !is_string($token)) { - $this->enabledModules = []; - return $this->enabledModules; - } $url = ConfigSanitizerUtil::sanitizeUrl(getenv('MAGENTO_BASE_URL')) . $this->moduleUrl; @@ -156,10 +155,17 @@ public function getEnabledModules() $response = curl_exec($ch); if (!$response) { - $this->enabledModules = []; - } else { - $this->enabledModules = json_decode($response); + $message = "Could not retrieve Modules from Magento Instance."; + $context = [ + "Admin Module List Url" => $url, + "MAGENTO_ADMIN_USERNAME" => getenv("MAGENTO_ADMIN_USERNAME"), + "MAGENTO_ADMIN_PASSWORD" => getenv("MAGENTO_ADMIN_PASSWORD"), + ]; + throw new TestFrameworkException($message, $context); } + + $this->enabledModules = json_decode($response); + return $this->enabledModules; } @@ -189,23 +195,14 @@ public function getModulesPath() return $this->enabledModulePaths; } - $enabledModules = $this->getEnabledModules(); - if (empty($enabledModules) && !MftfApplicationConfig::getConfig()->forceGenerateEnabled()) { - trigger_error( - "Could not retrieve enabled modules from provided 'MAGENTO_BASE_URL'," . - "please make sure Magento is available at this url", - E_USER_ERROR - ); - } - $allModulePaths = $this->aggregateTestModulePaths(); - if (empty($enabledModules)) { + if (MftfApplicationConfig::getConfig()->forceGenerateEnabled()) { $this->enabledModulePaths = $this->applyCustomModuleMethods($allModulePaths); return $this->enabledModulePaths; } - $enabledModules = array_merge($enabledModules, $this->getModuleWhitelist()); + $enabledModules = array_merge($this->getEnabledModules(), $this->getModuleWhitelist()); $enabledDirectoryPaths = $this->getEnabledDirectoryPaths($enabledModules, $allModulePaths); $this->enabledModulePaths = $this->applyCustomModuleMethods($enabledDirectoryPaths); @@ -229,6 +226,7 @@ private function aggregateTestModulePaths() // Define the Module paths from default TESTS_MODULE_PATH $modulePath = defined('TESTS_MODULE_PATH') ? TESTS_MODULE_PATH : TESTS_BP; + $modulePath = rtrim($modulePath, DIRECTORY_SEPARATOR); // Define the Module paths from vendor modules $vendorCodePath = PROJECT_ROOT @@ -237,8 +235,8 @@ private function aggregateTestModulePaths() $codePathsToPattern = [ $modulePath => '', - $appCodePath => '/Test/Mftf', - $vendorCodePath => '/Test/Mftf' + $appCodePath => DIRECTORY_SEPARATOR . 'Test' . DIRECTORY_SEPARATOR . 'Mftf', + $vendorCodePath => DIRECTORY_SEPARATOR . 'Test' . DIRECTORY_SEPARATOR . 'Mftf' ]; foreach ($codePathsToPattern as $codePath => $pattern) { @@ -263,17 +261,36 @@ private function globRelevantPaths($testPath, $pattern) $relevantPaths = []; if (file_exists($testPath)) { - $relevantPaths = glob($testPath . '*/*' . $pattern); + $relevantPaths = $this->globRelevantWrapper($testPath, $pattern); } foreach ($relevantPaths as $codePath) { $mainModName = basename(str_replace($pattern, '', $codePath)); $modulePaths[$mainModName][] = $codePath; + + if (MftfApplicationConfig::getConfig()->verboseEnabled()) { + LoggingUtil::getInstance()->getLogger(ModuleResolver::class)->debug( + "including module", + ['module' => $mainModName, 'path' => $codePath] + ); + } } return $modulePaths; } + /** + * Glob wrapper for globRelevantPaths function + * + * @param string $testPath + * @param string $pattern + * @return array + */ + private static function globRelevantWrapper($testPath, $pattern) + { + return glob($testPath . '*' . DIRECTORY_SEPARATOR . '*' . $pattern); + } + /** * Takes a multidimensional array of module paths and flattens to return a one dimensional array of test paths * @@ -302,9 +319,15 @@ private function getEnabledDirectoryPaths($enabledModules, $allModulePaths) { $enabledDirectoryPaths = []; foreach ($enabledModules as $magentoModuleName) { - $moduleShortName = explode('_', $magentoModuleName)[1]; + // Magento_Backend -> Backend or DevDocs -> DevDocs (if whitelisted has no underscore) + $moduleShortName = explode('_', $magentoModuleName)[1] ?? $magentoModuleName; if (!isset($this->knownDirectories[$moduleShortName]) && !isset($allModulePaths[$moduleShortName])) { continue; + } elseif (isset($this->knownDirectories[$moduleShortName]) && !isset($allModulePaths[$moduleShortName])) { + LoggingUtil::getInstance()->getLogger(ModuleResolver::class)->warn( + "Known directory could not match to an existing path.", + ['knownDirectory' => $moduleShortName] + ); } else { $enabledDirectoryPaths[$moduleShortName] = $allModulePaths[$moduleShortName]; } @@ -319,12 +342,14 @@ private function getEnabledDirectoryPaths($enabledModules, $allModulePaths) */ private function printMagentoVersionInfo() { - if (MftfApplicationConfig::getConfig()->forceGenerateEnabled()) { return; } $url = ConfigSanitizerUtil::sanitizeUrl(getenv('MAGENTO_BASE_URL')) . $this->versionUrl; - print "Fetching version information from {$url}\n"; + LoggingUtil::getInstance()->getLogger(ModuleResolver::class)->info( + "Fetching version information.", + ['url' => $url] + ); $ch = curl_init($url); curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "GET"); @@ -335,22 +360,29 @@ private function printMagentoVersionInfo() $response = "No version information available."; } - if (MftfApplicationConfig::getConfig()->verboseEnabled()) { - print "\nVersion Information: {$response}\n"; - } + LoggingUtil::getInstance()->getLogger(ModuleResolver::class)->info( + 'version information', + ['version' => $response] + ); } /** * Get the API token for admin. * - * @return string|bool + * @return string|boolean */ protected function getAdminToken() { $login = $_ENV['MAGENTO_ADMIN_USERNAME'] ?? null; $password = $_ENV['MAGENTO_ADMIN_PASSWORD'] ?? null; if (!$login || !$password || !isset($_ENV['MAGENTO_BASE_URL'])) { - return false; + $message = "Cannot retrieve API token without credentials and base url, please fill out .env."; + $context = [ + "MAGENTO_BASE_URL" => getenv("MAGENTO_BASE_URL"), + "MAGENTO_ADMIN_USERNAME" => getenv("MAGENTO_ADMIN_USERNAME"), + "MAGENTO_ADMIN_PASSWORD" => getenv("MAGENTO_ADMIN_PASSWORD"), + ]; + throw new TestFrameworkException($message, $context); } $url = ConfigSanitizerUtil::sanitizeUrl($_ENV['MAGENTO_BASE_URL']) . $this->adminTokenUrl; @@ -371,9 +403,25 @@ protected function getAdminToken() curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); $response = curl_exec($ch); - if (!$response) { - return $response; + $responseCode = curl_getinfo($ch)['http_code']; + + if ($responseCode !== 200) { + if ($responseCode == 0) { + $details = "Could not find Magento Instance at given MAGENTO_BASE_URL"; + } else { + $details = $responseCode . " " . Response::$statusTexts[$responseCode]; + } + + $message = "Could not retrieve API token from Magento Instance ({$details})"; + $context = [ + "tokenUrl" => $url, + "responseCode" => $responseCode, + "MAGENTO_ADMIN_USERNAME" => getenv("MAGENTO_ADMIN_USERNAME"), + "MAGENTO_ADMIN_PASSWORD" => getenv("MAGENTO_ADMIN_PASSWORD"), + ]; + throw new TestFrameworkException($message, $context); } + return json_decode($response); } @@ -400,7 +448,10 @@ protected function applyCustomModuleMethods($modulesPath) $customModulePaths = $this->getCustomModulePaths(); array_map(function ($value) { - print "Including module path: {$value}\n"; + LoggingUtil::getInstance()->getLogger(ModuleResolver::class)->info( + "including custom module", + ['module' => $value] + ); }, $customModulePaths); return $this->flattenAllModulePaths(array_merge($modulePathsResult, $customModulePaths)); @@ -418,7 +469,10 @@ private function removeBlacklistModules($modulePaths) foreach ($modulePathsResult as $moduleName => $modulePath) { if (in_array($moduleName, $this->getModuleBlacklist())) { unset($modulePathsResult[$moduleName]); - print "Excluding module: {$moduleName}\n"; + LoggingUtil::getInstance()->getLogger(ModuleResolver::class)->info( + "excluding module", + ['module' => $moduleName] + ); } } diff --git a/src/Magento/FunctionalTestingFramework/Util/Protocol/CurlInterface.php b/src/Magento/FunctionalTestingFramework/Util/Protocol/CurlInterface.php index 06d843fea..171fd01e6 100644 --- a/src/Magento/FunctionalTestingFramework/Util/Protocol/CurlInterface.php +++ b/src/Magento/FunctionalTestingFramework/Util/Protocol/CurlInterface.php @@ -22,8 +22,8 @@ interface CurlInterface /** * Add additional option to cURL. * - * @param int $option - * @param int|string|bool|array $value + * @param integer $option + * @param integer|string|boolean|array $value * @return $this */ public function addOption($option, $value); @@ -31,10 +31,10 @@ public function addOption($option, $value); /** * Send request to the remote server. * - * @param string $url + * @param string $url * @param array|string $body - * @param string $method - * @param array $headers + * @param string $method + * @param array $headers * @return void */ public function write($url, $body = [], $method = CurlInterface::POST, $headers = []); diff --git a/src/Magento/FunctionalTestingFramework/Util/Protocol/CurlTransport.php b/src/Magento/FunctionalTestingFramework/Util/Protocol/CurlTransport.php index 4372efadd..5a9133a05 100644 --- a/src/Magento/FunctionalTestingFramework/Util/Protocol/CurlTransport.php +++ b/src/Magento/FunctionalTestingFramework/Util/Protocol/CurlTransport.php @@ -92,8 +92,8 @@ public function setOptions(array $options = []) /** * Add additional option to cURL. * - * @param int $option - * @param int|string|bool|array $value + * @param integer $option + * @param integer|string|boolean|array $value * @return $this */ public function addOption($option, $value) @@ -117,10 +117,10 @@ public function setConfig(array $config = []) /** * Send request to the remote server. * - * @param string $url + * @param string $url * @param array|string $body - * @param string $method - * @param array $headers + * @param string $method + * @param array $headers * @return void * @throws TestFrameworkException */ @@ -208,7 +208,7 @@ protected function getResource() /** * Get last error number. * - * @return int + * @return integer */ public function getErrno() { @@ -228,7 +228,7 @@ public function getError() /** * Get information regarding a specific transfer. * - * @param int $opt CURLINFO option + * @param integer $opt CURLINFO option. * @return string|array */ public function getInfo($opt = 0) @@ -278,7 +278,7 @@ public function multiRequest(array $urls, array $options = []) * Extract the response code from a response string. * * @param string $responseStr - * @return int + * @return integer */ public static function extractCode($responseStr) { diff --git a/src/Magento/FunctionalTestingFramework/Util/Sorter/ParallelGroupSorter.php b/src/Magento/FunctionalTestingFramework/Util/Sorter/ParallelGroupSorter.php index 555eb5244..8654d64a7 100644 --- a/src/Magento/FunctionalTestingFramework/Util/Sorter/ParallelGroupSorter.php +++ b/src/Magento/FunctionalTestingFramework/Util/Sorter/ParallelGroupSorter.php @@ -28,24 +28,24 @@ public function __construct() /** * Function which returns tests and suites split according to desired number of lines divded into groups. * - * @param array $suiteConfiguration - * @param array $testNameToSize - * @param integer $lines + * @param array $suiteConfiguration + * @param array $testNameToSize + * @param integer $time * @return array * @throws TestFrameworkException */ - public function getTestsGroupedBySize($suiteConfiguration, $testNameToSize, $lines) + public function getTestsGroupedBySize($suiteConfiguration, $testNameToSize, $time) { // we must have the lines argument in order to create the test groups - if ($lines == 0) { + if ($time == 0) { throw new TestFrameworkException( - "Please provide the argument '--lines' to the robo command in order to". + "Please provide the argument '--time' to the robo command in order to". " generate grouped tests manifests for a parallel execution" ); } $testGroups = []; - $splitSuiteNamesToTests = $this->createGroupsWithinSuites($suiteConfiguration, $lines); + $splitSuiteNamesToTests = $this->createGroupsWithinSuites($suiteConfiguration, $time); $splitSuiteNamesToSize = $this->getSuiteToSize($splitSuiteNamesToTests); $entriesForGeneration = array_merge($testNameToSize, $splitSuiteNamesToSize); arsort($entriesForGeneration); @@ -58,7 +58,7 @@ public function getTestsGroupedBySize($suiteConfiguration, $testNameToSize, $lin continue; } - $testGroup = $this->createTestGroup($lines, $testName, $testSize, $testNameToSizeForUse); + $testGroup = $this->createTestGroup($time, $testName, $testSize, $testNameToSizeForUse); $testGroups[$nodeNumber] = $testGroup; // unset the test which have been used. @@ -88,26 +88,28 @@ public function getResultingSuiteConfig() * a test to be used as a starting point, the size of a starting test, an array of tests available to be added to * the group. * - * @param integer $lineMaximum - * @param string $testName + * @param integer $timeMaximum + * @param string $testName * @param integer $testSize - * @param array $testNameToSizeForUse + * @param array $testNameToSizeForUse * @return array */ - private function createTestGroup($lineMaximum, $testName, $testSize, $testNameToSizeForUse) + private function createTestGroup($timeMaximum, $testName, $testSize, $testNameToSizeForUse) { $group[$testName] = $testSize; - if ($testSize < $lineMaximum) { - while (array_sum($group) < $lineMaximum && !empty($testNameToSizeForUse)) { + if ($testSize < $timeMaximum) { + while (array_sum($group) < $timeMaximum && !empty($testNameToSizeForUse)) { $groupSize = array_sum($group); - $lineGoal = $lineMaximum - $groupSize; + $lineGoal = $timeMaximum - $groupSize; $testNameForUse = $this->getClosestLineCount($testNameToSizeForUse, $lineGoal); - $testSizeForUse = $testNameToSizeForUse[$testNameForUse]; - unset($testNameToSizeForUse[$testNameForUse]); + if ($testNameToSizeForUse[$testNameForUse] < $lineGoal) { + $testSizeForUse = $testNameToSizeForUse[$testNameForUse]; + $group[$testNameForUse] = $testSizeForUse; + } - $group[$testNameForUse] = $testSizeForUse; + unset($testNameToSizeForUse[$testNameForUse]); } } @@ -118,7 +120,7 @@ private function createTestGroup($lineMaximum, $testName, $testSize, $testNameTo * Function which takes a group of available tests mapped to size and a desired number of lines matching with the * test of closest size and returning. * - * @param array $testGroup + * @param array $testGroup * @param integer $desiredValue * @return string */ @@ -127,8 +129,12 @@ private function getClosestLineCount($testGroup, $desiredValue) $winner = key($testGroup); $closestThreshold = $desiredValue; foreach ($testGroup as $testName => $testValue) { - $testThreshold = abs($desiredValue - $testValue); - if ($closestThreshold > $testThreshold) { + // find the difference between the desired value and test candidate for the group + $testThreshold = $desiredValue - $testValue; + + // if we see that the gap between the desired value is non-negative and lower than the current closest make + // the test the winner. + if ($closestThreshold > $testThreshold && $testThreshold > 0) { $closestThreshold = $testThreshold; $winner = $testName; } @@ -141,7 +147,7 @@ private function getClosestLineCount($testGroup, $desiredValue) * Function which takes an array of test names mapped to suite name and a size limitation for each group of tests. * The function divides suites that are over the specified limit and returns the resulting suites in an array. * - * @param array $suiteConfiguration + * @param array $suiteConfiguration * @param integer $lineLimit * @return array */ @@ -187,7 +193,7 @@ private function getSuiteNameToTestSize($suiteConfiguration) foreach ($test as $testName) { $suiteNameToTestSize[$suite][$testName] = TestObjectHandler::getInstance() ->getObject($testName) - ->getTestActionCount(); + ->getEstimatedDuration(); } } @@ -222,32 +228,32 @@ private function getSuiteToSize($suiteNamesToTests) * Input {suitename = 'sample', tests = ['test1' => 100,'test2' => 150, 'test3' => 300], linelimit = 275} * Result { ['sample_01' => ['test3' => 300], 'sample_02' => ['test2' => 150, 'test1' => 100]] } * - * @param string $suiteName - * @param array $tests - * @param integer $lineLimit + * @param string $suiteName + * @param array $tests + * @param integer $maxTime * @return array */ - private function splitTestSuite($suiteName, $tests, $lineLimit) + private function splitTestSuite($suiteName, $tests, $maxTime) { arsort($tests); - $split_suites = []; + $splitSuites = []; $availableTests = $tests; - $split_count = 0; + $splitCount = 0; foreach ($tests as $test => $size) { if (!array_key_exists($test, $availableTests)) { continue; } - $group = $this->createTestGroup($lineLimit, $test, $size, $availableTests); - $split_suites["{$suiteName}_${split_count}"] = $group; - $this->addSuiteToConfig($suiteName, "{$suiteName}_${split_count}", $group); + $group = $this->createTestGroup($maxTime, $test, $size, $availableTests); + $splitSuites["{$suiteName}_${splitCount}"] = $group; + $this->addSuiteToConfig($suiteName, "{$suiteName}_${splitCount}", $group); $availableTests = array_diff_key($availableTests, $group); - $split_count++; + $splitCount++; } - return $split_suites; + return $splitSuites; } /** @@ -257,7 +263,7 @@ private function splitTestSuite($suiteName, $tests, $lineLimit) * * @param string $originalSuiteName * @param string $newSuiteName - * @param array $tests + * @param array $tests * @return void */ private function addSuiteToConfig($originalSuiteName, $newSuiteName, $tests) diff --git a/src/Magento/FunctionalTestingFramework/Util/TestGenerator.php b/src/Magento/FunctionalTestingFramework/Util/TestGenerator.php index 211f010c0..af39f6ad1 100644 --- a/src/Magento/FunctionalTestingFramework/Util/TestGenerator.php +++ b/src/Magento/FunctionalTestingFramework/Util/TestGenerator.php @@ -6,6 +6,7 @@ namespace Magento\FunctionalTestingFramework\Util; +use Magento\FunctionalTestingFramework\DataGenerator\Handlers\CredentialStore; use Magento\FunctionalTestingFramework\DataGenerator\Objects\EntityDataObject; use Magento\FunctionalTestingFramework\Exceptions\TestReferenceException; use Magento\FunctionalTestingFramework\Suite\Handlers\SuiteObjectHandler; @@ -16,6 +17,7 @@ use Magento\FunctionalTestingFramework\DataGenerator\Handlers\DataObjectHandler; use Magento\FunctionalTestingFramework\Test\Objects\TestHookObject; use Magento\FunctionalTestingFramework\Test\Objects\TestObject; +use Magento\FunctionalTestingFramework\Util\Logger\LoggingUtil; use Magento\FunctionalTestingFramework\Util\Manifest\BaseTestManifest; use Magento\FunctionalTestingFramework\Util\Manifest\TestManifestFactory; use Magento\FunctionalTestingFramework\Test\Util\ActionObjectExtractor; @@ -63,16 +65,16 @@ class TestGenerator /** * Debug flag. * - * @var bool + * @var boolean */ private $debug; /** * TestGenerator constructor. * - * @param string $exportDir - * @param array $tests - * @param bool $debug + * @param string $exportDir + * @param array $tests + * @param boolean $debug */ private function __construct($exportDir, $tests, $debug = false) { @@ -92,9 +94,9 @@ private function __construct($exportDir, $tests, $debug = false) /** * Singleton method to retrieve Test Generator * - * @param string $dir - * @param array $tests - * @param bool $debug + * @param string $dir + * @param array $tests + * @param boolean $debug * @return TestGenerator */ public static function getInstance($dir = null, $tests = [], $debug = false) @@ -130,11 +132,10 @@ private function loadAllTestObjects($testsToIgnore) // them in the current context. $invalidTestObjects = array_intersect_key($this->tests, $testsToIgnore); if (!empty($invalidTestObjects)) { - $errorMsg = "Cannot reference the following tests for generation without accompanying suite:\n"; - array_walk($invalidTestObjects, function ($value, $key) use (&$errorMsg) { - $errorMsg.= "\t{$key}\n"; - }); - throw new TestReferenceException($errorMsg); + throw new TestReferenceException( + "Cannot reference test configuration for generation without accompanying suite.", + ['tests' => array_keys($invalidTestObjects)] + ); } return $this->tests; @@ -155,7 +156,7 @@ private function createCestFile($testPhp, $filename) $file = fopen($exportFilePath, 'w'); if (!$file) { - throw new \Exception("Could not open the file!"); + throw new \Exception("Could not open the file."); } fwrite($file, $testPhp); @@ -167,7 +168,7 @@ private function createCestFile($testPhp, $filename) * to the createCestFile function. * * @param BaseTestManifest $testManifest - * @param array $testsToIgnore + * @param array $testsToIgnore * @return void * @throws TestReferenceException * @throws \Exception @@ -209,7 +210,7 @@ private function assembleTestPhp($testObject) $hookPhp = $this->generateHooksPhp($testObject->getHooks()); $testsPhp = $this->generateTestPhp($testObject); } catch (TestReferenceException $e) { - throw new TestReferenceException($e->getMessage() . " in Test \"" . $testObject->getName() . "\""); + throw new TestReferenceException($e->getMessage() . "\n" . $testObject->getFilename()); } $cestPhp = "<?php\n"; @@ -229,7 +230,7 @@ private function assembleTestPhp($testObject) * Load ALL Test objects. Loop over and pass each to the assembleTestPhp function. * * @param BaseTestManifest $testManifest - * @param array $testsToIgnore + * @param array $testsToIgnore * @return array */ private function assembleAllTestPhp($testManifest, array $testsToIgnore) @@ -285,6 +286,7 @@ private function generateUseStatementsPhp() $useStatementsPhp .= "use Magento\FunctionalTestingFramework\DataGenerator\Handlers\DataObjectHandler;\n"; $useStatementsPhp .= "use Magento\FunctionalTestingFramework\DataGenerator\Persist\DataPersistenceHandler;\n"; $useStatementsPhp .= "use Magento\FunctionalTestingFramework\DataGenerator\Objects\EntityDataObject;\n"; + $useStatementsPhp .= "use Magento\FunctionalTestingFramework\DataGenerator\Handlers\CredentialStore;\n"; $useStatementsPhp .= "use \Codeception\Util\Locator;\n"; $allureStatements = [ @@ -308,7 +310,7 @@ private function generateUseStatementsPhp() /** * Generates Annotations PHP for given object, using given scope to determine indentation and additional output. * - * @param array $annotationsObject + * @param array $annotationsObject * @param boolean $isMethod * @return string */ @@ -347,7 +349,7 @@ private function generateAnnotationsPhp($annotationsObject, $isMethod = false) /** * Method which returns formatted method level annotation based on type and name(s). * - * @param string $annotationType + * @param string $annotationType * @param string|null $annotationName * @return null|string */ @@ -413,7 +415,6 @@ private function generateClassAnnotations($annotationType, $annotationName) $annotationToAppend = null; switch ($annotationType) { - case "title": $annotationToAppend = sprintf(" * @Title(\"%s\")\n", $annotationName[0]); break; @@ -446,9 +447,9 @@ private function generateClassAnnotations($annotationType, $annotationName) * statement to handle each unique action. At the bottom of the case statement there is a generic function that can * construct the PHP string for nearly half of all Codeception actions. * - * @param array $actionObjects - * @param array|bool $hookObject - * @param string $actor + * @param array $actionObjects + * @param array|boolean $hookObject + * @param string $actor * @return string * @throws TestReferenceException * @throws \Exception @@ -487,8 +488,10 @@ public function generateStepsPhp($actionObjects, $hookObject = false, $actor = " $dependentSelector = null; $visible = null; $command = null; + $arguments = null; $sortOrder = null; $storeCode = null; + $format = null; $assertExpected = null; $assertActual = null; @@ -500,7 +503,10 @@ public function generateStepsPhp($actionObjects, $hookObject = false, $actor = " $this->validateXmlAttributesMutuallyExclusive($stepKey, $actionObject->getType(), $customActionAttributes); if (isset($customActionAttributes['command'])) { - $command = $customActionAttributes['command']; + $command = $this->addUniquenessFunctionCall($customActionAttributes['command']); + } + if (isset($customActionAttributes['arguments'])) { + $arguments = $this->addUniquenessFunctionCall($customActionAttributes['arguments']); } if (isset($customActionAttributes['attribute'])) { @@ -526,6 +532,17 @@ public function generateStepsPhp($actionObjects, $hookObject = false, $actor = " $input = $this->addUniquenessFunctionCall($customActionAttributes['regex']); } + if (isset($customActionAttributes['date']) && isset($customActionAttributes['format'])) { + $input = $this->addUniquenessFunctionCall($customActionAttributes['date']); + if ($input === "") { + $input = "\"Now\""; + } + $format = $this->addUniquenessFunctionCall($customActionAttributes['format']); + if ($format === "") { + $format = "\"r\""; + } + } + if (isset($customActionAttributes['expected'])) { $assertExpected = $this->resolveValueByType( $customActionAttributes['expected'], @@ -601,6 +618,10 @@ public function generateStepsPhp($actionObjects, $hookObject = false, $actor = " // Argument must be a closure function, not a string. $function = trim($function, '"'); } + // turn $javaVariable => \$javaVariable but not {$mftfVariable} + if ($actionObject->getType() == "executeJS") { + $function = preg_replace('/(?<!{)(\$[\w\d_]+)/', '\\\\$1', $function); + } } if (isset($customActionAttributes['html'])) { @@ -747,7 +768,7 @@ public function generateStepsPhp($actionObjects, $hookObject = false, $actor = " $testSteps .= $contextSetter; $testSteps .= $deleteEntityFunctionCall; } else { - $url = $this->resolveEnvReferences([$url])[0]; + $url = $this->resolveAllRuntimeReferences([$url])[0]; $url = $this->resolveTestVariable([$url], null)[0]; $output = sprintf( "\t\t$%s->deleteEntityByUrl(%s);\n", @@ -1217,7 +1238,8 @@ public function generateStepsPhp($actionObjects, $hookObject = false, $actor = " $stepKey, $actor, $actionObject, - $this->wrapWithDoubleQuotes($command) + $command, + $arguments ); $testSteps .= sprintf( "\t\t$%s->comment(\$%s);\n", @@ -1232,6 +1254,19 @@ public function generateStepsPhp($actionObjects, $hookObject = false, $actor = " $argRef .= str_replace(ucfirst($fieldKey), "", $stepKey) . "Fields['{$fieldKey}'] = ${input};\n"; $testSteps .= $argRef; break; + case "generateDate": + $timezone = "America/Los_Angeles"; + if (isset($customActionAttributes['timezone'])) { + $timezone = $customActionAttributes['timezone']; + } + + $dateGenerateCode = "\t\t\$date = new \DateTime();\n"; + $dateGenerateCode .= "\t\t\$date->setTimestamp(strtotime({$input}));\n"; + $dateGenerateCode .= "\t\t\$date->setTimezone(new \DateTimeZone(\"{$timezone}\"));\n"; + $dateGenerateCode .= "\t\t\${$stepKey} = \$date->format({$format});\n"; + + $testSteps .= $dateGenerateCode; + break; default: $testSteps .= $this->wrapFunctionCall($actor, $actionObject, $selector, $input, $parameter); } @@ -1310,8 +1345,8 @@ private function trimVariableIfNeeded($input) /** * Replaces all matches into given outputArg with. Variable scope determined by delimiter given. * - * @param array $matches - * @param string &$outputArg + * @param array $matches + * @param string $outputArg * @param string $delimiter * @return void * @throws \Exception @@ -1368,7 +1403,7 @@ private function processQuoteBreaks($match, $argument, $replacement) * Replaces any occurrences of stepKeys in input, if they are found within the given actionGroup. * Necessary to allow for use of grab/createData actions in actionGroups. * @param string $input - * @param array $actionGroupOrigin + * @param array $actionGroupOrigin * @return string */ private function resolveStepKeyReferences($input, $actionGroupOrigin) @@ -1385,8 +1420,16 @@ private function resolveStepKeyReferences($input, $actionGroupOrigin) $testInvocationKey = ucfirst($actionGroupOrigin[ActionGroupObject::ACTION_GROUP_ORIGIN_TEST_REF]); foreach ($stepKeys as $stepKey) { - if (strpos($output, $stepKey)) { - $output = str_replace($stepKey, $stepKey . $testInvocationKey, $output); + // MQE-1011 + $stepKeyVarRef = "$" . $stepKey; + $classVarRef = "\$this->$stepKey"; + + if (strpos($output, $stepKeyVarRef) !== false) { + $output = str_replace($stepKeyVarRef, $stepKeyVarRef . $testInvocationKey, $output); + } + + if (strpos($output, $classVarRef) !== false) { + $output = str_replace($classVarRef, $classVarRef . $testInvocationKey, $output); } } return $output; @@ -1518,7 +1561,14 @@ private function generateTestPhp($test) $testAnnotations = $this->generateAnnotationsPhp($test->getAnnotations(), true); $dependencies = 'AcceptanceTester $I'; if ($test->isSkipped()) { - $steps = "\t\t" . '$scenario->skip("This test is skipped");' . "\n"; + $skipString = "This test is skipped due to the following issues:\\n"; + $issues = $test->getAnnotations()['skip'] ?? null; + if (isset($issues)) { + $skipString .= implode("\\n", $issues); + } else { + $skipString .= "No issues have been specified."; + } + $steps = "\t\t" . '$scenario->skip("' . $skipString . '");' . "\n"; $dependencies .= ', \Codeception\Scenario $scenario'; } else { try { @@ -1666,7 +1716,7 @@ private function wrapFunctionCall($actor, $action, ...$args) if (!is_array($args)) { $args = [$args]; } - $args = $this->resolveEnvReferences($args); + $args = $this->resolveAllRuntimeReferences($args); $args = $this->resolveTestVariable($args, $action->getActionOrigin()); $output .= implode(", ", array_filter($args, function($value) { return $value !== null; })) . ");\n"; return $output; @@ -1697,7 +1747,7 @@ private function wrapFunctionCallWithReturnValue($returnVariable, $actor, $actio if (!is_array($args)) { $args = [$args]; } - $args = $this->resolveEnvReferences($args); + $args = $this->resolveAllRuntimeReferences($args); $args = $this->resolveTestVariable($args, $action->getActionOrigin()); $output .= implode(", ", array_filter($args, function($value) { return $value !== null; })) . ");\n"; return $output; @@ -1706,22 +1756,22 @@ private function wrapFunctionCallWithReturnValue($returnVariable, $actor, $actio /** * Resolves {{_ENV.variable}} into getenv("variable") for test-runtime ENV referencing. - * @param array $args + * @param array $args + * @param string $regex + * @param string $func * @return array */ - private function resolveEnvReferences($args) + private function resolveRuntimeReference($args, $regex, $func) { - $envRegex = "/{{_ENV\.([\w]+)}}/"; - $newArgs = []; foreach ($args as $key => $arg) { - preg_match_all($envRegex, $arg, $matches); + preg_match_all($regex, $arg, $matches); if (!empty($matches[0])) { $fullMatch = $matches[0][0]; - $envVariable = $matches[1][0]; + $refVariable = $matches[1][0]; unset($matches); - $replacement = "getenv(\"{$envVariable}\")"; + $replacement = "{$func}(\"{$refVariable}\")"; $outputArg = $this->processQuoteBreaks($fullMatch, $arg, $replacement); $newArgs[$key] = $outputArg; @@ -1734,6 +1784,28 @@ private function resolveEnvReferences($args) return $newArgs; } + /** + * Takes a predefined list of potentially matching special paramts and they needed function replacement and performs + * replacements on the tests args. + * + * @param array $args + * @return array + */ + private function resolveAllRuntimeReferences($args) + { + $runtimeReferenceRegex = [ + "/{{_ENV\.([\w]+)}}/" => 'getenv', + "/{{_CREDS\.([\w]+)}}/" => 'CredentialStore::getInstance()->getSecret' + ]; + + $argResult = $args; + foreach ($runtimeReferenceRegex as $regex => $func) { + $argResult = $this->resolveRuntimeReference($argResult, $regex, $func); + } + + return $argResult; + } + /** * Validates parameter array format, making sure user has enclosed string with square brackets. * @@ -1786,7 +1858,7 @@ private function resolveValueByType($value, $type) * Convert input string to boolean equivalent. * * @param string $inStr - * @return bool|null + * @return boolean|null */ private function toBoolean($inStr) { @@ -1797,7 +1869,7 @@ private function toBoolean($inStr) * Convert input string to number equivalent. * * @param string $inStr - * @return int|float|null + * @return integer|float|null */ private function toNumber($inStr) { @@ -1826,7 +1898,7 @@ private function stripQuotes($inStr) * * @param string $key * @param string $tagName - * @param array $attributes + * @param array $attributes * @return void */ private function validateXmlAttributesMutuallyExclusive($key, $tagName, $attributes) @@ -1885,7 +1957,7 @@ private function validateXmlAttributesMutuallyExclusive($key, $tagName, $attribu * * @param string $key * @param string $tagName - * @param array $attributes + * @param array $attributes * @return void */ private function printRuleErrorToConsole($key, $tagName, $attributes) diff --git a/src/Magento/FunctionalTestingFramework/Util/Validation/DuplicateNodeValidationUtil.php b/src/Magento/FunctionalTestingFramework/Util/Validation/DuplicateNodeValidationUtil.php new file mode 100644 index 000000000..8508f1abe --- /dev/null +++ b/src/Magento/FunctionalTestingFramework/Util/Validation/DuplicateNodeValidationUtil.php @@ -0,0 +1,79 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\FunctionalTestingFramework\Util\Validation; + +use Magento\FunctionalTestingFramework\Exceptions\Collector\ExceptionCollector; +use Magento\FunctionalTestingFramework\Exceptions\XmlException; + +/** + * Class DuplicateNodeValidationUtil + * @package Magento\FunctionalTestingFramework\Util\Validation + */ +class DuplicateNodeValidationUtil +{ + /** + * Key to use as unique identifier in validation + * @var string + */ + private $uniqueKey; + + /** + * ExceptionColletor used to catch errors. + * @var ExceptionCollector + */ + private $exceptionCollector; + + /** + * DuplicateNodeValidationUtil constructor. + * @param string $uniqueKey + * @param ExceptionCollector $exceptionCollector + */ + public function __construct($uniqueKey, $exceptionCollector) + { + $this->uniqueKey = $uniqueKey; + $this->exceptionCollector = $exceptionCollector; + } + + /** + * Parses through parent's children to find and flag duplicate values in given uniqueKey. + * + * @param \DOMElement $parentNode + * @param string $filename + * @return void + */ + public function validateChildUniqueness(\DOMElement $parentNode, $filename) + { + $childNodes = $parentNode->childNodes; + $type = ucfirst($parentNode->tagName); + + $keyValues = []; + for ($i = 0; $i < $childNodes->length; $i++) { + $currentNode = $childNodes->item($i); + + if (!is_a($currentNode, \DOMElement::class)) { + continue; + } + + if ($currentNode->hasAttribute($this->uniqueKey)) { + $keyValues[] = $currentNode->getAttribute($this->uniqueKey); + } + } + + $withoutDuplicates = array_unique($keyValues); + + if (count($withoutDuplicates) != count($keyValues)) { + $duplicates = array_diff_assoc($keyValues, $withoutDuplicates); + $keyError = ""; + foreach ($duplicates as $duplicateKey => $duplicateValue) { + $keyError .= "\t{$this->uniqueKey}: {$duplicateValue} is used more than once.\n"; + } + + $errorMsg = "{$type} cannot use {$this->uniqueKey}s more than once.\t\n{$keyError}\tin file: {$filename}"; + $this->exceptionCollector->addError($filename, $errorMsg); + } + } +} diff --git a/src/Magento/FunctionalTestingFramework/XmlParser/PageParser.php b/src/Magento/FunctionalTestingFramework/XmlParser/PageParser.php index 0070dc08f..643370504 100644 --- a/src/Magento/FunctionalTestingFramework/XmlParser/PageParser.php +++ b/src/Magento/FunctionalTestingFramework/XmlParser/PageParser.php @@ -30,7 +30,7 @@ class PageParser implements ParserInterface /** * PageParser Constructor * @param ObjectManagerInterface $objectManager - * @param DataInterface $configData + * @param DataInterface $configData */ public function __construct(ObjectManagerInterface $objectManager, DataInterface $configData) { diff --git a/src/Magento/FunctionalTestingFramework/XmlParser/SectionParser.php b/src/Magento/FunctionalTestingFramework/XmlParser/SectionParser.php index 4a77b97b2..ff8f92c42 100644 --- a/src/Magento/FunctionalTestingFramework/XmlParser/SectionParser.php +++ b/src/Magento/FunctionalTestingFramework/XmlParser/SectionParser.php @@ -31,7 +31,7 @@ class SectionParser implements ParserInterface /** * SectionParser constructor. * @param ObjectManagerInterface $objectManager - * @param DataInterface $configData + * @param DataInterface $configData */ public function __construct(ObjectManagerInterface $objectManager, DataInterface $configData) { diff --git a/src/Magento/FunctionalTestingFramework/_bootstrap.php b/src/Magento/FunctionalTestingFramework/_bootstrap.php new file mode 100644 index 000000000..2e769fb8f --- /dev/null +++ b/src/Magento/FunctionalTestingFramework/_bootstrap.php @@ -0,0 +1,64 @@ +<?php +// @codingStandardsIgnoreFile +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +// define framework basepath for schema pathing +defined('FW_BP') || define('FW_BP', realpath(__DIR__ . '/../../../')); + +// get the root path of the project (we will always be installed under vendor) +$projectRootPath = substr(FW_BP, 0, strpos(FW_BP, DIRECTORY_SEPARATOR . 'vendor' . DIRECTORY_SEPARATOR)); + +if (empty($projectRootPath)) { + // If ProjectRootPath is empty, we are not under vendor and are executing standalone. + require_once (realpath(FW_BP . "/dev/tests/functional/_bootstrap.php")); + return; +} + +// set Magento_BP as Root_Project Path +define('PROJECT_ROOT', $projectRootPath); +defined('MAGENTO_BP') || define('MAGENTO_BP', realpath($projectRootPath)); + +// load .env (if it exists) +$envFilepath = realpath(MAGENTO_BP . '/dev/tests/acceptance/'); +if (file_exists($envFilepath . DIRECTORY_SEPARATOR . '.env')) { + $env = new \Dotenv\Loader($envFilepath . DIRECTORY_SEPARATOR . '.env'); + $env->load(); + + if (array_key_exists('TESTS_MODULE_PATH', $_ENV) xor array_key_exists('TESTS_BP', $_ENV)) { + throw new Exception( + 'You must define both parameters TESTS_BP and TESTS_MODULE_PATH or neither parameter' + ); + } + + foreach ($_ENV as $key => $var) { + defined($key) || define($key, $var); + } + + defined('MAGENTO_CLI_COMMAND_PATH') || define( + 'MAGENTO_CLI_COMMAND_PATH', + 'dev/tests/acceptance/utils/command.php' + ); + $env->setEnvironmentVariable('MAGENTO_CLI_COMMAND_PATH', MAGENTO_CLI_COMMAND_PATH); + + defined('MAGENTO_CLI_COMMAND_PARAMETER') || define('MAGENTO_CLI_COMMAND_PARAMETER', 'command'); + $env->setEnvironmentVariable('MAGENTO_CLI_COMMAND_PARAMETER', MAGENTO_CLI_COMMAND_PARAMETER); +} + +// TODO REMOVE THIS CODE ONCE WE HAVE STOPPED SUPPORTING dev/tests/acceptance PATH +// define TEST_PATH and TEST_MODULE_PATH +defined('TESTS_BP') || define('TESTS_BP', realpath(MAGENTO_BP . DIRECTORY_SEPARATOR . 'dev/tests/acceptance/')); + +$RELATIVE_TESTS_MODULE_PATH = '/tests/functional/Magento/FunctionalTest'; +defined('TESTS_MODULE_PATH') || define( + 'TESTS_MODULE_PATH', + realpath(TESTS_BP . $RELATIVE_TESTS_MODULE_PATH) +); + +// add the debug flag here +$debugMode = $_ENV['MFTF_DEBUG'] ?? false; +if (!(bool)$debugMode && extension_loaded('xdebug')) { + xdebug_disable(); +}