303 lines
9.7 KiB
Python
303 lines
9.7 KiB
Python
"""frosted/test/test_api.py.
|
|
|
|
Tests all major functionality of the Frosted API
|
|
|
|
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
|
|
documentation files (the "Software"), to deal in the Software without restriction, including without limitation
|
|
the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and
|
|
to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
|
|
The above copyright notice and this permission notice shall be included in all copies or
|
|
substantial portions of the Software.
|
|
|
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
|
|
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
|
|
THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
|
|
CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
|
OTHER DEALINGS IN THE SOFTWARE.
|
|
|
|
"""
|
|
from __future__ import absolute_import, division, print_function, unicode_literals
|
|
|
|
import os
|
|
import sys
|
|
import tempfile
|
|
from io import StringIO
|
|
|
|
import pytest
|
|
from pies.overrides import *
|
|
|
|
from frosted.api import check_path, check_recursive
|
|
from frosted.messages import PythonSyntaxError, UnusedImport
|
|
from frosted.reporter import Reporter
|
|
|
|
from .utils import LoggingReporter, Node
|
|
|
|
|
|
def test_syntax_error():
|
|
"""syntax_error reports that there was a syntax error in the source file.
|
|
|
|
It reports to the error stream and includes the filename, line number, error message, actual line of source and a
|
|
caret pointing to where the error is
|
|
|
|
"""
|
|
err = StringIO()
|
|
reporter = Reporter(err, err)
|
|
reporter.flake(PythonSyntaxError('foo.py', 'a problem', 3, 7, 'bad line of source', verbose=True))
|
|
assert ("foo.py:3:7:E402::a problem\n"
|
|
"bad line of source\n"
|
|
" ^\n") == err.getvalue()
|
|
|
|
|
|
def test_syntax_errorNoOffset():
|
|
"""syntax_error doesn't include a caret pointing to the error if offset is passed as None."""
|
|
err = StringIO()
|
|
reporter = Reporter(err, err)
|
|
reporter.flake(PythonSyntaxError('foo.py', 'a problem', 3, None, 'bad line of source', verbose=True))
|
|
assert ("foo.py:3:0:E402::a problem\n"
|
|
"bad line of source\n") == err.getvalue()
|
|
|
|
|
|
def test_multiLineSyntaxError():
|
|
""" If there's a multi-line syntax error, then we only report the last line.
|
|
|
|
The offset is adjusted so that it is relative to the start of the last line
|
|
|
|
"""
|
|
err = StringIO()
|
|
lines = ['bad line of source', 'more bad lines of source']
|
|
reporter = Reporter(err, err)
|
|
reporter.flake(PythonSyntaxError('foo.py', 'a problem', 3, len(lines[0]) + 7, '\n'.join(lines), verbose=True))
|
|
assert ("foo.py:3:6:E402::a problem\n" +
|
|
lines[-1] + "\n" +
|
|
" ^\n") == err.getvalue()
|
|
|
|
|
|
def test_unexpected_error():
|
|
"""unexpected_error reports an error processing a source file."""
|
|
err = StringIO()
|
|
reporter = Reporter(None, err)
|
|
reporter.unexpected_error('source.py', 'error message')
|
|
assert 'source.py: error message\n' == err.getvalue()
|
|
|
|
|
|
def test_flake():
|
|
"""flake reports a code warning from Frosted.
|
|
|
|
It is exactly the str() of a frosted.messages.Message
|
|
|
|
"""
|
|
out = StringIO()
|
|
reporter = Reporter(out, None)
|
|
message = UnusedImport('foo.py', Node(42), 'bar')
|
|
reporter.flake(message)
|
|
assert out.getvalue() == "%s\n" % (message,)
|
|
|
|
|
|
def make_temp_file(content):
|
|
"""Make a temporary file containing C{content} and return a path to it."""
|
|
_, fpath = tempfile.mkstemp()
|
|
if not hasattr(content, 'decode'):
|
|
content = content.encode('ascii')
|
|
fd = open(fpath, 'wb')
|
|
fd.write(content)
|
|
fd.close()
|
|
return fpath
|
|
|
|
|
|
def assert_contains_output(path, flakeList):
|
|
"""Assert that provided causes at minimal the errors provided in the error list."""
|
|
out = StringIO()
|
|
count = check_path(path, Reporter(out, out), verbose=True)
|
|
out_string = out.getvalue()
|
|
assert len(flakeList) >= count
|
|
for flake in flakeList:
|
|
assert flake in out_string
|
|
|
|
|
|
def get_errors(path):
|
|
"""Get any warnings or errors reported by frosted for the file at path."""
|
|
log = []
|
|
reporter = LoggingReporter(log)
|
|
count = check_path(path, reporter)
|
|
return count, log
|
|
|
|
|
|
def test_missingTrailingNewline():
|
|
"""Source which doesn't end with a newline shouldn't cause any exception to
|
|
|
|
be raised nor an error indicator to be returned by check.
|
|
|
|
"""
|
|
fName = make_temp_file("def foo():\n\tpass\n\t")
|
|
assert_contains_output(fName, [])
|
|
|
|
|
|
def test_check_pathNonExisting():
|
|
"""check_path handles non-existing files"""
|
|
count, errors = get_errors('extremo')
|
|
assert count == 1
|
|
assert errors == [('unexpected_error', 'extremo', 'No such file or directory')]
|
|
|
|
|
|
def test_multilineSyntaxError():
|
|
"""Source which includes a syntax error which results in the raised SyntaxError.
|
|
|
|
text containing multiple lines of source are reported with only
|
|
the last line of that source.
|
|
|
|
"""
|
|
source = """\
|
|
def foo():
|
|
'''
|
|
|
|
def bar():
|
|
pass
|
|
|
|
def baz():
|
|
'''quux'''
|
|
"""
|
|
# Sanity check - SyntaxError.text should be multiple lines, if it
|
|
# isn't, something this test was unprepared for has happened.
|
|
def evaluate(source):
|
|
exec(source)
|
|
try:
|
|
evaluate(source)
|
|
except SyntaxError:
|
|
e = sys.exc_info()[1]
|
|
assert e.text.count('\n') > 1
|
|
else:
|
|
assert False
|
|
|
|
sourcePath = make_temp_file(source)
|
|
assert_contains_output(
|
|
sourcePath,
|
|
["""\
|
|
%s:8:10:E402::invalid syntax
|
|
'''quux'''
|
|
^
|
|
""" % (sourcePath,)])
|
|
|
|
|
|
def test_eofSyntaxError():
|
|
"""The error reported for source files which end prematurely causing a
|
|
syntax error reflects the cause for the syntax error.
|
|
|
|
"""
|
|
sourcePath = make_temp_file("def foo(")
|
|
assert_contains_output(sourcePath, ["""\
|
|
%s:1:8:E402::unexpected EOF while parsing
|
|
def foo(
|
|
^
|
|
""" % (sourcePath,)])
|
|
|
|
|
|
def test_nonDefaultFollowsDefaultSyntaxError():
|
|
""" Source which has a non-default argument following a default argument
|
|
|
|
should include the line number of the syntax error
|
|
However these exceptions do not include an offset
|
|
|
|
"""
|
|
source = """\
|
|
def foo(bar=baz, bax):
|
|
pass
|
|
"""
|
|
sourcePath = make_temp_file(source)
|
|
last_line = ' ^\n' if sys.version_info >= (3, 2) else ''
|
|
column = '7:' if sys.version_info >= (3, 2) else '0:'
|
|
assert_contains_output(sourcePath, ["""\
|
|
%s:1:%sE402::non-default argument follows default argument
|
|
def foo(bar=baz, bax):
|
|
%s""" % (sourcePath, column, last_line)])
|
|
|
|
|
|
def test_nonKeywordAfterKeywordSyntaxError():
|
|
"""Source which has a non-keyword argument after a keyword argument
|
|
|
|
should include the line number of the syntax error
|
|
However these exceptions do not include an offset
|
|
"""
|
|
source = """\
|
|
foo(bar=baz, bax)
|
|
"""
|
|
sourcePath = make_temp_file(source)
|
|
last_line = ' ^\n' if sys.version_info >= (3, 2) else ''
|
|
column = '12:' if sys.version_info >= (3, 2) else '0:'
|
|
assert_contains_output(
|
|
sourcePath,
|
|
["""\
|
|
%s:1:%sE402::non-keyword arg after keyword arg
|
|
foo(bar=baz, bax)
|
|
%s""" % (sourcePath, column, last_line)])
|
|
|
|
|
|
def test_invalidEscape():
|
|
"""The invalid escape syntax raises ValueError in Python 2."""
|
|
sourcePath = make_temp_file(r"foo = '\xyz'")
|
|
if PY2:
|
|
decoding_error = "%s: problem decoding source\n" % (sourcePath,)
|
|
else:
|
|
decoding_error = "(unicode error) 'unicodeescape' codec can't decode bytes"
|
|
assert_contains_output(sourcePath, (decoding_error, ))
|
|
|
|
|
|
def test_permissionDenied():
|
|
"""If the source file is not readable, this is reported on standard error."""
|
|
sourcePath = make_temp_file('')
|
|
os.chmod(sourcePath, 0)
|
|
count, errors = get_errors(sourcePath)
|
|
assert count == 1
|
|
assert errors == [('unexpected_error', sourcePath, "Permission denied")]
|
|
|
|
|
|
def test_frostedWarning():
|
|
"""If the source file has a frosted warning, this is reported as a 'flake'."""
|
|
sourcePath = make_temp_file("import foo")
|
|
count, errors = get_errors(sourcePath)
|
|
assert count == 1
|
|
assert errors == [('flake', str(UnusedImport(sourcePath, Node(1), 'foo')))]
|
|
|
|
|
|
@pytest.mark.skipif("PY3")
|
|
def test_misencodedFileUTF8():
|
|
"""If a source file contains bytes which cannot be decoded, this is reported on stderr."""
|
|
SNOWMAN = chr(0x2603)
|
|
source = ("""\
|
|
# coding: ascii
|
|
x = "%s"
|
|
""" % SNOWMAN).encode('utf-8')
|
|
sourcePath = make_temp_file(source)
|
|
assert_contains_output(sourcePath, ["%s: problem decoding source\n" % (sourcePath, )])
|
|
|
|
|
|
def test_misencodedFileUTF16():
|
|
"""If a source file contains bytes which cannot be decoded, this is reported on stderr."""
|
|
SNOWMAN = chr(0x2603)
|
|
source = ("""\
|
|
# coding: ascii
|
|
x = "%s"
|
|
""" % SNOWMAN).encode('utf-16')
|
|
sourcePath = make_temp_file(source)
|
|
assert_contains_output(sourcePath, ["%s: problem decoding source\n" % (sourcePath,)])
|
|
|
|
|
|
def test_check_recursive():
|
|
"""check_recursive descends into each directory, finding Python files and reporting problems."""
|
|
tempdir = tempfile.mkdtemp()
|
|
os.mkdir(os.path.join(tempdir, 'foo'))
|
|
file1 = os.path.join(tempdir, 'foo', 'bar.py')
|
|
fd = open(file1, 'wb')
|
|
fd.write("import baz\n".encode('ascii'))
|
|
fd.close()
|
|
file2 = os.path.join(tempdir, 'baz.py')
|
|
fd = open(file2, 'wb')
|
|
fd.write("import contraband".encode('ascii'))
|
|
fd.close()
|
|
log = []
|
|
reporter = LoggingReporter(log)
|
|
warnings = check_recursive([tempdir], reporter)
|
|
assert warnings == 2
|
|
assert sorted(log) == sorted([('flake', str(UnusedImport(file1, Node(1), 'baz'))),
|
|
('flake', str(UnusedImport(file2, Node(1), 'contraband')))])
|