diff --git a/.env.example b/.env.example new file mode 100644 index 000000000..c04afaab8 --- /dev/null +++ b/.env.example @@ -0,0 +1,40 @@ +#Copyright © Magento, Inc. All rights reserved. +#See COPYING.txt for license details. + +#*** Set the base URL for your Magento instance ***# +MAGENTO_BASE_URL=http://devdocs.magento.com/ + +#*** Set the Admin Username and Password for your Magento instance ***# +MAGENTO_BACKEND_NAME=admin +MAGENTO_ADMIN_USERNAME=admin +MAGENTO_ADMIN_PASSWORD=123123q + +#*** Path to CLI entry point and command parameter name. Uncomment and change if folder structure differs from standard Magento installation +#MAGENTO_CLI_COMMAND_PATH=dev/tests/acceptance/utils/command.php +#MAGENTO_CLI_COMMAND_PARAMETER=command + +#*** Selenium Server Protocol, Host, Port, and Path, with local defaults. Uncomment and change if not running Selenium locally. +#SELENIUM_HOST=127.0.0.1 +#SELENIUM_PORT=4444 +#SELENIUM_PROTOCOL=http +#SELENIUM_PATH=/wd/hub + +#*** Browser for running tests, default chrome. Uncomment and change if you want to run tests on another browser (ex. firefox). +BROWSER=chrome + +#*** Uncomment and set host & port if your dev environment needs different value other than MAGENTO_BASE_URL for Rest API Requests ***# +#MAGENTO_RESTAPI_SERVER_HOST= +#MAGENTO_RESTAPI_SERVER_PORT= + +#*** Uncomment these properties to set up a dev environment with symlinked projects ***# +#TESTS_BP= +#FW_BP= +#TESTS_MODULE_PATH= + +#*** These properties impact the modules loaded into MFTF, you can point to your own full path, or a custom set of modules located with the core set +MODULE_WHITELIST=Magento_Framework,Magento_ConfigurableProductWishlist,Magento_ConfigurableProductCatalogSearch +#CUSTOM_MODULE_PATHS= + +#*** Bool property which allows the user to toggle debug output during test execution +#MFTF_DEBUG= +#*** End of .env ***# diff --git a/.gitignore b/.gitignore index c3c8d97b0..da2119a16 100755 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,6 @@ build/* clover.xml coverage/ .vscode +codeception.yml +dev/tests/functional/MFTF.suite.yml +dev/tests/functional/_output \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 805595c1a..8d403f911 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,56 @@ Magento Functional Testing Framework Changelog ================================================ +2.2.0 +----- +### Enhancements +* Traceability + * Javascript errors are now logged and reported in test output. + * Test failures are no longer overwritten by failures in an `` hook. + * Tests will no longer execute an `` hook twice if a failure triggered in the `` hook. + * Tests marked with `` will now appear in generated Allure reports. + * Along with the above, the `robo group` command no longer omits the `skip` group (skipped tests are picked up but not fully executed). +* Modularity + * MFTF no longer relies on relative pathing to determine its path to tests or Magento (favoring composer information if available). + * Tests and test materials are now read in from Magento modules as well as extensions in addition to `dev/tests/acceptance`. + * See DevDocs `Getting Started` for details on expected paths and merge order. +* Customizability + * Creation of Suites is now supported + * `` can include tests via `name`, module, or `` tags. + * Consolidation of preconditions can be achieved via use of `` tags in a `` + * All normal test actions are supported + * Data returned from actions is not available for reference in subsequent tests (`createData` or `grab` actions). + * `robo generate:tests` generates all suites and tests, and can be given a JSON configuration to generate specific test/suites. + * See MFTF Devdocs "Suite" page for more details. + * `` may now be called against a `url` instead of a stepKey reference. + * `` may now be given an additional `x/y` offset. + * `` now returns a variable based on what the executed script returns. + * Added `` `type="block"`. + * `` elements may now be blank (contain no child sections). +* Maintainability + * `robo generate:tests --config parallel` now accepts a `--lines` argument, for grouping and sorting based on test length. + * `robo generate:tests` now checks for: + * Duplicate step keys within an `actionGroup`. + * Ambiguous or invalid `stepKey` references (in merge files). + * `robo generate:tests` now suppresses warnings by default. The command now accepts a `--verbose` flag to show full output including warnings. + +### Fixes +* Exception message for the `` action now correctly references the `selector` given. +* Usage of multiple parameterized elements in a `selector` now correctly resolves all element references. +* Usage of multiple uniqueness references on the same entity now generate correctly. +* Persisted entity references are correctly interpolated with `` url of `type="admin"`. +* Metadata that contains 2 or more params in its `url` now correctly resolve parameters. +* Arguments can now be passed to `x` and `y` attributes in `actionGroup`. +* Arguments can now be passed to nested `` action elements. +* The `` action can now be used to assert against empty strings. +* Empty `` elements within an `` now generate correctly. +* Mapping of the `` to the custom command has been fixed. + +### GitHub Issues/Pull requests: +* [#89](https://github.com/magento/magento2-functional-testing-framework/pull/89) -- Add ability to use array entities as arguments. +* [#68](https://github.com/magento/magento2-functional-testing-framework/issues/68) -- Excessive double quotes are being generated in WaitForElementChange method arguments (fixed in [#103](https://github.com/magento/magento2-functional-testing-framework/pull/103)) +* [#31](https://github.com/magento/magento2-functional-testing-framework/issues/31) -- Can't run tests without a store having "default" store code (fixed in [#86](https://github.com/magento/magento2-functional-testing-framework/pull/86)) + 2.1.2 ----- ### Enhancements diff --git a/RoboFile.php b/RoboFile.php new file mode 100644 index 000000000..c0af4c72c --- /dev/null +++ b/RoboFile.php @@ -0,0 +1,214 @@ +_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/mftf b/bin/mftf new file mode 100755 index 000000000..623e3bc61 --- /dev/null +++ b/bin/mftf @@ -0,0 +1,30 @@ +#!/usr/bin/env php + +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->run(); +} catch (\Exception $e) { + while ($e) { + echo $e->getMessage(); + echo $e->getTraceAsString(); + echo "\n\n"; + $e = $e->getPrevious(); + } + exit(1); +} diff --git a/bin/static-checks b/bin/static-checks index f1f58016f..c711726e4 100755 --- a/bin/static-checks +++ b/bin/static-checks @@ -8,7 +8,7 @@ set -e echo "===============================" echo " CODE SNIFFER" echo "===============================" -vendor/bin/phpcs ./src --standard=./dev/tests/static/Magento +vendor/bin/phpcs ./src --standard=./dev/tests/static/Magento --ignore=src/Magento/FunctionalTestingFramework/Group,src/Magento/FunctionalTestingFramework/AcceptanceTester.php vendor/bin/phpcs ./dev/tests/unit --standard=./dev/tests/static/Magento vendor/bin/phpcs ./dev/tests/verification --standard=./dev/tests/static/Magento --ignore=dev/tests/verification/_generated echo "" @@ -22,7 +22,7 @@ echo "" echo "===============================" echo " MESS DETECTOR" echo "===============================" -vendor/bin/phpmd ./src text /dev/tests/static/Magento/CodeMessDetector/ruleset.xml --exclude _generated +vendor/bin/phpmd ./src text /dev/tests/static/Magento/CodeMessDetector/ruleset.xml --exclude _generated,src/Magento/FunctionalTestingFramework/Group,src/Magento/FunctionalTestingFramework/AcceptanceTester.php echo "" echo "===============================" diff --git a/bin/static-checks.bat b/bin/static-checks.bat index 70cd44ceb..0b9402094 100644 --- a/bin/static-checks.bat +++ b/bin/static-checks.bat @@ -5,7 +5,7 @@ @echo off @echo ===============================PHP CODE SNIFFER REPORT=============================== -call vendor\bin\phpcs .\src --standard=.\dev\tests\static\Magento +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 @@ -13,7 +13,7 @@ call vendor\bin\phpcs .\dev\tests\verification --standard=.\dev\tests\static\Mag call vendor\bin\phpcpd .\src @echo "===============================PHP MESS DETECTOR REPORT=============================== -vendor\bin\phpmd .\src text \dev\tests\static\Magento\CodeMessDetector\ruleset.xml --exclude _generated +vendor\bin\phpmd .\src text \dev\tests\static\Magento\CodeMessDetector\ruleset.xml --exclude _generated,src\Magento\FunctionalTestingFramework\Group,src\Magento\FunctionalTestingFramework\AcceptanceTester.php @echo ===============================MAGENTO COPYRIGHT REPORT=============================== echo msgbox "INFO:Copyright check currently not run as part of .bat implementation" > "%temp%\popup.vbs" diff --git a/bootstrap.php b/bootstrap.php new file mode 100644 index 000000000..7a708e8a8 --- /dev/null +++ b/bootstrap.php @@ -0,0 +1,8 @@ +0.2", "fzaninotto/faker": "^1.6", - "mustache/mustache": "~2.5" + "mustache/mustache": "~2.5", + "symfony/process": "^2.8 || ^3.1 || ^4.0", + "vlucas/phpdotenv": "^2.4" }, "require-dev": { "squizlabs/php_codesniffer": "1.5.3", @@ -30,7 +34,8 @@ }, "autoload": { "psr-4": { - "Magento\\FunctionalTestingFramework\\": "src/Magento/FunctionalTestingFramework" + "Magento\\FunctionalTestingFramework\\": "src/Magento/FunctionalTestingFramework", + "MFTF\\": "dev/tests/functional/MFTF" } }, "autoload-dev": { @@ -46,5 +51,6 @@ "hooks": { "pre-push": "bin/all-checks" } - } + }, + "bin": ["bin/mftf"] } diff --git a/composer.lock b/composer.lock new file mode 100644 index 000000000..de8728da1 --- /dev/null +++ b/composer.lock @@ -0,0 +1,5384 @@ +{ + "_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", + "This file is @generated automatically" + ], + "content-hash": "d394ebb149854c00f790da70a8f0a3f3", + "packages": [ + { + "name": "allure-framework/allure-codeception", + "version": "1.2.6", + "source": { + "type": "git", + "url": "https://github.com/allure-framework/allure-codeception.git", + "reference": "e2672e57e6839edb770fa5de5f2b3fcc47917aa1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/allure-framework/allure-codeception/zipball/e2672e57e6839edb770fa5de5f2b3fcc47917aa1", + "reference": "e2672e57e6839edb770fa5de5f2b3fcc47917aa1", + "shasum": "" + }, + "require": { + "allure-framework/allure-php-api": "~1.1.0", + "codeception/codeception": "~2.1", + "php": ">=5.4.0", + "symfony/filesystem": ">=2.6", + "symfony/finder": ">=2.6" + }, + "type": "library", + "autoload": { + "psr-0": { + "Yandex": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "Ivan Krutov", + "email": "vania-pooh@yandex-team.ru", + "role": "Developer" + } + ], + "description": "A Codeception adapter for Allure report.", + "homepage": "http://allure.qatools.ru/", + "keywords": [ + "allure", + "attachments", + "cases", + "codeception", + "report", + "steps", + "testing" + ], + "time": "2017-12-27T11:38:21+00:00" + }, + { + "name": "allure-framework/allure-php-api", + "version": "1.1.4", + "source": { + "type": "git", + "url": "https://github.com/allure-framework/allure-php-adapter-api.git", + "reference": "a462a0da121681577033e13c123b6cc4e89cdc64" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/allure-framework/allure-php-adapter-api/zipball/a462a0da121681577033e13c123b6cc4e89cdc64", + "reference": "a462a0da121681577033e13c123b6cc4e89cdc64", + "shasum": "" + }, + "require": { + "jms/serializer": ">=0.16.0", + "moontoast/math": ">=1.1.0", + "php": ">=5.4.0", + "phpunit/phpunit": ">=4.0.0", + "ramsey/uuid": ">=3.0.0", + "symfony/http-foundation": ">=2.0" + }, + "type": "library", + "autoload": { + "psr-0": { + "Yandex": [ + "src/", + "test/" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "Ivan Krutov", + "email": "vania-pooh@yandex-team.ru", + "role": "Developer" + } + ], + "description": "PHP API for Allure adapter", + "homepage": "http://allure.qatools.ru/", + "keywords": [ + "allure", + "api", + "php", + "report" + ], + "time": "2016-12-07T12:15:46+00:00" + }, + { + "name": "behat/gherkin", + "version": "v4.4.5", + "source": { + "type": "git", + "url": "https://github.com/Behat/Gherkin.git", + "reference": "5c14cff4f955b17d20d088dec1bde61c0539ec74" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Behat/Gherkin/zipball/5c14cff4f955b17d20d088dec1bde61c0539ec74", + "reference": "5c14cff4f955b17d20d088dec1bde61c0539ec74", + "shasum": "" + }, + "require": { + "php": ">=5.3.1" + }, + "require-dev": { + "phpunit/phpunit": "~4.5|~5", + "symfony/phpunit-bridge": "~2.7|~3", + "symfony/yaml": "~2.3|~3" + }, + "suggest": { + "symfony/yaml": "If you want to parse features, represented in YAML files" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.4-dev" + } + }, + "autoload": { + "psr-0": { + "Behat\\Gherkin": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Konstantin Kudryashov", + "email": "ever.zet@gmail.com", + "homepage": "http://everzet.com" + } + ], + "description": "Gherkin DSL parser for PHP 5.3", + "homepage": "http://behat.org/", + "keywords": [ + "BDD", + "Behat", + "Cucumber", + "DSL", + "gherkin", + "parser" + ], + "time": "2016-10-30T11:50:56+00:00" + }, + { + "name": "codeception/codeception", + "version": "2.3.9", + "source": { + "type": "git", + "url": "https://github.com/Codeception/Codeception.git", + "reference": "104f46fa0bde339f1bcc3a375aac21eb36e65a1e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Codeception/Codeception/zipball/104f46fa0bde339f1bcc3a375aac21eb36e65a1e", + "reference": "104f46fa0bde339f1bcc3a375aac21eb36e65a1e", + "shasum": "" + }, + "require": { + "behat/gherkin": "~4.4.0", + "codeception/stub": "^1.0", + "ext-json": "*", + "ext-mbstring": "*", + "facebook/webdriver": ">=1.1.3 <2.0", + "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", + "symfony/dom-crawler": ">=2.7 <5.0", + "symfony/event-dispatcher": ">=2.7 <5.0", + "symfony/finder": ">=2.7 <5.0", + "symfony/yaml": ">=2.7 <5.0" + }, + "require-dev": { + "codeception/specify": "~0.3", + "facebook/graph-sdk": "~5.3", + "flow/jsonpath": "~0.2", + "monolog/monolog": "~1.8", + "pda/pheanstalk": "~3.0", + "php-amqplib/php-amqplib": "~2.4", + "predis/predis": "^1.0", + "squizlabs/php_codesniffer": "~2.0", + "symfony/process": ">=2.7 <5.0", + "vlucas/phpdotenv": "^2.4.0" + }, + "suggest": { + "aws/aws-sdk-php": "For using AWS Auth in REST module and Queue module", + "codeception/phpbuiltinserver": "Start and stop PHP built-in web server for your tests", + "codeception/specify": "BDD-style code blocks", + "codeception/verify": "BDD-style assertions", + "flow/jsonpath": "For using JSONPath in REST module", + "league/factory-muffin": "For DataFactory module", + "league/factory-muffin-faker": "For Faker support in DataFactory module", + "phpseclib/phpseclib": "for SFTP option in FTP Module", + "stecman/symfony-console-completion": "For BASH autocompletion", + "symfony/phpunit-bridge": "For phpunit-bridge support" + }, + "bin": [ + "codecept" + ], + "type": "library", + "extra": { + "branch-alias": [] + }, + "autoload": { + "psr-4": { + "Codeception\\": "src\\Codeception", + "Codeception\\Extension\\": "ext" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Michael Bodnarchuk", + "email": "davert@mail.ua", + "homepage": "http://codegyre.com" + } + ], + "description": "BDD-style testing framework", + "homepage": "http://codeception.com/", + "keywords": [ + "BDD", + "TDD", + "acceptance testing", + "functional testing", + "unit testing" + ], + "time": "2018-02-26T23:29:41+00:00" + }, + { + "name": "codeception/stub", + "version": "1.0.2", + "source": { + "type": "git", + "url": "https://github.com/Codeception/Stub.git", + "reference": "95fb7a36b81890dd2e5163e7ab31310df6f1bb99" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Codeception/Stub/zipball/95fb7a36b81890dd2e5163e7ab31310df6f1bb99", + "reference": "95fb7a36b81890dd2e5163e7ab31310df6f1bb99", + "shasum": "" + }, + "require": { + "phpunit/phpunit-mock-objects": ">2.3 <7.0" + }, + "require-dev": { + "phpunit/phpunit": ">=4.8 <8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Codeception\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Flexible Stub wrapper for PHPUnit's Mock Builder", + "time": "2018-02-18T13:56:56+00:00" + }, + { + "name": "consolidation/annotated-command", + "version": "2.8.3", + "source": { + "type": "git", + "url": "https://github.com/consolidation/annotated-command.git", + "reference": "8f8f5da2ca06fbd3a85f7d551c49f844b7c59437" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/consolidation/annotated-command/zipball/8f8f5da2ca06fbd3a85f7d551c49f844b7c59437", + "reference": "8f8f5da2ca06fbd3a85f7d551c49f844b7c59437", + "shasum": "" + }, + "require": { + "consolidation/output-formatters": "^3.1.12", + "php": ">=5.4.0", + "psr/log": "^1", + "symfony/console": "^2.8|^3|^4", + "symfony/event-dispatcher": "^2.5|^3|^4", + "symfony/finder": "^2.5|^3|^4" + }, + "require-dev": { + "greg-1-anderson/composer-test-scenarios": "^1", + "phpunit/phpunit": "^4.8", + "satooshi/php-coveralls": "^1.0.2 | dev-master", + "squizlabs/php_codesniffer": "^2.7" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "Consolidation\\AnnotatedCommand\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Greg Anderson", + "email": "greg.1.anderson@greenknowe.org" + } + ], + "description": "Initialize Symfony Console commands from annotated command class methods.", + "time": "2018-02-23T16:32:04+00:00" + }, + { + "name": "consolidation/config", + "version": "1.0.9", + "source": { + "type": "git", + "url": "https://github.com/consolidation/config.git", + "reference": "34ca8d7c1ee60a7b591b10617114cf1210a2e92c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/consolidation/config/zipball/34ca8d7c1ee60a7b591b10617114cf1210a2e92c", + "reference": "34ca8d7c1ee60a7b591b10617114cf1210a2e92c", + "shasum": "" + }, + "require": { + "dflydev/dot-access-data": "^1.1.0", + "grasmash/expander": "^1", + "php": ">=5.4.0" + }, + "require-dev": { + "greg-1-anderson/composer-test-scenarios": "^1", + "phpunit/phpunit": "^4", + "satooshi/php-coveralls": "^1.0", + "squizlabs/php_codesniffer": "2.*", + "symfony/console": "^2.5|^3|^4", + "symfony/yaml": "^2.8.11|^3|^4" + }, + "suggest": { + "symfony/yaml": "Required to use Consolidation\\Config\\Loader\\YamlConfigLoader" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Consolidation\\Config\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Greg Anderson", + "email": "greg.1.anderson@greenknowe.org" + } + ], + "description": "Provide configuration services for a commandline tool.", + "time": "2017-12-22T17:28:19+00:00" + }, + { + "name": "consolidation/log", + "version": "1.0.5", + "source": { + "type": "git", + "url": "https://github.com/consolidation/log.git", + "reference": "dbc7c535f319a4a2d5a5077738f8eb7c10df8821" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/consolidation/log/zipball/dbc7c535f319a4a2d5a5077738f8eb7c10df8821", + "reference": "dbc7c535f319a4a2d5a5077738f8eb7c10df8821", + "shasum": "" + }, + "require": { + "php": ">=5.5.0", + "psr/log": "~1.0", + "symfony/console": "^2.8|^3|^4" + }, + "require-dev": { + "phpunit/phpunit": "4.*", + "satooshi/php-coveralls": "dev-master", + "squizlabs/php_codesniffer": "2.*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Consolidation\\Log\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Greg Anderson", + "email": "greg.1.anderson@greenknowe.org" + } + ], + "description": "Improved Psr-3 / Psr\\Log logger based on Symfony Console components.", + "time": "2017-11-29T01:44:16+00:00" + }, + { + "name": "consolidation/output-formatters", + "version": "3.2.0", + "source": { + "type": "git", + "url": "https://github.com/consolidation/output-formatters.git", + "reference": "da889e4bce19f145ca4ec5b1725a946f4eb625a9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/consolidation/output-formatters/zipball/da889e4bce19f145ca4ec5b1725a946f4eb625a9", + "reference": "da889e4bce19f145ca4ec5b1725a946f4eb625a9", + "shasum": "" + }, + "require": { + "php": ">=5.4.0", + "symfony/console": "^2.8|^3|^4", + "symfony/finder": "^2.5|^3|^4" + }, + "require-dev": { + "g-1-a/composer-test-scenarios": "^2", + "phpunit/phpunit": "^5.7.27", + "satooshi/php-coveralls": "^2", + "squizlabs/php_codesniffer": "^2.7", + "symfony/console": "3.2.3", + "symfony/var-dumper": "^2.8|^3|^4", + "victorjonsson/markdowndocs": "^1.3" + }, + "suggest": { + "symfony/var-dumper": "For using the var_dump formatter" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Consolidation\\OutputFormatters\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Greg Anderson", + "email": "greg.1.anderson@greenknowe.org" + } + ], + "description": "Format text by applying transformations provided by plug-in formatters.", + "time": "2018-03-20T15:18:32+00:00" + }, + { + "name": "consolidation/robo", + "version": "1.2.2", + "source": { + "type": "git", + "url": "https://github.com/consolidation/Robo.git", + "reference": "9ef2724f72feb017517a755564516dbde99e15e4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/consolidation/Robo/zipball/9ef2724f72feb017517a755564516dbde99e15e4", + "reference": "9ef2724f72feb017517a755564516dbde99e15e4", + "shasum": "" + }, + "require": { + "consolidation/annotated-command": "^2.8.2", + "consolidation/config": "^1.0.1", + "consolidation/log": "~1", + "consolidation/output-formatters": "^3.1.13", + "grasmash/yaml-expander": "^1.3", + "league/container": "^2.2", + "php": ">=5.5.0", + "symfony/console": "^2.8|^3|^4", + "symfony/event-dispatcher": "^2.5|^3|^4", + "symfony/filesystem": "^2.5|^3|^4", + "symfony/finder": "^2.5|^3|^4", + "symfony/process": "^2.5|^3|^4" + }, + "replace": { + "codegyre/robo": "< 1.0" + }, + "require-dev": { + "codeception/aspect-mock": "^1|^2.1.1", + "codeception/base": "^2.3.7", + "codeception/verify": "^0.3.2", + "goaop/framework": "~2.1.2", + "greg-1-anderson/composer-test-scenarios": "^1", + "natxet/cssmin": "3.0.4", + "patchwork/jsqueeze": "~2", + "pear/archive_tar": "^1.4.2", + "phpunit/php-code-coverage": "~2|~4", + "satooshi/php-coveralls": "^2", + "squizlabs/php_codesniffer": "^2.8" + }, + "suggest": { + "henrikbjorn/lurker": "For monitoring filesystem changes in taskWatch", + "natxet/CssMin": "For minifying CSS files in taskMinify", + "patchwork/jsqueeze": "For minifying JS files in taskMinify", + "pear/archive_tar": "Allows tar archives to be created and extracted in taskPack and taskExtract, respectively." + }, + "bin": [ + "robo" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev", + "dev-state": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Robo\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Davert", + "email": "davert.php@resend.cc" + } + ], + "description": "Modern task runner", + "time": "2018-02-28T01:03:54+00:00" + }, + { + "name": "container-interop/container-interop", + "version": "1.2.0", + "source": { + "type": "git", + "url": "https://github.com/container-interop/container-interop.git", + "reference": "79cbf1341c22ec75643d841642dd5d6acd83bdb8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/container-interop/container-interop/zipball/79cbf1341c22ec75643d841642dd5d6acd83bdb8", + "reference": "79cbf1341c22ec75643d841642dd5d6acd83bdb8", + "shasum": "" + }, + "require": { + "psr/container": "^1.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Interop\\Container\\": "src/Interop/Container/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Promoting the interoperability of container objects (DIC, SL, etc.)", + "homepage": "https://github.com/container-interop/container-interop", + "time": "2017-02-14T19:40:03+00:00" + }, + { + "name": "dflydev/dot-access-data", + "version": "v1.1.0", + "source": { + "type": "git", + "url": "https://github.com/dflydev/dflydev-dot-access-data.git", + "reference": "3fbd874921ab2c041e899d044585a2ab9795df8a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/dflydev/dflydev-dot-access-data/zipball/3fbd874921ab2c041e899d044585a2ab9795df8a", + "reference": "3fbd874921ab2c041e899d044585a2ab9795df8a", + "shasum": "" + }, + "require": { + "php": ">=5.3.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "psr-0": { + "Dflydev\\DotAccessData": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Dragonfly Development Inc.", + "email": "info@dflydev.com", + "homepage": "http://dflydev.com" + }, + { + "name": "Beau Simensen", + "email": "beau@dflydev.com", + "homepage": "http://beausimensen.com" + }, + { + "name": "Carlos Frutos", + "email": "carlos@kiwing.it", + "homepage": "https://github.com/cfrutos" + } + ], + "description": "Given a deep data structure, access data by dot notation.", + "homepage": "https://github.com/dflydev/dflydev-dot-access-data", + "keywords": [ + "access", + "data", + "dot", + "notation" + ], + "time": "2017-01-20T21:14:22+00:00" + }, + { + "name": "doctrine/annotations", + "version": "v1.4.0", + "source": { + "type": "git", + "url": "https://github.com/doctrine/annotations.git", + "reference": "54cacc9b81758b14e3ce750f205a393d52339e97" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/annotations/zipball/54cacc9b81758b14e3ce750f205a393d52339e97", + "reference": "54cacc9b81758b14e3ce750f205a393d52339e97", + "shasum": "" + }, + "require": { + "doctrine/lexer": "1.*", + "php": "^5.6 || ^7.0" + }, + "require-dev": { + "doctrine/cache": "1.*", + "phpunit/phpunit": "^5.7" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.4.x-dev" + } + }, + "autoload": { + "psr-4": { + "Doctrine\\Common\\Annotations\\": "lib/Doctrine/Common/Annotations" + } + }, + "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": "Docblock Annotations Parser", + "homepage": "http://www.doctrine-project.org", + "keywords": [ + "annotations", + "docblock", + "parser" + ], + "time": "2017-02-24T16:22:25+00:00" + }, + { + "name": "doctrine/collections", + "version": "v1.4.0", + "source": { + "type": "git", + "url": "https://github.com/doctrine/collections.git", + "reference": "1a4fb7e902202c33cce8c55989b945612943c2ba" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/collections/zipball/1a4fb7e902202c33cce8c55989b945612943c2ba", + "reference": "1a4fb7e902202c33cce8c55989b945612943c2ba", + "shasum": "" + }, + "require": { + "php": "^5.6 || ^7.0" + }, + "require-dev": { + "doctrine/coding-standard": "~0.1@dev", + "phpunit/phpunit": "^5.7" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.3.x-dev" + } + }, + "autoload": { + "psr-0": { + "Doctrine\\Common\\Collections\\": "lib/" + } + }, + "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": "Collections Abstraction library", + "homepage": "http://www.doctrine-project.org", + "keywords": [ + "array", + "collections", + "iterator" + ], + "time": "2017-01-03T10:49:41+00:00" + }, + { + "name": "doctrine/instantiator", + "version": "1.0.5", + "source": { + "type": "git", + "url": "https://github.com/doctrine/instantiator.git", + "reference": "8e884e78f9f0eb1329e445619e04456e64d8051d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/instantiator/zipball/8e884e78f9f0eb1329e445619e04456e64d8051d", + "reference": "8e884e78f9f0eb1329e445619e04456e64d8051d", + "shasum": "" + }, + "require": { + "php": ">=5.3,<8.0-DEV" + }, + "require-dev": { + "athletic/athletic": "~0.1.8", + "ext-pdo": "*", + "ext-phar": "*", + "phpunit/phpunit": "~4.0", + "squizlabs/php_codesniffer": "~2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Doctrine\\Instantiator\\": "src/Doctrine/Instantiator/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Marco Pivetta", + "email": "ocramius@gmail.com", + "homepage": "http://ocramius.github.com/" + } + ], + "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors", + "homepage": "https://github.com/doctrine/instantiator", + "keywords": [ + "constructor", + "instantiate" + ], + "time": "2015-06-14T21:17:01+00:00" + }, + { + "name": "doctrine/lexer", + "version": "v1.0.1", + "source": { + "type": "git", + "url": "https://github.com/doctrine/lexer.git", + "reference": "83893c552fd2045dd78aef794c31e694c37c0b8c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/lexer/zipball/83893c552fd2045dd78aef794c31e694c37c0b8c", + "reference": "83893c552fd2045dd78aef794c31e694c37c0b8c", + "shasum": "" + }, + "require": { + "php": ">=5.3.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-0": { + "Doctrine\\Common\\Lexer\\": "lib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "Base library for a lexer that can be used in Top-Down, Recursive Descent Parsers.", + "homepage": "http://www.doctrine-project.org", + "keywords": [ + "lexer", + "parser" + ], + "time": "2014-09-09T13:34:57+00:00" + }, + { + "name": "epfremme/swagger-php", + "version": "v2.0.0", + "source": { + "type": "git", + "url": "https://github.com/epfremmer/swagger-php.git", + "reference": "eee28a442b7e6220391ec953d3c9b936354f23bc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/epfremmer/swagger-php/zipball/eee28a442b7e6220391ec953d3c9b936354f23bc", + "reference": "eee28a442b7e6220391ec953d3c9b936354f23bc", + "shasum": "" + }, + "require": { + "doctrine/annotations": "^1.2", + "doctrine/collections": "^1.3", + "jms/serializer": "^1.1", + "php": ">=5.5", + "phpoption/phpoption": "^1.1", + "symfony/yaml": "^2.7|^3.1" + }, + "require-dev": { + "mockery/mockery": "^0.9.4", + "phpunit/phpunit": "~4.8|~5.0", + "satooshi/php-coveralls": "^1.0" + }, + "type": "package", + "autoload": { + "psr-4": { + "Epfremme\\Swagger\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Edward Pfremmer", + "email": "epfremme@nerdery.com" + } + ], + "description": "Library for parsing swagger documentation into PHP entities for use in testing and code generation", + "time": "2016-09-26T17:24:17+00:00" + }, + { + "name": "facebook/webdriver", + "version": "1.5.0", + "source": { + "type": "git", + "url": "https://github.com/facebook/php-webdriver.git", + "reference": "86b5ca2f67173c9d34340845dd690149c886a605" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/facebook/php-webdriver/zipball/86b5ca2f67173c9d34340845dd690149c886a605", + "reference": "86b5ca2f67173c9d34340845dd690149c886a605", + "shasum": "" + }, + "require": { + "ext-curl": "*", + "ext-zip": "*", + "php": "^5.6 || ~7.0", + "symfony/process": "^2.8 || ^3.1 || ^4.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^2.0", + "guzzle/guzzle": "^3.4.1", + "php-coveralls/php-coveralls": "^1.0.2", + "php-mock/php-mock-phpunit": "^1.1", + "phpunit/phpunit": "^5.7", + "sebastian/environment": "^1.3.4 || ^2.0 || ^3.0", + "squizlabs/php_codesniffer": "^2.6", + "symfony/var-dumper": "^3.3 || ^4.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-community": "1.5-dev" + } + }, + "autoload": { + "psr-4": { + "Facebook\\WebDriver\\": "lib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "description": "A PHP client for Selenium WebDriver", + "homepage": "https://github.com/facebook/php-webdriver", + "keywords": [ + "facebook", + "php", + "selenium", + "webdriver" + ], + "time": "2017-11-15T11:08:09+00:00" + }, + { + "name": "flow/jsonpath", + "version": "0.4.0", + "source": { + "type": "git", + "url": "https://github.com/FlowCommunications/JSONPath.git", + "reference": "f0222818d5c938e4ab668ab2e2c079bd51a27112" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/FlowCommunications/JSONPath/zipball/f0222818d5c938e4ab668ab2e2c079bd51a27112", + "reference": "f0222818d5c938e4ab668ab2e2c079bd51a27112", + "shasum": "" + }, + "require": { + "php": ">=5.4.0" + }, + "require-dev": { + "peekmo/jsonpath": "dev-master", + "phpunit/phpunit": "^4.0" + }, + "type": "library", + "autoload": { + "psr-0": { + "Flow\\JSONPath": "src/", + "Flow\\JSONPath\\Test": "tests/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Stephen Frank", + "email": "stephen@flowsa.com" + } + ], + "description": "JSONPath implementation for parsing, searching and flattening arrays", + "time": "2018-03-04T16:39:47+00:00" + }, + { + "name": "fzaninotto/faker", + "version": "v1.7.1", + "source": { + "type": "git", + "url": "https://github.com/fzaninotto/Faker.git", + "reference": "d3ed4cc37051c1ca52d22d76b437d14809fc7e0d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/fzaninotto/Faker/zipball/d3ed4cc37051c1ca52d22d76b437d14809fc7e0d", + "reference": "d3ed4cc37051c1ca52d22d76b437d14809fc7e0d", + "shasum": "" + }, + "require": { + "php": "^5.3.3 || ^7.0" + }, + "require-dev": { + "ext-intl": "*", + "phpunit/phpunit": "^4.0 || ^5.0", + "squizlabs/php_codesniffer": "^1.5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.8-dev" + } + }, + "autoload": { + "psr-4": { + "Faker\\": "src/Faker/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "François Zaninotto" + } + ], + "description": "Faker is a PHP library that generates fake data for you.", + "keywords": [ + "data", + "faker", + "fixtures" + ], + "time": "2017-08-15T16:48:10+00:00" + }, + { + "name": "grasmash/expander", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/grasmash/expander.git", + "reference": "95d6037344a4be1dd5f8e0b0b2571a28c397578f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/grasmash/expander/zipball/95d6037344a4be1dd5f8e0b0b2571a28c397578f", + "reference": "95d6037344a4be1dd5f8e0b0b2571a28c397578f", + "shasum": "" + }, + "require": { + "dflydev/dot-access-data": "^1.1.0", + "php": ">=5.4" + }, + "require-dev": { + "greg-1-anderson/composer-test-scenarios": "^1", + "phpunit/phpunit": "^4|^5.5.4", + "satooshi/php-coveralls": "^1.0.2|dev-master", + "squizlabs/php_codesniffer": "^2.7" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Grasmash\\Expander\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Matthew Grasmick" + } + ], + "description": "Expands internal property references in PHP arrays file.", + "time": "2017-12-21T22:14:55+00:00" + }, + { + "name": "grasmash/yaml-expander", + "version": "1.4.0", + "source": { + "type": "git", + "url": "https://github.com/grasmash/yaml-expander.git", + "reference": "3f0f6001ae707a24f4d9733958d77d92bf9693b1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/grasmash/yaml-expander/zipball/3f0f6001ae707a24f4d9733958d77d92bf9693b1", + "reference": "3f0f6001ae707a24f4d9733958d77d92bf9693b1", + "shasum": "" + }, + "require": { + "dflydev/dot-access-data": "^1.1.0", + "php": ">=5.4", + "symfony/yaml": "^2.8.11|^3|^4" + }, + "require-dev": { + "greg-1-anderson/composer-test-scenarios": "^1", + "phpunit/phpunit": "^4.8|^5.5.4", + "satooshi/php-coveralls": "^1.0.2|dev-master", + "squizlabs/php_codesniffer": "^2.7" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Grasmash\\YamlExpander\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Matthew Grasmick" + } + ], + "description": "Expands internal property references in a yaml file.", + "time": "2017-12-16T16:06:03+00:00" + }, + { + "name": "guzzlehttp/guzzle", + "version": "6.3.0", + "source": { + "type": "git", + "url": "https://github.com/guzzle/guzzle.git", + "reference": "f4db5a78a5ea468d4831de7f0bf9d9415e348699" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/f4db5a78a5ea468d4831de7f0bf9d9415e348699", + "reference": "f4db5a78a5ea468d4831de7f0bf9d9415e348699", + "shasum": "" + }, + "require": { + "guzzlehttp/promises": "^1.0", + "guzzlehttp/psr7": "^1.4", + "php": ">=5.5" + }, + "require-dev": { + "ext-curl": "*", + "phpunit/phpunit": "^4.0 || ^5.0", + "psr/log": "^1.0" + }, + "suggest": { + "psr/log": "Required for using the Log middleware" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "6.2-dev" + } + }, + "autoload": { + "files": [ + "src/functions_include.php" + ], + "psr-4": { + "GuzzleHttp\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + } + ], + "description": "Guzzle is a PHP HTTP client library", + "homepage": "http://guzzlephp.org/", + "keywords": [ + "client", + "curl", + "framework", + "http", + "http client", + "rest", + "web service" + ], + "time": "2017-06-22T18:50:49+00:00" + }, + { + "name": "guzzlehttp/promises", + "version": "v1.3.1", + "source": { + "type": "git", + "url": "https://github.com/guzzle/promises.git", + "reference": "a59da6cf61d80060647ff4d3eb2c03a2bc694646" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/promises/zipball/a59da6cf61d80060647ff4d3eb2c03a2bc694646", + "reference": "a59da6cf61d80060647ff4d3eb2c03a2bc694646", + "shasum": "" + }, + "require": { + "php": ">=5.5.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.4-dev" + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\Promise\\": "src/" + }, + "files": [ + "src/functions_include.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + } + ], + "description": "Guzzle promises library", + "keywords": [ + "promise" + ], + "time": "2016-12-20T10:07:11+00:00" + }, + { + "name": "guzzlehttp/psr7", + "version": "1.4.2", + "source": { + "type": "git", + "url": "https://github.com/guzzle/psr7.git", + "reference": "f5b8a8512e2b58b0071a7280e39f14f72e05d87c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/f5b8a8512e2b58b0071a7280e39f14f72e05d87c", + "reference": "f5b8a8512e2b58b0071a7280e39f14f72e05d87c", + "shasum": "" + }, + "require": { + "php": ">=5.4.0", + "psr/http-message": "~1.0" + }, + "provide": { + "psr/http-message-implementation": "1.0" + }, + "require-dev": { + "phpunit/phpunit": "~4.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.4-dev" + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\Psr7\\": "src/" + }, + "files": [ + "src/functions_include.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "Tobias Schultze", + "homepage": "https://github.com/Tobion" + } + ], + "description": "PSR-7 message implementation that also provides common utility methods", + "keywords": [ + "http", + "message", + "request", + "response", + "stream", + "uri", + "url" + ], + "time": "2017-03-20T17:10:46+00:00" + }, + { + "name": "jms/metadata", + "version": "1.6.0", + "source": { + "type": "git", + "url": "https://github.com/schmittjoh/metadata.git", + "reference": "6a06970a10e0a532fb52d3959547123b84a3b3ab" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/schmittjoh/metadata/zipball/6a06970a10e0a532fb52d3959547123b84a3b3ab", + "reference": "6a06970a10e0a532fb52d3959547123b84a3b3ab", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "require-dev": { + "doctrine/cache": "~1.0", + "symfony/cache": "~3.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.5.x-dev" + } + }, + "autoload": { + "psr-0": { + "Metadata\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "Johannes M. Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "Class/method/property metadata management in PHP", + "keywords": [ + "annotations", + "metadata", + "xml", + "yaml" + ], + "time": "2016-12-05T10:18:33+00:00" + }, + { + "name": "jms/parser-lib", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/schmittjoh/parser-lib.git", + "reference": "c509473bc1b4866415627af0e1c6cc8ac97fa51d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/schmittjoh/parser-lib/zipball/c509473bc1b4866415627af0e1c6cc8ac97fa51d", + "reference": "c509473bc1b4866415627af0e1c6cc8ac97fa51d", + "shasum": "" + }, + "require": { + "phpoption/phpoption": ">=0.9,<2.0-dev" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "psr-0": { + "JMS\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache2" + ], + "description": "A library for easily creating recursive-descent parsers.", + "time": "2012-11-18T18:08:43+00:00" + }, + { + "name": "jms/serializer", + "version": "1.11.0", + "source": { + "type": "git", + "url": "https://github.com/schmittjoh/serializer.git", + "reference": "e7c53477ff55c21d1b1db7d062edc050a24f465f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/schmittjoh/serializer/zipball/e7c53477ff55c21d1b1db7d062edc050a24f465f", + "reference": "e7c53477ff55c21d1b1db7d062edc050a24f465f", + "shasum": "" + }, + "require": { + "doctrine/annotations": "^1.0", + "doctrine/instantiator": "^1.0.3", + "jms/metadata": "~1.1", + "jms/parser-lib": "1.*", + "php": "^5.5|^7.0", + "phpcollection/phpcollection": "~0.1", + "phpoption/phpoption": "^1.1" + }, + "conflict": { + "twig/twig": "<1.12" + }, + "require-dev": { + "doctrine/orm": "~2.1", + "doctrine/phpcr-odm": "^1.3|^2.0", + "ext-pdo_sqlite": "*", + "jackalope/jackalope-doctrine-dbal": "^1.1.5", + "phpunit/phpunit": "^4.8|^5.0", + "propel/propel1": "~1.7", + "psr/container": "^1.0", + "symfony/dependency-injection": "^2.7|^3.3|^4.0", + "symfony/expression-language": "^2.6|^3.0", + "symfony/filesystem": "^2.1", + "symfony/form": "~2.1|^3.0", + "symfony/translation": "^2.1|^3.0", + "symfony/validator": "^2.2|^3.0", + "symfony/yaml": "^2.1|^3.0", + "twig/twig": "~1.12|~2.0" + }, + "suggest": { + "doctrine/cache": "Required if you like to use cache functionality.", + "doctrine/collections": "Required if you like to use doctrine collection types as ArrayCollection.", + "symfony/yaml": "Required if you'd like to serialize data to YAML format." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.11-dev" + } + }, + "autoload": { + "psr-0": { + "JMS\\Serializer": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "Asmir Mustafic", + "email": "goetas@gmail.com" + }, + { + "name": "Johannes M. Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "Library for (de-)serializing data of any complexity; supports XML, JSON, and YAML.", + "homepage": "http://jmsyst.com/libs/serializer", + "keywords": [ + "deserialization", + "jaxb", + "json", + "serialization", + "xml" + ], + "time": "2018-02-04T17:48:54+00:00" + }, + { + "name": "league/container", + "version": "2.4.1", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/container.git", + "reference": "43f35abd03a12977a60ffd7095efd6a7808488c0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/container/zipball/43f35abd03a12977a60ffd7095efd6a7808488c0", + "reference": "43f35abd03a12977a60ffd7095efd6a7808488c0", + "shasum": "" + }, + "require": { + "container-interop/container-interop": "^1.2", + "php": "^5.4.0 || ^7.0" + }, + "provide": { + "container-interop/container-interop-implementation": "^1.2", + "psr/container-implementation": "^1.0" + }, + "replace": { + "orno/di": "~2.0" + }, + "require-dev": { + "phpunit/phpunit": "4.*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-2.x": "2.x-dev", + "dev-1.x": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "League\\Container\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Phil Bennett", + "email": "philipobenito@gmail.com", + "homepage": "http://www.philipobenito.com", + "role": "Developer" + } + ], + "description": "A fast and intuitive dependency injection container.", + "homepage": "https://github.com/thephpleague/container", + "keywords": [ + "container", + "dependency", + "di", + "injection", + "league", + "provider", + "service" + ], + "time": "2017-05-10T09:20:27+00:00" + }, + { + "name": "moontoast/math", + "version": "1.1.2", + "source": { + "type": "git", + "url": "https://github.com/ramsey/moontoast-math.git", + "reference": "c2792a25df5cad4ff3d760dd37078fc5b6fccc79" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ramsey/moontoast-math/zipball/c2792a25df5cad4ff3d760dd37078fc5b6fccc79", + "reference": "c2792a25df5cad4ff3d760dd37078fc5b6fccc79", + "shasum": "" + }, + "require": { + "ext-bcmath": "*", + "php": ">=5.3.3" + }, + "require-dev": { + "jakub-onderka/php-parallel-lint": "^0.9.0", + "phpunit/phpunit": "^4.7|>=5.0 <5.4", + "satooshi/php-coveralls": "^0.6.1", + "squizlabs/php_codesniffer": "^2.3" + }, + "type": "library", + "autoload": { + "psr-4": { + "Moontoast\\Math\\": "src/Moontoast/Math/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "Ben Ramsey", + "email": "ben@benramsey.com", + "homepage": "https://benramsey.com" + } + ], + "description": "A mathematics library, providing functionality for large numbers", + "homepage": "https://github.com/ramsey/moontoast-math", + "keywords": [ + "bcmath", + "math" + ], + "time": "2017-02-16T16:54:46+00:00" + }, + { + "name": "mustache/mustache", + "version": "v2.12.0", + "source": { + "type": "git", + "url": "https://github.com/bobthecow/mustache.php.git", + "reference": "fe8fe72e9d580591854de404cc59a1b83ca4d19e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/bobthecow/mustache.php/zipball/fe8fe72e9d580591854de404cc59a1b83ca4d19e", + "reference": "fe8fe72e9d580591854de404cc59a1b83ca4d19e", + "shasum": "" + }, + "require": { + "php": ">=5.2.4" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "~1.11", + "phpunit/phpunit": "~3.7|~4.0|~5.0" + }, + "type": "library", + "autoload": { + "psr-0": { + "Mustache": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Justin Hileman", + "email": "justin@justinhileman.info", + "homepage": "http://justinhileman.com" + } + ], + "description": "A Mustache implementation in PHP.", + "homepage": "https://github.com/bobthecow/mustache.php", + "keywords": [ + "mustache", + "templating" + ], + "time": "2017-07-11T12:54:05+00:00" + }, + { + "name": "myclabs/deep-copy", + "version": "1.7.0", + "source": { + "type": "git", + "url": "https://github.com/myclabs/DeepCopy.git", + "reference": "3b8a3a99ba1f6a3952ac2747d989303cbd6b7a3e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/3b8a3a99ba1f6a3952ac2747d989303cbd6b7a3e", + "reference": "3b8a3a99ba1f6a3952ac2747d989303cbd6b7a3e", + "shasum": "" + }, + "require": { + "php": "^5.6 || ^7.0" + }, + "require-dev": { + "doctrine/collections": "^1.0", + "doctrine/common": "^2.6", + "phpunit/phpunit": "^4.1" + }, + "type": "library", + "autoload": { + "psr-4": { + "DeepCopy\\": "src/DeepCopy/" + }, + "files": [ + "src/DeepCopy/deep_copy.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Create deep copies (clones) of your objects", + "keywords": [ + "clone", + "copy", + "duplicate", + "object", + "object graph" + ], + "time": "2017-10-19T19:58:43+00:00" + }, + { + "name": "paragonie/random_compat", + "version": "v2.0.11", + "source": { + "type": "git", + "url": "https://github.com/paragonie/random_compat.git", + "reference": "5da4d3c796c275c55f057af5a643ae297d96b4d8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/paragonie/random_compat/zipball/5da4d3c796c275c55f057af5a643ae297d96b4d8", + "reference": "5da4d3c796c275c55f057af5a643ae297d96b4d8", + "shasum": "" + }, + "require": { + "php": ">=5.2.0" + }, + "require-dev": { + "phpunit/phpunit": "4.*|5.*" + }, + "suggest": { + "ext-libsodium": "Provides a modern crypto API that can be used to generate random bytes." + }, + "type": "library", + "autoload": { + "files": [ + "lib/random.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Paragon Initiative Enterprises", + "email": "security@paragonie.com", + "homepage": "https://paragonie.com" + } + ], + "description": "PHP 5.x polyfill for random_bytes() and random_int() from PHP 7", + "keywords": [ + "csprng", + "pseudorandom", + "random" + ], + "time": "2017-09-27T21:40:39+00:00" + }, + { + "name": "phar-io/manifest", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/phar-io/manifest.git", + "reference": "2df402786ab5368a0169091f61a7c1e0eb6852d0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/manifest/zipball/2df402786ab5368a0169091f61a7c1e0eb6852d0", + "reference": "2df402786ab5368a0169091f61a7c1e0eb6852d0", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-phar": "*", + "phar-io/version": "^1.0.1", + "php": "^5.6 || ^7.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", + "time": "2017-03-05T18:14:27+00:00" + }, + { + "name": "phar-io/version", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/phar-io/version.git", + "reference": "a70c0ced4be299a63d32fa96d9281d03e94041df" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/version/zipball/a70c0ced4be299a63d32fa96d9281d03e94041df", + "reference": "a70c0ced4be299a63d32fa96d9281d03e94041df", + "shasum": "" + }, + "require": { + "php": "^5.6 || ^7.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Library for handling version information and constraints", + "time": "2017-03-05T17:38:23+00:00" + }, + { + "name": "phpcollection/phpcollection", + "version": "0.5.0", + "source": { + "type": "git", + "url": "https://github.com/schmittjoh/php-collection.git", + "reference": "f2bcff45c0da7c27991bbc1f90f47c4b7fb434a6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/schmittjoh/php-collection/zipball/f2bcff45c0da7c27991bbc1f90f47c4b7fb434a6", + "reference": "f2bcff45c0da7c27991bbc1f90f47c4b7fb434a6", + "shasum": "" + }, + "require": { + "phpoption/phpoption": "1.*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "0.4-dev" + } + }, + "autoload": { + "psr-0": { + "PhpCollection": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache2" + ], + "authors": [ + { + "name": "Johannes M. Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "General-Purpose Collection Library for PHP", + "keywords": [ + "collection", + "list", + "map", + "sequence", + "set" + ], + "time": "2015-05-17T12:39:23+00:00" + }, + { + "name": "phpdocumentor/reflection-common", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/ReflectionCommon.git", + "reference": "21bdeb5f65d7ebf9f43b1b25d404f87deab5bfb6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/21bdeb5f65d7ebf9f43b1b25d404f87deab5bfb6", + "reference": "21bdeb5f65d7ebf9f43b1b25d404f87deab5bfb6", + "shasum": "" + }, + "require": { + "php": ">=5.5" + }, + "require-dev": { + "phpunit/phpunit": "^4.6" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": [ + "src" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jaap van Otterdijk", + "email": "opensource@ijaap.nl" + } + ], + "description": "Common reflection classes used by phpdocumentor to reflect the code structure", + "homepage": "http://www.phpdoc.org", + "keywords": [ + "FQSEN", + "phpDocumentor", + "phpdoc", + "reflection", + "static analysis" + ], + "time": "2017-09-11T18:02:19+00:00" + }, + { + "name": "phpdocumentor/reflection-docblock", + "version": "4.3.0", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", + "reference": "94fd0001232e47129dd3504189fa1c7225010d08" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/94fd0001232e47129dd3504189fa1c7225010d08", + "reference": "94fd0001232e47129dd3504189fa1c7225010d08", + "shasum": "" + }, + "require": { + "php": "^7.0", + "phpdocumentor/reflection-common": "^1.0.0", + "phpdocumentor/type-resolver": "^0.4.0", + "webmozart/assert": "^1.0" + }, + "require-dev": { + "doctrine/instantiator": "~1.0.5", + "mockery/mockery": "^1.0", + "phpunit/phpunit": "^6.4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.x-dev" + } + }, + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": [ + "src/" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mike van Riel", + "email": "me@mikevanriel.com" + } + ], + "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", + "time": "2017-11-30T07:14:17+00:00" + }, + { + "name": "phpdocumentor/type-resolver", + "version": "0.4.0", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/TypeResolver.git", + "reference": "9c977708995954784726e25d0cd1dddf4e65b0f7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/9c977708995954784726e25d0cd1dddf4e65b0f7", + "reference": "9c977708995954784726e25d0cd1dddf4e65b0f7", + "shasum": "" + }, + "require": { + "php": "^5.5 || ^7.0", + "phpdocumentor/reflection-common": "^1.0" + }, + "require-dev": { + "mockery/mockery": "^0.9.4", + "phpunit/phpunit": "^5.2||^4.8.24" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": [ + "src/" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mike van Riel", + "email": "me@mikevanriel.com" + } + ], + "time": "2017-07-14T14:27:02+00:00" + }, + { + "name": "phpoption/phpoption", + "version": "1.5.0", + "source": { + "type": "git", + "url": "https://github.com/schmittjoh/php-option.git", + "reference": "94e644f7d2051a5f0fcf77d81605f152eecff0ed" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/schmittjoh/php-option/zipball/94e644f7d2051a5f0fcf77d81605f152eecff0ed", + "reference": "94e644f7d2051a5f0fcf77d81605f152eecff0ed", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "require-dev": { + "phpunit/phpunit": "4.7.*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.3-dev" + } + }, + "autoload": { + "psr-0": { + "PhpOption\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache2" + ], + "authors": [ + { + "name": "Johannes M. Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "Option Type for PHP", + "keywords": [ + "language", + "option", + "php", + "type" + ], + "time": "2015-07-25T16:39:46+00:00" + }, + { + "name": "phpspec/prophecy", + "version": "1.7.5", + "source": { + "type": "git", + "url": "https://github.com/phpspec/prophecy.git", + "reference": "dfd6be44111a7c41c2e884a336cc4f461b3b2401" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpspec/prophecy/zipball/dfd6be44111a7c41c2e884a336cc4f461b3b2401", + "reference": "dfd6be44111a7c41c2e884a336cc4f461b3b2401", + "shasum": "" + }, + "require": { + "doctrine/instantiator": "^1.0.2", + "php": "^5.3|^7.0", + "phpdocumentor/reflection-docblock": "^2.0|^3.0.2|^4.0", + "sebastian/comparator": "^1.1|^2.0", + "sebastian/recursion-context": "^1.0|^2.0|^3.0" + }, + "require-dev": { + "phpspec/phpspec": "^2.5|^3.2", + "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.7.x-dev" + } + }, + "autoload": { + "psr-0": { + "Prophecy\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Konstantin Kudryashov", + "email": "ever.zet@gmail.com", + "homepage": "http://everzet.com" + }, + { + "name": "Marcello Duarte", + "email": "marcello.duarte@gmail.com" + } + ], + "description": "Highly opinionated mocking framework for PHP 5.3+", + "homepage": "https://github.com/phpspec/prophecy", + "keywords": [ + "Double", + "Dummy", + "fake", + "mock", + "spy", + "stub" + ], + "time": "2018-02-19T10:16:54+00:00" + }, + { + "name": "phpunit/php-code-coverage", + "version": "5.3.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-code-coverage.git", + "reference": "661f34d0bd3f1a7225ef491a70a020ad23a057a1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/661f34d0bd3f1a7225ef491a70a020ad23a057a1", + "reference": "661f34d0bd3f1a7225ef491a70a020ad23a057a1", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-xmlwriter": "*", + "php": "^7.0", + "phpunit/php-file-iterator": "^1.4.2", + "phpunit/php-text-template": "^1.2.1", + "phpunit/php-token-stream": "^2.0.1", + "sebastian/code-unit-reverse-lookup": "^1.0.1", + "sebastian/environment": "^3.0", + "sebastian/version": "^2.0.1", + "theseer/tokenizer": "^1.1" + }, + "require-dev": { + "phpunit/phpunit": "^6.0" + }, + "suggest": { + "ext-xdebug": "^2.5.5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.3.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", + "homepage": "https://github.com/sebastianbergmann/php-code-coverage", + "keywords": [ + "coverage", + "testing", + "xunit" + ], + "time": "2017-12-06T09:29:45+00:00" + }, + { + "name": "phpunit/php-file-iterator", + "version": "1.4.5", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-file-iterator.git", + "reference": "730b01bc3e867237eaac355e06a36b85dd93a8b4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/730b01bc3e867237eaac355e06a36b85dd93a8b4", + "reference": "730b01bc3e867237eaac355e06a36b85dd93a8b4", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.4.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sb@sebastian-bergmann.de", + "role": "lead" + } + ], + "description": "FilterIterator implementation that filters files based on a list of suffixes.", + "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", + "keywords": [ + "filesystem", + "iterator" + ], + "time": "2017-11-27T13:52:08+00:00" + }, + { + "name": "phpunit/php-text-template", + "version": "1.2.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-text-template.git", + "reference": "31f8b717e51d9a2afca6c9f046f5d69fc27c8686" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/31f8b717e51d9a2afca6c9f046f5d69fc27c8686", + "reference": "31f8b717e51d9a2afca6c9f046f5d69fc27c8686", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Simple template engine.", + "homepage": "https://github.com/sebastianbergmann/php-text-template/", + "keywords": [ + "template" + ], + "time": "2015-06-21T13:50:34+00:00" + }, + { + "name": "phpunit/php-timer", + "version": "1.0.9", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-timer.git", + "reference": "3dcf38ca72b158baf0bc245e9184d3fdffa9c46f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/3dcf38ca72b158baf0bc245e9184d3fdffa9c46f", + "reference": "3dcf38ca72b158baf0bc245e9184d3fdffa9c46f", + "shasum": "" + }, + "require": { + "php": "^5.3.3 || ^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sb@sebastian-bergmann.de", + "role": "lead" + } + ], + "description": "Utility class for timing", + "homepage": "https://github.com/sebastianbergmann/php-timer/", + "keywords": [ + "timer" + ], + "time": "2017-02-26T11:10:40+00:00" + }, + { + "name": "phpunit/php-token-stream", + "version": "2.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-token-stream.git", + "reference": "791198a2c6254db10131eecfe8c06670700904db" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/791198a2c6254db10131eecfe8c06670700904db", + "reference": "791198a2c6254db10131eecfe8c06670700904db", + "shasum": "" + }, + "require": { + "ext-tokenizer": "*", + "php": "^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^6.2.4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Wrapper around PHP's tokenizer extension.", + "homepage": "https://github.com/sebastianbergmann/php-token-stream/", + "keywords": [ + "tokenizer" + ], + "time": "2017-11-27T05:48:46+00:00" + }, + { + "name": "phpunit/phpunit", + "version": "6.5.7", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/phpunit.git", + "reference": "6bd77b57707c236833d2b57b968e403df060c9d9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/6bd77b57707c236833d2b57b968e403df060c9d9", + "reference": "6bd77b57707c236833d2b57b968e403df060c9d9", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-json": "*", + "ext-libxml": "*", + "ext-mbstring": "*", + "ext-xml": "*", + "myclabs/deep-copy": "^1.6.1", + "phar-io/manifest": "^1.0.1", + "phar-io/version": "^1.0", + "php": "^7.0", + "phpspec/prophecy": "^1.7", + "phpunit/php-code-coverage": "^5.3", + "phpunit/php-file-iterator": "^1.4.3", + "phpunit/php-text-template": "^1.2.1", + "phpunit/php-timer": "^1.0.9", + "phpunit/phpunit-mock-objects": "^5.0.5", + "sebastian/comparator": "^2.1", + "sebastian/diff": "^2.0", + "sebastian/environment": "^3.1", + "sebastian/exporter": "^3.1", + "sebastian/global-state": "^2.0", + "sebastian/object-enumerator": "^3.0.3", + "sebastian/resource-operations": "^1.0", + "sebastian/version": "^2.0.1" + }, + "conflict": { + "phpdocumentor/reflection-docblock": "3.0.2", + "phpunit/dbunit": "<3.0" + }, + "require-dev": { + "ext-pdo": "*" + }, + "suggest": { + "ext-xdebug": "*", + "phpunit/php-invoker": "^1.1" + }, + "bin": [ + "phpunit" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "6.5.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "The PHP Unit Testing framework.", + "homepage": "https://phpunit.de/", + "keywords": [ + "phpunit", + "testing", + "xunit" + ], + "time": "2018-02-26T07:01:09+00:00" + }, + { + "name": "phpunit/phpunit-mock-objects", + "version": "5.0.6", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/phpunit-mock-objects.git", + "reference": "33fd41a76e746b8fa96d00b49a23dadfa8334cdf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit-mock-objects/zipball/33fd41a76e746b8fa96d00b49a23dadfa8334cdf", + "reference": "33fd41a76e746b8fa96d00b49a23dadfa8334cdf", + "shasum": "" + }, + "require": { + "doctrine/instantiator": "^1.0.5", + "php": "^7.0", + "phpunit/php-text-template": "^1.2.1", + "sebastian/exporter": "^3.1" + }, + "conflict": { + "phpunit/phpunit": "<6.0" + }, + "require-dev": { + "phpunit/phpunit": "^6.5" + }, + "suggest": { + "ext-soap": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Mock Object library for PHPUnit", + "homepage": "https://github.com/sebastianbergmann/phpunit-mock-objects/", + "keywords": [ + "mock", + "xunit" + ], + "time": "2018-01-06T05:45:45+00:00" + }, + { + "name": "psr/container", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/container.git", + "reference": "b7ce3b176482dbbc1245ebf52b181af44c2cf55f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/container/zipball/b7ce3b176482dbbc1245ebf52b181af44c2cf55f", + "reference": "b7ce3b176482dbbc1245ebf52b181af44c2cf55f", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Container\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common Container Interface (PHP FIG PSR-11)", + "homepage": "https://github.com/php-fig/container", + "keywords": [ + "PSR-11", + "container", + "container-interface", + "container-interop", + "psr" + ], + "time": "2017-02-14T16:28:37+00:00" + }, + { + "name": "psr/http-message", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-message.git", + "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-message/zipball/f6561bf28d520154e4b0ec72be95418abe6d9363", + "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP messages", + "homepage": "https://github.com/php-fig/http-message", + "keywords": [ + "http", + "http-message", + "psr", + "psr-7", + "request", + "response" + ], + "time": "2016-08-06T14:39:51+00:00" + }, + { + "name": "psr/log", + "version": "1.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/log.git", + "reference": "4ebe3a8bf773a19edfe0a84b6585ba3d401b724d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/log/zipball/4ebe3a8bf773a19edfe0a84b6585ba3d401b724d", + "reference": "4ebe3a8bf773a19edfe0a84b6585ba3d401b724d", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Log\\": "Psr/Log/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interface for logging libraries", + "homepage": "https://github.com/php-fig/log", + "keywords": [ + "log", + "psr", + "psr-3" + ], + "time": "2016-10-10T12:19:37+00:00" + }, + { + "name": "ramsey/uuid", + "version": "3.7.3", + "source": { + "type": "git", + "url": "https://github.com/ramsey/uuid.git", + "reference": "44abcdad877d9a46685a3a4d221e3b2c4b87cb76" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ramsey/uuid/zipball/44abcdad877d9a46685a3a4d221e3b2c4b87cb76", + "reference": "44abcdad877d9a46685a3a4d221e3b2c4b87cb76", + "shasum": "" + }, + "require": { + "paragonie/random_compat": "^1.0|^2.0", + "php": "^5.4 || ^7.0" + }, + "replace": { + "rhumsaa/uuid": "self.version" + }, + "require-dev": { + "codeception/aspect-mock": "^1.0 | ~2.0.0", + "doctrine/annotations": "~1.2.0", + "goaop/framework": "1.0.0-alpha.2 | ^1.0 | ^2.1", + "ircmaxell/random-lib": "^1.1", + "jakub-onderka/php-parallel-lint": "^0.9.0", + "mockery/mockery": "^0.9.9", + "moontoast/math": "^1.1", + "php-mock/php-mock-phpunit": "^0.3|^1.1", + "phpunit/phpunit": "^4.7|^5.0", + "squizlabs/php_codesniffer": "^2.3" + }, + "suggest": { + "ext-libsodium": "Provides the PECL libsodium extension for use with the SodiumRandomGenerator", + "ext-uuid": "Provides the PECL UUID extension for use with the PeclUuidTimeGenerator and PeclUuidRandomGenerator", + "ircmaxell/random-lib": "Provides RandomLib for use with the RandomLibAdapter", + "moontoast/math": "Provides support for converting UUID to 128-bit integer (in string form).", + "ramsey/uuid-console": "A console application for generating UUIDs with ramsey/uuid", + "ramsey/uuid-doctrine": "Allows the use of Ramsey\\Uuid\\Uuid as Doctrine field type." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Ramsey\\Uuid\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Marijn Huizendveld", + "email": "marijn.huizendveld@gmail.com" + }, + { + "name": "Thibaud Fabre", + "email": "thibaud@aztech.io" + }, + { + "name": "Ben Ramsey", + "email": "ben@benramsey.com", + "homepage": "https://benramsey.com" + } + ], + "description": "Formerly rhumsaa/uuid. A PHP 5.4+ library for generating RFC 4122 version 1, 3, 4, and 5 universally unique identifiers (UUID).", + "homepage": "https://github.com/ramsey/uuid", + "keywords": [ + "guid", + "identifier", + "uuid" + ], + "time": "2018-01-20T00:28:24+00:00" + }, + { + "name": "sebastian/code-unit-reverse-lookup", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", + "reference": "4419fcdb5eabb9caa61a27c7a1db532a6b55dd18" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/4419fcdb5eabb9caa61a27c7a1db532a6b55dd18", + "reference": "4419fcdb5eabb9caa61a27c7a1db532a6b55dd18", + "shasum": "" + }, + "require": { + "php": "^5.6 || ^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^5.7 || ^6.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Looks up which function or method a line of code belongs to", + "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/", + "time": "2017-03-04T06:30:41+00:00" + }, + { + "name": "sebastian/comparator", + "version": "2.1.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/comparator.git", + "reference": "34369daee48eafb2651bea869b4b15d75ccc35f9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/34369daee48eafb2651bea869b4b15d75ccc35f9", + "reference": "34369daee48eafb2651bea869b4b15d75ccc35f9", + "shasum": "" + }, + "require": { + "php": "^7.0", + "sebastian/diff": "^2.0 || ^3.0", + "sebastian/exporter": "^3.1" + }, + "require-dev": { + "phpunit/phpunit": "^6.4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.1.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@2bepublished.at" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides the functionality to compare PHP values for equality", + "homepage": "https://github.com/sebastianbergmann/comparator", + "keywords": [ + "comparator", + "compare", + "equality" + ], + "time": "2018-02-01T13:46:46+00:00" + }, + { + "name": "sebastian/diff", + "version": "2.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/diff.git", + "reference": "347c1d8b49c5c3ee30c7040ea6fc446790e6bddd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/347c1d8b49c5c3ee30c7040ea6fc446790e6bddd", + "reference": "347c1d8b49c5c3ee30c7040ea6fc446790e6bddd", + "shasum": "" + }, + "require": { + "php": "^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^6.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Kore Nordmann", + "email": "mail@kore-nordmann.de" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Diff implementation", + "homepage": "https://github.com/sebastianbergmann/diff", + "keywords": [ + "diff" + ], + "time": "2017-08-03T08:09:46+00:00" + }, + { + "name": "sebastian/environment", + "version": "3.1.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/environment.git", + "reference": "cd0871b3975fb7fc44d11314fd1ee20925fce4f5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/cd0871b3975fb7fc44d11314fd1ee20925fce4f5", + "reference": "cd0871b3975fb7fc44d11314fd1ee20925fce4f5", + "shasum": "" + }, + "require": { + "php": "^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^6.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.1.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides functionality to handle HHVM/PHP environments", + "homepage": "http://www.github.com/sebastianbergmann/environment", + "keywords": [ + "Xdebug", + "environment", + "hhvm" + ], + "time": "2017-07-01T08:51:00+00:00" + }, + { + "name": "sebastian/exporter", + "version": "3.1.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/exporter.git", + "reference": "234199f4528de6d12aaa58b612e98f7d36adb937" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/234199f4528de6d12aaa58b612e98f7d36adb937", + "reference": "234199f4528de6d12aaa58b612e98f7d36adb937", + "shasum": "" + }, + "require": { + "php": "^7.0", + "sebastian/recursion-context": "^3.0" + }, + "require-dev": { + "ext-mbstring": "*", + "phpunit/phpunit": "^6.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.1.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@2bepublished.at" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + } + ], + "description": "Provides the functionality to export PHP variables for visualization", + "homepage": "http://www.github.com/sebastianbergmann/exporter", + "keywords": [ + "export", + "exporter" + ], + "time": "2017-04-03T13:19:02+00:00" + }, + { + "name": "sebastian/global-state", + "version": "2.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/global-state.git", + "reference": "e8ba02eed7bbbb9e59e43dedd3dddeff4a56b0c4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/e8ba02eed7bbbb9e59e43dedd3dddeff4a56b0c4", + "reference": "e8ba02eed7bbbb9e59e43dedd3dddeff4a56b0c4", + "shasum": "" + }, + "require": { + "php": "^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^6.0" + }, + "suggest": { + "ext-uopz": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Snapshotting of global state", + "homepage": "http://www.github.com/sebastianbergmann/global-state", + "keywords": [ + "global state" + ], + "time": "2017-04-27T15:39:26+00:00" + }, + { + "name": "sebastian/object-enumerator", + "version": "3.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-enumerator.git", + "reference": "7cfd9e65d11ffb5af41198476395774d4c8a84c5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/7cfd9e65d11ffb5af41198476395774d4c8a84c5", + "reference": "7cfd9e65d11ffb5af41198476395774d4c8a84c5", + "shasum": "" + }, + "require": { + "php": "^7.0", + "sebastian/object-reflector": "^1.1.1", + "sebastian/recursion-context": "^3.0" + }, + "require-dev": { + "phpunit/phpunit": "^6.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Traverses array structures and object graphs to enumerate all referenced objects", + "homepage": "https://github.com/sebastianbergmann/object-enumerator/", + "time": "2017-08-03T12:35:26+00:00" + }, + { + "name": "sebastian/object-reflector", + "version": "1.1.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-reflector.git", + "reference": "773f97c67f28de00d397be301821b06708fca0be" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/773f97c67f28de00d397be301821b06708fca0be", + "reference": "773f97c67f28de00d397be301821b06708fca0be", + "shasum": "" + }, + "require": { + "php": "^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^6.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Allows reflection of object attributes, including inherited and non-public ones", + "homepage": "https://github.com/sebastianbergmann/object-reflector/", + "time": "2017-03-29T09:07:27+00:00" + }, + { + "name": "sebastian/recursion-context", + "version": "3.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/recursion-context.git", + "reference": "5b0cd723502bac3b006cbf3dbf7a1e3fcefe4fa8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/5b0cd723502bac3b006cbf3dbf7a1e3fcefe4fa8", + "reference": "5b0cd723502bac3b006cbf3dbf7a1e3fcefe4fa8", + "shasum": "" + }, + "require": { + "php": "^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^6.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + } + ], + "description": "Provides functionality to recursively process PHP variables", + "homepage": "http://www.github.com/sebastianbergmann/recursion-context", + "time": "2017-03-03T06:23:57+00:00" + }, + { + "name": "sebastian/resource-operations", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/resource-operations.git", + "reference": "ce990bb21759f94aeafd30209e8cfcdfa8bc3f52" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/ce990bb21759f94aeafd30209e8cfcdfa8bc3f52", + "reference": "ce990bb21759f94aeafd30209e8cfcdfa8bc3f52", + "shasum": "" + }, + "require": { + "php": ">=5.6.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides a list of PHP built-in functions that operate on resources", + "homepage": "https://www.github.com/sebastianbergmann/resource-operations", + "time": "2015-07-28T20:34:47+00:00" + }, + { + "name": "sebastian/version", + "version": "2.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/version.git", + "reference": "99732be0ddb3361e16ad77b68ba41efc8e979019" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/99732be0ddb3361e16ad77b68ba41efc8e979019", + "reference": "99732be0ddb3361e16ad77b68ba41efc8e979019", + "shasum": "" + }, + "require": { + "php": ">=5.6" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that helps with managing the version number of Git-hosted PHP projects", + "homepage": "https://github.com/sebastianbergmann/version", + "time": "2016-10-03T07:35:21+00:00" + }, + { + "name": "symfony/browser-kit", + "version": "v3.4.6", + "source": { + "type": "git", + "url": "https://github.com/symfony/browser-kit.git", + "reference": "490f27762705c8489bd042fe3e9377a191dba9b4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/browser-kit/zipball/490f27762705c8489bd042fe3e9377a191dba9b4", + "reference": "490f27762705c8489bd042fe3e9377a191dba9b4", + "shasum": "" + }, + "require": { + "php": "^5.5.9|>=7.0.8", + "symfony/dom-crawler": "~2.8|~3.0|~4.0" + }, + "require-dev": { + "symfony/css-selector": "~2.8|~3.0|~4.0", + "symfony/process": "~2.8|~3.0|~4.0" + }, + "suggest": { + "symfony/process": "" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.4-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\BrowserKit\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony BrowserKit Component", + "homepage": "https://symfony.com", + "time": "2018-01-03T07:37:34+00:00" + }, + { + "name": "symfony/console", + "version": "v3.4.6", + "source": { + "type": "git", + "url": "https://github.com/symfony/console.git", + "reference": "067339e9b8ec30d5f19f5950208893ff026b94f7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/console/zipball/067339e9b8ec30d5f19f5950208893ff026b94f7", + "reference": "067339e9b8ec30d5f19f5950208893ff026b94f7", + "shasum": "" + }, + "require": { + "php": "^5.5.9|>=7.0.8", + "symfony/debug": "~2.8|~3.0|~4.0", + "symfony/polyfill-mbstring": "~1.0" + }, + "conflict": { + "symfony/dependency-injection": "<3.4", + "symfony/process": "<3.3" + }, + "require-dev": { + "psr/log": "~1.0", + "symfony/config": "~3.3|~4.0", + "symfony/dependency-injection": "~3.4|~4.0", + "symfony/event-dispatcher": "~2.8|~3.0|~4.0", + "symfony/lock": "~3.4|~4.0", + "symfony/process": "~3.3|~4.0" + }, + "suggest": { + "psr/log": "For using the console logger", + "symfony/event-dispatcher": "", + "symfony/lock": "", + "symfony/process": "" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.4-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\Console\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Console Component", + "homepage": "https://symfony.com", + "time": "2018-02-26T15:46:28+00:00" + }, + { + "name": "symfony/css-selector", + "version": "v3.4.6", + "source": { + "type": "git", + "url": "https://github.com/symfony/css-selector.git", + "reference": "544655f1fc078a9cd839fdda2b7b1e64627c826a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/css-selector/zipball/544655f1fc078a9cd839fdda2b7b1e64627c826a", + "reference": "544655f1fc078a9cd839fdda2b7b1e64627c826a", + "shasum": "" + }, + "require": { + "php": "^5.5.9|>=7.0.8" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.4-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\CssSelector\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jean-François Simon", + "email": "jeanfrancois.simon@sensiolabs.com" + }, + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony CssSelector Component", + "homepage": "https://symfony.com", + "time": "2018-02-03T14:55:07+00:00" + }, + { + "name": "symfony/debug", + "version": "v3.4.6", + "source": { + "type": "git", + "url": "https://github.com/symfony/debug.git", + "reference": "9b1071f86e79e1999b3d3675d2e0e7684268b9bc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/debug/zipball/9b1071f86e79e1999b3d3675d2e0e7684268b9bc", + "reference": "9b1071f86e79e1999b3d3675d2e0e7684268b9bc", + "shasum": "" + }, + "require": { + "php": "^5.5.9|>=7.0.8", + "psr/log": "~1.0" + }, + "conflict": { + "symfony/http-kernel": ">=2.3,<2.3.24|~2.4.0|>=2.5,<2.5.9|>=2.6,<2.6.2" + }, + "require-dev": { + "symfony/http-kernel": "~2.8|~3.0|~4.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.4-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\Debug\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Debug Component", + "homepage": "https://symfony.com", + "time": "2018-02-28T21:49:22+00:00" + }, + { + "name": "symfony/dom-crawler", + "version": "v3.4.6", + "source": { + "type": "git", + "url": "https://github.com/symfony/dom-crawler.git", + "reference": "2bb5d3101cc01f4fe580e536daf4f1959bc2d24d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/2bb5d3101cc01f4fe580e536daf4f1959bc2d24d", + "reference": "2bb5d3101cc01f4fe580e536daf4f1959bc2d24d", + "shasum": "" + }, + "require": { + "php": "^5.5.9|>=7.0.8", + "symfony/polyfill-mbstring": "~1.0" + }, + "require-dev": { + "symfony/css-selector": "~2.8|~3.0|~4.0" + }, + "suggest": { + "symfony/css-selector": "" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.4-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\DomCrawler\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony DomCrawler Component", + "homepage": "https://symfony.com", + "time": "2018-02-22T10:48:49+00:00" + }, + { + "name": "symfony/event-dispatcher", + "version": "v3.4.6", + "source": { + "type": "git", + "url": "https://github.com/symfony/event-dispatcher.git", + "reference": "58990682ac3fdc1f563b7e705452921372aad11d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/58990682ac3fdc1f563b7e705452921372aad11d", + "reference": "58990682ac3fdc1f563b7e705452921372aad11d", + "shasum": "" + }, + "require": { + "php": "^5.5.9|>=7.0.8" + }, + "conflict": { + "symfony/dependency-injection": "<3.3" + }, + "require-dev": { + "psr/log": "~1.0", + "symfony/config": "~2.8|~3.0|~4.0", + "symfony/dependency-injection": "~3.3|~4.0", + "symfony/expression-language": "~2.8|~3.0|~4.0", + "symfony/stopwatch": "~2.8|~3.0|~4.0" + }, + "suggest": { + "symfony/dependency-injection": "", + "symfony/http-kernel": "" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.4-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\EventDispatcher\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony EventDispatcher Component", + "homepage": "https://symfony.com", + "time": "2018-02-14T10:03:57+00:00" + }, + { + "name": "symfony/filesystem", + "version": "v3.4.6", + "source": { + "type": "git", + "url": "https://github.com/symfony/filesystem.git", + "reference": "253a4490b528597aa14d2bf5aeded6f5e5e4a541" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/253a4490b528597aa14d2bf5aeded6f5e5e4a541", + "reference": "253a4490b528597aa14d2bf5aeded6f5e5e4a541", + "shasum": "" + }, + "require": { + "php": "^5.5.9|>=7.0.8" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.4-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\Filesystem\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Filesystem Component", + "homepage": "https://symfony.com", + "time": "2018-02-22T10:48:49+00:00" + }, + { + "name": "symfony/finder", + "version": "v3.4.6", + "source": { + "type": "git", + "url": "https://github.com/symfony/finder.git", + "reference": "a479817ce0a9e4adfd7d39c6407c95d97c254625" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/finder/zipball/a479817ce0a9e4adfd7d39c6407c95d97c254625", + "reference": "a479817ce0a9e4adfd7d39c6407c95d97c254625", + "shasum": "" + }, + "require": { + "php": "^5.5.9|>=7.0.8" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.4-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\Finder\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Finder Component", + "homepage": "https://symfony.com", + "time": "2018-03-05T18:28:11+00:00" + }, + { + "name": "symfony/http-foundation", + "version": "v3.4.6", + "source": { + "type": "git", + "url": "https://github.com/symfony/http-foundation.git", + "reference": "6f5935723c11b4125fc9927db6ad2feaa196e175" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/6f5935723c11b4125fc9927db6ad2feaa196e175", + "reference": "6f5935723c11b4125fc9927db6ad2feaa196e175", + "shasum": "" + }, + "require": { + "php": "^5.5.9|>=7.0.8", + "symfony/polyfill-mbstring": "~1.1", + "symfony/polyfill-php70": "~1.6" + }, + "require-dev": { + "symfony/expression-language": "~2.8|~3.0|~4.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.4-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\HttpFoundation\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony HttpFoundation Component", + "homepage": "https://symfony.com", + "time": "2018-02-22T10:48:49+00:00" + }, + { + "name": "symfony/polyfill-mbstring", + "version": "v1.7.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-mbstring.git", + "reference": "78be803ce01e55d3491c1397cf1c64beb9c1b63b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/78be803ce01e55d3491c1397cf1c64beb9c1b63b", + "reference": "78be803ce01e55d3491c1397cf1c64beb9c1b63b", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "suggest": { + "ext-mbstring": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.7-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Polyfill\\Mbstring\\": "" + }, + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for the Mbstring extension", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "mbstring", + "polyfill", + "portable", + "shim" + ], + "time": "2018-01-30T19:27:44+00:00" + }, + { + "name": "symfony/polyfill-php70", + "version": "v1.7.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php70.git", + "reference": "3532bfcd8f933a7816f3a0a59682fc404776600f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php70/zipball/3532bfcd8f933a7816f3a0a59682fc404776600f", + "reference": "3532bfcd8f933a7816f3a0a59682fc404776600f", + "shasum": "" + }, + "require": { + "paragonie/random_compat": "~1.0|~2.0", + "php": ">=5.3.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.7-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Polyfill\\Php70\\": "" + }, + "files": [ + "bootstrap.php" + ], + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 7.0+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "time": "2018-01-30T19:27:44+00:00" + }, + { + "name": "symfony/process", + "version": "v3.4.8", + "source": { + "type": "git", + "url": "https://github.com/symfony/process.git", + "reference": "4b7d64e852886319e93ddfdecff0d744ab87658b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/process/zipball/4b7d64e852886319e93ddfdecff0d744ab87658b", + "reference": "4b7d64e852886319e93ddfdecff0d744ab87658b", + "shasum": "" + }, + "require": { + "php": "^5.5.9|>=7.0.8" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.4-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\Process\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Process Component", + "homepage": "https://symfony.com", + "time": "2018-04-03T05:22:50+00:00" + }, + { + "name": "symfony/yaml", + "version": "v3.4.6", + "source": { + "type": "git", + "url": "https://github.com/symfony/yaml.git", + "reference": "6af42631dcf89e9c616242c900d6c52bd53bd1bb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/yaml/zipball/6af42631dcf89e9c616242c900d6c52bd53bd1bb", + "reference": "6af42631dcf89e9c616242c900d6c52bd53bd1bb", + "shasum": "" + }, + "require": { + "php": "^5.5.9|>=7.0.8" + }, + "conflict": { + "symfony/console": "<3.4" + }, + "require-dev": { + "symfony/console": "~3.4|~4.0" + }, + "suggest": { + "symfony/console": "For validating YAML files using the lint command" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.4-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\Yaml\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Yaml Component", + "homepage": "https://symfony.com", + "time": "2018-02-16T09:50:28+00:00" + }, + { + "name": "theseer/tokenizer", + "version": "1.1.0", + "source": { + "type": "git", + "url": "https://github.com/theseer/tokenizer.git", + "reference": "cb2f008f3f05af2893a87208fe6a6c4985483f8b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/cb2f008f3f05af2893a87208fe6a6c4985483f8b", + "reference": "cb2f008f3f05af2893a87208fe6a6c4985483f8b", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-tokenizer": "*", + "ext-xmlwriter": "*", + "php": "^7.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + } + ], + "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", + "time": "2017-04-07T12:08:54+00:00" + }, + { + "name": "vlucas/phpdotenv", + "version": "v2.4.0", + "source": { + "type": "git", + "url": "https://github.com/vlucas/phpdotenv.git", + "reference": "3cc116adbe4b11be5ec557bf1d24dc5e3a21d18c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/3cc116adbe4b11be5ec557bf1d24dc5e3a21d18c", + "reference": "3cc116adbe4b11be5ec557bf1d24dc5e3a21d18c", + "shasum": "" + }, + "require": { + "php": ">=5.3.9" + }, + "require-dev": { + "phpunit/phpunit": "^4.8 || ^5.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.4-dev" + } + }, + "autoload": { + "psr-4": { + "Dotenv\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause-Attribution" + ], + "authors": [ + { + "name": "Vance Lucas", + "email": "vance@vancelucas.com", + "homepage": "http://www.vancelucas.com" + } + ], + "description": "Loads environment variables from `.env` to `getenv()`, `$_ENV` and `$_SERVER` automagically.", + "keywords": [ + "dotenv", + "env", + "environment" + ], + "time": "2016-09-01T10:05:43+00:00" + }, + { + "name": "webmozart/assert", + "version": "1.3.0", + "source": { + "type": "git", + "url": "https://github.com/webmozart/assert.git", + "reference": "0df1908962e7a3071564e857d86874dad1ef204a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/webmozart/assert/zipball/0df1908962e7a3071564e857d86874dad1ef204a", + "reference": "0df1908962e7a3071564e857d86874dad1ef204a", + "shasum": "" + }, + "require": { + "php": "^5.3.3 || ^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.6", + "sebastian/version": "^1.0.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.3-dev" + } + }, + "autoload": { + "psr-4": { + "Webmozart\\Assert\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + } + ], + "description": "Assertions to validate method input/output with nice error messages.", + "keywords": [ + "assert", + "check", + "validate" + ], + "time": "2018-01-29T19:49:41+00:00" + } + ], + "packages-dev": [ + { + "name": "brainmaestro/composer-git-hooks", + "version": "v2.4.3", + "source": { + "type": "git", + "url": "https://github.com/BrainMaestro/composer-git-hooks.git", + "reference": "2e4d8bd95ee916f3951a55438752f8adab8b69fb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/BrainMaestro/composer-git-hooks/zipball/2e4d8bd95ee916f3951a55438752f8adab8b69fb", + "reference": "2e4d8bd95ee916f3951a55438752f8adab8b69fb", + "shasum": "" + }, + "require": { + "symfony/console": "^3.2 || ^4.0" + }, + "require-dev": { + "phpunit/phpunit": "^5.7" + }, + "bin": [ + "cghooks" + ], + "type": "library", + "autoload": { + "psr-4": { + "BrainMaestro\\GitHooks\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ezinwa Okpoechi", + "email": "brainmaestro@outlook.com" + } + ], + "description": "Easily manage git hooks in your composer config", + "keywords": [ + "HOOK", + "composer", + "git" + ], + "time": "2018-02-27T10:23:55+00:00" + }, + { + "name": "codacy/coverage", + "version": "1.4.1", + "source": { + "type": "git", + "url": "https://github.com/codacy/php-codacy-coverage.git", + "reference": "546ac2e000a004571c1000ecff4d68a98706db3a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/codacy/php-codacy-coverage/zipball/546ac2e000a004571c1000ecff4d68a98706db3a", + "reference": "546ac2e000a004571c1000ecff4d68a98706db3a", + "shasum": "" + }, + "require": { + "gitonomy/gitlib": ">=1.0", + "php": ">=5.3.3", + "symfony/console": "~2.5|~3.0|~4.0" + }, + "require-dev": { + "phpunit/phpunit": "~6.5" + }, + "bin": [ + "bin/codacycoverage" + ], + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jakob Pupke", + "email": "jakob.pupke@gmail.com" + } + ], + "description": "Sends PHP test coverage information to Codacy.", + "homepage": "https://github.com/codacy/php-codacy-coverage", + "time": "2017-12-20T14:31:46+00:00" + }, + { + "name": "codeception/aspect-mock", + "version": "2.1.1", + "source": { + "type": "git", + "url": "https://github.com/Codeception/AspectMock.git", + "reference": "bf3c000599c0dc75ecb52e19dee2b8ed294cf7ba" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Codeception/AspectMock/zipball/bf3c000599c0dc75ecb52e19dee2b8ed294cf7ba", + "reference": "bf3c000599c0dc75ecb52e19dee2b8ed294cf7ba", + "shasum": "" + }, + "require": { + "goaop/framework": "^2.0.0", + "php": ">=5.6.0", + "symfony/finder": "~2.4|~3.0" + }, + "require-dev": { + "codeception/base": "~2.1", + "codeception/specify": "~0.3", + "codeception/verify": "~0.2" + }, + "type": "library", + "autoload": { + "psr-0": { + "AspectMock": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Michael Bodnarchuk", + "email": "davert@codeception.com" + } + ], + "description": "Experimental Mocking Framework powered by Aspects", + "time": "2017-10-24T10:20:17+00:00" + }, + { + "name": "gitonomy/gitlib", + "version": "v1.0.3", + "source": { + "type": "git", + "url": "https://github.com/gitonomy/gitlib.git", + "reference": "1c2b0605115786613cb517798046c8ab57c17097" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/gitonomy/gitlib/zipball/1c2b0605115786613cb517798046c8ab57c17097", + "reference": "1c2b0605115786613cb517798046c8ab57c17097", + "shasum": "" + }, + "require": { + "php": "^5.3 || ^7.0", + "symfony/process": "^2.3|^3.0|^4.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.8.35|^5.7", + "psr/log": "^1.0" + }, + "suggest": { + "psr/log": "Add some log" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "psr-4": { + "Gitonomy\\Git\\": "src/Gitonomy/Git/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Alexandre Salomé", + "email": "alexandre.salome@gmail.com", + "homepage": "http://alexandre-salome.fr" + }, + { + "name": "Julien DIDIER", + "email": "genzo.wm@gmail.com", + "homepage": "http://www.jdidier.net" + } + ], + "description": "Library for accessing git", + "homepage": "http://gitonomy.com", + "time": "2018-01-10T11:34:47+00:00" + }, + { + "name": "goaop/framework", + "version": "2.1.2", + "source": { + "type": "git", + "url": "https://github.com/goaop/framework.git", + "reference": "6e2a0fe13c1943db02a67588cfd27692bddaffa5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/goaop/framework/zipball/6e2a0fe13c1943db02a67588cfd27692bddaffa5", + "reference": "6e2a0fe13c1943db02a67588cfd27692bddaffa5", + "shasum": "" + }, + "require": { + "doctrine/annotations": "~1.0", + "goaop/parser-reflection": "~1.2", + "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" + }, + "suggest": { + "symfony/console": "Enables the usage of the command-line tool." + }, + "bin": [ + "bin/aspect" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "psr-4": { + "Go\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Lisachenko Alexander", + "homepage": "https://github.com/lisachenko" + } + ], + "description": "Framework for aspect-oriented programming in PHP.", + "homepage": "http://go.aopphp.com/", + "keywords": [ + "aop", + "aspect", + "library", + "php" + ], + "time": "2017-07-12T11:46:25+00:00" + }, + { + "name": "goaop/parser-reflection", + "version": "1.4.1", + "source": { + "type": "git", + "url": "https://github.com/goaop/parser-reflection.git", + "reference": "d9c1dcc7ce4a5284fe3530e011faf9c9c10e1166" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/goaop/parser-reflection/zipball/d9c1dcc7ce4a5284fe3530e011faf9c9c10e1166", + "reference": "d9c1dcc7ce4a5284fe3530e011faf9c9c10e1166", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^1.2|^2.0|^3.0", + "php": ">=5.6.0" + }, + "require-dev": { + "phpunit/phpunit": "~4.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Go\\ParserReflection\\": "src" + }, + "files": [ + "src/bootstrap.php" + ], + "exclude-from-classmap": [ + "/tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Alexander Lisachenko", + "email": "lisachenko.it@gmail.com" + } + ], + "description": "Provides reflection information, based on raw source", + "time": "2018-03-19T15:57:41+00:00" + }, + { + "name": "guzzle/guzzle", + "version": "v3.8.1", + "source": { + "type": "git", + "url": "https://github.com/guzzle/guzzle.git", + "reference": "4de0618a01b34aa1c8c33a3f13f396dcd3882eba" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/4de0618a01b34aa1c8c33a3f13f396dcd3882eba", + "reference": "4de0618a01b34aa1c8c33a3f13f396dcd3882eba", + "shasum": "" + }, + "require": { + "ext-curl": "*", + "php": ">=5.3.3", + "symfony/event-dispatcher": ">=2.1" + }, + "replace": { + "guzzle/batch": "self.version", + "guzzle/cache": "self.version", + "guzzle/common": "self.version", + "guzzle/http": "self.version", + "guzzle/inflection": "self.version", + "guzzle/iterator": "self.version", + "guzzle/log": "self.version", + "guzzle/parser": "self.version", + "guzzle/plugin": "self.version", + "guzzle/plugin-async": "self.version", + "guzzle/plugin-backoff": "self.version", + "guzzle/plugin-cache": "self.version", + "guzzle/plugin-cookie": "self.version", + "guzzle/plugin-curlauth": "self.version", + "guzzle/plugin-error-response": "self.version", + "guzzle/plugin-history": "self.version", + "guzzle/plugin-log": "self.version", + "guzzle/plugin-md5": "self.version", + "guzzle/plugin-mock": "self.version", + "guzzle/plugin-oauth": "self.version", + "guzzle/service": "self.version", + "guzzle/stream": "self.version" + }, + "require-dev": { + "doctrine/cache": "*", + "monolog/monolog": "1.*", + "phpunit/phpunit": "3.7.*", + "psr/log": "1.0.*", + "symfony/class-loader": "*", + "zendframework/zend-cache": "<2.3", + "zendframework/zend-log": "<2.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.8-dev" + } + }, + "autoload": { + "psr-0": { + "Guzzle": "src/", + "Guzzle\\Tests": "tests/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "Guzzle Community", + "homepage": "https://github.com/guzzle/guzzle/contributors" + } + ], + "description": "Guzzle is a PHP HTTP client library and framework for building RESTful web service clients", + "homepage": "http://guzzlephp.org/", + "keywords": [ + "client", + "curl", + "framework", + "http", + "http client", + "rest", + "web service" + ], + "abandoned": "guzzlehttp/guzzle", + "time": "2014-01-28T22:29:15+00:00" + }, + { + "name": "jakubledl/dissect", + "version": "v1.0.1", + "source": { + "type": "git", + "url": "https://github.com/jakubledl/dissect.git", + "reference": "d3a391de31e45a247e95cef6cf58a91c05af67c4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/jakubledl/dissect/zipball/d3a391de31e45a247e95cef6cf58a91c05af67c4", + "reference": "d3a391de31e45a247e95cef6cf58a91c05af67c4", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "require-dev": { + "symfony/console": "~2.1" + }, + "suggest": { + "symfony/console": "for the command-line tool" + }, + "bin": [ + "bin/dissect.php", + "bin/dissect" + ], + "type": "library", + "autoload": { + "psr-0": { + "Dissect": [ + "src/" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "unlicense" + ], + "authors": [ + { + "name": "Jakub Lédl", + "email": "jakubledl@gmail.com" + } + ], + "description": "Lexing and parsing in pure PHP", + "homepage": "https://github.com/jakubledl/dissect", + "keywords": [ + "ast", + "lexing", + "parser", + "parsing" + ], + "time": "2013-01-29T21:29:14+00:00" + }, + { + "name": "nikic/php-parser", + "version": "v3.1.5", + "source": { + "type": "git", + "url": "https://github.com/nikic/PHP-Parser.git", + "reference": "bb87e28e7d7b8d9a7fda231d37457c9210faf6ce" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/bb87e28e7d7b8d9a7fda231d37457c9210faf6ce", + "reference": "bb87e28e7d7b8d9a7fda231d37457c9210faf6ce", + "shasum": "" + }, + "require": { + "ext-tokenizer": "*", + "php": ">=5.5" + }, + "require-dev": { + "phpunit/phpunit": "~4.0|~5.0" + }, + "bin": [ + "bin/php-parse" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0-dev" + } + }, + "autoload": { + "psr-4": { + "PhpParser\\": "lib/PhpParser" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Nikita Popov" + } + ], + "description": "A PHP parser written in PHP", + "keywords": [ + "parser", + "php" + ], + "time": "2018-02-28T20:30:58+00:00" + }, + { + "name": "pdepend/pdepend", + "version": "2.5.2", + "source": { + "type": "git", + "url": "https://github.com/pdepend/pdepend.git", + "reference": "9daf26d0368d4a12bed1cacae1a9f3a6f0adf239" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/pdepend/pdepend/zipball/9daf26d0368d4a12bed1cacae1a9f3a6f0adf239", + "reference": "9daf26d0368d4a12bed1cacae1a9f3a6f0adf239", + "shasum": "" + }, + "require": { + "php": ">=5.3.7", + "symfony/config": "^2.3.0|^3|^4", + "symfony/dependency-injection": "^2.3.0|^3|^4", + "symfony/filesystem": "^2.3.0|^3|^4" + }, + "require-dev": { + "phpunit/phpunit": "^4.8|^5.7", + "squizlabs/php_codesniffer": "^2.0.0" + }, + "bin": [ + "src/bin/pdepend" + ], + "type": "library", + "autoload": { + "psr-4": { + "PDepend\\": "src/main/php/PDepend" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "description": "Official version of pdepend to be handled with Composer", + "time": "2017-12-13T13:21:38+00:00" + }, + { + "name": "php-coveralls/php-coveralls", + "version": "v1.1.0", + "source": { + "type": "git", + "url": "https://github.com/php-coveralls/php-coveralls.git", + "reference": "37f8f83fe22224eb9d9c6d593cdeb33eedd2a9ad" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-coveralls/php-coveralls/zipball/37f8f83fe22224eb9d9c6d593cdeb33eedd2a9ad", + "reference": "37f8f83fe22224eb9d9c6d593cdeb33eedd2a9ad", + "shasum": "" + }, + "require": { + "ext-json": "*", + "ext-simplexml": "*", + "guzzle/guzzle": "^2.8 || ^3.0", + "php": "^5.3.3 || ^7.0", + "psr/log": "^1.0", + "symfony/config": "^2.1 || ^3.0 || ^4.0", + "symfony/console": "^2.1 || ^3.0 || ^4.0", + "symfony/stopwatch": "^2.0 || ^3.0 || ^4.0", + "symfony/yaml": "^2.0 || ^3.0 || ^4.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.8.35 || ^5.4.3 || ^6.0" + }, + "suggest": { + "symfony/http-kernel": "Allows Symfony integration" + }, + "bin": [ + "bin/coveralls" + ], + "type": "library", + "autoload": { + "psr-4": { + "Satooshi\\": "src/Satooshi/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Kitamura Satoshi", + "email": "with.no.parachute@gmail.com", + "homepage": "https://www.facebook.com/satooshi.jp" + } + ], + "description": "PHP client library for Coveralls API", + "homepage": "https://github.com/php-coveralls/php-coveralls", + "keywords": [ + "ci", + "coverage", + "github", + "test" + ], + "time": "2017-12-06T23:17:56+00:00" + }, + { + "name": "phpmd/phpmd", + "version": "2.6.0", + "source": { + "type": "git", + "url": "https://github.com/phpmd/phpmd.git", + "reference": "4e9924b2c157a3eb64395460fcf56b31badc8374" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpmd/phpmd/zipball/4e9924b2c157a3eb64395460fcf56b31badc8374", + "reference": "4e9924b2c157a3eb64395460fcf56b31badc8374", + "shasum": "" + }, + "require": { + "ext-xml": "*", + "pdepend/pdepend": "^2.5", + "php": ">=5.3.9" + }, + "require-dev": { + "phpunit/phpunit": "^4.0", + "squizlabs/php_codesniffer": "^2.0" + }, + "bin": [ + "src/bin/phpmd" + ], + "type": "project", + "autoload": { + "psr-0": { + "PHPMD\\": "src/main/php" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Manuel Pichler", + "email": "github@manuel-pichler.de", + "homepage": "https://github.com/manuelpichler", + "role": "Project Founder" + }, + { + "name": "Other contributors", + "homepage": "https://github.com/phpmd/phpmd/graphs/contributors", + "role": "Contributors" + }, + { + "name": "Marc Würth", + "email": "ravage@bluewin.ch", + "homepage": "https://github.com/ravage84", + "role": "Project Maintainer" + } + ], + "description": "PHPMD is a spin-off project of PHP Depend and aims to be a PHP equivalent of the well known Java tool PMD.", + "homepage": "http://phpmd.org/", + "keywords": [ + "mess detection", + "mess detector", + "pdepend", + "phpmd", + "pmd" + ], + "time": "2017-01-20T14:41:10+00:00" + }, + { + "name": "rregeer/phpunit-coverage-check", + "version": "0.1.4", + "source": { + "type": "git", + "url": "https://github.com/richardregeer/phpunit-coverage-check.git", + "reference": "c703ab643d54f959c5c219dd2b08892a0e3dd80d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/richardregeer/phpunit-coverage-check/zipball/c703ab643d54f959c5c219dd2b08892a0e3dd80d", + "reference": "c703ab643d54f959c5c219dd2b08892a0e3dd80d", + "shasum": "" + }, + "require": { + "php": ">=5.5.0" + }, + "bin": [ + "bin/coverage-check" + ], + "type": "library", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Richard Regeer", + "email": "rich2309@gmail.com" + } + ], + "description": "Check the code coverage using the clover report of phpunit", + "keywords": [ + "ci", + "code coverage", + "php", + "phpunit", + "testing", + "unittest" + ], + "time": "2017-10-13T06:34:31+00:00" + }, + { + "name": "sebastian/finder-facade", + "version": "1.2.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/finder-facade.git", + "reference": "4a3174709c2dc565fe5fb26fcf827f6a1fc7b09f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/finder-facade/zipball/4a3174709c2dc565fe5fb26fcf827f6a1fc7b09f", + "reference": "4a3174709c2dc565fe5fb26fcf827f6a1fc7b09f", + "shasum": "" + }, + "require": { + "symfony/finder": "~2.3|~3.0|~4.0", + "theseer/fdomdocument": "~1.3" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "FinderFacade is a convenience wrapper for Symfony's Finder component.", + "homepage": "https://github.com/sebastianbergmann/finder-facade", + "time": "2017-11-18T17:31:49+00:00" + }, + { + "name": "sebastian/phpcpd", + "version": "3.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/phpcpd.git", + "reference": "dfed51c1288790fc957c9433e2f49ab152e8a564" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/phpcpd/zipball/dfed51c1288790fc957c9433e2f49ab152e8a564", + "reference": "dfed51c1288790fc957c9433e2f49ab152e8a564", + "shasum": "" + }, + "require": { + "php": "^5.6|^7.0", + "phpunit/php-timer": "^1.0.6", + "sebastian/finder-facade": "^1.1", + "sebastian/version": "^1.0|^2.0", + "symfony/console": "^2.7|^3.0|^4.0" + }, + "bin": [ + "phpcpd" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Copy/Paste Detector (CPD) for PHP code.", + "homepage": "https://github.com/sebastianbergmann/phpcpd", + "time": "2017-11-16T08:49:28+00:00" + }, + { + "name": "squizlabs/php_codesniffer", + "version": "1.5.3", + "source": { + "type": "git", + "url": "https://github.com/squizlabs/PHP_CodeSniffer.git", + "reference": "396178ada8499ec492363587f037125bf7b07fcc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/396178ada8499ec492363587f037125bf7b07fcc", + "reference": "396178ada8499ec492363587f037125bf7b07fcc", + "shasum": "" + }, + "require": { + "ext-tokenizer": "*", + "php": ">=5.1.2" + }, + "suggest": { + "phpunit/php-timer": "dev-master" + }, + "bin": [ + "scripts/phpcs" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-phpcs-fixer": "2.0.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" + ], + "authors": [ + { + "name": "Greg Sherwood", + "role": "lead" + } + ], + "description": "PHP_CodeSniffer tokenises 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" + }, + { + "name": "symfony/config", + "version": "v3.4.6", + "source": { + "type": "git", + "url": "https://github.com/symfony/config.git", + "reference": "05e10567b529476a006b00746c5f538f1636810e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/config/zipball/05e10567b529476a006b00746c5f538f1636810e", + "reference": "05e10567b529476a006b00746c5f538f1636810e", + "shasum": "" + }, + "require": { + "php": "^5.5.9|>=7.0.8", + "symfony/filesystem": "~2.8|~3.0|~4.0" + }, + "conflict": { + "symfony/dependency-injection": "<3.3", + "symfony/finder": "<3.3" + }, + "require-dev": { + "symfony/dependency-injection": "~3.3|~4.0", + "symfony/event-dispatcher": "~3.3|~4.0", + "symfony/finder": "~3.3|~4.0", + "symfony/yaml": "~3.0|~4.0" + }, + "suggest": { + "symfony/yaml": "To use the yaml reference dumper" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.4-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\Config\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Config Component", + "homepage": "https://symfony.com", + "time": "2018-02-14T10:03:57+00:00" + }, + { + "name": "symfony/dependency-injection", + "version": "v3.4.6", + "source": { + "type": "git", + "url": "https://github.com/symfony/dependency-injection.git", + "reference": "12e901abc1cb0d637a0e5abe9923471361d96b07" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/12e901abc1cb0d637a0e5abe9923471361d96b07", + "reference": "12e901abc1cb0d637a0e5abe9923471361d96b07", + "shasum": "" + }, + "require": { + "php": "^5.5.9|>=7.0.8", + "psr/container": "^1.0" + }, + "conflict": { + "symfony/config": "<3.3.7", + "symfony/finder": "<3.3", + "symfony/proxy-manager-bridge": "<3.4", + "symfony/yaml": "<3.4" + }, + "provide": { + "psr/container-implementation": "1.0" + }, + "require-dev": { + "symfony/config": "~3.3|~4.0", + "symfony/expression-language": "~2.8|~3.0|~4.0", + "symfony/yaml": "~3.4|~4.0" + }, + "suggest": { + "symfony/config": "", + "symfony/expression-language": "For using expressions in service container configuration", + "symfony/finder": "For using double-star glob patterns or when GLOB_BRACE portability is required", + "symfony/proxy-manager-bridge": "Generate service proxies to lazy load them", + "symfony/yaml": "" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.4-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\DependencyInjection\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony DependencyInjection Component", + "homepage": "https://symfony.com", + "time": "2018-03-04T03:54:53+00:00" + }, + { + "name": "symfony/stopwatch", + "version": "v3.4.6", + "source": { + "type": "git", + "url": "https://github.com/symfony/stopwatch.git", + "reference": "eb17cfa072cab26537ac37e9c4ece6c0361369af" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/stopwatch/zipball/eb17cfa072cab26537ac37e9c4ece6c0361369af", + "reference": "eb17cfa072cab26537ac37e9c4ece6c0361369af", + "shasum": "" + }, + "require": { + "php": "^5.5.9|>=7.0.8" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.4-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\Stopwatch\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Stopwatch Component", + "homepage": "https://symfony.com", + "time": "2018-02-17T14:55:25+00:00" + }, + { + "name": "theseer/fdomdocument", + "version": "1.6.6", + "source": { + "type": "git", + "url": "https://github.com/theseer/fDOMDocument.git", + "reference": "6e8203e40a32a9c770bcb62fe37e68b948da6dca" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/theseer/fDOMDocument/zipball/6e8203e40a32a9c770bcb62fe37e68b948da6dca", + "reference": "6e8203e40a32a9c770bcb62fe37e68b948da6dca", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "lib-libxml": "*", + "php": ">=5.3.3" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "lead" + } + ], + "description": "The classes contained within this repository extend the standard DOM to use exceptions at all occasions of errors instead of PHP warnings or notices. They also add various custom methods and shortcuts for convenience and to simplify the usage of DOM.", + "homepage": "https://github.com/theseer/fDOMDocument", + "time": "2017-06-30T11:53:12+00:00" + } + ], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": { + "php": "7.0.2|7.0.4|~7.0.6|~7.1.0|~7.2.0" + }, + "platform-dev": [] +} diff --git a/dev/tests/_bootstrap.php b/dev/tests/_bootstrap.php index d7e72c8f3..655e33910 100644 --- a/dev/tests/_bootstrap.php +++ b/dev/tests/_bootstrap.php @@ -16,8 +16,12 @@ 'includePaths' => [PROJECT_ROOT . '/src'] ]); -// force php to generate -$GLOBALS['FORCE_PHP_GENERATE'] = true; +// set mftf appplication context +\Magento\FunctionalTestingFramework\Config\MftfApplicationConfig::create( + true, + \Magento\FunctionalTestingFramework\Config\MftfApplicationConfig::GENERATION_PHASE, + true +); // Load needed framework env params $TEST_ENVS = [ @@ -42,6 +46,7 @@ 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', __DIR__); $utilDir = DIRECTORY_SEPARATOR . 'Util'. DIRECTORY_SEPARATOR . '*.php'; diff --git a/dev/tests/functional/MFTF.suite.dist.yml b/dev/tests/functional/MFTF.suite.dist.yml new file mode 100644 index 000000000..a54620385 --- /dev/null +++ b/dev/tests/functional/MFTF.suite.dist.yml @@ -0,0 +1,35 @@ +# Copyright © Magento, Inc. All rights reserved. +# See COPYING.txt for license details. + +# Codeception Test Suite Configuration +# +# Suite for acceptance tests. +# Perform tests in browser using the WebDriver or PhpBrowser. +# If you need both WebDriver and PHPBrowser tests - create a separate suite. + +class_name: AcceptanceTester +namespace: Magento\FunctionalTestingFramework +modules: + enabled: + - \Magento\FunctionalTestingFramework\Module\MagentoWebDriver + - \Magento\FunctionalTestingFramework\Helper\Acceptance + - \Magento\FunctionalTestingFramework\Helper\MagentoFakerData + - \Magento\FunctionalTestingFramework\Module\MagentoAssert + - Asserts + config: + \Magento\FunctionalTestingFramework\Module\MagentoWebDriver: + url: "%MAGENTO_BASE_URL%" + backend_name: "%MAGENTO_BACKEND_NAME%" + browser: '%BROWSER%' + window_size: maximize + username: "%MAGENTO_ADMIN_USERNAME%" + password: "%MAGENTO_ADMIN_PASSWORD%" + pageload_timeout: 30 + host: %SELENIUM_HOST% + port: %SELENIUM_PORT% + protocol: %SELENIUM_PROTOCOL% + path: %SELENIUM_PATH% + capabilities: + chromeOptions: + args: ["--start-maximized", "--disable-extensions", "--enable-automation"] + diff --git a/dev/tests/functional/MFTF/FunctionalTest/DevDocs/Page/MFTFDocPage.xml b/dev/tests/functional/MFTF/FunctionalTest/DevDocs/Page/MFTFDocPage.xml new file mode 100644 index 000000000..2b706ed67 --- /dev/null +++ b/dev/tests/functional/MFTF/FunctionalTest/DevDocs/Page/MFTFDocPage.xml @@ -0,0 +1,14 @@ + + + + + +
+ + diff --git a/dev/tests/functional/MFTF/FunctionalTest/DevDocs/Section/ContentSection.xml b/dev/tests/functional/MFTF/FunctionalTest/DevDocs/Section/ContentSection.xml new file mode 100644 index 000000000..cf82b69e9 --- /dev/null +++ b/dev/tests/functional/MFTF/FunctionalTest/DevDocs/Section/ContentSection.xml @@ -0,0 +1,14 @@ + + + + +
+ +
+
diff --git a/dev/tests/functional/MFTF/FunctionalTest/DevDocs/Test/DevDocsTest.xml b/dev/tests/functional/MFTF/FunctionalTest/DevDocs/Test/DevDocsTest.xml new file mode 100644 index 000000000..eceeb3f58 --- /dev/null +++ b/dev/tests/functional/MFTF/FunctionalTest/DevDocs/Test/DevDocsTest.xml @@ -0,0 +1,24 @@ + + + + + + + + + + <description value="Magento Functional Testing Framework Documentation is available."/> + <severity value="CRITICAL"/> + <group value="mftf"/> + </annotations> + + <amOnPage stepKey="openMFTFDevDocPage" url="{{MFTFDocPage.url}}" /> + <see stepKey="verifyPageIntroText" selector="{{contentSection.pageIntro}}" userInput="Introduction to the Magento Functional Testing Framework" /> + </test> +</tests> diff --git a/dev/tests/functional/MFTF/_bootstrap.php b/dev/tests/functional/MFTF/_bootstrap.php new file mode 100755 index 000000000..6a524e867 --- /dev/null +++ b/dev/tests/functional/MFTF/_bootstrap.php @@ -0,0 +1,7 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +require_once dirname(__DIR__) . DIRECTORY_SEPARATOR . '_bootstrap.php'; \ No newline at end of file diff --git a/dev/tests/functional/_bootstrap.php b/dev/tests/functional/_bootstrap.php new file mode 100755 index 000000000..e51658f23 --- /dev/null +++ b/dev/tests/functional/_bootstrap.php @@ -0,0 +1,35 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +define('PROJECT_ROOT', dirname(dirname(dirname(__DIR__)))); +require_once PROJECT_ROOT . '/vendor/autoload.php'; +$RELATIVE_FW_PATH = PROJECT_ROOT; + +//Load constants from .env file +if (file_exists(PROJECT_ROOT . '/.env')) { + $env = new \Dotenv\Loader(PROJECT_ROOT . '/.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('FW_BP') || define('FW_BP', PROJECT_ROOT); + +// add the debug flag here +$debug_mode = $_ENV['MFTF_DEBUG'] ?? false; +if (!(bool)$debug_mode && extension_loaded('xdebug')) { + xdebug_disable(); +} + +$RELATIVE_TESTS_MODULE_PATH = '/MFTF/FunctionalTest'; + +defined('TESTS_BP') || define('TESTS_BP', __DIR__); +defined('TESTS_MODULE_PATH') || define('TESTS_MODULE_PATH', TESTS_BP . $RELATIVE_TESTS_MODULE_PATH); diff --git a/dev/tests/unit/Magento/FunctionalTestFramework/Page/Handlers/PageObjectHandlerTest.php b/dev/tests/unit/Magento/FunctionalTestFramework/Page/Handlers/PageObjectHandlerTest.php index 62264569a..62e87022d 100644 --- a/dev/tests/unit/Magento/FunctionalTestFramework/Page/Handlers/PageObjectHandlerTest.php +++ b/dev/tests/unit/Magento/FunctionalTestFramework/Page/Handlers/PageObjectHandlerTest.php @@ -51,6 +51,25 @@ public function testGetPageObject() $this->assertNull($invalidPage); } + public function testGetEmptyPage() + { + $mockData = [ + "testPage1" => [ + "url" => "testURL1", + "module" => "testModule1", + "section" => [ + ], + "area" => "test" + ]]; + $this->setMockParserOutput($mockData); + + // get pages + $page = PageObjectHandler::getInstance()->getObject('testPage1'); + + // Empty page has been read in and gotten without an exception being thrown. + $this->addToAssertionCount(1); + } + /** * Function used to set mock for parser return and force init method to run between tests. * diff --git a/dev/tests/unit/Magento/FunctionalTestFramework/Suite/Handlers/SuiteObjectHandlerTest.php b/dev/tests/unit/Magento/FunctionalTestFramework/Suite/Handlers/SuiteObjectHandlerTest.php new file mode 100644 index 000000000..ede99bf34 --- /dev/null +++ b/dev/tests/unit/Magento/FunctionalTestFramework/Suite/Handlers/SuiteObjectHandlerTest.php @@ -0,0 +1,109 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Tests\unit\Magento\FunctionalTestFramework\Suite\Handlers; + +use AspectMock\Test as AspectMock; +use Magento\FunctionalTestingFramework\ObjectManager\ObjectManager; +use Magento\FunctionalTestingFramework\ObjectManagerFactory; +use Magento\FunctionalTestingFramework\Suite\Handlers\SuiteObjectHandler; +use Magento\FunctionalTestingFramework\Suite\Parsers\SuiteDataParser; +use Magento\FunctionalTestingFramework\Test\Handlers\TestObjectHandler; +use Magento\FunctionalTestingFramework\Test\Parsers\TestDataParser; +use PHPUnit\Framework\TestCase; +use tests\unit\Util\SuiteDataArrayBuilder; +use tests\unit\Util\TestDataArrayBuilder; + +class SuiteObjectHandlerTest extends TestCase +{ + /** + * Tests basic parsing and accesors of suite object and suite object supporting classes + */ + public function testGetSuiteObject() + { + $suiteDataArrayBuilder = new SuiteDataArrayBuilder(); + $mockData = $suiteDataArrayBuilder + ->withName('basicTestSuite') + ->withAfterHook() + ->withBeforeHook() + ->includeTests(['simpleTest']) + ->includeGroups(['group1']) + ->excludeTests(['group1Test2']) + ->excludeGroups(['group2']) + ->build(); + + $testDataArrayBuilder = new TestDataArrayBuilder(); + $mockSimpleTest = $testDataArrayBuilder + ->withName('simpleTest') + ->withTestActions() + ->build(); + + $mockGroup1Test1 = $testDataArrayBuilder + ->withName('group1Test1') + ->withAnnotations(['group' => [['value' => 'group1']]]) + ->withTestActions() + ->build(); + + $mockGroup1Test2 = $testDataArrayBuilder + ->withName('group1Test2') + ->withAnnotations(['group' => [['value' => 'group1']]]) + ->withTestActions() + ->build(); + + $mockGroup2Test1 = $testDataArrayBuilder + ->withName('group2Test1') + ->withAnnotations(['group' => [['value' => 'group2']]]) + ->withTestActions() + ->build(); + + $mockTestData = ['tests' => array_merge($mockSimpleTest, $mockGroup1Test1, $mockGroup1Test2, $mockGroup2Test1)]; + $this->setMockTestAndSuiteParserOutput($mockTestData, $mockData); + + // parse and retrieve suite object with mocked data + $basicTestSuiteObj = SuiteObjectHandler::getInstance()->getObject('basicTestSuite'); + + // assert on created suite object + $this->assertEquals($basicTestSuiteObj->getName(), 'basicTestSuite'); + $this->assertCount(2, $basicTestSuiteObj->getTests()); + $this->assertNotEmpty($basicTestSuiteObj->getBeforeHook()); + $this->assertNotEmpty($basicTestSuiteObj->getAfterHook()); + } + + /** + * 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) + { + // 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(); + $instance = AspectMock::double( + ObjectManager::class, + ['create' => function ($clazz) use ($mockDataParser, $mockSuiteDataParser) { + if ($clazz == TestDataParser::class) { + return $mockDataParser; + } + + if ($clazz == SuiteDataParser::class) { + return $mockSuiteDataParser; + } + }] + )->make(); + // bypass the private constructor + AspectMock::double(ObjectManagerFactory::class, ['getObjectManager' => $instance]); + } +} diff --git a/dev/tests/unit/Magento/FunctionalTestFramework/Test/Config/ActionGroupDomTest.php b/dev/tests/unit/Magento/FunctionalTestFramework/Test/Config/ActionGroupDomTest.php new file mode 100644 index 000000000..b7ea66f65 --- /dev/null +++ b/dev/tests/unit/Magento/FunctionalTestFramework/Test/Config/ActionGroupDomTest.php @@ -0,0 +1,32 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Tests\unit\Magento\FunctionalTestFramework\Test\Config; + +use Magento\FunctionalTestingFramework\Exceptions\Collector\ExceptionCollector; +use Magento\FunctionalTestingFramework\Test\Config\ActionGroupDom; +use PHPUnit\Framework\TestCase; + +class ActionGroupDomTest extends TestCase +{ + /** + * Test Action Group duplicate step key validation + */ + public function testActionGroupDomStepKeyValidation() + { + $sampleXml = "<actionGroups> + <actionGroup name=\"actionGroupWithoutArguments\"> + <wait time=\"1\" stepKey=\"waitForNothing\" /> + <wait time=\"2\" stepKey=\"waitForNothing\" /> + </actionGroup> + </actionGroups>"; + + $exceptionCollector = new ExceptionCollector(); + $actionDom = new ActionGroupDom($sampleXml, 'dupeStepKeyActionGroup.xml', $exceptionCollector); + + $this->expectException(\Exception::class); + $exceptionCollector->throwException(); + } +} diff --git a/dev/tests/unit/Magento/FunctionalTestFramework/Test/Objects/ActionObjectTest.php b/dev/tests/unit/Magento/FunctionalTestFramework/Test/Objects/ActionObjectTest.php index 98646d0c4..b5aa8c4b1 100644 --- a/dev/tests/unit/Magento/FunctionalTestFramework/Test/Objects/ActionObjectTest.php +++ b/dev/tests/unit/Magento/FunctionalTestFramework/Test/Objects/ActionObjectTest.php @@ -255,6 +255,36 @@ public function testResolveDataInUserInput() $this->assertEquals($expected, $actionObject->getCustomActionAttributes()); } + /** + * {{EntityDataObject.values}} should be replaced with ["value1","value2"] + */ + public function testResolveArrayData() + { + // Set up mocks + $actionObject = new ActionObject('merge123', 'fillField', [ + 'selector' => '#selector', + 'userInput' => '{{EntityDataObject.values}}' + ]); + $entityDataObject = new EntityDataObject('EntityDataObject', 'test', [ + 'values' => [ + 'value1', + 'value2', + '"My" Value' + ] + ], [], '', ''); + $this->mockDataHandlerWithData($entityDataObject); + + // Call the method under test + $actionObject->resolveReferences(); + + // Verify + $expected = [ + 'selector' => '#selector', + 'userInput' => '["value1","value2","\"My\" Value"]' + ]; + $this->assertEquals($expected, $actionObject->getCustomActionAttributes()); + } + /** * Action object should throw an exception if a reference to a parameterized selector has too few given args. */ diff --git a/dev/tests/unit/Magento/FunctionalTestFramework/Test/Util/ActionObjectExtractorTest.php b/dev/tests/unit/Magento/FunctionalTestFramework/Test/Util/ActionObjectExtractorTest.php new file mode 100644 index 000000000..873e622f7 --- /dev/null +++ b/dev/tests/unit/Magento/FunctionalTestFramework/Test/Util/ActionObjectExtractorTest.php @@ -0,0 +1,106 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace tests\unit\Magento\FunctionalTestFramework\Test\Util; + +use Magento\FunctionalTestingFramework\Test\Objects\ActionObject; +use Magento\FunctionalTestingFramework\Test\Util\ActionObjectExtractor; +use PHPUnit\Framework\TestCase; + +class ActionObjectExtractorTest extends TestCase +{ + /** @var ActionObjectExtractor */ + private $testActionObjectExtractor; + + /** + * Setup method + */ + public function setUp() + { + $this->testActionObjectExtractor = new ActionObjectExtractor(); + } + + /** + * Tests basic action object extraction with a valid parser array. + */ + public function testBasicActionObjectExtration() + { + $actionObjects = $this->testActionObjectExtractor->extractActions($this->createBasicActionObjectArray()); + $this->assertCount(1, $actionObjects); + + /** @var ActionObject $firstElement */ + $firstElement = array_values($actionObjects)[0]; + $this->assertEquals('testAction1', $firstElement->getStepKey()); + $this->assertCount(1, $firstElement->getCustomActionAttributes()); + } + + /** + * Tests an invalid merge order reference (i.e. a step referencing itself). + */ + 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'); + } + + /** + * Validates a warning is printed to the console when multiple actions reference the same actions for merging. + */ + public function testAmbiguousMergeOrderRefernece() + { + $ambiguousArray = $this->createBasicActionObjectArray('testAction1'); + $ambiguousArray = array_merge( + $ambiguousArray, + $this->createBasicActionObjectArray('testAction2', 'testAction1') + ); + + $ambiguousArray = array_merge( + $ambiguousArray, + $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'); + } + + /** + * Utility function to return mock parser output for testing extraction into ActionObjects. + * + * @param string $stepKey + * @param string $before + * @param string $after + * @return array + */ + private function createBasicActionObjectArray($stepKey = 'testAction1', $before = null, $after = null) + { + $baseArray = [ + $stepKey => [ + "nodeName" => "sampleAction", + "stepKey" => $stepKey, + "someAttribute" => "someAttributeValue" + ] + ]; + + if ($before) { + $baseArray[$stepKey] = array_merge($baseArray[$stepKey], ['before' => $before]); + } + + if ($after) { + $baseArray[$stepKey] = array_merge($baseArray[$stepKey], ['after' => $after]); + } + + return $baseArray; + } +} diff --git a/dev/tests/unit/Magento/FunctionalTestFramework/Util/Sorter/ParallelGroupSorterTest.php b/dev/tests/unit/Magento/FunctionalTestFramework/Util/Sorter/ParallelGroupSorterTest.php new file mode 100644 index 000000000..1acd31522 --- /dev/null +++ b/dev/tests/unit/Magento/FunctionalTestFramework/Util/Sorter/ParallelGroupSorterTest.php @@ -0,0 +1,112 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Tests\unit\Magento\FunctionalTestFramework\Util\Sorter; + +use AspectMock\Test as AspectMock; +use Magento\FunctionalTestingFramework\Suite\Handlers\SuiteObjectHandler; +use Magento\FunctionalTestingFramework\Suite\Objects\SuiteObject; +use Magento\FunctionalTestingFramework\Test\Handlers\TestObjectHandler; +use Magento\FunctionalTestingFramework\Test\Objects\TestObject; +use Magento\FunctionalTestingFramework\Util\Sorter\ParallelGroupSorter; +use PHPUnit\Framework\TestCase; + +class ParallelGroupSorterTest extends TestCase +{ + /** + * Test a basic sort of available tests based on size + */ + public function testBasicTestGroupSort() + { + $sampleTestArray = [ + 'test1' => 100, + 'test2' => 300, + 'test3' => 50, + 'test4' => 60, + 'test5' => 25, + 'test6' => 125, + 'test7' => 250, + 'test8' => 1, + 'test9' => 80, + 'test10' => 25 + ]; + + $expectedResult = [ + 1 => ['test2'], + 2 => ['test7'], + 3 => ['test6', 'test9'], + 4 => ['test1', 'test4', 'test3'], + 5 => ['test5', 'test10', 'test8'] + ]; + + $testSorter = new ParallelGroupSorter(); + $actualResult = $testSorter->getTestsGroupedBySize([], $sampleTestArray, 200); + + $this->assertCount(5, $actualResult); + + foreach ($actualResult as $gropuNumber => $actualTests) { + $expectedTests = $expectedResult[$gropuNumber]; + $this->assertEquals($expectedTests, array_keys($actualTests)); + } + } + + /** + * Test a sort of both tests and a suite which is larger than the given line limitation + */ + 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(); + + $mockHandler = AspectMock::double( + TestObjectHandler::class, + ['getObject' => function () use ($mockTest1) { + return $mockTest1; + }] + )->make(); + + AspectMock::double(TestObjectHandler::class, ['getInstance' => $mockHandler])->make(); + + // create test to size array + $sampleTestArray = [ + 'test1' => 100, + 'test2' => 300, + 'test3' => 500, + 'test4' => 60, + 'test5' => 125 + ]; + + // create mock suite references + $sampleSuiteArray = [ + 'mockSuite1' => ['mockTest1', 'mockTest2'] + ]; + + // perform sort + $testSorter = new ParallelGroupSorter(); + $actualResult = $testSorter->getTestsGroupedBySize($sampleSuiteArray, $sampleTestArray, 200); + + // verify the resulting groups + $this->assertCount(5, $actualResult); + + $expectedResults = [ + 1 => ['test3'], + 2 => ['test2'], + 3 => ['mockSuite1_0'], + 4 => ['mockSuite1_1'], + 5 => ['test5', 'test4', 'test1'] + ]; + + foreach ($actualResult as $groupNum => $group) { + $this->assertEquals($expectedResults[$groupNum], array_keys($group)); + } + } +} diff --git a/dev/tests/unit/Magento/FunctionalTestFramework/Test/Util/TestNameValidationUtilTest.php b/dev/tests/unit/Magento/FunctionalTestFramework/Util/Validation/NameValidationUtilTest.php similarity index 90% rename from dev/tests/unit/Magento/FunctionalTestFramework/Test/Util/TestNameValidationUtilTest.php rename to dev/tests/unit/Magento/FunctionalTestFramework/Util/Validation/NameValidationUtilTest.php index 8b50062ed..595033012 100644 --- a/dev/tests/unit/Magento/FunctionalTestFramework/Test/Util/TestNameValidationUtilTest.php +++ b/dev/tests/unit/Magento/FunctionalTestFramework/Util/Validation/NameValidationUtilTest.php @@ -7,10 +7,10 @@ namespace tests\unit\Magento\FunctionalTestFramework\Test\Util; use Magento\FunctionalTestingFramework\Exceptions\XmlException; -use Magento\FunctionalTestingFramework\Test\Util\TestNameValidationUtil; +use Magento\FunctionalTestingFramework\Util\Validation\NameValidationUtil; use PHPUnit\Framework\TestCase; -class TestNameValidationUtilTest extends TestCase +class NameValidationUtilTest extends TestCase { /** * Validate name with curly braces throws exception @@ -69,6 +69,6 @@ public function testSpacesInTestName() private function validateBlacklistedTestName($testName) { $this->expectException(XmlException::class); - TestNameValidationUtil::validateName($testName); + NameValidationUtil::validateName($testName, "Test"); } } diff --git a/dev/tests/unit/Util/SuiteDataArrayBuilder.php b/dev/tests/unit/Util/SuiteDataArrayBuilder.php new file mode 100644 index 000000000..85411b447 --- /dev/null +++ b/dev/tests/unit/Util/SuiteDataArrayBuilder.php @@ -0,0 +1,234 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace tests\unit\Util; + +use Magento\FunctionalTestingFramework\Suite\Util\SuiteObjectExtractor; +use Magento\FunctionalTestingFramework\Test\Util\ActionObjectExtractor; +use Magento\FunctionalTestingFramework\Test\Util\TestObjectExtractor; + +class SuiteDataArrayBuilder +{ + /** + * Mock test name + * + * @var string + */ + private $name; + + /** + * Mock before action name + * + * @var string + */ + private $testActionBeforeName = 'testActionBefore'; + + /** + * Mock after action name + * + * @var string + */ + private $testActionAfterName = 'testActionAfter'; + + /** + * Array containing before hook actions + * + * @var array + */ + private $beforeHook = []; + + /** + * Arrat containing after hook actions + * + * @var array + */ + private $afterHook = []; + + /** + * Array which contains tests, groups, modules included as part of suite + * + * @var array + */ + private $includes = []; + + /** + * Array which contains, tests, groups, module excluded from suite + * + * @var array + */ + private $excludes = []; + + /** + * Mock test action type + * + * @var string + */ + public $testActionType = 'testAction'; + + /** + * Function which sets the name of the mock suite array + * + * @param string $name + * @return $this + */ + public function withName($name) + { + $this->name = $name; + return $this; + } + + /** + * Function which takes an array of test names and formats them as included raw suite data + * + * @param array $tests + * @return $this + */ + public function includeTests($tests) + { + $this->includes = $this->appendEntriesToSuiteContents($this->includes, 'test', $tests); + return $this; + } + + /** + * Function which takes an array of test names and formats them as excluded raw suite data + * + * @param array $tests + * @return $this + */ + public function excludeTests($tests) + { + $this->excludes = $this->appendEntriesToSuiteContents($this->excludes, 'test', $tests); + return $this; + } + + /** + * Function which takes an array of group names and formats them as included raw suite data + * + * @param array $groups + * @return $this + */ + public function includeGroups($groups) + { + $this->includes = $this->appendEntriesToSuiteContents($this->includes, 'group', $groups); + return $this; + } + + /** + * Function which takes an array of group names and formats them as excluded raw suite data + * + * @param array $groups + * @return $this + */ + public function excludeGroups($groups) + { + $this->excludes = $this->appendEntriesToSuiteContents($this->excludes, 'groups', $groups); + return $this; + } + + /** + * Function which takes an array of module names and formats them as included raw suite data + * + * @param array $modules + * @return $this + */ + public function includeModules($modules) + { + $this->includes = $this->appendEntriesToSuiteContents($this->includes, 'module', $modules); + return $this; + } + + /** + * Function which takes an array of group names and formats them as excluded raw suite data + * + * @param array $modules + * @return $this + */ + public function excludeModules($modules) + { + $this->excludes = $this->appendEntriesToSuiteContents($this->excludes, 'module', $modules); + return $this; + } + + /** + * Function which takes an array of current include/exclude contents, a type (group, module, or test) and contents + * to be appended to the array and returns a propelry formatted array representative of parsed suite data. + * + * @param array $currentContents + * @param string $type + * @param array $contents + * @return array + */ + private function appendEntriesToSuiteContents($currentContents, $type, $contents) + { + $newContents = $currentContents; + foreach ($contents as $entry) { + $newContents[$entry] = [ + SuiteObjectExtractor::NODE_NAME => $type, + SuiteObjectExtractor::NAME => $entry + ]; + } + + return $newContents; + } + + /** + * Add an after hook passed in by arg (or default if no arg) + * + * @param null $afterHook + * @return $this + */ + public function withAfterHook($afterHook = null) + { + if ($afterHook == null) { + $this->afterHook = [$this->testActionAfterName => [ + ActionObjectExtractor::NODE_NAME => $this->testActionType, + ActionObjectExtractor::TEST_STEP_MERGE_KEY => $this->testActionAfterName + + ]]; + } else { + $this->afterHook = $afterHook; + } + + return $this; + } + + /** + * Add a before hook passed in by arg (or default if no arg) + * + * @param null $beforeHook + * @return $this + */ + public function withBeforeHook($beforeHook = null) + { + if ($beforeHook == null) { + $this->beforeHook = [$this->testActionBeforeName => [ + ActionObjectExtractor::NODE_NAME => $this->testActionType, + ActionObjectExtractor::TEST_STEP_MERGE_KEY => $this->testActionBeforeName + ]]; + } else { + $this->beforeHook = $beforeHook; + } + + return $this; + } + + /** + * Function which takes all class properties set and generates an array representing suite data as parsed from xml. + * + * @return array + */ + public function build() + { + return ['suites' => [ + $this->name => [ + SuiteObjectExtractor::NAME => $this->name, + TestObjectExtractor::TEST_BEFORE_HOOK => $this->beforeHook, + TestObjectExtractor::TEST_AFTER_HOOK => $this->afterHook, + SuiteObjectExtractor::INCLUDE_TAG_NAME => $this->includes, + SuiteObjectExtractor::EXCLUDE_TAG_NAME => $this->excludes + ] + ]]; + } +} diff --git a/dev/tests/verification/Resources/ActionGroupUsingNestedArgument.txt b/dev/tests/verification/Resources/ActionGroupUsingNestedArgument.txt new file mode 100644 index 000000000..b978a3188 --- /dev/null +++ b/dev/tests/verification/Resources/ActionGroupUsingNestedArgument.txt @@ -0,0 +1,33 @@ +<?php +namespace Magento\AcceptanceTest\_default\Backend; + +use Magento\FunctionalTestingFramework\AcceptanceTester; +use Magento\FunctionalTestingFramework\DataGenerator\Handlers\DataObjectHandler; +use Magento\FunctionalTestingFramework\DataGenerator\Persist\DataPersistenceHandler; +use Magento\FunctionalTestingFramework\DataGenerator\Objects\EntityDataObject; +use \Codeception\Util\Locator; +use Yandex\Allure\Adapter\Annotation\Features; +use Yandex\Allure\Adapter\Annotation\Stories; +use Yandex\Allure\Adapter\Annotation\Title; +use Yandex\Allure\Adapter\Annotation\Description; +use Yandex\Allure\Adapter\Annotation\Parameter; +use Yandex\Allure\Adapter\Annotation\Severity; +use Yandex\Allure\Adapter\Model\SeverityLevel; +use Yandex\Allure\Adapter\Annotation\TestCaseId; + +/** + */ +class ActionGroupUsingNestedArgumentCest +{ + /** + * @Parameter(name = "AcceptanceTester", value="$I") + * @param AcceptanceTester $I + * @return void + * @throws \Exception + */ + public function ActionGroupUsingNestedArgument(AcceptanceTester $I) + { + $grabProductsActionGroup = $I->grabMultiple("selector"); + $I->assertCount(99, $grabProductsActionGroup); + } +} diff --git a/dev/tests/verification/Resources/ActionGroupWithDataOverrideTest.txt b/dev/tests/verification/Resources/ActionGroupWithDataOverrideTest.txt index b46508433..0809289b3 100644 --- a/dev/tests/verification/Resources/ActionGroupWithDataOverrideTest.txt +++ b/dev/tests/verification/Resources/ActionGroupWithDataOverrideTest.txt @@ -1,5 +1,5 @@ <?php -namespace Magento\AcceptanceTest\_generated\Backend; +namespace Magento\AcceptanceTest\_default\Backend; use Magento\FunctionalTestingFramework\AcceptanceTester; use Magento\FunctionalTestingFramework\DataGenerator\Handlers\DataObjectHandler; @@ -57,7 +57,6 @@ class ActionGroupWithDataOverrideTestCest public function _failed(AcceptanceTester $I) { $I->saveScreenshot(); - $this->_after($I); } /** diff --git a/dev/tests/verification/Resources/ActionGroupWithDataTest.txt b/dev/tests/verification/Resources/ActionGroupWithDataTest.txt index ec74168b8..fe3cf57c1 100644 --- a/dev/tests/verification/Resources/ActionGroupWithDataTest.txt +++ b/dev/tests/verification/Resources/ActionGroupWithDataTest.txt @@ -1,5 +1,5 @@ <?php -namespace Magento\AcceptanceTest\_generated\Backend; +namespace Magento\AcceptanceTest\_default\Backend; use Magento\FunctionalTestingFramework\AcceptanceTester; use Magento\FunctionalTestingFramework\DataGenerator\Handlers\DataObjectHandler; @@ -57,7 +57,6 @@ class ActionGroupWithDataTestCest public function _failed(AcceptanceTester $I) { $I->saveScreenshot(); - $this->_after($I); } /** diff --git a/dev/tests/verification/Resources/ActionGroupWithDefaultArgumentAndStringSelectorParam.txt b/dev/tests/verification/Resources/ActionGroupWithDefaultArgumentAndStringSelectorParam.txt index de83088a1..9c56afff4 100644 --- a/dev/tests/verification/Resources/ActionGroupWithDefaultArgumentAndStringSelectorParam.txt +++ b/dev/tests/verification/Resources/ActionGroupWithDefaultArgumentAndStringSelectorParam.txt @@ -1,5 +1,5 @@ <?php -namespace Magento\AcceptanceTest\_generated\Backend; +namespace Magento\AcceptanceTest\_default\Backend; use Magento\FunctionalTestingFramework\AcceptanceTester; use Magento\FunctionalTestingFramework\DataGenerator\Handlers\DataObjectHandler; diff --git a/dev/tests/verification/Resources/ActionGroupWithMultipleParameterSelectorsFromDefaultArgument.txt b/dev/tests/verification/Resources/ActionGroupWithMultipleParameterSelectorsFromDefaultArgument.txt index d1457b121..aa213c80f 100644 --- a/dev/tests/verification/Resources/ActionGroupWithMultipleParameterSelectorsFromDefaultArgument.txt +++ b/dev/tests/verification/Resources/ActionGroupWithMultipleParameterSelectorsFromDefaultArgument.txt @@ -1,5 +1,5 @@ <?php -namespace Magento\AcceptanceTest\_generated\Backend; +namespace Magento\AcceptanceTest\_default\Backend; use Magento\FunctionalTestingFramework\AcceptanceTester; use Magento\FunctionalTestingFramework\DataGenerator\Handlers\DataObjectHandler; diff --git a/dev/tests/verification/Resources/ActionGroupWithNoArguments.txt b/dev/tests/verification/Resources/ActionGroupWithNoArguments.txt index 591241f09..45807663a 100644 --- a/dev/tests/verification/Resources/ActionGroupWithNoArguments.txt +++ b/dev/tests/verification/Resources/ActionGroupWithNoArguments.txt @@ -1,5 +1,5 @@ <?php -namespace Magento\AcceptanceTest\_generated\Backend; +namespace Magento\AcceptanceTest\_default\Backend; use Magento\FunctionalTestingFramework\AcceptanceTester; use Magento\FunctionalTestingFramework\DataGenerator\Handlers\DataObjectHandler; diff --git a/dev/tests/verification/Resources/ActionGroupWithNoDefaultTest.txt b/dev/tests/verification/Resources/ActionGroupWithNoDefaultTest.txt index 0c022f825..5cc1d7f39 100644 --- a/dev/tests/verification/Resources/ActionGroupWithNoDefaultTest.txt +++ b/dev/tests/verification/Resources/ActionGroupWithNoDefaultTest.txt @@ -1,5 +1,5 @@ <?php -namespace Magento\AcceptanceTest\_generated\Backend; +namespace Magento\AcceptanceTest\_default\Backend; use Magento\FunctionalTestingFramework\AcceptanceTester; use Magento\FunctionalTestingFramework\DataGenerator\Handlers\DataObjectHandler; @@ -57,7 +57,6 @@ class ActionGroupWithNoDefaultTestCest public function _failed(AcceptanceTester $I) { $I->saveScreenshot(); - $this->_after($I); } /** diff --git a/dev/tests/verification/Resources/ActionGroupWithPassedArgumentAndStringSelectorParam.txt b/dev/tests/verification/Resources/ActionGroupWithPassedArgumentAndStringSelectorParam.txt index b3a6cc89f..043a71c94 100644 --- a/dev/tests/verification/Resources/ActionGroupWithPassedArgumentAndStringSelectorParam.txt +++ b/dev/tests/verification/Resources/ActionGroupWithPassedArgumentAndStringSelectorParam.txt @@ -1,5 +1,5 @@ <?php -namespace Magento\AcceptanceTest\_generated\Backend; +namespace Magento\AcceptanceTest\_default\Backend; use Magento\FunctionalTestingFramework\AcceptanceTester; use Magento\FunctionalTestingFramework\DataGenerator\Handlers\DataObjectHandler; @@ -29,6 +29,6 @@ class ActionGroupWithPassedArgumentAndStringSelectorParamCest */ public function ActionGroupWithPassedArgumentAndStringSelectorParam(AcceptanceTester $I) { - $I->see("John".msq("UniquePerson"), "#element .test1"); + $I->see("John" . msq("UniquePerson"), "#element .test1"); } } diff --git a/dev/tests/verification/Resources/ActionGroupWithPersistedData.txt b/dev/tests/verification/Resources/ActionGroupWithPersistedData.txt index 2a15e8394..2707770d5 100644 --- a/dev/tests/verification/Resources/ActionGroupWithPersistedData.txt +++ b/dev/tests/verification/Resources/ActionGroupWithPersistedData.txt @@ -1,5 +1,5 @@ <?php -namespace Magento\AcceptanceTest\_generated\Backend; +namespace Magento\AcceptanceTest\_default\Backend; use Magento\FunctionalTestingFramework\AcceptanceTester; use Magento\FunctionalTestingFramework\DataGenerator\Handlers\DataObjectHandler; @@ -57,7 +57,6 @@ class ActionGroupWithPersistedDataCest public function _failed(AcceptanceTester $I) { $I->saveScreenshot(); - $this->_after($I); } /** diff --git a/dev/tests/verification/Resources/ActionGroupWithSimpleDataUsageFromDefaultArgument.txt b/dev/tests/verification/Resources/ActionGroupWithSimpleDataUsageFromDefaultArgument.txt index 37c9977ed..3774e639d 100644 --- a/dev/tests/verification/Resources/ActionGroupWithSimpleDataUsageFromDefaultArgument.txt +++ b/dev/tests/verification/Resources/ActionGroupWithSimpleDataUsageFromDefaultArgument.txt @@ -1,5 +1,5 @@ <?php -namespace Magento\AcceptanceTest\_generated\Backend; +namespace Magento\AcceptanceTest\_default\Backend; use Magento\FunctionalTestingFramework\AcceptanceTester; use Magento\FunctionalTestingFramework\DataGenerator\Handlers\DataObjectHandler; diff --git a/dev/tests/verification/Resources/ActionGroupWithSimpleDataUsageFromPassedArgument.txt b/dev/tests/verification/Resources/ActionGroupWithSimpleDataUsageFromPassedArgument.txt index 9e385f72b..a1e5a316b 100644 --- a/dev/tests/verification/Resources/ActionGroupWithSimpleDataUsageFromPassedArgument.txt +++ b/dev/tests/verification/Resources/ActionGroupWithSimpleDataUsageFromPassedArgument.txt @@ -1,5 +1,5 @@ <?php -namespace Magento\AcceptanceTest\_generated\Backend; +namespace Magento\AcceptanceTest\_default\Backend; use Magento\FunctionalTestingFramework\AcceptanceTester; use Magento\FunctionalTestingFramework\DataGenerator\Handlers\DataObjectHandler; diff --git a/dev/tests/verification/Resources/ActionGroupWithSingleParameterSelectorFromDefaultArgument.txt b/dev/tests/verification/Resources/ActionGroupWithSingleParameterSelectorFromDefaultArgument.txt index 6fbf041b6..d04e2ba80 100644 --- a/dev/tests/verification/Resources/ActionGroupWithSingleParameterSelectorFromDefaultArgument.txt +++ b/dev/tests/verification/Resources/ActionGroupWithSingleParameterSelectorFromDefaultArgument.txt @@ -1,5 +1,5 @@ <?php -namespace Magento\AcceptanceTest\_generated\Backend; +namespace Magento\AcceptanceTest\_default\Backend; use Magento\FunctionalTestingFramework\AcceptanceTester; use Magento\FunctionalTestingFramework\DataGenerator\Handlers\DataObjectHandler; diff --git a/dev/tests/verification/Resources/ActionGroupWithSingleParameterSelectorFromPassedArgument.txt b/dev/tests/verification/Resources/ActionGroupWithSingleParameterSelectorFromPassedArgument.txt index b74fb7179..fb10e7847 100644 --- a/dev/tests/verification/Resources/ActionGroupWithSingleParameterSelectorFromPassedArgument.txt +++ b/dev/tests/verification/Resources/ActionGroupWithSingleParameterSelectorFromPassedArgument.txt @@ -1,5 +1,5 @@ <?php -namespace Magento\AcceptanceTest\_generated\Backend; +namespace Magento\AcceptanceTest\_default\Backend; use Magento\FunctionalTestingFramework\AcceptanceTester; use Magento\FunctionalTestingFramework\DataGenerator\Handlers\DataObjectHandler; @@ -29,6 +29,6 @@ class ActionGroupWithSingleParameterSelectorFromPassedArgumentCest */ public function ActionGroupWithSingleParameterSelectorFromPassedArgument(AcceptanceTester $I) { - $I->see("Doe", "#element .John".msq("UniquePerson")); + $I->see("Doe", "#element .John" . msq("UniquePerson")); } } diff --git a/dev/tests/verification/Resources/ActionGroupWithStepKeyReferences.txt b/dev/tests/verification/Resources/ActionGroupWithStepKeyReferences.txt index 8a56a6906..ac97746b8 100644 --- a/dev/tests/verification/Resources/ActionGroupWithStepKeyReferences.txt +++ b/dev/tests/verification/Resources/ActionGroupWithStepKeyReferences.txt @@ -1,5 +1,5 @@ <?php -namespace Magento\AcceptanceTest\_generated\Backend; +namespace Magento\AcceptanceTest\_default\Backend; use Magento\FunctionalTestingFramework\AcceptanceTester; use Magento\FunctionalTestingFramework\DataGenerator\Handlers\DataObjectHandler; diff --git a/dev/tests/verification/Resources/ActionGroupWithTopLevelPersistedData.txt b/dev/tests/verification/Resources/ActionGroupWithTopLevelPersistedData.txt index aaa1378ca..743864b79 100644 --- a/dev/tests/verification/Resources/ActionGroupWithTopLevelPersistedData.txt +++ b/dev/tests/verification/Resources/ActionGroupWithTopLevelPersistedData.txt @@ -1,5 +1,5 @@ <?php -namespace Magento\AcceptanceTest\_generated\Backend; +namespace Magento\AcceptanceTest\_default\Backend; use Magento\FunctionalTestingFramework\AcceptanceTester; use Magento\FunctionalTestingFramework\DataGenerator\Handlers\DataObjectHandler; @@ -57,7 +57,6 @@ class ActionGroupWithTopLevelPersistedDataCest public function _failed(AcceptanceTester $I) { $I->saveScreenshot(); - $this->_after($I); } /** diff --git a/dev/tests/verification/Resources/ArgumentWithSameNameAsElement.txt b/dev/tests/verification/Resources/ArgumentWithSameNameAsElement.txt index 05fddcfa3..bb59a6758 100644 --- a/dev/tests/verification/Resources/ArgumentWithSameNameAsElement.txt +++ b/dev/tests/verification/Resources/ArgumentWithSameNameAsElement.txt @@ -1,5 +1,5 @@ <?php -namespace Magento\AcceptanceTest\_generated\Backend; +namespace Magento\AcceptanceTest\_default\Backend; use Magento\FunctionalTestingFramework\AcceptanceTester; use Magento\FunctionalTestingFramework\DataGenerator\Handlers\DataObjectHandler; @@ -57,7 +57,6 @@ class ArgumentWithSameNameAsElementCest public function _failed(AcceptanceTester $I) { $I->saveScreenshot(); - $this->_after($I); } /** diff --git a/dev/tests/verification/Resources/AssertTest.txt b/dev/tests/verification/Resources/AssertTest.txt index c34065533..60eed6a8d 100644 --- a/dev/tests/verification/Resources/AssertTest.txt +++ b/dev/tests/verification/Resources/AssertTest.txt @@ -1,5 +1,5 @@ <?php -namespace Magento\AcceptanceTest\_generated\Backend; +namespace Magento\AcceptanceTest\_default\Backend; use Magento\FunctionalTestingFramework\AcceptanceTester; use Magento\FunctionalTestingFramework\DataGenerator\Handlers\DataObjectHandler; diff --git a/dev/tests/verification/Resources/BasicActionGroupTest.txt b/dev/tests/verification/Resources/BasicActionGroupTest.txt index dae0f2aba..86835364e 100644 --- a/dev/tests/verification/Resources/BasicActionGroupTest.txt +++ b/dev/tests/verification/Resources/BasicActionGroupTest.txt @@ -1,5 +1,5 @@ <?php -namespace Magento\AcceptanceTest\_generated\Backend; +namespace Magento\AcceptanceTest\_default\Backend; use Magento\FunctionalTestingFramework\AcceptanceTester; use Magento\FunctionalTestingFramework\DataGenerator\Handlers\DataObjectHandler; diff --git a/dev/tests/verification/Resources/BasicFunctionalTest.txt b/dev/tests/verification/Resources/BasicFunctionalTest.txt index 9d4a0a3dd..57bd9dd01 100644 --- a/dev/tests/verification/Resources/BasicFunctionalTest.txt +++ b/dev/tests/verification/Resources/BasicFunctionalTest.txt @@ -1,5 +1,5 @@ <?php -namespace Magento\AcceptanceTest\_generated\Backend; +namespace Magento\AcceptanceTest\_default\Backend; use Magento\FunctionalTestingFramework\AcceptanceTester; use Magento\FunctionalTestingFramework\DataGenerator\Handlers\DataObjectHandler; @@ -46,7 +46,6 @@ class BasicFunctionalTestCest public function _failed(AcceptanceTester $I) { $I->saveScreenshot(); - $this->_after($I); } /** @@ -60,6 +59,8 @@ class BasicFunctionalTestCest */ public function BasicFunctionalTest(AcceptanceTester $I) { + $I->comment(""); + $I->comment(""); $someVarDefinition = $I->grabValueFrom(); $I->acceptPopup(); $I->amOnPage("/test/url"); @@ -70,8 +71,13 @@ class BasicFunctionalTestCest $I->click(".functionalTestSelector"); $I->clickWithLeftButton(".functionalTestSelector"); $I->clickWithRightButton(".functionalTestSelector"); + $I->clickWithLeftButton("#element#element .200", 200, 300); + $I->clickWithRightButton("#element .4123#element", 200, 300); $I->closeTab(); $I->conditionalClick(".functionalTestSelector", ".functionalDependentTestSelector", true); + $I->amGoingTo("delete entity that has the createDataKey: createKey1"); + $createKey1->deleteEntity(); + $I->deleteEntityByUrl("/V1/categories{$grabbedData}"); $I->dontSee("someInput", ".functionalTestSelector"); $I->dontSeeCheckboxIsChecked(".functionalTestSelector"); $I->dontSeeCookie("someInput"); @@ -88,7 +94,8 @@ class BasicFunctionalTestCest $I->dontSeeOptionIsSelected(".functionalTestSelector", "someInput"); $I->doubleClick(".functionalTestSelector"); $I->dragAndDrop(".functionalTestSelector", ".functionalTestSelector2"); - $I->executeJS("someJSFunction"); + $I->dragAndDrop(".functionalTestSelector", ".functionalTestSelector2", 100, 900); + $executeJSKey1 = $I->executeJS("someJSFunction"); $I->fillField(".functionalTestSelector", "someInput"); $I->fillField(".functionalTestSelector", "0"); $grabAttributeFromKey1 = $I->grabAttributeFrom(".functionalTestSelector", "someInput"); @@ -106,6 +113,7 @@ class BasicFunctionalTestCest $I->moveMouseOver(".functionalTestSelector"); $I->openNewTab(); $I->pauseExecution(); + $I->performOn("#selector", function(\WebDriverElement $el) {return $el->isDisplayed();}); $I->pressKey("#page", "a"); $I->pressKey("#page", ['ctrl','a'],'new'); $I->pressKey("#page", ['shift','111'],'1','x'); @@ -144,6 +152,7 @@ class BasicFunctionalTestCest $I->waitForElement(".functionalTestSelector", 30); $I->waitForElementNotVisible(".functionalTestSelector", 30); $I->waitForElementVisible(".functionalTestSelector", 30); + $I->waitForElementChange("#selector", function(\WebDriverElement $el) {return $el->isDisplayed();}); $I->waitForJS("someJsFunction", 30); $I->waitForText("someInput", 30, ".functionalTestSelector"); } diff --git a/dev/tests/verification/Resources/BasicMergeTest.txt b/dev/tests/verification/Resources/BasicMergeTest.txt index 0b457be66..2cecb803f 100644 --- a/dev/tests/verification/Resources/BasicMergeTest.txt +++ b/dev/tests/verification/Resources/BasicMergeTest.txt @@ -1,5 +1,5 @@ <?php -namespace Magento\AcceptanceTest\_generated\Backend; +namespace Magento\AcceptanceTest\_default\Backend; use Magento\FunctionalTestingFramework\AcceptanceTester; use Magento\FunctionalTestingFramework\DataGenerator\Handlers\DataObjectHandler; @@ -48,7 +48,6 @@ class BasicMergeTestCest public function _failed(AcceptanceTester $I) { $I->saveScreenshot(); - $this->_after($I); } /** diff --git a/dev/tests/verification/Resources/CharacterReplacementTest.txt b/dev/tests/verification/Resources/CharacterReplacementTest.txt index 18c3ed1f8..4ae325fba 100644 --- a/dev/tests/verification/Resources/CharacterReplacementTest.txt +++ b/dev/tests/verification/Resources/CharacterReplacementTest.txt @@ -1,5 +1,5 @@ <?php -namespace Magento\AcceptanceTest\_generated\Backend; +namespace Magento\AcceptanceTest\_default\Backend; use Magento\FunctionalTestingFramework\AcceptanceTester; use Magento\FunctionalTestingFramework\DataGenerator\Handlers\DataObjectHandler; diff --git a/dev/tests/verification/Resources/DataReplacementTest.txt b/dev/tests/verification/Resources/DataReplacementTest.txt index f2cc79e27..d5121931c 100644 --- a/dev/tests/verification/Resources/DataReplacementTest.txt +++ b/dev/tests/verification/Resources/DataReplacementTest.txt @@ -1,5 +1,5 @@ <?php -namespace Magento\AcceptanceTest\_generated\Backend; +namespace Magento\AcceptanceTest\_default\Backend; use Magento\FunctionalTestingFramework\AcceptanceTester; use Magento\FunctionalTestingFramework\DataGenerator\Handlers\DataObjectHandler; @@ -33,20 +33,22 @@ class DataReplacementTestCest $I->conditionalClick("Doe", "#John", true); $I->amOnUrl("John.html"); $I->searchAndMultiSelectOption("#selector", ["John", "Doe"]); - $I->fillField("#selector", "StringBefore ".msq("uniqueData")."John StringAfter"); - $I->fillField("#".msq("uniqueData")."John", "input"); - $I->dragAndDrop("#".msq("uniqueData")."John", msq("uniqueData")."John"); - $I->conditionalClick(msq("uniqueData")."John", "#".msq("uniqueData")."John", true); - $I->amOnUrl(msq("uniqueData")."John.html"); - $I->searchAndMultiSelectOption("#selector", [msq("uniqueData")."John", "Doe"]); - $I->fillField("#selector", "StringBefore Doe".msq("uniqueData")." StringAfter"); - $I->fillField("#Doe".msq("uniqueData"), "input"); - $I->dragAndDrop("#Doe".msq("uniqueData"), "Doe".msq("uniqueData")); - $I->conditionalClick("Doe".msq("uniqueData"), "#Doe".msq("uniqueData"), true); - $I->amOnUrl("Doe".msq("uniqueData").".html"); - $I->searchAndMultiSelectOption("#selector", ["John", "Doe".msq("uniqueData")]); - $I->searchAndMultiSelectOption("#selector", [msq("uniqueData")."John", "Doe".msq("uniqueData")]); - $I->selectMultipleOptions("#Doe".msq("uniqueData"), "#element", [msq("uniqueData")."John", "Doe".msq("uniqueData")]); + $I->fillField("#selector", "StringBefore " . msq("uniqueData") . "John StringAfter"); + $I->fillField("#" . msq("uniqueData") . "John", "input"); + $I->dragAndDrop("#" . msq("uniqueData") . "John", msq("uniqueData") . "John"); + $I->conditionalClick(msq("uniqueData") . "John", "#" . msq("uniqueData") . "John", true); + $I->amOnUrl(msq("uniqueData") . "John.html"); + $I->searchAndMultiSelectOption("#selector", [msq("uniqueData") . "John", "Doe"]); + $I->click("#" . msq("uniqueData") . "John#" . msq("uniqueData") . "John"); + $I->click("#Doe" . msq("uniqueData") . "#Doe" . msq("uniqueData")); + $I->fillField("#selector", "StringBefore Doe" . msq("uniqueData") . " StringAfter"); + $I->fillField("#Doe" . msq("uniqueData"), "input"); + $I->dragAndDrop("#Doe" . msq("uniqueData"), "Doe" . msq("uniqueData")); + $I->conditionalClick("Doe" . msq("uniqueData"), "#Doe" . msq("uniqueData"), true); + $I->amOnUrl("Doe" . msq("uniqueData") . ".html"); + $I->searchAndMultiSelectOption("#selector", ["John", "Doe" . msq("uniqueData")]); + $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"); } } diff --git a/dev/tests/verification/Resources/HookActionsTest.txt b/dev/tests/verification/Resources/HookActionsTest.txt index 0bcd95ca5..2e9eb609b 100644 --- a/dev/tests/verification/Resources/HookActionsTest.txt +++ b/dev/tests/verification/Resources/HookActionsTest.txt @@ -1,5 +1,5 @@ <?php -namespace Magento\AcceptanceTest\_generated\Backend; +namespace Magento\AcceptanceTest\_default\Backend; use Magento\FunctionalTestingFramework\AcceptanceTester; use Magento\FunctionalTestingFramework\DataGenerator\Handlers\DataObjectHandler; @@ -73,7 +73,6 @@ class HookActionsTestCest public function _failed(AcceptanceTester $I) { $I->saveScreenshot(); - $this->_after($I); } /** diff --git a/dev/tests/verification/Resources/LocatorFunctionTest.txt b/dev/tests/verification/Resources/LocatorFunctionTest.txt index ff3afbb3e..37e5ae9fb 100644 --- a/dev/tests/verification/Resources/LocatorFunctionTest.txt +++ b/dev/tests/verification/Resources/LocatorFunctionTest.txt @@ -1,5 +1,5 @@ <?php -namespace Magento\AcceptanceTest\_generated\Backend; +namespace Magento\AcceptanceTest\_default\Backend; use Magento\FunctionalTestingFramework\AcceptanceTester; use Magento\FunctionalTestingFramework\DataGenerator\Handlers\DataObjectHandler; diff --git a/dev/tests/verification/Resources/MergedActionGroupTest.txt b/dev/tests/verification/Resources/MergedActionGroupTest.txt index 01655eed4..ab9b46f8d 100644 --- a/dev/tests/verification/Resources/MergedActionGroupTest.txt +++ b/dev/tests/verification/Resources/MergedActionGroupTest.txt @@ -1,5 +1,5 @@ <?php -namespace Magento\AcceptanceTest\_generated\Backend; +namespace Magento\AcceptanceTest\_default\Backend; use Magento\FunctionalTestingFramework\AcceptanceTester; use Magento\FunctionalTestingFramework\DataGenerator\Handlers\DataObjectHandler; @@ -57,7 +57,6 @@ class MergedActionGroupTestCest public function _failed(AcceptanceTester $I) { $I->saveScreenshot(); - $this->_after($I); } /** diff --git a/dev/tests/verification/Resources/MergedReferencesTest.txt b/dev/tests/verification/Resources/MergedReferencesTest.txt index 0774d06f1..519db4314 100644 --- a/dev/tests/verification/Resources/MergedReferencesTest.txt +++ b/dev/tests/verification/Resources/MergedReferencesTest.txt @@ -1,5 +1,5 @@ <?php -namespace Magento\AcceptanceTest\_generated\Backend; +namespace Magento\AcceptanceTest\_default\Backend; use Magento\FunctionalTestingFramework\AcceptanceTester; use Magento\FunctionalTestingFramework\DataGenerator\Handlers\DataObjectHandler; @@ -46,7 +46,6 @@ class MergedReferencesTestCest public function _failed(AcceptanceTester $I) { $I->saveScreenshot(); - $this->_after($I); } /** diff --git a/dev/tests/verification/Resources/MultipleActionGroupsTest.txt b/dev/tests/verification/Resources/MultipleActionGroupsTest.txt index 4272c72fe..d9b32bfce 100644 --- a/dev/tests/verification/Resources/MultipleActionGroupsTest.txt +++ b/dev/tests/verification/Resources/MultipleActionGroupsTest.txt @@ -1,5 +1,5 @@ <?php -namespace Magento\AcceptanceTest\_generated\Backend; +namespace Magento\AcceptanceTest\_default\Backend; use Magento\FunctionalTestingFramework\AcceptanceTester; use Magento\FunctionalTestingFramework\DataGenerator\Handlers\DataObjectHandler; @@ -57,7 +57,6 @@ class MultipleActionGroupsTestCest public function _failed(AcceptanceTester $I) { $I->saveScreenshot(); - $this->_after($I); } /** diff --git a/dev/tests/verification/Resources/PageReplacementTest.txt b/dev/tests/verification/Resources/PageReplacementTest.txt index ce83ac9a8..5621e5c7a 100644 --- a/dev/tests/verification/Resources/PageReplacementTest.txt +++ b/dev/tests/verification/Resources/PageReplacementTest.txt @@ -1,5 +1,5 @@ <?php -namespace Magento\AcceptanceTest\_generated\Backend; +namespace Magento\AcceptanceTest\_default\Backend; use Magento\FunctionalTestingFramework\AcceptanceTester; use Magento\FunctionalTestingFramework\DataGenerator\Handlers\DataObjectHandler; diff --git a/dev/tests/verification/Resources/ParameterArrayTest.txt b/dev/tests/verification/Resources/ParameterArrayTest.txt index 3bb986b10..f60478037 100644 --- a/dev/tests/verification/Resources/ParameterArrayTest.txt +++ b/dev/tests/verification/Resources/ParameterArrayTest.txt @@ -1,5 +1,5 @@ <?php -namespace Magento\AcceptanceTest\_generated\Backend; +namespace Magento\AcceptanceTest\_default\Backend; use Magento\FunctionalTestingFramework\AcceptanceTester; use Magento\FunctionalTestingFramework\DataGenerator\Handlers\DataObjectHandler; @@ -32,19 +32,19 @@ class ParameterArrayTestCest $simpleDataKey = new DataPersistenceHandler($simpleParamData, []); $simpleDataKey->createEntity(); $I->searchAndMultiSelectOption("#selector", ["name"]); - $I->searchAndMultiSelectOption("#selector", [msq("simpleParamData")."prename"]); - $I->searchAndMultiSelectOption("#selector", ["postname".msq("simpleParamData")]); + $I->searchAndMultiSelectOption("#selector", [msq("simpleParamData") . "prename"]); + $I->searchAndMultiSelectOption("#selector", ["postname" . msq("simpleParamData")]); $I->searchAndMultiSelectOption("#selector", [$simpleDataKey->getCreatedDataByName('name')]); $I->searchAndMultiSelectOption("#selector", ["name", $simpleDataKey->getCreatedDataByName('name')]); $I->searchAndMultiSelectOption("#selector", ['someKey' => $simpleDataKey->getCreatedDataByName('name')]); $I->searchAndMultiSelectOption("#selector", ['someKey' => "name"]); - $I->searchAndMultiSelectOption("#selector", ['someKey' => msq("simpleParamData")."prename"]); - $I->searchAndMultiSelectOption("#selector", ['someKey' => "postname".msq("simpleParamData")]); + $I->searchAndMultiSelectOption("#selector", ['someKey' => msq("simpleParamData") . "prename"]); + $I->searchAndMultiSelectOption("#selector", ['someKey' => "postname" . msq("simpleParamData")]); $I->unselectOption("#selector", ['foo']); $I->unselectOption("#selector", ['foo', 'bar']); $I->unselectOption("#selector", ["name"]); - $I->unselectOption("#selector", [msq("simpleParamData")."prename"]); - $I->unselectOption("#selector", ["postname".msq("simpleParamData")]); + $I->unselectOption("#selector", [msq("simpleParamData") . "prename"]); + $I->unselectOption("#selector", ["postname" . msq("simpleParamData")]); $I->unselectOption("#selector", [$simpleDataKey->getCreatedDataByName('name')]); $I->unselectOption("#selector", ["name", $simpleDataKey->getCreatedDataByName('name')]); } diff --git a/dev/tests/verification/Resources/PersistedReplacementTest.txt b/dev/tests/verification/Resources/PersistedReplacementTest.txt index 81321e7ed..2c335c7f0 100644 --- a/dev/tests/verification/Resources/PersistedReplacementTest.txt +++ b/dev/tests/verification/Resources/PersistedReplacementTest.txt @@ -1,5 +1,5 @@ <?php -namespace Magento\AcceptanceTest\_generated\Backend; +namespace Magento\AcceptanceTest\_default\Backend; use Magento\FunctionalTestingFramework\AcceptanceTester; use Magento\FunctionalTestingFramework\DataGenerator\Handlers\DataObjectHandler; @@ -50,6 +50,7 @@ class PersistedReplacementTestCest $createdData->createEntity(); $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->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 f4d607c0b..53db9bcf8 100644 --- a/dev/tests/verification/Resources/PersistenceCustomFieldsTest.txt +++ b/dev/tests/verification/Resources/PersistenceCustomFieldsTest.txt @@ -1,5 +1,5 @@ <?php -namespace Magento\AcceptanceTest\_generated\Backend; +namespace Magento\AcceptanceTest\_default\Backend; use Magento\FunctionalTestingFramework\AcceptanceTester; use Magento\FunctionalTestingFramework\DataGenerator\Handlers\DataObjectHandler; diff --git a/dev/tests/verification/Resources/SectionReplacementTest.txt b/dev/tests/verification/Resources/SectionReplacementTest.txt index 73b34fe30..07a8e7400 100644 --- a/dev/tests/verification/Resources/SectionReplacementTest.txt +++ b/dev/tests/verification/Resources/SectionReplacementTest.txt @@ -1,5 +1,5 @@ <?php -namespace Magento\AcceptanceTest\_generated\Backend; +namespace Magento\AcceptanceTest\_default\Backend; use Magento\FunctionalTestingFramework\AcceptanceTester; use Magento\FunctionalTestingFramework\DataGenerator\Handlers\DataObjectHandler; @@ -38,14 +38,14 @@ class SectionReplacementTestCest $I->click("#John .Doe"); $I->click("#John-Doe .Tiberius"); $I->click("#John-Doe .John [Tiberius]"); - $I->click("#element .".msq("uniqueData")."John"); - $I->click("#".msq("uniqueData")."John .stringLiteral2"); - $I->click("#".msq("uniqueData")."John-stringLiteral2 .stringLiteral3"); - $I->click("#".msq("uniqueData")."John-stringLiteral2 ."); - $I->click("#element .Doe".msq("uniqueData")); - $I->click("#Doe".msq("uniqueData")." .stringLiteral2"); - $I->click("#Doe".msq("uniqueData")."-stringLiteral2 .stringLiteral3"); - $I->click("#Doe".msq("uniqueData")."-stringLiteral2 .Doe"); + $I->click("#element ." . msq("uniqueData") . "John"); + $I->click("#" . msq("uniqueData") . "John .stringLiteral2"); + $I->click("#" . msq("uniqueData") . "John-stringLiteral2 .stringLiteral3"); + $I->click("#" . msq("uniqueData") . "John-stringLiteral2 ." . msq("uniqueData") . "John [stringLiteral3]"); + $I->click("#element .Doe" . msq("uniqueData")); + $I->click("#Doe" . msq("uniqueData") . " .stringLiteral2"); + $I->click("#Doe" . msq("uniqueData") . "-stringLiteral2 .stringLiteral3"); + $I->click("#Doe" . msq("uniqueData") . "-stringLiteral2 .Doe" . msq("uniqueData") . " [stringLiteral3]"); $I->amGoingTo("create entity that has the stepKey: createdData"); $simpleData = DataObjectHandler::getInstance()->getObject("simpleData"); $createdData = new DataPersistenceHandler($simpleData, []); @@ -60,7 +60,7 @@ class SectionReplacementTestCest $I->click("#John-Doe .John [Tiberius]"); $I->click("#stringLiteral1-" . $createdData->getCreatedDataByName('firstname') . " .John"); $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("#stringLiteral1-" . $createdData->getCreatedDataByName('firstname') . " ." . msq("uniqueData") . "John"); + $I->click("#stringLiteral1-" . $createdData->getCreatedDataByName('firstname') . " .Doe" . msq("uniqueData")); } } diff --git a/dev/tests/verification/TestModule/ActionGroup/BasicActionGroup.xml b/dev/tests/verification/TestModule/ActionGroup/BasicActionGroup.xml index 1fec39772..94e7a6b60 100644 --- a/dev/tests/verification/TestModule/ActionGroup/BasicActionGroup.xml +++ b/dev/tests/verification/TestModule/ActionGroup/BasicActionGroup.xml @@ -48,4 +48,15 @@ </arguments> <see selector="{{SampleSection.oneParamElement(someArgument)}}" userInput="{{someArgument}}" stepKey="see1" /> </actionGroup> + + <actionGroup name="actionGroupWithNestedArgument"> + <arguments> + <argument name="count" defaultValue="10" type="string"/> + </arguments> + <grabMultiple selector="selector" stepKey="grabProducts"/> + <assertCount stepKey="assertCount"> + <expectedResult type="int">{{count}}</expectedResult> + <actualResult type="variable">grabProducts</actualResult> + </assertCount> + </actionGroup> </actionGroups> diff --git a/dev/tests/verification/TestModule/ActionGroup/XmlDuplicateActionGroup.xml b/dev/tests/verification/TestModule/ActionGroup/XmlDuplicateActionGroup.xml index 8c5ba7402..fcc22acca 100644 --- a/dev/tests/verification/TestModule/ActionGroup/XmlDuplicateActionGroup.xml +++ b/dev/tests/verification/TestModule/ActionGroup/XmlDuplicateActionGroup.xml @@ -16,8 +16,8 @@ <amOnSubdomain stepKey="aosd2" url="1"/> <amOnUrl stepKey="aou1" url="1"/> <amOnUrl stepKey="aou2" url="1"/> - <appendField selector="1" stepKey="ap1"/> - <appendField selector="1" stepKey="ap2"/> + <appendField selector="1" stepKey="apf1"/> + <appendField selector="1" stepKey="apf2"/> <attachFile selector="1" stepKey="atf1"/> <attachFile selector="1" stepKey="atf2"/> <cancelPopup stepKey="cp1"/> @@ -164,8 +164,8 @@ <seeInField stepKey="seeinfield12"/> <seeInFormFields selector="2" parameterArray="[1]" stepKey="seeinformfields1"/> <seeInFormFields selector="2" parameterArray="[1]" stepKey="seeinformfields12"/> - <seeInPageSource html="1" stepKey="seeinsource1"/> - <seeInPageSource html="1" stepKey="seeinsource12"/> + <seeInPageSource html="1" stepKey="seeinpgsource1"/> + <seeInPageSource html="1" stepKey="seeinpgsource12"/> <seeInPopup stepKey="seeinpopup1"/> <seeInPopup stepKey="seeinpopup12"/> <seeInSource html="1" stepKey="seeinsource1"/> diff --git a/dev/tests/verification/TestModule/Data/ReplacementData.xml b/dev/tests/verification/TestModule/Data/ReplacementData.xml index 1b5417f75..4d91fa84e 100644 --- a/dev/tests/verification/TestModule/Data/ReplacementData.xml +++ b/dev/tests/verification/TestModule/Data/ReplacementData.xml @@ -26,4 +26,12 @@ <data key="sampleField2">moreData</data> <requiredEntity type="test2">originalValue2</requiredEntity> </entity> + <entity name="offset" type="simpleData"> + <data key="x">200</data> + <data key="y">300</data> + </entity> + <entity name="emptyData" type="backend"> + <data key="noData"></data> + <data key="definitelyNoData"/> + </entity> </entities> diff --git a/dev/tests/verification/TestModule/Test/ActionGroupTest.xml b/dev/tests/verification/TestModule/Test/ActionGroupTest.xml index 72617c858..94b9e74dd 100644 --- a/dev/tests/verification/TestModule/Test/ActionGroupTest.xml +++ b/dev/tests/verification/TestModule/Test/ActionGroupTest.xml @@ -114,4 +114,10 @@ <test name="ActionGroupWithStepKeyReferences"> <actionGroup ref="FunctionActionGroupWithStepKeyReferences" stepKey="actionGroup"/> </test> + + <test name="ActionGroupUsingNestedArgument"> + <actionGroup ref="actionGroupWithNestedArgument" stepKey="actionGroup"> + <argument name="count" value="99"/> + </actionGroup> + </test> </tests> diff --git a/dev/tests/verification/TestModule/Test/BasicFunctionalTest.xml b/dev/tests/verification/TestModule/Test/BasicFunctionalTest.xml index a2f0d11b0..bd7c23522 100644 --- a/dev/tests/verification/TestModule/Test/BasicFunctionalTest.xml +++ b/dev/tests/verification/TestModule/Test/BasicFunctionalTest.xml @@ -22,6 +22,8 @@ <after> <amOnPage url="/afterUrl" stepKey="afterAmOnPageKey"/> </after> + <comment stepKey="basicCommentWithNoData" userInput="{{emptyData.noData}}"/> + <comment stepKey="basicCommentWithDefinitelyNoData" userInput="{{emptyData.definitelyNoData}}"/> <grabValueFrom stepKey="someVarDefinition"/> <acceptPopup stepKey="acceptPopupKey1"/> <amOnPage stepKey="amOnPageKey1" url="/test/url"/> @@ -32,8 +34,12 @@ <click selector=".functionalTestSelector" stepKey="clickKey1"/> <clickWithLeftButton selector=".functionalTestSelector" stepKey="clickWithLeftButtonKey1"/> <clickWithRightButton selector=".functionalTestSelector" stepKey="clickWithRightButtonKey1"/> + <clickWithLeftButton selector="{{SampleSection.simpleElement}}{{SampleSection.simpleElementOneParam(offset.x)}}" x="{{offset.x}}" y="{{offset.y}}" stepKey="clickWithLeftButtonKeyXY1" /> + <clickWithRightButton selector="{{SampleSection.simpleElementOneParam('4123')}}{{SampleSection.simpleElement}}" x="{{offset.x}}" y="{{offset.y}}" stepKey="clickWithRightButtonKeyXY1" /> <closeTab stepKey="closeTabKey1"/> <conditionalClick selector=".functionalTestSelector" dependentSelector=".functionalDependentTestSelector" visible="true" stepKey="conditionalClickKey1"/> + <deleteData stepKey="deleteKey1" createDataKey="createKey1"/> + <deleteData stepKey="deleteKey2" url="/V1/categories{$grabbedData}"/> <dontSee userInput="someInput" selector=".functionalTestSelector" stepKey="dontSeeKey1" /> <dontSeeCheckboxIsChecked selector=".functionalTestSelector" stepKey="dontSeeCheckboxIsCheckedKey1"/> <dontSeeCookie userInput="someInput" stepKey="dontSeeCookieKey1"/> @@ -50,6 +56,7 @@ <dontSeeOptionIsSelected selector=".functionalTestSelector" userInput="someInput" stepKey="dontSeeOptionIsSelectedKey1" /> <doubleClick selector=".functionalTestSelector" stepKey="doubleClickKey1"/> <dragAndDrop selector1=".functionalTestSelector" selector2=".functionalTestSelector2" stepKey="dragAndDropKey1" /> + <dragAndDrop selector1=".functionalTestSelector" selector2=".functionalTestSelector2" stepKey="dragAndDropKey2" x="100" y="900"/> <executeJS function="someJSFunction" stepKey="executeJSKey1"/> <fillField selector=".functionalTestSelector" userInput="someInput" stepKey="fillFieldKey1" /> <fillField selector=".functionalTestSelector" userInput="0" stepKey="fillFieldKey2" /> @@ -67,6 +74,7 @@ <moveMouseOver selector=".functionalTestSelector" stepKey="moveMouseOverKey1"/> <openNewTab stepKey="openNewTabKey1"/> <pauseExecution stepKey="pauseExecutionKey1"/> + <performOn selector="#selector" function="function(\WebDriverElement $el) {return $el->isDisplayed();}" stepKey="performOnKey1"/> <pressKey selector="#page" userInput="a" stepKey="pressKey1"/> <pressKey selector="#page" parameterArray="[['ctrl','a'],'new']" stepKey="pressKey2"/> <pressKey selector="#page" parameterArray="[['shift','111'],'1','x']" stepKey="pressKey3"/> @@ -107,6 +115,7 @@ <waitForElement time="30" selector=".functionalTestSelector" stepKey="waitForElementKey1" /> <waitForElementNotVisible selector=".functionalTestSelector" time="30" stepKey="waitForElementNotVisibleKey1" /> <waitForElementVisible selector=".functionalTestSelector" time="30" stepKey="waitForElementVisibleKey1" /> + <waitForElementChange selector="#selector" function="function(\WebDriverElement $el) {return $el->isDisplayed();}" stepKey="waitForElementChangeKey1"/> <waitForJS function="someJsFunction" time="30" stepKey="waitForJSKey1" /> <waitForText selector=".functionalTestSelector" userInput="someInput" time="30" stepKey="waitForText1"/> </test> diff --git a/dev/tests/verification/TestModule/Test/DataReplacementTest.xml b/dev/tests/verification/TestModule/Test/DataReplacementTest.xml index 7aadebb47..f12c70b26 100644 --- a/dev/tests/verification/TestModule/Test/DataReplacementTest.xml +++ b/dev/tests/verification/TestModule/Test/DataReplacementTest.xml @@ -22,6 +22,8 @@ <conditionalClick stepKey="dependentSelectorReplaceMSQPrefix" dependentSelector="#{{uniqueData.firstname}}" selector="{{uniqueData.firstname}}" visible="true"/> <amOnUrl stepKey="urlReplaceMSQPrefix" url="{{uniqueData.firstname}}.html"/> <searchAndMultiSelectOption stepKey="parameterArrayReplacementMSQPrefix" selector="#selector" parameterArray="[{{uniqueData.firstname}}, {{simpleData.lastname}}]"/> + <click stepKey="selectorReplaceDupedMSQPrefix" selector="#{{uniqueData.firstname}}#{{uniqueData.firstname}}"/> + <click stepKey="selectorReplaceDupedMSQSuffix" selector="#{{uniqueData.lastname}}#{{uniqueData.lastname}}"/> <fillField stepKey="inputReplaceMSQSuffix" selector="#selector" userInput="StringBefore {{uniqueData.lastname}} StringAfter"/> <fillField stepKey="selectorReplaceMSQSuffix" selector="#{{uniqueData.lastname}}" userInput="input"/> diff --git a/dev/tests/verification/TestModule/Test/PersistedReplacementTest.xml b/dev/tests/verification/TestModule/Test/PersistedReplacementTest.xml index d46381748..26885fda4 100644 --- a/dev/tests/verification/TestModule/Test/PersistedReplacementTest.xml +++ b/dev/tests/verification/TestModule/Test/PersistedReplacementTest.xml @@ -16,6 +16,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"/> <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/Tests/ActionGroupGenerationTest.php b/dev/tests/verification/Tests/ActionGroupGenerationTest.php index e62ea24a5..a26e9ad87 100644 --- a/dev/tests/verification/Tests/ActionGroupGenerationTest.php +++ b/dev/tests/verification/Tests/ActionGroupGenerationTest.php @@ -107,4 +107,15 @@ public function testActionGroupWithStepKeyReferences() { $this->generateAndCompareTest('ActionGroupWithStepKeyReferences'); } + + /** + * Test generation of a test referencing an action group that uses stepKey references (grabFrom/CreateData) + * + * @throws \Exception + * @throws \Magento\FunctionalTestingFramework\Exceptions\TestReferenceException + */ + public function testActionGroupWithNestedArgument() + { + $this->generateAndCompareTest('ActionGroupUsingNestedArgument'); + } } diff --git a/dev/tests/verification/Tests/SuiteGenerationTest.php b/dev/tests/verification/Tests/SuiteGenerationTest.php index 44428d128..b1790ac81 100644 --- a/dev/tests/verification/Tests/SuiteGenerationTest.php +++ b/dev/tests/verification/Tests/SuiteGenerationTest.php @@ -6,6 +6,7 @@ namespace tests\verification\Tests; +use Magento\Framework\Module\Dir; use Magento\FunctionalTestingFramework\Suite\SuiteGenerator; use Magento\FunctionalTestingFramework\Util\TestManifest; use Symfony\Component\Yaml\Yaml; @@ -16,13 +17,6 @@ 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; - const MANIFEST_RESULTS = [ - 'IncludeTest2Cest.php', - 'additionalTestCest.php', - 'IncludeTestCest.php', - 'additionalIncludeTest2Cest.php' - ]; - /** * Flag to track existence of config.yml file * @@ -47,6 +41,11 @@ public static function setUpBeforeClass() return; } + // destroy manifest file if it exists + if (file_exists(self::getManifestFilePath())) { + unlink(self::getManifestFilePath()); + } + $configYml = fopen(self::CONFIG_YML_FILE, "w"); fclose($configYml); } @@ -58,6 +57,13 @@ public function testSuiteGeneration1() { $groupName = 'functionalSuite1'; + $expectedContents = [ + 'additionalTestCest.php', + 'additionalIncludeTest2Cest.php', + 'IncludeTest2Cest.php', + 'IncludeTestCest.php' + ]; + // Generate the Suite SuiteGenerator::getInstance()->generateSuite($groupName); @@ -78,23 +84,11 @@ public function testSuiteGeneration1() $groupName . DIRECTORY_SEPARATOR; - // Validate test manifest contents - $actualManifest = $suiteResultBaseDir . 'testManifest.txt'; - $actualTestReferences = explode(PHP_EOL, file_get_contents($actualManifest)); - - for ($i = 0; $i < count($actualTestReferences); $i++) { - if (empty($actualTestReferences[$i])) { - continue; - } - - $this->assertStringEndsWith(self::MANIFEST_RESULTS[$i], $actualTestReferences[$i]); - $this->assertNotFalse(strpos($actualTestReferences[$i], $groupName)); - } + // Validate tests have been generated + $dirContents = array_diff(scandir($suiteResultBaseDir), ['..', '.']); - // Validate expected php files exist - foreach (self::MANIFEST_RESULTS as $expectedTestReference) { - $cestName = explode(":", $expectedTestReference, 2); - $this->assertFileExists($suiteResultBaseDir . $cestName[0]); + foreach ($expectedContents as $expectedFile) { + $this->assertTrue(in_array($expectedFile, $dirContents)); } } @@ -116,4 +110,20 @@ public static function tearDownAfterClass() unlink(self::CONFIG_YML_FILE); } + + /** + * Getter for manifest file path + * + * @return string + */ + private static function getManifestFilePath() + { + return TESTS_BP . + DIRECTORY_SEPARATOR . + "verification" . + DIRECTORY_SEPARATOR . + "_generated" . + DIRECTORY_SEPARATOR . + 'testManifest.txt'; + } } diff --git a/etc/di.xml b/etc/di.xml index f9e7e146f..d03ed2267 100644 --- a/etc/di.xml +++ b/etc/di.xml @@ -286,11 +286,12 @@ </arguments> </virtualType> - <virtualType name="Magento\FunctionalTestingFramework\Config\Reader\ActionGroupData" type="Magento\FunctionalTestingFramework\Config\Reader\Filesystem"> + <virtualType name="Magento\FunctionalTestingFramework\Config\Reader\ActionGroupData" type="Magento\FunctionalTestingFramework\Test\Config\Reader\Filesystem"> <arguments> <argument name="fileResolver" xsi:type="object">Magento\FunctionalTestingFramework\Config\FileResolver\Module</argument> <argument name="converter" xsi:type="object">Magento\FunctionalTestingFramework\Config\ActionGroupDataConverter</argument> <argument name="schemaLocator" xsi:type="object">Magento\FunctionalTestingFramework\Config\SchemaLocator\ActionGroup</argument> + <argument name="domDocumentClass" xsi:type="string">Magento\FunctionalTestingFramework\Test\Config\ActionGroupDom</argument> <argument name="idAttributes" xsi:type="array"> <item name="/actionGroups/actionGroup" xsi:type="string">name</item> <item name="/actionGroups/actionGroup/arguments/argument" xsi:type="string">name</item> @@ -358,11 +359,15 @@ <argument name="schemaLocator" xsi:type="object">Magento\FunctionalTestingFramework\Config\SchemaLocator\SuiteData</argument> <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|&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> <item name="/suites/suite/include/(group|test|module)" xsi:type="string">name</item> <item name="/suites/suite/exclude/(group|test|module)" xsi:type="string">name</item> </argument> <argument name="fileName" xsi:type="string">*.xml</argument> - <argument name="defaultScope" xsi:type="string">_suite</argument> + <argument name="defaultScope" xsi:type="string">Suite</argument> </arguments> </virtualType> @@ -370,8 +375,10 @@ <arguments> <argument name="assocArrayAttributes" xsi:type="array"> <item name="/suites/suite" xsi:type="string">name</item> - <item name="/suites/suite/(before|after)/(createData|deleteData)" xsi:type="string">stepKey</item> + <item name="/suites/suite/(before|after)/remove" xsi:type="string">keyForRemoval</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> <item name="/suites/suite/include/(group|test|module)" xsi:type="string">name</item> <item name="/suites/suite/exclude/(group|test|module)" xsi:type="string">name</item> </argument> diff --git a/src/Magento/FunctionalTestingFramework/Allure/Adapter/MagentoAllureAdapter.php b/src/Magento/FunctionalTestingFramework/Allure/Adapter/MagentoAllureAdapter.php new file mode 100644 index 000000000..393f29008 --- /dev/null +++ b/src/Magento/FunctionalTestingFramework/Allure/Adapter/MagentoAllureAdapter.php @@ -0,0 +1,94 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\FunctionalTestingFramework\Allure\Adapter; + +use Magento\FunctionalTestingFramework\Data\Argument\Interpreter\NullType; +use Magento\FunctionalTestingFramework\Suite\Handlers\SuiteObjectHandler; +use Yandex\Allure\Adapter\AllureAdapter; +use Codeception\Event\SuiteEvent; + +/** + * Class MagentoAllureAdapter + * + * Extends AllureAdapter to provide further information for allure reports + * + * @package Magento\FunctionalTestingFramework\Allure + */ + +class MagentoAllureAdapter extends AllureAdapter +{ + /** + * Array of group values passed to test runner command + * + * @return String + */ + private function getGroup() + { + if ($this->options['groups'] != null) { + return $this->options['groups'][0]; + } + return null; + } + + /** + * Override of parent method to set suitename as suitename and group name concatenated + * + * @param SuiteEvent $suiteEvent + * @return void + */ + public function suiteBefore(SuiteEvent $suiteEvent) + { + $changeSuiteEvent = $suiteEvent; + + if ($this->getGroup() != null) { + $suite = $suiteEvent->getSuite(); + $suiteName = ($suite->getName()) . "\\" . $this->sanitizeGroupName($this->getGroup()); + + call_user_func(\Closure::bind( + function () use ($suite, $suiteName) { + $suite->name = $suiteName; + }, + null, + $suite + )); + + //change suiteEvent + $changeSuiteEvent = new SuiteEvent( + $suiteEvent->getSuite(), + $suiteEvent->getResult(), + $suiteEvent->getSettings() + ); + } + // call parent function + parent::suiteBefore($changeSuiteEvent); + } + + /** + * Function which santizes any group names changed by the framework for execution in order to consolidate reporting. + * + * @param string $group + * @return string + */ + private function sanitizeGroupName($group) + { + $suiteNames = array_keys(SuiteObjectHandler::getInstance()->getAllObjects()); + $exactMatch = in_array($group, $suiteNames); + + // if this is an existing suite name we dont' need to worry about changing it + if ($exactMatch || strpos($group, "_") === false) { + return $group; + } + + // if we can't find this group in the generated suites we have to assume that the group was split for generation + $groupNameSplit = explode("_", $group); + array_pop($groupNameSplit); + $originalName = implode("_", $groupNameSplit); + + // confirm our original name is one of the existing suite names otherwise just return the original group name + $originalName = in_array($originalName, $suiteNames) ? $originalName : $group; + return $originalName; + } +} diff --git a/src/Magento/FunctionalTestingFramework/Config/FileResolver/Module.php b/src/Magento/FunctionalTestingFramework/Config/FileResolver/Module.php index c949aa961..7cff19c87 100644 --- a/src/Magento/FunctionalTestingFramework/Config/FileResolver/Module.php +++ b/src/Magento/FunctionalTestingFramework/Config/FileResolver/Module.php @@ -20,7 +20,7 @@ class Module implements FileResolverInterface * * @var ModuleResolver */ - private $moduleResolver; + protected $moduleResolver; /** * Module constructor. @@ -40,6 +40,20 @@ public function __construct(ModuleResolver $moduleResolver = null) * @return array|\Iterator,\Countable */ public function get($filename, $scope) + { + $iterator = new File($this->getPaths($filename, $scope)); + return $iterator; + } + + /** + * Function which takes a string representing filename and a scope represnting directory scope to glob for matched + * patterns against. Returns the file matching the patterns given by the module resolver. + * + * @param string $filename + * @param string $scope + * @return array + */ + protected function getPaths($filename, $scope) { $modulesPath = $this->moduleResolver->getModulesPath(); $paths = []; @@ -48,7 +62,6 @@ public function get($filename, $scope) $paths = array_merge($paths, glob($path)); } - $iterator = new File($paths); - return $iterator; + return $paths; } } diff --git a/src/Magento/FunctionalTestingFramework/Config/FileResolver/Root.php b/src/Magento/FunctionalTestingFramework/Config/FileResolver/Root.php index e728fa418..93e74a82c 100644 --- a/src/Magento/FunctionalTestingFramework/Config/FileResolver/Root.php +++ b/src/Magento/FunctionalTestingFramework/Config/FileResolver/Root.php @@ -9,11 +9,13 @@ use Magento\FunctionalTestingFramework\Config\FileResolverInterface; use Magento\FunctionalTestingFramework\Util\Iterator\File; -class Root implements FileResolverInterface +class Root extends Module { + const ROOT_SUITE_DIR = "_suite"; /** - * Retrieve the list of configuration files with given name that relate to specified scope at the tests level + * Retrieve the list of configuration files with given name that relate to specified scope at the root level as well + * as any extension based suite configuration. * * @param string $filename * @param string $scope @@ -21,8 +23,19 @@ class Root implements FileResolverInterface */ public function get($filename, $scope) { - $paths = glob(dirname(TESTS_BP) . DIRECTORY_SEPARATOR . $scope . DIRECTORY_SEPARATOR . $filename); + // first pick up the root level test suite dir + $paths = glob( + dirname(TESTS_BP) . DIRECTORY_SEPARATOR . self::ROOT_SUITE_DIR + . DIRECTORY_SEPARATOR . $filename + ); - return new File($paths); + // then merge this path into the module based paths + // Since we are sharing this code with Module based resolution we will unncessarily glob against modules in the + // dev/tests dir tree, however as we plan to migrate to app/code this will be a temporary unneeded check. + $paths = array_merge($paths, $this->getPaths($filename, $scope)); + + // create and return the iterator for these file paths + $iterator = new File($paths); + return $iterator; } } diff --git a/src/Magento/FunctionalTestingFramework/Config/MftfApplicationConfig.php b/src/Magento/FunctionalTestingFramework/Config/MftfApplicationConfig.php new file mode 100644 index 000000000..4d4500b1e --- /dev/null +++ b/src/Magento/FunctionalTestingFramework/Config/MftfApplicationConfig.php @@ -0,0 +1,127 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\FunctionalTestingFramework\Config; + +use Magento\FunctionalTestingFramework\Exceptions\TestFrameworkException; + +class MftfApplicationConfig +{ + const GENERATION_PHASE = "generation"; + const EXECUTION_PHASE = "execution"; + const MFTF_PHASES = [self::GENERATION_PHASE, self::EXECUTION_PHASE]; + + /** + * Determines whether the user has specified a force option for generation + * + * @var bool + */ + private $forceGenerate; + + /** + * String which identifies the current phase of mftf execution + * + * @var string + */ + private $phase; + + /** + * Determines whether the user would like to execute mftf in a verbose run. + * + * @var bool + */ + private $verboseEnabled; + + /** + * MftfApplicationConfig Singelton Instance + * + * @var MftfApplicationConfig + */ + private static $MFTF_APPLICATION_CONTEXT; + + /** + * MftfApplicationConfig constructor. + * + * @param bool $forceGenerate + * @param string $phase + * @param bool $verboseEnabled + * @throws TestFrameworkException + */ + private function __construct($forceGenerate = false, $phase = self::EXECUTION_PHASE, $verboseEnabled = null) + { + $this->forceGenerate = $forceGenerate; + + if (!in_array($phase, self::MFTF_PHASES)) { + throw new TestFrameworkException("{$phase} is not an mftf phase"); + } + + $this->phase = $phase; + $this->verboseEnabled = $verboseEnabled; + } + + /** + * 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 + * @return void + */ + public static function create($forceGenerate, $phase, $verboseEnabled) + { + if (self::$MFTF_APPLICATION_CONTEXT == null) { + self::$MFTF_APPLICATION_CONTEXT = new MftfApplicationConfig($forceGenerate, $phase, $verboseEnabled); + } + } + + /** + * This function returns an instance of the MftfApplicationConfig which is created once the application starts. + * + * @return MftfApplicationConfig + */ + public static function getConfig() + { + // TODO explicitly set this with AcceptanceTester or MagentoWebDriver + // during execution we cannot guarantee the use of the robofile so we return the default application config, + // we don't want to set the application context in case the user explicitly does so at a later time. + if (self::$MFTF_APPLICATION_CONTEXT == null) { + return new MftfApplicationConfig(); + } + + return self::$MFTF_APPLICATION_CONTEXT; + } + + /** + * Returns a booelan indiciating whether or not the user has indicated a forced generation. + * + * @return bool + */ + public function forceGenerateEnabled() + { + return $this->forceGenerate; + } + + /** + * 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 + */ + public function verboseEnabled() + { + return $this->verboseEnabled ?? getenv('MFTF_DEBUG'); + } + + /** + * Returns a string which indicates the phase of mftf execution. + * + * @return string + */ + public function getPhase() + { + return $this->phase; + } +} diff --git a/src/Magento/FunctionalTestingFramework/Console/BuildProjectCommand.php b/src/Magento/FunctionalTestingFramework/Console/BuildProjectCommand.php new file mode 100644 index 000000000..6e29ac071 --- /dev/null +++ b/src/Magento/FunctionalTestingFramework/Console/BuildProjectCommand.php @@ -0,0 +1,89 @@ +<?php +// @codingStandardsIgnoreFile +/** + * 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\Filesystem\Filesystem; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Process\Process; +use Magento\FunctionalTestingFramework\Util\Env\EnvProcessor; + +class BuildProjectCommand extends Command +{ + /** + * Env processor manages .env files. + * + * @var \Magento\FunctionalTestingFramework\Util\Env\EnvProcessor + */ + private $envProcessor; + + /** + * Configures the current command. + * + * @return void + */ + 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'); + $env = $this->envProcessor->getEnv(); + foreach ($env as $key => $value) { + $this->addOption($key, null, InputOption::VALUE_REQUIRED, '', $value); + } + } + + /** + * 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) + { + $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"); + + $setupEnvCommand = new SetupEnvCommand(); + $commandInput = []; + $options = $input->getOptions(); + $env = array_keys($this->envProcessor->getEnv()); + foreach ($options as $key => $value) { + if (in_array($key, $env)) { + $commandInput['--' . $key] = $value; + } + } + $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"); + } + + $output->writeln('<info>The project built successfully.</info>'); + } +} diff --git a/src/Magento/FunctionalTestingFramework/Console/SetupEnvCommand.php b/src/Magento/FunctionalTestingFramework/Console/SetupEnvCommand.php new file mode 100644 index 000000000..e0ef6053a --- /dev/null +++ b/src/Magento/FunctionalTestingFramework/Console/SetupEnvCommand.php @@ -0,0 +1,64 @@ +<?php +// @codingStandardsIgnoreFile +/** + * 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\InputOption; +use Symfony\Component\Console\Exception\InvalidOptionException; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; +use Magento\FunctionalTestingFramework\Util\Env\EnvProcessor; + +class SetupEnvCommand extends Command +{ + /** + * Env processor manages .env files. + * + * @var \Magento\FunctionalTestingFramework\Util\Env\EnvProcessor + */ + private $envProcessor; + + /** + * Configures the current command. + * + * @return void + */ + protected function configure() + { + $this->setName('setup:env'); + $this->setDescription("Generate .env file."); + $this->envProcessor = new EnvProcessor(BP . DIRECTORY_SEPARATOR . '.env'); + $env = $this->envProcessor->getEnv(); + foreach ($env as $key => $value) { + $this->addOption($key, null, InputOption::VALUE_REQUIRED, '', $value); + } + } + + /** + * 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) + { + $config = $this->envProcessor->getEnv(); + $userEnv = []; + foreach ($config as $key => $value) { + if ($input->getOption($key) === '') { + throw new InvalidOptionException(sprintf("Parameter $key cannot be empty.", $key)); + } + $userEnv[$key] = $input->getOption($key); + } + $this->envProcessor->putEnvFile($userEnv); + $output->writeln(".env configuration successfully applied.\n"); + } +} diff --git a/src/Magento/FunctionalTestingFramework/DataGenerator/Persist/Curl/WebapiExecutor.php b/src/Magento/FunctionalTestingFramework/DataGenerator/Persist/Curl/WebapiExecutor.php index e0a58d470..9a93cded3 100644 --- a/src/Magento/FunctionalTestingFramework/DataGenerator/Persist/Curl/WebapiExecutor.php +++ b/src/Magento/FunctionalTestingFramework/DataGenerator/Persist/Curl/WebapiExecutor.php @@ -55,11 +55,12 @@ class WebapiExecutor extends AbstractExecutor implements CurlInterface * * @param string $storeCode */ - public function __construct($storeCode = 'default') + public function __construct($storeCode = null) { if (!isset(parent::$baseUrl)) { parent::resolveBaseUrl(); } + $this->storeCode = $storeCode; $this->transport = new CurlTransport(); $this->authorize(); @@ -72,7 +73,7 @@ public function __construct($storeCode = 'default') */ protected function authorize() { - $authUrl = parent::$baseUrl . 'rest/' . $this->storeCode . self::ADMIN_AUTH_URL; + $authUrl = $this->getFormattedUrl(self::ADMIN_AUTH_URL); $authCreds = [ 'username' => getenv('MAGENTO_ADMIN_USERNAME'), 'password' => getenv('MAGENTO_ADMIN_PASSWORD') @@ -97,7 +98,7 @@ protected function authorize() public function write($url, $data = [], $method = CurlInterface::POST, $headers = []) { $this->transport->write( - parent::$baseUrl . 'rest/' . $this->storeCode . '/' . trim($url, '/'), + $this->getFormattedUrl($url), json_encode($data, JSON_PRETTY_PRINT), $method, array_unique(array_merge($headers, $this->headers)) @@ -138,4 +139,19 @@ public function close() { $this->transport->close(); } + + /** + * Builds and returns URL for request, appending storeCode if needed. + * @param string $resource + * @return string + */ + public function getFormattedUrl($resource) + { + $urlResult = parent::$baseUrl . 'rest/'; + if ($this->storeCode != null) { + $urlResult .= $this->storeCode . "/"; + } + $urlResult.= trim($resource, "/"); + return $urlResult; + } } diff --git a/src/Magento/FunctionalTestingFramework/DataGenerator/Persist/CurlHandler.php b/src/Magento/FunctionalTestingFramework/DataGenerator/Persist/CurlHandler.php index 15c3677d7..63144a9ff 100644 --- a/src/Magento/FunctionalTestingFramework/DataGenerator/Persist/CurlHandler.php +++ b/src/Magento/FunctionalTestingFramework/DataGenerator/Persist/CurlHandler.php @@ -81,7 +81,7 @@ class CurlHandler * @param EntityDataObject $entityObject * @param string $storeCode */ - public function __construct($operation, $entityObject, $storeCode = 'default') + public function __construct($operation, $entityObject, $storeCode = null) { $this->operation = $operation; $this->entityObject = $entityObject; @@ -206,7 +206,7 @@ private function resolveUrlReference($urlIn, $entityObjects) EntityDataObject::CEST_UNIQUE_VALUE ); if (null !== $param) { - $urlOut = str_replace($paramValue, $param, $urlIn); + $urlOut = str_replace($paramValue, $param, $urlOut); continue; } } diff --git a/src/Magento/FunctionalTestingFramework/DataGenerator/Persist/DataPersistenceHandler.php b/src/Magento/FunctionalTestingFramework/DataGenerator/Persist/DataPersistenceHandler.php index 6e2f8d2b7..290a14c4e 100644 --- a/src/Magento/FunctionalTestingFramework/DataGenerator/Persist/DataPersistenceHandler.php +++ b/src/Magento/FunctionalTestingFramework/DataGenerator/Persist/DataPersistenceHandler.php @@ -63,7 +63,7 @@ public function __construct($entityObject, $dependentObjects = [], $customFields } else { $this->entityObject = clone $entityObject; } - $this->storeCode = 'default'; + $this->storeCode = null; foreach ($dependentObjects as $dependentObject) { $this->dependentObjects[] = $dependentObject->getCreatedObject(); @@ -96,16 +96,11 @@ public function createEntity($storeCode = null) * * @param string $updateDataName * @param array $updateDependentObjects - * @param string $storeCode * @return void */ - public function updateEntity($updateDataName, $updateDependentObjects = [], $storeCode = null) + public function updateEntity($updateDataName, $updateDependentObjects = []) { - if (!empty($storeCode)) { - $this->storeCode = $storeCode; - } - foreach ($updateDependentObjects as $dependentObject) { $this->dependentObjects[] = $dependentObject->getCreatedObject(); } @@ -146,14 +141,10 @@ public function getEntity($index = null, $storeCode = null) /** * Function which executes a delete request based on specific operation metadata * - * @param string $storeCode * @return void */ - public function deleteEntity($storeCode = null) + public function deleteEntity() { - if (!empty($storeCode)) { - $this->storeCode = $storeCode; - } $curlHandler = new CurlHandler('delete', $this->createdObject, $this->storeCode); $curlHandler->executeRequest($this->dependentObjects); } diff --git a/src/Magento/FunctionalTestingFramework/Exceptions/Collector/ExceptionCollector.php b/src/Magento/FunctionalTestingFramework/Exceptions/Collector/ExceptionCollector.php new file mode 100644 index 000000000..4b67add0b --- /dev/null +++ b/src/Magento/FunctionalTestingFramework/Exceptions/Collector/ExceptionCollector.php @@ -0,0 +1,67 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\FunctionalTestingFramework\Exceptions\Collector; + +class ExceptionCollector +{ + /** + * Private array containing all errors to be thrown as part of the exception. + * + * @var array + */ + private $errors = []; + + /** + * Function to add a filename and message for the filename + * + * @param string $filename + * @param string $message + * @return void + */ + public function addError($filename, $message) + { + $error[$filename] = $message; + $this->errors = array_merge_recursive($this->errors, $error); + } + + /** + * Function which throws an exception when there are errors present. + * + * @return void + * @throws \Exception + */ + public function throwException() + { + if (empty($this->errors)) { + return; + } + + $errorMsg = implode("\n\n", $this->formatErrors($this->errors)); + throw new \Exception("\n" . $errorMsg); + } + + /** + * If there are multiple exceptions for a single file, the function flattens the array so they can be printed + * as separate messages. + * + * @param array $errors + * @return array + */ + private function formatErrors($errors) + { + $flattenedErrors = []; + foreach ($errors as $key => $errorMsg) { + if (is_array($errorMsg)) { + $flattenedErrors = array_merge($flattenedErrors, $this->formatErrors($errorMsg)); + continue; + } + + $flattenedErrors[] = $errorMsg; + } + + return $flattenedErrors; + } +} diff --git a/src/Magento/FunctionalTestingFramework/Extension/ErrorLogger.php b/src/Magento/FunctionalTestingFramework/Extension/ErrorLogger.php new file mode 100644 index 000000000..822395921 --- /dev/null +++ b/src/Magento/FunctionalTestingFramework/Extension/ErrorLogger.php @@ -0,0 +1,71 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\FunctionalTestingFramework\Extension; + +/** + * Class ErrorLogger + * @package Magento\FunctionalTestingFramework\Extension + */ +class ErrorLogger +{ + /** + * Error Logger Instance + * @var ErrorLogger + */ + private static $errorLogger; + + /** + * Singleton method to return ErrorLogger. + * @return ErrorLogger + */ + public static function getInstance() + { + if (!self::$errorLogger) { + self::$errorLogger = new ErrorLogger(); + } + + return self::$errorLogger; + } + + /** + * ErrorLogger constructor. + */ + private function __construct() + { + // private constructor + } + + /** + * Loops through stepEvent for browser log entries + * @param \Facebook\WebDriver\Remote\RemoteWebDriver $webDriver + * @param \Codeception\Event\StepEvent $stepEvent + * @return void + */ + public function logErrors($webDriver, $stepEvent) + { + //Types available should be "server", "browser", "driver". Only care about browser at the moment. + $browserLogEntries = $webDriver->manage()->getLog("browser"); + foreach ($browserLogEntries as $entry) { + if (array_key_exists("source", $entry) && $entry["source"] === "javascript") { + $this->logError("javascript", $stepEvent, $entry); + } + } + } + + /** + * Logs errors to console/report. + * @param string $type + * @param \Codeception\Event\StepEvent $stepEvent + * @param array $entry + * @return void + */ + private function logError($type, $stepEvent, $entry) + { + //TODO Add to overall log + $stepEvent->getTest()->getScenario()->comment("{$type} ERROR({$entry["level"]}) - " . $entry["message"]); + } +} diff --git a/src/Magento/FunctionalTestingFramework/Extension/TestContextExtension.php b/src/Magento/FunctionalTestingFramework/Extension/TestContextExtension.php new file mode 100644 index 000000000..c03743dc3 --- /dev/null +++ b/src/Magento/FunctionalTestingFramework/Extension/TestContextExtension.php @@ -0,0 +1,85 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\FunctionalTestingFramework\Extension; + +use \Codeception\Events; +use Magento\FunctionalTestingFramework\Extension\ErrorLogger; + +/** + * Class TestContextExtension + * @SuppressWarnings(PHPMD.UnusedPrivateField) + */ +class TestContextExtension extends \Codeception\Extension +{ + const TEST_PHASE_AFTER = "_after"; + + /** + * Codeception Events Mapping to methods + * @var array + */ + public static $events = [ + Events::TEST_FAIL => 'testFail', + Events::STEP_AFTER => 'afterStep' + ]; + + /** + * Codeception event listener function, triggered on test failure. + * @param \Codeception\Event\FailEvent $e + * @return void + */ + public function testFail(\Codeception\Event\FailEvent $e) + { + $cest = $e->getTest(); + $context = $this->extractContext($e->getFail()->getTrace(), $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) { + 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 string $class + * @return string + */ + public function extractContext($trace, $class) + { + foreach ($trace as $entry) { + $traceClass = $entry["class"] ?? null; + if (strpos($traceClass, $class) != 0) { + return $entry["function"]; + } + } + return null; + } + + /** + * Codeception event listener function, triggered after step. + * Calls ErrorLogger to log JS errors encountered. + * @param \Codeception\Event\StepEvent $e + * @return void + */ + public function afterStep(\Codeception\Event\StepEvent $e) + { + $webDriver = $this->getModule("\Magento\FunctionalTestingFramework\Module\MagentoWebDriver")->webDriver; + ErrorLogger::getInstance()->logErrors($webDriver, $e); + } +} diff --git a/src/Magento/FunctionalTestingFramework/Module/MagentoWebDriver.php b/src/Magento/FunctionalTestingFramework/Module/MagentoWebDriver.php index 9e50687a3..1917e8bab 100644 --- a/src/Magento/FunctionalTestingFramework/Module/MagentoWebDriver.php +++ b/src/Magento/FunctionalTestingFramework/Module/MagentoWebDriver.php @@ -13,11 +13,13 @@ 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\Persist\Curl\WebapiExecutor; use Magento\FunctionalTestingFramework\Util\Protocol\CurlTransport; use Magento\FunctionalTestingFramework\Util\Protocol\CurlInterface; use Magento\Setup\Exception; @@ -435,9 +437,8 @@ public function scrollToTopOfPage() * @param string $command * @returns string */ - public function executeMagentoCLICommand($command) + public function magentoCLI($command) { - $apiURL = $this->config['url'] . getenv('MAGENTO_CLI_COMMAND_PATH'); $executor = new CurlTransport(); $executor->write($apiURL, [getenv('MAGENTO_CLI_COMMAND_PARAMETER') => $command], CurlInterface::POST, []); @@ -446,6 +447,20 @@ public function executeMagentoCLICommand($command) return $response; } + /** + * Runs DELETE request to delete a Magento entity against the url given. + * @param string $url + * @return string + */ + public function deleteEntityByUrl($url) + { + $executor = new WebapiExecutor(null); + $executor->write($url, [], CurlInterface::DELETE, []); + $response = $executor->read(); + $executor->close(); + return $response; + } + /** * Conditional click for an area that should be visible * @@ -458,7 +473,7 @@ public function conditionalClick($selector, $dependentSelector, $visible) { $el = $this->_findElements($dependentSelector); if (sizeof($el) > 1) { - throw new \Exception("more than one element matches selector " . $selector); + throw new \Exception("more than one element matches selector " . $dependentSelector); } $clickCondition = null; @@ -512,6 +527,36 @@ public function _before(TestInterface $test) parent::_before($test); } + /** + * Override for codeception's default dragAndDrop to include offset options. + * @param string $source + * @param string $target + * @param int $xOffset + * @param int $yOffset + * @return void + */ + public function dragAndDrop($source, $target, $xOffset = null, $yOffset = null) + { + if ($xOffset !== null || $yOffset !== null) { + $snodes = $this->matchFirstOrFail($this->baseElement, $source); + $tnodes = $this->matchFirstOrFail($this->baseElement, $target); + + $targetX = intval($tnodes->getLocation()->getX() + $xOffset); + $targetY = intval($tnodes->getLocation()->getY() + $yOffset); + + $travelX = intval($targetX - $snodes->getLocation()->getX()); + $travelY = intval($targetY - $snodes->getLocation()->getY()); + + $action = new WebDriverActions($this->webDriver); + $action->moveToElement($snodes)->perform(); + $action->clickAndHold($snodes)->perform(); + $action->moveByOffset($travelX, $travelY)->perform(); + $action->release()->perform(); + } else { + parent::dragAndDrop($source, $target); + } + } + /** * Override for _failed method in Codeception method. Adds png and html attachments to allure report * following parent execution of test failure processing. @@ -527,6 +572,10 @@ public function _failed(TestInterface $test, $fail) $this->saveScreenshot(); } + if ($this->current_test == null) { + throw new \RuntimeException("Suite condition failure: \n" . $fail->getMessage()); + } + $this->addAttachment($this->pngReport, $test->getMetadata()->getName() . '.png', 'image/png'); $this->addAttachment($this->htmlReport, $test->getMetadata()->getName() . '.html', 'text/html'); @@ -540,8 +589,12 @@ public function _failed(TestInterface $test, $fail) */ public function saveScreenshot() { - $test = $this->current_test; - $filename = preg_replace('~\W~', '.', Descriptor::getTestSignature($test)); + $testDescription = "unknown." . uniqid(); + if ($this->current_test != null) { + $testDescription = Descriptor::getTestSignature($this->current_test); + } + + $filename = preg_replace('~\W~', '.', $testDescription); $outputDir = codecept_output_dir(); $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'); diff --git a/src/Magento/FunctionalTestingFramework/Page/Handlers/PageObjectHandler.php b/src/Magento/FunctionalTestingFramework/Page/Handlers/PageObjectHandler.php index 1985af473..0c47ab7c3 100644 --- a/src/Magento/FunctionalTestingFramework/Page/Handlers/PageObjectHandler.php +++ b/src/Magento/FunctionalTestingFramework/Page/Handlers/PageObjectHandler.php @@ -62,7 +62,7 @@ private function __construct() } $module = $pageData[self::MODULE]; - $sectionNames = array_keys($pageData[self::SECTION]); + $sectionNames = array_keys($pageData[self::SECTION] ?? []); $parameterized = $pageData[self::PARAMETERIZED] ?? false; $this->pageObjects[$pageName] = diff --git a/src/Magento/FunctionalTestingFramework/Page/etc/SectionObject.xsd b/src/Magento/FunctionalTestingFramework/Page/etc/SectionObject.xsd index 04463838d..a2457f4e4 100644 --- a/src/Magento/FunctionalTestingFramework/Page/etc/SectionObject.xsd +++ b/src/Magento/FunctionalTestingFramework/Page/etc/SectionObject.xsd @@ -106,6 +106,7 @@ <xs:enumeration value="multiselect"/> <xs:enumeration value="wysiwyg"/> <xs:enumeration value="iframe"/> + <xs:enumeration value="block"/> </xs:restriction> </xs:simpleType> diff --git a/src/Magento/FunctionalTestingFramework/Suite/Generators/GroupClassGenerator.php b/src/Magento/FunctionalTestingFramework/Suite/Generators/GroupClassGenerator.php index cfc28ea5b..e374d0e6e 100644 --- a/src/Magento/FunctionalTestingFramework/Suite/Generators/GroupClassGenerator.php +++ b/src/Magento/FunctionalTestingFramework/Suite/Generators/GroupClassGenerator.php @@ -26,7 +26,10 @@ class GroupClassGenerator const REQUIRED_ENTITY_KEY = 'requiredEntities'; const LAST_REQUIRED_ENTITY_TAG = 'last'; const MUSTACHE_VAR_TAG = 'var'; - + const MAGENTO_CLI_COMMAND_COMMAND = 'command'; + const REPLACEMENT_ACTIONS = [ + 'comment' => 'print' + ]; const GROUP_DIR_NAME = 'Group'; /** @@ -36,6 +39,16 @@ class GroupClassGenerator */ private $mustacheEngine; + /** + * Static function to return group directory path for precondition files. + * + * @return string + */ + public static function getGroupDirPath() + { + return dirname(dirname(__DIR__)) . DIRECTORY_SEPARATOR . self::GROUP_DIR_NAME . DIRECTORY_SEPARATOR; + } + /** * GroupClassGenerator constructor */ @@ -60,10 +73,7 @@ public function generateGroupClass($suiteObject) { $classContent = $this->createClassContent($suiteObject); $configEntry = self::GROUP_DIR_NAME . DIRECTORY_SEPARATOR . $suiteObject->getName(); - $filePath = dirname(dirname(__DIR__)) . - DIRECTORY_SEPARATOR . - $configEntry . - '.php'; + $filePath = self::getGroupDirPath() . $suiteObject->getName() . '.php'; file_put_contents($filePath, $classContent); return str_replace(DIRECTORY_SEPARATOR, "\\", $configEntry); @@ -83,14 +93,30 @@ private function createClassContent($suiteObject) $mustacheData[self::BEFORE_MUSTACHE_KEY] = $this->buildHookMustacheArray($suiteObject->getBeforeHook()); $mustacheData[self::AFTER_MUSTACHE_KEY] = $this->buildHookMustacheArray($suiteObject->getAfterHook()); - $mustacheData[self::MUSTACHE_VAR_TAG] = array_merge( - $mustacheData[self::BEFORE_MUSTACHE_KEY]['createData'] ?? [], - $mustacheData[self::AFTER_MUSTACHE_KEY]['createData'] ?? [] + $mustacheData[self::MUSTACHE_VAR_TAG] = $this->extractClassVar( + $mustacheData[self::BEFORE_MUSTACHE_KEY], + $mustacheData[self::AFTER_MUSTACHE_KEY] ); return $this->mustacheEngine->render(self::MUSTACHE_TEMPLATE_NAME, $mustacheData); } + /** + * Function which takes the before and after arrays containing the steps for the hook objects and extracts + * any variables names needed by the class template. + * + * @param array $beforeArray + * @param array $afterArray + * @return array + */ + private function extractClassVar($beforeArray, $afterArray) + { + $beforeVar = $beforeArray[self::MUSTACHE_VAR_TAG] ?? []; + $afterVar = $afterArray[self::MUSTACHE_VAR_TAG] ?? []; + + return array_merge($beforeVar, $afterVar); + } + /** * Function which takes hook objects and transforms data into array for mustache template engine. * @@ -100,26 +126,115 @@ private function createClassContent($suiteObject) private function buildHookMustacheArray($hookObj) { $mustacheHookArray = []; + $actions = []; + $hasWebDriverActions = false; + foreach ($hookObj->getActions() as $action) { /** @var ActionObject $action */ + $index = count($actions); + //deleteData contains either url or createDataKey, if it contains the former it needs special formatting + if ($action->getType() !== "createData" + && !array_key_exists(TestGenerator::REQUIRED_ENTITY_REFERENCE, $action->getCustomActionAttributes())) { + if (!$hasWebDriverActions) { + $hasWebDriverActions = true; + } + + $actions = $this->buildWebDriverActionsMustacheArray($action, $actions, $index); + continue; + } + + // add these as vars to be created a class level in the template + if ($action->getType() == 'createData') { + $mustacheHookArray[self::MUSTACHE_VAR_TAG][] = [self::ENTITY_MERGE_KEY => $action->getStepKey()]; + } + $entityArray = []; $entityArray[self::ENTITY_MERGE_KEY] = $action->getStepKey(); - $entityArray[self::ENTITY_NAME_TAG] = - $action->getCustomActionAttributes()['entity'] ?? - $action->getCustomActionAttributes()[TestGenerator::REQUIRED_ENTITY_REFERENCE]; - - // if there is more than 1 custom attribute, we can assume there are required entities - if (count($action->getCustomActionAttributes()) > 1) { - $entityArray[self::REQUIRED_ENTITY_KEY] = - $this->buildReqEntitiesMustacheArray($action->getCustomActionAttributes()); - } + $entityArray[$action->getType()] = $action->getStepKey(); - $mustacheHookArray[$action->getType()][] = $entityArray; + $entityArray = $this->buildPersistenceMustacheArray($action, $entityArray); + $actions[$index] = $entityArray; + } + $mustacheHookArray['actions'] = $actions; + if ($hasWebDriverActions) { + array_unshift($mustacheHookArray['actions'], ['webDriverInit' => true]); + $mustacheHookArray['actions'][] = ['webDriverReset' => true]; } return $mustacheHookArray; } + /** + * Takes an action object and array of generated action steps. Converst the action object into generated php and + * appends the entry to the given array. The result is returned by the function. + * + * @param ActionObject $action + * @param array $actionEntries + * @return array + */ + private function buildWebDriverActionsMustacheArray($action, $actionEntries) + { + $step = TestGenerator::getInstance()->generateStepsPhp([$action], false, 'webDriver'); + $rawPhp = str_replace(["\t", "\n"], "", $step); + $multipleCommands = explode(";", $rawPhp, -1); + foreach ($multipleCommands as $command) { + $actionEntries = $this->replaceReservedTesterFunctions($command . ";", $actionEntries, 'webDriver'); + } + + return $actionEntries; + } + + /** + * Takes a generated php step, an array containing generated php entries for the template, and the actor name + * for the generated step. + * + * @param string $formattedStep + * @param array $actionEntries + * @param string $actor + * @return array + */ + private function replaceReservedTesterFunctions($formattedStep, $actionEntries, $actor) + { + foreach (self::REPLACEMENT_ACTIONS as $testAction => $replacement) { + $testActionCall = "\${$actor}->{$testAction}"; + if (substr($formattedStep, 0, strlen($testActionCall)) == $testActionCall) { + $resultingStep = str_replace($testActionCall, $replacement, $formattedStep); + $actionEntries[] = ['action' => $resultingStep]; + } else { + $actionEntries[] = ['action' => $formattedStep]; + } + } + + return $actionEntries; + } + + /** + * Takes an action object of persistence type and formats an array entiry for mustache template interpretation. + * + * @param ActionObject $action + * @param array $entityArray + * @return array + */ + private function buildPersistenceMustacheArray($action, $entityArray) + { + $entityArray[self::ENTITY_NAME_TAG] = + $action->getCustomActionAttributes()['entity'] ?? + $action->getCustomActionAttributes()[TestGenerator::REQUIRED_ENTITY_REFERENCE]; + + // append entries for any required entities to this entry + if (array_key_exists('requiredEntities', $action->getCustomActionAttributes())) { + $entityArray[self::REQUIRED_ENTITY_KEY] = + $this->buildReqEntitiesMustacheArray($action->getCustomActionAttributes()); + } + + // append entries for customFields if specified by the user. + if (array_key_exists('customFields', $action->getCustomActionAttributes())) { + $entityArray['customFields'] = $action->getStepKey() . 'Fields'; + } + + return $entityArray; + } + /** * Function which takes any required entities under a 'createData' tag and transforms data into array to be consumed * by mustache template. diff --git a/src/Magento/FunctionalTestingFramework/Suite/Handlers/SuiteObjectHandler.php b/src/Magento/FunctionalTestingFramework/Suite/Handlers/SuiteObjectHandler.php index 6795bef5b..8930de98c 100644 --- a/src/Magento/FunctionalTestingFramework/Suite/Handlers/SuiteObjectHandler.php +++ b/src/Magento/FunctionalTestingFramework/Suite/Handlers/SuiteObjectHandler.php @@ -5,16 +5,12 @@ */ namespace Magento\FunctionalTestingFramework\Suite\Handlers; -use Exception; use Magento\FunctionalTestingFramework\ObjectManager\ObjectHandlerInterface; use Magento\FunctionalTestingFramework\ObjectManagerFactory; use Magento\FunctionalTestingFramework\Suite\Objects\SuiteObject; use Magento\FunctionalTestingFramework\Suite\Parsers\SuiteDataParser; use Magento\FunctionalTestingFramework\Suite\Util\SuiteObjectExtractor; -use Magento\FunctionalTestingFramework\Test\Handlers\TestObjectHandler; -use Magento\FunctionalTestingFramework\Test\Util\TestHookObjectExtractor; use Magento\FunctionalTestingFramework\Test\Util\ObjectExtractor; -use Magento\Ui\Test\Unit\Component\PagingTest; /** * Class SuiteObjectHandler @@ -82,6 +78,26 @@ public function getAllObjects() return $this->suiteObjects; } + /** + * Function which return all tests referenced by suites. + * + * @return array + */ + public function getAllTestReferences() + { + $testsReferencedInSuites = []; + $suites = $this->getAllObjects(); + + foreach ($suites as $suite) { + /** @var SuiteObject $suite */ + $test_keys = array_keys($suite->getTests()); + $testToSuiteName = array_fill_keys($test_keys, [$suite->getName()]); + $testsReferencedInSuites = array_merge_recursive($testsReferencedInSuites, $testToSuiteName); + } + + return $testsReferencedInSuites; + } + /** * Method to parse all suite data xml into objects. * diff --git a/src/Magento/FunctionalTestingFramework/Suite/Objects/SuiteObject.php b/src/Magento/FunctionalTestingFramework/Suite/Objects/SuiteObject.php index c0a2be9e9..bf560eac3 100644 --- a/src/Magento/FunctionalTestingFramework/Suite/Objects/SuiteObject.php +++ b/src/Magento/FunctionalTestingFramework/Suite/Objects/SuiteObject.php @@ -117,6 +117,16 @@ public function requiresGroupFile() return !empty($this->hooks); } + /** + * Getter for the Hook Array which contains the before/after objects. + * + * @return TestHookObject[] + */ + public function getHooks() + { + return $this->hooks; + } + /** * Getter for before hooks. * @@ -124,7 +134,7 @@ public function requiresGroupFile() */ public function getBeforeHook() { - return $this->hooks['before']; + return $this->hooks['before'] ?? null; } /** @@ -134,6 +144,6 @@ public function getBeforeHook() */ public function getAfterHook() { - return $this->hooks['after']; + return $this->hooks['after'] ?? null; } } diff --git a/src/Magento/FunctionalTestingFramework/Suite/SuiteGenerator.php b/src/Magento/FunctionalTestingFramework/Suite/SuiteGenerator.php index b09d26d94..9c96742b4 100644 --- a/src/Magento/FunctionalTestingFramework/Suite/SuiteGenerator.php +++ b/src/Magento/FunctionalTestingFramework/Suite/SuiteGenerator.php @@ -6,12 +6,13 @@ namespace Magento\FunctionalTestingFramework\Suite; -use Magento\Framework\Phrase; -use Magento\Framework\Validator\Exception; +use Magento\FunctionalTestingFramework\Exceptions\TestReferenceException; 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\Manifest\BaseTestManifest; use Magento\FunctionalTestingFramework\Util\TestGenerator; use Symfony\Component\Yaml\Yaml; @@ -55,12 +56,75 @@ private function __construct() public static function getInstance() { if (!self::$SUITE_GENERATOR_INSTANCE) { + // clear any previous configurations before any generation occurs. + self::clearPreviousGroupPreconditions(); + self::clearPreviousSessionConfigEntries(); self::$SUITE_GENERATOR_INSTANCE = new SuiteGenerator(); } return self::$SUITE_GENERATOR_INSTANCE; } + /** + * Function which takes all suite configurations and generates to appropriate directory, updating yml configuration + * as needed. Returns an array of all tests generated keyed by test name. + * + * @param BaseTestManifest $testManifest + * @return void + */ + public function generateAllSuites($testManifest) + { + $suites = array_keys(SuiteObjectHandler::getInstance()->getAllObjects()); + if ($testManifest != null) { + $suites = $testManifest->getSuiteConfig(); + } + + foreach ($suites as $suiteName => $suiteContent) { + $firstElement = array_values($suiteContent)[0]; + + // if the first element is a string we know that we simply have an array of tests + if (is_string($firstElement)) { + $this->generateSuiteFromTest($suiteName, $suiteContent); + } + + // if our first element is an array we know that we have split the suites + if (is_array($firstElement)) { + $this->generateSplitSuiteFromTest($suiteName, $suiteContent); + } + } + } + + /** + * 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. @@ -71,23 +135,125 @@ public static function getInstance() public function generateSuite($suiteName) { /**@var SuiteObject $suite **/ - $suite = SuiteObjectHandler::getInstance()->getObject($suiteName); + $this->generateSuiteFromTest($suiteName, []); + } + + /** + * Function which takes a suite name and a set of test names. The function then generates all relevant supporting + * files and classes for the suite. The function takes an optional argument for suites which are split by a parallel + * run so that any pre/post conditions can be duplicated. + * + * @param string $suiteName + * @param array $tests + * @param string $originalSuiteName + * @return void + */ + private function generateSuiteFromTest($suiteName, $tests = [], $originalSuiteName = null) + { $relativePath = TestGenerator::GENERATED_DIR . DIRECTORY_SEPARATOR . $suiteName; - $fullPath = TESTS_MODULE_PATH . DIRECTORY_SEPARATOR . $relativePath; - $groupNamespace = null; + $fullPath = TESTS_MODULE_PATH . DIRECTORY_SEPARATOR . $relativePath . DIRECTORY_SEPARATOR; DirSetupUtil::createGroupDir($fullPath); - $this->generateRelevantGroupTests($suiteName, $suite->getTests()); - if ($suite->requiresGroupFile()) { - // if the suite requires a group file, generate it and set the namespace - $groupNamespace = $this->groupClassGenerator->generateGroupClass($suite); + $relevantTests = []; + if (!empty($tests)) { + $this->validateTestsReferencedInSuite($suiteName, $tests, $originalSuiteName); + foreach ($tests as $testName) { + $relevantTests[$testName] = TestObjectHandler::getInstance()->getObject($testName); + } + } else { + $relevantTests = SuiteObjectHandler::getInstance()->getObject($suiteName)->getTests(); } + $this->generateRelevantGroupTests($suiteName, $relevantTests); + $groupNamespace = $this->generateGroupFile($suiteName, $relevantTests, $originalSuiteName); + $this->appendEntriesToConfig($suiteName, $fullPath, $groupNamespace); print "Suite ${suiteName} generated to ${relativePath}.\n"; } + /** + * Function which validates tests passed in as custom configuration against the configuration defined by the user to + * prevent possible invalid test configurations from executing. + * + * @param string $suiteName + * @param array $testsReferenced + * @param string $originalSuiteName + * @return void + * @throws TestReferenceException + */ + 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 "; + + array_walk($testsReferenced, function ($value) use (&$invalidTestRef, $possibleTestRef, &$errorMsg) { + if (!array_key_exists($value, $possibleTestRef)) { + $invalidTestRef.= "\t{$value}\n"; + } + }); + + if ($invalidTestRef != null) { + throw new TestReferenceException($errorMsg . $invalidTestRef); + } + } + + /** + * Function for generating split groups of tests (following a parallel execution). Takes a paralle suite config + * and generates applicable suites. + * + * @param string $suiteName + * @param array $suiteContent + * @return void + */ + private function generateSplitSuiteFromTest($suiteName, $suiteContent) + { + foreach ($suiteContent as $suiteSplitName => $tests) { + $this->generateSuiteFromTest($suiteSplitName, $tests, $suiteName); + } + } + + /** + * Function which takes a suite name, array of tests, and an original suite name. The function takes these args + * and generates a group file which captures suite level preconditions. + * + * @param string $suiteName + * @param array $tests + * @param string $originalSuiteName + * @return null|string + */ + private function generateGroupFile($suiteName, $tests, $originalSuiteName) + { + // if there's an original suite name we know that this test came from a split group. + if ($originalSuiteName) { + // create the new suite object + /** @var SuiteObject $originalSuite */ + $originalSuite = SuiteObjectHandler::getInstance()->getObject($originalSuiteName); + $suiteObject = new SuiteObject( + $suiteName, + $tests, + [], + $originalSuite->getHooks() + ); + } else { + $suiteObject = SuiteObjectHandler::getInstance()->getObject($suiteName); + // we have to handle the case when there is a custom configuration for an existing suite. + if (count($suiteObject->getTests()) != count($tests)) { + return $this->generateGroupFile($suiteName, $tests, $suiteName); + } + } + + if (!$suiteObject->requiresGroupFile()) { + // if we do not require a group file we don't need a namespace + return null; + } + + // if the suite requires a group file, generate it and set the namespace + return $this->groupClassGenerator->generateGroupClass($suiteObject); + } + /** * Function which accepts a suite name and suite path and appends a new group entry to the codeception.yml.dist * file in order to register the set of tests as a new group. Also appends group object location if required @@ -100,31 +266,53 @@ public function generateSuite($suiteName) */ private function appendEntriesToConfig($suiteName, $suitePath, $groupNamespace) { - $configYmlPath = dirname(dirname(TESTS_BP)) . DIRECTORY_SEPARATOR; - $configYmlFile = $configYmlPath . self::YAML_CODECEPTION_CONFIG_FILENAME; - $defaultConfigYmlFile = $configYmlPath . self::YAML_CODECEPTION_DIST_FILENAME; $relativeSuitePath = substr($suitePath, strlen(dirname(dirname(TESTS_BP))) + 1); - $ymlContents = null; - if (file_exists($configYmlFile)) { - $ymlContents = file_get_contents($configYmlFile); - } else { - $ymlContents = file_get_contents($defaultConfigYmlFile); - } - - $ymlArray = Yaml::parse($ymlContents) ?? []; + $ymlArray = self::getYamlFileContents(); if (!array_key_exists(self::YAML_GROUPS_TAG, $ymlArray)) { $ymlArray[self::YAML_GROUPS_TAG]= []; } - $ymlArray[self::YAML_GROUPS_TAG][$suiteName] = [$relativeSuitePath]; - if ($groupNamespace) { $ymlArray[self::YAML_EXTENSIONS_TAG][self::YAML_ENABLED_TAG][] = $groupNamespace; } + $ymlArray[self::YAML_GROUPS_TAG][$suiteName] = [$relativeSuitePath]; $ymlText = self::YAML_COPYRIGHT_TEXT . Yaml::dump($ymlArray, 10); - file_put_contents($configYmlFile, $ymlText); + file_put_contents(self::getYamlConfigFilePath() . self::YAML_CODECEPTION_CONFIG_FILENAME, $ymlText); + } + + /** + * Function which takes the current config.yml array and clears any previous configuration for suite group object + * files. + * + * @return void + */ + private static function clearPreviousSessionConfigEntries() + { + $ymlArray = self::getYamlFileContents(); + $newYmlArray = $ymlArray; + // if the yaml entries haven't already been cleared + if (array_key_exists(self::YAML_EXTENSIONS_TAG, $ymlArray)) { + foreach ($ymlArray[self::YAML_EXTENSIONS_TAG][self::YAML_ENABLED_TAG] as $key => $entry) { + if (preg_match('/(Group\\\\.*)/', $entry)) { + unset($newYmlArray[self::YAML_EXTENSIONS_TAG][self::YAML_ENABLED_TAG][$key]); + } + + } + + // needed for proper yml file generation based on indices + $newYmlArray[self::YAML_EXTENSIONS_TAG][self::YAML_ENABLED_TAG] = + array_values($newYmlArray[self::YAML_EXTENSIONS_TAG][self::YAML_ENABLED_TAG]); + + } + + if (array_key_exists(self::YAML_GROUPS_TAG, $newYmlArray)) { + unset($newYmlArray[self::YAML_GROUPS_TAG]); + } + + $ymlText = self::YAML_COPYRIGHT_TEXT . Yaml::dump($newYmlArray, 10); + file_put_contents(self::getYamlConfigFilePath() . self::YAML_CODECEPTION_CONFIG_FILENAME, $ymlText); } /** @@ -139,6 +327,47 @@ private function appendEntriesToConfig($suiteName, $suitePath, $groupNamespace) private function generateRelevantGroupTests($path, $tests) { $testGenerator = TestGenerator::getInstance($path, $tests); - $testGenerator->createAllTestFiles(); + $testGenerator->createAllTestFiles(null, []); + } + + /** + * Function which on first execution deletes all generate php in the MFTF Group directory + * + * @return void + */ + private static function clearPreviousGroupPreconditions() + { + $groupFilePath = GroupClassGenerator::getGroupDirPath(); + array_map('unlink', glob("$groupFilePath*.php")); + } + + /** + * Function to return contents of codeception.yml file for config changes. + * + * @return array + */ + private static function getYamlFileContents() + { + $configYmlFile = self::getYamlConfigFilePath() . self::YAML_CODECEPTION_CONFIG_FILENAME; + $defaultConfigYmlFile = self::getYamlConfigFilePath() . self::YAML_CODECEPTION_DIST_FILENAME; + + $ymlContents = null; + if (file_exists($configYmlFile)) { + $ymlContents = file_get_contents($configYmlFile); + } else { + $ymlContents = file_get_contents($defaultConfigYmlFile); + } + + return Yaml::parse($ymlContents) ?? []; + } + + /** + * Static getter for the Config yml filepath (as path cannot be stored in a const) + * + * @return string + */ + private static function getYamlConfigFilePath() + { + return dirname(dirname(TESTS_BP)) . DIRECTORY_SEPARATOR; } } diff --git a/src/Magento/FunctionalTestingFramework/Suite/Util/SuiteObjectExtractor.php b/src/Magento/FunctionalTestingFramework/Suite/Util/SuiteObjectExtractor.php index 76bc409ef..4db1abfc7 100644 --- a/src/Magento/FunctionalTestingFramework/Suite/Util/SuiteObjectExtractor.php +++ b/src/Magento/FunctionalTestingFramework/Suite/Util/SuiteObjectExtractor.php @@ -6,12 +6,14 @@ namespace Magento\FunctionalTestingFramework\Suite\Util; use Exception; +use Magento\FunctionalTestingFramework\Exceptions\XmlException; use Magento\FunctionalTestingFramework\Suite\Objects\SuiteObject; use Magento\FunctionalTestingFramework\Test\Handlers\TestObjectHandler; use Magento\FunctionalTestingFramework\Test\Objects\TestObject; use Magento\FunctionalTestingFramework\Test\Util\BaseObjectExtractor; use Magento\FunctionalTestingFramework\Test\Util\TestHookObjectExtractor; use Magento\FunctionalTestingFramework\Test\Util\TestObjectExtractor; +use Magento\FunctionalTestingFramework\Util\Validation\NameValidationUtil; class SuiteObjectExtractor extends BaseObjectExtractor { @@ -37,19 +39,47 @@ public function __construct() * * @param array $parsedSuiteData * @return array + * @throws XmlException + * @SuppressWarnings(PHPMD.CyclomaticComplexity) + * @SuppressWarnings(PHPMD.NPathComplexity) */ public function parseSuiteDataIntoObjects($parsedSuiteData) { $suiteObjects = []; $testHookObjectExtractor = new TestHookObjectExtractor(); + + // make sure there are suites defined before trying to parse as objects. + if (!array_key_exists(self::SUITE_ROOT_TAG, $parsedSuiteData)) { + return $suiteObjects; + } + foreach ($parsedSuiteData[self::SUITE_ROOT_TAG] as $parsedSuite) { if (!is_array($parsedSuite)) { // skip non array items parsed from suite (suite objects will always be arrays) continue; } + // validate the name used isn't using special char or the "default" reserved name + NameValidationUtil::validateName($parsedSuite[self::NAME], 'Suite'); + if ($parsedSuite[self::NAME] == 'default') { + throw new XmlException("A Suite can not have the name \"default\""); + } + $suiteHooks = []; + //Check for collisions between suite name and existing group name + $suiteName = $parsedSuite[self::NAME]; + $testGroupConflicts = TestObjectHandler::getInstance()->getTestsByGroup($suiteName); + if (!empty($testGroupConflicts)) { + $testGroupConflictsFileNames = ""; + foreach ($testGroupConflicts as $test) { + $testGroupConflictsFileNames .= $test->getFilename() . "\n"; + } + $exceptionmessage = "\"Suite names and Group names can not have the same value. \t\n" . + "Suite: \"{$suiteName}\" also exists as a group annotation in: \n{$testGroupConflictsFileNames}"; + throw new XmlException($exceptionmessage); + } + //extract include and exclude references $groupTestsToInclude = $parsedSuite[self::INCLUDE_TAG_NAME] ?? []; $groupTestsToExclude = $parsedSuite[self::EXCLUDE_TAG_NAME] ?? []; @@ -78,6 +108,12 @@ 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] + )); + } // create the new suite object $suiteObjects[$parsedSuite[self::NAME]] = new SuiteObject( diff --git a/src/Magento/FunctionalTestingFramework/Suite/etc/suiteSchema.xsd b/src/Magento/FunctionalTestingFramework/Suite/etc/suiteSchema.xsd index c57b58d02..34ef37594 100644 --- a/src/Magento/FunctionalTestingFramework/Suite/etc/suiteSchema.xsd +++ b/src/Magento/FunctionalTestingFramework/Suite/etc/suiteSchema.xsd @@ -48,17 +48,17 @@ <xs:element type="moduleSuiteOptionType" name="module" minOccurs="0"/> </xs:choice> </xs:complexType> - <xs:complexType name="suiteHookType"> - <xs:choice minOccurs="0" maxOccurs="unbounded"> - <xs:group ref="dataOperationTags" maxOccurs="unbounded" minOccurs="0"/> - </xs:choice> - </xs:complexType> + <!--<xs:complexType name="suiteHookType">--> + <!--<xs:choice minOccurs="0" maxOccurs="unbounded">--> + <!--<xs:group ref="dataOperationTags" maxOccurs="unbounded" minOccurs="0"/>--> + <!--</xs:choice>--> + <!--</xs:complexType>--> <xs:complexType name="suiteType"> <xs:choice minOccurs="0" maxOccurs="unbounded"> <xs:element type="includeType" name="include" maxOccurs="1"/> <xs:element type="excludeType" name="exclude" maxOccurs="1"/> - <xs:element type="suiteHookType" name="before" maxOccurs="1"/> - <xs:element type="suiteHookType" name="after" maxOccurs="1"/> + <xs:element type="hookType" name="before" maxOccurs="1"/> + <xs:element type="hookType" name="after" maxOccurs="1"/> </xs:choice> <xs:attribute type="xs:string" name="name"/> </xs:complexType> diff --git a/src/Magento/FunctionalTestingFramework/Suite/views/SuiteClass.mustache b/src/Magento/FunctionalTestingFramework/Suite/views/SuiteClass.mustache index 87fd6caf8..cee281f36 100644 --- a/src/Magento/FunctionalTestingFramework/Suite/views/SuiteClass.mustache +++ b/src/Magento/FunctionalTestingFramework/Suite/views/SuiteClass.mustache @@ -17,9 +17,11 @@ use Magento\FunctionalTestingFramework\DataGenerator\Persist\DataPersistenceHand class {{suiteName}} extends \Codeception\GroupObject { public static $group = '{{suiteName}}'; - private static $TEST_COUNT = {{testCount}}; - private static $CURRENT_TEST_RUN = 0; - + private $testCount = {{testCount}}; + private $preconditionFailure = null; + private $currentTestRun = 0; + private static $HOOK_EXECUTION_INIT = "\n/******** Beginning execution of {{suiteName}} suite %s block ********/\n"; + private static $HOOK_EXECUTION_END = "\n/******** Execution of {{suiteName}} suite %s block complete ********/\n"; {{#var}} private ${{stepKey}}; {{/var}} @@ -28,10 +30,28 @@ class {{suiteName}} extends \Codeception\GroupObject public function _before(\Codeception\Event\TestEvent $e) { // increment test count per execution - self::$CURRENT_TEST_RUN++; + $this->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 { + {{> testActions}} + } catch (\Exception $exception) { + $this->preconditionFailure = $exception->getMessage(); + } - if (self::$CURRENT_TEST_RUN == 1) { - {{> dataPersistence}} + print sprintf(self::$HOOK_EXECUTION_END, "before"); } } {{/before}} @@ -39,15 +59,22 @@ class {{suiteName}} extends \Codeception\GroupObject {{#after}} public function _after(\Codeception\Event\TestEvent $e) { - if (self::$CURRENT_TEST_RUN == self::$TEST_COUNT) { - {{> dataPersistence}} - } + $this->executePostConditions(); } - public function _failed(\Codeception\Event\TestEvent $e) + + private function executePostConditions() { - if (self::$CURRENT_TEST_RUN == self::$TEST_COUNT) { - {{> dataPersistence}} + if ($this->currentTestRun == $this->testCount) { + print sprintf(self::$HOOK_EXECUTION_INIT, "after"); + + try { + {{> testActions}} + } catch (\Exception $exception) { + print $exception->getMessage(); + } + + print sprintf(self::$HOOK_EXECUTION_END, "after"); } } {{/after}} diff --git a/src/Magento/FunctionalTestingFramework/Suite/views/partials/dataPersistence.mustache b/src/Magento/FunctionalTestingFramework/Suite/views/partials/dataPersistence.mustache deleted file mode 100644 index 91602e594..000000000 --- a/src/Magento/FunctionalTestingFramework/Suite/views/partials/dataPersistence.mustache +++ /dev/null @@ -1,8 +0,0 @@ -{{#createData}} -${{entityName}} = DataObjectHandler::getInstance()->getObject("{{entityName}}"); -$this->{{stepKey}} = new DataPersistenceHandler(${{entityName}}, [{{#requiredEntities}}$this->{{entityName}}{{^last}}, {{/last}}{{/requiredEntities}}]); -$this->{{stepKey}}->createEntity(); -{{/createData}} -{{#deleteData}} -$this->{{entityName}}->deleteEntity(); -{{/deleteData}} diff --git a/src/Magento/FunctionalTestingFramework/Suite/views/partials/testActions.mustache b/src/Magento/FunctionalTestingFramework/Suite/views/partials/testActions.mustache new file mode 100644 index 000000000..46951aa46 --- /dev/null +++ b/src/Magento/FunctionalTestingFramework/Suite/views/partials/testActions.mustache @@ -0,0 +1,32 @@ +{{#actions}} +{{#webDriverInit}} +$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(); +{{/webDriverInit}} +{{#webDriverReset}} + +// reset configuration and close session +$this->getModule('\Magento\FunctionalTestingFramework\Module\MagentoWebDriver')->_resetConfig(); +$webDriver->webDriver->close(); +$webDriver->webDriver = null; +{{/webDriverReset}} +{{#action}} +{{{action}}} +{{/action}} +{{#createData}} +${{entityName}} = DataObjectHandler::getInstance()->getObject("{{entityName}}"); +$this->{{stepKey}} = new DataPersistenceHandler(${{entityName}}, [{{#requiredEntities}}$this->{{entityName}}{{^last}}, {{/last}}{{/requiredEntities}}]{{#customFields}}, ${{customFields}}{{/customFields}}); +$this->{{stepKey}}->createEntity(); +{{/createData}} +{{#deleteData}} +$this->{{entityName}}->deleteEntity(); +{{/deleteData}} +{{/actions}} diff --git a/src/Magento/FunctionalTestingFramework/Test/Config/ActionGroupDom.php b/src/Magento/FunctionalTestingFramework/Test/Config/ActionGroupDom.php new file mode 100644 index 000000000..794007dde --- /dev/null +++ b/src/Magento/FunctionalTestingFramework/Test/Config/ActionGroupDom.php @@ -0,0 +1,38 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\FunctionalTestingFramework\Test\Config; + +use Magento\FunctionalTestingFramework\Exceptions\Collector\ExceptionCollector; + +class ActionGroupDom extends Dom +{ + const ACTION_GROUP_FILE_NAME_ENDING = "ActionGroup.xml"; + + /** + * 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|null $filename + * @param ExceptionCollector $exceptionCollector + * @return \DOMDocument + */ + public function initDom($xml, $filename = null, $exceptionCollector = null) + { + $dom = parent::initDom($xml); + + if (strpos($filename, self::ACTION_GROUP_FILE_NAME_ENDING)) { + $actionGroupNodes = $dom->getElementsByTagName('actionGroup'); + foreach ($actionGroupNodes as $actionGroupNode) { + /** @var \DOMElement $actionGroupNode */ + $actionGroupNode->setAttribute(self::TEST_META_FILENAME_ATTRIBUTE, $filename); + $this->validateDomStepKeys($actionGroupNode, $filename, 'Action Group', $exceptionCollector); + } + } + + return $dom; + } +} diff --git a/src/Magento/FunctionalTestingFramework/Test/Config/Dom.php b/src/Magento/FunctionalTestingFramework/Test/Config/Dom.php index a2d9c180f..24710243f 100644 --- a/src/Magento/FunctionalTestingFramework/Test/Config/Dom.php +++ b/src/Magento/FunctionalTestingFramework/Test/Config/Dom.php @@ -6,6 +6,7 @@ namespace Magento\FunctionalTestingFramework\Test\Config; +use Magento\FunctionalTestingFramework\Exceptions\Collector\ExceptionCollector; use Magento\FunctionalTestingFramework\Exceptions\XmlException; use Magento\FunctionalTestingFramework\Config\Dom\NodeMergingConfig; use Magento\FunctionalTestingFramework\Config\Dom\NodePathMatcher; @@ -21,6 +22,7 @@ class Dom extends \Magento\FunctionalTestingFramework\Config\Dom * TestDom constructor. * @param string $xml * @param string $filename + * @param ExceptionCollector $exceptionCollector * @param array $idAttributes * @param string $typeAttributeName * @param string $schemaFile @@ -29,6 +31,7 @@ class Dom extends \Magento\FunctionalTestingFramework\Config\Dom public function __construct( $xml, $filename, + $exceptionCollector, array $idAttributes = [], $typeAttributeName = null, $schemaFile = null, @@ -38,7 +41,7 @@ public function __construct( $this->nodeMergingConfig = new NodeMergingConfig(new NodePathMatcher(), $idAttributes); $this->typeAttributeName = $typeAttributeName; $this->errorFormat = $errorFormat; - $this->dom = $this->initDom($xml, $filename); + $this->dom = $this->initDom($xml, $filename, $exceptionCollector); $this->rootNamespace = $this->dom->lookupNamespaceUri($this->dom->namespaceURI); } @@ -47,9 +50,10 @@ public function __construct( * * @param string $xml * @param string|null $filename + * @param ExceptionCollector $exceptionCollector * @return \DOMDocument */ - public function initDom($xml, $filename = null) + public function initDom($xml, $filename = null, $exceptionCollector = null) { $dom = parent::initDom($xml); @@ -58,7 +62,7 @@ public function initDom($xml, $filename = null) foreach ($testNodes as $testNode) { /** @var \DOMElement $testNode */ $testNode->setAttribute(self::TEST_META_FILENAME_ATTRIBUTE, $filename); - $this->validateTestDomStepKeys($testNode, $filename); + $this->validateDomStepKeys($testNode, $filename, 'Test', $exceptionCollector); } } @@ -70,11 +74,12 @@ public function initDom($xml, $filename = null) * * @param string $xml * @param string|null $filename + * @param ExceptionCollector $exceptionCollector * @return void */ - public function merge($xml, $filename = null) + public function merge($xml, $filename = null, $exceptionCollector = null) { - $dom = $this->initDom($xml, $filename); + $dom = $this->initDom($xml, $filename, $exceptionCollector); $this->mergeNode($dom->documentElement, ''); } @@ -83,10 +88,12 @@ public function merge($xml, $filename = null) * * @param \DOMElement $testNode * @param string $filename + * @param string $type + * @param ExceptionCollector $exceptionCollector * @return void * @throws XmlException */ - private function validateTestDomStepKeys($testNode, $filename) + protected function validateDomStepKeys($testNode, $filename, $type, $exceptionCollector) { $childNodes = $testNode->childNodes; @@ -99,7 +106,7 @@ private function validateTestDomStepKeys($testNode, $filename) } if (in_array($currentNode->nodeName, self::TEST_HOOK_NAMES)) { - $this->validateTestDomStepKeys($currentNode, $filename); + $this->validateDomStepKeys($currentNode, $filename, $type, $exceptionCollector); } if ($currentNode->hasAttribute('stepKey')) { @@ -116,7 +123,8 @@ private function validateTestDomStepKeys($testNode, $filename) $stepKeyError .= "\tstepKey: {$duplicateValue} is used more than once.\n"; } - throw new XmlException("Tests cannot use stepKey more than once!\t\n{$stepKeyError}\tin file: {$filename}"); + $errorMsg = "{$type}s cannot use stepKey more than once.\t\n{$stepKeyError}\tin file: {$filename}"; + $exceptionCollector->addError($filename, $errorMsg); } } } diff --git a/src/Magento/FunctionalTestingFramework/Test/Config/Reader/Filesystem.php b/src/Magento/FunctionalTestingFramework/Test/Config/Reader/Filesystem.php index c2d16a13a..d96a926c5 100644 --- a/src/Magento/FunctionalTestingFramework/Test/Config/Reader/Filesystem.php +++ b/src/Magento/FunctionalTestingFramework/Test/Config/Reader/Filesystem.php @@ -6,6 +6,7 @@ namespace Magento\FunctionalTestingFramework\Test\Config\Reader; +use Magento\FunctionalTestingFramework\Exceptions\Collector\ExceptionCollector; use Magento\FunctionalTestingFramework\Util\Iterator\File; class Filesystem extends \Magento\FunctionalTestingFramework\Config\Reader\Filesystem @@ -19,6 +20,8 @@ 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) { @@ -27,17 +30,18 @@ public function readFiles($fileList) $configMerger = $this->createConfigMerger( $this->domDocumentClass, $content, - $fileList->getFilename() + $fileList->getFilename(), + $exceptionCollector ); } else { - $configMerger->merge($content, $fileList->getFilename()); + $configMerger->merge($content, $fileList->getFilename(), $exceptionCollector); } } catch (\Magento\FunctionalTestingFramework\Config\Dom\ValidationException $e) { throw new \Exception("Invalid XML in file " . $key . ":\n" . $e->getMessage()); } } + $exceptionCollector->throwException(); if ($this->validationState->isValidationRequired()) { - $errors = []; if ($configMerger && !$configMerger->validate($this->schemaFile, $errors)) { $message = "Invalid Document \n"; throw new \Exception($message . implode("\n", $errors)); @@ -57,14 +61,16 @@ public function readFiles($fileList) * @param string $mergerClass * @param string $initialContents * @param string $filename + * @param ExceptionCollector $exceptionCollector * @return \Magento\FunctionalTestingFramework\Config\Dom * @throws \UnexpectedValueException */ - protected function createConfigMerger($mergerClass, $initialContents, $filename = null) + protected function createConfigMerger($mergerClass, $initialContents, $filename = null, $exceptionCollector = null) { $result = new $mergerClass( $initialContents, $filename, + $exceptionCollector, $this->idAttributes, null, $this->perFileSchema diff --git a/src/Magento/FunctionalTestingFramework/Test/Objects/ActionGroupObject.php b/src/Magento/FunctionalTestingFramework/Test/Objects/ActionGroupObject.php index 90be0a643..fdef1107e 100644 --- a/src/Magento/FunctionalTestingFramework/Test/Objects/ActionGroupObject.php +++ b/src/Magento/FunctionalTestingFramework/Test/Objects/ActionGroupObject.php @@ -125,33 +125,15 @@ private function getResolvedActionsWithArgs($arguments, $actionReferenceKey) { $resolvedActions = []; - // $regexPattern match on: $matches[0] {{section.element(arg.field)}} - // $matches[1] = section.element - // $matches[2] = arg.field - $regexPattern = '/{{([\w.\[\]]+)\(*([\w.$\',\s\[\]]+)*\)*}}/'; - foreach ($this->parsedActions as $action) { $varAttributes = array_intersect($this->varAttributes, array_keys($action->getCustomActionAttributes())); $newActionAttributes = []; if (!empty($varAttributes)) { - // 1 check to see if we have pertinent var - foreach ($varAttributes as $varAttribute) { - $attributeValue = $action->getCustomActionAttributes()[$varAttribute]; - preg_match_all($regexPattern, $attributeValue, $matches); - if (empty($matches[0])) { - continue; - } - - //get rid of full match {{arg.field(arg.field)}} - array_shift($matches); - - $newActionAttributes[$varAttribute] = $this->replaceAttributeArguments( - $arguments, - $attributeValue, - $matches - ); - } + $newActionAttributes = $this->resolveAttributesWithArguments( + $arguments, + $action->getCustomActionAttributes() + ); } // we append the action reference key to any linked action and the action's merge key as the user might @@ -159,7 +141,7 @@ private function getResolvedActionsWithArgs($arguments, $actionReferenceKey) $resolvedActions[$action->getStepKey() . ucfirst($actionReferenceKey)] = new ActionObject( $action->getStepKey() . ucfirst($actionReferenceKey), $action->getType(), - array_merge($action->getCustomActionAttributes(), $newActionAttributes), + array_replace_recursive($action->getCustomActionAttributes(), $newActionAttributes), $action->getLinkedAction() == null ? null : $action->getLinkedAction() . ucfirst($actionReferenceKey), $action->getOrderOffset(), [self::ACTION_GROUP_ORIGIN_NAME => $this->name, @@ -170,6 +152,50 @@ private function getResolvedActionsWithArgs($arguments, $actionReferenceKey) return $resolvedActions; } + /** + * Resolves all references to arguments in attributes, and subAttributes. + * @param array $arguments + * @param array $attributes + * @return array + */ + private function resolveAttributesWithArguments($arguments, $attributes) + { + // $regexPattern match on: $matches[0] {{section.element(arg.field)}} + // $matches[1] = section.element + // $matches[2] = arg.field + $regexPattern = '/{{([\w.\[\]]+)\(*([\w.$\',\s\[\]]+)*\)*}}/'; + + $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( + $arguments, + $attributeValue + ); + continue; + } + + preg_match_all($regexPattern, $attributeValue, $matches); + + if (empty($matches[0])) { + continue; + } + + //get rid of full match {{arg.field(arg.field)}} + array_shift($matches); + + $newActionAttributes[$attributeKey] = $this->replaceAttributeArguments( + $arguments, + $attributeValue, + $matches + ); + } + 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. diff --git a/src/Magento/FunctionalTestingFramework/Test/Objects/ActionObject.php b/src/Magento/FunctionalTestingFramework/Test/Objects/ActionObject.php index 24971cef5..a0436bc19 100644 --- a/src/Magento/FunctionalTestingFramework/Test/Objects/ActionObject.php +++ b/src/Magento/FunctionalTestingFramework/Test/Objects/ActionObject.php @@ -5,8 +5,10 @@ */ namespace Magento\FunctionalTestingFramework\Test\Objects; +use Magento\FunctionalTestingFramework\Config\MftfApplicationConfig; use Magento\FunctionalTestingFramework\DataGenerator\Handlers\DataObjectHandler; use Magento\FunctionalTestingFramework\DataGenerator\Objects\EntityDataObject; +use Magento\FunctionalTestingFramework\Exceptions\XmlException; use Magento\FunctionalTestingFramework\ObjectManager\ObjectHandlerInterface; use Magento\FunctionalTestingFramework\Page\Objects\PageObject; use Magento\FunctionalTestingFramework\Page\Objects\SectionObject; @@ -20,7 +22,16 @@ class ActionObject { const __ENV = "_ENV"; - const DATA_ENABLED_ATTRIBUTES = ["userInput", "parameterArray", "expected", "actual"]; + const DATA_ENABLED_ATTRIBUTES = [ + "userInput", + "parameterArray", + "expected", + "actual", + "x", + "y", + "expectedResult", + "actualResult" + ]; const SELECTOR_ENABLED_ATTRIBUTES = [ 'selector', 'dependentSelector', @@ -34,14 +45,15 @@ class ActionObject const ASSERTION_ATTRIBUTES = ["expectedResult" => "expected", "actualResult" => "actual"]; const ASSERTION_TYPE_ATTRIBUTE = "type"; const ASSERTION_VALUE_ATTRIBUTE = "value"; + const DELETE_DATA_MUTUAL_EXCLUSIVE_ATTRIBUTES = ["url", "createDataKey"]; const EXTERNAL_URL_AREA_INVALID_ACTIONS = ['amOnPage']; + const FUNCTION_CLOSURE_ACTIONS = ['waitForElementChange', 'performOn']; const MERGE_ACTION_ORDER_AFTER = 'after'; const MERGE_ACTION_ORDER_BEFORE = 'before'; const ACTION_ATTRIBUTE_URL = 'url'; const ACTION_ATTRIBUTE_SELECTOR = 'selector'; const ACTION_ATTRIBUTE_VARIABLE_REGEX_PARAMETER = '/\(.+\)/'; - const ACTION_ATTRIBUTE_VARIABLE_REGEX_PATTERN = '/{{[\w]+\.[\w\[\]]+}}/'; - const ACTION_ATTRIBUTE_VARIABLE_REGEX_PATTERN_WITH_PARAMS= '/{{[\w]+\.[\w]+\(.+\)}}/'; + const ACTION_ATTRIBUTE_VARIABLE_REGEX_PATTERN = '/({{[\w]+\.[\w\[\]]+}})|({{[\w]+\.[\w]+\(.+\)}})/'; /** * The unique identifier for the action @@ -230,6 +242,9 @@ public function resolveReferences() $this->resolveSelectorReferenceAndTimeout(); $this->resolveUrlReference(); $this->resolveDataInputReferences(); + if ($this->getType() == "deleteData") { + $this->validateMutuallyExclusiveAttributes(self::DELETE_DATA_MUTUAL_EXCLUSIVE_ATTRIBUTES); + } } } @@ -251,7 +266,8 @@ public function trimAssertionAttributes() $oldAttributes = array_intersect($actionAttributeKeys, ActionObject::OLD_ASSERTION_ATTRIBUTES); if (!empty($oldAttributes)) { // @codingStandardsIgnoreStart - if ($GLOBALS['GENERATE_TESTS'] ?? false == true) { + $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); } // @codingStandardsIgnoreEnd @@ -371,7 +387,7 @@ private function resolveDataInputReferences() foreach ($relevantDataAttributes as $dataAttribute) { $varInput = $this->actionAttributes[$dataAttribute]; $replacement = $this->findAndReplaceReferences(DataObjectHandler::getInstance(), $varInput); - if ($replacement != null) { + if ($replacement !== null) { $this->resolvedCustomAttributes[$dataAttribute] = $replacement; } } @@ -444,7 +460,7 @@ private function stripAndReturnParameters($reference) private function findAndReplaceReferences($objectHandler, $inputString) { //look for parameter area, if so use different regex - $regex = $this->resolveRegexPatternForReference($inputString); + $regex = ActionObject::ACTION_ATTRIBUTE_VARIABLE_REGEX_PATTERN; preg_match_all($regex, $inputString, $matches); @@ -480,9 +496,13 @@ private function findAndReplaceReferences($objectHandler, $inputString) $this->setTimeout($obj->getElement($objField)->getTimeout()); } elseif (get_class($obj) == EntityDataObject::class) { $replacement = $this->resolveEntityDataObjectReference($obj, $match); + + if (is_array($replacement)) { + $replacement = '["' . implode('","', array_map('addSlashes', $replacement)) . '"]'; + } } - if ($replacement == null) { + if ($replacement === null) { if (get_class($objectHandler) != DataObjectHandler::class) { return $this->findAndReplaceReferences(DataObjectHandler::getInstance(), $outputString); } else { @@ -497,6 +517,28 @@ private function findAndReplaceReferences($objectHandler, $inputString) return $outputString; } + /** + * Validates that the mutually exclusive attributes passed in don't all occur. + * @param array $attributes + * @return void + * @throws TestReferenceException + */ + private function validateMutuallyExclusiveAttributes(array $attributes) + { + $matches = array_intersect($attributes, array_keys($this->getCustomActionAttributes())); + if (count($matches) > 1) { + throw new TestReferenceException( + "Actions of type '{$this->getType()}' must only contain one attribute of types '" + . implode("', '", $attributes) . "'" + ); + } elseif (count($matches) == 0) { + throw new TestReferenceException( + "Actions of type '{$this->getType()}' must contain at least one attribute of types '" + . implode("', '", $attributes) . "'" + ); + } + } + /** * Validates the page objects area 'external' against a list of known incompatible types * @@ -514,20 +556,6 @@ private function validateUrlAreaAgainstActionType($obj) } } - /** - * Determines whether the given $inputString has (params), and returns the appropriate regex for use in matching. - * @param string $inputString - * @return string - */ - private function resolveRegexPatternForReference($inputString) - { - if (preg_match(ActionObject::ACTION_ATTRIBUTE_VARIABLE_REGEX_PATTERN_WITH_PARAMS, $inputString) === 1) { - return ActionObject::ACTION_ATTRIBUTE_VARIABLE_REGEX_PATTERN_WITH_PARAMS; - } else { - return ActionObject::ACTION_ATTRIBUTE_VARIABLE_REGEX_PATTERN; - } - } - /** * Gets the object's dataByName with given $match, differentiating behavior between <array> and <data> nodes. * @param string $obj diff --git a/src/Magento/FunctionalTestingFramework/Test/Objects/TestObject.php b/src/Magento/FunctionalTestingFramework/Test/Objects/TestObject.php index de64100b8..8d8700868 100644 --- a/src/Magento/FunctionalTestingFramework/Test/Objects/TestObject.php +++ b/src/Magento/FunctionalTestingFramework/Test/Objects/TestObject.php @@ -43,6 +43,13 @@ class TestObject */ private $hooks = []; + /** + * String of filename of test + * + * @var String + */ + private $filename; + /** * TestObject constructor. * @@ -50,13 +57,15 @@ class TestObject * @param ActionObject[] $parsedSteps * @param array $annotations * @param TestHookObject[] $hooks + * @param String $filename */ - public function __construct($name, $parsedSteps, $annotations, $hooks) + public function __construct($name, $parsedSteps, $annotations, $hooks, $filename = null) { $this->name = $name; $this->parsedSteps = $parsedSteps; $this->annotations = $annotations; $this->hooks = $hooks; + $this->filename = $filename; } /** @@ -69,6 +78,29 @@ public function getName() return $this->name; } + /** + * Getter for the Test Filename + * + * @return string + */ + public function getFilename() + { + return $this->filename; + } + + /** + * Getter for the skip_test boolean + * + * @return string + */ + public function isSkipped() + { + if (array_key_exists('group', $this->annotations) && (in_array("skip", $this->annotations['group']))) { + return true; + } + return false; + } + /** * Getter for Codeception format name * @@ -100,6 +132,10 @@ public function getAnnotations() */ public function getHooks() { + // if this test is skipped we do not want any before/after actions to generate as the tests will not run + if ($this->isSkipped()) { + return []; + } return $this->hooks; } @@ -110,6 +146,11 @@ public function getHooks() */ public function getTestActionCount() { + // 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()); @@ -120,7 +161,6 @@ public function getTestActionCount() } $testActions = count($this->getOrderedActions()); - return $hookActions + $testActions; } @@ -159,4 +199,21 @@ public function getOrderedActions() $mergeUtil = new ActionMergeUtil($this->getName(), "Test"); return $mergeUtil->resolveActionSteps($this->parsedSteps); } + + /** + * Get information about actions and steps in test. + * + * @return array + */ + public function getDebugInformation() + { + $debugInformation = []; + $orderList = $this->getOrderedActions(); + + foreach ($orderList as $action) { + $debugInformation[] = "\t" . $action->getType() . ' ' . $action->getStepKey(); + } + + return $debugInformation; + } } diff --git a/src/Magento/FunctionalTestingFramework/Test/Util/ActionGroupObjectExtractor.php b/src/Magento/FunctionalTestingFramework/Test/Util/ActionGroupObjectExtractor.php index a49246d44..aeef21760 100644 --- a/src/Magento/FunctionalTestingFramework/Test/Util/ActionGroupObjectExtractor.php +++ b/src/Magento/FunctionalTestingFramework/Test/Util/ActionGroupObjectExtractor.php @@ -17,6 +17,7 @@ class ActionGroupObjectExtractor extends BaseObjectExtractor { const DEFAULT_VALUE = 'defaultValue'; const ACTION_GROUP_ARGUMENTS = 'arguments'; + const FILENAME = 'filename'; /** * Action Object Extractor for converting actions into objects @@ -47,9 +48,11 @@ public function extractActionGroup($actionGroupData) $actionGroupData, self::NODE_NAME, self::ACTION_GROUP_ARGUMENTS, - self::NAME + self::NAME, + self::FILENAME ); + // TODO filename is now available to the ActionGroupObject, integrate this into debug and error statements $actions = $this->actionObjectExtractor->extractActions($actionData); if (array_key_exists(self::ACTION_GROUP_ARGUMENTS, $actionGroupData)) { diff --git a/src/Magento/FunctionalTestingFramework/Test/Util/ActionObjectExtractor.php b/src/Magento/FunctionalTestingFramework/Test/Util/ActionObjectExtractor.php index 7f50e0b3c..a3ce58a5e 100644 --- a/src/Magento/FunctionalTestingFramework/Test/Util/ActionObjectExtractor.php +++ b/src/Magento/FunctionalTestingFramework/Test/Util/ActionObjectExtractor.php @@ -6,6 +6,9 @@ namespace Magento\FunctionalTestingFramework\Test\Util; +use Magento\FunctionalTestingFramework\Config\MftfApplicationConfig; +use Magento\FunctionalTestingFramework\Exceptions\TestFrameworkException; +use Magento\FunctionalTestingFramework\Exceptions\TestReferenceException; use Magento\FunctionalTestingFramework\Exceptions\XmlException; use Magento\FunctionalTestingFramework\Test\Objects\ActionObject; @@ -42,12 +45,14 @@ public function __construct() * irrelevant tags and returned as an array of ActionObjects. * * @param array $testActions + * @param string $testName * @return array * @throws XmlException */ - public function extractActions($testActions) + public function extractActions($testActions, $testName = null) { $actions = []; + $stepKeyRefs = []; foreach ($testActions as $actionName => $actionData) { $stepKey = $actionData[self::TEST_STEP_MERGE_KEY]; @@ -61,8 +66,6 @@ public function extractActions($testActions) self::TEST_STEP_MERGE_KEY, self::NODE_NAME ); - $linkedAction = null; - $order = null; // Flatten AssertSorted "array" element to parameterArray if (isset($actionData["array"])) { @@ -73,22 +76,14 @@ public function extractActions($testActions) $actionAttributes = $this->processActionGroupArgs($actionAttributes); } - if (array_key_exists(self::TEST_ACTION_BEFORE, $actionData) - and array_key_exists(self::TEST_ACTION_AFTER, $actionData)) { - throw new XmlException(sprintf(self::BEFORE_AFTER_ERROR_MSG, $actionName)); - } - - if (array_key_exists(self::TEST_ACTION_BEFORE, $actionData)) { - $linkedAction = $actionData[self::TEST_ACTION_BEFORE]; - $order = self::TEST_ACTION_BEFORE; - } elseif (array_key_exists(self::TEST_ACTION_AFTER, $actionData)) { - $linkedAction = $actionData[self::TEST_ACTION_AFTER]; - $order = self::TEST_ACTION_AFTER; - } - + $linkedAction = $this->processLinkedActions($actionName, $actionData); $actions = $this->extractFieldActions($actionData, $actions); $actionAttributes = $this->extractFieldReferences($actionData, $actionAttributes); + if ($linkedAction['stepKey'] != null) { + $stepKeyRefs[$linkedAction['stepKey']][] = $stepKey; + } + // TODO this is to be implemented later. Currently the schema does not use or need return var. /*if (array_key_exists(ActionGroupObjectHandler::TEST_ACTION_RETURN_VARIABLE, $actionData)) { $returnVariable = $actionData[ActionGroupObjectHandler::TEST_ACTION_RETURN_VARIABLE]; @@ -98,14 +93,44 @@ public function extractActions($testActions) $stepKey, $actionData[self::NODE_NAME], $actionAttributes, - $linkedAction, - $order + $linkedAction['stepKey'], + $linkedAction['order'] ); } + $this->auditMergeSteps($stepKeyRefs, $testName); + return $actions; } + /** + * Function which processes any actions which have an explicit reference to an additional step for merging purposes. + * Returns an array with keys corresponding to the linked action's stepKey and order. + * + * @param string $actionName + * @param array $actionData + * @return array + * @throws XmlException + */ + private function processLinkedActions($actionName, $actionData) + { + $linkedAction =['stepKey' => null, 'order' => null]; + if (array_key_exists(self::TEST_ACTION_BEFORE, $actionData) + and array_key_exists(self::TEST_ACTION_AFTER, $actionData)) { + throw new XmlException(sprintf(self::BEFORE_AFTER_ERROR_MSG, $actionName)); + } + + if (array_key_exists(self::TEST_ACTION_BEFORE, $actionData)) { + $linkedAction['stepKey'] = $actionData[self::TEST_ACTION_BEFORE]; + $linkedAction['order'] = self::TEST_ACTION_BEFORE; + } elseif (array_key_exists(self::TEST_ACTION_AFTER, $actionData)) { + $linkedAction['stepKey'] = $actionData[self::TEST_ACTION_AFTER]; + $linkedAction['order'] = self::TEST_ACTION_AFTER; + } + + return $linkedAction; + } + /** * 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. @@ -178,7 +203,7 @@ private function extractFieldReferences($actionData, $actionAttributes) return $actionAttributes; } - $attributes[self::ACTION_OBJECT_PERSISTENCE_FIELDS][self::NODE_NAME] = 'fields'; + $attributes = []; foreach ($actionAttributes as $attributeName => $attributeValue) { if (!is_array($attributeValue) || $attributeValue[self::NODE_NAME] != self::DATA_PERSISTENCE_CUSTOM_FIELD) { $attributes[$attributeName] = $attributeValue; @@ -188,6 +213,56 @@ private function extractFieldReferences($actionData, $actionAttributes) $attributes[self::ACTION_OBJECT_PERSISTENCE_FIELDS][] = $attributeName; } + if (array_key_exists(self::ACTION_OBJECT_PERSISTENCE_FIELDS, $attributes)) { + $attributes[self::ACTION_OBJECT_PERSISTENCE_FIELDS][self::NODE_NAME] = 'fields'; + } + return $attributes; } + + /** + * Function which validates stepKey references within mergeable actions + * + * @param array $stepKeyRefs + * @param string $testName + * @return void + * @throws TestReferenceException + */ + private function auditMergeSteps($stepKeyRefs, $testName) + { + if (empty($stepKeyRefs)) { + return; + } + + // check for step keys which are referencing themselves as before/after + $invalidStepRef = array_filter($stepKeyRefs, function ($value, $key) { + return in_array($key, $value); + }, 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); + } + + // check for ambiguous references to step keys (multiple refs across test merges). + $atRiskStepRef = array_filter($stepKeyRefs, function ($value) { + 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; + } + } } diff --git a/src/Magento/FunctionalTestingFramework/Test/Util/TestNameValidationUtil.php b/src/Magento/FunctionalTestingFramework/Test/Util/TestNameValidationUtil.php deleted file mode 100644 index 61124a3b3..000000000 --- a/src/Magento/FunctionalTestingFramework/Test/Util/TestNameValidationUtil.php +++ /dev/null @@ -1,61 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ - -namespace Magento\FunctionalTestingFramework\Test\Util; - -use Magento\FunctionalTestingFramework\Exceptions\XmlException; - -class TestNameValidationUtil -{ - const BLACKLISTED_CHAR = [ - " " => "spaces", - "," => "commas", - "'" => "single quotes", - "\"" => "double quotes", - "{" => "curly braces", - "}" => "curly braces", - "$" => "dollar signs", - "(" => "parenthesis", - ")" => "parenthesis" - ]; - - /** - * Function which runs a validation against the blacklisted char defined in this class. Validation occurs to insure - * allure report does not error/future devOps builds do not error against illegal char. - * - * @param string $testName - * @return void - * @throws XmlException - */ - public static function validateName($testName) - { - $testChars = str_split($testName); - - $diff = array_intersect($testChars, array_keys(self::BLACKLISTED_CHAR)); - if (count($diff) > 0) { - $errorMessage = "Test name \"${testName}\" contains illegal characters, please fix and re-run."; - $uniqueDiff = array_unique(array_map(['self', 'nameMapper'], $diff)); - - foreach ($uniqueDiff as $diffChar) { - $errorMessage .= "\nTest names cannot contain " . $diffChar; - } - - throw new XmlException($errorMessage); - } - } - - /** - * Function which maps the blacklisted char to its name, function is used by the array map above. - * - * @param string $val - * @return string - * @SuppressWarnings(PHPMD.UnusedPrivateMethod) - */ - private static function nameMapper($val) - { - return self::BLACKLISTED_CHAR[$val]; - } -} diff --git a/src/Magento/FunctionalTestingFramework/Test/Util/TestObjectExtractor.php b/src/Magento/FunctionalTestingFramework/Test/Util/TestObjectExtractor.php index d9044a0a1..be4e3c97c 100644 --- a/src/Magento/FunctionalTestingFramework/Test/Util/TestObjectExtractor.php +++ b/src/Magento/FunctionalTestingFramework/Test/Util/TestObjectExtractor.php @@ -6,8 +6,11 @@ namespace Magento\FunctionalTestingFramework\Test\Util; +use Magento\FunctionalTestingFramework\Exceptions\TestFrameworkException; use Magento\FunctionalTestingFramework\Exceptions\XmlException; +use Magento\FunctionalTestingFramework\Test\Objects\ActionObject; use Magento\FunctionalTestingFramework\Test\Objects\TestObject; +use Magento\FunctionalTestingFramework\Util\Validation\NameValidationUtil; /** * Class TestObjectExtractor @@ -61,7 +64,7 @@ public function __construct() public function extractTestData($testData) { // validate the test name for blacklisted char (will cause allure report issues) MQE-483 - TestNameValidationUtil::validateName($testData[self::NAME]); + NameValidationUtil::validateName($testData[self::NAME], "Test"); $testAnnotations = []; $testHooks = []; @@ -108,7 +111,7 @@ public function extractTestData($testData) try { return new TestObject( $testData[self::NAME], - $this->actionObjectExtractor->extractActions($testActions), + $this->actionObjectExtractor->extractActions($testActions, $testData[self::NAME]), $testAnnotations, $testHooks, $filename diff --git a/src/Magento/FunctionalTestingFramework/Test/etc/Actions/dataActions.xsd b/src/Magento/FunctionalTestingFramework/Test/etc/Actions/dataActions.xsd index ad8ea9414..1a7ea2d78 100644 --- a/src/Magento/FunctionalTestingFramework/Test/etc/Actions/dataActions.xsd +++ b/src/Magento/FunctionalTestingFramework/Test/etc/Actions/dataActions.xsd @@ -67,7 +67,6 @@ <xs:attribute ref="entity" use="required"/> <xs:attribute ref="createDataKey" use="required"/> <xs:attributeGroup ref="commonActionAttributes"/> - <xs:attribute ref="storeCode"/> </xs:complexType> <xs:complexType name="deleteDataType"> @@ -76,9 +75,9 @@ </xs:annotation> <xs:simpleContent> <xs:extension base="xs:string"> - <xs:attribute ref="createDataKey" use="required"/> + <xs:attribute ref="url"/> + <xs:attribute ref="createDataKey"/> <xs:attributeGroup ref="commonActionAttributes"/> - <xs:attribute ref="storeCode"/> </xs:extension> </xs:simpleContent> </xs:complexType> diff --git a/src/Magento/FunctionalTestingFramework/Test/etc/actionGroupSchema.xsd b/src/Magento/FunctionalTestingFramework/Test/etc/actionGroupSchema.xsd index 5c5fc0059..74badd40c 100644 --- a/src/Magento/FunctionalTestingFramework/Test/etc/actionGroupSchema.xsd +++ b/src/Magento/FunctionalTestingFramework/Test/etc/actionGroupSchema.xsd @@ -32,6 +32,7 @@ </xs:element> </xs:choice> <xs:attribute type="xs:string" name="name" use="required"/> + <xs:attribute type="xs:string" name="filename"/> </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 ebb2e3570..9aa772858 100644 --- a/src/Magento/FunctionalTestingFramework/Test/etc/actionTypeTags.xsd +++ b/src/Magento/FunctionalTestingFramework/Test/etc/actionTypeTags.xsd @@ -269,7 +269,22 @@ <xs:documentation> End point selector for drag-and-drop. </xs:documentation> - </xs:annotation></xs:attribute> + </xs:annotation> + </xs:attribute> + <xs:attribute type="xs:string" name="x"> + <xs:annotation> + <xs:documentation> + X offset for drag-and-drop destination. + </xs:documentation> + </xs:annotation> + </xs:attribute> + <xs:attribute type="xs:string" name="y"> + <xs:annotation> + <xs:documentation> + Y offset for drag-and-drop destination. + </xs:documentation> + </xs:annotation> + </xs:attribute> <xs:attributeGroup ref="commonActionAttributes"/> </xs:extension> </xs:simpleContent> diff --git a/src/Magento/FunctionalTestingFramework/Util/ConfigSanitizerUtil.php b/src/Magento/FunctionalTestingFramework/Util/ConfigSanitizerUtil.php index c6244ca82..802db4127 100644 --- a/src/Magento/FunctionalTestingFramework/Util/ConfigSanitizerUtil.php +++ b/src/Magento/FunctionalTestingFramework/Util/ConfigSanitizerUtil.php @@ -6,6 +6,8 @@ namespace Magento\FunctionalTestingFramework\Util; +use Magento\FunctionalTestingFramework\Config\MftfApplicationConfig; + /** * Class ConfigSanitizerUtil */ @@ -86,8 +88,7 @@ private static function validateConfigBasedVars($config) */ public static function sanitizeUrl($url) { - $forceGenerate = $GLOBALS['FORCE_PHP_GENERATE'] ?? false; - if (strlen($url) == 0 && $forceGenerate === false) { + if (strlen($url) == 0 && !MftfApplicationConfig::getConfig()->forceGenerateEnabled()) { trigger_error("MAGENTO_BASE_URL must be defined in .env", E_USER_ERROR); } diff --git a/src/Magento/FunctionalTestingFramework/Util/Env/EnvProcessor.php b/src/Magento/FunctionalTestingFramework/Util/Env/EnvProcessor.php new file mode 100644 index 000000000..7129ff091 --- /dev/null +++ b/src/Magento/FunctionalTestingFramework/Util/Env/EnvProcessor.php @@ -0,0 +1,99 @@ +<?php +// @codingStandardsIgnoreFile +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types = 1); + +namespace Magento\FunctionalTestingFramework\Util\Env; + +/** + * Helper class EnvProcessor for reading and writing .env files. + * + * @package Magento\FunctionalTestingFramework\Util\Env + */ +class EnvProcessor +{ + /** + * File .env location. + * + * @var string + */ + private $envFile = ''; + + /** + * File .env.example location. + * + * @var string + */ + private $envExampleFile = ''; + + /** + * Array of environment variables form file. + * + * @var array + */ + private $env = []; + + /** + * EnvProcessor constructor. + * @param string $envFile + */ + public function __construct( + string $envFile = '' + ) { + $this->envFile = $envFile; + $this->envExampleFile = $envFile . '.example'; + } + + /** + * Serves for parsing '.env.example' file into associative array. + * + * @return array + */ + public function parseEnvFile(): array + { + $envLines = file( + $this->envExampleFile, + FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES + ); + $env = []; + foreach ($envLines as $line) { + // do not use commented out lines + if (strpos($line, '#') !== 0) { + list($key, $value) = explode('=', $line); + $env[$key] = $value; + } + } + return $env; + } + + /** + * Serves for putting array with environment variables into .env file. + * + * @param array $config + * @return void + */ + public function putEnvFile(array $config = []) + { + $envData = ''; + foreach ($config as $key => $value) { + $envData .= $key . '=' . $value . PHP_EOL; + } + file_put_contents($this->envFile, $envData); + } + + /** + * Retrieves '.env.example' file as associative array. + * + * @return array + */ + public function getEnv(): array + { + if (empty($this->env)) { + $this->env = $this->parseEnvFile(); + } + return $this->env; + } +} diff --git a/src/Magento/FunctionalTestingFramework/Util/Filesystem/DirSetupUtil.php b/src/Magento/FunctionalTestingFramework/Util/Filesystem/DirSetupUtil.php index 32c03966e..4207a2c8c 100644 --- a/src/Magento/FunctionalTestingFramework/Util/Filesystem/DirSetupUtil.php +++ b/src/Magento/FunctionalTestingFramework/Util/Filesystem/DirSetupUtil.php @@ -11,6 +11,13 @@ class DirSetupUtil { + /** + * Array which will track any previously cleared directories, to prevent any unintended removal. + * + * @var array + */ + private static $DIR_CONTEXT = []; + /** * Method used to clean export dir if needed and create new empty export dir. * @@ -19,11 +26,17 @@ class DirSetupUtil */ public static function createGroupDir($fullPath) { + // make sure we haven't already cleaned up this directory at any point before deletion + if (in_array($fullPath, self::$DIR_CONTEXT)) { + return; + } + if (file_exists($fullPath)) { self::rmDirRecursive($fullPath); } mkdir($fullPath, 0777, true); + self::$DIR_CONTEXT[] = $fullPath; } /** diff --git a/src/Magento/FunctionalTestingFramework/Util/Manifest/BaseTestManifest.php b/src/Magento/FunctionalTestingFramework/Util/Manifest/BaseTestManifest.php index f31271a8d..3c3eb6de9 100644 --- a/src/Magento/FunctionalTestingFramework/Util/Manifest/BaseTestManifest.php +++ b/src/Magento/FunctionalTestingFramework/Util/Manifest/BaseTestManifest.php @@ -6,6 +6,8 @@ namespace Magento\FunctionalTestingFramework\Util\Manifest; +use Magento\FunctionalTestingFramework\Suite\Handlers\SuiteObjectHandler; +use Magento\FunctionalTestingFramework\Suite\Objects\SuiteObject; use Magento\FunctionalTestingFramework\Test\Objects\TestObject; abstract class BaseTestManifest @@ -24,16 +26,25 @@ abstract class BaseTestManifest */ protected $relativeDirPath; + /** + * Suite configuration in the format suite name to test name. Overwritten during a custom configuration. + * + * @var array + */ + protected $suiteConfiguration; + /** * TestManifest constructor. * * @param string $path * @param string $runConfig + * @param array $suiteConfiguration */ - public function __construct($path, $runConfig) + public function __construct($path, $runConfig, $suiteConfiguration) { $this->runTypeConfig = $runConfig; $this->relativeDirPath = substr($path, strlen(dirname(dirname(TESTS_BP))) + 1); + $this->suiteConfiguration = $suiteConfiguration; } /** @@ -57,8 +68,41 @@ abstract public function addTest($testObject); /** * Function which generates the actual manifest(s) once the relevant tests have been added to the array. * - * @param int|null $nodes * @return void */ - abstract public function generate($nodes = null); + abstract public function generate(); + + /** + * Getter for the suite configuration. + * + * @return array + */ + public function getSuiteConfig() + { + if ($this->suiteConfiguration === null) { + return []; + } + + $suiteToTestNames = []; + if (empty($this->suiteConfiguration)) { + // if there is no configuration passed we can assume the user wants all suites generated as specified. + foreach (SuiteObjectHandler::getInstance()->getAllObjects() as $suite => $suiteObj) { + /** @var SuiteObject $suitObj */ + $suiteToTestNames[$suite] = array_keys($suiteObj->getTests()); + } + } else { + // we need to loop through the configuration to make sure we capture suites with no specific config + foreach ($this->suiteConfiguration as $suiteName => $test) { + if (empty($test)) { + $suiteToTestNames[$suiteName] = + array_keys(SuiteObjectHandler::getInstance()->getObject($suiteName)->getTests()); + continue; + } + + $suiteToTestNames[$suiteName] = $test; + } + } + + return $suiteToTestNames; + } } diff --git a/src/Magento/FunctionalTestingFramework/Util/Manifest/DefaultTestManifest.php b/src/Magento/FunctionalTestingFramework/Util/Manifest/DefaultTestManifest.php index 20173d508..f6af0fe88 100644 --- a/src/Magento/FunctionalTestingFramework/Util/Manifest/DefaultTestManifest.php +++ b/src/Magento/FunctionalTestingFramework/Util/Manifest/DefaultTestManifest.php @@ -7,6 +7,7 @@ namespace Magento\FunctionalTestingFramework\Util\Manifest; use Magento\FunctionalTestingFramework\Test\Objects\TestObject; +use Magento\FunctionalTestingFramework\Util\Filesystem\DirSetupUtil; class DefaultTestManifest extends BaseTestManifest { @@ -19,6 +20,13 @@ class DefaultTestManifest extends BaseTestManifest */ protected $manifestPath; + /** + * A static array to track which test manifests have been cleared to prevent overwriting during generation. + * + * @var array + */ + private static $CLEARED_MANIFESTS = []; + /** * An array containing all test names for output. * @@ -28,15 +36,15 @@ class DefaultTestManifest extends BaseTestManifest /** * DefaultTestManifest constructor. - * @param string $path + * + * @param array $suiteConfiguration + * @param string $testPath */ - public function __construct($path) + public function __construct($suiteConfiguration, $testPath) { - $this->manifestPath = $path . DIRECTORY_SEPARATOR . 'testManifest.txt'; - parent::__construct($path, self::DEFAULT_CONFIG); - - $fileResource = fopen($this->manifestPath, 'w'); - fclose($fileResource); + $this->manifestPath = dirname($testPath) . DIRECTORY_SEPARATOR . 'testManifest.txt'; + $this->cleanManifest($this->manifestPath); + parent::__construct($testPath, self::DEFAULT_CONFIG, $suiteConfiguration); } /** @@ -53,10 +61,9 @@ public function addTest($testObject) /** * Function which outputs a list of all test files to the defined testManifest.txt file. * - * @param int|null $nodes * @return void */ - public function generate($nodes = null) + public function generate() { $fileResource = fopen($this->manifestPath, 'a'); @@ -65,6 +72,44 @@ public function generate($nodes = null) fwrite($fileResource, $line . PHP_EOL); } + $this->generateSuiteEntries($fileResource); + fclose($fileResource); } + + /** + * Function which takes the test suites passed to the manifest and generates corresponding entries in the manifest. + * + * @param resource $fileResource + * @return void + */ + protected function generateSuiteEntries($fileResource) + { + foreach ($this->getSuiteConfig() as $suiteName => $tests) { + $line = "-g {$suiteName}"; + fwrite($fileResource, $line . PHP_EOL); + } + } + + /** + * Function which checks the path for an existing test manifest and clears if the file has not already been cleared + * during current runtime. + * + * @param string $path + * @return void + */ + private function cleanManifest($path) + { + // if we have already cleared the file then simply return + if (in_array($path, self::$CLEARED_MANIFESTS)) { + return; + } + + // if the file exists remove + if (file_exists($path)) { + unlink($path); + } + + self::$CLEARED_MANIFESTS[] = $path; + } } diff --git a/src/Magento/FunctionalTestingFramework/Util/Manifest/ParallelTestManifest.php b/src/Magento/FunctionalTestingFramework/Util/Manifest/ParallelTestManifest.php index d9ec813ac..12ce5d701 100644 --- a/src/Magento/FunctionalTestingFramework/Util/Manifest/ParallelTestManifest.php +++ b/src/Magento/FunctionalTestingFramework/Util/Manifest/ParallelTestManifest.php @@ -6,9 +6,16 @@ namespace Magento\FunctionalTestingFramework\Util\Manifest; +use Codeception\Suite; use Magento\Framework\Exception\RuntimeException; +use Magento\FunctionalTestingFramework\Suite\Handlers\SuiteObjectHandler; +use Magento\FunctionalTestingFramework\Suite\Objects\SuiteObject; +use Magento\FunctionalTestingFramework\Test\Handlers\TestObjectHandler; use Magento\FunctionalTestingFramework\Test\Objects\TestObject; use Magento\FunctionalTestingFramework\Util\Filesystem\DirSetupUtil; +use Magento\FunctionalTestingFramework\Util\Sorter\ParallelGroupSorter; +use RecursiveArrayIterator; +use RecursiveIteratorIterator; class ParallelTestManifest extends BaseTestManifest { @@ -21,6 +28,20 @@ class ParallelTestManifest extends BaseTestManifest */ private $testNameToSize = []; + /** + * Class variable to store resulting group config. + * + * @var array + */ + private $testGroups; + + /** + * An instance of the group sorter which will take suites and tests organizing them to be run together. + * + * @var ParallelGroupSorter + */ + private $parallelGroupSorter; + /** * Path to the directory that will contain all test group files * @@ -31,12 +52,14 @@ class ParallelTestManifest extends BaseTestManifest /** * TestManifest constructor. * - * @param string $path + * @param array $suiteConfiguration + * @param string $testPath */ - public function __construct($path) + public function __construct($suiteConfiguration, $testPath) { - $this->dirPath = $path . DIRECTORY_SEPARATOR . 'groups'; - parent::__construct($path, self::PARALLEL_CONFIG); + $this->dirPath = dirname($testPath) . DIRECTORY_SEPARATOR . 'groups'; + $this->parallelGroupSorter = new ParallelGroupSorter(); + parent::__construct($testPath, self::PARALLEL_CONFIG, $suiteConfiguration); } /** @@ -50,46 +73,95 @@ public function addTest($testObject) $this->testNameToSize[$testObject->getCodeceptionName()] = $testObject->getTestActionCount(); } + /** + * Function which generates test groups based on arg passed. The function builds groups using the args as an upper + * limit. + * + * @param int $lines + * @return void + */ + public function createTestGroups($lines) + { + $this->testGroups = $this->parallelGroupSorter->getTestsGroupedBySize( + $this->getSuiteConfig(), + $this->testNameToSize, + $lines + ); + + $this->suiteConfiguration = $this->parallelGroupSorter->getResultingSuiteConfig(); + } + /** * Function which generates the actual manifest once the relevant tests have been added to the array. * - * @param int $nodes * @return void */ - public function generate($nodes = null) + public function generate() { - if ($nodes == null) { - $nodes = 2; + DirSetupUtil::createGroupDir($this->dirPath); + $suites = $this->getFlattenedSuiteConfiguration($this->suiteConfiguration ?? []); + + foreach ($this->testGroups as $groupNumber => $groupContents) { + $this->generateGroupFile($groupContents, $groupNumber, $suites); } + } - DirSetupUtil::createGroupDir($this->dirPath); - arsort($this->testNameToSize); - $node = $nodes; - - foreach (array_keys($this->testNameToSize) as $testName) { - $node = $this->getNodeOrder($node, $nodes); - $nodeString = strval($node); - $fileResource = fopen($this->dirPath . DIRECTORY_SEPARATOR . "group{$nodeString}.txt", 'a'); - $line = $this->relativeDirPath . DIRECTORY_SEPARATOR . $testName . '.php'; + /** + * Function which simply returns the private sorter used by the manifest. + * + * @return ParallelGroupSorter + */ + public function getSorter() + { + return $this->parallelGroupSorter; + } + + /** + * Function which takes an array containing entries representing the test execution as well as the associated group + * 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 + * @return void + */ + private function generateGroupFile($testGroup, $nodeNumber, $suites) + { + foreach ($testGroup as $entryName => $testValue) { + $fileResource = fopen($this->dirPath . DIRECTORY_SEPARATOR . "group{$nodeNumber}.txt", 'a'); + + $line = null; + if (array_key_exists($entryName, $suites)) { + $line = "-g {$entryName}"; + } else { + $line = $this->relativeDirPath . DIRECTORY_SEPARATOR . $entryName . '.php'; + } fwrite($fileResource, $line . PHP_EOL); fclose($fileResource); } } /** - * Function which independently iterates node position based on number of desired nodes. + * Function which recusrively parses a given potentially multidimensional array of suites containing their split + * groups. The result is a flattened array of suite names to relevant tests for generation of the manifest. * - * @param int $currentNode - * @param int $nodes - * @return int + * @param array $multiDimensionalSuites + * @return array */ - private function getNodeOrder($currentNode, $nodes) + private function getFlattenedSuiteConfiguration($multiDimensionalSuites) { - $adjustedRef = $currentNode + 1; - if ($adjustedRef <= $nodes) { - return $currentNode + 1; + $suites = []; + foreach ($multiDimensionalSuites as $suiteName => $suiteContent) { + $value = array_values($suiteContent)[0]; + if (is_array($value)) { + $suites = array_merge($suites, $this->getFlattenedSuiteConfiguration($suiteContent)); + continue; + } + + $suites[$suiteName] = $suiteContent; } - return 1; + return $suites; } } diff --git a/src/Magento/FunctionalTestingFramework/Util/Manifest/SingleRunTestManifest.php b/src/Magento/FunctionalTestingFramework/Util/Manifest/SingleRunTestManifest.php index ace4e43f7..94004e3be 100644 --- a/src/Magento/FunctionalTestingFramework/Util/Manifest/SingleRunTestManifest.php +++ b/src/Magento/FunctionalTestingFramework/Util/Manifest/SingleRunTestManifest.php @@ -12,28 +12,27 @@ class SingleRunTestManifest extends DefaultTestManifest /** * SingleRunTestManifest constructor. - * @param string $path + * + * @param array $suiteConfiguration + * @param string $testPath */ - public function __construct($path) + public function __construct($suiteConfiguration, $testPath) { - $this->manifestPath = $path . DIRECTORY_SEPARATOR . 'testManifest.txt'; - parent::__construct($path, self::SINGLE_RUN_CONFIG); - - $fileResource = fopen($this->manifestPath, 'w'); - fclose($fileResource); + parent::__construct($suiteConfiguration, $testPath); + $this->runTypeConfig = self::SINGLE_RUN_CONFIG; } /** * Function which generates the actual manifest once the relevant tests have been added to the array. * - * @param int|null $nodes * @return void */ - public function generate($nodes = null) + public function generate() { $fileResource = fopen($this->manifestPath, 'a'); $line = $this->relativeDirPath . DIRECTORY_SEPARATOR; fwrite($fileResource, $line . PHP_EOL); + $this->generateSuiteEntries($fileResource); fclose($fileResource); } } diff --git a/src/Magento/FunctionalTestingFramework/Util/Manifest/TestManifestFactory.php b/src/Magento/FunctionalTestingFramework/Util/Manifest/TestManifestFactory.php index e4bac25de..51b34bc80 100644 --- a/src/Magento/FunctionalTestingFramework/Util/Manifest/TestManifestFactory.php +++ b/src/Magento/FunctionalTestingFramework/Util/Manifest/TestManifestFactory.php @@ -6,6 +6,9 @@ namespace Magento\FunctionalTestingFramework\Util\Manifest; +use Magento\FunctionalTestingFramework\Test\Handlers\TestObjectHandler; +use Magento\FunctionalTestingFramework\Util\TestGenerator; + class TestManifestFactory { /** @@ -19,21 +22,28 @@ private function __construct() /** * Static function which takes path and config to return the appropriate manifest output type. * - * @param String $path * @param String $runConfig + * @param array $suiteConfiguration + * @param String $testPath * @return BaseTestManifest */ - public static function makeManifest($path, $runConfig) + public static function makeManifest($runConfig, $suiteConfiguration, $testPath = TestGenerator::DEFAULT_DIR) { + $testDirFullPath = TESTS_MODULE_PATH + . DIRECTORY_SEPARATOR + . TestGenerator::GENERATED_DIR + . DIRECTORY_SEPARATOR + . $testPath; + switch ($runConfig) { case 'singleRun': - return new SingleRunTestManifest($path); + return new SingleRunTestManifest($suiteConfiguration, $testDirFullPath); case 'parallel': - return new ParallelTestManifest($path); + return new ParallelTestManifest($suiteConfiguration, $testDirFullPath); default: - return new DefaultTestManifest($path); + return new DefaultTestManifest($suiteConfiguration, $testDirFullPath); } } diff --git a/src/Magento/FunctionalTestingFramework/Util/ModuleResolver.php b/src/Magento/FunctionalTestingFramework/Util/ModuleResolver.php index 060366f9a..f3a6eb6e6 100644 --- a/src/Magento/FunctionalTestingFramework/Util/ModuleResolver.php +++ b/src/Magento/FunctionalTestingFramework/Util/ModuleResolver.php @@ -6,6 +6,8 @@ namespace Magento\FunctionalTestingFramework\Util; +use Magento\FunctionalTestingFramework\Config\MftfApplicationConfig; + /** * Class ModuleResolver, resolve module path based on enabled modules of target Magento instance. * @@ -126,13 +128,11 @@ private function __construct() */ public function getEnabledModules() { - $testGenerationPhase = $GLOBALS['GENERATE_TESTS'] ?? false; - if (isset($this->enabledModules)) { return $this->enabledModules; } - if ($testGenerationPhase) { + if (MftfApplicationConfig::getConfig()->getPhase() == MftfApplicationConfig::GENERATION_PHASE) { $this->printMagentoVersionInfo(); } @@ -190,9 +190,7 @@ public function getModulesPath() } $enabledModules = $this->getEnabledModules(); - $forceGeneration = $GLOBALS['FORCE_PHP_GENERATE'] ?? false; - - if (empty($enabledModules) && !$forceGeneration) { + 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", @@ -223,9 +221,8 @@ private function aggregateTestModulePaths() { $allModulePaths = []; - // TODO update these paths when we switch a composer based pathing // Define the Module paths from app/code - $appCodePath = dirname(dirname(dirname(PROJECT_ROOT))) + $appCodePath = MAGENTO_BP . DIRECTORY_SEPARATOR . 'app' . DIRECTORY_SEPARATOR . 'code' . DIRECTORY_SEPARATOR; @@ -234,14 +231,14 @@ private function aggregateTestModulePaths() $modulePath = defined('TESTS_MODULE_PATH') ? TESTS_MODULE_PATH : TESTS_BP; // Define the Module paths from vendor modules - $vendorCodePath = dirname(dirname(dirname(PROJECT_ROOT))) + $vendorCodePath = PROJECT_ROOT . DIRECTORY_SEPARATOR . 'vendor' . DIRECTORY_SEPARATOR; $codePathsToPattern = [ - $appCodePath => '/Test/Acceptance', $modulePath => '', - $vendorCodePath => '/Test/Acceptance' + $appCodePath => '/Test/Mftf', + $vendorCodePath => '/Test/Mftf' ]; foreach ($codePathsToPattern as $codePath => $pattern) { @@ -322,8 +319,13 @@ 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}"; + print "Fetching version information from {$url}\n"; + $ch = curl_init($url); curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "GET"); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); @@ -333,7 +335,9 @@ private function printMagentoVersionInfo() $response = "No version information available."; } - print "\nVersion Information: {$response}\n"; + if (MftfApplicationConfig::getConfig()->verboseEnabled()) { + print "\nVersion Information: {$response}\n"; + } } /** diff --git a/src/Magento/FunctionalTestingFramework/Util/Sorter/ParallelGroupSorter.php b/src/Magento/FunctionalTestingFramework/Util/Sorter/ParallelGroupSorter.php new file mode 100644 index 000000000..555eb5244 --- /dev/null +++ b/src/Magento/FunctionalTestingFramework/Util/Sorter/ParallelGroupSorter.php @@ -0,0 +1,272 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\FunctionalTestingFramework\Util\Sorter; + +use Magento\FunctionalTestingFramework\Exceptions\TestFrameworkException; +use Magento\FunctionalTestingFramework\Test\Handlers\TestObjectHandler; + +class ParallelGroupSorter +{ + /** + * An array of newly split suite object names mapped to their corresponding objects. + * + * @var array + */ + private $suiteConfig = []; + + /** + * ParallelGroupSorter constructor. + */ + public function __construct() + { + // empty constructor + } + + /** + * 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 + * @return array + * @throws TestFrameworkException + */ + public function getTestsGroupedBySize($suiteConfiguration, $testNameToSize, $lines) + { + // we must have the lines argument in order to create the test groups + if ($lines == 0) { + throw new TestFrameworkException( + "Please provide the argument '--lines' to the robo command in order to". + " generate grouped tests manifests for a parallel execution" + ); + } + + $testGroups = []; + $splitSuiteNamesToTests = $this->createGroupsWithinSuites($suiteConfiguration, $lines); + $splitSuiteNamesToSize = $this->getSuiteToSize($splitSuiteNamesToTests); + $entriesForGeneration = array_merge($testNameToSize, $splitSuiteNamesToSize); + arsort($entriesForGeneration); + + $testNameToSizeForUse = $entriesForGeneration; + $nodeNumber = 1; + foreach ($entriesForGeneration as $testName => $testSize) { + if (!array_key_exists($testName, $testNameToSizeForUse)) { + // skip tests which have already been added to a group + continue; + } + + $testGroup = $this->createTestGroup($lines, $testName, $testSize, $testNameToSizeForUse); + $testGroups[$nodeNumber] = $testGroup; + + // unset the test which have been used. + $testNameToSizeForUse = array_diff_key($testNameToSizeForUse, $testGroup); + $nodeNumber++; + } + + return $testGroups; + } + + /** + * Function which returns the newly formed suite objects created as a part of the sort + * + * @return array + */ + public function getResultingSuiteConfig() + { + if (empty($this->suiteConfig)) { + return null; + } + + return $this->suiteConfig; + } + + /** + * Function which constructs a group of tests to be run together based on the desired number of lines per group, + * 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 $testSize + * @param array $testNameToSizeForUse + * @return array + */ + private function createTestGroup($lineMaximum, $testName, $testSize, $testNameToSizeForUse) + { + $group[$testName] = $testSize; + + if ($testSize < $lineMaximum) { + while (array_sum($group) < $lineMaximum && !empty($testNameToSizeForUse)) { + $groupSize = array_sum($group); + $lineGoal = $lineMaximum - $groupSize; + + $testNameForUse = $this->getClosestLineCount($testNameToSizeForUse, $lineGoal); + $testSizeForUse = $testNameToSizeForUse[$testNameForUse]; + unset($testNameToSizeForUse[$testNameForUse]); + + $group[$testNameForUse] = $testSizeForUse; + } + } + + return $group; + } + + /** + * 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 integer $desiredValue + * @return string + */ + private function getClosestLineCount($testGroup, $desiredValue) + { + $winner = key($testGroup); + $closestThreshold = $desiredValue; + foreach ($testGroup as $testName => $testValue) { + $testThreshold = abs($desiredValue - $testValue); + if ($closestThreshold > $testThreshold) { + $closestThreshold = $testThreshold; + $winner = $testName; + } + } + + return $winner; + } + + /** + * 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 integer $lineLimit + * @return array + */ + private function createGroupsWithinSuites($suiteConfiguration, $lineLimit) + { + $suiteNameToTestSize = $this->getSuiteNameToTestSize($suiteConfiguration); + $suiteNameToSize = $this->getSuiteToSize($suiteNameToTestSize); + + // divide the suites up within the array + $suitesForResize = array_filter($suiteNameToSize, function ($val) use ($lineLimit) { + return $val > $lineLimit; + }); + + // remove the suites for resize from the original list + $remainingSuites = array_diff_key($suiteNameToTestSize, $suitesForResize); + + foreach ($remainingSuites as $remainingSuite => $tests) { + $this->addSuiteToConfig($remainingSuite, null, $tests); + } + + $resultingGroups = []; + foreach ($suitesForResize as $suiteName => $suiteSize) { + $resultingGroups = array_merge( + $resultingGroups, + $this->splitTestSuite($suiteName, $suiteNameToTestSize[$suiteName], $lineLimit) + ); + } + + // merge the resulting divisions with the appropriately sized suites + return array_merge($remainingSuites, $resultingGroups); + } + + /** + * Function which takes the given suite configuration and returns an array of suite to test size. + * + * @param array $suiteConfiguration + * @return array + */ + private function getSuiteNameToTestSize($suiteConfiguration) + { + $suiteNameToTestSize = []; + foreach ($suiteConfiguration as $suite => $test) { + foreach ($test as $testName) { + $suiteNameToTestSize[$suite][$testName] = TestObjectHandler::getInstance() + ->getObject($testName) + ->getTestActionCount(); + } + } + + return $suiteNameToTestSize; + } + + /** + * Function which takes a multidimensional array containing a suite name mapped to an array of tests names as keys + * with their sizes as values. The function returns an array of suite name to size of the corresponding mapped + * tests. + * + * @param array $suiteNamesToTests + * @return array + */ + private function getSuiteToSize($suiteNamesToTests) + { + $suiteNamesToSize = []; + foreach ($suiteNamesToTests as $name => $tests) { + $size = array_sum($tests); + $suiteNamesToSize[$name] = $size; + } + + return $suiteNamesToSize; + } + + /** + * Function which takes a suite name, an array of tests affiliated with that suite, and a maximum number of lines. + * The function uses the limit to split up the oversized suite and returns an array of suites representative of the + * previously oversized suite. + * + * E.g. + * 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 + * @return array + */ + private function splitTestSuite($suiteName, $tests, $lineLimit) + { + arsort($tests); + $split_suites = []; + $availableTests = $tests; + $split_count = 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); + + $availableTests = array_diff_key($availableTests, $group); + $split_count++; + } + + return $split_suites; + } + + /** + * Function which takes a new suite, the original suite from which it was sourced, and an array of tests now + * associated with thew new suite. The function takes this information and creates a new suite object stored in + * the sorter for later retrieval, copying the pre/post conditions from the original suite. + * + * @param string $originalSuiteName + * @param string $newSuiteName + * @param array $tests + * @return void + */ + private function addSuiteToConfig($originalSuiteName, $newSuiteName, $tests) + { + if ($newSuiteName == null) { + $this->suiteConfig[$originalSuiteName] = array_keys($tests); + return; + } + + $this->suiteConfig[$originalSuiteName][$newSuiteName] = array_keys($tests); + } +} diff --git a/src/Magento/FunctionalTestingFramework/Util/TestGenerator.php b/src/Magento/FunctionalTestingFramework/Util/TestGenerator.php index affbbd36d..211f010c0 100644 --- a/src/Magento/FunctionalTestingFramework/Util/TestGenerator.php +++ b/src/Magento/FunctionalTestingFramework/Util/TestGenerator.php @@ -8,6 +8,7 @@ use Magento\FunctionalTestingFramework\DataGenerator\Objects\EntityDataObject; use Magento\FunctionalTestingFramework\Exceptions\TestReferenceException; +use Magento\FunctionalTestingFramework\Suite\Handlers\SuiteObjectHandler; use Magento\FunctionalTestingFramework\Test\Handlers\ActionGroupObjectHandler; use Magento\FunctionalTestingFramework\Test\Handlers\TestObjectHandler; use Magento\FunctionalTestingFramework\Test\Objects\ActionGroupObject; @@ -23,12 +24,13 @@ /** * Class TestGenerator - * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) + * @SuppressWarnings(PHPMD) */ class TestGenerator { const REQUIRED_ENTITY_REFERENCE = 'createDataKey'; const GENERATED_DIR = '_generated'; + const DEFAULT_DIR = 'default'; /** * Path to the export dir. @@ -51,21 +53,40 @@ class TestGenerator */ private $tests; + /** + * Symfony console output interface. + * + * @var \Symfony\Component\Console\Output\ConsoleOutput + */ + private $consoleOutput; + + /** + * Debug flag. + * + * @var bool + */ + private $debug; + /** * TestGenerator constructor. * * @param string $exportDir * @param array $tests + * @param bool $debug */ - private function __construct($exportDir, $tests) + private function __construct($exportDir, $tests, $debug = false) { // private constructor for factory - $this->exportDirName = $exportDir ?? self::GENERATED_DIR; - $this->exportDirectory = rtrim( - TESTS_MODULE_PATH . DIRECTORY_SEPARATOR . self::GENERATED_DIR . DIRECTORY_SEPARATOR . $exportDir, - DIRECTORY_SEPARATOR - ); + $this->exportDirName = $exportDir ?? self::DEFAULT_DIR; + $exportDir = $exportDir ?? self::DEFAULT_DIR; + $this->exportDirectory = TESTS_MODULE_PATH + . DIRECTORY_SEPARATOR + . self::GENERATED_DIR + . DIRECTORY_SEPARATOR + . $exportDir; $this->tests = $tests; + $this->consoleOutput = new \Symfony\Component\Console\Output\ConsoleOutput(); + $this->debug = $debug; } /** @@ -73,11 +94,12 @@ private function __construct($exportDir, $tests) * * @param string $dir * @param array $tests + * @param bool $debug * @return TestGenerator */ - public static function getInstance($dir = null, $tests = null) + public static function getInstance($dir = null, $tests = [], $debug = false) { - return new TestGenerator($dir, $tests); + return new TestGenerator($dir, $tests, $debug); } /** @@ -91,14 +113,28 @@ public function getExportDir() } /** - * Load all Test files as Objects using the Test Object Handler. + * Load all Test files as Objects using the Test Object Handler, additionally validates test references being loaded + * for validity. * + * @param array $testsToIgnore * @return array */ - private function loadAllTestObjects() + private function loadAllTestObjects($testsToIgnore) { - if ($this->tests === null) { - return TestObjectHandler::getInstance()->getAllObjects(); + if ($this->tests === null || empty($this->tests)) { + $testObjects = TestObjectHandler::getInstance()->getAllObjects(); + return array_diff_key($testObjects, $testsToIgnore); + } + + // If we have a custom configuration, we need to check the tests passed in to insure that we can generate + // 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); } return $this->tests; @@ -130,20 +166,25 @@ private function createCestFile($testPhp, $filename) * Assemble ALL PHP strings using the assembleAllTestPhp function. Loop over and pass each array item * to the createCestFile function. * - * @param string $runConfig - * @param int $nodes + * @param BaseTestManifest $testManifest + * @param array $testsToIgnore * @return void * @throws TestReferenceException * @throws \Exception */ - public function createAllTestFiles($runConfig = null, $nodes = null) + public function createAllTestFiles($testManifest = null, $testsToIgnore = null) { - DirSetupUtil::createGroupDir($this->exportDirectory); + if ($this->tests === null) { + // no-op if the test configuration is null + return; + } - // create our manifest file here - $testManifest = TestManifestFactory::makeManifest($this->exportDirectory, $runConfig); - $testPhpArray = $this->assembleAllTestPhp($testManifest, $nodes); + DirSetupUtil::createGroupDir($this->exportDirectory); + if ($testsToIgnore === null) { + $testsToIgnore = SuiteObjectHandler::getInstance()->getAllTestReferences(); + } + $testPhpArray = $this->assembleAllTestPhp($testManifest, $testsToIgnore); foreach ($testPhpArray as $testPhpFile) { $this->createCestFile($testPhpFile[1], $testPhpFile[0]); } @@ -168,11 +209,11 @@ 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() . " in Test \"" . $testObject->getName() . "\""); } $cestPhp = "<?php\n"; - $cestPhp .= "namespace Magento\AcceptanceTest\\" . $this->exportDirName . "\Backend;\n\n"; + $cestPhp .= "namespace Magento\AcceptanceTest\\_" . $this->exportDirName . "\Backend;\n\n"; $cestPhp .= $usePhp; $cestPhp .= $classAnnotationsPhp; $cestPhp .= sprintf("class %s\n", $className); @@ -188,30 +229,49 @@ private function assembleTestPhp($testObject) * Load ALL Test objects. Loop over and pass each to the assembleTestPhp function. * * @param BaseTestManifest $testManifest - * @param int $nodes + * @param array $testsToIgnore * @return array - * @throws TestReferenceException - * @throws \Exception */ - private function assembleAllTestPhp($testManifest, $nodes) + private function assembleAllTestPhp($testManifest, array $testsToIgnore) { /** @var TestObject[] $testObjects */ - $testObjects = $this->loadAllTestObjects(); + $testObjects = $this->loadAllTestObjects($testsToIgnore); $cestPhpArray = []; foreach ($testObjects as $test) { + $this->debug("<comment>Start creating test: " . $test->getCodeceptionName() . "</comment>"); $php = $this->assembleTestPhp($test); $cestPhpArray[] = [$test->getCodeceptionName(), $php]; - //write to manifest here if config is not single run - $testManifest->addTest($test); - } + $debugInformation = $test->getDebugInformation(); + $this->debug($debugInformation); + $this->debug("<comment>Finish creating test: " . $test->getCodeceptionName() . "</comment>" . PHP_EOL); - $testManifest->generate($nodes); + //write to manifest here if manifest is not null + if ($testManifest != null) { + $testManifest->addTest($test); + } + } return $cestPhpArray; } + /** + * Output information in console when debug flag is enabled. + * + * @param array|string $messages + * @return void + */ + private function debug($messages) + { + if ($this->debug && $messages) { + $messages = (array)$messages; + foreach ($messages as $message) { + $this->consoleOutput->writeln($message); + } + } + } + /** * Creates a PHP string for the necessary Allure and AcceptanceTester use statements. * Since we don't support other dependencies at this time, this function takes no parameter. @@ -269,9 +329,9 @@ private function generateAnnotationsPhp($annotationsObject, $isMethod = false) continue; } if (!$isMethod) { - $annotationsPhp.= $this->generateClassAnnotations($annotationType, $annotationName); + $annotationsPhp .= $this->generateClassAnnotations($annotationType, $annotationName); } else { - $annotationsPhp.= $this->generateMethodAnnotations($annotationType, $annotationName); + $annotationsPhp .= $this->generateMethodAnnotations($annotationType, $annotationName); } } @@ -388,18 +448,18 @@ private function generateClassAnnotations($annotationType, $annotationName) * * @param array $actionObjects * @param array|bool $hookObject + * @param string $actor * @return string * @throws TestReferenceException * @throws \Exception * @SuppressWarnings(PHPMD) */ - private function generateStepsPhp($actionObjects, $hookObject = false) + public function generateStepsPhp($actionObjects, $hookObject = false, $actor = "I") { //TODO: Refactor Method according to PHPMD warnings, remove @SuppressWarnings accordingly. $testSteps = ""; foreach ($actionObjects as $actionObject) { - $actor = "I"; $stepKey = $actionObject->getStepKey(); $customActionAttributes = $actionObject->getCustomActionAttributes(); $attribute = null; @@ -428,6 +488,7 @@ private function generateStepsPhp($actionObjects, $hookObject = false) $visible = null; $command = null; $sortOrder = null; + $storeCode = null; $assertExpected = null; $assertActual = null; @@ -457,6 +518,7 @@ private function generateStepsPhp($actionObjects, $hookObject = false) $input = $this->addUniquenessFunctionCall($customActionAttributes['userInput']); } elseif (isset($customActionAttributes['url'])) { $input = $this->addUniquenessFunctionCall($customActionAttributes['url']); + $url = $this->addUniquenessFunctionCall($customActionAttributes['url']); } elseif (isset($customActionAttributes['expectedValue'])) { //For old Assert backwards Compatibility, remove when deprecating $assertExpected = $this->addUniquenessFunctionCall($customActionAttributes['expectedValue']); @@ -497,10 +559,9 @@ private function generateStepsPhp($actionObjects, $hookObject = false) // validate the param array is in the correct format $this->validateParameterArray($customActionAttributes['parameterArray']); - $parameterArray = "[" . $this->addUniquenessToParamArray( - $customActionAttributes['parameterArray'] - ) - . "]"; + $parameterArray = "["; + $parameterArray .= $this->addUniquenessToParamArray($customActionAttributes['parameterArray']); + $parameterArray .= "]"; } if (isset($customActionAttributes['requiredAction'])) { @@ -536,6 +597,10 @@ private function generateStepsPhp($actionObjects, $hookObject = false) if (isset($customActionAttributes['function'])) { $function = $this->addUniquenessFunctionCall($customActionAttributes['function']); + if (in_array($actionObject->getType(), ActionObject::FUNCTION_CLOSURE_ACTIONS)) { + // Argument must be a closure function, not a string. + $function = trim($function, '"'); + } } if (isset($customActionAttributes['html'])) { @@ -582,6 +647,9 @@ private function generateStepsPhp($actionObjects, $hookObject = false) $visible = $customActionAttributes['visible']; } + if (isset($customActionAttributes['storeCode'])) { + $storeCode = $customActionAttributes['storeCode']; + } switch ($actionObject->getType()) { case "createData": $entity = $customActionAttributes['entity']; @@ -638,8 +706,8 @@ private function generateStepsPhp($actionObjects, $hookObject = false) ); } - if (isset($customActionAttributes['storeCode'])) { - $createEntityFunctionCall .= sprintf("\"%s\");\n", $customActionAttributes['storeCode']); + if ($storeCode) { + $createEntityFunctionCall .= sprintf("\"%s\");\n", $storeCode); } else { $createEntityFunctionCall .= ");\n"; } @@ -660,18 +728,33 @@ private function generateStepsPhp($actionObjects, $hookObject = false) $testSteps .= $createEntityFunctionCall; break; case "deleteData": - $key = $customActionAttributes['createDataKey']; - //Add an informative statement to help the user debug test runs - $testSteps .= sprintf( - "\t\t$%s->amGoingTo(\"delete entity that has the createDataKey: %s\");\n", - $actor, - $key - ); + if (isset($customActionAttributes['createDataKey'])) { + $key = $customActionAttributes['createDataKey']; + //Add an informative statement to help the user debug test runs + $contextSetter = sprintf( + "\t\t$%s->amGoingTo(\"delete entity that has the createDataKey: %s\");\n", + $actor, + $key + ); + $deleteEntityFunctionCall = ""; - if ($hookObject) { - $testSteps .= sprintf("\t\t\$this->%s->deleteEntity();\n", $key); + if ($hookObject) { + $deleteEntityFunctionCall .= sprintf("\t\t\$this->%s->deleteEntity();\n", $key); + } else { + $deleteEntityFunctionCall .= sprintf("\t\t$%s->deleteEntity();\n", $key); + } + + $testSteps .= $contextSetter; + $testSteps .= $deleteEntityFunctionCall; } else { - $testSteps .= sprintf("\t\t$%s->deleteEntity();\n", $key); + $url = $this->resolveEnvReferences([$url])[0]; + $url = $this->resolveTestVariable([$url], null)[0]; + $output = sprintf( + "\t\t$%s->deleteEntityByUrl(%s);\n", + $actor, + $url + ); + $testSteps .= $output; } break; case "updateData": @@ -720,8 +803,8 @@ private function generateStepsPhp($actionObjects, $hookObject = false) ); } - if (isset($customActionAttributes['storeCode'])) { - $updateEntityFunctionCall .= sprintf("\"%s\");\n", $customActionAttributes['storeCode']); + if ($storeCode) { + $updateEntityFunctionCall .= sprintf(", \"%s\");\n", $storeCode); } else { $updateEntityFunctionCall .= ");\n"; } @@ -787,8 +870,8 @@ private function generateStepsPhp($actionObjects, $hookObject = false) $getEntityFunctionCall .= 'null'; } - if (isset($customActionAttributes['storeCode'])) { - $getEntityFunctionCall .= sprintf(", \"%s\");\n", $customActionAttributes['storeCode']); + if ($storeCode) { + $getEntityFunctionCall .= sprintf(", \"%s\");\n", $storeCode); } else { $getEntityFunctionCall .= ");\n"; } @@ -832,7 +915,7 @@ private function generateStepsPhp($actionObjects, $hookObject = false) break; case "switchToNextTab": case "switchToPreviousTab": - $testSteps .= $this->wrapFunctionCall($actor, $actionObject, $this->stripWrappedQuotes($input)); + $testSteps .= $this->wrapFunctionCall($actor, $actionObject, $input); break; case "clickWithLeftButton": case "clickWithRightButton": @@ -897,7 +980,7 @@ private function generateStepsPhp($actionObjects, $hookObject = false) $testSteps .= $this->wrapFunctionCall($actor, $actionObject, $selector, $parameterArray, $button); break; case "dragAndDrop": - $testSteps .= $this->wrapFunctionCall($actor, $actionObject, $selector1, $selector2); + $testSteps .= $this->wrapFunctionCall($actor, $actionObject, $selector1, $selector2, $x, $y); break; case "selectMultipleOptions": $testSteps .= $this->wrapFunctionCall( @@ -913,7 +996,12 @@ private function generateStepsPhp($actionObjects, $hookObject = false) $testSteps .= $this->wrapFunctionCall($actor, $actionObject, $function); break; case "executeJS": - $testSteps .= $this->wrapFunctionCall($actor, $actionObject, $function); + $testSteps .= $this->wrapFunctionCallWithReturnValue( + $stepKey, + $actor, + $actionObject, + $function + ); break; case "performOn": case "waitForElementChange": @@ -1139,12 +1227,10 @@ private function generateStepsPhp($actionObjects, $hookObject = false) break; case "field": $fieldKey = $actionObject->getCustomActionAttributes()['key']; - $argRef= "\t\t\$" . str_replace( - ucfirst($fieldKey), - "", - $stepKey - ) . "Fields['{$fieldKey}'] = ${input};\n"; - $testSteps.= $this->resolveTestVariable($argRef, [$input], $actionObject->getActionOrigin()); + $input = $this->resolveTestVariable([$input], $actionObject->getActionOrigin())[0]; + $argRef = "\t\t\$"; + $argRef .= str_replace(ucfirst($fieldKey), "", $stepKey) . "Fields['{$fieldKey}'] = ${input};\n"; + $testSteps .= $argRef; break; default: $testSteps .= $this->wrapFunctionCall($actor, $actionObject, $selector, $input, $parameter); @@ -1173,19 +1259,16 @@ private function resolveLocatorFunctionInAttribute($attribute) * Resolves replacement of $input$ and $$input$$ in given function, recursing and replacing individual arguments * Also determines if each argument requires any quote replacement. * - * @param string $inputString * @param array $args * @param array $actionOrigin - * @return string + * @return array * @throws \Exception */ - private function resolveTestVariable($inputString, $args, $actionOrigin) + private function resolveTestVariable($args, $actionOrigin) { - $outputString = $inputString; - - //Loop through each argument, replace and then replace - foreach ($args as $arg) { - if ($arg == null) { + $newArgs = []; + foreach ($args as $key => $arg) { + if ($arg === null) { continue; } $outputArg = $arg; @@ -1202,10 +1285,10 @@ private function resolveTestVariable($inputString, $args, $actionOrigin) $outputArg = $this->resolveStepKeyReferences($outputArg, $actionOrigin); - $outputString = str_replace($arg, $outputArg, $outputString); + $newArgs[$key] = $outputArg; } - return $outputString; + return $newArgs; } /** @@ -1366,6 +1449,7 @@ private function stripAndSplitReference($reference, $delimiter) * @return string * @throws TestReferenceException * @throws \Exception + * @SuppressWarnings(PHPMD.CyclomaticComplexity) */ private function generateHooksPhp($hookObjects) { @@ -1407,12 +1491,6 @@ private function generateHooksPhp($hookObjects) throw new TestReferenceException($e->getMessage() . " in Element \"" . $type . "\""); } - if ($hookObject->getType() == TestObjectExtractor::TEST_FAILED_HOOK) { - $steps.="\t\t"; - $steps.='$this->_after($I);'; - $steps.="\n"; - } - $hooks .= sprintf("\tpublic function _{$type}(%s)\n", $dependencies); $hooks .= "\t{\n"; $hooks .= $steps; @@ -1439,10 +1517,15 @@ private function generateTestPhp($test) $testName = str_replace(' ', '', $testName); $testAnnotations = $this->generateAnnotationsPhp($test->getAnnotations(), true); $dependencies = 'AcceptanceTester $I'; - try { - $steps = $this->generateStepsPhp($test->getOrderedActions()); - } catch (TestReferenceException $e) { - throw new TestReferenceException($e->getMessage() . " in Test \"" . $test->getName() . "\""); + if ($test->isSkipped()) { + $steps = "\t\t" . '$scenario->skip("This test is skipped");' . "\n"; + $dependencies .= ', \Codeception\Scenario $scenario'; + } else { + try { + $steps = $this->generateStepsPhp($test->getOrderedActions()); + } catch (TestReferenceException $e) { + throw new TestReferenceException($e->getMessage() . " in Test \"" . $test->getName() . "\""); + } } $testPhp .= $testAnnotations; @@ -1497,26 +1580,17 @@ private function addUniquenessToParamArray($input) */ private function addUniquenessFunctionCall($input) { - $output = ''; - - preg_match('/' . EntityDataObject::CEST_UNIQUE_FUNCTION . '\("[\w]+"\)/', $input, $matches); - if (!empty($matches)) { - $parts = preg_split('/' . EntityDataObject::CEST_UNIQUE_FUNCTION . '\("[\w]+"\)/', $input, -1); - for ($i = 0; $i < count($parts); $i++) { - $parts[$i] = $this->stripWrappedQuotes($parts[$i]); - } - if (!empty($parts[0])) { - $output = $this->wrapWithDoubleQuotes($parts[0]); - } - $output .= $output === '' ? $matches[0] : '.' . $matches[0]; - if (!empty($parts[1])) { - $output .= '.' . $this->wrapWithDoubleQuotes($parts[1]); - } - } else { - $output = $this->wrapWithDoubleQuotes($input); + $output = $this->wrapWithDoubleQuotes($input); + + //Match on msq(\"entityName\") + preg_match_all('/' . EntityDataObject::CEST_UNIQUE_FUNCTION . '\(\\\\"[\w]+\\\\"\)/', $output, $matches); + foreach (array_unique($matches[0]) as $match) { + preg_match('/\\\\"([\w]+)\\\\"/', $match, $entityMatch); + $entity = $entityMatch[1]; + $output = str_replace($match, '" . msq("' . $entity . '") . "', $output); } - - return $output; + // trim unnecessary "" . and . "" + return preg_replace('/(?(?<![\\\\])"" \. )| \. ""/', "", $output); } /** @@ -1585,17 +1659,17 @@ private function wrapFunctionCall($actor, $action, ...$args) if (null === $args[$i]) { continue; } - if (!$isFirst) { - $output .= ', '; + if ($args[$i] === "") { + $args[$i] = '"' . $args[$i] . '"'; } - $output .= $args[$i]; - $isFirst = false; } - $output .= ");\n"; - - $output = $this->resolveEnvReferences($output, $args); - - return $this->resolveTestVariable($output, $args, $action->getActionOrigin()); + if (!is_array($args)) { + $args = [$args]; + } + $args = $this->resolveEnvReferences($args); + $args = $this->resolveTestVariable($args, $action->getActionOrigin()); + $output .= implode(", ", array_filter($args, function($value) { return $value !== null; })) . ");\n"; + return $output; } /** @@ -1616,33 +1690,32 @@ private function wrapFunctionCallWithReturnValue($returnVariable, $actor, $actio if (null === $args[$i]) { continue; } - if (!$isFirst) { - $output .= ', '; + if ($args[$i] === "") { + $args[$i] = '"' . $args[$i] . '"'; } - $output .= $args[$i]; - $isFirst = false; } - $output .= ");\n"; - - $output = $this->resolveEnvReferences($output, $args); - - return $this->resolveTestVariable($output, $args, $action->getActionOrigin()); + if (!is_array($args)) { + $args = [$args]; + } + $args = $this->resolveEnvReferences($args); + $args = $this->resolveTestVariable($args, $action->getActionOrigin()); + $output .= implode(", ", array_filter($args, function($value) { return $value !== null; })) . ");\n"; + return $output; } // @codingStandardsIgnoreEnd /** * Resolves {{_ENV.variable}} into getenv("variable") for test-runtime ENV referencing. - * @param string $inputString * @param array $args - * @return string + * @return array */ - private function resolveEnvReferences($inputString, $args) + private function resolveEnvReferences($args) { $envRegex = "/{{_ENV\.([\w]+)}}/"; - $outputString = $inputString; + $newArgs = []; - foreach ($args as $arg) { + foreach ($args as $key => $arg) { preg_match_all($envRegex, $arg, $matches); if (!empty($matches[0])) { $fullMatch = $matches[0][0]; @@ -1651,11 +1724,14 @@ private function resolveEnvReferences($inputString, $args) $replacement = "getenv(\"{$envVariable}\")"; $outputArg = $this->processQuoteBreaks($fullMatch, $arg, $replacement); - $outputString = str_replace($arg, $outputArg, $outputString); + $newArgs[$key] = $outputArg; + continue; } + $newArgs[$key] = $arg; } - return $outputString; + // override passed in args for use later. + return $newArgs; } /** @@ -1756,10 +1832,11 @@ private function stripQuotes($inStr) private function validateXmlAttributesMutuallyExclusive($key, $tagName, $attributes) { $rules = [ - ['attributes' => [ - 'selector', - 'selectorArray', - ] + [ + 'attributes' => [ + 'selector', + 'selectorArray', + ] ], [ 'attributes' => [ diff --git a/src/Magento/FunctionalTestingFramework/Util/Validation/NameValidationUtil.php b/src/Magento/FunctionalTestingFramework/Util/Validation/NameValidationUtil.php new file mode 100644 index 000000000..60b4660fb --- /dev/null +++ b/src/Magento/FunctionalTestingFramework/Util/Validation/NameValidationUtil.php @@ -0,0 +1,53 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\FunctionalTestingFramework\Util\Validation; + +use Magento\FunctionalTestingFramework\Exceptions\XmlException; + +class NameValidationUtil +{ + const PHP_CLASS_REGEX_PATTERN = '/^[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*$/'; + + /** + * Function which runs a validation against the blacklisted char defined in this class. Validation occurs to insure + * allure report does not error/future devOps builds do not error against illegal char. + * + * @param string $name + * @param string $type + * @return void + * @throws XmlException + */ + public static function validateName($name, $type) + { + $startingPos = 0; + $illegalCharArray = []; + $nameToEvaluate = $name; + + while ($startingPos < strlen($nameToEvaluate)) { + $startingPos++; + $partialName = substr($nameToEvaluate, 0, $startingPos); + $valid = boolval(preg_match(self::PHP_CLASS_REGEX_PATTERN, $partialName)); + + if (!$valid) { + $illegalChar = str_split($partialName)[$startingPos -1]; + $illegalCharArray[] = $illegalChar; + $nameToEvaluate = str_replace($illegalChar, "", $nameToEvaluate); + $startingPos--; + } + } + + if (!empty($illegalCharArray)) { + $errorMessage = "{$type} name \"${name}\" contains illegal characters, please fix and re-run."; + + foreach ($illegalCharArray as $diffChar) { + $errorMessage .= "\nTest names cannot contain '{$diffChar}'"; + } + + throw new XmlException($errorMessage); + } + } +}