-
-
Notifications
You must be signed in to change notification settings - Fork 34
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #72 from jitseniesen/unittest
Add support for unittest
- Loading branch information
Showing
4 changed files
with
297 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,105 @@ | ||
# -*- coding: utf-8 -*- | ||
# | ||
# Copyright © 2017 Spyder Project Contributors | ||
# Licensed under the terms of the MIT License | ||
# (see LICENSE.txt for details) | ||
"""Tests for unittestrunner.py""" | ||
|
||
# Local imports | ||
from spyder_unittest.backend.runnerbase import Category | ||
from spyder_unittest.backend.unittestrunner import UnittestRunner | ||
|
||
|
||
def test_unittestrunner_load_data(): | ||
output = """test_isupper (teststringmethods.TestStringMethods) ... ok | ||
test_split (teststringmethods.TestStringMethods) ... ok | ||
extra text\n""" | ||
runner = UnittestRunner(None) | ||
res = runner.load_data(output) | ||
assert len(res) == 2 | ||
|
||
assert res[0].category == Category.OK | ||
assert res[0].status == 'ok' | ||
assert res[0].name == 'teststringmethods.TestStringMethods.test_isupper' | ||
assert res[0].message == '' | ||
assert res[0].extra_text == '' | ||
|
||
assert res[1].category == Category.OK | ||
assert res[1].status == 'ok' | ||
assert res[1].name == 'teststringmethods.TestStringMethods.test_split' | ||
assert res[1].message == '' | ||
assert res[1].extra_text == 'extra text\n' | ||
|
||
|
||
def test_unittestrunner_load_data_removes_footer(): | ||
output = """test1 (test_foo.Bar) ... ok | ||
---------------------------------------------------------------------- | ||
Ran 1 test in 0.000s | ||
OK | ||
""" | ||
runner = UnittestRunner(None) | ||
res = runner.load_data(output) | ||
assert len(res) == 1 | ||
assert res[0].category == Category.OK | ||
assert res[0].status == 'ok' | ||
assert res[0].name == 'test_foo.Bar.test1' | ||
assert res[0].extra_text == '' | ||
|
||
|
||
def test_unittestrunner_load_data_with_exception(): | ||
output = """test1 (test_foo.Bar) ... FAIL | ||
test2 (test_foo.Bar) ... ok | ||
====================================================================== | ||
FAIL: test1 (test_foo.Bar) | ||
---------------------------------------------------------------------- | ||
Traceback (most recent call last): | ||
File "/somepath/test_foo.py", line 5, in test1 | ||
self.assertEqual(1, 2) | ||
AssertionError: 1 != 2 | ||
""" | ||
runner = UnittestRunner(None) | ||
res = runner.load_data(output) | ||
assert len(res) == 2 | ||
|
||
assert res[0].category == Category.FAIL | ||
assert res[0].status == 'FAIL' | ||
assert res[0].name == 'test_foo.Bar.test1' | ||
assert res[0].extra_text.startswith('Traceback') | ||
assert res[0].extra_text.endswith('AssertionError: 1 != 2\n') | ||
|
||
assert res[1].category == Category.OK | ||
assert res[1].status == 'ok' | ||
assert res[1].name == 'test_foo.Bar.test2' | ||
assert res[1].extra_text == '' | ||
|
||
|
||
def test_try_parse_header_with_ok(): | ||
runner = UnittestRunner(None) | ||
text = 'test_isupper (testfoo.TestStringMethods) ... ok' | ||
res = runner.try_parse_result(text) | ||
assert res == ('test_isupper', 'testfoo.TestStringMethods', 'ok', '') | ||
|
||
|
||
def test_try_parse_header_with_xfail(): | ||
runner = UnittestRunner(None) | ||
text = 'test_isupper (testfoo.TestStringMethods) ... expected failure' | ||
res = runner.try_parse_result(text) | ||
assert res == ('test_isupper', 'testfoo.TestStringMethods', | ||
'expected failure', '') | ||
|
||
|
||
def test_try_parse_header_with_message(): | ||
runner = UnittestRunner(None) | ||
text = "test_nothing (testfoo.Tests) ... skipped 'msg'" | ||
res = runner.try_parse_result(text) | ||
assert res == ('test_nothing', 'testfoo.Tests', 'skipped', 'msg') | ||
|
||
|
||
def test_try_parse_header_starting_with_digit(): | ||
runner = UnittestRunner(None) | ||
text = '0est_isupper (testfoo.TestStringMethods) ... ok' | ||
res = runner.try_parse_result(text) | ||
assert res is None |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,152 @@ | ||
# -*- coding: utf-8 -*- | ||
# | ||
# Copyright © 2017 Spyder Project Contributors | ||
# Licensed under the terms of the MIT License | ||
# (see LICENSE.txt for details) | ||
"""Support for unittest framework.""" | ||
|
||
# Standard library imports | ||
import re | ||
|
||
# Third party imports | ||
from qtpy.QtCore import QTextCodec | ||
from spyder.py3compat import to_text_string | ||
|
||
# Local imports | ||
from spyder_unittest.backend.runnerbase import Category, RunnerBase, TestResult | ||
|
||
|
||
class UnittestRunner(RunnerBase): | ||
"""Class for running tests with unittest module in standard library.""" | ||
|
||
executable = 'python' | ||
|
||
def create_argument_list(self): | ||
"""Create argument list for testing process.""" | ||
return ['-m', 'unittest', 'discover', '-v'] | ||
|
||
def finished(self): | ||
""" | ||
Called when the unit test process has finished. | ||
This function reads the results and emits `sig_finished`. | ||
""" | ||
qbytearray = self.process.readAllStandardOutput() | ||
locale_codec = QTextCodec.codecForLocale() | ||
output = to_text_string(locale_codec.toUnicode(qbytearray.data())) | ||
testresults = self.load_data(output) # overrides base class method | ||
self.sig_finished.emit(testresults, output) | ||
|
||
def load_data(self, output): | ||
""" | ||
Read and parse output from unittest module. | ||
Returns | ||
------- | ||
list of TestResult | ||
Unit test results. | ||
""" | ||
res = [] | ||
lines = output.splitlines() | ||
line_index = 0 | ||
test_index = None | ||
|
||
while line_index < len(lines): | ||
data = self.try_parse_result(lines[line_index]) | ||
if data: | ||
name = data[1] + '.' + data[0] | ||
if data[2] == 'ok': | ||
cat = Category.OK | ||
elif data[2] == 'FAIL' or data[2] == 'ERROR': | ||
cat = Category.FAIL | ||
else: | ||
cat = Category.SKIP | ||
tr = TestResult(cat, data[2], name, data[3], 0, '') | ||
res.append(tr) | ||
line_index += 1 | ||
test_index = -1 | ||
continue | ||
|
||
data = self.try_parse_exception_header(lines, line_index) | ||
if data: | ||
line_index = data[0] | ||
name = data[2] + '.' + data[1] | ||
test_index = next(i for i, tr in enumerate(res) | ||
if tr.name == name) | ||
|
||
data = self.try_parse_footer(lines, line_index) | ||
if data: | ||
line_index = data | ||
test_index = -1 | ||
continue | ||
|
||
if test_index is not None: | ||
text = res[test_index].extra_text + lines[line_index] + '\n' | ||
res[test_index] = res[test_index]._replace(extra_text=text) | ||
line_index += 1 | ||
|
||
return res | ||
|
||
def try_parse_result(self, line): | ||
""" | ||
Try to parse a line of text as a test result. | ||
Returns | ||
------- | ||
tuple of str or None | ||
If line represents a test result, then return a tuple with four | ||
strings: the name of the test function, the name of the test class, | ||
the test result, and the reason (if no reason is given, the fourth | ||
string is empty). Otherwise, return None. | ||
""" | ||
regexp = (r'([^\d\W]\w*) \(([^\d\W][\w.]*)\) \.\.\. ' | ||
'(ok|FAIL|ERROR|skipped|expected failure|unexpected success)' | ||
"( '([^']*)')?\Z") | ||
match = re.match(regexp, line) | ||
if match: | ||
msg = match.groups()[4] or '' | ||
return match.groups()[:3] + (msg, ) | ||
else: | ||
return None | ||
|
||
def try_parse_exception_header(self, lines, line_index): | ||
""" | ||
Try to parse the header of an exception in unittest output. | ||
Returns | ||
------- | ||
(int, str, str) or None | ||
If an exception header is parsed successfully, then return a tuple | ||
with the new line index, the name of the test function, and the | ||
name of the test class. Otherwise, return None. | ||
""" | ||
if lines[line_index] != '': | ||
return None | ||
if not all(char == '=' for char in lines[line_index + 1]): | ||
return None | ||
regexp = r'\w+: ([^\d\W]\w*) \(([^\d\W][\w.]*)\)\Z' | ||
match = re.match(regexp, lines[line_index + 2]) | ||
if not match: | ||
return None | ||
if not all(char == '-' for char in lines[line_index + 3]): | ||
return None | ||
return (line_index + 4, ) + match.groups() | ||
|
||
def try_parse_footer(self, lines, line_index): | ||
""" | ||
Try to parse footer of unittest output. | ||
Returns | ||
------- | ||
int or None | ||
New line index if footer is parsed successfully, None otherwise | ||
""" | ||
if lines[line_index] != '': | ||
return None | ||
if not all(char == '-' for char in lines[line_index + 1]): | ||
return None | ||
if not re.match(r'^Ran [\d]+ tests? in', lines[line_index + 2]): | ||
return None | ||
if lines[line_index + 3] != '': | ||
return None | ||
return line_index + 5 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters