spyder_unittest-0.4.1/ 0000755 0001750 0001750 00000000000 13662215740 015245 5 ustar jitse jitse 0000000 0000000 spyder_unittest-0.4.1/CHANGELOG.md 0000644 0001750 0001750 00000036721 13662215653 017072 0 ustar jitse jitse 0000000 0000000 # History of changes
## Version 0.4.1 (2020/05/23)
This release fixes several bugs and other issues, allowing the plugin to be
used with Spyder 4.1. This release can not be used with Python 2.
### Issues Closed
* [Issue 154](https://github.com/spyder-ide/spyder-unittest/issues/154) - Make plugin depend on Python 3 ([PR 155](https://github.com/spyder-ide/spyder-unittest/pull/155))
* [Issue 145](https://github.com/spyder-ide/spyder-unittest/issues/145) - Go to test definition only works when run from root dir ([PR 149](https://github.com/spyder-ide/spyder-unittest/pull/149))
* [Issue 138](https://github.com/spyder-ide/spyder-unittest/issues/138) - Move CI to github actions ([PR 143](https://github.com/spyder-ide/spyder-unittest/pull/143))
* [Issue 127](https://github.com/spyder-ide/spyder-unittest/issues/127) - Teardown function's logs not captured ([PR 151](https://github.com/spyder-ide/spyder-unittest/pull/151))
* [Issue 115](https://github.com/spyder-ide/spyder-unittest/issues/115) - Report pytest plugins used while running a test suite ([PR 146](https://github.com/spyder-ide/spyder-unittest/pull/146))
* [Issue 47](https://github.com/spyder-ide/spyder-unittest/issues/47) - pytest statuses "expected-fail" and "unexpectedly passing" not yet reflected in Category ([PR 151](https://github.com/spyder-ide/spyder-unittest/pull/151))
In this release 6 issues were closed.
### Pull Requests Merged
* [PR 155](https://github.com/spyder-ide/spyder-unittest/pull/155) - PR: Require Python 3.5 or later ([154](https://github.com/spyder-ide/spyder-unittest/issues/154))
* [PR 153](https://github.com/spyder-ide/spyder-unittest/pull/153) - Fix tests that could never fail
* [PR 152](https://github.com/spyder-ide/spyder-unittest/pull/152) - Fix pytest output processing
* [PR 151](https://github.com/spyder-ide/spyder-unittest/pull/151) - Fix pytest backend ([47](https://github.com/spyder-ide/spyder-unittest/issues/47), [127](https://github.com/spyder-ide/spyder-unittest/issues/127))
* [PR 150](https://github.com/spyder-ide/spyder-unittest/pull/150) - Fix test_pytestrunner_start
* [PR 149](https://github.com/spyder-ide/spyder-unittest/pull/149) - Fix pytest test filename path resolution ([145](https://github.com/spyder-ide/spyder-unittest/issues/145))
* [PR 148](https://github.com/spyder-ide/spyder-unittest/pull/148) - Use set_status_label function
* [PR 147](https://github.com/spyder-ide/spyder-unittest/pull/147) - Fix abbreviator if name has parameters with dots
* [PR 146](https://github.com/spyder-ide/spyder-unittest/pull/146) - Show version info of test installed frameworks and their plugins ([115](https://github.com/spyder-ide/spyder-unittest/issues/115))
* [PR 144](https://github.com/spyder-ide/spyder-unittest/pull/144) - Dynamic sizing of text editor window ([12202](https://github.com/spyder-ide/spyder/issues/12202))
* [PR 143](https://github.com/spyder-ide/spyder-unittest/pull/143) - Move CI to GitHub actions ([138](https://github.com/spyder-ide/spyder-unittest/issues/138))
* [PR 141](https://github.com/spyder-ide/spyder-unittest/pull/141) - Fix status label
* [PR 139](https://github.com/spyder-ide/spyder-unittest/pull/139) - Fix TextEditor constructor
In this release 13 pull requests were closed.
## Version 0.4.0 (2020/01/07)
This release updates the plugin to be used with Spyder 4 and fixes some bugs.
### Issues Closed
* [Issue 133](https://github.com/spyder-ide/spyder-unittest/issues/133) - Colours make text hard to read when run in dark mode ([PR 135](https://github.com/spyder-ide/spyder-unittest/pull/135))
* [Issue 129](https://github.com/spyder-ide/spyder-unittest/issues/129) - Docstrings in test functions confuse unittest's output parser ([PR 134](https://github.com/spyder-ide/spyder-unittest/pull/134))
* [Issue 128](https://github.com/spyder-ide/spyder-unittest/issues/128) - KeyError: 'test not found' ([PR 132](https://github.com/spyder-ide/spyder-unittest/pull/132))
In this release 3 issues were closed.
### Pull Requests Merged
* [PR 135](https://github.com/spyder-ide/spyder-unittest/pull/135) - PR: Use appropriate colours when Spyder is in dark mode ([133](https://github.com/spyder-ide/spyder-unittest/issues/133))
* [PR 134](https://github.com/spyder-ide/spyder-unittest/pull/134) - PR: Allow for unittest tests to have docstrings ([129](https://github.com/spyder-ide/spyder-unittest/issues/129))
* [PR 132](https://github.com/spyder-ide/spyder-unittest/pull/132) - PR: Use nodeid provided by pytest in itemcollected hook ([128](https://github.com/spyder-ide/spyder-unittest/issues/128))
* [PR 131](https://github.com/spyder-ide/spyder-unittest/pull/131) - PR: Compatibility fixes for Spyder 4
In this release 4 pull requests were closed.
## Version 0.3.1 (2018/06/15)
This version fixes some bugs and also includes some cosmetic changes.
### Issues Closed
* [Issue 117](https://github.com/spyder-ide/spyder-unittest/issues/117) - Rename "py.test" to "pytest" throughout ([PR 119](https://github.com/spyder-ide/spyder-unittest/pull/119))
* [Issue 113](https://github.com/spyder-ide/spyder-unittest/issues/113) - NameError in test file causes internal error ([PR 118](https://github.com/spyder-ide/spyder-unittest/pull/118))
* [Issue 112](https://github.com/spyder-ide/spyder-unittest/issues/112) - Plugin confused by tests writing to `sys.__stdout__` ([PR 114](https://github.com/spyder-ide/spyder-unittest/pull/114))
In this release 3 issues were closed.
### Pull Requests Merged
* [PR 121](https://github.com/spyder-ide/spyder-unittest/pull/121) - PR: Update readme to remove funding appeal, harmonize with other readmes and minor fixes
* [PR 120](https://github.com/spyder-ide/spyder-unittest/pull/120) - Remove unused variables when initializing localization
* [PR 119](https://github.com/spyder-ide/spyder-unittest/pull/119) - Replace 'py.test' by 'pytest' ([117](https://github.com/spyder-ide/spyder-unittest/issues/117))
* [PR 118](https://github.com/spyder-ide/spyder-unittest/pull/118) - Use str() to convert pytest's longrepr to a string ([113](https://github.com/spyder-ide/spyder-unittest/issues/113))
* [PR 114](https://github.com/spyder-ide/spyder-unittest/pull/114) - Use ZMQ sockets to communicate results of pytest run ([112](https://github.com/spyder-ide/spyder-unittest/issues/112))
In this release 5 pull requests were closed.
## Version 0.3.0 (2018/02/16)
This version includes improved support of `py.test` (test results are displayed as they come in, double clicking on a test result opens the test in the editor) as well as various other improvements.
### Issues Closed
* [Issue 106](https://github.com/spyder-ide/spyder-unittest/issues/106) - After sorting, test details are lost ([PR 110](https://github.com/spyder-ide/spyder-unittest/pull/110))
* [Issue 103](https://github.com/spyder-ide/spyder-unittest/issues/103) - "Go to" not working unless working directory is correctly set ([PR 109](https://github.com/spyder-ide/spyder-unittest/pull/109))
* [Issue 98](https://github.com/spyder-ide/spyder-unittest/issues/98) - Running unittest tests within py.test results in error ([PR 102](https://github.com/spyder-ide/spyder-unittest/pull/102))
* [Issue 96](https://github.com/spyder-ide/spyder-unittest/issues/96) - Use new colors for passed and failed tests ([PR 108](https://github.com/spyder-ide/spyder-unittest/pull/108))
* [Issue 94](https://github.com/spyder-ide/spyder-unittest/issues/94) - Enable sorting in table of test results ([PR 104](https://github.com/spyder-ide/spyder-unittest/pull/104))
* [Issue 93](https://github.com/spyder-ide/spyder-unittest/issues/93) - Handle errors in py.test's collection phase ([PR 99](https://github.com/spyder-ide/spyder-unittest/pull/99))
* [Issue 92](https://github.com/spyder-ide/spyder-unittest/issues/92) - Retitle "Kill" (tests) button to "Stop" ([PR 107](https://github.com/spyder-ide/spyder-unittest/pull/107))
* [Issue 89](https://github.com/spyder-ide/spyder-unittest/issues/89) - Write tests for UnitTestPlugin ([PR 95](https://github.com/spyder-ide/spyder-unittest/pull/95))
* [Issue 87](https://github.com/spyder-ide/spyder-unittest/issues/87) - Don't display test time when using unittest ([PR 105](https://github.com/spyder-ide/spyder-unittest/pull/105))
* [Issue 86](https://github.com/spyder-ide/spyder-unittest/issues/86) - Use sensible precision when displaying test times ([PR 105](https://github.com/spyder-ide/spyder-unittest/pull/105))
* [Issue 83](https://github.com/spyder-ide/spyder-unittest/issues/83) - Changes for compatibility with new undocking behavior of Spyder ([PR 84](https://github.com/spyder-ide/spyder-unittest/pull/84))
* [Issue 77](https://github.com/spyder-ide/spyder-unittest/issues/77) - Be smarter about abbreviating test names
* [Issue 71](https://github.com/spyder-ide/spyder-unittest/issues/71) - Save before running tests (?) ([PR 101](https://github.com/spyder-ide/spyder-unittest/pull/101))
* [Issue 50](https://github.com/spyder-ide/spyder-unittest/issues/50) - Use py.test's API to run tests ([PR 91](https://github.com/spyder-ide/spyder-unittest/pull/91))
* [Issue 43](https://github.com/spyder-ide/spyder-unittest/issues/43) - Save selected test framework ([PR 90](https://github.com/spyder-ide/spyder-unittest/pull/90))
* [Issue 31](https://github.com/spyder-ide/spyder-unittest/issues/31) - Add issues/PRs templates ([PR 111](https://github.com/spyder-ide/spyder-unittest/pull/111))
* [Issue 13](https://github.com/spyder-ide/spyder-unittest/issues/13) - Display test results as they come in ([PR 91](https://github.com/spyder-ide/spyder-unittest/pull/91))
* [Issue 12](https://github.com/spyder-ide/spyder-unittest/issues/12) - Double clicking on test name should take you somewhere useful ([PR 100](https://github.com/spyder-ide/spyder-unittest/pull/100))
In this release 18 issues were closed.
### Pull Requests Merged
* [PR 111](https://github.com/spyder-ide/spyder-unittest/pull/111) - Update docs for new release ([31](https://github.com/spyder-ide/spyder-unittest/issues/31))
* [PR 110](https://github.com/spyder-ide/spyder-unittest/pull/110) - Emit modelReset after sorting test results ([106](https://github.com/spyder-ide/spyder-unittest/issues/106))
* [PR 109](https://github.com/spyder-ide/spyder-unittest/pull/109) - Store full path to file containing test in TestResult ([103](https://github.com/spyder-ide/spyder-unittest/issues/103))
* [PR 108](https://github.com/spyder-ide/spyder-unittest/pull/108) - Use paler shade of red as background for failing tests ([96](https://github.com/spyder-ide/spyder-unittest/issues/96))
* [PR 107](https://github.com/spyder-ide/spyder-unittest/pull/107) - Relabel 'Kill' button ([92](https://github.com/spyder-ide/spyder-unittest/issues/92))
* [PR 105](https://github.com/spyder-ide/spyder-unittest/pull/105) - Improve display of test times ([87](https://github.com/spyder-ide/spyder-unittest/issues/87), [86](https://github.com/spyder-ide/spyder-unittest/issues/86))
* [PR 104](https://github.com/spyder-ide/spyder-unittest/pull/104) - Allow user to sort tests ([94](https://github.com/spyder-ide/spyder-unittest/issues/94))
* [PR 102](https://github.com/spyder-ide/spyder-unittest/pull/102) - Use nodeid when collecting tests using py.test ([98](https://github.com/spyder-ide/spyder-unittest/issues/98))
* [PR 101](https://github.com/spyder-ide/spyder-unittest/pull/101) - Save all files before running tests ([71](https://github.com/spyder-ide/spyder-unittest/issues/71))
* [PR 100](https://github.com/spyder-ide/spyder-unittest/pull/100) - Implement go to test definition for py.test ([12](https://github.com/spyder-ide/spyder-unittest/issues/12))
* [PR 99](https://github.com/spyder-ide/spyder-unittest/pull/99) - Handle errors encountered when py.test collect tests ([93](https://github.com/spyder-ide/spyder-unittest/issues/93))
* [PR 97](https://github.com/spyder-ide/spyder-unittest/pull/97) - Abbreviate module names when displaying test names
* [PR 95](https://github.com/spyder-ide/spyder-unittest/pull/95) - Add unit tests for plugin ([89](https://github.com/spyder-ide/spyder-unittest/issues/89))
* [PR 91](https://github.com/spyder-ide/spyder-unittest/pull/91) - Display py.test results as they come in ([50](https://github.com/spyder-ide/spyder-unittest/issues/50), [13](https://github.com/spyder-ide/spyder-unittest/issues/13))
* [PR 90](https://github.com/spyder-ide/spyder-unittest/pull/90) - Load and save configuration for tests ([43](https://github.com/spyder-ide/spyder-unittest/issues/43))
* [PR 85](https://github.com/spyder-ide/spyder-unittest/pull/85) - Remove PySide from CI scripts and remove Scrutinizer
* [PR 84](https://github.com/spyder-ide/spyder-unittest/pull/84) - PR: Show undock action ([83](https://github.com/spyder-ide/spyder-unittest/issues/83))
In this release 17 pull requests were closed.
## Version 0.2.0 (2017/08/20)
The main change in this version is that it adds support for tests written
using the `unittest` framework available in the standard Python library.
### Issues Closed
* [Issue 79](https://github.com/spyder-ide/spyder-unittest/issues/79) - Remove QuantifiedCode
* [Issue 74](https://github.com/spyder-ide/spyder-unittest/issues/74) - Also test against spyder's master branch in CI
* [Issue 70](https://github.com/spyder-ide/spyder-unittest/issues/70) - Point contributors to ciocheck
* [Issue 41](https://github.com/spyder-ide/spyder-unittest/issues/41) - Add function for registering test frameworks
* [Issue 15](https://github.com/spyder-ide/spyder-unittest/issues/15) - Check whether test framework is installed
* [Issue 11](https://github.com/spyder-ide/spyder-unittest/issues/11) - Abbreviate test names
* [Issue 4](https://github.com/spyder-ide/spyder-unittest/issues/4) - Add unittest support
In this release 7 issues were closed.
### Pull Requests Merged
* [PR 82](https://github.com/spyder-ide/spyder-unittest/pull/82) - Enable Scrutinizer
* [PR 81](https://github.com/spyder-ide/spyder-unittest/pull/81) - Update README.md
* [PR 80](https://github.com/spyder-ide/spyder-unittest/pull/80) - Install Spyder from github 3.x branch when testing on Circle
* [PR 78](https://github.com/spyder-ide/spyder-unittest/pull/78) - Properly handle test frameworks which are not installed
* [PR 75](https://github.com/spyder-ide/spyder-unittest/pull/75) - Shorten test name displayed in widget
* [PR 72](https://github.com/spyder-ide/spyder-unittest/pull/72) - Support unittest
* [PR 69](https://github.com/spyder-ide/spyder-unittest/pull/69) - Process coverage stats using coveralls
* [PR 68](https://github.com/spyder-ide/spyder-unittest/pull/68) - Add framework registry for associating testing frameworks with runners
* [PR 67](https://github.com/spyder-ide/spyder-unittest/pull/67) - Install the tests alongside the module
In this release 9 pull requests were closed.
## Version 0.1.2 (2017/03/04)
This version fixes a bug in the packaging code.
### Pull Requests Merged
* [PR 63](https://github.com/spyder-ide/spyder-unittest/pull/63) - Fix parsing of module version
In this release 1 pull request was closed.
## Version 0.1.1 (2017/02/11)
This version improves the packaging. The code itself was not changed.
### Issues Closed
* [Issue 58](https://github.com/spyder-ide/spyder-unittest/issues/58) - Normalized copyright information
* [Issue 57](https://github.com/spyder-ide/spyder-unittest/issues/57) - Depend on nose and pytest at installation
* [Issue 56](https://github.com/spyder-ide/spyder-unittest/issues/56) - Add the test suite to the release tarball
In this release 3 issues were closed.
### Pull Requests Merged
* [PR 59](https://github.com/spyder-ide/spyder-unittest/pull/59) - Improve distributed package
In this release 1 pull request was closed.
## Version 0.1.0 (2017/02/05)
Initial release, supporting nose and py.test frameworks.
spyder_unittest-0.4.1/LICENSE.txt 0000644 0001750 0001750 00000002105 13047645510 017065 0 ustar jitse jitse 0000000 0000000 The MIT License (MIT)
Copyright © 2013 Spyder Project Contributors
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.
spyder_unittest-0.4.1/MANIFEST.in 0000644 0001750 0001750 00000000142 13047650733 017002 0 ustar jitse jitse 0000000 0000000 include CHANGELOG.md
include LICENSE.txt
include README.md
recursive-include spyder_unittest *.py
spyder_unittest-0.4.1/PKG-INFO 0000644 0001750 0001750 00000002102 13662215740 016335 0 ustar jitse jitse 0000000 0000000 Metadata-Version: 1.2
Name: spyder_unittest
Version: 0.4.1
Summary: Plugin to run tests from within the Spyder IDE
Home-page: https://github.com/spyder-ide/spyder-unittest
Author: Spyder Project Contributors
License: MIT
Description:
This is a plugin for the Spyder IDE that integrates popular unit test
frameworks. It allows you to run tests and view the results.
The plugin supports the `unittest` framework in the Python
standard library and the `pytest` and `nose` testing frameworks.
Keywords: Qt PyQt4 PyQt5 spyder plugins testing
Platform: UNKNOWN
Classifier: Development Status :: 4 - Beta
Classifier: Environment :: X11 Applications :: Qt
Classifier: Environment :: Win32 (MS Windows)
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Topic :: Software Development :: Testing
Classifier: Topic :: Text Editors :: Integrated Development Environments (IDE)
Requires-Python: >=3.5
spyder_unittest-0.4.1/README.md 0000644 0001750 0001750 00000013202 13644304544 016523 0 ustar jitse jitse 0000000 0000000 # Spyder-Unittest
## Project information
[](./LICENSE)
[](https://www.anaconda.com/download/)
[](https://www.anaconda.com/download/)
[](https://pypi.org/project/spyder-unittest/)
[](https://gitter.im/spyder-ide/public)
[](#backers)
[](#sponsors)
## Build status
[](https://github.com/spyder-ide/spyder-notebook/actions?query=workflow%3A%22Windows+tests%22)
[](https://github.com/spyder-ide/spyder-notebook/actions?query=workflow%3A%22Linux+tests%22)
[](https://github.com/spyder-ide/spyder-notebook/actions?query=workflow%3A%22Macos+tests%22)
[](https://codecov.io/gh/spyder-ide/spyder-notebook/branch/master)
[](https://crowdin.com/project/spyder-unittest)
*Copyright © 2014 Spyder Project Contributors*

## Description
Spyder-unittest is a plugin that integrates popular unit test frameworks
with Spyder, allowing you to run test suites and view the results in the IDE.
The plugin supports the `unittest` module in the Python standard library
as well as the `pytest` and `nose` testing frameworks.
Support for `pytest` is most complete at the moment.
## Installation
The unittest plugin is available in the `spyder-ide` channel in Anaconda and in PyPI,
so it can be installed with the following commands:
* Using Anaconda: `conda install -c spyder-ide spyder-unittest`
* Using pip: `pip install spyder-unittest`
All dependencies will be automatically installed.
You have to restart Spyder before you can use the plugin.
## Usage
The plugin adds an item `Run unit tests` to the `Run` menu in Spyder.
Click on this to run the unit tests. After you specify the testing framework
and the directory under which the tests are stored, the tests are run.
The `Unit testing` window pane (displayed at the top of this file) will pop up
with the results. If you are using `pytest`, you can double-click on a test
to view it in the editor.
If you want to run tests in a different directory or switch testing
frameworks, click `Configure` in the Options menu (cogwheel icon),
which is located in the upper right corner of the `Unit testing` pane.
## Feedback
Bug reports, feature requests and other ideas are more than welcome on the
[issue tracker](https://github.com/spyder-ide/spyder-unittest/issues).
Use the [Spyder Google Group](https://groups.google.com/group/spyderlib)
or our [Gitter Chatroom](https://gitter.im/spyder-ide/public)
for general discussion.
## Development
Development of the plugin is done at https://github.com/spyder-ide/spyder-unittest .
You can install the development version of the plugin by cloning the git repository
and running `pip install .`, possibly with the `--editable` flag.
The plugin has the following dependencies:
* [spyder](https://github.com/spyder-ide/spyder) (obviously), at least version 4.0
* [lxml](http://lxml.de/)
* the testing framework that you will be using: [pytest](https://pytest.org)
and/or [nose](https://nose.readthedocs.io)
In order to run the tests distributed with this plugin, you need
[nose](https://nose.readthedocs.io), [pytest](https://pytest.org)
and [pytest-qt](https://github.com/pytest-dev/pytest-qt). If you use Python 2,
you also need [mock](https://github.com/testing-cabal/mock).
You are very welcome to submit code contributions in the form of pull
requests to the
[issue tracker](https://github.com/spyder-ide/spyder-unittest/issues).
GitHub is configured to run pull requests automatically against the test suite
and against several automatic style checkers using
[ciocheck](https://github.com/ContinuumIO/ciocheck).
The style checkers can be rather finicky so you may want to install ciocheck
locally and run them before submitting the code.
## Contributing
Everyone is welcome to contribute! The document [Contributing to Spyder](
https://github.com/spyder-ide/spyder/blob/master/CONTRIBUTING.md)
also applies to the unittest plugin.
We are grateful to the entire Spyder community for their support, without which
this plugin and the whole of Spyder would be a lot less awesome.
## More information
[Main Website](https://www.spyder-ide.org/)
[Download Spyder (with Anaconda)](https://www.anaconda.com/download/)
[Spyder Github](https://github.com/spyder-ide/spyder)
[Troubleshooting Guide and FAQ](
https://github.com/spyder-ide/spyder/wiki/Troubleshooting-Guide-and-FAQ)
[Development Wiki](https://github.com/spyder-ide/spyder/wiki/Dev:-Index)
[Gitter Chatroom](https://gitter.im/spyder-ide/public)
[Google Group](https://groups.google.com/group/spyderlib)
[@Spyder_IDE on Twitter](https://twitter.com/spyder_ide)
[@SpyderIDE on Facebook](https://www.facebook.com/SpyderIDE/)
[Support Spyder on OpenCollective](https://opencollective.com/spyder/)
spyder_unittest-0.4.1/setup.cfg 0000644 0001750 0001750 00000000107 13662215740 017064 0 ustar jitse jitse 0000000 0000000 [tool:pytest]
python_classes =
[egg_info]
tag_build =
tag_date = 0
spyder_unittest-0.4.1/setup.py 0000644 0001750 0001750 00000004774 13662040473 016772 0 ustar jitse jitse 0000000 0000000 # -*- coding: utf-8 -*-
#
# Copyright © 2013 Spyder Project Contributors
# Licensed under the terms of the MIT License
# (see LICENSE.txt for details)
"""
Setup script for spyder_unittest
"""
from setuptools import setup, find_packages
import os
import os.path as osp
def get_version():
"""Get version from source file"""
import codecs
with codecs.open("spyder_unittest/__init__.py", encoding="utf-8") as f:
lines = f.read().splitlines()
for l in lines:
if "__version__" in l:
version = l.split("=")[1].strip()
version = version.replace("'", '').replace('"', '')
return version
def get_package_data(name, extlist):
"""Return data files for package *name* with extensions in *extlist*"""
flist = []
# Workaround to replace os.path.relpath (not available until Python 2.6):
offset = len(name) + len(os.pathsep)
for dirpath, _dirnames, filenames in os.walk(name):
for fname in filenames:
if not fname.startswith('.') and osp.splitext(fname)[1] in extlist:
flist.append(osp.join(dirpath, fname)[offset:])
return flist
# Requirements
REQUIREMENTS = ['lxml', 'spyder>=3', 'pyzmq']
EXTLIST = ['.jpg', '.png', '.json', '.mo', '.ini']
LIBNAME = 'spyder_unittest'
LONG_DESCRIPTION = """
This is a plugin for the Spyder IDE that integrates popular unit test
frameworks. It allows you to run tests and view the results.
The plugin supports the `unittest` framework in the Python
standard library and the `pytest` and `nose` testing frameworks.
"""
setup(
name=LIBNAME,
version=get_version(),
packages=find_packages(),
package_data={LIBNAME: get_package_data(LIBNAME, EXTLIST)},
keywords=["Qt PyQt4 PyQt5 spyder plugins testing"],
python_requires='>=3.5',
install_requires=REQUIREMENTS,
url='https://github.com/spyder-ide/spyder-unittest',
license='MIT',
author="Spyder Project Contributors",
description='Plugin to run tests from within the Spyder IDE',
long_description=LONG_DESCRIPTION,
classifiers=[
'Development Status :: 4 - Beta',
'Environment :: X11 Applications :: Qt',
'Environment :: Win32 (MS Windows)',
'Intended Audience :: Developers',
'License :: OSI Approved :: MIT License',
'Operating System :: OS Independent',
'Programming Language :: Python :: 3',
'Topic :: Software Development :: Testing',
'Topic :: Text Editors :: Integrated Development Environments (IDE)'])
spyder_unittest-0.4.1/spyder_unittest/ 0000755 0001750 0001750 00000000000 13662215740 020512 5 ustar jitse jitse 0000000 0000000 spyder_unittest-0.4.1/spyder_unittest/__init__.py 0000644 0001750 0001750 00000000445 13662215653 022631 0 ustar jitse jitse 0000000 0000000 # -*- coding: utf-8 -*-
#
# Copyright © 2013 Spyder Project Contributors
# Licensed under the terms of the MIT License
# (see LICENSE.txt for details)
"""Spyder unitest plugin."""
# Local imports
from .unittestplugin import UnitTestPlugin as PLUGIN_CLASS
__version__ = '0.4.1'
PLUGIN_CLASS
spyder_unittest-0.4.1/spyder_unittest/backend/ 0000755 0001750 0001750 00000000000 13662215740 022101 5 ustar jitse jitse 0000000 0000000 spyder_unittest-0.4.1/spyder_unittest/backend/__init__.py 0000644 0001750 0001750 00000000334 13047645510 024211 0 ustar jitse jitse 0000000 0000000 # -*- coding: utf-8 -*-
#
# Copyright © 2013 Spyder Project Contributors
# Licensed under the terms of the MIT License
# (see LICENSE.txt for details)
"""Parts of the unittest plugin that are not related to the GUI."""
spyder_unittest-0.4.1/spyder_unittest/backend/abbreviator.py 0000644 0001750 0001750 00000005665 13646270615 024773 0 ustar jitse jitse 0000000 0000000 # -*- coding: utf-8 -*-
#
# Copyright © 2017 Spyder Project Contributors
# Licensed under the terms of the MIT License
# (see LICENSE.txt for details)
"""Class for abbreviating test names."""
class Abbreviator:
"""
Abbreviates names so that abbreviation identifies name uniquely.
First, if the name contains brackets, the part in brackets starting at
the first bracket is removed from the name. Then, all names are split
in components separated by full stops (like module names in Python).
Every component is abbreviated by the smallest prefix not shared by
other names in the same directory, except for the last component which
is not changed. Finally, the part in brackets, which was removed at the
beginning, is appended to the abbreviated name.
Attributes
----------
dic : dict of (str, [str, Abbreviator])
keys are the first-level components, values are a list, with the
abbreviation as its first element and an Abbreviator for abbreviating
the higher-level components as its second element.
"""
def __init__(self, names=[]):
"""
Constructor.
Arguments
---------
names : list of str
list of words which needs to be abbreviated.
"""
self.dic = {}
for name in names:
self.add(name)
def add(self, name):
"""
Add name to list of names to be abbreviated.
Arguments
---------
name : str
"""
name = name.split('[', 1)[0]
if '.' not in name:
return
len_abbrev = 1
start, rest = name.split('.', 1)
for other in self.dic:
if start[:len_abbrev] == other[:len_abbrev]:
if start == other:
break
while (start[:len_abbrev] == other[:len_abbrev]
and len_abbrev < len(start)
and len_abbrev < len(other)):
len_abbrev += 1
if len_abbrev == len(start):
self.dic[other][0] = other[:len_abbrev + 1]
elif len_abbrev == len(other):
self.dic[other][0] = other
len_abbrev += 1
else:
if len(self.dic[other][0]) < len_abbrev:
self.dic[other][0] = other[:len_abbrev]
else:
self.dic[start] = [start[:len_abbrev], Abbreviator()]
self.dic[start][1].add(rest)
def abbreviate(self, name):
"""Return abbreviation of name."""
if '[' in name:
name, parameters = name.split('[', 1)
parameters = '[' + parameters
else:
parameters = ''
if '.' in name:
start, rest = name.split('.', 1)
res = (self.dic[start][0]
+ '.' + self.dic[start][1].abbreviate(rest))
else:
res = name
return res + parameters
spyder_unittest-0.4.1/spyder_unittest/backend/frameworkregistry.py 0000644 0001750 0001750 00000004025 13224452267 026243 0 ustar jitse jitse 0000000 0000000 # -*- coding: utf-8 -*-
#
# Copyright © 2017 Spyder Project Contributors
# Licensed under the terms of the MIT License
# (see LICENSE.txt for details)
"""Keep track of testing frameworks and create test runners when requested."""
class FrameworkRegistry():
"""
Registry of testing frameworks and their associated runners.
The test runner for a framework is responsible for running the tests and
parsing the results. It should implement the interface of RunnerBase.
Frameworks should first be registered using `.register()`. This registry
can then create the assoicated test runner when `.create_runner()` is
called.
Attributes
----------
frameworks : dict of (str, type)
Dictionary mapping names of testing frameworks to the types of the
associated runners.
"""
def __init__(self):
"""Initialize self."""
self.frameworks = {}
def register(self, runner_class):
"""Register runner class for a testing framework.
Parameters
----------
runner_class : type
Class used for creating tests runners for the framework.
"""
self.frameworks[runner_class.name] = runner_class
def create_runner(self, framework, widget, tempfilename):
"""Create test runner associated to some testing framework.
This creates an instance of the runner class whose `name` attribute
equals `framework`.
Parameters
----------
framework : str
Name of testing framework.
widget : UnitTestWidget
Unit test widget which constructs the test runner.
resultfilename : str or None
Name of file in which to store test results. If None, use default.
Returns
-------
RunnerBase
Newly created test runner
Exceptions
----------
KeyError
Provided testing framework has not been registered.
"""
cls = self.frameworks[framework]
return cls(widget, tempfilename)
spyder_unittest-0.4.1/spyder_unittest/backend/noserunner.py 0000644 0001750 0001750 00000007261 13646270600 024655 0 ustar jitse jitse 0000000 0000000 # -*- coding: utf-8 -*-
#
# Copyright © 2013 Spyder Project Contributors
# Licensed under the terms of the MIT License
# (see LICENSE.txt for details)
"""Support for Nose framework."""
# Third party imports
from lxml import etree
from spyder.config.base import get_translation
# Local imports
from spyder_unittest.backend.runnerbase import Category, RunnerBase, TestResult
try:
_ = get_translation("unittest", dirname="spyder_unittest")
except KeyError:
import gettext
_ = gettext.gettext
class NoseRunner(RunnerBase):
"""Class for running tests within Nose framework."""
module = 'nose'
name = 'nose'
def get_versions(self):
"""Return versions of framework and its plugins."""
import nose
from pkg_resources import iter_entry_points
versions = ['nose {}'.format(nose.__version__)]
for entry_point, _ in (nose.plugins.manager.EntryPointPluginManager
.entry_points):
for ep in iter_entry_points(entry_point):
versions.append(
" {} {}".format(ep.dist.project_name, ep.dist.version))
return versions
def create_argument_list(self):
"""Create argument list for testing process."""
return [
'-m', self.module, '--with-xunit',
'--xunit-file={}'.format(self.resultfilename)
]
def finished(self):
"""Called when the unit test process has finished."""
output = self.read_all_process_output()
testresults = self.load_data()
self.sig_finished.emit(testresults, output)
def load_data(self):
"""
Read and parse unit test results.
This function reads the unit test results from the file with name
`self.resultfilename` and parses them. The file should contain the
test results in JUnitXML format.
Returns
-------
list of TestResult
Unit test results.
"""
try:
data = etree.parse(self.resultfilename).getroot()
except OSError:
data = []
testresults = []
for testcase in data:
category = Category.OK
status = 'ok'
name = '{}.{}'.format(testcase.get('classname'),
testcase.get('name'))
message = ''
time = float(testcase.get('time'))
extras = []
for child in testcase:
if child.tag in ('error', 'failure', 'skipped'):
if child.tag == 'skipped':
category = Category.SKIP
else:
category = Category.FAIL
status = child.tag
type_ = child.get('type')
message = child.get('message', default='')
if type_ and message:
message = '{0}: {1}'.format(type_, message)
elif type_:
message = type_
if child.text:
extras.append(child.text)
elif child.tag in ('system-out', 'system-err'):
if child.tag == 'system-out':
heading = _('Captured stdout')
else:
heading = _('Captured stderr')
contents = child.text.rstrip('\n')
extras.append('----- {} -----\n{}'.format(heading,
contents))
extra_text = '\n\n'.join(extras)
testresults.append(
TestResult(category, status, name, message, time, extra_text))
return testresults
spyder_unittest-0.4.1/spyder_unittest/backend/pytestrunner.py 0000644 0001750 0001750 00000012637 13657202727 025253 0 ustar jitse jitse 0000000 0000000 # -*- coding: utf-8 -*-
#
# Copyright © 2013 Spyder Project Contributors
# Licensed under the terms of the MIT License
# (see LICENSE.txt for details)
"""Support for pytest framework."""
# Standard library imports
import os
import os.path as osp
# Local imports
from spyder_unittest.backend.runnerbase import Category, RunnerBase, TestResult
from spyder_unittest.backend.zmqstream import ZmqStreamReader
class PyTestRunner(RunnerBase):
"""Class for running tests within pytest framework."""
module = 'pytest'
name = 'pytest'
def get_versions(self):
"""Return versions of framework and its plugins."""
import pytest
versions = ['pytest {}'.format(pytest.__version__)]
class GetPluginVersionsPlugin():
def pytest_cmdline_main(self, config):
nonlocal versions
plugininfo = config.pluginmanager.list_plugin_distinfo()
if plugininfo:
for plugin, dist in plugininfo:
versions.append(" {} {}".format(dist.project_name,
dist.version))
# --capture=sys needed on Windows to avoid
# ValueError: saved filedescriptor not valid anymore
pytest.main(['-V', '--capture=sys'],
plugins=[GetPluginVersionsPlugin()])
return versions
def create_argument_list(self):
"""Create argument list for testing process."""
pyfile = os.path.join(os.path.dirname(__file__), 'pytestworker.py')
return [pyfile, str(self.reader.port)]
def start(self, config, pythonpath):
"""Start process which will run the unit test suite."""
self.config = config
self.reader = ZmqStreamReader()
self.reader.sig_received.connect(self.process_output)
RunnerBase.start(self, config, pythonpath)
def process_output(self, output):
"""
Process output of test process.
Parameters
----------
output : list
list of decoded Python object sent by test process.
"""
collected_list = []
collecterror_list = []
starttest_list = []
result_list = []
for result_item in output:
if result_item['event'] == 'config':
self.rootdir = result_item['rootdir']
elif result_item['event'] == 'collected':
testname = convert_nodeid_to_testname(result_item['nodeid'])
collected_list.append(testname)
elif result_item['event'] == 'collecterror':
tupl = logreport_collecterror_to_tuple(result_item)
collecterror_list.append(tupl)
elif result_item['event'] == 'starttest':
starttest_list.append(logreport_starttest_to_str(result_item))
elif result_item['event'] == 'logreport':
testresult = logreport_to_testresult(result_item, self.rootdir)
result_list.append(testresult)
if collected_list:
self.sig_collected.emit(collected_list)
if collecterror_list:
self.sig_collecterror.emit(collecterror_list)
if starttest_list:
self.sig_starttest.emit(starttest_list)
if result_list:
self.sig_testresult.emit(result_list)
def finished(self):
"""
Called when the unit test process has finished.
This function emits `sig_finished`.
"""
self.reader.close()
output = self.read_all_process_output()
self.sig_finished.emit([] if "no tests ran" in output else None, output)
def normalize_module_name(name):
"""
Convert module name reported by pytest to Python conventions.
This function strips the .py suffix and replaces '/' by '.', so that
'ham/spam.py' becomes 'ham.spam'.
"""
if name.endswith('.py'):
name = name[:-3]
return name.replace('/', '.')
def convert_nodeid_to_testname(nodeid):
"""Convert a nodeid to a test name."""
module, name = nodeid.split('::', 1)
module = normalize_module_name(module)
return '{}.{}'.format(module, name)
def logreport_collecterror_to_tuple(report):
"""Convert a 'collecterror' logreport to a (str, str) tuple."""
module = normalize_module_name(report['nodeid'])
return (module, report['longrepr'])
def logreport_starttest_to_str(report):
"""Convert a 'starttest' logreport to a str."""
return convert_nodeid_to_testname(report['nodeid'])
def logreport_to_testresult(report, rootdir):
"""Convert a logreport sent by test process to a TestResult."""
status = report['outcome']
if report['outcome'] in ('failed', 'xpassed') or report['witherror']:
cat = Category.FAIL
elif report['outcome'] in ('passed', 'xfailed'):
cat = Category.OK
else:
cat = Category.SKIP
testname = convert_nodeid_to_testname(report['nodeid'])
message = report.get('message', '')
extra_text = report.get('longrepr', '')
if 'sections' in report:
if extra_text:
extra_text += '\n'
for (heading, text) in report['sections']:
extra_text += '----- {} -----\n{}'.format(heading, text)
filename = osp.join(rootdir, report['filename'])
result = TestResult(cat, status, testname, message=message,
time=report['duration'], extra_text=extra_text,
filename=filename, lineno=report['lineno'])
return result
spyder_unittest-0.4.1/spyder_unittest/backend/pytestworker.py 0000644 0001750 0001750 00000012327 13657202727 025247 0 ustar jitse jitse 0000000 0000000 # -*- coding: utf-8 -*-
#
# Copyright © 2017 Spyder Project Contributors
# Licensed under the terms of the MIT License
# (see LICENSE.txt for details)
"""
Script for running pytest tests.
This script is meant to be run in a separate process by a PyTestRunner.
It runs tests via the pytest framework and prints the results so that the
PyTestRunner can read them.
"""
# Standard library imports
import sys
# Third party imports
import pytest
# Local imports
from spyder.config.base import get_translation
from spyder_unittest.backend.zmqstream import ZmqStreamWriter
try:
_ = get_translation("unittest", dirname="spyder_unittest")
except KeyError: # pragma: no cover
import gettext
_ = gettext.gettext
class FileStub():
"""Stub for ZmqStreamWriter which instead writes to a file."""
def __init__(self, filename):
"""Constructor; connect to specified filename."""
self.file = open(filename, 'w')
def write(self, obj):
"""Write Python object to file."""
self.file.write(str(obj) + '\n')
def close(self):
"""Close file."""
self.file.close()
class SpyderPlugin():
"""Pytest plugin which reports in format suitable for Spyder."""
def __init__(self, writer):
"""Constructor."""
self.writer = writer
def initialize_logreport(self):
"""Reset accumulator variables."""
self.status = '---'
self.duration = 0
self.longrepr = []
self.sections = []
self.had_error = False
self.was_skipped = False
self.was_xfail = False
def pytest_report_header(self, config, startdir):
"""Called by pytest before any reporting."""
self.writer.write({
'event': 'config',
'rootdir': str(config.rootdir)
})
def pytest_collectreport(self, report):
"""Called by pytest after collecting tests from a file."""
if report.outcome == 'failed':
self.writer.write({
'event': 'collecterror',
'nodeid': report.nodeid,
'longrepr': str(report.longrepr)
})
def pytest_itemcollected(self, item):
"""Called by pytest when a test item is collected."""
self.writer.write({
'event': 'collected',
'nodeid': item.nodeid
})
def pytest_runtest_logstart(self, nodeid, location):
"""Called by pytest before running a test."""
self.writer.write({
'event': 'starttest',
'nodeid': nodeid
})
self.initialize_logreport()
def pytest_runtest_logreport(self, report):
"""Called by pytest when a phase of a test is completed."""
if report.when == 'call':
self.status = report.outcome
self.duration = report.duration
else:
if report.outcome == 'failed':
self.had_error = True
elif report.outcome == 'skipped':
self.was_skipped = True
if hasattr(report, 'wasxfail'):
self.was_xfail = True
self.longrepr.append(report.wasxfail if report.wasxfail else _(
'WAS EXPECTED TO FAIL'))
self.sections = report.sections # already accumulated over phases
if report.longrepr:
first_msg_idx = len(self.longrepr)
if hasattr(report.longrepr, 'reprcrash'):
self.longrepr.append(report.longrepr.reprcrash.message)
if isinstance(report.longrepr, tuple):
self.longrepr.append(report.longrepr[2])
elif isinstance(report.longrepr, str):
self.longrepr.append(report.longrepr)
else:
self.longrepr.append(str(report.longrepr))
if report.outcome == 'failed' and report.when in (
'setup', 'teardown'):
self.longrepr[first_msg_idx] = '{} {}: {}'.format(
_('ERROR at'), report.when, self.longrepr[first_msg_idx])
def pytest_runtest_logfinish(self, nodeid, location):
"""Called by pytest when the entire test is completed."""
if self.was_xfail:
if self.status == 'passed':
self.status = 'xpassed'
else: # 'skipped'
self.status = 'xfailed'
elif self.was_skipped:
self.status = 'skipped'
data = {'event': 'logreport',
'outcome': self.status,
'witherror': self.had_error,
'sections': self.sections,
'duration': self.duration,
'nodeid': nodeid,
'filename': location[0],
'lineno': location[1]}
if self.longrepr:
msg_lines = self.longrepr[0].rstrip().splitlines()
data['message'] = msg_lines[0]
start_item = 1 if len(msg_lines) == 1 else 0
data['longrepr'] = '\n'.join(self.longrepr[start_item:])
self.writer.write(data)
def main(args):
"""Run pytest with the Spyder plugin."""
if args[1] == 'file':
writer = FileStub('pytestworker.log')
else:
writer = ZmqStreamWriter(int(args[1]))
pytest.main(args[2:], plugins=[SpyderPlugin(writer)])
writer.close()
if __name__ == '__main__':
main(sys.argv)
spyder_unittest-0.4.1/spyder_unittest/backend/runnerbase.py 0000644 0001750 0001750 00000017231 13657202727 024630 0 ustar jitse jitse 0000000 0000000 # -*- coding: utf-8 -*-
#
# Copyright © 2013 Spyder Project Contributors
# Licensed under the terms of the MIT License
# (see LICENSE.txt for details)
"""Classes for running tests within various frameworks."""
# Standard library imports
import os
import tempfile
# Third party imports
from qtpy.QtCore import (QObject, QProcess, QProcessEnvironment, QTextCodec,
Signal)
from spyder.py3compat import to_text_string
from spyder.utils.misc import get_python_executable
try:
from importlib.util import find_spec as find_spec_or_loader
except ImportError: # Python 2
from pkgutil import find_loader as find_spec_or_loader
class Category:
"""Enum type representing category of test result."""
FAIL = 1
OK = 2
SKIP = 3
PENDING = 4
class TestResult:
"""Class representing the result of running a single test."""
def __init__(self, category, status, name, message='', time=None,
extra_text='', filename=None, lineno=None):
"""
Construct a test result.
Parameters
----------
category : Category
status : str
name : str
message : str
time : float or None
extra_text : str
filename : str or None
lineno : int or None
"""
self.category = category
self.status = status
self.name = name
self.message = message
self.time = time
extra_text = extra_text.rstrip()
if extra_text:
self.extra_text = extra_text.split("\n")
else:
self.extra_text = []
self.filename = filename
self.lineno = lineno
def __eq__(self, other):
"""Test for equality."""
return self.__dict__ == other.__dict__
class RunnerBase(QObject):
"""
Base class for running tests with a framework that uses JUnit XML.
This is an abstract class, meant to be subclassed before being used.
Concrete subclasses should define executable and create_argument_list(),
All communication back to the caller is done via signals.
Attributes
----------
module : str
Name of Python module for test framework. This needs to be defined
before the user can run tests.
name : str
Name of test framework, as presented to user.
process : QProcess or None
Process running the unit test suite.
resultfilename : str
Name of file in which test results are stored.
Signals
-------
sig_collected(list of str)
Emitted when tests are collected.
sig_collecterror(list of (str, str) tuples)
Emitted when errors are encountered during collection. First element
of tuple is test name, second element is error message.
sig_starttest(list of str)
Emitted just before tests are run.
sig_testresult(list of TestResult)
Emitted when tests are finished.
sig_finished(list of TestResult, str)
Emitted when test process finishes. First argument contains the test
results, second argument contains the output of the test process.
sig_stop()
Emitted when test process is being stopped.
"""
sig_collected = Signal(object)
sig_collecterror = Signal(object)
sig_starttest = Signal(object)
sig_testresult = Signal(object)
sig_finished = Signal(object, str)
sig_stop = Signal()
def __init__(self, widget, resultfilename=None):
"""
Construct test runner.
Parameters
----------
widget : UnitTestWidget
Unit test widget which constructs the test runner.
resultfilename : str or None
Name of file in which to store test results. If None, use default.
"""
QObject.__init__(self, widget)
self.process = None
if resultfilename is None:
self.resultfilename = os.path.join(tempfile.gettempdir(),
'unittest.results')
else:
self.resultfilename = resultfilename
@classmethod
def is_installed(cls):
"""
Check whether test framework is installed.
This function tests whether self.module is installed, but it does not
import it.
Returns
-------
bool
True if framework is installed, False otherwise.
"""
return find_spec_or_loader(cls.module) is not None
def get_versions(self):
"""
Return versions of framework and its installed plugins.
This function must only be called for installed frameworks.
Returns
-------
list of str
Strings with framework or plugin name, followed by
its version.
"""
raise NotImplementedError
def create_argument_list(self):
"""
Create argument list for testing process (dummy).
This function should be defined before calling self.start().
"""
raise NotImplementedError
def _prepare_process(self, config, pythonpath):
"""
Prepare and return process for running the unit test suite.
This sets the working directory and environment.
"""
process = QProcess(self)
process.setProcessChannelMode(QProcess.MergedChannels)
process.setWorkingDirectory(config.wdir)
process.finished.connect(self.finished)
if pythonpath:
env = QProcessEnvironment.systemEnvironment()
old_python_path = env.value('PYTHONPATH', None)
python_path_str = os.pathsep.join(pythonpath)
if old_python_path:
python_path_str += os.pathsep + old_python_path
env.insert('PYTHONPATH', python_path_str)
process.setProcessEnvironment(env)
return process
def start(self, config, pythonpath):
"""
Start process which will run the unit test suite.
The process is run in the working directory specified in 'config',
with the directories in `pythonpath` added to the Python path for the
test process. The test results are written to the file
`self.resultfilename`. The standard output and error are also recorded.
Once the process is finished, `self.finished()` will be called.
Parameters
----------
config : TestConfig
Unit test configuration.
pythonpath : list of str
List of directories to be added to the Python path
Raises
------
RuntimeError
If process failed to start.
"""
self.process = self._prepare_process(config, pythonpath)
executable = get_python_executable()
p_args = self.create_argument_list()
try:
os.remove(self.resultfilename)
except OSError:
pass
self.process.start(executable, p_args)
running = self.process.waitForStarted()
if not running:
raise RuntimeError
def finished(self):
"""
Called when the unit test process has finished.
This function should be implemented in derived classes. It should read
the results (if necessary) and emit `sig_finished`.
"""
raise NotImplementedError
def read_all_process_output(self):
"""Read and return all output from `self.process` as unicode."""
qbytearray = self.process.readAllStandardOutput()
locale_codec = QTextCodec.codecForLocale()
return to_text_string(locale_codec.toUnicode(qbytearray.data()))
def stop_if_running(self):
"""Stop testing process if it is running."""
if self.process and self.process.state() == QProcess.Running:
self.process.kill()
self.sig_stop.emit()
spyder_unittest-0.4.1/spyder_unittest/backend/tests/ 0000755 0001750 0001750 00000000000 13662215740 023243 5 ustar jitse jitse 0000000 0000000 spyder_unittest-0.4.1/spyder_unittest/backend/tests/__init__.py 0000644 0001750 0001750 00000000302 13074157120 025341 0 ustar jitse jitse 0000000 0000000 # -*- coding: utf-8 -*-
#
# Copyright © 2017 Spyder Project Contributors
# Licensed under the terms of the MIT License
# (see LICENSE.txt for details)
"""Tests for spyder_unittest.backend ."""
spyder_unittest-0.4.1/spyder_unittest/backend/tests/test_abbreviator.py 0000644 0001750 0001750 00000004677 13646270615 027176 0 ustar jitse jitse 0000000 0000000 # -*- coding: utf-8 -*-
#
# Copyright © 2017 Spyder Project Contributors
# Licensed under the terms of the MIT License
# (see LICENSE.txt for details)
"""Tests for abbreviator.py"""
# Local imports
from spyder_unittest.backend.abbreviator import Abbreviator
def test_abbreviator_with_one_word():
abb = Abbreviator()
abb.add('ham')
assert abb.abbreviate('ham') == 'ham'
def test_abbreviator_with_one_word_with_two_components():
abb = Abbreviator()
abb.add('ham.spam')
assert abb.abbreviate('ham.spam') == 'h.spam'
def test_abbreviator_with_one_word_with_three_components():
abb = Abbreviator()
abb.add('ham.spam.eggs')
assert abb.abbreviate('ham.spam.eggs') == 'h.s.eggs'
def test_abbreviator_without_common_prefix():
abb = Abbreviator(['ham.foo', 'spam.foo'])
assert abb.abbreviate('ham.foo') == 'h.foo'
assert abb.abbreviate('spam.foo') == 's.foo'
def test_abbreviator_with_prefix():
abb = Abbreviator(['test_ham.x', 'test_spam.x'])
assert abb.abbreviate('test_ham.x') == 'test_h.x'
assert abb.abbreviate('test_spam.x') == 'test_s.x'
def test_abbreviator_with_first_word_prefix_of_second():
abb = Abbreviator(['ham.x', 'hameggs.x'])
assert abb.abbreviate('ham.x') == 'ham.x'
assert abb.abbreviate('hameggs.x') == 'hame.x'
def test_abbreviator_with_second_word_prefix_of_first():
abb = Abbreviator(['hameggs.x', 'ham.x'])
assert abb.abbreviate('hameggs.x') == 'hame.x'
assert abb.abbreviate('ham.x') == 'ham.x'
def test_abbreviator_with_three_words():
abb = Abbreviator(['hamegg.x', 'hameggs.x', 'hall.x'])
assert abb.abbreviate('hamegg.x') == 'hamegg.x'
assert abb.abbreviate('hameggs.x') == 'hameggs.x'
assert abb.abbreviate('hall.x') == 'hal.x'
def test_abbreviator_with_multilevel():
abb = Abbreviator(['ham.eggs.foo', 'ham.spam.bar', 'eggs.ham.foo',
'eggs.hamspam.bar'])
assert abb.abbreviate('ham.eggs.foo') == 'h.e.foo'
assert abb.abbreviate('ham.spam.bar') == 'h.s.bar'
assert abb.abbreviate('eggs.ham.foo') == 'e.ham.foo'
assert abb.abbreviate('eggs.hamspam.bar') == 'e.hams.bar'
def test_abbreviator_with_one_word_and_parameters_with_dot():
abb = Abbreviator()
abb.add('ham[.]')
assert abb.abbreviate('ham[x.]') == 'ham[x.]'
def test_abbreviator_with_one_word_with_two_components_and_parameters_with_dot():
abb = Abbreviator()
abb.add('ham.spam[.]')
assert abb.abbreviate('ham.spam[x.]') == 'h.spam[x.]'
spyder_unittest-0.4.1/spyder_unittest/backend/tests/test_frameworkregistry.py 0000644 0001750 0001750 00000001475 13224452267 030452 0 ustar jitse jitse 0000000 0000000 # -*- coding: utf-8 -*-
#
# Copyright © 2017 Spyder Project Contributors
# Licensed under the terms of the MIT License
# (see LICENSE.txt for details)
"""Tests for frameworkregistry.py"""
# Third party imports
import pytest
# Local imports
from spyder_unittest.backend.frameworkregistry import FrameworkRegistry
class MockRunner:
name = 'foo'
def __init__(self, *args):
self.init_args = args
def test_frameworkregistry_when_empty():
reg = FrameworkRegistry()
with pytest.raises(KeyError):
reg.create_runner('foo', None, 'temp.txt')
def test_frameworkregistry_after_registering():
reg = FrameworkRegistry()
reg.register(MockRunner)
runner = reg.create_runner('foo', None, 'temp.txt')
assert isinstance(runner, MockRunner)
assert runner.init_args == (None, 'temp.txt')
spyder_unittest-0.4.1/spyder_unittest/backend/tests/test_noserunner.py 0000644 0001750 0001750 00000010072 13646270600 027050 0 ustar jitse jitse 0000000 0000000 # -*- coding: utf-8 -*-
#
# Copyright © 2013 Spyder Project Contributors
# Licensed under the terms of the MIT License
# (see LICENSE.txt for details)
"""Tests for noserunner.py"""
# Local imports
from spyder_unittest.backend.noserunner import NoseRunner
from spyder_unittest.backend.runnerbase import Category
def test_noserunner_load_data(tmpdir):
result_file = tmpdir.join('results')
result_txt = """
text
text2
"""
result_file.write(result_txt)
runner = NoseRunner(None, result_file.strpath)
results = runner.load_data()
assert len(results) == 3
assert results[0].category == Category.OK
assert results[0].status == 'ok'
assert results[0].name == 'test_foo.test1'
assert results[0].message == ''
assert results[0].time == 0.04
assert results[0].extra_text == []
assert results[1].category == Category.FAIL
assert results[1].status == 'failure'
assert results[1].name == 'test_foo.test2'
assert results[1].message == 'failure message'
assert results[1].time == 0.01
assert results[1].extra_text == ['text']
assert results[2].category == Category.SKIP
assert results[2].status == 'skipped'
assert results[2].name == 'test_foo.test3'
assert results[2].message == 'skip message'
assert results[2].time == 0.05
assert results[2].extra_text == ['text2']
def test_noserunner_load_data_failing_test_with_stdout(tmpdir):
result_file = tmpdir.join('results')
result_txt = """
text
stdout text
"""
result_file.write(result_txt)
runner = NoseRunner(None, result_file.strpath)
results = runner.load_data()
assert results[0].extra_text == ['text', '', '----- Captured stdout -----', 'stdout text']
def test_noserunner_load_data_passing_test_with_stdout(tmpdir):
result_file = tmpdir.join('results')
result_txt = """
stdout text
"""
result_file.write(result_txt)
runner = NoseRunner(None, result_file.strpath)
results = runner.load_data()
assert results[0].extra_text == ['----- Captured stdout -----', 'stdout text']
def test_get_versions_without_plugins(monkeypatch):
import nose
import pkg_resources
monkeypatch.setattr(nose, '__version__', '1.2.3')
monkeypatch.setattr(pkg_resources, 'iter_entry_points', lambda x: ())
runner = NoseRunner(None)
assert runner.get_versions() == ['nose 1.2.3']
def test_get_versions_with_plugins(monkeypatch):
import nose
import pkg_resources
monkeypatch.setattr(nose, '__version__', '1.2.3')
dist = pkg_resources.Distribution(project_name='myPlugin',
version='4.5.6')
ep = pkg_resources.EntryPoint('name', 'module_name', dist=dist)
monkeypatch.setattr(pkg_resources,
'iter_entry_points',
lambda ept: (x for x in (ep,) if ept == nose.plugins
.manager.EntryPointPluginManager
.entry_points[0][0]))
runner = NoseRunner(None)
assert runner.get_versions() == ['nose 1.2.3', ' myPlugin 4.5.6']
spyder_unittest-0.4.1/spyder_unittest/backend/tests/test_pytestrunner.py 0000644 0001750 0001750 00000017767 13657202727 027465 0 ustar jitse jitse 0000000 0000000 # -*- coding: utf-8 -*-
#
# Copyright © 2017 Spyder Project Contributors
# Licensed under the terms of the MIT License
# (see LICENSE.txt for details)
"""Tests for pytestrunner.py"""
# Standard library imports
import os
import os.path as osp
# Third party imports
import pytest
from qtpy.QtCore import QByteArray
# Local imports
from spyder_unittest.backend.pytestrunner import (PyTestRunner,
logreport_to_testresult)
from spyder_unittest.backend.runnerbase import Category, TestResult
from spyder_unittest.widgets.configdialog import Config
try:
from unittest.mock import Mock
except ImportError:
from mock import Mock # Python 2
def test_pytestrunner_is_installed():
assert PyTestRunner(None).is_installed()
def test_pytestrunner_create_argument_list(monkeypatch):
MockZMQStreamReader = Mock()
monkeypatch.setattr(
'spyder_unittest.backend.pytestrunner.ZmqStreamReader',
MockZMQStreamReader)
mock_reader = MockZMQStreamReader()
mock_reader.port = 42
runner = PyTestRunner(None, 'results')
runner.reader = mock_reader
monkeypatch.setattr('spyder_unittest.backend.pytestrunner.os.path.dirname',
lambda _: 'dir')
pyfile, port = runner.create_argument_list()
assert pyfile == 'dir{}pytestworker.py'.format(os.sep)
assert port == '42'
def test_pytestrunner_start(monkeypatch):
MockZMQStreamReader = Mock()
monkeypatch.setattr(
'spyder_unittest.backend.pytestrunner.ZmqStreamReader',
MockZMQStreamReader)
mock_reader = MockZMQStreamReader()
MockRunnerBase = Mock(name='RunnerBase')
monkeypatch.setattr('spyder_unittest.backend.pytestrunner.RunnerBase',
MockRunnerBase)
runner = PyTestRunner(None, 'results')
config = Config()
runner.start(config, ['pythondir'])
assert runner.config is config
assert runner.reader is mock_reader
runner.reader.sig_received.connect.assert_called_once_with(
runner.process_output)
MockRunnerBase.start.assert_called_once_with(runner, config, ['pythondir'])
def test_pytestrunner_process_output_with_collected(qtbot):
runner = PyTestRunner(None)
output = [{'event': 'collected', 'nodeid': 'spam.py::ham'},
{'event': 'collected', 'nodeid': 'eggs.py::bacon'}]
with qtbot.waitSignal(runner.sig_collected) as blocker:
runner.process_output(output)
expected = ['spam.ham', 'eggs.bacon']
assert blocker.args == [expected]
def test_pytestrunner_process_output_with_collecterror(qtbot):
runner = PyTestRunner(None)
output = [{
'event': 'collecterror',
'nodeid': 'ham/spam.py',
'longrepr': 'msg'
}]
with qtbot.waitSignal(runner.sig_collecterror) as blocker:
runner.process_output(output)
expected = [('ham.spam', 'msg')]
assert blocker.args == [expected]
def test_pytestrunner_process_output_with_starttest(qtbot):
runner = PyTestRunner(None)
output = [{'event': 'starttest', 'nodeid': 'ham/spam.py::ham'},
{'event': 'starttest', 'nodeid': 'ham/eggs.py::bacon'}]
with qtbot.waitSignal(runner.sig_starttest) as blocker:
runner.process_output(output)
expected = ['ham.spam.ham', 'ham.eggs.bacon']
assert blocker.args == [expected]
@pytest.mark.parametrize('output,results', [
('== 1 passed in 0.10s ==', None),
('== no tests ran 0.01s ==', [])
])
def test_pytestrunner_finished(qtbot, output, results):
mock_reader = Mock()
mock_reader.close = lambda: None
runner = PyTestRunner(None)
runner.reader = mock_reader
runner.read_all_process_output = lambda: output
with qtbot.waitSignal(runner.sig_finished) as blocker:
runner.finished()
assert blocker.args == [results, output]
def standard_logreport_output():
return {
'event': 'logreport',
'outcome': 'passed',
'witherror': False,
'nodeid': 'foo.py::bar',
'filename': 'foo.py',
'lineno': 24,
'duration': 42
}
def test_pytestrunner_process_output_with_logreport_passed(qtbot):
runner = PyTestRunner(None)
runner.rootdir = 'ham'
output = [standard_logreport_output()]
with qtbot.waitSignal(runner.sig_testresult) as blocker:
runner.process_output(output)
expected = [TestResult(Category.OK, 'passed', 'foo.bar', time=42,
filename=osp.join('ham', 'foo.py'), lineno=24)]
assert blocker.args == [expected]
@pytest.mark.parametrize('outcome,witherror,category', [
('passed', True, Category.FAIL),
('passed', False, Category.OK),
('failed', True, Category.FAIL),
('failed', False, Category.FAIL),
# ('skipped', True, this is not possible)
('skipped', False, Category.SKIP),
('xfailed', True, Category.FAIL),
('xfailed', False, Category.OK),
('xpassed', True, Category.FAIL),
('xpassed', False, Category.FAIL),
('---', True, Category.FAIL)
# ('---', False, this is not possible)
])
def test_logreport_to_testresult_with_outcome_and_possible_error(outcome,
witherror,
category):
report = standard_logreport_output()
report['outcome'] = outcome
report['witherror'] = witherror
expected = TestResult(category, outcome, 'foo.bar', time=42,
filename=osp.join('ham', 'foo.py'), lineno=24)
assert logreport_to_testresult(report, 'ham') == expected
def test_logreport_to_testresult_with_message():
report = standard_logreport_output()
report['message'] = 'msg'
expected = TestResult(Category.OK, 'passed', 'foo.bar', message='msg',
time=42, filename=osp.join('ham', 'foo.py'),
lineno=24)
assert logreport_to_testresult(report, 'ham') == expected
def test_logreport_to_testresult_with_extratext():
report = standard_logreport_output()
report['longrepr'] = 'long msg'
expected = TestResult(Category.OK, 'passed', 'foo.bar', time=42,
extra_text='long msg',
filename=osp.join('ham', 'foo.py'), lineno=24)
assert logreport_to_testresult(report, 'ham') == expected
@pytest.mark.parametrize('longrepr,prefix', [
('', ''),
('msg', '\n')
])
def test_logreport_to_testresult_with_output(longrepr, prefix):
report = standard_logreport_output()
report['longrepr'] = longrepr
report['sections'] = [['Captured stdout call', 'ham\n'],
['Captured stderr call', 'spam\n']]
txt = (longrepr + prefix +
'----- Captured stdout call -----\nham\n'
'----- Captured stderr call -----\nspam\n')
expected = TestResult(Category.OK, 'passed', 'foo.bar', time=42,
extra_text=txt, filename=osp.join('ham', 'foo.py'),
lineno=24)
assert logreport_to_testresult(report, 'ham') == expected
def test_get_versions_without_plugins(monkeypatch):
import pytest
monkeypatch.setattr(pytest, '__version__', '1.2.3')
from _pytest.config import PytestPluginManager
monkeypatch.setattr(
PytestPluginManager,
'list_plugin_distinfo', lambda _: ())
runner = PyTestRunner(None)
assert runner.get_versions() == ['pytest 1.2.3']
def test_get_versions_with_plugins(monkeypatch):
import pytest
import pkg_resources
monkeypatch.setattr(pytest, '__version__', '1.2.3')
dist1 = pkg_resources.Distribution(project_name='myPlugin1',
version='4.5.6')
dist2 = pkg_resources.Distribution(project_name='myPlugin2',
version='7.8.9')
from _pytest.config import PytestPluginManager
monkeypatch.setattr(
PytestPluginManager,
'list_plugin_distinfo', lambda _: (('1', dist1), ('2', dist2)))
runner = PyTestRunner(None)
assert runner.get_versions() == ['pytest 1.2.3', ' myPlugin1 4.5.6',
' myPlugin2 7.8.9']
spyder_unittest-0.4.1/spyder_unittest/backend/tests/test_pytestworker.py 0000644 0001750 0001750 00000025600 13657202727 027446 0 ustar jitse jitse 0000000 0000000 # -*- coding: utf-8 -*-
#
# Copyright © 2017 Spyder Project Contributors
# Licensed under the terms of the MIT License
# (see LICENSE.txt for details)
"""Tests for pytestworker.py"""
# Standard library imports
import os
# Third party imports
import pytest
# Local imports
from spyder_unittest.backend.pytestworker import SpyderPlugin, main
from spyder_unittest.backend.zmqstream import ZmqStreamWriter
try:
from unittest.mock import call, create_autospec, MagicMock, Mock
except ImportError:
from mock import call, create_autospec, MagicMock, Mock # Python 2
class EmptyClass:
pass
@pytest.fixture
def plugin():
mock_writer = create_autospec(ZmqStreamWriter)
return SpyderPlugin(mock_writer)
def test_spyderplugin_test_report_header(plugin):
import pathlib
config = EmptyClass()
config.rootdir = pathlib.PurePosixPath('/myRootDir')
plugin.pytest_report_header(config, None)
plugin.writer.write.assert_called_once_with({
'event': 'config',
'rootdir': '/myRootDir'
})
@pytest.fixture
def plugin_ini():
mock_writer = create_autospec(ZmqStreamWriter)
plugin = SpyderPlugin(mock_writer)
plugin.status = '---'
plugin.duration = 0
plugin.longrepr = []
plugin.sections = []
plugin.had_error = False
plugin.was_skipped = False
plugin.was_xfail = False
return plugin
def test_spyderplugin_test_collectreport_with_success(plugin):
report = EmptyClass()
report.outcome = 'success'
report.nodeid = 'foo.py::bar'
plugin.pytest_collectreport(report)
plugin.writer.write.assert_not_called()
def test_spyderplugin_test_collectreport_with_failure(plugin):
report = EmptyClass()
report.outcome = 'failed'
report.nodeid = 'foo.py::bar'
report.longrepr = MagicMock()
report.longrepr.__str__.return_value = 'message'
plugin.pytest_collectreport(report)
plugin.writer.write.assert_called_once_with({
'event': 'collecterror',
'nodeid': 'foo.py::bar',
'longrepr': 'message'
})
def test_spyderplugin_test_itemcollected(plugin):
testitem = EmptyClass()
testitem.nodeid = 'foo.py::bar'
plugin.pytest_itemcollected(testitem)
plugin.writer.write.assert_called_once_with({
'event': 'collected',
'nodeid': 'foo.py::bar'
})
def standard_logreport():
report = EmptyClass()
report.when = 'call'
report.outcome = 'passed'
report.nodeid = 'foo.py::bar'
report.duration = 42
report.sections = []
report.longrepr = None
report.location = ('foo.py', 24, 'bar')
return report
def test_pytest_runtest_logreport_passed(plugin_ini):
report = standard_logreport()
report.sections = ['output']
plugin_ini.pytest_runtest_logreport(report)
assert plugin_ini.status == 'passed'
assert plugin_ini.duration == 42
assert plugin_ini.sections == ['output']
assert plugin_ini.had_error is False
assert plugin_ini.was_skipped is False
assert plugin_ini.was_xfail is False
def test_pytest_runtest_logreport_failed(plugin_ini):
report = standard_logreport()
report.when = 'teardown'
report.outcome = 'failed'
plugin_ini.pytest_runtest_logreport(report)
assert plugin_ini.status == '---'
assert plugin_ini.duration == 0
assert plugin_ini.had_error is True
assert plugin_ini.was_skipped is False
assert plugin_ini.was_xfail is False
def test_pytest_runtest_logreport_skipped(plugin_ini):
report = standard_logreport()
report.when = 'setup'
report.outcome = 'skipped'
plugin_ini.pytest_runtest_logreport(report)
assert plugin_ini.status == '---'
assert plugin_ini.duration == 0
assert plugin_ini.had_error is False
assert plugin_ini.was_skipped is True
assert plugin_ini.was_xfail is False
@pytest.mark.parametrize('xfail_msg,longrepr', [
('msg', 'msg'),
('', 'WAS EXPECTED TO FAIL')
])
def test_pytest_runtest_logreport_xfail(plugin_ini, xfail_msg, longrepr):
report = standard_logreport()
report.wasxfail = xfail_msg
plugin_ini.pytest_runtest_logreport(report)
assert plugin_ini.status == 'passed'
assert plugin_ini.duration == 42
assert plugin_ini.had_error is False
assert plugin_ini.was_skipped is False
assert plugin_ini.was_xfail is True
assert plugin_ini.longrepr == [longrepr]
def test_pytest_runtest_logreport_with_reprcrash_longrepr(plugin_ini):
class MockLongrepr:
def __init__(self):
self.reprcrash = EmptyClass()
self.reprcrash.message = 'msg'
def __str__(self):
return 'reprtraceback'
report = standard_logreport()
report.longrepr = MockLongrepr()
plugin_ini.pytest_runtest_logreport(report)
assert plugin_ini.longrepr == ['msg', 'reprtraceback']
def test_pytest_runtest_logreport_with_tuple_longrepr(plugin_ini):
report = standard_logreport()
report.longrepr = ('path', 'lineno', 'msg')
plugin_ini.pytest_runtest_logreport(report)
assert plugin_ini.longrepr == ['msg']
def test_pytest_runtest_logreport_with_str_longrepr(plugin_ini):
report = standard_logreport()
report.longrepr = 'msg'
plugin_ini.pytest_runtest_logreport(report)
assert plugin_ini.longrepr == ['msg']
def test_pytest_runtest_logreport_with_excinfo_longrepr(plugin_ini):
class MockLongrepr:
def __str__(self):
return 'msg'
report = standard_logreport()
report.longrepr = MockLongrepr()
plugin_ini.pytest_runtest_logreport(report)
assert plugin_ini.longrepr == ['msg']
@pytest.mark.parametrize('when,longrepr,expected',[
('setup', [], ['ERROR at setup: msg']),
('call', [], ['msg']),
('teardown', ['prev msg'], ['prev msg', 'ERROR at teardown: msg'])
])
def test_pytest_runtest_logreport_error_in_setup_or_teardown_message(
plugin_ini, when, longrepr, expected):
report = standard_logreport()
report.when = when
report.outcome = 'failed'
report.longrepr = 'msg'
plugin_ini.longrepr = longrepr
plugin_ini.pytest_runtest_logreport(report)
assert plugin_ini.longrepr == expected
def test_pytest_runtest_logreport_error_in_setup_or_teardown_multiple_messages(
plugin_ini):
class MockLongrepr:
def __init__(self):
self.reprcrash = EmptyClass()
self.reprcrash.message = 'msg'
def __str__(self):
return 'reprtraceback'
report = standard_logreport()
report.when = 'setup'
report.outcome = 'failed'
report.longrepr = MockLongrepr()
plugin_ini.pytest_runtest_logreport(report)
assert plugin_ini.longrepr == ['ERROR at setup: msg', 'reprtraceback']
def test_pytest_runtest_logfinish_skipped(plugin_ini):
nodeid = 'foo.py::bar'
location = ('foo.py', 24)
plugin_ini.was_skipped = True
plugin_ini.duration = 42
plugin_ini.pytest_runtest_logfinish(nodeid, location)
plugin_ini.writer.write.assert_called_once_with({
'event': 'logreport',
'outcome': 'skipped',
'witherror': False,
'nodeid': 'foo.py::bar',
'duration': 42,
'sections': [],
'filename': 'foo.py',
'lineno': 24
})
def test_pytest_runtest_logfinish_xfailed(plugin_ini):
nodeid = 'foo.py::bar'
location = ('foo.py', 24)
plugin_ini.was_xfail = True
plugin_ini.status = 'skipped'
plugin_ini.duration = 42
plugin_ini.pytest_runtest_logfinish(nodeid, location)
plugin_ini.writer.write.assert_called_once_with({
'event': 'logreport',
'outcome': 'xfailed',
'witherror': False,
'nodeid': 'foo.py::bar',
'duration': 42,
'sections': [],
'filename': 'foo.py',
'lineno': 24
})
def test_pytest_runtest_logfinish_xpassed(plugin_ini):
nodeid = 'foo.py::bar'
location = ('foo.py', 24)
plugin_ini.was_xfail = True
plugin_ini.status = 'passed'
plugin_ini.duration = 42
plugin_ini.pytest_runtest_logfinish(nodeid, location)
plugin_ini.writer.write.assert_called_once_with({
'event': 'logreport',
'outcome': 'xpassed',
'witherror': False,
'nodeid': 'foo.py::bar',
'duration': 42,
'sections': [],
'filename': 'foo.py',
'lineno': 24
})
@pytest.mark.parametrize('self_longrepr,message,longrepr', [
(['msg1 line1'], 'msg1 line1', ''),
(['msg1 line1\nmsg1 line2'], 'msg1 line1', 'msg1 line1\nmsg1 line2'),
(['msg1 line1', 'msg2'], 'msg1 line1', 'msg2'),
(['msg1 line1\nmsg1 line2', 'msg2'], 'msg1 line1',
'msg1 line1\nmsg1 line2\nmsg2'),
])
def test_pytest_runtest_logfinish_handles_longrepr(plugin_ini, self_longrepr,
message, longrepr):
nodeid = 'foo.py::bar'
location = ('foo.py', 24)
plugin_ini.status = 'passed'
plugin_ini.duration = 42
plugin_ini.longrepr = self_longrepr
plugin_ini.pytest_runtest_logfinish(nodeid, location)
plugin_ini.writer.write.assert_called_once_with({
'event': 'logreport',
'outcome': 'passed',
'witherror': False,
'nodeid': 'foo.py::bar',
'duration': 42,
'sections': [],
'filename': 'foo.py',
'lineno': 24,
'message': message,
'longrepr': longrepr
})
def test_pytestworker_integration(monkeypatch, tmpdir):
os.chdir(tmpdir.strpath)
testfilename = tmpdir.join('test_foo.py').strpath
with open(testfilename, 'w') as f:
f.write("def test_ok(): assert 1+1 == 2\n"
"def test_fail(): assert 1+1 == 3\n")
mock_writer = create_autospec(ZmqStreamWriter)
MockZmqStreamWriter = Mock(return_value=mock_writer)
monkeypatch.setattr(
'spyder_unittest.backend.pytestworker.ZmqStreamWriter',
MockZmqStreamWriter)
main(['mockscriptname', '42', testfilename])
args = mock_writer.write.call_args_list
assert args[0][0][0]['event'] == 'config'
assert 'rootdir' in args[0][0][0]
assert args[1][0][0]['event'] == 'collected'
assert args[1][0][0]['nodeid'] == 'test_foo.py::test_ok'
assert args[2][0][0]['event'] == 'collected'
assert args[2][0][0]['nodeid'] == 'test_foo.py::test_fail'
assert args[3][0][0]['event'] == 'starttest'
assert args[3][0][0]['nodeid'] == 'test_foo.py::test_ok'
assert args[4][0][0]['event'] == 'logreport'
assert args[4][0][0]['outcome'] == 'passed'
assert args[4][0][0]['nodeid'] == 'test_foo.py::test_ok'
assert args[4][0][0]['sections'] == []
assert args[4][0][0]['filename'] == 'test_foo.py'
assert args[4][0][0]['lineno'] == 0
assert 'duration' in args[4][0][0]
assert args[5][0][0]['event'] == 'starttest'
assert args[5][0][0]['nodeid'] == 'test_foo.py::test_fail'
assert args[6][0][0]['event'] == 'logreport'
assert args[6][0][0]['outcome'] == 'failed'
assert args[6][0][0]['nodeid'] == 'test_foo.py::test_fail'
assert args[6][0][0]['sections'] == []
assert args[6][0][0]['filename'] == 'test_foo.py'
assert args[6][0][0]['lineno'] == 1
assert 'duration' in args[6][0][0]
spyder_unittest-0.4.1/spyder_unittest/backend/tests/test_runnerbase.py 0000644 0001750 0001750 00000006074 13657202727 027034 0 ustar jitse jitse 0000000 0000000 # -*- coding: utf-8 -*-
#
# Copyright © 2013 Spyder Project Contributors
# Licensed under the terms of the MIT License
# (see LICENSE.txt for details)
"""Tests for baserunner.py"""
# Standard library imports
import os
# Third party imports
import pytest
# Local imports
from spyder_unittest.backend.runnerbase import RunnerBase
from spyder_unittest.widgets.configdialog import Config
try:
from unittest.mock import Mock
except ImportError:
from mock import Mock # Python 2
def test_runnerbase_with_nonexisting_module():
class FooRunner(RunnerBase):
module = 'nonexisiting'
assert not FooRunner.is_installed()
@pytest.mark.parametrize('pythonpath,env_pythonpath', [
([], None),
(['pythonpath'], None),
(['pythonpath'], 'old')
])
def test_runnerbase_prepare_process(monkeypatch, pythonpath, env_pythonpath):
MockQProcess = Mock()
monkeypatch.setattr('spyder_unittest.backend.runnerbase.QProcess',
MockQProcess)
mock_process = MockQProcess()
MockEnvironment = Mock()
monkeypatch.setattr(
'spyder_unittest.backend.runnerbase.QProcessEnvironment.systemEnvironment',
MockEnvironment)
mock_environment = MockEnvironment()
mock_environment.configure_mock(**{'value.return_value': env_pythonpath})
config = Config('myRunner', 'wdir')
runner = RunnerBase(None, 'results')
runner._prepare_process(config, pythonpath)
mock_process.setWorkingDirectory.assert_called_once_with('wdir')
mock_process.finished.connect.assert_called_once_with(runner.finished)
if pythonpath:
if env_pythonpath:
mock_environment.insert.assert_any_call('PYTHONPATH',
'pythonpath{}{}'.format(
os.pathsep,
env_pythonpath))
else:
mock_environment.insert.assert_any_call('PYTHONPATH', 'pythonpath')
mock_process.setProcessEnvironment.assert_called_once()
else:
mock_environment.insert.assert_not_called()
mock_process.setProcessEnvironment.assert_not_called()
def test_runnerbase_start(monkeypatch):
MockQProcess = Mock()
monkeypatch.setattr('spyder_unittest.backend.runnerbase.QProcess',
MockQProcess)
mock_process = MockQProcess()
mock_remove = Mock(side_effect=OSError())
monkeypatch.setattr('spyder_unittest.backend.runnerbase.os.remove',
mock_remove)
monkeypatch.setattr(
'spyder_unittest.backend.runnerbase.get_python_executable',
lambda: 'python')
runner = RunnerBase(None, 'results')
runner._prepare_process = lambda c, p: mock_process
runner.create_argument_list = lambda: ['arg1', 'arg2']
config = Config('pytest', 'wdir')
mock_process.waitForStarted = lambda: False
with pytest.raises(RuntimeError):
runner.start(config, ['pythondir'])
mock_process.start.assert_called_once_with('python', ['arg1', 'arg2'])
mock_remove.assert_called_once_with('results')
spyder_unittest-0.4.1/spyder_unittest/backend/tests/test_unittestrunner.py 0000644 0001750 0001750 00000012475 13646270600 027774 0 ustar jitse jitse 0000000 0000000 # -*- 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_with_two_tests():
output = """test_isupper (teststringmethods.TestStringMethods) ... ok
test_split (teststringmethods.TestStringMethods) ... ok
----------------------------------------------------------------------
Ran 2 tests in 0.012s
OK
"""
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 == []
def test_unittestrunner_load_data_with_one_test():
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
----------------------------------------------------------------------
Ran 2 tests in 0.012s
FAILED (failures=1)
"""
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[0].startswith('Traceback')
assert res[0].extra_text[-1].endswith('AssertionError: 1 != 2')
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_unittestrunner_load_data_with_comment():
output = """test1 (test_foo.Bar)
comment ... ok
test2 (test_foo.Bar) ... ok
----------------------------------------------------------------------
Ran 2 tests in 0.000s
OK
"""
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 == 'test_foo.Bar.test1'
assert res[0].extra_text == []
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_unittestrunner_load_data_with_fail_and_comment():
output = """test1 (test_foo.Bar)
comment ... FAIL
======================================================================
FAIL: test1 (test_foo.Bar)
comment
----------------------------------------------------------------------
Traceback (most recent call last):
File "/somepath/test_foo.py", line 30, in test1
self.assertEqual(1, 2)
AssertionError: 1 != 2
----------------------------------------------------------------------
Ran 1 test in 0.000s
FAILED (failures=1)
"""
runner = UnittestRunner(None)
res = runner.load_data(output)
assert len(res) == 1
assert res[0].category == Category.FAIL
assert res[0].status == 'FAIL'
assert res[0].name == 'test_foo.Bar.test1'
assert res[0].extra_text[0].startswith('Traceback')
assert res[0].extra_text[-1].endswith('AssertionError: 1 != 2')
def test_try_parse_header_with_ok():
runner = UnittestRunner(None)
lines = ['test_isupper (testfoo.TestStringMethods) ... ok']
res = runner.try_parse_result(lines, 0)
assert res == (1, 'test_isupper', 'testfoo.TestStringMethods', 'ok', '')
def test_try_parse_header_with_xfail():
runner = UnittestRunner(None)
lines = ['test_isupper (testfoo.TestStringMethods) ... expected failure']
res = runner.try_parse_result(lines, 0)
assert res == (1, 'test_isupper', 'testfoo.TestStringMethods',
'expected failure', '')
def test_try_parse_header_with_message():
runner = UnittestRunner(None)
lines = ["test_nothing (testfoo.Tests) ... skipped 'msg'"]
res = runner.try_parse_result(lines, 0)
assert res == (1, 'test_nothing', 'testfoo.Tests', 'skipped', 'msg')
def test_try_parse_header_starting_with_digit():
runner = UnittestRunner(None)
lines = ['0est_isupper (testfoo.TestStringMethods) ... ok']
res = runner.try_parse_result(lines, 0)
assert res is None
def test_get_versions(monkeypatch):
import platform
monkeypatch.setattr(platform, 'python_version', lambda: '1.2.3')
runner = UnittestRunner(None)
assert runner.get_versions() == ['unittest 1.2.3']
spyder_unittest-0.4.1/spyder_unittest/backend/tests/test_zmqstream.py 0000644 0001750 0001750 00000001033 13247011367 026672 0 ustar jitse jitse 0000000 0000000 # -*- coding: utf-8 -*-
#
# Copyright © 2018 Spyder Project Contributors
# Licensed under the terms of the MIT License
# (see LICENSE.txt for details)
"""Tests for zmqstream.py"""
# Local imports
from spyder_unittest.backend.zmqstream import ZmqStreamReader, ZmqStreamWriter
def test_zmqstream(qtbot):
manager = ZmqStreamReader()
worker = ZmqStreamWriter(manager.port)
with qtbot.waitSignal(manager.sig_received) as blocker:
worker.write(42)
assert blocker.args == [[42]]
worker.close()
manager.close()
spyder_unittest-0.4.1/spyder_unittest/backend/unittestrunner.py 0000644 0001750 0001750 00000012355 13646270600 025570 0 ustar jitse jitse 0000000 0000000 # -*- 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
# Local imports
from spyder_unittest.backend.runnerbase import Category, RunnerBase, TestResult
class UnittestRunner(RunnerBase):
"""Class for running tests with unittest module in standard library."""
module = 'unittest'
name = 'unittest'
def get_versions(self):
"""
Return versions of framework and its plugins.
As 'unittest' is a built-in framework, we return the python version.
"""
import platform
return ['unittest {}'.format(platform.python_version())]
def create_argument_list(self):
"""Create argument list for testing process."""
return ['-m', self.module, 'discover', '-v']
def finished(self):
"""
Called when the unit test process has finished.
This function reads the results and emits `sig_finished`.
"""
output = self.read_all_process_output()
testresults = self.load_data(output)
self.sig_finished.emit(testresults, output)
def load_data(self, output):
"""
Read and parse output from unittest module.
Any parsing errors are silently ignored.
Returns
-------
list of TestResult
Unit test results.
"""
res = []
lines = output.splitlines()
line_index = 0
try:
while lines[line_index]:
data = self.try_parse_result(lines, line_index)
if data:
line_index = data[0]
if data[3] == 'ok':
cat = Category.OK
elif data[3] == 'FAIL' or data[3] == 'ERROR':
cat = Category.FAIL
else:
cat = Category.SKIP
name = '{}.{}'.format(data[2], data[1])
tr = TestResult(category=cat, status=data[3], name=name,
message=data[4])
res.append(tr)
else:
line_index += 1
line_index += 1
while not (lines[line_index]
and all(c == '-' for c in lines[line_index])):
data = self.try_parse_exception_block(lines, line_index)
if data:
line_index = data[0]
test_index = next(
i for i, tr in enumerate(res)
if tr.name == '{}.{}'.format(data[2], data[1]))
res[test_index].extra_text = data[3]
else:
line_index += 1
except IndexError:
pass
return res
def try_parse_result(self, lines, line_index):
"""
Try to parse one or more lines of text as a test result.
Returns
-------
(int, str, str, str, str) or None
If a test result is parsed successfully then return a tuple with
the line index of the first line after the test result, 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.]*)\)'
match = re.match(regexp, lines[line_index])
if match:
function_name = match.group(1)
class_name = match.group(2)
else:
return None
while lines[line_index]:
regexp = (r' \.\.\. (ok|FAIL|ERROR|skipped|expected failure|'
r"unexpected success)( '([^']*)')?\Z")
match = re.search(regexp, lines[line_index])
if match:
result = match.group(1)
msg = match.group(3) or ''
return (line_index + 1, function_name, class_name, result, msg)
line_index += 1
return None
def try_parse_exception_block(self, lines, line_index):
"""
Try to parse a block detailing an exception in unittest output.
Returns
-------
(int, str, str, list of str) or None
If an exception block is parsed successfully, then return a tuple
with the line index of the first line after the block, the name of
the test function, the name of the test class, and the text of the
exception. Otherwise, return None.
"""
if not all(char == '=' for char in lines[line_index]):
return None
regexp = r'\w+: ([^\d\W]\w*) \(([^\d\W][\w.]*)\)\Z'
match = re.match(regexp, lines[line_index + 1])
if not match:
return None
line_index += 1
while not all(char == '-' for char in lines[line_index]):
if not lines[line_index]:
return None
line_index += 1
line_index += 1
exception_text = []
while lines[line_index]:
exception_text.append(lines[line_index])
line_index += 1
return (line_index, match.group(1), match.group(2), exception_text)
spyder_unittest-0.4.1/spyder_unittest/backend/zmqstream.py 0000644 0001750 0001750 00000006572 13247011367 024506 0 ustar jitse jitse 0000000 0000000 # -*- coding: utf-8 -*-
#
# Copyright © 2018 Spyder Project Contributors
# Licensed under the terms of the MIT License
# (see LICENSE.txt for details)
"""
Reader and writer for sending stream of python objects over a ZMQ socket.
The intended usage is that you construct a reader in one process and a writer
(with the same port number as the reader) in a worker process. The worker
process can then use the stream to send its result to the reader.
"""
from __future__ import print_function
# Standard library imports
import sys
# Third party imports
from qtpy.QtCore import QObject, QProcess, QSocketNotifier, Signal
from qtpy.QtWidgets import QApplication
import zmq
class ZmqStreamWriter:
"""Writer for sending stream of Python object over a ZMQ stream."""
def __init__(self, port):
"""
Constructor.
Arguments
---------
port : int
TCP port number to be used for the stream. This should equal the
`port` attribute of the corresponding `ZmqStreamReader`.
"""
context = zmq.Context()
self.socket = context.socket(zmq.PAIR)
self.socket.connect('tcp://localhost:{}'.format(port))
def write(self, obj):
"""Write arbitrary Python object to stream."""
self.socket.send_pyobj(obj)
def close(self):
"""Close stream."""
self.socket.close()
class ZmqStreamReader(QObject):
"""
Reader for receiving stream of Python objects via a ZMQ stream.
Attributes
----------
port : int
TCP port number used for the stream.
Signals
-------
sig_received(list)
Emitted when objects are received; argument is list of received
objects.
"""
sig_received = Signal(object)
def __init__(self):
"""Constructor; also constructs ZMQ stream."""
super(QObject, self).__init__()
self.context = zmq.Context()
self.socket = self.context.socket(zmq.PAIR)
self.port = self.socket.bind_to_random_port('tcp://*')
fid = self.socket.getsockopt(zmq.FD)
self.notifier = QSocketNotifier(fid, QSocketNotifier.Read, self)
self.notifier.activated.connect(self.received_message)
def received_message(self):
"""Called when a message is received."""
self.notifier.setEnabled(False)
messages = []
try:
while 1:
message = self.socket.recv_pyobj(flags=zmq.NOBLOCK)
messages.append(message)
except zmq.ZMQError:
pass
finally:
self.notifier.setEnabled(True)
if messages:
self.sig_received.emit(messages)
def close(self):
"""Read any remaining messages and close stream."""
self.received_message() # Flush remaining messages
self.notifier.setEnabled(False)
self.socket.close()
self.context.destroy()
if __name__ == '__main__':
# For testing, construct a ZMQ stream between two processes and send
# the number 42 over the stream
if len(sys.argv) == 1:
app = QApplication(sys.argv)
manager = ZmqStreamReader()
manager.sig_received.connect(print)
process = QProcess()
process.start('python', [sys.argv[0], str(manager.port)])
process.finished.connect(app.quit)
sys.exit(app.exec_())
else:
worker = ZmqStreamWriter(sys.argv[1])
worker.write(42)
spyder_unittest-0.4.1/spyder_unittest/tests/ 0000755 0001750 0001750 00000000000 13662215740 021654 5 ustar jitse jitse 0000000 0000000 spyder_unittest-0.4.1/spyder_unittest/tests/test_unittestplugin.py 0000644 0001750 0001750 00000011437 13603155222 026362 0 ustar jitse jitse 0000000 0000000 # -*- coding: utf-8 -*-
#
# Copyright © 2017 Spyder Project Contributors
# Licensed under the terms of the MIT License
# (see LICENSE.txt for details)
"""Tests for unittestplugin.py"""
# Third party imports
import pytest
from spyder.plugins.projects.projecttypes import EmptyProject
# Local imports
from spyder_unittest.unittestplugin import UnitTestPlugin
from spyder_unittest.widgets.configdialog import Config
try:
from unittest.mock import Mock
except ImportError:
from mock import Mock # Python 2
class PluginForTesting(UnitTestPlugin):
CONF_FILE = False
def __init__(self, parent):
UnitTestPlugin.__init__(self, parent)
@pytest.fixture
def plugin(qtbot):
"""Set up the unittest plugin."""
res = PluginForTesting(None)
qtbot.addWidget(res)
res.main = Mock()
res.main.get_spyder_pythonpath = lambda: 'fakepythonpath'
res.main.run_menu_actions = [42]
res.main.editor.pythonfile_dependent_actions = [42]
res.main.projects.get_active_project_path = lambda: None
res.register_plugin()
return res
def test_plugin_initialization(plugin):
plugin.show()
assert len(plugin.main.run_menu_actions) == 2
assert plugin.main.run_menu_actions[1].text() == 'Run unit tests'
def test_plugin_pythonpath(plugin):
# Test signal/slot connection
plugin.main.sig_pythonpath_changed.connect.assert_called_with(
plugin.update_pythonpath)
# Test pythonpath is set to path provided by Spyder
assert plugin.unittestwidget.pythonpath == 'fakepythonpath'
# Test that change in path propagates
plugin.main.get_spyder_pythonpath = lambda: 'anotherpath'
plugin.update_pythonpath()
assert plugin.unittestwidget.pythonpath == 'anotherpath'
def test_plugin_wdir(plugin, monkeypatch, tmpdir):
# Test signal/slot connections
plugin.main.workingdirectory.set_explorer_cwd.connect.assert_called_with(
plugin.update_default_wdir)
plugin.main.projects.sig_project_created.connect.assert_called_with(
plugin.handle_project_change)
plugin.main.projects.sig_project_loaded.connect.assert_called_with(
plugin.handle_project_change)
plugin.main.projects.sig_project_closed.connect.assert_called_with(
plugin.handle_project_change)
# Test default_wdir is set to current working dir
monkeypatch.setattr('spyder_unittest.unittestplugin.getcwd',
lambda: 'fakecwd')
plugin.update_default_wdir()
assert plugin.unittestwidget.default_wdir == 'fakecwd'
# Test after opening project, default_wdir is set to project dir
project = EmptyProject(str(tmpdir))
plugin.main.projects.get_active_project = lambda: project
plugin.main.projects.get_active_project_path = lambda: project.root_path
plugin.handle_project_change()
assert plugin.unittestwidget.default_wdir == str(tmpdir)
# Test after closing project, default_wdir is set back to cwd
plugin.main.projects.get_active_project = lambda: None
plugin.main.projects.get_active_project_path = lambda: None
plugin.handle_project_change()
assert plugin.unittestwidget.default_wdir == 'fakecwd'
def test_plugin_config(plugin, tmpdir, qtbot):
# Test config file does not exist and config is empty
config_file_path = tmpdir.join('.spyproject', 'config', 'unittest.ini')
assert not config_file_path.check()
assert plugin.unittestwidget.config is None
# Open project
project = EmptyProject(str(tmpdir))
plugin.main.projects.get_active_project = lambda: project
plugin.main.projects.get_active_project_path = lambda: project.root_path
plugin.handle_project_change()
# Test config file does exist but config is empty
assert config_file_path.check()
assert 'framework = ' in config_file_path.read().splitlines()
assert plugin.unittestwidget.config is None
# Set config and test that this is recorded in config file
config = Config(framework='unittest', wdir=str(tmpdir))
with qtbot.waitSignal(plugin.unittestwidget.sig_newconfig):
plugin.unittestwidget.config = config
assert 'framework = unittest' in config_file_path.read().splitlines()
# Close project and test that config is empty
plugin.main.projects.get_active_project = lambda: None
plugin.main.projects.get_active_project_path = lambda: None
plugin.handle_project_change()
assert plugin.unittestwidget.config is None
# Re-open project and test that config is correctly read
plugin.main.projects.get_active_project = lambda: project
plugin.main.projects.get_active_project_path = lambda: project.root_path
plugin.handle_project_change()
assert plugin.unittestwidget.config == config
def test_plugin_goto_in_editor(plugin, qtbot):
plugin.unittestwidget.sig_edit_goto.emit('somefile', 42)
plugin.main.editor.load.assert_called_with('somefile', 43, '')
spyder_unittest-0.4.1/spyder_unittest/unittestplugin.py 0000644 0001750 0001750 00000022114 13662040473 024161 0 ustar jitse jitse 0000000 0000000 # -*- coding: utf-8 -*-
#
# Copyright © 2013 Spyder Project Contributors
# Licensed under the terms of the MIT License
# (see LICENSE.txt for details)
"""Unit testing Plugin."""
# Standard library imports
import os.path as osp
# Third party imports
from qtpy.QtWidgets import QVBoxLayout
from spyder.api.plugins import SpyderPluginWidget
from spyder.config.base import get_translation
from spyder.config.gui import is_dark_interface
from spyder.py3compat import PY2, getcwd
from spyder.utils import icon_manager as ima
from spyder.utils.qthelpers import create_action
# Local imports
from spyder_unittest.widgets.configdialog import Config
from spyder_unittest.widgets.unittestgui import UnitTestWidget
_ = get_translation("unittest", dirname="spyder_unittest")
class UnitTestPlugin(SpyderPluginWidget):
"""Spyder plugin for unit testing."""
CONF_SECTION = 'unittest'
CONF_DEFAULTS = [(CONF_SECTION, {'framework': '', 'wdir': ''})]
CONF_NAMEMAP = {CONF_SECTION: [(CONF_SECTION, ['framework', 'wdir'])]}
CONF_VERSION = '0.1.0'
def __init__(self, parent):
"""
Initialize plugin and corresponding widget.
The part of the initialization that depends on `parent` is done in
`self.register_plugin()`.
"""
SpyderPluginWidget.__init__(self, parent)
# Create unit test widget and add to dockwindow
self.unittestwidget = UnitTestWidget(
self.main,
options_button=self.options_button,
options_menu=self._options_menu)
layout = QVBoxLayout()
layout.addWidget(self.unittestwidget)
self.setLayout(layout)
def update_pythonpath(self):
"""
Update Python path used to run unit tests.
This function is called whenever the Python path set in Spyder changes.
It synchronizes the Python path in the unittest widget with the Python
path in Spyder.
"""
self.unittestwidget.pythonpath = self.main.get_spyder_pythonpath()
def handle_project_change(self):
"""
Handle the event where the current project changes.
This updates the default working directory for running tests and loads
the test configuration from the project preferences.
"""
self.update_default_wdir()
self.load_config()
def update_default_wdir(self):
"""
Update default working dir for running unit tests.
The default working dir for running unit tests is set to the project
directory if a project is open, or the current working directory if no
project is opened. This function is called whenever this directory may
change.
"""
wdir = self.main.projects.get_active_project_path()
if not wdir: # if no project opened
wdir = getcwd()
self.unittestwidget.default_wdir = wdir
def load_config(self):
"""
Load test configuration from project preferences.
If the test configuration stored in the project preferences is valid,
then use it. If it is not valid (e.g., because the user never
configured testing for this project) or no project is opened, then
invalidate the current test configuration.
If necessary, patch the project preferences to include this plugin's
config options.
"""
project = self.main.projects.get_active_project()
if not project:
self.unittestwidget.set_config_without_emit(None)
return
if self.CONF_SECTION not in project.config._name_map:
project.config._name_map = project.config._name_map.copy()
project.config._name_map.update(self.CONF_NAMEMAP)
if self.CONF_SECTION not in project.config._configs_map:
config_class = project.config.get_config_class()
path = osp.join(project.root_path, '.spyproject', 'config')
conf = config_class(
name=self.CONF_SECTION,
defaults=self.CONF_DEFAULTS,
path=path,
load=True,
version=self.CONF_VERSION)
project.config._configs_map[self.CONF_SECTION] = conf
new_config = Config(
framework=project.get_option(self.CONF_SECTION, 'framework'),
wdir=project.get_option(self.CONF_SECTION, 'wdir'))
if not self.unittestwidget.config_is_valid(new_config):
new_config = None
self.unittestwidget.set_config_without_emit(new_config)
def save_config(self, test_config):
"""
Save test configuration in project preferences.
If no project is opened, then do not save.
"""
project = self.main.projects.get_active_project()
if not project:
return
project.set_option(self.CONF_SECTION, 'framework',
test_config.framework)
project.set_option(self.CONF_SECTION, 'wdir', test_config.wdir)
def goto_in_editor(self, filename, lineno):
"""
Go to specified line in editor.
This function is called when the unittest widget emits `sig_edit_goto`.
Note that the line number in the signal is zero based (the first line
is line 0), but the editor expects a one-based line number.
"""
self.main.editor.load(filename, lineno + 1, '')
# ----- SpyderPluginWidget API --------------------------------------------
def get_plugin_title(self):
"""Return widget title."""
return _("Unit testing")
def get_plugin_icon(self):
"""Return widget icon."""
return ima.icon('profiler')
def get_focus_widget(self):
"""Return the widget to give focus to this dockwidget when raised."""
return self.unittestwidget.testdataview
def get_plugin_actions(self):
"""Return a list of actions related to plugin."""
return self.unittestwidget.create_actions()
def on_first_registration(self):
"""Action to be performed on first plugin registration."""
self.main.tabify_plugins(self.main.help, self)
self.dockwidget.hide()
def register_plugin(self):
"""Register plugin in Spyder's main window."""
super(UnitTestPlugin, self).register_plugin()
# Get information from Spyder proper into plugin
self.update_pythonpath()
self.update_default_wdir()
self.unittestwidget.use_dark_interface(is_dark_interface())
# Connect to relevant signals
self.main.sig_pythonpath_changed.connect(self.update_pythonpath)
self.main.workingdirectory.set_explorer_cwd.connect(
self.update_default_wdir)
self.main.projects.sig_project_created.connect(
self.handle_project_change)
self.main.projects.sig_project_loaded.connect(
self.handle_project_change)
self.main.projects.sig_project_closed.connect(
self.handle_project_change)
self.unittestwidget.sig_newconfig.connect(self.save_config)
self.unittestwidget.sig_edit_goto.connect(self.goto_in_editor)
# Create action and add it to Spyder's menu
unittesting_act = create_action(
self,
_("Run unit tests"),
icon=ima.icon('profiler'),
shortcut="Shift+Alt+F11",
triggered=self.maybe_configure_and_start)
self.main.run_menu_actions += [unittesting_act]
self.main.editor.pythonfile_dependent_actions += [unittesting_act]
# Save all files before running tests
self.unittestwidget.pre_test_hook = self.main.editor.save_all
def refresh_plugin(self):
"""Refresh unit testing widget."""
self._options_menu.clear()
self.get_plugin_actions()
def closing_plugin(self, cancelable=False):
"""Perform actions before parent main window is closed."""
return True
def apply_plugin_settings(self, options):
"""Apply configuration file's plugin settings."""
pass
def check_compatibility(self):
"""
Check compatibility of the plugin.
This checks that the plugin is not run under Python 2.
Returns
-------
(bool, str)
The first value tells Spyder if the plugin has passed the
compatibility test defined in this method. The second value
is a message that must explain users why the plugin was
found to be incompatible (e.g. 'This plugin does not work
with PyQt4'). It will be shown at startup in a QMessageBox.
"""
if PY2:
msg = _('The unittest plugin does not work with Python 2.')
return (False, msg)
else:
return (True, '')
# ----- Public API --------------------------------------------------------
def maybe_configure_and_start(self):
"""
Ask for configuration if necessary and then run tests.
Raise unittest widget. If the current test configuration is
not valid (or not set), then ask the user to configure. Then
run the tests.
"""
if self.dockwidget:
self.switch_to_plugin()
self.unittestwidget.maybe_configure_and_start()
spyder_unittest-0.4.1/spyder_unittest/widgets/ 0000755 0001750 0001750 00000000000 13662215740 022160 5 ustar jitse jitse 0000000 0000000 spyder_unittest-0.4.1/spyder_unittest/widgets/__init__.py 0000644 0001750 0001750 00000000273 13047645510 024272 0 ustar jitse jitse 0000000 0000000 # -*- coding: utf-8 -*-
#
# Copyright © 2013 Spyder Project Contributors
# Licensed under the terms of the MIT License
# (see LICENSE.txt for details)
"""Widgets for unittest plugin."""
spyder_unittest-0.4.1/spyder_unittest/widgets/configdialog.py 0000644 0001750 0001750 00000012676 13311025216 025157 0 ustar jitse jitse 0000000 0000000 # -*- coding: utf-8 -*-
#
# Copyright © 2013 Spyder Project Contributors
# Licensed under the terms of the MIT License
# (see LICENSE.txt for details)
"""
Functionality for asking the user to specify the test configuration.
The main entry point is `ask_for_config()`.
"""
# Standard library imports
from collections import namedtuple
import os.path as osp
# Third party imports
from qtpy.compat import getexistingdirectory
from qtpy.QtCore import Slot
from qtpy.QtWidgets import (QApplication, QComboBox, QDialog, QDialogButtonBox,
QHBoxLayout, QLabel, QLineEdit, QPushButton,
QVBoxLayout)
from spyder.config.base import get_translation
from spyder.py3compat import getcwd, to_text_string
from spyder.utils import icon_manager as ima
try:
_ = get_translation("unittest", dirname="spyder_unittest")
except KeyError:
import gettext
_ = gettext.gettext
Config = namedtuple('Config', ['framework', 'wdir'])
Config.__new__.__defaults__ = (None, '')
class ConfigDialog(QDialog):
"""
Dialog window for specifying test configuration.
The window contains a combobox with all the frameworks, a line edit box for
specifying the working directory, a button to use a file browser for
selecting the directory, and OK and Cancel buttons. Initially, no framework
is selected and the OK button is disabled. Selecting a framework enables
the OK button.
"""
def __init__(self, frameworks, config, parent=None):
"""
Construct a dialog window.
Parameters
----------
frameworks : dict of (str, type)
Names of all supported frameworks with their associated class
(assumed to be a subclass of RunnerBase)
config : Config
Initial configuration
parent : QWidget
"""
super(ConfigDialog, self).__init__(parent)
self.setWindowTitle(_('Configure tests'))
layout = QVBoxLayout(self)
framework_layout = QHBoxLayout()
framework_label = QLabel(_('Test framework'))
framework_layout.addWidget(framework_label)
self.framework_combobox = QComboBox(self)
for ix, (name, runner) in enumerate(sorted(frameworks.items())):
installed = runner.is_installed()
if installed:
label = name
else:
label = '{} ({})'.format(name, _('not available'))
self.framework_combobox.addItem(label)
self.framework_combobox.model().item(ix).setEnabled(installed)
framework_layout.addWidget(self.framework_combobox)
layout.addLayout(framework_layout)
layout.addSpacing(10)
wdir_label = QLabel(_('Directory from which to run tests'))
layout.addWidget(wdir_label)
wdir_layout = QHBoxLayout()
self.wdir_lineedit = QLineEdit(self)
wdir_layout.addWidget(self.wdir_lineedit)
self.wdir_button = QPushButton(ima.icon('DirOpenIcon'), '', self)
self.wdir_button.setToolTip(_("Select directory"))
self.wdir_button.clicked.connect(lambda: self.select_directory())
wdir_layout.addWidget(self.wdir_button)
layout.addLayout(wdir_layout)
layout.addSpacing(20)
self.buttons = QDialogButtonBox(QDialogButtonBox.Ok |
QDialogButtonBox.Cancel)
layout.addWidget(self.buttons)
self.buttons.accepted.connect(self.accept)
self.buttons.rejected.connect(self.reject)
self.ok_button = self.buttons.button(QDialogButtonBox.Ok)
self.ok_button.setEnabled(False)
self.framework_combobox.currentIndexChanged.connect(
self.framework_changed)
self.framework_combobox.setCurrentIndex(-1)
if config.framework:
index = self.framework_combobox.findText(config.framework)
if index != -1:
self.framework_combobox.setCurrentIndex(index)
self.wdir_lineedit.setText(config.wdir)
@Slot(int)
def framework_changed(self, index):
"""Called when selected framework changes."""
if index != -1:
self.ok_button.setEnabled(True)
def select_directory(self):
"""Display dialog for user to select working directory."""
basedir = to_text_string(self.wdir_lineedit.text())
if not osp.isdir(basedir):
basedir = getcwd()
title = _("Select directory")
directory = getexistingdirectory(self, title, basedir)
if directory:
self.wdir_lineedit.setText(directory)
def get_config(self):
"""
Return the test configuration specified by the user.
Returns
-------
Config
Test configuration
"""
framework = self.framework_combobox.currentText()
if framework == '':
framework = None
return Config(framework=framework, wdir=self.wdir_lineedit.text())
def ask_for_config(frameworks, config, parent=None):
"""
Ask user to specify a test configuration.
This is a convenience function which displays a modal dialog window
of type `ConfigDialog`.
"""
dialog = ConfigDialog(frameworks, config, parent)
result = dialog.exec_()
if result == QDialog.Accepted:
return dialog.get_config()
if __name__ == '__main__':
app = QApplication([])
frameworks = ['nose', 'pytest', 'unittest']
config = Config(framework=None, wdir=getcwd())
print(ask_for_config(frameworks, config))
spyder_unittest-0.4.1/spyder_unittest/widgets/datatree.py 0000644 0001750 0001750 00000036434 13645540415 024336 0 ustar jitse jitse 0000000 0000000 # -*- coding: utf-8 -*-
#
# Copyright © 2017 Spyder Project Contributors
# Licensed under the terms of the MIT License
# (see LICENSE.txt for details)
"""Model and view classes for storing and displaying test results."""
# Standard library imports
from collections import Counter
from operator import attrgetter
# Third party imports
from qtpy import PYQT4
from qtpy.QtCore import QAbstractItemModel, QModelIndex, Qt, Signal
from qtpy.QtGui import QBrush, QColor, QFont
from qtpy.QtWidgets import QMenu, QTreeView
from spyder.config.base import get_translation
from spyder.utils.qthelpers import create_action
# Local imports
from spyder_unittest.backend.abbreviator import Abbreviator
from spyder_unittest.backend.runnerbase import Category
try:
_ = get_translation("unittest", dirname="spyder_unittest")
except KeyError:
import gettext
_ = gettext.gettext
COLORS = {
Category.OK: QBrush(QColor("#C1FFBA")),
Category.FAIL: QBrush(QColor("#FF5050")),
Category.SKIP: QBrush(QColor("#C5C5C5")),
Category.PENDING: QBrush(QColor("#C5C5C5"))
}
COLORS_DARK = {
Category.OK: QBrush(QColor("#008000")),
Category.FAIL: QBrush(QColor("#C6001E")),
Category.SKIP: QBrush(QColor("#505050")),
Category.PENDING: QBrush(QColor("#505050"))
}
STATUS_COLUMN = 0
NAME_COLUMN = 1
MESSAGE_COLUMN = 2
TIME_COLUMN = 3
HEADERS = [_('Status'), _('Name'), _('Message'), _('Time (ms)')]
TOPLEVEL_ID = 2 ** 32 - 1
class TestDataView(QTreeView):
"""
Tree widget displaying test results.
Signals
-------
sig_edit_goto(str, int): Emitted if editor should go to some position.
Arguments are file name and line number (zero-based).
"""
sig_edit_goto = Signal(str, int)
def __init__(self, parent=None):
"""Constructor."""
QTreeView.__init__(self, parent)
self.header().setDefaultAlignment(Qt.AlignCenter)
self.setItemsExpandable(True)
self.setSortingEnabled(True)
self.header().setSortIndicatorShown(False)
self.header().sortIndicatorChanged.connect(self.sortByColumn)
self.header().sortIndicatorChanged.connect(
lambda col, order: self.header().setSortIndicatorShown(True))
self.setExpandsOnDoubleClick(False)
self.doubleClicked.connect(self.go_to_test_definition)
def reset(self):
"""
Reset internal state of the view and read all data afresh from model.
This function is called whenever the model data changes drastically.
"""
QTreeView.reset(self)
self.resizeColumns()
self.spanFirstColumn(0, self.model().rowCount() - 1)
def rowsInserted(self, parent, firstRow, lastRow):
"""Called when rows are inserted."""
QTreeView.rowsInserted(self, parent, firstRow, lastRow)
self.resizeColumns()
self.spanFirstColumn(firstRow, lastRow)
def dataChanged(self, topLeft, bottomRight, roles=[]):
"""Called when data in model has changed."""
if PYQT4:
QTreeView.dataChanged(self, topLeft, bottomRight)
else:
QTreeView.dataChanged(self, topLeft, bottomRight, roles)
self.resizeColumns()
while topLeft.parent().isValid():
topLeft = topLeft.parent()
while bottomRight.parent().isValid():
bottomRight = bottomRight.parent()
self.spanFirstColumn(topLeft.row(), bottomRight.row())
def contextMenuEvent(self, event):
"""Called when user requests a context menu."""
index = self.indexAt(event.pos())
index = self.make_index_canonical(index)
if not index:
return # do nothing if no item under mouse position
contextMenu = self.build_context_menu(index)
contextMenu.exec_(event.globalPos())
def go_to_test_definition(self, index):
"""Ask editor to go to definition of test corresponding to index."""
index = self.make_index_canonical(index)
filename, lineno = self.model().data(index, Qt.UserRole)
if filename is not None:
if lineno is None:
lineno = 0
self.sig_edit_goto.emit(filename, lineno)
def make_index_canonical(self, index):
"""
Convert given index to canonical index for the same test.
For every test, the canonical index points to the item on the top level
in the first column corresponding to the given position. If the given
index is invalid, then return None.
"""
if not index.isValid():
return None
while index.parent().isValid(): # find top-level node
index = index.parent()
index = index.sibling(index.row(), 0) # go to first column
return index
def build_context_menu(self, index):
"""Build context menu for test item that given index points to."""
contextMenu = QMenu(self)
if self.isExpanded(index):
menuItem = create_action(self, _('Collapse'),
triggered=lambda: self.collapse(index))
else:
menuItem = create_action(self, _('Expand'),
triggered=lambda: self.expand(index))
menuItem.setEnabled(self.model().hasChildren(index))
contextMenu.addAction(menuItem)
menuItem = create_action(
self, _('Go to definition'),
triggered=lambda: self.go_to_test_definition(index))
test_location = self.model().data(index, Qt.UserRole)
menuItem.setEnabled(test_location[0] is not None)
contextMenu.addAction(menuItem)
return contextMenu
def resizeColumns(self):
"""Resize column to fit their contents."""
for col in range(self.model().columnCount()):
self.resizeColumnToContents(col)
def spanFirstColumn(self, firstRow, lastRow):
"""
Make first column span whole row in second-level children.
Note: Second-level children display the test output.
Arguments
---------
firstRow : int
Index of first row to act on.
lastRow : int
Index of last row to act on. Note that this row is included in the
range, following Qt conventions and contrary to Python conventions.
"""
model = self.model()
for row in range(firstRow, lastRow + 1):
index = model.index(row, 0)
for i in range(model.rowCount(index)):
self.setFirstColumnSpanned(i, index, True)
class TestDataModel(QAbstractItemModel):
"""
Model class storing test results for display.
Test results are stored as a list of TestResults in the property
`self.testresults`. Every test is exposed as a child of the root node,
with extra information as second-level nodes.
As in every model, an iteem of data is identified by its index, which is
a tuple (row, column, id). The id is TOPLEVEL_ID for top-level items.
For level-2 items, the id is the index of the test in `self.testresults`.
Attributes
----------
is_dark_interface : bool
Whether to use colours appropriate for a dark user interface.
Signals
-------
sig_summary(str)
Emitted with new summary if test results change.
"""
sig_summary = Signal(str)
def __init__(self, parent=None):
"""Constructor."""
QAbstractItemModel.__init__(self, parent)
self.abbreviator = Abbreviator()
self.is_dark_interface = False
self.testresults = []
try:
self.monospace_font = parent.window().editor.get_plugin_font()
except AttributeError: # If run standalone for testing
self.monospace_font = QFont("Courier New")
self.monospace_font.setPointSize(10)
@property
def testresults(self):
"""List of test results."""
return self._testresults
@testresults.setter
def testresults(self, new_value):
"""Setter for test results."""
self.beginResetModel()
self.abbreviator = Abbreviator(res.name for res in new_value)
self._testresults = new_value
self.endResetModel()
self.emit_summary()
def add_testresults(self, new_tests):
"""
Add new test results to the model.
Arguments
---------
new_tests : list of TestResult
"""
firstRow = len(self.testresults)
lastRow = firstRow + len(new_tests) - 1
for test in new_tests:
self.abbreviator.add(test.name)
self.beginInsertRows(QModelIndex(), firstRow, lastRow)
self.testresults.extend(new_tests)
self.endInsertRows()
self.emit_summary()
def update_testresults(self, new_results):
"""
Update some test results by new results.
The tests in `new_results` should already be included in
`self.testresults` (otherwise a `KeyError` is raised). This function
replaces the existing results by `new_results`.
Arguments
---------
new_results: list of TestResult
"""
idx_min = idx_max = None
for new_result in new_results:
for (idx, old_result) in enumerate(self.testresults):
if old_result.name == new_result.name:
self.testresults[idx] = new_result
if idx_min is None:
idx_min = idx_max = idx
else:
idx_min = min(idx_min, idx)
idx_max = max(idx_max, idx)
break
else:
raise KeyError('test not found')
if idx_min is not None:
self.dataChanged.emit(self.index(idx_min, 0),
self.index(idx_max, len(HEADERS) - 1))
self.emit_summary()
def index(self, row, column, parent=QModelIndex()):
"""
Construct index to given item of data.
If `parent` not valid, then the item of data is on the top level.
"""
if not self.hasIndex(row, column, parent): # check bounds etc.
return QModelIndex()
if not parent.isValid():
return self.createIndex(row, column, TOPLEVEL_ID)
else:
testresult_index = parent.row()
return self.createIndex(row, column, testresult_index)
def data(self, index, role):
"""
Return data in `role` for item of data that `index` points to.
If `role` is `DisplayRole`, then return string to display.
If `role` is `TooltipRole`, then return string for tool tip.
If `role` is `FontRole`, then return monospace font for level-2 items.
If `role` is `BackgroundRole`, then return background color.
If `role` is `TextAlignmentRole`, then return right-aligned for time.
If `role` is `UserRole`, then return location of test as (file, line).
"""
if not index.isValid():
return None
row = index.row()
column = index.column()
id = index.internalId()
if role == Qt.DisplayRole:
if id != TOPLEVEL_ID:
return self.testresults[id].extra_text[index.row()]
elif column == STATUS_COLUMN:
return self.testresults[row].status
elif column == NAME_COLUMN:
return self.abbreviator.abbreviate(self.testresults[row].name)
elif column == MESSAGE_COLUMN:
return self.testresults[row].message
elif column == TIME_COLUMN:
time = self.testresults[row].time
return '' if time is None else '{:.2f}'.format(time * 1e3)
elif role == Qt.ToolTipRole:
if id == TOPLEVEL_ID and column == NAME_COLUMN:
return self.testresults[row].name
elif role == Qt.FontRole:
if id != TOPLEVEL_ID:
return self.monospace_font
elif role == Qt.BackgroundRole:
if id == TOPLEVEL_ID:
testresult = self.testresults[row]
if self.is_dark_interface:
return COLORS_DARK[testresult.category]
else:
return COLORS[testresult.category]
elif role == Qt.TextAlignmentRole:
if id == TOPLEVEL_ID and column == TIME_COLUMN:
return Qt.AlignRight
elif role == Qt.UserRole:
if id == TOPLEVEL_ID:
testresult = self.testresults[row]
return (testresult.filename, testresult.lineno)
else:
return None
def headerData(self, section, orientation, role=Qt.DisplayRole):
"""Return data for specified header."""
if orientation == Qt.Horizontal and role == Qt.DisplayRole:
return HEADERS[section]
else:
return None
def parent(self, index):
"""Return index to parent of item that `index` points to."""
if not index.isValid():
return QModelIndex()
id = index.internalId()
if id == TOPLEVEL_ID:
return QModelIndex()
else:
return self.index(id, 0)
def rowCount(self, parent=QModelIndex()):
"""Return number of rows underneath `parent`."""
if not parent.isValid():
return len(self.testresults)
if parent.internalId() == TOPLEVEL_ID and parent.column() == 0:
return len(self.testresults[parent.row()].extra_text)
return 0
def columnCount(self, parent=QModelIndex()):
"""Return number of rcolumns underneath `parent`."""
if not parent.isValid():
return len(HEADERS)
else:
return 1
def sort(self, column, order):
"""Sort model by `column` in `order`."""
def key_time(result):
return result.time or -1
self.beginResetModel()
reverse = order == Qt.DescendingOrder
if column == STATUS_COLUMN:
self.testresults.sort(key=attrgetter('category', 'status'),
reverse=reverse)
elif column == NAME_COLUMN:
self.testresults.sort(key=attrgetter('name'), reverse=reverse)
elif column == MESSAGE_COLUMN:
self.testresults.sort(key=attrgetter('message'), reverse=reverse)
elif column == TIME_COLUMN:
self.testresults.sort(key=key_time, reverse=reverse)
self.endResetModel()
def summary(self):
"""Return summary for current results."""
def n_test_or_tests(n):
test_or_tests = _('test') if n == 1 else _('tests')
return '{} {}'.format(n, test_or_tests)
if not len(self.testresults):
return _('No results to show.')
counts = Counter(res.category for res in self.testresults)
if all(counts[cat] == 0
for cat in (Category.FAIL, Category.OK, Category.SKIP)):
txt = n_test_or_tests(counts[Category.PENDING])
return _('collected {}').format(txt)
msg = _('{} failed').format(n_test_or_tests(counts[Category.FAIL]))
msg += _(', {} passed').format(counts[Category.OK])
if counts[Category.SKIP]:
msg += _(', {} other').format(counts[Category.SKIP])
if counts[Category.PENDING]:
msg += _(', {} pending').format(counts[Category.PENDING])
return msg
def emit_summary(self):
"""Emit sig_summary with summary for current results."""
self.sig_summary.emit(self.summary())
spyder_unittest-0.4.1/spyder_unittest/widgets/tests/ 0000755 0001750 0001750 00000000000 13662215740 023322 5 ustar jitse jitse 0000000 0000000 spyder_unittest-0.4.1/spyder_unittest/widgets/tests/__init__.py 0000644 0001750 0001750 00000000302 13074157120 025420 0 ustar jitse jitse 0000000 0000000 # -*- coding: utf-8 -*-
#
# Copyright © 2017 Spyder Project Contributors
# Licensed under the terms of the MIT License
# (see LICENSE.txt for details)
"""Tests for spyder_unittest.widgets ."""
spyder_unittest-0.4.1/spyder_unittest/widgets/tests/test_configdialog.py 0000644 0001750 0001750 00000007112 13224452267 027362 0 ustar jitse jitse 0000000 0000000 # -*- coding: utf-8 -*-
#
# Copyright © 2013 Spyder Project Contributors
# Licensed under the terms of the MIT License
# (see LICENSE.txt for details)
"""Tests for configdialog.py."""
# Standard library imports
import os
# Third party imports
from qtpy.QtWidgets import QDialogButtonBox
# Local imports
from spyder_unittest.widgets.configdialog import Config, ConfigDialog
class SpamRunner:
name = 'spam'
@classmethod
def is_installed(cls):
return False
class HamRunner:
name = 'ham'
@classmethod
def is_installed(cls):
return True
class EggsRunner:
name = 'eggs'
@classmethod
def is_installed(cls):
return True
frameworks = {r.name: r for r in [SpamRunner, HamRunner, EggsRunner]}
def default_config():
return Config(framework=None, wdir=os.getcwd())
def test_configdialog_uses_frameworks(qtbot):
configdialog = ConfigDialog({'eggs': EggsRunner}, default_config())
assert configdialog.framework_combobox.count() == 1
assert configdialog.framework_combobox.itemText(0) == 'eggs'
def test_configdialog_indicates_unvailable_frameworks(qtbot):
configdialog = ConfigDialog({'spam': SpamRunner}, default_config())
assert configdialog.framework_combobox.count() == 1
assert configdialog.framework_combobox.itemText(
0) == 'spam (not available)'
def test_configdialog_disables_unavailable_frameworks(qtbot):
configdialog = ConfigDialog(frameworks, default_config())
model = configdialog.framework_combobox.model()
assert model.item(0).isEnabled() # eggs
assert model.item(1).isEnabled() # ham
assert not model.item(2).isEnabled() # spam
def test_configdialog_sets_initial_config(qtbot):
config = default_config()
configdialog = ConfigDialog(frameworks, config)
assert configdialog.get_config() == config
def test_configdialog_click_ham(qtbot):
configdialog = ConfigDialog(frameworks, default_config())
qtbot.addWidget(configdialog)
configdialog.framework_combobox.setCurrentIndex(1)
assert configdialog.get_config().framework == 'ham'
def test_configdialog_ok_initially_disabled(qtbot):
configdialog = ConfigDialog(frameworks, default_config())
qtbot.addWidget(configdialog)
assert not configdialog.buttons.button(QDialogButtonBox.Ok).isEnabled()
def test_configdialog_ok_setting_framework_initially_enables_ok(qtbot):
config = Config(framework='eggs', wdir=os.getcwd())
configdialog = ConfigDialog(frameworks, config)
qtbot.addWidget(configdialog)
assert configdialog.buttons.button(QDialogButtonBox.Ok).isEnabled()
def test_configdialog_clicking_pytest_enables_ok(qtbot):
configdialog = ConfigDialog(frameworks, default_config())
qtbot.addWidget(configdialog)
configdialog.framework_combobox.setCurrentIndex(1)
assert configdialog.buttons.button(QDialogButtonBox.Ok).isEnabled()
def test_configdialog_wdir_lineedit(qtbot):
configdialog = ConfigDialog(frameworks, default_config())
qtbot.addWidget(configdialog)
wdir = os.path.normpath(os.path.join(os.getcwd(), os.path.pardir))
configdialog.wdir_lineedit.setText(wdir)
assert configdialog.get_config().wdir == wdir
def test_configdialog_wdir_button(qtbot, monkeypatch):
configdialog = ConfigDialog(frameworks, default_config())
qtbot.addWidget(configdialog)
wdir = os.path.normpath(os.path.join(os.getcwd(), os.path.pardir))
monkeypatch.setattr(
'spyder_unittest.widgets.configdialog.getexistingdirectory',
lambda parent, caption, basedir: wdir)
configdialog.wdir_button.click()
assert configdialog.get_config().wdir == wdir
spyder_unittest-0.4.1/spyder_unittest/widgets/tests/test_datatree.py 0000644 0001750 0001750 00000023770 13643037630 026534 0 ustar jitse jitse 0000000 0000000 # -*- coding: utf-8 -*-
#
# Copyright © 2017 Spyder Project Contributors
# Licensed under the terms of the MIT License
# (see LICENSE.txt for details)
"""Tests for unittestgui.py."""
# Third party imports
from qtpy.QtCore import QModelIndex, QPoint, Qt
from qtpy.QtGui import QContextMenuEvent
import pytest
# Local imports
from spyder_unittest.backend.runnerbase import Category, TestResult
from spyder_unittest.widgets.datatree import (COLORS, COLORS_DARK,
TestDataModel, TestDataView)
try:
from unittest.mock import Mock
except ImportError:
from mock import Mock # Python 2
@pytest.fixture
def view_and_model(qtbot):
view = TestDataView()
model = TestDataModel()
# setModel() before populating testresults because setModel() does a sort
view.setModel(model)
res = [TestResult(Category.OK, 'status', 'foo.bar'),
TestResult(Category.FAIL, 'error', 'foo.bar', 'kadoom', 0,
'crash!\nboom!', filename='ham.py', lineno=42)]
model.testresults = res
return view, model
def test_contextMenuEvent_calls_exec(view_and_model, monkeypatch):
# test that a menu is displayed when clicking on an item
mock_exec = Mock()
monkeypatch.setattr('spyder_unittest.widgets.datatree.QMenu.exec_', mock_exec)
view, model = view_and_model
pos = view.visualRect(model.index(0, 0)).center()
event = QContextMenuEvent(QContextMenuEvent.Mouse, pos)
view.contextMenuEvent(event)
assert mock_exec.called
# test that no menu is displayed when clicking below the bottom item
mock_exec.reset_mock()
pos = view.visualRect(model.index(1, 0)).bottomRight()
pos += QPoint(0, 1)
event = QContextMenuEvent(QContextMenuEvent.Mouse, pos)
view.contextMenuEvent(event)
assert not mock_exec.called
def test_go_to_test_definition_with_invalid_target(view_and_model, qtbot):
view, model = view_and_model
with qtbot.assertNotEmitted(view.sig_edit_goto):
view.go_to_test_definition(model.index(0, 0))
def test_go_to_test_definition_with_valid_target(view_and_model, qtbot):
view, model = view_and_model
with qtbot.waitSignal(view.sig_edit_goto) as blocker:
view.go_to_test_definition(model.index(1, 0))
assert blocker.args == ['ham.py', 42]
def test_go_to_test_definition_with_lineno_none(view_and_model, qtbot):
view, model = view_and_model
res = model.testresults
res[1].lineno = None
model.testresults = res
with qtbot.waitSignal(view.sig_edit_goto) as blocker:
view.go_to_test_definition(model.index(1, 0))
assert blocker.args == ['ham.py', 0]
def test_make_index_canonical_with_index_in_column2(view_and_model):
view, model = view_and_model
index = model.index(1, 2)
res = view.make_index_canonical(index)
assert res == model.index(1, 0)
def test_make_index_canonical_with_level2_index(view_and_model):
view, model = view_and_model
index = model.index(1, 0, model.index(1, 0))
res = view.make_index_canonical(index)
assert res == model.index(1, 0)
def test_make_index_canonical_with_invalid_index(view_and_model):
view, model = view_and_model
index = QModelIndex()
res = view.make_index_canonical(index)
assert res is None
def test_build_context_menu(view_and_model):
view, model = view_and_model
menu = view.build_context_menu(model.index(0, 0))
assert menu.actions()[0].text() == 'Expand'
assert menu.actions()[1].text() == 'Go to definition'
def test_build_context_menu_with_disabled_entries(view_and_model):
view, model = view_and_model
menu = view.build_context_menu(model.index(0, 0))
assert menu.actions()[0].isEnabled() == False
assert menu.actions()[1].isEnabled() == False
def test_build_context_menu_with_enabled_entries(view_and_model):
view, model = view_and_model
menu = view.build_context_menu(model.index(1, 0))
assert menu.actions()[0].isEnabled() == True
assert menu.actions()[1].isEnabled() == True
def test_build_context_menu_with_expanded_entry(view_and_model):
view, model = view_and_model
view.expand(model.index(1, 0))
menu = view.build_context_menu(model.index(1, 0))
assert menu.actions()[0].text() == 'Collapse'
assert menu.actions()[0].isEnabled() == True
def test_testdatamodel_using_qtmodeltester(qtmodeltester):
model = TestDataModel()
res = [TestResult(Category.OK, 'status', 'foo.bar'),
TestResult(Category.FAIL, 'error', 'foo.bar', 'kadoom', 0,
'crash!\nboom!')]
model.testresults = res
qtmodeltester.check(model)
def test_testdatamodel_shows_abbreviated_name_in_table(qtbot):
model = TestDataModel()
res = TestResult(Category.OK, 'status', 'foo.bar', '', 0, '')
model.testresults = [res]
index = model.index(0, 1)
assert model.data(index, Qt.DisplayRole) == 'f.bar'
def test_testdatamodel_shows_full_name_in_tooltip(qtbot):
model = TestDataModel()
res = TestResult(Category.OK, 'status', 'foo.bar', '', 0, '')
model.testresults = [res]
index = model.index(0, 1)
assert model.data(index, Qt.ToolTipRole) == 'foo.bar'
def test_testdatamodel_shows_time(qtmodeltester):
model = TestDataModel()
res = TestResult(Category.OK, 'status', 'foo.bar', time=0.0012345)
model.testresults = [res]
index = model.index(0, 3)
assert model.data(index, Qt.DisplayRole) == '1.23'
assert model.data(index, Qt.TextAlignmentRole) == Qt.AlignRight
def test_testdatamodel_shows_time_when_zero(qtmodeltester):
model = TestDataModel()
res = TestResult(Category.OK, 'status', 'foo.bar', time=0)
model.testresults = [res]
assert model.data(model.index(0, 3), Qt.DisplayRole) == '0.00'
def test_testdatamodel_shows_time_when_blank(qtmodeltester):
model = TestDataModel()
res = TestResult(Category.OK, 'status', 'foo.bar')
model.testresults = [res]
assert model.data(model.index(0, 3), Qt.DisplayRole) == ''
@pytest.mark.parametrize('dark', [False, True])
def test_testdatamodel_data_background(dark):
model = TestDataModel()
if dark:
model.is_dark_interface = True
res = [TestResult(Category.OK, 'status', 'foo.bar'),
TestResult(Category.FAIL, 'error', 'foo.bar', 'kadoom')]
model.testresults = res
index = model.index(0, 0)
colors = COLORS_DARK if dark else COLORS
assert model.data(index, Qt.BackgroundRole) == colors[Category.OK]
index = model.index(1, 2)
assert model.data(index, Qt.BackgroundRole) == colors[Category.FAIL]
def test_testdatamodel_data_userrole():
model = TestDataModel()
res = [TestResult(Category.OK, 'status', 'foo.bar', filename='somefile',
lineno=42)]
model.testresults = res
index = model.index(0, 0)
assert model.data(index, Qt.UserRole) == ('somefile', 42)
def test_testdatamodel_add_tests(qtbot):
def check_args1(parent, begin, end):
return not parent.isValid() and begin == 0 and end == 0
def check_args2(parent, begin, end):
return not parent.isValid() and begin == 1 and end == 1
model = TestDataModel()
assert model.testresults == []
result1 = TestResult(Category.OK, 'status', 'foo.bar')
with qtbot.waitSignals([model.rowsInserted, model.sig_summary],
check_params_cbs=[check_args1, None],
raising=True):
model.add_testresults([result1])
assert model.testresults == [result1]
result2 = TestResult(Category.FAIL, 'error', 'foo.bar', 'kadoom')
with qtbot.waitSignals([model.rowsInserted, model.sig_summary],
check_params_cbs=[check_args2, None],
raising=True):
model.add_testresults([result2])
assert model.testresults == [result1, result2]
def test_testdatamodel_replace_tests(qtbot):
def check_args(topLeft, bottomRight, *args):
return (topLeft.row() == 0
and topLeft.column() == 0
and not topLeft.parent().isValid()
and bottomRight.row() == 0
and bottomRight.column() == 3
and not bottomRight.parent().isValid())
model = TestDataModel()
result1 = TestResult(Category.OK, 'status', 'foo.bar')
model.testresults = [result1]
result2 = TestResult(Category.FAIL, 'error', 'foo.bar', 'kadoom')
with qtbot.waitSignals([model.dataChanged, model.sig_summary],
check_params_cbs=[check_args, None],
raising=True):
model.update_testresults([result2])
assert model.testresults == [result2]
STANDARD_TESTRESULTS = [
TestResult(Category.OK, 'status', 'foo.bar', time=2),
TestResult(Category.FAIL, 'failure', 'fu.baz', 'kaboom',time=1),
TestResult(Category.FAIL, 'error', 'fu.bar', 'boom')]
def test_testdatamodel_sort_by_status_ascending(qtbot):
model = TestDataModel()
model.testresults = STANDARD_TESTRESULTS[:]
with qtbot.waitSignal(model.modelReset):
model.sort(0, Qt.AscendingOrder)
expected = [STANDARD_TESTRESULTS[k] for k in [2, 1, 0]]
assert model.testresults == expected
def test_testdatamodel_sort_by_status_descending():
model = TestDataModel()
model.testresults = STANDARD_TESTRESULTS[:]
model.sort(0, Qt.DescendingOrder)
expected = [STANDARD_TESTRESULTS[k] for k in [0, 1, 2]]
assert model.testresults == expected
def test_testdatamodel_sort_by_name():
model = TestDataModel()
model.testresults = STANDARD_TESTRESULTS[:]
model.sort(1, Qt.AscendingOrder)
expected = [STANDARD_TESTRESULTS[k] for k in [0, 2, 1]]
assert model.testresults == expected
def test_testdatamodel_sort_by_message():
model = TestDataModel()
model.testresults = STANDARD_TESTRESULTS[:]
model.sort(2, Qt.AscendingOrder)
expected = [STANDARD_TESTRESULTS[k] for k in [0, 2, 1]]
assert model.testresults == expected
def test_testdatamodel_sort_by_time():
model = TestDataModel()
model.testresults = STANDARD_TESTRESULTS[:]
model.sort(3, Qt.AscendingOrder)
expected = [STANDARD_TESTRESULTS[k] for k in [2, 1, 0]]
assert model.testresults == expected
spyder_unittest-0.4.1/spyder_unittest/widgets/tests/test_unittestgui.py 0000644 0001750 0001750 00000030503 13657202727 027325 0 ustar jitse jitse 0000000 0000000 # -*- coding: utf-8 -*-
#
# Copyright © 2013 Spyder Project Contributors
# Licensed under the terms of the MIT License
# (see LICENSE.txt for details)
"""Tests for unittestgui.py."""
# Standard library imports
import os
# Third party imports
from qtpy.QtCore import Qt, QProcess
import pytest
# Local imports
from spyder_unittest.backend.runnerbase import Category, TestResult
from spyder_unittest.widgets.configdialog import Config
from spyder_unittest.widgets.unittestgui import UnitTestWidget
try:
from unittest.mock import Mock
except ImportError:
from mock import Mock # Python 2
def test_unittestwidget_forwards_sig_edit_goto(qtbot):
widget = UnitTestWidget(None)
qtbot.addWidget(widget)
with qtbot.waitSignal(widget.sig_edit_goto) as blocker:
widget.testdataview.sig_edit_goto.emit('ham', 42)
assert blocker.args == ['ham', 42]
def test_unittestwidget_set_config_emits_newconfig(qtbot):
widget = UnitTestWidget(None)
qtbot.addWidget(widget)
config = Config(wdir=os.getcwd(), framework='unittest')
with qtbot.waitSignal(widget.sig_newconfig) as blocker:
widget.config = config
assert blocker.args == [config]
assert widget.config == config
def test_unittestwidget_set_config_does_not_emit_when_invalid(qtbot):
widget = UnitTestWidget(None)
qtbot.addWidget(widget)
config = Config(wdir=os.getcwd(), framework=None)
with qtbot.assertNotEmitted(widget.sig_newconfig):
widget.config = config
assert widget.config == config
def test_unittestwidget_config_with_unknown_framework_invalid(qtbot):
"""Check that if the framework in the config is not known,
config_is_valid() returns False"""
widget = UnitTestWidget(None)
qtbot.addWidget(widget)
config = Config(wdir=os.getcwd(), framework='unknown framework')
assert widget.config_is_valid(config) == False
def test_unittestwidget_process_finished_updates_results(qtbot):
widget = UnitTestWidget(None)
widget.testdatamodel = Mock()
widget.testdatamodel.summary = lambda: 'message'
widget.testdatamodel.testresults = []
results = [TestResult(Category.OK, 'ok', 'hammodule.spam')]
widget.process_finished(results, 'output')
assert widget.testdatamodel.testresults == results
def test_unittestwidget_process_finished_with_results_none(qtbot):
widget = UnitTestWidget(None)
widget.testdatamodel = Mock()
widget.testdatamodel.summary = lambda: 'message'
results = [TestResult(Category.OK, 'ok', 'hammodule.spam')]
widget.testdatamodel.testresults = results
widget.process_finished(None, 'output')
assert widget.testdatamodel.testresults == results
def test_unittestwidget_replace_pending_with_not_run(qtbot):
widget = UnitTestWidget(None)
widget.testdatamodel = Mock()
results = [TestResult(Category.PENDING, 'pending', 'hammodule.eggs'),
TestResult(Category.OK, 'ok', 'hammodule.spam')]
widget.testdatamodel.testresults = results
widget.replace_pending_with_not_run()
expected = [TestResult(Category.SKIP, 'not run', 'hammodule.eggs')]
widget.testdatamodel.update_testresults.assert_called_once_with(expected)
def test_unittestwidget_tests_collected(qtbot):
widget = UnitTestWidget(None)
widget.testdatamodel = Mock()
details = ['hammodule.spam', 'hammodule.eggs']
widget.tests_collected(details)
results = [TestResult(Category.PENDING, 'pending', 'hammodule.spam'),
TestResult(Category.PENDING, 'pending', 'hammodule.eggs')]
widget.testdatamodel.add_testresults.assert_called_once_with(results)
def test_unittestwidget_tests_started(qtbot):
widget = UnitTestWidget(None)
widget.testdatamodel = Mock()
details = ['hammodule.spam']
results = [TestResult(Category.PENDING, 'pending', 'hammodule.spam', 'running')]
widget.tests_started(details)
widget.testdatamodel.update_testresults.assert_called_once_with(results)
def test_unittestwidget_tests_collect_error(qtbot):
widget = UnitTestWidget(None)
widget.testdatamodel = Mock()
names_plus_msg = [('hammodule.spam', 'msg')]
results = [TestResult(Category.FAIL, 'failure', 'hammodule.spam',
'collection error', extra_text='msg')]
widget.tests_collect_error(names_plus_msg)
widget.testdatamodel.add_testresults.assert_called_once_with(results)
def test_unittestwidget_tests_yield_results(qtbot):
widget = UnitTestWidget(None)
widget.testdatamodel = Mock()
results = [TestResult(Category.OK, 'ok', 'hammodule.spam')]
widget.tests_yield_result(results)
widget.testdatamodel.update_testresults.assert_called_once_with(results)
def test_unittestwidget_set_message(qtbot):
widget = UnitTestWidget(None)
widget.status_label = Mock()
widget.set_status_label('xxx')
widget.status_label.setText.assert_called_once_with('xxx')
def test_run_tests_starts_testrunner(qtbot):
widget = UnitTestWidget(None)
mockRunner = Mock()
widget.framework_registry.create_runner = Mock(return_value=mockRunner)
config = Config(wdir=None, framework='ham')
widget.run_tests(config)
assert widget.framework_registry.create_runner.call_count == 1
assert widget.framework_registry.create_runner.call_args[0][0] == 'ham'
assert mockRunner.start.call_count == 1
def test_run_tests_with_pre_test_hook_returning_true(qtbot):
widget = UnitTestWidget(None)
mockRunner = Mock()
widget.framework_registry.create_runner = Mock(return_value=mockRunner)
widget.pre_test_hook = Mock(return_value=True)
widget.run_tests(Config())
assert widget.pre_test_hook.call_count == 1
assert mockRunner.start.call_count == 1
def test_run_tests_with_pre_test_hook_returning_false(qtbot):
widget = UnitTestWidget(None)
mockRunner = Mock()
widget.framework_registry.create_runner = Mock(return_value=mockRunner)
widget.pre_test_hook = Mock(return_value=False)
widget.run_tests(Config())
assert widget.pre_test_hook.call_count == 1
assert mockRunner.start.call_count == 0
@pytest.mark.parametrize('results,label',
[([TestResult(Category.OK, 'ok', '')], '0 tests failed, 1 passed'),
([], 'No results to show.')])
def test_unittestwidget_process_finished_updates_status_label(qtbot, results, label):
widget = UnitTestWidget(None)
widget.process_finished(results, 'output')
assert widget.status_label.text() == '{}'.format(label)
@pytest.mark.parametrize('framework', ['pytest', 'nose'])
def test_run_tests_and_display_results(qtbot, tmpdir, monkeypatch, framework):
"""Basic integration test."""
os.chdir(tmpdir.strpath)
testfilename = tmpdir.join('test_foo.py').strpath
with open(testfilename, 'w') as f:
f.write("def test_ok(): assert 1+1 == 2\n"
"def test_fail(): assert 1+1 == 3\n")
MockQMessageBox = Mock()
monkeypatch.setattr('spyder_unittest.widgets.unittestgui.QMessageBox',
MockQMessageBox)
widget = UnitTestWidget(None)
qtbot.addWidget(widget)
config = Config(wdir=tmpdir.strpath, framework=framework)
with qtbot.waitSignal(widget.sig_finished, timeout=10000, raising=True):
widget.run_tests(config)
MockQMessageBox.assert_not_called()
model = widget.testdatamodel
assert model.rowCount() == 2
assert model.index(0, 0).data(
Qt.DisplayRole) == 'ok' if framework == 'nose' else 'passed'
assert model.index(0, 1).data(Qt.DisplayRole) == 't.test_ok'
assert model.index(0, 1).data(Qt.ToolTipRole) == 'test_foo.test_ok'
assert model.index(0, 2).data(Qt.DisplayRole) == ''
assert model.index(1, 0).data(
Qt.DisplayRole) == 'failure' if framework == 'nose' else 'failed'
assert model.index(1, 1).data(Qt.DisplayRole) == 't.test_fail'
assert model.index(1, 1).data(Qt.ToolTipRole) == 'test_foo.test_fail'
def test_run_tests_using_unittest_and_display_results(qtbot, tmpdir,
monkeypatch):
"""Basic check."""
os.chdir(tmpdir.strpath)
testfilename = tmpdir.join('test_foo.py').strpath
with open(testfilename, 'w') as f:
f.write("import unittest\n"
"class MyTest(unittest.TestCase):\n"
" def test_ok(self): self.assertEqual(1+1, 2)\n"
" def test_fail(self): self.assertEqual(1+1, 3)\n")
MockQMessageBox = Mock()
monkeypatch.setattr('spyder_unittest.widgets.unittestgui.QMessageBox',
MockQMessageBox)
widget = UnitTestWidget(None)
qtbot.addWidget(widget)
config = Config(wdir=tmpdir.strpath, framework='unittest')
with qtbot.waitSignal(widget.sig_finished, timeout=10000, raising=True):
widget.run_tests(config)
MockQMessageBox.assert_not_called()
model = widget.testdatamodel
assert model.rowCount() == 2
assert model.index(0, 0).data(Qt.DisplayRole) == 'FAIL'
assert model.index(0, 1).data(Qt.DisplayRole) == 't.M.test_fail'
assert model.index(0, 1).data(Qt.ToolTipRole) == 'test_foo.MyTest.test_fail'
assert model.index(0, 2).data(Qt.DisplayRole) == ''
assert model.index(1, 0).data(Qt.DisplayRole) == 'ok'
assert model.index(1, 1).data(Qt.DisplayRole) == 't.M.test_ok'
assert model.index(1, 1).data(Qt.ToolTipRole) == 'test_foo.MyTest.test_ok'
assert model.index(1, 2).data(Qt.DisplayRole) == ''
@pytest.mark.parametrize('framework', ['unittest', 'pytest', 'nose'])
def test_run_with_no_tests_discovered_and_display_results(qtbot, tmpdir,
monkeypatch, framework):
"""Basic integration test."""
os.chdir(tmpdir.strpath)
MockQMessageBox = Mock()
monkeypatch.setattr('spyder_unittest.widgets.unittestgui.QMessageBox',
MockQMessageBox)
widget = UnitTestWidget(None)
qtbot.addWidget(widget)
config = Config(wdir=tmpdir.strpath, framework=framework)
with qtbot.waitSignal(widget.sig_finished, timeout=10000, raising=True):
widget.run_tests(config)
MockQMessageBox.assert_not_called()
model = widget.testdatamodel
assert model.rowCount() == 0
assert widget.status_label.text() == 'No results to show.'
def test_stop_running_tests_before_testresult_is_received(qtbot, tmpdir):
os.chdir(tmpdir.strpath)
testfilename = tmpdir.join('test_foo.py').strpath
with open(testfilename, 'w') as f:
f.write("import unittest\n"
"import time\n"
"class MyTest(unittest.TestCase):\n"
" def test_ok(self): \n"
" time.sleep(3)\n"
" self.assertTrue(True)\n")
widget = UnitTestWidget(None)
qtbot.addWidget(widget)
config = Config(wdir=tmpdir.strpath, framework='unittest')
widget.run_tests(config)
qtbot.waitUntil(lambda: widget.testrunner.process.state() == QProcess.Running)
widget.testrunner.stop_if_running()
assert widget.testdatamodel.rowCount() == 0
assert widget.status_label.text() == ''
def test_show_versions(monkeypatch):
mockQMessageBox = Mock()
monkeypatch.setattr('spyder_unittest.widgets.unittestgui.QMessageBox',
mockQMessageBox)
widget = UnitTestWidget(None)
monkeypatch.setattr(widget.framework_registry.frameworks['nose'],
'is_installed', lambda: False)
monkeypatch.setattr(widget.framework_registry.frameworks['pytest'],
'is_installed', lambda: True)
monkeypatch.setattr(widget.framework_registry.frameworks['unittest'],
'is_installed', lambda: True)
monkeypatch.setattr(widget.framework_registry.frameworks['nose'],
'get_versions', lambda _: [])
monkeypatch.setattr(widget.framework_registry.frameworks['pytest'],
'get_versions',
lambda _: ['pytest 1.2.3', ' plugin1 4.5.6',
' plugin2 7.8.9'])
monkeypatch.setattr(widget.framework_registry.frameworks['unittest'],
'get_versions', lambda _: ['unittest 1.2.3'])
widget.show_versions()
expected = ('Versions of frameworks and their installed plugins:\n\n'
'nose: not available\n\npytest 1.2.3\n plugin1 4.5.6\n '
'plugin2 7.8.9\n\nunittest 1.2.3')
mockQMessageBox.information.assert_called_with(widget, 'Dependencies',
expected)
spyder_unittest-0.4.1/spyder_unittest/widgets/unittestgui.py 0000644 0001750 0001750 00000035443 13662040473 025126 0 ustar jitse jitse 0000000 0000000 # -*- coding: utf-8 -*-
#
# Copyright © 2013 Spyder Project Contributors
# Licensed under the terms of the MIT License
# (see LICENSE.txt for details)
"""Unit Testing widget."""
from __future__ import with_statement
# Standard library imports
import copy
import os.path as osp
import sys
# Third party imports
from qtpy.QtCore import Signal
from qtpy.QtWidgets import (QHBoxLayout, QLabel, QMenu, QMessageBox,
QToolButton, QVBoxLayout, QWidget)
from spyder.config.base import get_conf_path, get_translation
from spyder.utils import icon_manager as ima
from spyder.utils.qthelpers import create_action, create_toolbutton
from spyder.plugins.variableexplorer.widgets.texteditor import TextEditor
from spyder.py3compat import PY3
# Local imports
from spyder_unittest.backend.frameworkregistry import FrameworkRegistry
from spyder_unittest.backend.noserunner import NoseRunner
from spyder_unittest.backend.runnerbase import Category, TestResult
from spyder_unittest.backend.unittestrunner import UnittestRunner
from spyder_unittest.widgets.configdialog import Config, ask_for_config
from spyder_unittest.widgets.datatree import TestDataModel, TestDataView
# This import uses Python 3 syntax, so importing it under Python 2 throws
# a SyntaxError which means that the plugin's check_compatibility method
# will never run.
if PY3:
from spyder_unittest.backend.pytestrunner import PyTestRunner
# This is needed for testing this module as a stand alone script
try:
_ = get_translation("unittest", dirname="spyder_unittest")
except KeyError:
import gettext
_ = gettext.gettext
# Supported testing framework
if PY3:
FRAMEWORKS = {NoseRunner, PyTestRunner, UnittestRunner}
else:
FRAMEWORKS = {NoseRunner, UnittestRunner}
class UnitTestWidget(QWidget):
"""
Unit testing widget.
Attributes
----------
config : Config or None
Configuration for running tests, or `None` if not set.
default_wdir : str
Default choice of working directory.
framework_registry : FrameworkRegistry
Registry of supported testing frameworks.
pre_test_hook : function returning bool or None
If set, contains function to run before running tests; abort the test
run if hook returns False.
pythonpath : list of str
Directories to be added to the Python path when running tests.
testrunner : TestRunner or None
Object associated with the current test process, or `None` if no test
process is running at the moment.
Signals
-------
sig_finished: Emitted when plugin finishes processing tests.
sig_newconfig(Config): Emitted when test config is changed.
Argument is new config, which is always valid.
sig_edit_goto(str, int): Emitted if editor should go to some position.
Arguments are file name and line number (zero-based).
"""
VERSION = '0.0.1'
sig_finished = Signal()
sig_newconfig = Signal(Config)
sig_edit_goto = Signal(str, int)
def __init__(self, parent, options_button=None, options_menu=None):
"""Unit testing widget."""
QWidget.__init__(self, parent)
self.setWindowTitle("Unit testing")
self.config = None
self.pythonpath = None
self.default_wdir = None
self.pre_test_hook = None
self.testrunner = None
self.output = None
self.testdataview = TestDataView(self)
self.testdatamodel = TestDataModel(self)
self.testdataview.setModel(self.testdatamodel)
self.testdataview.sig_edit_goto.connect(self.sig_edit_goto)
self.testdatamodel.sig_summary.connect(self.set_status_label)
self.framework_registry = FrameworkRegistry()
for runner in FRAMEWORKS:
self.framework_registry.register(runner)
self.start_button = create_toolbutton(self, text_beside_icon=True)
self.set_running_state(False)
self.status_label = QLabel('', self)
self.create_actions()
self.options_menu = options_menu or QMenu()
self.options_menu.addAction(self.config_action)
self.options_menu.addAction(self.log_action)
self.options_menu.addAction(self.collapse_action)
self.options_menu.addAction(self.expand_action)
self.options_menu.addAction(self.versions_action)
self.options_button = options_button or QToolButton(self)
self.options_button.setIcon(ima.icon('tooloptions'))
self.options_button.setPopupMode(QToolButton.InstantPopup)
self.options_button.setMenu(self.options_menu)
self.options_button.setAutoRaise(True)
hlayout = QHBoxLayout()
hlayout.addWidget(self.start_button)
hlayout.addStretch()
hlayout.addWidget(self.status_label)
hlayout.addStretch()
hlayout.addWidget(self.options_button)
layout = QVBoxLayout()
layout.addLayout(hlayout)
layout.addWidget(self.testdataview)
self.setLayout(layout)
@property
def config(self):
"""Return current test configuration."""
return self._config
@config.setter
def config(self, new_config):
"""Set test configuration and emit sig_newconfig if valid."""
self._config = new_config
if self.config_is_valid():
self.sig_newconfig.emit(new_config)
def set_config_without_emit(self, new_config):
"""Set test configuration but do not emit any signal."""
self._config = new_config
def use_dark_interface(self, flag):
"""Set whether widget should use colours appropriate for dark UI."""
self.testdatamodel.is_dark_interface = flag
def create_actions(self):
"""Create the actions for the unittest widget."""
self.config_action = create_action(
self,
text=_("Configure ..."),
icon=ima.icon('configure'),
triggered=self.configure)
self.log_action = create_action(
self,
text=_('Show output'),
icon=ima.icon('log'),
triggered=self.show_log)
self.collapse_action = create_action(
self,
text=_('Collapse all'),
icon=ima.icon('collapse'),
triggered=self.testdataview.collapseAll)
self.expand_action = create_action(
self,
text=_('Expand all'),
icon=ima.icon('expand'),
triggered=self.testdataview.expandAll)
self.versions_action = create_action(
self,
text=_('Dependencies'),
icon=ima.icon('advanced'),
triggered=self.show_versions)
return [
self.config_action, self.log_action, self.collapse_action,
self.expand_action, self.versions_action
]
def show_log(self):
"""Show output of testing process."""
if self.output:
te = TextEditor(
self.output,
title=_("Unit testing output"),
readonly=True)
te.show()
te.exec_()
def show_versions(self):
"""Show versions of frameworks and their plugins"""
versions = [_('Versions of frameworks and their installed plugins:')]
for name, runner in sorted(self.framework_registry.frameworks.items()):
version = (runner.get_versions(self) if runner.is_installed()
else None)
versions.append('\n'.join(version) if version else
'{}: {}'.format(name, _('not available')))
QMessageBox.information(self, _('Dependencies'),
_('\n\n'.join(versions)))
def configure(self):
"""Configure tests."""
if self.config:
oldconfig = self.config
else:
oldconfig = Config(wdir=self.default_wdir)
frameworks = self.framework_registry.frameworks
config = ask_for_config(frameworks, oldconfig)
if config:
self.config = config
def config_is_valid(self, config=None):
"""
Return whether configuration for running tests is valid.
Parameters
----------
config : Config or None
configuration for unit tests. If None, use `self.config`.
"""
if config is None:
config = self.config
return (config and config.framework
and config.framework in self.framework_registry.frameworks
and osp.isdir(config.wdir))
def maybe_configure_and_start(self):
"""
Ask for configuration if necessary and then run tests.
If the current test configuration is not valid (or not set(,
then ask the user to configure. Then run the tests.
"""
if not self.config_is_valid():
self.configure()
if self.config_is_valid():
self.run_tests()
def run_tests(self, config=None):
"""
Run unit tests.
First, run `self.pre_test_hook` if it is set, and abort if its return
value is `False`.
Then, run the unit tests.
The process's output is consumed by `read_output()`.
When the process finishes, the `finish` signal is emitted.
Parameters
----------
config : Config or None
configuration for unit tests. If None, use `self.config`.
In either case, configuration should be valid.
"""
if self.pre_test_hook:
if self.pre_test_hook() is False:
return
if config is None:
config = self.config
pythonpath = self.pythonpath
self.testdatamodel.testresults = []
self.testdetails = []
tempfilename = get_conf_path('unittest.results')
self.testrunner = self.framework_registry.create_runner(
config.framework, self, tempfilename)
self.testrunner.sig_finished.connect(self.process_finished)
self.testrunner.sig_collected.connect(self.tests_collected)
self.testrunner.sig_collecterror.connect(self.tests_collect_error)
self.testrunner.sig_starttest.connect(self.tests_started)
self.testrunner.sig_testresult.connect(self.tests_yield_result)
self.testrunner.sig_stop.connect(self.tests_stopped)
try:
self.testrunner.start(config, pythonpath)
except RuntimeError:
QMessageBox.critical(self,
_("Error"), _("Process failed to start"))
else:
self.set_running_state(True)
self.set_status_label(_('Running tests ...'))
def set_running_state(self, state):
"""
Change start/stop button according to whether tests are running.
If tests are running, then display a stop button, otherwise display
a start button.
Parameters
----------
state : bool
Set to True if tests are running.
"""
button = self.start_button
try:
button.clicked.disconnect()
except TypeError: # raised if not connected to any handler
pass
if state:
button.setIcon(ima.icon('stop'))
button.setText(_('Stop'))
button.setToolTip(_('Stop current test process'))
if self.testrunner:
button.clicked.connect(self.testrunner.stop_if_running)
else:
button.setIcon(ima.icon('run'))
button.setText(_("Run tests"))
button.setToolTip(_('Run unit tests'))
button.clicked.connect(
lambda checked: self.maybe_configure_and_start())
def process_finished(self, testresults, output):
"""
Called when unit test process finished.
This function collects and shows the test results and output.
Parameters
----------
testresults : list of TestResult or None
`None` indicates all test results have already been transmitted.
output : str
"""
self.output = output
self.set_running_state(False)
self.testrunner = None
self.log_action.setEnabled(bool(output))
if testresults is not None:
self.testdatamodel.testresults = testresults
self.replace_pending_with_not_run()
self.sig_finished.emit()
def replace_pending_with_not_run(self):
"""Change status of pending tests to 'not run''."""
new_results = []
for res in self.testdatamodel.testresults:
if res.category == Category.PENDING:
new_res = copy.copy(res)
new_res.category = Category.SKIP
new_res.status = _('not run')
new_results.append(new_res)
if new_results:
self.testdatamodel.update_testresults(new_results)
def tests_collected(self, testnames):
"""Called when tests are collected."""
testresults = [TestResult(Category.PENDING, _('pending'), name)
for name in testnames]
self.testdatamodel.add_testresults(testresults)
def tests_started(self, testnames):
"""Called when tests are about to be run."""
testresults = [TestResult(Category.PENDING, _('pending'), name,
message=_('running'))
for name in testnames]
self.testdatamodel.update_testresults(testresults)
def tests_collect_error(self, testnames_plus_msg):
"""Called when errors are encountered during collection."""
testresults = [TestResult(Category.FAIL, _('failure'), name,
message=_('collection error'),
extra_text=msg)
for name, msg in testnames_plus_msg]
self.testdatamodel.add_testresults(testresults)
def tests_yield_result(self, testresults):
"""Called when test results are received."""
self.testdatamodel.update_testresults(testresults)
def tests_stopped(self):
"""Called when tests are stopped"""
self.status_label.setText('')
def set_status_label(self, msg):
"""
Set status label to the specified message.
Arguments
---------
msg: str
"""
self.status_label.setText('{}'.format(msg))
def test():
"""
Run widget test.
Show the unittest widgets, configured so that our own tests are run when
the user clicks "Run tests".
"""
from spyder.utils.qthelpers import qapplication
app = qapplication()
widget = UnitTestWidget(None)
# set wdir to .../spyder_unittest
wdir = osp.abspath(osp.join(osp.dirname(__file__), osp.pardir))
widget.config = Config('pytest', wdir)
# add wdir's parent to python path, so that `import spyder_unittest` works
rootdir = osp.abspath(osp.join(wdir, osp.pardir))
widget.pythonpath = [rootdir]
widget.resize(800, 600)
widget.show()
sys.exit(app.exec_())
if __name__ == '__main__':
test()
spyder_unittest-0.4.1/spyder_unittest.egg-info/ 0000755 0001750 0001750 00000000000 13662215740 022204 5 ustar jitse jitse 0000000 0000000 spyder_unittest-0.4.1/spyder_unittest.egg-info/PKG-INFO 0000644 0001750 0001750 00000002102 13662215740 023274 0 ustar jitse jitse 0000000 0000000 Metadata-Version: 1.2
Name: spyder-unittest
Version: 0.4.1
Summary: Plugin to run tests from within the Spyder IDE
Home-page: https://github.com/spyder-ide/spyder-unittest
Author: Spyder Project Contributors
License: MIT
Description:
This is a plugin for the Spyder IDE that integrates popular unit test
frameworks. It allows you to run tests and view the results.
The plugin supports the `unittest` framework in the Python
standard library and the `pytest` and `nose` testing frameworks.
Keywords: Qt PyQt4 PyQt5 spyder plugins testing
Platform: UNKNOWN
Classifier: Development Status :: 4 - Beta
Classifier: Environment :: X11 Applications :: Qt
Classifier: Environment :: Win32 (MS Windows)
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Topic :: Software Development :: Testing
Classifier: Topic :: Text Editors :: Integrated Development Environments (IDE)
Requires-Python: >=3.5
spyder_unittest-0.4.1/spyder_unittest.egg-info/SOURCES.txt 0000644 0001750 0001750 00000002747 13662215740 024102 0 ustar jitse jitse 0000000 0000000 CHANGELOG.md
LICENSE.txt
MANIFEST.in
README.md
setup.cfg
setup.py
spyder_unittest/__init__.py
spyder_unittest/unittestplugin.py
spyder_unittest.egg-info/PKG-INFO
spyder_unittest.egg-info/SOURCES.txt
spyder_unittest.egg-info/dependency_links.txt
spyder_unittest.egg-info/requires.txt
spyder_unittest.egg-info/top_level.txt
spyder_unittest/backend/__init__.py
spyder_unittest/backend/abbreviator.py
spyder_unittest/backend/frameworkregistry.py
spyder_unittest/backend/noserunner.py
spyder_unittest/backend/pytestrunner.py
spyder_unittest/backend/pytestworker.py
spyder_unittest/backend/runnerbase.py
spyder_unittest/backend/unittestrunner.py
spyder_unittest/backend/zmqstream.py
spyder_unittest/backend/tests/__init__.py
spyder_unittest/backend/tests/test_abbreviator.py
spyder_unittest/backend/tests/test_frameworkregistry.py
spyder_unittest/backend/tests/test_noserunner.py
spyder_unittest/backend/tests/test_pytestrunner.py
spyder_unittest/backend/tests/test_pytestworker.py
spyder_unittest/backend/tests/test_runnerbase.py
spyder_unittest/backend/tests/test_unittestrunner.py
spyder_unittest/backend/tests/test_zmqstream.py
spyder_unittest/tests/test_unittestplugin.py
spyder_unittest/widgets/__init__.py
spyder_unittest/widgets/configdialog.py
spyder_unittest/widgets/datatree.py
spyder_unittest/widgets/unittestgui.py
spyder_unittest/widgets/tests/__init__.py
spyder_unittest/widgets/tests/test_configdialog.py
spyder_unittest/widgets/tests/test_datatree.py
spyder_unittest/widgets/tests/test_unittestgui.py spyder_unittest-0.4.1/spyder_unittest.egg-info/dependency_links.txt 0000644 0001750 0001750 00000000001 13662215740 026252 0 ustar jitse jitse 0000000 0000000
spyder_unittest-0.4.1/spyder_unittest.egg-info/requires.txt 0000644 0001750 0001750 00000000025 13662215740 024601 0 ustar jitse jitse 0000000 0000000 lxml
spyder>=3
pyzmq
spyder_unittest-0.4.1/spyder_unittest.egg-info/top_level.txt 0000644 0001750 0001750 00000000020 13662215740 024726 0 ustar jitse jitse 0000000 0000000 spyder_unittest