Add support for enhanced code-smell tests. (#36332)
* Add support for enhanced code-smell tests: - Path selection handled by ansible-test. - Optional path filtering based on extension. - Optional path filtering based on prefixes. - Optional lint friendly output. * Enhance no-assert code-smell test. * Enhance no-tests-as-filters code-smell test.pull/4420/head
parent
ce80485ecd
commit
2b6ac4561b
|
@ -3,6 +3,7 @@ from __future__ import absolute_import, print_function
|
||||||
|
|
||||||
import abc
|
import abc
|
||||||
import glob
|
import glob
|
||||||
|
import json
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
|
|
||||||
|
@ -13,6 +14,7 @@ from lib.util import (
|
||||||
run_command,
|
run_command,
|
||||||
import_plugins,
|
import_plugins,
|
||||||
load_plugins,
|
load_plugins,
|
||||||
|
parse_to_dict,
|
||||||
ABC,
|
ABC,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -94,7 +96,7 @@ def command_sanity(args):
|
||||||
options = ''
|
options = ''
|
||||||
|
|
||||||
if isinstance(test, SanityCodeSmellTest):
|
if isinstance(test, SanityCodeSmellTest):
|
||||||
result = test.test(args)
|
result = test.test(args, targets)
|
||||||
elif isinstance(test, SanityMultipleVersion):
|
elif isinstance(test, SanityMultipleVersion):
|
||||||
result = test.test(args, targets, python_version=version)
|
result = test.test(args, targets, python_version=version)
|
||||||
options = ' --python %s' % version
|
options = ' --python %s' % version
|
||||||
|
@ -205,19 +207,52 @@ class SanityCodeSmellTest(SanityTest):
|
||||||
"""Sanity test script."""
|
"""Sanity test script."""
|
||||||
def __init__(self, path):
|
def __init__(self, path):
|
||||||
name = os.path.splitext(os.path.basename(path))[0]
|
name = os.path.splitext(os.path.basename(path))[0]
|
||||||
|
config = os.path.splitext(path)[0] + '.json'
|
||||||
|
|
||||||
self.path = path
|
self.path = path
|
||||||
|
self.config = config if os.path.exists(config) else None
|
||||||
|
|
||||||
super(SanityCodeSmellTest, self).__init__(name)
|
super(SanityCodeSmellTest, self).__init__(name)
|
||||||
|
|
||||||
def test(self, args):
|
def test(self, args, targets):
|
||||||
"""
|
"""
|
||||||
:type args: SanityConfig
|
:type args: SanityConfig
|
||||||
|
:type targets: SanityTargets
|
||||||
:rtype: SanityResult
|
:rtype: SanityResult
|
||||||
"""
|
"""
|
||||||
cmd = [self.path]
|
cmd = [self.path]
|
||||||
env = ansible_environment(args, color=False)
|
env = ansible_environment(args, color=False)
|
||||||
|
|
||||||
|
pattern = None
|
||||||
|
|
||||||
|
if self.config:
|
||||||
|
with open(self.config, 'r') as config_fd:
|
||||||
|
config = json.load(config_fd)
|
||||||
|
|
||||||
|
output = config.get('output')
|
||||||
|
extensions = config.get('extensions')
|
||||||
|
prefixes = config.get('prefixes')
|
||||||
|
|
||||||
|
if output == 'path-line-column-message':
|
||||||
|
pattern = '^(?P<path>[^:]*):(?P<line>[0-9]+):(?P<column>[0-9]+): (?P<message>.*)$'
|
||||||
|
elif output == 'path-message':
|
||||||
|
pattern = '^(?P<path>[^:]*): (?P<message>.*)$'
|
||||||
|
else:
|
||||||
|
pattern = ApplicationError('Unsupported output type: %s' % output)
|
||||||
|
|
||||||
|
paths = sorted(i.path for i in targets.include)
|
||||||
|
|
||||||
|
if extensions:
|
||||||
|
paths = [p for p in paths if os.path.splitext(p)[1] in extensions or (p.startswith('bin/') and '.py' in extensions)]
|
||||||
|
|
||||||
|
if prefixes:
|
||||||
|
paths = [p for p in paths if any(p.startswith(pre) for pre in prefixes)]
|
||||||
|
|
||||||
|
if not paths:
|
||||||
|
return SanitySkipped(self.name)
|
||||||
|
|
||||||
|
cmd += paths
|
||||||
|
|
||||||
try:
|
try:
|
||||||
stdout, stderr = run_command(args, cmd, env=env, capture=True)
|
stdout, stderr = run_command(args, cmd, env=env, capture=True)
|
||||||
status = 0
|
status = 0
|
||||||
|
@ -226,6 +261,19 @@ class SanityCodeSmellTest(SanityTest):
|
||||||
stderr = ex.stderr
|
stderr = ex.stderr
|
||||||
status = ex.status
|
status = ex.status
|
||||||
|
|
||||||
|
if stdout and not stderr:
|
||||||
|
if pattern:
|
||||||
|
matches = [parse_to_dict(pattern, line) for line in stdout.splitlines()]
|
||||||
|
|
||||||
|
messages = [SanityMessage(
|
||||||
|
message=m['message'],
|
||||||
|
path=m['path'],
|
||||||
|
line=int(m.get('line', 0)),
|
||||||
|
column=int(m.get('column', 0)),
|
||||||
|
) for m in matches]
|
||||||
|
|
||||||
|
return SanityFailure(self.name, messages=messages)
|
||||||
|
|
||||||
if stderr or status:
|
if stderr or status:
|
||||||
summary = u'%s' % SubprocessError(cmd=cmd, status=status, stderr=stderr, stdout=stdout)
|
summary = u'%s' % SubprocessError(cmd=cmd, status=status, stderr=stderr, stdout=stdout)
|
||||||
return SanityFailure(self.name, summary=summary)
|
return SanityFailure(self.name, summary=summary)
|
||||||
|
|
|
@ -0,0 +1,10 @@
|
||||||
|
{
|
||||||
|
"extensions": [
|
||||||
|
".py"
|
||||||
|
],
|
||||||
|
"prefixes": [
|
||||||
|
"bin/",
|
||||||
|
"lib/ansible/"
|
||||||
|
],
|
||||||
|
"output": "path-line-column-message"
|
||||||
|
}
|
|
@ -1,40 +1,29 @@
|
||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
from __future__ import print_function
|
from __future__ import print_function
|
||||||
|
|
||||||
import os
|
|
||||||
import re
|
import re
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
from collections import defaultdict
|
|
||||||
|
|
||||||
PATH = 'lib/ansible'
|
|
||||||
ASSERT_RE = re.compile(r'.*(?<![-:a-zA-Z#][ -])\bassert\b(?!:).*')
|
ASSERT_RE = re.compile(r'.*(?<![-:a-zA-Z#][ -])\bassert\b(?!:).*')
|
||||||
|
|
||||||
all_matches = defaultdict(list)
|
|
||||||
|
|
||||||
for dirpath, dirnames, filenames in os.walk(PATH):
|
def main():
|
||||||
for filename in filenames:
|
failed = False
|
||||||
path = os.path.join(dirpath, filename)
|
|
||||||
if not os.path.isfile(path) or not path.endswith('.py'):
|
|
||||||
continue
|
|
||||||
|
|
||||||
|
for path in sys.argv[1:]:
|
||||||
with open(path, 'r') as f:
|
with open(path, 'r') as f:
|
||||||
for i, line in enumerate(f.readlines()):
|
for i, line in enumerate(f.readlines()):
|
||||||
matches = ASSERT_RE.findall(line)
|
matches = ASSERT_RE.findall(line)
|
||||||
|
|
||||||
if matches:
|
if matches:
|
||||||
all_matches[path].append((i + 1, line.index('assert') + 1, matches))
|
failed = True
|
||||||
|
lineno = i + 1
|
||||||
|
colno = line.index('assert') + 1
|
||||||
|
print('%s:%d:%d: raise AssertionError instead of: %s' % (path, lineno, colno, matches[0][colno - 1:]))
|
||||||
|
|
||||||
|
if failed:
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
if all_matches:
|
if __name__ == '__main__':
|
||||||
print('Use of assert in production code is not recommended.')
|
main()
|
||||||
print('Python will remove all assert statements if run with optimizations')
|
|
||||||
print('Alternatives:')
|
|
||||||
print(' if not isinstance(value, dict):')
|
|
||||||
print(' raise AssertionError("Expected a dict for value")')
|
|
||||||
|
|
||||||
for path, matches in all_matches.items():
|
|
||||||
for line_matches in matches:
|
|
||||||
for match in line_matches[2]:
|
|
||||||
print('%s:%d:%d: %s' % ((path,) + line_matches[:2] + (match,)))
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
{
|
||||||
|
"extensions": [
|
||||||
|
".yml",
|
||||||
|
".yaml"
|
||||||
|
],
|
||||||
|
"output": "path-line-column-message"
|
||||||
|
}
|
|
@ -19,12 +19,9 @@
|
||||||
|
|
||||||
from __future__ import print_function
|
from __future__ import print_function
|
||||||
|
|
||||||
import os
|
|
||||||
import re
|
import re
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
from collections import defaultdict
|
|
||||||
|
|
||||||
from ansible.plugins.test import core, files, mathstuff
|
from ansible.plugins.test import core, files, mathstuff
|
||||||
|
|
||||||
TESTS = list(core.TestModule().tests().keys()) + list(files.TestModule().tests().keys()) + list(mathstuff.TestModule().tests().keys())
|
TESTS = list(core.TestModule().tests().keys()) + list(files.TestModule().tests().keys()) + list(mathstuff.TestModule().tests().keys())
|
||||||
|
@ -48,41 +45,42 @@ TEST_MAP = {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
FILTER_RE = re.compile(r'((.+?)\s*([\w \.\'"]+)(\s*)\|(\s*)(\w+))')
|
FILTER_RE = re.compile(r'((.+?)\s*(?P<left>[\w \.\'"]+)(\s*)\|(\s*)(?P<filter>\w+))')
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
all_matches = defaultdict(list)
|
failed = False
|
||||||
|
|
||||||
for root, dirs, filenames in os.walk('.'):
|
for path in sys.argv[1:]:
|
||||||
for name in filenames:
|
with open(path) as f:
|
||||||
if os.path.splitext(name)[1] not in ('.yml', '.yaml'):
|
text = f.read()
|
||||||
|
|
||||||
|
lines = text.splitlines(True)
|
||||||
|
previous = 0
|
||||||
|
offset = 0
|
||||||
|
lineno = 0
|
||||||
|
|
||||||
|
for match in FILTER_RE.finditer(text):
|
||||||
|
filter_name = match.group('filter')
|
||||||
|
test_name = TEST_MAP.get(filter_name, filter_name)
|
||||||
|
|
||||||
|
if test_name not in TESTS:
|
||||||
continue
|
continue
|
||||||
path = os.path.join(root, name)
|
|
||||||
|
|
||||||
with open(path) as f:
|
failed = True
|
||||||
text = f.read()
|
left = match.group('left').strip()
|
||||||
|
start = match.start('left')
|
||||||
|
|
||||||
for match in FILTER_RE.findall(text):
|
while start >= offset:
|
||||||
filter_name = match[5]
|
previous = offset
|
||||||
|
offset += len(lines[lineno])
|
||||||
|
lineno += 1
|
||||||
|
|
||||||
try:
|
colno = start - previous + 1
|
||||||
test_name = TEST_MAP[filter_name]
|
|
||||||
except KeyError:
|
|
||||||
test_name = filter_name
|
|
||||||
|
|
||||||
if test_name not in TESTS:
|
print('%s:%d:%d: use `%s is %s` instead of `%s | %s`' % (path, lineno, colno, left, test_name, left, filter_name))
|
||||||
continue
|
|
||||||
|
|
||||||
all_matches[path].append(match[0])
|
if failed:
|
||||||
|
|
||||||
if all_matches:
|
|
||||||
print('Use of Ansible provided Jinja2 tests as filters is deprecated.')
|
|
||||||
print('Please update to use `is` syntax such as `result is failed`.')
|
|
||||||
|
|
||||||
for path, matches in all_matches.items():
|
|
||||||
for match in matches:
|
|
||||||
print('%s: %s' % (path, match,))
|
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue