Skip to content

MQE-1676: Static check for unused arguments in action group. #555

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 13 commits into from
Jan 27, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion docs/commands/mftf.md
Original file line number Diff line number Diff line change
Expand Up @@ -491,11 +491,18 @@ To run specific static check scripts
```bash
vendor/bin/mftf static-checks testDependencies
```
```bash
vendor/bin/mftf static-checks actionGroupArguments
```
```bash
vendor/bin/mftf static-checks testDependencies actionGroupArguments
```

#### Existing static checks

* Test Dependency: Checks that test dependencies do not violate Magento module's composer dependencies.

* Action Group Unused Arguments: Checks that action groups do not have unused arguments.

### `upgrade:tests`

Applies all the MFTF major version upgrade scripts to test components in the given path (`test.xml`, `data.xml`, etc).
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
<?php
/**
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/

namespace Magento\FunctionalTestingFramework\StaticCheck;

use Magento\FunctionalTestingFramework\Config\MftfApplicationConfig;
use Magento\FunctionalTestingFramework\Exceptions\XmlException;
use Magento\FunctionalTestingFramework\Test\Handlers\ActionGroupObjectHandler;
use Magento\FunctionalTestingFramework\Test\Objects\ActionGroupObject;
use Symfony\Component\Console\Input\InputInterface;
use Magento\FunctionalTestingFramework\Util\ModuleResolver;
use Symfony\Component\Finder\Finder;
use Exception;

/**
* Class ActionGroupArgumentsCheck
* @package Magento\FunctionalTestingFramework\StaticCheck
*/
class ActionGroupArgumentsCheck implements StaticCheckInterface
{

const ACTIONGROUP_XML_REGEX_PATTERN = '/<actionGroup\sname=(?: (?!<\/actionGroup>).)*/mxs';
const ACTIONGROUP_ARGUMENT_REGEX_PATTERN = '/<argument[^\/>]*name="([^"\']*)/mxs';
const ACTIONGROUP_NAME_REGEX_PATTERN = '/<actionGroup name=["\']([^\'"]*)/';

const ERROR_LOG_FILENAME = 'mftf-arguments-checks';
const ERROR_LOG_MESSAGE = 'MFTF Action Group Unused Arguments Check';

/**
* Array containing all errors found after running the execute() function.
* @var array
*/
private $errors = [];

/**
* String representing the output summary found after running the execute() function.
* @var string
*/
private $output;

/**
* Checks unused arguments in action groups and prints out error to file.
*
* @param InputInterface $input
* @return void
* @throws Exception
*/
public function execute(InputInterface $input)
{
MftfApplicationConfig::create(
true,
MftfApplicationConfig::UNIT_TEST_PHASE,
false,
MftfApplicationConfig::LEVEL_NONE,
true
);

$allModules = ModuleResolver::getInstance()->getModulesPath();

$actionGroupXmlFiles = StaticCheckHelper::buildFileList(
$allModules,
DIRECTORY_SEPARATOR . 'ActionGroup' . DIRECTORY_SEPARATOR
);

$this->errors = $this->findErrorsInFileSet($actionGroupXmlFiles);

$this->output = StaticCheckHelper::printErrorsToFile(
$this->errors,
self::ERROR_LOG_FILENAME,
self::ERROR_LOG_MESSAGE
);
}

/**
* Return array containing all errors found after running the execute() function.
* @return array
*/
public function getErrors()
{
return $this->errors;
}

/**
* Return string of a short human readable result of the check. For example: "No unused arguments found."
* @return string
*/
public function getOutput()
{
return $this->output;
}

/**
* Finds all unused arguments in given set of actionGroup files
* @param Finder $files
* @return array $testErrors
*/
private function findErrorsInFileSet($files)
{
$actionGroupErrors = [];
foreach ($files as $filePath) {
$contents = file_get_contents($filePath);
preg_match_all(self::ACTIONGROUP_XML_REGEX_PATTERN, $contents, $actionGroups);
$actionGroupToArguments = $this->buildUnusedArgumentList($actionGroups[0]);
$actionGroupErrors += $this->setErrorOutput($actionGroupToArguments, $filePath);
}
return $actionGroupErrors;
}

/**
* Builds array of action group => unused arguments
* @param array $actionGroups
* @return array $actionGroupToArguments
*/
private function buildUnusedArgumentList($actionGroups)
{
$actionGroupToArguments = [];

foreach ($actionGroups as $actionGroupXml) {
preg_match(self::ACTIONGROUP_NAME_REGEX_PATTERN, $actionGroupXml, $actionGroupName);
$unusedArguments = $this->findUnusedArguments($actionGroupXml);
if (!empty($unusedArguments)) {
$actionGroupToArguments[$actionGroupName[1]] = $unusedArguments;
}
}
return $actionGroupToArguments;
}

/**
* Returns unused arguments in an action group
* @param string $actionGroupXml
* @return array
*/
private function findUnusedArguments($actionGroupXml)
{
$unusedArguments = [];

preg_match_all(self::ACTIONGROUP_ARGUMENT_REGEX_PATTERN, $actionGroupXml, $arguments);
preg_match(self::ACTIONGROUP_NAME_REGEX_PATTERN, $actionGroupXml, $actionGroupName);
try {
$actionGroup = ActionGroupObjectHandler::getInstance()->getObject($actionGroupName[1]);
} catch (XmlException $e) {
}
foreach ($arguments[1] as $argument) {
//pattern to match all argument references
$patterns = [
'(\{{2}' . $argument . '(\.[a-zA-Z0-9_\[\]\(\).,\'\/ ]+)?}{2})',
'([(,\s\'$$]' . $argument . '(\.[a-zA-Z0-9_$\[\]]+)?[),\s\'])'
];
// matches entity references
if (preg_match($patterns[0], $actionGroupXml)) {
continue;
}
//matches parametrized references
if (preg_match($patterns[1], $actionGroupXml)) {
continue;
}
//for extending action groups, exclude arguments that are also defined in parent action group
if ($this->isParentActionGroupArgument($argument, $actionGroup)) {
continue;
}
$unusedArguments[] = $argument;
}
return $unusedArguments;
}

/**
* Checks if the argument is also defined in the parent for extending action groups.
* @param string $argument
* @param ActionGroupObject $actionGroup
* @return boolean
*/
private function isParentActionGroupArgument($argument, $actionGroup)
{
$parentActionGroupName = $actionGroup->getParentName();
if ($parentActionGroupName !== null) {
$parentActionGroup = ActionGroupObjectHandler::getInstance()->getObject($parentActionGroupName);
$parentArguments = $parentActionGroup->getArguments();
foreach ($parentArguments as $parentArgument) {
if ($argument === $parentArgument->getName()) {
return true;
}
}
}
return false;
}

/**
* Builds and returns error output for violating references
*
* @param array $actionGroupToArguments
* @param string $path
* @return mixed
*/
private function setErrorOutput($actionGroupToArguments, $path)
{
$actionGroupErrors = [];
if (!empty($actionGroupToArguments)) {
// Build error output
$errorOutput = "\nFile \"{$path->getRealPath()}\"";
$errorOutput .= "\ncontains action group(s) with unused arguments.\n\t\t";
foreach ($actionGroupToArguments as $actionGroup => $arguments) {
$errorOutput .= "\n\t {$actionGroup} has unused argument(s): " . implode(", ", $arguments);
}
$actionGroupErrors[$path->getRealPath()][] = $errorOutput;
}
return $actionGroupErrors;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
<?php
/**
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
namespace Magento\FunctionalTestingFramework\StaticCheck;

use Symfony\Component\Finder\Finder;

class StaticCheckHelper
{
/**
* Prints out given errors to file, and returns summary result string
* @param array $errors
* @param string $filename
* @param string $message
* @return string
*/
public static function printErrorsToFile($errors, $filename, $message)
{
if (empty($errors)) {
return $message . ": No errors found.";
}

$outputPath = getcwd() . DIRECTORY_SEPARATOR . $filename . ".txt";
$fileResource = fopen($outputPath, 'w');

foreach ($errors as $test => $error) {
fwrite($fileResource, $error[0] . PHP_EOL);
}

fclose($fileResource);
$errorCount = count($errors);
$output = $message . ": Errors found across {$errorCount} file(s). Error details output to {$outputPath}";

return $output;
}

/**
* Builds list of all XML files in given modulePaths + path given
* @param array $modulePaths
* @param string $path
* @return Finder
*/
public static function buildFileList($modulePaths, $path)
{
$finder = new Finder();
foreach ($modulePaths as $modulePath) {
if (!realpath($modulePath . $path)) {
continue;
}
$finder->files()->in($modulePath . $path)->name("*.xml");
}
return $finder->files();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ public function __construct(array $checks = [])
{
$this->checks = [
'testDependencies' => new TestDependencyCheck(),
'actionGroupArguments' => new ActionGroupArgumentsCheck(),
] + $checks;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ class TestDependencyCheck implements StaticCheckInterface
const ACTIONGROUP_REGEX_PATTERN = '/ref=["\']([^\'"]*)/';
const ACTIONGROUP_ARGUMENT_REGEX_PATTERN = '/<argument[^\/>]*name="([^"\']*)/';

const ERROR_LOG_FILENAME = 'mftf-dependency-checks';
const ERROR_LOG_MESSAGE = 'MFTF File Dependency Check';

/**
* Array of FullModuleName => [dependencies]
* @var array
Expand Down Expand Up @@ -113,17 +116,21 @@ public function execute(InputInterface $input)
DIRECTORY_SEPARATOR . 'Data' . DIRECTORY_SEPARATOR,
];
// These files can contain references to other modules.
$testXmlFiles = $this->buildFileList($allModules, $filePaths[0]);
$actionGroupXmlFiles = $this->buildFileList($allModules, $filePaths[1]);
$dataXmlFiles= $this->buildFileList($allModules, $filePaths[2]);
$testXmlFiles = StaticCheckHelper::buildFileList($allModules, $filePaths[0]);
$actionGroupXmlFiles = StaticCheckHelper::buildFileList($allModules, $filePaths[1]);
$dataXmlFiles= StaticCheckHelper::buildFileList($allModules, $filePaths[2]);

$this->errors = [];
$this->errors += $this->findErrorsInFileSet($testXmlFiles);
$this->errors += $this->findErrorsInFileSet($actionGroupXmlFiles);
$this->errors += $this->findErrorsInFileSet($dataXmlFiles);

// hold on to the output and print any errors to a file
$this->output = $this->printErrorsToFile();
$this->output = StaticCheckHelper::printErrorsToFile(
$this->errors,
self::ERROR_LOG_FILENAME,
self::ERROR_LOG_MESSAGE
);
}

/**
Expand Down Expand Up @@ -413,24 +420,6 @@ private function getModuleDependenciesFromReferences($array)
return $filenames;
}

/**
* Builds list of all XML files in given modulePaths + path given
* @param string $modulePaths
* @param string $path
* @return Finder
*/
private function buildFileList($modulePaths, $path)
{
$finder = new Finder();
foreach ($modulePaths as $modulePath) {
if (!realpath($modulePath . $path)) {
continue;
}
$finder->files()->in($modulePath . $path)->name("*.xml");
}
return $finder->files();
}

/**
* Attempts to find any MFTF entity by its name. Returns null if none are found.
* @param string $name
Expand Down Expand Up @@ -461,32 +450,4 @@ private function findEntity($name)
}
return null;
}

/**
* Prints out given errors to file, and returns summary result string
* @return string
*/
private function printErrorsToFile()
{
$errors = $this->getErrors();

if (empty($errors)) {
return "No Dependency errors found.";
}

$outputPath = getcwd() . DIRECTORY_SEPARATOR . "mftf-dependency-checks.txt";
$fileResource = fopen($outputPath, 'w');
$header = "MFTF File Dependency Check:\n";
fwrite($fileResource, $header);

foreach ($errors as $test => $error) {
fwrite($fileResource, $error[0] . PHP_EOL);
}

fclose($fileResource);
$errorCount = count($errors);
$output = "Dependency errors found across {$errorCount} file(s). Error details output to {$outputPath}";

return $output;
}
}