spyder_unittest-0.4.1/0000755000175000017500000000000013662215740015245 5ustar jitsejitse00000000000000spyder_unittest-0.4.1/CHANGELOG.md0000644000175000017500000003672113662215653017072 0ustar jitsejitse00000000000000# 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.txt0000644000175000017500000000210513047645510017065 0ustar jitsejitse00000000000000The 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.in0000644000175000017500000000014213047650733017002 0ustar jitsejitse00000000000000include CHANGELOG.md include LICENSE.txt include README.md recursive-include spyder_unittest *.py spyder_unittest-0.4.1/PKG-INFO0000644000175000017500000000210213662215740016335 0ustar jitsejitse00000000000000Metadata-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.md0000644000175000017500000001320213644304544016523 0ustar jitsejitse00000000000000# Spyder-Unittest ## Project information [![license](https://img.shields.io/pypi/l/spyder-unittest.svg)](./LICENSE) [![conda version](https://img.shields.io/conda/v/spyder-ide/spyder-unittest.svg)](https://www.anaconda.com/download/) [![download count](https://img.shields.io/conda/d/spyder-ide/spyder-unittest.svg)](https://www.anaconda.com/download/) [![pypi version](https://img.shields.io/pypi/v/spyder-unittest.svg)](https://pypi.org/project/spyder-unittest/) [![Join the chat at https://gitter.im/spyder-ide/public](https://badges.gitter.im/spyder-ide/spyder.svg)](https://gitter.im/spyder-ide/public) [![OpenCollective Backers](https://opencollective.com/spyder/backers/badge.svg?color=blue)](#backers) [![OpenCollective Sponsors](https://opencollective.com/spyder/sponsors/badge.svg?color=blue)](#sponsors) ## Build status [![Windows status](https://github.com/spyder-ide/spyder-unittest/workflows/Windows%20tests/badge.svg)](https://github.com/spyder-ide/spyder-notebook/actions?query=workflow%3A%22Windows+tests%22) [![Linux status](https://github.com/spyder-ide/spyder-unittest/workflows/Linux%20tests/badge.svg)](https://github.com/spyder-ide/spyder-notebook/actions?query=workflow%3A%22Linux+tests%22) [![MacOS status](https://github.com/spyder-ide/spyder-unittest/workflows/Macos%20tests/badge.svg)](https://github.com/spyder-ide/spyder-notebook/actions?query=workflow%3A%22Macos+tests%22) [![codecov](https://codecov.io/gh/spyder-ide/spyder-unittest/branch/master/graph/badge.svg)](https://codecov.io/gh/spyder-ide/spyder-notebook/branch/master) [![Crowdin](https://badges.crowdin.net/spyder-unittest/localized.svg)](https://crowdin.com/project/spyder-unittest) *Copyright © 2014 Spyder Project Contributors* ![Screenshot of spyder-unittest plugin showing test results](./screenshot.png) ## 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.cfg0000644000175000017500000000010713662215740017064 0ustar jitsejitse00000000000000[tool:pytest] python_classes = [egg_info] tag_build = tag_date = 0 spyder_unittest-0.4.1/setup.py0000644000175000017500000000477413662040473016772 0ustar jitsejitse00000000000000# -*- 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/0000755000175000017500000000000013662215740020512 5ustar jitsejitse00000000000000spyder_unittest-0.4.1/spyder_unittest/__init__.py0000644000175000017500000000044513662215653022631 0ustar jitsejitse00000000000000# -*- 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/0000755000175000017500000000000013662215740022101 5ustar jitsejitse00000000000000spyder_unittest-0.4.1/spyder_unittest/backend/__init__.py0000644000175000017500000000033413047645510024211 0ustar jitsejitse00000000000000# -*- 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.py0000644000175000017500000000566513646270615024773 0ustar jitsejitse00000000000000# -*- 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.py0000644000175000017500000000402513224452267026243 0ustar jitsejitse00000000000000# -*- 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.py0000644000175000017500000000726113646270600024655 0ustar jitsejitse00000000000000# -*- 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.py0000644000175000017500000001263713657202727025253 0ustar jitsejitse00000000000000# -*- 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.py0000644000175000017500000001232713657202727025247 0ustar jitsejitse00000000000000# -*- 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.py0000644000175000017500000001723113657202727024630 0ustar jitsejitse00000000000000# -*- 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/0000755000175000017500000000000013662215740023243 5ustar jitsejitse00000000000000spyder_unittest-0.4.1/spyder_unittest/backend/tests/__init__.py0000644000175000017500000000030213074157120025341 0ustar jitsejitse00000000000000# -*- 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.py0000644000175000017500000000467713646270615027176 0ustar jitsejitse00000000000000# -*- 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.py0000644000175000017500000000147513224452267030452 0ustar jitsejitse00000000000000# -*- 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.py0000644000175000017500000001007213646270600027050 0ustar jitsejitse00000000000000# -*- 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.py0000644000175000017500000001776713657202727027465 0ustar jitsejitse00000000000000# -*- 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.py0000644000175000017500000002560013657202727027446 0ustar jitsejitse00000000000000# -*- 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.py0000644000175000017500000000607413657202727027034 0ustar jitsejitse00000000000000# -*- 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.py0000644000175000017500000001247513646270600027774 0ustar jitsejitse00000000000000# -*- 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.py0000644000175000017500000000103313247011367026672 0ustar jitsejitse00000000000000# -*- 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.py0000644000175000017500000001235513646270600025570 0ustar jitsejitse00000000000000# -*- 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.py0000644000175000017500000000657213247011367024506 0ustar jitsejitse00000000000000# -*- 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/0000755000175000017500000000000013662215740021654 5ustar jitsejitse00000000000000spyder_unittest-0.4.1/spyder_unittest/tests/test_unittestplugin.py0000644000175000017500000001143713603155222026362 0ustar jitsejitse00000000000000# -*- 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.py0000644000175000017500000002211413662040473024161 0ustar jitsejitse00000000000000# -*- 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/0000755000175000017500000000000013662215740022160 5ustar jitsejitse00000000000000spyder_unittest-0.4.1/spyder_unittest/widgets/__init__.py0000644000175000017500000000027313047645510024272 0ustar jitsejitse00000000000000# -*- 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.py0000644000175000017500000001267613311025216025157 0ustar jitsejitse00000000000000# -*- 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.py0000644000175000017500000003643413645540415024336 0ustar jitsejitse00000000000000# -*- 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/0000755000175000017500000000000013662215740023322 5ustar jitsejitse00000000000000spyder_unittest-0.4.1/spyder_unittest/widgets/tests/__init__.py0000644000175000017500000000030213074157120025420 0ustar jitsejitse00000000000000# -*- 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.py0000644000175000017500000000711213224452267027362 0ustar jitsejitse00000000000000# -*- 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.py0000644000175000017500000002377013643037630026534 0ustar jitsejitse00000000000000# -*- 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.py0000644000175000017500000003050313657202727027325 0ustar jitsejitse00000000000000# -*- 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.py0000644000175000017500000003544313662040473025126 0ustar jitsejitse00000000000000# -*- 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/0000755000175000017500000000000013662215740022204 5ustar jitsejitse00000000000000spyder_unittest-0.4.1/spyder_unittest.egg-info/PKG-INFO0000644000175000017500000000210213662215740023274 0ustar jitsejitse00000000000000Metadata-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.txt0000644000175000017500000000274713662215740024102 0ustar jitsejitse00000000000000CHANGELOG.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.pyspyder_unittest-0.4.1/spyder_unittest.egg-info/dependency_links.txt0000644000175000017500000000000113662215740026252 0ustar jitsejitse00000000000000 spyder_unittest-0.4.1/spyder_unittest.egg-info/requires.txt0000644000175000017500000000002513662215740024601 0ustar jitsejitse00000000000000lxml spyder>=3 pyzmq spyder_unittest-0.4.1/spyder_unittest.egg-info/top_level.txt0000644000175000017500000000002013662215740024726 0ustar jitsejitse00000000000000spyder_unittest