Update for v1.0beta4

This commit is contained in:
James Deng 2024-04-22 13:45:53 +08:00
commit 91a5524b15
71 changed files with 6451 additions and 0 deletions

1
.gitignore vendored Normal file
View file

@ -0,0 +1 @@
__pycache__/

36
README.md Normal file
View file

@ -0,0 +1,36 @@
工厂测试源码。
- 支持图形界面
## 依赖
- Python3
- unittest
- PyQt5
## 源码目录
```
├── cricket # Cricket is a graphical tool that helps you run your test suites.
├── gui-main
└── tests # factory test case
├── auto # auto test case
└── manual # manual test case
```
## 测试项
测试项有自动测试项和手动测试项两类。自动测试项无需人工干预,自动判定测试是否通过。手动测试需要人工参与,判断是否通过。
添加测试项规则:
- 自动测试项和手动测试项分别添加到`tests/auto``tests/manual`目录
- 每个模块一个文件,以`test_`开头,文件里定义一个类,继承`unittest.TestCase`
- 测试项为类的方法,方法名以`test_`开头
- 类里定义一个字典`LANGUAGES`,用于支持多国语音
## 多国语言
## TODO
- 支持命令行

18
cricket/AUTHORS Normal file
View file

@ -0,0 +1,18 @@
Cricket was originally created in January 2013.
The PRIMARY AUTHORS are (and/or have been):
Russell Keith-Magee
And here is an inevitably incomplete list of MUCH-APPRECIATED CONTRIBUTORS --
people who have submitted patches, reported bugs, added translations, helped
answer newbie questions, and generally made Cricket that much better:
Richard Jones <richard@mechanicalcat.net>
Ben Khoo (khoobks)
Tennessee Leeuwenburg <tleeuwenburg@gmail.com>
Curtin Maloney (funkybob)
Stephen McDonald <steve@jupo.org>
Simon Meers <simon@simonmeers.com>
Thomas Sutton <me@thomas-sutton.id.au>
Sebastian Vetter <sebastian@roadside-developer.com>
Markus Zapke-Gründemann (keimlink)

27
cricket/LICENSE Normal file
View file

@ -0,0 +1,27 @@
Copyright (c) 2013 Russell Keith-Magee.
All rights reserved.
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
3. Neither the name of Cricket nor the names of its contributors may
be used to endorse or promote products derived from this software without
specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

8
cricket/MANIFEST.in Normal file
View file

@ -0,0 +1,8 @@
include AUTHORS
include LICENSE
include requirements*.txt
recursive-include docs *.bat
recursive-include docs *.py
recursive-include docs *.rst
recursive-include docs Makefile
recursive-include tests *.py

135
cricket/PKG-INFO Normal file
View file

@ -0,0 +1,135 @@
Metadata-Version: 1.1
Name: cricket
Version: 0.2.5
Summary: A graphical tool to assist running test suites.
Home-page: http://pybee.org/cricket
Author: Russell Keith-Magee
Author-email: russell@keith-magee.com
License: New BSD
Description: Cricket
=======
Cricket is part of the `BeeWare suite`_. The project website is `http://pybee.org/cricket`_.
Cricket is a graphical tool that helps you run your test suites.
Normal unittest test runners dump all output to the console, and provide very
little detail while the suite is running. As a result:
* You can't start looking at failures until the test suite has completed running,
* It isn't a very accessible format for identifying patterns in test failures,
* It can be hard (or cumbersome) to re-run any tests that have failed.
Why the name ``cricket``? `Test Cricket`_ is the most prestigious version of
the game of cricket. Games last for up to 5 days... just like running some
test suites. The usual approach for making cricket watchable is a generous
dose of beer; in programming, `Balmer Peak`_ limits come into effect, so
something else is required...
.. _BeeWare suite: http://pybee.org/
.. _http://pybee.org/cricket: http://pybee.org/cricket
.. _Test Cricket: http://en.wikipedia.org/wiki/Test_cricket
.. _Balmer Peak: http://xkcd.com/323/
Quickstart
----------
At present, Cricket has support for:
* Pre-Django 1.6 project test suites,
* Django 1.6+ project test suites using unittest2-style discovery, and
* unittest project test suites.
In your Django project, install cricket, and then run it::
$ pip install cricket
$ cricket-django
``cricket-django`` will also work in Django's own tests directory -- i.e., you
can use ``cricket-django`` to run Django's own test suite (for Django 1.6 or
later).
In a unittest project, install cricket, and then run it::
$ pip install cricket
$ cricket-unittest
This will pop up a GUI window. Hit "Run all", and watch your test suite
execute. A progress bar is displayed in the bottom right hand corner of
the window, along with an estimate of time remaining.
While the suite is running, you can click on test names to see the output
of that test. If the test passed, it will be displayed in green; other test
results will be shown in other colors.
Problems under Ubuntu
~~~~~~~~~~~~~~~~~~~~~
Ubuntu's packaging of Python omits the ``idlelib`` library from it's base
package. If you're using Python 2.7 on Ubuntu 13.04, you can install
``idlelib`` by running::
$ sudo apt-get install idle-python2.7
For other versions of Python and Ubuntu, you'll need to adjust this as
appropriate.
Problems under Windows
~~~~~~~~~~~~~~~~~~~~~~
If you're running Cricket in a virtualenv under Windows, you'll need to set an
environment variable so that Cricket can find the TCL graphics library::
$ set TCL_LIBRARY=c:\Python27\tcl\tcl8.5
You'll need to adjust the exact path to reflect your local Python install.
You may find it helpful to put this line in the ``activate.bat`` script
for your virtual environment so that it is automatically set whenever the
virtualenv is activated.
Documentation
-------------
Documentation for cricket can be found on `Read The Docs`_.
Community
---------
Cricket is part of the `BeeWare suite`_. You can talk to the community through:
* `@pybeeware on Twitter`_
* The `BeeWare Users Mailing list`_, for questions about how to use the BeeWare suite.
* The `BeeWare Developers Mailing list`_, for discussing the development of new features in the BeeWare suite, and ideas for new tools for the suite.
Contributing
------------
If you experience problems with cricket, `log them on GitHub`_. If you want to contribute code, please `fork the code`_ and `submit a pull request`_.
.. _Read The Docs: https://cricket.readthedocs.io
.. _@pybeeware on Twitter: https://twitter.com/pybeeware
.. _BeeWare Users Mailing list: https://groups.google.com/forum/#!forum/beeware-users
.. _BeeWare Developers Mailing list: https://groups.google.com/forum/#!forum/beeware-developers
.. _log them on Github: https://github.com/pybee/cricket/issues
.. _fork the code: https://github.com/pybee/cricket
.. _submit a pull request: https://github.com/pybee/cricket/pulls
Platform: UNKNOWN
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: BSD License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 2
Classifier: Programming Language :: Python :: 2.7
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.3
Classifier: Programming Language :: Python :: 3.4
Classifier: Topic :: Software Development
Classifier: Topic :: Software Development :: Testing
Classifier: Topic :: Utilities

113
cricket/README.rst Normal file
View file

@ -0,0 +1,113 @@
Cricket
=======
Cricket is part of the `BeeWare suite`_. The project website is `http://pybee.org/cricket`_.
Cricket is a graphical tool that helps you run your test suites.
Normal unittest test runners dump all output to the console, and provide very
little detail while the suite is running. As a result:
* You can't start looking at failures until the test suite has completed running,
* It isn't a very accessible format for identifying patterns in test failures,
* It can be hard (or cumbersome) to re-run any tests that have failed.
Why the name ``cricket``? `Test Cricket`_ is the most prestigious version of
the game of cricket. Games last for up to 5 days... just like running some
test suites. The usual approach for making cricket watchable is a generous
dose of beer; in programming, `Balmer Peak`_ limits come into effect, so
something else is required...
.. _BeeWare suite: http://pybee.org/
.. _http://pybee.org/cricket: http://pybee.org/cricket
.. _Test Cricket: http://en.wikipedia.org/wiki/Test_cricket
.. _Balmer Peak: http://xkcd.com/323/
Quickstart
----------
At present, Cricket has support for:
* Pre-Django 1.6 project test suites,
* Django 1.6+ project test suites using unittest2-style discovery, and
* unittest project test suites.
In your Django project, install cricket, and then run it::
$ pip install cricket
$ cricket-django
``cricket-django`` will also work in Django's own tests directory -- i.e., you
can use ``cricket-django`` to run Django's own test suite (for Django 1.6 or
later).
In a unittest project, install cricket, and then run it::
$ pip install cricket
$ cricket-unittest
This will pop up a GUI window. Hit "Run all", and watch your test suite
execute. A progress bar is displayed in the bottom right hand corner of
the window, along with an estimate of time remaining.
While the suite is running, you can click on test names to see the output
of that test. If the test passed, it will be displayed in green; other test
results will be shown in other colors.
Problems under Ubuntu
~~~~~~~~~~~~~~~~~~~~~
Ubuntu's packaging of Python omits the ``idlelib`` library from it's base
package. If you're using Python 2.7 on Ubuntu 13.04, you can install
``idlelib`` by running::
$ sudo apt-get install idle-python2.7
For other versions of Python and Ubuntu, you'll need to adjust this as
appropriate.
Problems under Windows
~~~~~~~~~~~~~~~~~~~~~~
If you're running Cricket in a virtualenv under Windows, you'll need to set an
environment variable so that Cricket can find the TCL graphics library::
$ set TCL_LIBRARY=c:\Python27\tcl\tcl8.5
You'll need to adjust the exact path to reflect your local Python install.
You may find it helpful to put this line in the ``activate.bat`` script
for your virtual environment so that it is automatically set whenever the
virtualenv is activated.
Documentation
-------------
Documentation for cricket can be found on `Read The Docs`_.
Community
---------
Cricket is part of the `BeeWare suite`_. You can talk to the community through:
* `@pybeeware on Twitter`_
* The `BeeWare Users Mailing list`_, for questions about how to use the BeeWare suite.
* The `BeeWare Developers Mailing list`_, for discussing the development of new features in the BeeWare suite, and ideas for new tools for the suite.
Contributing
------------
If you experience problems with cricket, `log them on GitHub`_. If you want to contribute code, please `fork the code`_ and `submit a pull request`_.
.. _Read The Docs: https://cricket.readthedocs.io
.. _@pybeeware on Twitter: https://twitter.com/pybeeware
.. _BeeWare Users Mailing list: https://groups.google.com/forum/#!forum/beeware-users
.. _BeeWare Developers Mailing list: https://groups.google.com/forum/#!forum/beeware-developers
.. _log them on Github: https://github.com/pybee/cricket/issues
.. _fork the code: https://github.com/pybee/cricket
.. _submit a pull request: https://github.com/pybee/cricket/pulls

View file

@ -0,0 +1,135 @@
Metadata-Version: 1.1
Name: cricket
Version: 0.2.5
Summary: A graphical tool to assist running test suites.
Home-page: http://pybee.org/cricket
Author: Russell Keith-Magee
Author-email: russell@keith-magee.com
License: New BSD
Description: Cricket
=======
Cricket is part of the `BeeWare suite`_. The project website is `http://pybee.org/cricket`_.
Cricket is a graphical tool that helps you run your test suites.
Normal unittest test runners dump all output to the console, and provide very
little detail while the suite is running. As a result:
* You can't start looking at failures until the test suite has completed running,
* It isn't a very accessible format for identifying patterns in test failures,
* It can be hard (or cumbersome) to re-run any tests that have failed.
Why the name ``cricket``? `Test Cricket`_ is the most prestigious version of
the game of cricket. Games last for up to 5 days... just like running some
test suites. The usual approach for making cricket watchable is a generous
dose of beer; in programming, `Balmer Peak`_ limits come into effect, so
something else is required...
.. _BeeWare suite: http://pybee.org/
.. _http://pybee.org/cricket: http://pybee.org/cricket
.. _Test Cricket: http://en.wikipedia.org/wiki/Test_cricket
.. _Balmer Peak: http://xkcd.com/323/
Quickstart
----------
At present, Cricket has support for:
* Pre-Django 1.6 project test suites,
* Django 1.6+ project test suites using unittest2-style discovery, and
* unittest project test suites.
In your Django project, install cricket, and then run it::
$ pip install cricket
$ cricket-django
``cricket-django`` will also work in Django's own tests directory -- i.e., you
can use ``cricket-django`` to run Django's own test suite (for Django 1.6 or
later).
In a unittest project, install cricket, and then run it::
$ pip install cricket
$ cricket-unittest
This will pop up a GUI window. Hit "Run all", and watch your test suite
execute. A progress bar is displayed in the bottom right hand corner of
the window, along with an estimate of time remaining.
While the suite is running, you can click on test names to see the output
of that test. If the test passed, it will be displayed in green; other test
results will be shown in other colors.
Problems under Ubuntu
~~~~~~~~~~~~~~~~~~~~~
Ubuntu's packaging of Python omits the ``idlelib`` library from it's base
package. If you're using Python 2.7 on Ubuntu 13.04, you can install
``idlelib`` by running::
$ sudo apt-get install idle-python2.7
For other versions of Python and Ubuntu, you'll need to adjust this as
appropriate.
Problems under Windows
~~~~~~~~~~~~~~~~~~~~~~
If you're running Cricket in a virtualenv under Windows, you'll need to set an
environment variable so that Cricket can find the TCL graphics library::
$ set TCL_LIBRARY=c:\Python27\tcl\tcl8.5
You'll need to adjust the exact path to reflect your local Python install.
You may find it helpful to put this line in the ``activate.bat`` script
for your virtual environment so that it is automatically set whenever the
virtualenv is activated.
Documentation
-------------
Documentation for cricket can be found on `Read The Docs`_.
Community
---------
Cricket is part of the `BeeWare suite`_. You can talk to the community through:
* `@pybeeware on Twitter`_
* The `BeeWare Users Mailing list`_, for questions about how to use the BeeWare suite.
* The `BeeWare Developers Mailing list`_, for discussing the development of new features in the BeeWare suite, and ideas for new tools for the suite.
Contributing
------------
If you experience problems with cricket, `log them on GitHub`_. If you want to contribute code, please `fork the code`_ and `submit a pull request`_.
.. _Read The Docs: https://cricket.readthedocs.io
.. _@pybeeware on Twitter: https://twitter.com/pybeeware
.. _BeeWare Users Mailing list: https://groups.google.com/forum/#!forum/beeware-users
.. _BeeWare Developers Mailing list: https://groups.google.com/forum/#!forum/beeware-developers
.. _log them on Github: https://github.com/pybee/cricket/issues
.. _fork the code: https://github.com/pybee/cricket
.. _submit a pull request: https://github.com/pybee/cricket/pulls
Platform: UNKNOWN
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: BSD License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 2
Classifier: Programming Language :: Python :: 2.7
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.3
Classifier: Programming Language :: Python :: 3.4
Classifier: Topic :: Software Development
Classifier: Topic :: Software Development :: Testing
Classifier: Topic :: Utilities

View file

@ -0,0 +1,44 @@
AUTHORS
LICENSE
MANIFEST.in
README.rst
requirements_dev.txt
setup.cfg
setup.py
cricket/__init__.py
cricket/compat.py
cricket/events.py
cricket/executor.py
cricket/main.py
cricket/model.py
cricket/pipes.py
cricket/view.py
cricket.egg-info/PKG-INFO
cricket.egg-info/SOURCES.txt
cricket.egg-info/dependency_links.txt
cricket.egg-info/entry_points.txt
cricket.egg-info/pbr.json
cricket.egg-info/requires.txt
cricket.egg-info/top_level.txt
cricket/django/__init__.py
cricket/django/__main__.py
cricket/django/discoverer.py
cricket/django/django_runtests.py
cricket/django/executor.py
cricket/django/model.py
cricket/unittest/__init__.py
cricket/unittest/__main__.py
cricket/unittest/discoverer.py
cricket/unittest/executor.py
cricket/unittest/model.py
docs/Makefile
docs/conf.py
docs/index.rst
docs/make.bat
docs/releases.rst
docs/internals/backends.rst
docs/internals/contributing.rst
docs/internals/roadmap.rst
tests/__init__.py
tests/test_models.py
tests/test_unit_integration.py

View file

@ -0,0 +1 @@

View file

@ -0,0 +1,4 @@
[console_scripts]
cricket-django = cricket.django.__main__:main
cricket-unittest = cricket.unittest.__main__:main

View file

@ -0,0 +1 @@
{"is_release": false, "git_version": "4636fe3"}

View file

@ -0,0 +1 @@
tkreadonly

View file

@ -0,0 +1,2 @@
cricket
tests

View file

@ -0,0 +1,9 @@
# Examples of valid version strings
# __version__ = '1.2.3.dev1' # Development release 1
# __version__ = '1.2.3a1' # Alpha Release 1
# __version__ = '1.2.3b1' # Beta Release 1
# __version__ = '1.2.3rc1' # RC Release 1
# __version__ = '1.2.3' # Final Release
# __version__ = '1.2.3.post1' # Post Release 1
__version__ = '0.2.5'

14
cricket/cricket/compat.py Normal file
View file

@ -0,0 +1,14 @@
# This is for backwards compatibility with Python version < 2.7 only
# for Python 2.7 and 3.x the default unittest is the correct package
# to use. unittest2 is a backport of this package to be used in
# versions < 2.7.
# Use
# from cricket.compat import unittest
#
# to make versions work with all version of Python. This will be slowly
# deprecated in the future as Python < 2.7 becomes more and mor
# obsolete.
from __future__ import absolute_import
import unittest # NOQA
if not hasattr(unittest.TestCase, 'assertIsNotNone'):
import unittest2 as unittest # NOQA

View file

View file

@ -0,0 +1,13 @@
'''
This is the main entry point for running Django test suites.
'''
from cricket.main import main as cricket_main
from cricket.django.model import DjangoProject
def main():
cricket_main(DjangoProject)
if __name__ == "__main__":
main()

View file

@ -0,0 +1,39 @@
from __future__ import absolute_import
import unittest
from django.conf import settings
try:
from django.test.simple import DjangoTestSuiteRunner
except ImportError:
DjangoTestSuiteRunner = None
from django.test.utils import get_runner
# Dynamically retrieve the test runner class for this project.
TestRunnerClass = get_runner(settings, None)
class TestDiscoverer(TestRunnerClass):
"""A Django test runner that prints out all the test that will be run.
Doesn't actually run any of the tests.
"""
def _output_suite(self, suite):
for test in suite:
# Django 1.6 introduce the new-style test runner.
# If that test runner is in use, we use the full test name.
# If we're still using a pre 1.6-style runner, we need to
# drop out all everything between the app name and the test module.
if isinstance(test, unittest.TestSuite):
self._output_suite(test)
elif (DjangoTestSuiteRunner
and issubclass(TestRunnerClass, DjangoTestSuiteRunner)):
parts = test.id().split('.')
tests_index = parts.index('tests')
print('%s.%s.%s' % (parts[tests_index - 1], parts[-2], parts[-1]))
else:
print(test.id())
def run_tests(self, test_labels, extra_tests=None, **kwargs):
self._output_suite(self.build_suite(test_labels))
return 0

View file

@ -0,0 +1,49 @@
#!/usr/bin/env python
import os
import warnings
import argparse
from django.utils import importlib
import runtests
def django_tests(runner, labels):
state = runtests.setup(1, labels)
module_name, runner_class_name = runner.rsplit('.', 1)
module = importlib.import_module(module_name)
TestRunner = getattr(module, runner_class_name)
runner = TestRunner(
verbosity=1,
interactive=False,
failfast=False,
)
# Catch warnings thrown in test DB setup -- remove in Django 1.9
with warnings.catch_warnings():
warnings.filterwarnings(
'ignore',
"Custom SQL location '<app_label>/models/sql' is deprecated, "
"use '<app_label>/sql' instead.",
PendingDeprecationWarning
)
failures = runner.run_tests(labels or runtests.get_installed())
runtests.teardown(state)
return failures
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument("--settings", help="The settings file to use.", action="store")
parser.add_argument("--testrunner", help="The test runner to use.", action="store")
parser.add_argument('args', nargs=argparse.REMAINDER, help='Test labels to execute.')
options = parser.parse_args()
os.environ.setdefault("DJANGO_SETTINGS_MODULE", options.settings)
django_tests(options.testrunner, options.args)

View file

@ -0,0 +1,48 @@
from __future__ import absolute_import
try:
from coverage import coverage
except ImportError:
coverage = None
from django.conf import settings
try:
from django.test.simple import DjangoTestSuiteRunner
except ImportError:
DjangoTestSuiteRunner = None
from django.test.utils import get_runner
from cricket.pipes import PipedTestRunner
# Dynamically retrieve the test runner class for this project.
TestRunnerClass = get_runner(settings, None)
class TestExecutor(TestRunnerClass):
"""A Django test runner that runs the test suite.
Formats output in a machine-readable format.
"""
def run_suite(self, suite, **kwargs):
# Django 1.6 introduce the new-style test runner.
# If that test runner is in use, we use the full test name.
# If we're still using a pre 1.6-style runner, we need to
# drop out all everything between the app name and the test module.
use_old_discovery = (DjangoTestSuiteRunner and
issubclass(TestRunnerClass, DjangoTestSuiteRunner))
return PipedTestRunner(use_old_discovery=use_old_discovery).run(suite)
class TestCoverageExecutor(TestExecutor):
"""A Django test runner that runs the test suite with coverage
Formats output in a machine-readable format.
"""
def run_suite(self, suite, **kwargs):
cov = coverage()
cov.start()
result = super(TestCoverageExecutor, self).run_suite(suite, **kwargs)
cov.stop()
cov.save()
return result

View file

@ -0,0 +1,74 @@
'''
In general, you would expect that there would only be one project class
specified in this file. It provides the interface to executing test
collecetion and execution.
'''
import os
import sys
from cricket.model import Project
class DjangoProject(Project):
'''
The Project is a wrapper around the command-line calls to interface
to test collection and test execution
'''
def __init__(self, options=None):
self.settings = None
if options and hasattr(options, 'settings'):
self.settings = options.settings
super(DjangoProject, self).__init__()
@classmethod
def add_arguments(cls, parser):
"""Add Django-specific settings to the argument parser.
"""
settings_help = ("The Python path to a settings module, e.g. "
"\"myproject.settings.main\". If this isn't provided, the "
"DJANGO_SETTINGS_MODULE environment variable will be "
"used.")
parser.add_argument('--settings', help=settings_help)
@property
def script(self):
if os.path.exists(os.path.join(os.getcwd(), 'manage.py')):
# We're running the test suite on a normal Django project
script = ['manage.py', 'test', '--noinput']
elif os.path.exists(os.path.join(os.getcwd(), 'runtests.py')):
# We're running Django's own test script
script = [os.path.join(os.path.dirname(__file__), 'django_runtests.py')]
os.environ['PYTHONPATH'] = os.getcwd()
if self.settings is None:
self.settings = 'test_sqlite'
else:
raise Exception("Can't find a Django test suite to execute.")
return script
def discover_commandline(self):
"Command lineDiscover all available tests in a project."
command = [sys.executable] + self.script
if self.settings:
command.append('--settings={0}'.format(self.settings))
command.append('--testrunner=cricket.django.discoverer.TestDiscoverer')
return command
def execute_commandline(self, labels):
"Return the command line to execute the specified test labels"
command = [sys.executable] + self.script
if self.settings:
command.append('--settings={0}'.format(self.settings))
if self.coverage:
command.append('--testrunner=cricket.django.executor.TestCoverageExecutor')
else:
command.append('--testrunner=cricket.django.executor.TestExecutor')
command.extend(labels)
return command

21
cricket/cricket/events.py Normal file
View file

@ -0,0 +1,21 @@
class EventSource(object):
"""A source of GUI events.
An event source can receive handlers for events, and
can emit events.
"""
_events = {}
@classmethod
def bind(cls, event, handler):
cls._events.setdefault(cls, {}).setdefault(event, []).append(handler)
def emit(self, event, **data):
try:
for handler in self._events[self.__class__][event]:
handler(self, **data)
except KeyError:
# No handler registered for event.
pass

260
cricket/cricket/executor.py Normal file
View file

@ -0,0 +1,260 @@
import json
import subprocess
import sys
from threading import Thread
try:
from Queue import Queue, Empty
except ImportError:
from queue import Queue, Empty # python 3.x
from cricket.events import EventSource
from cricket.model import TestMethod
from cricket.pipes import PipedTestResult, PipedTestRunner
def enqueue_output(out, queue):
"""A utility method for consuming piped output from a subprocess.
Reads content from `out` one line at a time, and puts it onto
queue for consumption in a separate thread.
"""
for line in iter(out.readline, b''):
queue.put(line.strip().decode('utf-8'))
out.close()
class Executor(EventSource):
"A wrapper around the subprocess that executes tests."
def __init__(self, project, module, count, labels):
self.project = project
self.module = module
self.proc = subprocess.Popen(
self.project.execute_commandline(labels),
stdin=None,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
shell=False,
close_fds='posix' in sys.builtin_module_names
)
# Piped stdout/stderr reads are blocking; therefore, we need to
# do all our readline calls in a background thread, and use a
# queue object to store lines that have been read.
self.stdout = Queue()
t = Thread(target=enqueue_output, args=(self.proc.stdout, self.stdout))
t.daemon = True
t.start()
self.stderr = Queue()
t = Thread(target=enqueue_output, args=(self.proc.stderr, self.stderr))
t.daemon = True
t.start()
# The TestMethod object currently under execution.
self.current_test = None
# An accumulator of ouput from the tests. If buffer is None,
# then the test suite isn't currently running - it's in suite
# setup/teardown.
self.buffer = None
# An accumulator for error output from the tests.
self.error_buffer = []
# The timestamp when current_test started
self.start_time = None
# The total count of tests under execution
self.total_count = count
# The count of tests that have been executed.
self.completed_count = 0
# The count of specific test results.
self.result_count = {}
@property
def is_running(self):
"Return True if this runner currently running."
return self.proc.poll() is None
@property
def any_failed(self):
return sum(self.result_count.get(state, 0) for state in TestMethod.FAILING_STATES)
def terminate(self):
"Stop the executor."
self.proc.terminate()
def poll(self):
"Poll the runner looking for new test output"
stopped = False
finished = False
# Read from stdout, building a buffer.
lines = []
try:
while True:
lines.append(self.stdout.get(block=False))
except Empty:
# queue.get() raises an exception when the queue is empty.
# This means there is no more output to consume at this time.
pass
# Read from stderr, building a buffer.
try:
while True:
self.error_buffer.append(self.stderr.get(block=False))
except Empty:
# queue.get() raises an exception when the queue is empty.
# This means there is no more output to consume at this time.
pass
# Check to see if the subprocess is still running.
# If it isn't, raise an error.
if self.proc is None:
stopped = True
elif self.proc.poll() is not None:
stopped = True
# Process all the full lines that are available
for line in lines:
# Look for a separator.
if line in (PipedTestResult.RESULT_SEPARATOR, PipedTestRunner.START_TEST_RESULTS, PipedTestRunner.END_TEST_RESULTS):
if self.buffer is None:
# Preamble is finished. Set up the line buffer.
self.buffer = []
else:
# Start of new test result; record the last result
# Then, work out what content goes where.
pre = json.loads(self.buffer[0])
post = json.loads(self.buffer[1])
if post['status'] == 'OK':
status = TestMethod.STATUS_PASS
error = None
elif post['status'] == 's':
status = TestMethod.STATUS_SKIP
error = 'Skipped: ' + post.get('error')
elif post['status'] == 'F':
status = TestMethod.STATUS_FAIL
error = post.get('error')
elif post['status'] == 'x':
status = TestMethod.STATUS_EXPECTED_FAIL
error = post.get('error')
elif post['status'] == 'u':
status = TestMethod.STATUS_UNEXPECTED_SUCCESS
error = None
elif post['status'] == 'E':
status = TestMethod.STATUS_ERROR
error = post.get('error')
output = post.get('output')
if output:
print(f'{self.current_test.path}:')
print(output)
if error:
print(error)
# Increase the count of executed tests
self.completed_count = self.completed_count + 1
# Get the start and end times for the test
start_time = float(pre['start_time'])
end_time = float(post['end_time'])
self.current_test.description = post['description']
self.current_test.set_result(
status=status,
output=output,
error=error,
duration=end_time - start_time,
)
# Work out how long the suite has left to run (approximately)
if self.start_time is None:
self.start_time = start_time
total_duration = end_time - self.start_time
time_per_test = total_duration / self.completed_count
remaining_time = (self.total_count - self.completed_count) * time_per_test
if remaining_time > 4800:
remaining = '%s hours' % int(remaining_time / 2400)
elif remaining_time > 2400:
remaining = '%s hour' % int(remaining_time / 2400)
elif remaining_time > 120:
remaining = '%s mins' % int(remaining_time / 60)
elif remaining_time > 60:
remaining = '%s min' % int(remaining_time / 60)
else:
remaining = '%ss' % int(remaining_time)
# Update test result counts
self.result_count.setdefault(status, 0)
self.result_count[status] = self.result_count[status] + 1
# Notify the display to update.
self.emit('test_end', module=self.module, test_path=self.current_test.path, result=status, remaining_time=remaining)
# Clear the decks for the next test.
self.current_test = None
self.buffer = []
if line == PipedTestRunner.END_TEST_RESULTS:
# End of test execution.
# Mark the runner as finished, and move back
# to a pre-test state in the results.
finished = True
self.buffer = None
else:
# Not a separator line, so it's actual content.
if self.buffer is None:
# Suite isn't running yet - just display the output
# as a status update line.
self.emit('test_status_update', module=self.module, update=line)
else:
# Suite is running - have we got an active test?
# Doctest (and some other tools) output invisible escape sequences.
# Strip these if they exist.
if line.startswith('\x1b'):
line = line[line.find('{'):]
# Store the cleaned buffer
self.buffer.append(line)
# If we don't have an currently active test, this line will
# contain the path for the test.
if self.current_test is None:
try:
# No active test; first line tells us which test is running.
pre = json.loads(line)
except ValueError:
self.emit('suit_end', module=self.module)
return True
self.current_test = self.project.confirm_exists(pre['path'])
self.emit('test_start', module=self.module, test_path=pre['path'])
# If we're not finished, requeue the event.
if finished:
if self.error_buffer:
self.emit('suite_end', module=self.module, error='\n'.join(self.error_buffer))
else:
self.emit('suite_end', module=self.module)
return False
elif stopped:
# Suite has stopped producing output.
if self.error_buffer:
self.emit('suite_error', module=self.module, error='\n'.join(self.error_buffer))
else:
self.emit('suite_error', module=self.module, error='Test output ended unexpectedly')
# Suite has finished; don't requeue
return False
else:
# Still running - requeue event.
return True

21
cricket/cricket/lang.py Normal file
View file

@ -0,0 +1,21 @@
from typing import Union
import os
import json
class SimpleLang(object):
def __init__(self, file = None):
file = file if file else 'languages.json'
self._load_from_file(file)
self._current_lang = 'zh'
def _load_from_file(self, file: str):
path = os.path.dirname(os.path.abspath(__file__)) + '/' + file
with open(path) as f:
self.lang_dict = json.load(f)
@property
def current_lang(self):
return self._current_lang
def get_text(self, key):
return self.lang_dict[self._current_lang][key]

View file

@ -0,0 +1,36 @@
{
"zh": {
"title": "工厂测试",
"run_all_button": "运行",
"run_selected_button": "运行选择",
"stop_button": "停止",
"quit_button": "退出",
"reboot_button": "重启",
"poweroff_button": "关闭",
"test_table_head": ["模块", "测试项", "结果"],
"test_step": "测试步骤",
"start_button": "开始",
"play_button": "播放",
"record_button": "录音",
"playback_button": "回放",
"pass_button": "通过",
"fail_button": "失败"
},
"en": {
"title": "Factory Test",
"run_all_button": "Run",
"run_selected_button": "Run Selected",
"stop_button": "Stop",
"quit_button": "Quit",
"reboot_button": "Reboot",
"poweroff_button": "Shutdown",
"test_table_head": ["Module", "Test Item", "Result"],
"test_step": "Test Step",
"start_button": "Start",
"play_button": "Play",
"record_button": "Record",
"playback_button": "Playback",
"pass_button": "Pass",
"fail_button": "Fail"
}
}

112
cricket/cricket/main.py Normal file
View file

@ -0,0 +1,112 @@
'''
The purpose of this module is to set up the Cricket GUI,
load a "project" for discovering and executing tests, and
to initiate the GUI main loop.
'''
from argparse import ArgumentParser
import subprocess
import sys
# try:
# from Tkinter import *
# except ImportError:
# from tkinter import *
# from cricket.view import (
# MainWindow,
# TestLoadErrorDialog,
# IgnorableTestLoadErrorDialog
# )
from PyQt5.QtWidgets import QApplication, QMessageBox
from cricket.qtview import MainWindow
from cricket.model import ModelLoadError
def main(Model):
"""Run the main loop of the app.
Take the project Model as the argument. This model will be
instantiated as part of the main loop.
"""
parser = ArgumentParser()
parser.add_argument("--version", help="Display version number and exit", action="store_true")
Model.add_arguments(parser)
options = parser.parse_args()
# Check the shortcut options
if options.version:
import cricket
print(cricket.__version__)
return
# Set up the root Tk context
# root = Tk()
# Construct an empty window
# view = MainWindow(root)
# Try to load the project. If any error occurs during
# project load, show an error dialog
# project = None
# while project is None:
try:
# Create the project objects
project = Model(options)
runner = subprocess.Popen(
project.discover_commandline(),
stdin=None,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
shell=False,
)
test_list = []
for line in runner.stdout:
test = line.strip().decode('utf-8')
if test.startswith('unittest.loader._FailedTest.'):
print('Load failed:', test.replace('unittest.loader._FailedTest.', ''))
sys.exit(1)
test_list.append(line.strip().decode('utf-8'))
errors = []
for line in runner.stderr:
errors.append(line.strip().decode('utf-8'))
if errors and not test_list:
raise ModelLoadError('\n'.join(errors))
project.refresh(test_list, errors)
except ModelLoadError as e:
# Load failed; destroy the project and show an error dialog.
# If the user selects cancel, quit.
project = None
sys.exit(1)
# dialog = TestLoadErrorDialog(root, e.trace)
# if dialog.status == dialog.CANCEL:
# sys.exit(1)
if project.errors:
sys.exit(1)
# dialog = IgnorableTestLoadErrorDialog(root, '\n'.join(project.errors))
# if dialog.status == dialog.CANCEL:
# sys.exit(1)
# Set up the root Tk context
app = QApplication([])
# Construct an empty window
view = MainWindow(app)
# Set the project for the main window.
# This populates the tree, and sets listeners for
# future tree modifications.
view.project = project
# Run the main loop
try:
view.mainloop()
except KeyboardInterrupt:
view.on_quit()

514
cricket/cricket/model.py Normal file
View file

@ -0,0 +1,514 @@
"""A module containing a data representation for the test suite.
This is the "Model" of the MVC world.
Each object in the model is an event source; views/controllers
can bind to events on the model to be notified of changes.
"""
from datetime import datetime
from cricket.events import EventSource
class ModelLoadError(Exception):
def __init__(self, trace):
super(ModelLoadError, self).__init__()
self.trace = trace
class TestMethod(EventSource):
"""A data representation of an individual test method.
Emits:
* 'new' when a new node is added
* 'inactive' when the test method is made inactive in the suite.
* 'active' when the test method is made active in the suite.
* 'status_update' when the pass/fail status of the method is updated.
"""
STATUS_PASS = 100
STATUS_SKIP = 200
STATUS_FAIL = 300
STATUS_EXPECTED_FAIL = 310
STATUS_UNEXPECTED_SUCCESS = 320
STATUS_ERROR = 400
FAILING_STATES = (STATUS_FAIL, STATUS_UNEXPECTED_SUCCESS, STATUS_ERROR)
STATUS_LABELS = {
STATUS_PASS: 'passed',
STATUS_SKIP: 'skipped',
STATUS_FAIL: 'failures',
STATUS_EXPECTED_FAIL: 'expected failures',
STATUS_UNEXPECTED_SUCCESS: 'unexpected successes',
STATUS_ERROR: 'errors',
}
def __init__(self, name, testCase):
self.name = name
self.description = ''
self._active = True
self._result = None
# Set the parent of the TestMethod
self.parent = testCase
self.parent[name] = self
self.parent._update_active()
# Announce that there is a new test method
self.emit('new')
def __repr__(self):
return u'TestMethod %s' % self.path
@property
def path(self):
"The dotted-path name that identifies this test method to the test runner"
return u'%s.%s' % (self.parent.path, self.name)
@property
def active(self):
"Is this test method currently active?"
return self._active
def set_active(self, is_active, cascade=True):
"""Explicitly set the active state of the test method
If cascade is True, the parent testCase will be prompted
to check it's current active status.
"""
if self._active:
if not is_active:
self._active = False
self.emit('inactive')
if cascade:
self.parent._update_active()
else:
if is_active:
self._active = True
self.emit('active')
if cascade:
self.parent._update_active()
def toggle_active(self):
"Toggle the current active status of this test method"
self.set_active(not self.active)
@property
def status(self):
try:
return self._result['status']
except TypeError:
return None
@property
def output(self):
try:
return self._result['output']
except TypeError:
return None
@property
def error(self):
try:
return self._result['error']
except TypeError:
return None
@property
def duration(self):
try:
return self._result['duration']
except TypeError:
return None
def set_result(self, status, output, error, duration):
self._result = {
'status': status,
'output': output,
'error': error,
'duration': duration,
}
self.emit('status_update')
class TestCase(dict, EventSource):
"""A data representation of a test case, wrapping multiple test methods.
Emits:
* 'new' when a new node is added
* 'inactive' when the test method is made inactive in the suite.
* 'active' when the test method is made active in the suite.
"""
def __init__(self, name, testApp):
super(TestCase, self).__init__()
self.name = name
self._active = True
# Set the parent of the TestCase
self.parent = testApp
self.parent[name] = self
self.parent._update_active()
# Announce that there is a new TestCase
self.emit('new')
def __repr__(self):
return u'TestCase %s' % self.path
@property
def path(self):
"The dotted-path name that identifies this Test Case to the test runner"
return u'%s.%s' % (self.parent.path, self.name)
@property
def active(self):
"Is this test method currently active?"
return self._active
def set_active(self, is_active, cascade=True):
"""Explicitly set the active state of the test case.
Forces all methods on this test case to set to the same
active status.
If cascade is True, the parent test module will be prompted
to check it's current active status.
"""
if self._active:
if not is_active:
self._active = False
self.emit('inactive')
if cascade:
self.parent._update_active()
for testMethod in self.values():
testMethod.set_active(False, cascade=False)
else:
if is_active:
self._active = True
self.emit('active')
if cascade:
self.parent._update_active()
for testMethod in self.values():
testMethod.set_active(True, cascade=False)
def toggle_active(self):
"Toggle the current active status of this test case"
self.set_active(not self.active)
def find_tests(self, active=True, status=None, labels=None):
"""Find the test labels matching the search criteria.
This will check:
* active: if the method is currently an active test
* status: if the last run status of the method is in the provided list
* labels: if the method label is in the provided list
Returns a count of tests found, plus the labels needed to
execute those tests.
"""
tests = []
count = 0
for testMethod_name, testMethod in self.items():
include = True
# If only active tests have been requested, the method
# must be active.
if active and not testMethod.active:
include = False
# If a list of statuses has been provided, the
# method status must be in that list.
if status and testMethod.status not in status:
include = False
# If a list of test labels has been provided, the method
# must be named explicitly
if labels and testMethod.path not in labels:
include = False
if include:
count = count + 1
tests.append(testMethod.path)
# If all the tests are included, then just reference the test case.
if len(self) == count:
return len(self), self.path
return count, tests
def _purge(self, timestamp):
"Purge any test method that isn't current as of the timestamp"
for testMethod_name, testMethod in self.items():
if testMethod.timestamp != timestamp:
self.pop(testMethod_name)
def _update_active(self):
"Check the active status of all child nodes, and update the status of this node accordingly"
for testMethod_name, testMethod in self.items():
if testMethod.active:
# As soon as we find an active child, this node
# must be marked active, and no other checks are
# required.
self.set_active(True)
return
self.set_active(False)
class TestModule(dict, EventSource):
"""A data representation of a module. It may contain test cases, or other modules.
Emits:
* 'new' when a new node is added
* 'inactive' when the test method is made inactive in the suite.
* 'active' when the test method is made active in the suite.
"""
def __init__(self, name, parent):
super(TestModule, self).__init__()
self.name = name
self._active = True
# Set the parent of the TestModule.
self.parent = parent
self.parent[name] = self
# Announce that there is a new test case
self.emit('new')
def __repr__(self):
return u'TestModule %s' % self.path
@property
def path(self):
"The dotted-path name that identifies this app to the test runner"
if self.parent.path:
return u'%s.%s' % (self.parent.path, self.name)
return self.name
@property
def active(self):
"Is this test method currently active?"
return self._active
def set_active(self, is_active, cascade=True):
"""Explicitly set the active state of the test case.
Forces all test cases and test modules held by this test module
to be set to the same active status
If cascade is True, the parent test module will be prompted
to check it's current active status.
"""
if self._active:
if not is_active:
self._active = False
self.emit('inactive')
if cascade:
self.parent._update_active()
for testModule in self.values():
testModule.set_active(False, cascade=False)
else:
if is_active:
self._active = True
self.emit('active')
if cascade:
self.parent._update_active()
for testModule in self.values():
testModule.set_active(True, cascade=False)
def toggle_active(self):
"Toggle the current active status of this test case"
self.set_active(not self.active)
def find_tests(self, active=True, status=None, labels=None):
"""Find the test labels matching the search criteria.
This will check:
* active: if the method is currently an active test
* status: if the last run status of the method is in the provided list
* labels: if the method label is in the provided list
Returns a count of tests found, plus the labels needed to
execute those tests.
"""
tests = []
count = 0
found_partial = False
for testModule_name, testModule in self.items():
include = True
# If only active tests have been requested, the module
# must be active.
if active and not testModule.active:
include = False
# If a list of test labels has been provided, either the
# module, or a test *in* the module, must be named explicitly.
if labels:
if testModule.path in labels:
# The module is named explicitly. Include all active
# subtests of this module
subcount, subtests = testModule.find_tests(True, status)
else:
# The module isn't named. Look for all subtests.
# Search for subtests that match.
subcount, subtests = testModule.find_tests(active, status, labels)
else:
subcount, subtests = testModule.find_tests(active, status)
if include:
count = count + subcount
if isinstance(subtests, list):
found_partial = True
tests.extend(subtests)
else:
tests.append(subtests)
# No partials found; just reference the app.
if not found_partial:
return count, self.path
return count, tests
def _purge(self, timestamp):
"""Search all submodules and test cases looking for stale test methods.
Purge any test module without any test cases, and any test Case with no
test methods.
"""
for testModule_name, testModule in self.items():
testModule._purge(timestamp)
if len(testModule) == 0:
self.pop(testModule_name)
def _update_active(self):
"Check the active status of all child nodes, and update the status of this node accordingly"
for subModule_name, subModule in self.items():
if subModule.active:
self.set_active(True)
return
self.set_active(False)
class Project(dict, EventSource):
"""A data representation of an project, containing 1+ test apps.
"""
def __init__(self):
super(Project, self).__init__()
self.errors = []
self.coverage = False
def __repr__(self):
return u'Project'
@classmethod
def add_arguments(cls, parser):
"""Add project specific commandline arguments to the *parser*
object. *parser* is an instance of argparse.ArgumentParser.
"""
pass
@property
def path(self):
"The dotted-path name that identifies this project to the test runner"
return ''
def find_tests(self, active=True, status=None, labels=None):
"""Find the test labels matching the search criteria.
Returns a count of tests found, plus the labels needed to
execute those tests.
"""
tests = []
count = 0
found_partial = False
for testApp_name, testApp in self.items():
include = True
# If only active tests have been requested, the module
# must be active.
if active and not testApp.active:
include = False
# If a list of test labels has been provided, either the
# module, or a test *in* the module, must be named explicitly.
if labels:
if testApp.path in labels:
# The module is named explicitly. Include all active
# subtests of this module
subcount, subtests = testApp.find_tests(True, status)
else:
# The module isn't named. Look for all subtests.
# Search for subtests that match.
subcount, subtests = testApp.find_tests(active, status, labels)
else:
subcount, subtests = testApp.find_tests(active, status)
if include:
count = count + subcount
if isinstance(subtests, list):
found_partial = True
tests.extend(subtests)
else:
tests.append(subtests)
# No partials found; just reference the app.
if not found_partial:
return count, []
return count, tests
def confirm_exists(self, test_label, timestamp=None):
"""Confirm that the given test label exists in the current data model.
If it doesn't, create a representation for it.
"""
parts = test_label.split('.')
if len(parts) < 2:
return
parentModule = self
for testModule_name in parts[:-2]:
try:
testModule = parentModule[testModule_name]
except KeyError:
testModule = TestModule(testModule_name, parentModule)
parentModule = testModule
try:
testCase = parentModule[parts[-2]]
except KeyError:
testCase = TestCase(parts[-2], parentModule)
try:
testMethod = testCase[parts[-1]]
except KeyError:
testMethod = TestMethod(parts[-1], testCase)
testMethod.timestamp = timestamp
return testMethod
def refresh(self, test_list, errors=None):
"""Refresh the project representation so that it contains only the tests in test_list
test_list should be a list of dotted-path test names.
"""
timestamp = datetime.now()
# Make sure there is a data representation for every test in the list.
for test_label in test_list:
self.confirm_exists(test_label, timestamp)
for testModule_name, testModule in self.items():
testModule._purge(timestamp)
if len(testModule) == 0:
self.pop(testModule_name)
self.errors = errors if errors is not None else []
def _update_active(self):
"Exists for API consistency"
pass

220
cricket/cricket/pipes.py Normal file
View file

@ -0,0 +1,220 @@
from __future__ import absolute_import
import json
try:
from StringIO import StringIO
except ImportError:
from io import StringIO
import sys
import time
import traceback
if sys.version_info < (2, 7):
import unittest2 as unittest
else:
import unittest
def trim_docstring(docstring):
"""Trim leading spaces in docstring indentation.
Algorithm taken from PEP 257:
http://www.python.org/dev/peps/pep-0257/#id20
"""
# Convert tabs to spaces (following the normal Python rules)
# and split into a list of lines:
lines = docstring.expandtabs().splitlines()
# Determine minimum indentation (first line doesn't count):
indent = sys.maxsize
for line in lines[1:]:
stripped = line.lstrip()
if stripped:
indent = min(indent, len(line) - len(stripped))
# Remove indentation (first line is special):
trimmed = [lines[0].strip()]
if indent < sys.maxsize:
for line in lines[1:]:
trimmed.append(line[indent:].rstrip())
# Strip off trailing and leading blank lines:
while trimmed and not trimmed[-1]:
trimmed.pop()
while trimmed and not trimmed[0]:
trimmed.pop(0)
# Return a single string:
return '\n'.join(trimmed)
class PipedTestResult(unittest.result.TestResult):
"""A test result class that can print test results in a machine-parseable format.
Used by PipedTestRunner.
"""
RESULT_SEPARATOR = '\x1f' # ASCII US (Unit Separator)
def __init__(self, stream, use_old_discovery=True):
super(PipedTestResult, self).__init__()
self.stream = stream
self.use_old_discovery = use_old_discovery
self._first = True
# Create a clean buffer for stdout content.
self._stdout = StringIO()
sys.stdout = self._stdout
# The test runner is very lightly stateful. It's possible
# for a test to raise an error before the test has actually
# started; we need to make sure that we output a header line
# for the misbehaving test.
self._current_test = None
def description(self, test):
try:
# Wrapped _ErrorHolder objects have their own description
return trim_docstring(test.description)
except AttributeError:
# Fall back to the docstring on the method itself.
if test._testMethodDoc:
return trim_docstring(test._testMethodDoc)
else:
return 'No description'
def startTest(self, test):
super(PipedTestResult, self).startTest(test)
# We know we're starting a new test - record it.
self._current_test = test
self._stdout = StringIO()
sys.stdout = self._stdout
if self.use_old_discovery:
parts = test.id().split('.')
tests_index = parts.index('tests')
path = '%s.%s.%s' % (parts[tests_index - 1], parts[-2], parts[-1])
else:
path = test.id()
body = {
'path': path,
'start_time': time.time()
}
if self._first:
self.stream.write(PipedTestRunner.START_TEST_RESULTS + '\n')
self._first = False
else:
self.stream.write(self.RESULT_SEPARATOR + '\n')
self.stream.write('%s\n' % json.dumps(body))
self.stream.flush()
def addSuccess(self, test):
super(PipedTestResult, self).addSuccess(test)
body = {
'status': 'OK',
'end_time': time.time(),
'description': self.description(test),
'output': self._stdout.getvalue(),
}
self.stream.write('%s\n' % json.dumps(body))
self.stream.flush()
self._current_test = None
def addError(self, test, err):
# If there's no current test, the error occurred during test
# setup. Output a test start line so the protocol isn't confused.
if self._current_test is None:
self.startTest(test)
super(PipedTestResult, self).addError(test, err)
body = {
'status': 'E',
'end_time': time.time(),
'description': self.description(test),
'error': ''.join(traceback.format_exception(*err)),
'output': self._stdout.getvalue(),
}
self.stream.write('%s\n' % json.dumps(body))
self.stream.flush()
self._current_test = None
def addFailure(self, test, err):
super(PipedTestResult, self).addFailure(test, err)
body = {
'status': 'F',
'end_time': time.time(),
'description': self.description(test),
'error': ''.join(traceback.format_exception(*err)),
'output': self._stdout.getvalue(),
}
self.stream.write('%s\n' % json.dumps(body))
self.stream.flush()
self._current_test = None
def addSkip(self, test, reason):
super(PipedTestResult, self).addSkip(test, reason)
body = {
'status': 's',
'end_time': time.time(),
'description': self.description(test),
'error': reason,
'output': self._stdout.getvalue(),
}
self.stream.write('%s\n' % json.dumps(body))
self.stream.flush()
self._current_test = None
def addExpectedFailure(self, test, err):
super(PipedTestResult, self).addExpectedFailure(test, err)
body = {
'status': 'x',
'end_time': time.time(),
'description': self.description(test),
'error': ''.join(traceback.format_exception(*err)),
'output': self._stdout.getvalue(),
}
self.stream.write('%s\n' % json.dumps(body))
self.stream.flush()
self._current_test = None
def addUnexpectedSuccess(self, test):
super(PipedTestResult, self).addUnexpectedSuccess(test)
body = {
'status': 'u',
'end_time': time.time(),
'description': self.description(test),
'output': self._stdout.getvalue(),
}
self.stream.write('%s\n' % json.dumps(body))
self.stream.flush()
self._current_test = None
class PipedTestRunner(unittest.TextTestRunner):
"""A test runner class that displays results in machine-parseable format.
It prints out the names of tests as they are run, errors as they
occur, and a summary of the results at the end of the test run.
"""
START_TEST_RESULTS = '\x02' # ASCII STX (Start of Text)
END_TEST_RESULTS = '\x03' # ASCII ETX (End of Text)
def __init__(self, stream=sys.stdout, use_old_discovery=False):
self.stream = stream
self.use_old_discovery = use_old_discovery
def run(self, test):
"Run the given test case or test suite."
# Remember stdout reference so it can be restored later
old_stdout = sys.stdout
# Create the result pipe, and run the tests with it.
result = PipedTestResult(self.stream, self.use_old_discovery)
# test is TestSuite, as TestSuite.run(result)
# via BaseTestSuite.__call__
test(result)
# Report end of test run
self.stream.write(self.END_TEST_RESULTS + '\n')
self.stream.flush()
# Restore the stdout reference
sys.stdout = old_stdout
return result

509
cricket/cricket/qtview.py Normal file
View file

@ -0,0 +1,509 @@
"""A module containing a visual representation of the testModule.
This is the "View" of the MVC world.
"""
from PyQt5.QtCore import Qt, QTimer
from PyQt5.QtGui import QColor
from PyQt5.QtWidgets import (
QMainWindow,
QFrame,
QVBoxLayout,
QGridLayout,
QPushButton,
QGroupBox,
QTableWidget,
QStatusBar,
QTableWidgetItem
)
import os
from importlib import import_module
from cricket.model import TestMethod, TestCase, TestModule
from cricket.executor import Executor
from cricket.lang import SimpleLang
# Display constants for test status
STATUS = {
TestMethod.STATUS_PASS: {
'description': u'Pass',
'symbol': u'\u25cf',
'tag': 'pass',
'color': '#28C025',
},
TestMethod.STATUS_SKIP: {
'description': u'Skipped',
'symbol': u'S',
'tag': 'skip',
'color': '#259EBF'
},
TestMethod.STATUS_FAIL: {
'description': u'Failure',
'symbol': u'F',
'tag': 'fail',
'color': '#E32C2E'
},
TestMethod.STATUS_EXPECTED_FAIL: {
'description': u'Expected\n failure',
'symbol': u'X',
'tag': 'expected',
'color': '#3C25BF'
},
TestMethod.STATUS_UNEXPECTED_SUCCESS: {
'description': u'Unexpected\n success',
'symbol': u'U',
'tag': 'unexpected',
'color': '#C82788'
},
TestMethod.STATUS_ERROR: {
'description': 'Error',
'symbol': u'E',
'tag': 'error',
'color': '#E4742C'
},
}
STATUS_DEFAULT = {
'description': 'Not\nexecuted',
'symbol': u'',
'tag': None,
'color': '#BFBFBF',
}
class MainWindow(QMainWindow, SimpleLang):
def __init__(self, root):
super().__init__()
self._project = None
self.test_table = {}
self.test_list = {}
self.run_status = {}
self.executor = {}
self.root = root
self.setWindowTitle(self.get_text('title'))
# self.showFullScreen()
# Set up the main content for the window.
self._setup_main_content()
# Set up listeners for runner events.
Executor.bind('test_status_update', self.on_executorStatusUpdate)
Executor.bind('test_start', self.on_executorTestStart)
Executor.bind('test_end', self.on_executorTestEnd)
Executor.bind('suite_end', self.on_executorSuiteEnd)
Executor.bind('suite_error', self.on_executorSuiteError)
######################################################
# Internal GUI layout methods.
######################################################
def _setup_main_content(self):
'''
The button toolbar runs as a horizontal area at the top of the GUI.
It is a persistent GUI component
'''
self.content = QFrame(self)
self.content_layout = QVBoxLayout(self.content)
toolbar = QFrame(self.content)
layout = QGridLayout(toolbar)
self.run_all_button = QPushButton(self.get_text('run_all_button'), toolbar)
self.run_all_button.clicked.connect(self.cmd_run_all)
self.run_all_button.setFocus()
layout.addWidget(self.run_all_button, 0, 0)
self.run_selected_button = QPushButton(self.get_text('run_selected_button'),
toolbar)
self.run_selected_button.setDisabled(True)
self.run_selected_button.clicked.connect(self.cmd_run_selected)
layout.addWidget(self.run_selected_button, 0, 1)
self.stop_button = QPushButton(self.get_text('stop_button'), toolbar)
self.stop_button.setDisabled(True)
self.stop_button.clicked.connect(self.cmd_stop)
layout.addWidget(self.stop_button, 0 , 2)
self.reboot_button = QPushButton(self.get_text('reboot_button'), toolbar)
self.reboot_button.clicked.connect(self.cmd_reboot)
layout.addWidget(self.reboot_button, 0, 3)
self.poweroff_button = QPushButton(self.get_text('poweroff_button'), toolbar)
self.poweroff_button.clicked.connect(self.cmd_poweroff)
layout.addWidget(self.poweroff_button, 0, 4)
self.content_layout.addWidget(toolbar)
self.setCentralWidget(self.content)
def _setup_test_table(self, name):
module = import_module(name)
box = QGroupBox(module.MODULE_NAME[self.current_lang], self.content)
layout = QVBoxLayout(box)
columns = self.get_text('test_table_head')
table = QTableWidget(box)
table.setColumnCount(len(columns))
table.setHorizontalHeaderLabels(columns)
table.setSelectionBehavior(QTableWidget.SelectRows)
table.itemSelectionChanged.connect(self.on_testMethodSelected)
layout.addWidget(table)
self.test_table[name] = table
status = QStatusBar(box)
status.showMessage('Not running')
layout.addWidget(status)
self.run_status[name] = status
self.content_layout.addWidget(box)
######################################################
# Handlers for setting a new project
######################################################
@property
def project(self):
return self._project
def _get_text(self, subModuleName, subModule, key):
module = import_module(subModule.parent.path)
testcase = getattr(module, subModuleName)
if hasattr(testcase, 'LANGUAGES'):
langs = getattr(testcase, 'LANGUAGES')
lang = langs.get(self.current_lang)
if lang is not None:
text = lang.get(key)
if text is not None:
return text
return key
def _add_test_module(self, parentNode, testModule):
for subModuleName, subModule in sorted(testModule.items()):
if isinstance(subModule, TestModule):
self._add_test_module(parentNode, subModule)
else:
for testMethod_name, testMethod in sorted(subModule.items()):
self.test_list.setdefault(parentNode, []).append(testMethod.path)
table = self.test_table[parentNode]
row = table.rowCount()
table.insertRow(row)
item = QTableWidgetItem(self._get_text(subModuleName, subModule, subModuleName))
table.setItem(row, 0, item)
item = QTableWidgetItem(self._get_text(subModuleName, subModule, testMethod_name))
table.setItem(row, 1, item)
item = QTableWidgetItem('')
item.setData(Qt.UserRole, testMethod.path)
table.setItem(row, 2, item)
@project.setter
def project(self, project):
self._project = project
# Get a count of active tests to display in the status bar.
# count, labels = self.project.find_tests(True)
# self.run_summary.set('T:%s P:0 F:0 E:0 X:0 U:0 S:0' % count)
# Populate the initial tree nodes. This is recursive, because
# the tree could be of arbitrary depth.
for testModule_name, testModule in sorted(project.items()):
self._setup_test_table(testModule_name)
self._add_test_module(testModule_name, testModule)
self.executor[testModule_name] = None
self.showFullScreen()
TestMethod.bind('status_update', self.on_nodeStatusUpdate)
######################################################
# TK Main loop
######################################################
def mainloop(self):
self.root.exec_()
######################################################
# User commands
######################################################
def cmd_poweroff(self):
self.stop()
# self.root.quit()
os.system('poweroff')
def cmd_reboot(self):
self.stop()
# self.root.quit()
os.system('reboot')
def cmd_quit(self):
"Command: Quit"
# If the runner is currently running, kill it.
self.stop()
self.root.quit()
def cmd_stop(self, event=None):
"Command: The stop button has been pressed"
self.stop()
def cmd_run_all(self, event=None):
"Command: The Run all button has been pressed"
# If the executor isn't currently running, we can
# start a test run.
for module, executor in self.executor.items():
if not executor or not executor.is_running:
self.run(module)
def cmd_run_selected(self, event=None):
"Command: The 'run selected' button has been pressed"
for module, table in self.test_table.items():
# If the executor isn't currently running, we can
# start a test run.
labels = []
for item in table.selectedItems():
data = item.data(Qt.UserRole)
if data is not None:
labels.append(data)
if labels and (not self.executor[module] or
not self.executor[module].is_running):
self.run(module, labels=labels)
######################################################
# GUI Callbacks
######################################################
def on_testMethodSelected(self):
"Event handler: a test case has been selected in the tree"
# update "run selected" button enabled state
self.set_selected_button_state()
def on_nodeStatusUpdate(self, node):
"Event handler: a node on the tree has received a status update"
module = node.path.split('.')[0]
table = self.test_table[module]
columnCount = table.columnCount()
for row in range(table.rowCount()):
item = table.item(row, columnCount-1)
if item is not None:
if item.data(Qt.UserRole) == node.path:
for column in range(columnCount):
_item = table.item(row, column)
_item.setForeground(QColor(STATUS[node.status]['color']))
item.setText(STATUS[node.status]['description'])
break
def on_testProgress(self, executor):
"Event handler: a periodic update to poll the runner for output, generating GUI updates"
if executor and executor.poll():
QTimer.singleShot(100, lambda: self.on_testProgress(executor))
def on_executorStatusUpdate(self, event, module, update):
"The executor has some progress to report"
# Update the status line.
self.run_status[module].showMessage(update)
def on_executorTestStart(self, event, module, test_path):
"The executor has started running a new test."
# Update status line
self.run_status[module].showMessage('Running %s...' % test_path)
def on_executorTestEnd(self, event, module, test_path, result, remaining_time):
"The executor has finished running a test."
self.run_status[module].showMessage('')
# Update the progress meter
# self.progress_value.set(self.progress_value.get() + 1)
# Update the run summary
# self.run_summary.set('T:%(total)s P:%(pass)s F:%(fail)s E:%(error)s X:%(expected)s U:%(unexpected)s S:%(skip)s, ~%(remaining)s remaining' % {
# 'total': self.executor.total_count,
# 'pass': self.executor.result_count.get(TestMethod.STATUS_PASS, 0),
# 'fail': self.executor.result_count.get(TestMethod.STATUS_FAIL, 0),
# 'error': self.executor.result_count.get(TestMethod.STATUS_ERROR, 0),
# 'expected': self.executor.result_count.get(TestMethod.STATUS_EXPECTED_FAIL, 0),
# 'unexpected': self.executor.result_count.get(TestMethod.STATUS_UNEXPECTED_SUCCESS, 0),
# 'skip': self.executor.result_count.get(TestMethod.STATUS_SKIP, 0),
# 'remaining': remaining_time
# })
# If the test that just fininshed is the one (and only one)
# selected on the tree, update the display.
# current_tree = self.current_test_tree
# if len(current_tree.selection()) == 1:
# # One test selected.
# if current_tree.selection()[0] == test_path:
# # If the test that just finished running is the selected
# # test, force reset the selection, which will generate a
# # selection event, forcing a refresh of the result page.
# current_tree.selection_set(current_tree.selection())
# else:
# # No or Multiple tests selected
# self.name.set('')
# self.test_status.set('')
# self.duration.set('')
# self.description.delete('1.0', END)
# self._hide_test_output()
# self._hide_test_errors()
def on_executorSuiteEnd(self, event, module, error=None):
"The test suite finished running."
# Display the final results
self.run_status[module].showMessage('Finished.')
# if error:
# TestErrorsDialog(self.root, error)
# if self.executor[module].any_failed:
# dialog = tkMessageBox.showerror
# else:
# dialog = tkMessageBox.showinfo
# message = ', '.join(
# '%d %s' % (count, TestMethod.STATUS_LABELS[state])
# for state, count in sorted(self.executor[module].result_count.items()))
# dialog(message=message or 'No tests were ran')
# Reset the running summary.
# self.run_summary.set('T:%(total)s P:%(pass)s F:%(fail)s E:%(error)s X:%(expected)s U:%(unexpected)s S:%(skip)s' % {
# 'total': self.executor.total_count,
# 'pass': self.executor.result_count.get(TestMethod.STATUS_PASS, 0),
# 'fail': self.executor.result_count.get(TestMethod.STATUS_FAIL, 0),
# 'error': self.executor.result_count.get(TestMethod.STATUS_ERROR, 0),
# 'expected': self.executor.result_count.get(TestMethod.STATUS_EXPECTED_FAIL, 0),
# 'unexpected': self.executor.result_count.get(TestMethod.STATUS_UNEXPECTED_SUCCESS, 0),
# 'skip': self.executor.result_count.get(TestMethod.STATUS_SKIP, 0),
# })
# Reset the buttons
self.reset_button_states_on_end()
# Drop the reference to the executor
self.executor[module] = None
def on_executorSuiteError(self, event, module, error):
"An error occurred running the test suite."
# Display the error in a dialog
self.run_status[module].showMessage('Error running test suite.')
# FailedTestDialog(self.root, error)
# Drop the reference to the executor
self.executor[module] = None
def reset_button_states_on_end(self):
"A test run has ended and we should enable or disable buttons as appropriate."
is_stoped = True
for executor in self.executor.values():
if executor and executor.is_running:
is_stoped = False
if is_stoped:
self.stop_button.setDisabled(True)
self.run_all_button.setDisabled(False)
self.set_selected_button_state()
def set_selected_button_state(self):
is_running = False
for executor in self.executor.values():
if executor and executor.is_running:
is_running = True
is_selected = False
for table in self.test_table.values():
if table.selectedItems():
is_selected = True
if is_running:
self.run_selected_button.setDisabled(True)
elif is_selected:
self.run_selected_button.setDisabled(False)
else:
self.run_selected_button.setDisabled(True)
######################################################
# GUI utility methods
######################################################
def run(self, module, active=True, status=None, labels=None):
"""Run the test suite.
If active=True, only active tests will be run.
If status is provided, only tests whose most recent run
status matches the set provided will be executed.
If labels is provided, only tests with those labels will
be executed
"""
labels = labels if labels else self.test_list[module]
self.run_status[module].showMessage('Running...')
# self.run_summary.set('T:%s P:0 F:0 E:0 X:0 U:0 S:0' % count)
self.run_all_button.setDisabled(True)
self.run_selected_button.setDisabled(True)
self.stop_button.setDisabled(False)
# self.progress['maximum'] = count
# self.progress_value.set(0)
# Create the runner
self.executor[module] = Executor(self.project, module, len(labels), labels)
# Queue the first progress handling event
QTimer.singleShot(100, lambda: self.on_testProgress(self.executor[module]))
def stop(self):
"Stop the test suite."
for module, executor in self.executor.items():
if executor and executor.is_running:
self.run_status[module].showMessage('Stopping...')
executor.terminate()
executor = None
self.run_status[module].showMessage('Stopped.')
self.reset_button_states_on_end()
def _hide_test_output(self):
"Hide the test output panel on the test results page"
self.output_label.grid_remove()
self.output.grid_remove()
self.output_scrollbar.grid_remove()
self.details_frame.rowconfigure(3, weight=0)
def _show_test_output(self, content):
"Show the test output panel on the test results page"
self.output.delete('1.0', END)
self.output.insert('1.0', content)
self.output_label.grid()
self.output.grid()
self.output_scrollbar.grid()
self.details_frame.rowconfigure(3, weight=5)
def _hide_test_errors(self):
"Hide the test error panel on the test results page"
self.error_label.grid_remove()
self.error.grid_remove()
self.error_scrollbar.grid_remove()
def _show_test_errors(self, content):
"Show the test error panel on the test results page"
self.error.delete('1.0', END)
self.error.insert('1.0', content)
self.error_label.grid()
self.error.grid()
self.error_scrollbar.grid()

View file

View file

@ -0,0 +1,13 @@
'''
This is the main entry point for running unittest test suites.
'''
from cricket.main import main as cricket_main
from cricket.unittest.model import UnittestProject
def main():
cricket_main(UnittestProject)
if __name__ == "__main__":
main()

View file

@ -0,0 +1,60 @@
'''
The whole purpose of this module is to generate printed output.
It should be of the form:
'module.testcase.specifictest'
'module.testcase.specifictest2'
'module2.testcase.specifictest'
etc
Its primary API is the command-line, but it can
just as easily be called programmatically (see __main__)
'''
import unittest
def consume(iterable):
input = list(iterable)
while input:
item = input.pop(0)
try:
data = iter(item)
input = list(data) + input
except:
yield item
class PyTestDiscoverer:
def __init__(self):
self.collected_tests = []
def __str__(self):
'''
Builds the dotted namespace expected by cricket
'''
resultstr = '\n'.join(self.collected_tests)
return resultstr.strip()
def collect_tests(self):
'''
Collect a list of potentially runnable tests
'''
loader = unittest.TestLoader()
suite = loader.discover('.')
flatresults = list(consume(suite))
named = [r.id() for r in flatresults]
self.collected_tests = named
if __name__ == '__main__':
PTD = PyTestDiscoverer()
PTD.collect_tests()
print(str(PTD))

View file

@ -0,0 +1,90 @@
'''
This is a thing which, when run, produces a stream
of well-formed test result outputs. Its processing is
initiated by the top-level Executor class.
Its main API is the command line, but it's just as sensible to
call into it. See __main__ for usage
'''
import argparse
import unittest
try:
from coverage import coverage
except ImportError:
coverage = None
from cricket import pipes
class PyTestExecutor(object):
'''
This is a thing which, when run, produces a stream
of well-formed test result outputs. Its processing is
initiated by the top-level Executor class
'''
def __init__(self):
# Allows the executor to run a specified list of tests
self.specified_list = None
def run_only(self, specified_list):
self.specified_list = specified_list
def stream_suite(self, suite):
pipes.PipedTestRunner().run(suite)
def stream_results(self):
'''
1.) Discover all tests if necessary
2.) Otherwise fetch specific tests
3.) Execute-and-stream
'''
loader = unittest.TestLoader()
if not self.specified_list:
suite = loader.discover('.')
else:
# FIXME: TestCase stuck when sleep in test method
# for module in self.specified_list:
# suite = loader.loadTestsFromName(module)
# self.stream_suite(suite)
suite = loader.loadTestsFromNames(self.specified_list)
self.stream_suite(suite)
class PyTestCoverageExecutor(PyTestExecutor):
'''
A version of PyTestExecutor that gathers coverage data.
'''
def stream_suite(self, suite):
cov = coverage()
cov.start()
super(PyTestCoverageExecutor, self).stream_suite(suite)
cov.stop()
cov.save()
if __name__ == '__main__':
parser = argparse.ArgumentParser()
parser.add_argument("--coverage", help="Generate coverage data for the test run", action="store_true")
parser.add_argument(
'labels', nargs=argparse.REMAINDER,
help='Test labels to run.'
)
options = parser.parse_args()
if options.coverage:
PTE = PyTestCoverageExecutor()
else:
PTE = PyTestExecutor()
if options.labels:
PTE.run_only(options.labels)
PTE.stream_results()

View file

@ -0,0 +1,20 @@
import sys
from cricket.model import Project
class UnittestProject(Project):
def __init__(self, options=None):
super(UnittestProject, self).__init__()
def discover_commandline(self):
"Command line: Discover all available tests in a project."
return [sys.executable, '-m', 'cricket.unittest.discoverer']
def execute_commandline(self, labels):
"Return the command line to execute the specified test labels"
args = [sys.executable, '-m', 'cricket.unittest.executor']
if self.coverage:
args.append('--coverage')
return args + labels

1173
cricket/cricket/view.py Normal file

File diff suppressed because it is too large Load diff

153
cricket/docs/Makefile Normal file
View file

@ -0,0 +1,153 @@
# Makefile for Sphinx documentation
#
# You can set these variables from the command line.
SPHINXOPTS =
SPHINXBUILD = sphinx-build
PAPER =
BUILDDIR = _build
# Internal variables.
PAPEROPT_a4 = -D latex_paper_size=a4
PAPEROPT_letter = -D latex_paper_size=letter
ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
# the i18n builder cannot share the environment and doctrees with the others
I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext
help:
@echo "Please use \`make <target>' where <target> is one of"
@echo " html to make standalone HTML files"
@echo " dirhtml to make HTML files named index.html in directories"
@echo " singlehtml to make a single large HTML file"
@echo " pickle to make pickle files"
@echo " json to make JSON files"
@echo " htmlhelp to make HTML files and a HTML help project"
@echo " qthelp to make HTML files and a qthelp project"
@echo " devhelp to make HTML files and a Devhelp project"
@echo " epub to make an epub"
@echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
@echo " latexpdf to make LaTeX files and run them through pdflatex"
@echo " text to make text files"
@echo " man to make manual pages"
@echo " texinfo to make Texinfo files"
@echo " info to make Texinfo files and run them through makeinfo"
@echo " gettext to make PO message catalogs"
@echo " changes to make an overview of all changed/added/deprecated items"
@echo " linkcheck to check all external links for integrity"
@echo " doctest to run all doctests embedded in the documentation (if enabled)"
clean:
-rm -rf $(BUILDDIR)/*
html:
$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
@echo
@echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
dirhtml:
$(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
@echo
@echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
singlehtml:
$(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
@echo
@echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
pickle:
$(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
@echo
@echo "Build finished; now you can process the pickle files."
json:
$(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
@echo
@echo "Build finished; now you can process the JSON files."
htmlhelp:
$(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
@echo
@echo "Build finished; now you can run HTML Help Workshop with the" \
".hhp project file in $(BUILDDIR)/htmlhelp."
qthelp:
$(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
@echo
@echo "Build finished; now you can run "qcollectiongenerator" with the" \
".qhcp project file in $(BUILDDIR)/qthelp, like this:"
@echo "# qcollectiongenerator $(BUILDDIR)/qthelp/Django-Cricket.qhcp"
@echo "To view the help file:"
@echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Django-Cricket.qhc"
devhelp:
$(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
@echo
@echo "Build finished."
@echo "To view the help file:"
@echo "# mkdir -p $$HOME/.local/share/devhelp/Django-Cricket"
@echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/Django-Cricket"
@echo "# devhelp"
epub:
$(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
@echo
@echo "Build finished. The epub file is in $(BUILDDIR)/epub."
latex:
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
@echo
@echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
@echo "Run \`make' in that directory to run these through (pdf)latex" \
"(use \`make latexpdf' here to do that automatically)."
latexpdf:
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
@echo "Running LaTeX files through pdflatex..."
$(MAKE) -C $(BUILDDIR)/latex all-pdf
@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
text:
$(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
@echo
@echo "Build finished. The text files are in $(BUILDDIR)/text."
man:
$(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
@echo
@echo "Build finished. The manual pages are in $(BUILDDIR)/man."
texinfo:
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
@echo
@echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo."
@echo "Run \`make' in that directory to run these through makeinfo" \
"(use \`make info' here to do that automatically)."
info:
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
@echo "Running Texinfo files through makeinfo..."
make -C $(BUILDDIR)/texinfo info
@echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo."
gettext:
$(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale
@echo
@echo "Build finished. The message catalogs are in $(BUILDDIR)/locale."
changes:
$(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
@echo
@echo "The overview file is in $(BUILDDIR)/changes."
linkcheck:
$(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
@echo
@echo "Link check complete; look for any errors in the above output " \
"or in $(BUILDDIR)/linkcheck/output.txt."
doctest:
$(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
@echo "Testing of doctests in the sources finished, look at the " \
"results in $(BUILDDIR)/doctest/output.txt."

246
cricket/docs/conf.py Normal file
View file

@ -0,0 +1,246 @@
# -*- coding: utf-8 -*-
#
# Cricket documentation build configuration file, created by
# sphinx-quickstart on Sat Feb 9 10:44:39 2013.
#
# This file is execfile()d with the current directory set to its containing dir.
#
# Note that not all possible configuration values are present in this
# autogenerated file.
#
# All configuration values have a default; values that are commented out
# serve to show the default.
import sys, os
sys.path.insert(1, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
import cricket
# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
#sys.path.insert(0, os.path.abspath('.'))
# -- General configuration -----------------------------------------------------
# If your documentation needs a minimal Sphinx version, state it here.
#needs_sphinx = '1.0'
# Add any Sphinx extension module names here, as strings. They can be extensions
# coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
extensions = []
# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']
# The suffix of source filenames.
source_suffix = '.rst'
# The encoding of source files.
#source_encoding = 'utf-8-sig'
# The master toctree document.
master_doc = 'index'
# General information about the project.
project = u'Cricket'
copyright = u'2013, Russell Keith-Magee'
# The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the
# built documents.
#
# The short X.Y version.
version = '.'.join(str(n) for n in cricket.NUM_VERSION[:2])
# The full version, including alpha/beta/rc tags.
release = cricket.VERSION
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
#language = None
# There are two options for replacing |today|: either, you set today to some
# non-false value, then it is used:
#today = ''
# Else, today_fmt is used as the format for a strftime call.
#today_fmt = '%B %d, %Y'
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
exclude_patterns = ['_build']
# The reST default role (used for this markup: `text`) to use for all documents.
#default_role = None
# If true, '()' will be appended to :func: etc. cross-reference text.
#add_function_parentheses = True
# If true, the current module name will be prepended to all description
# unit titles (such as .. function::).
#add_module_names = True
# If true, sectionauthor and moduleauthor directives will be shown in the
# output. They are ignored by default.
#show_authors = False
# The name of the Pygments (syntax highlighting) style to use.
pygments_style = 'sphinx'
# A list of ignored prefixes for module index sorting.
#modindex_common_prefix = []
# -- Options for HTML output ---------------------------------------------------
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
html_theme = 'default'
# Theme options are theme-specific and customize the look and feel of a theme
# further. For a list of options available for each theme, see the
# documentation.
#html_theme_options = {}
# Add any paths that contain custom themes here, relative to this directory.
#html_theme_path = []
# The name for this set of Sphinx documents. If None, it defaults to
# "<project> v<release> documentation".
#html_title = None
# A shorter title for the navigation bar. Default is the same as html_title.
#html_short_title = None
# The name of an image file (relative to this directory) to place at the top
# of the sidebar.
#html_logo = None
# The name of an image file (within the static path) to use as favicon of the
# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
# pixels large.
#html_favicon = None
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ['_static']
# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
# using the given strftime format.
#html_last_updated_fmt = '%b %d, %Y'
# If true, SmartyPants will be used to convert quotes and dashes to
# typographically correct entities.
#html_use_smartypants = True
# Custom sidebar templates, maps document names to template names.
#html_sidebars = {}
# Additional templates that should be rendered to pages, maps page names to
# template names.
#html_additional_pages = {}
# If false, no module index is generated.
#html_domain_indices = True
# If false, no index is generated.
#html_use_index = True
# If true, the index is split into individual pages for each letter.
#html_split_index = False
# If true, links to the reST sources are added to the pages.
#html_show_sourcelink = True
# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
#html_show_sphinx = True
# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
#html_show_copyright = True
# If true, an OpenSearch description file will be output, and all pages will
# contain a <link> tag referring to it. The value of this option must be the
# base URL from which the finished HTML is served.
#html_use_opensearch = ''
# This is the file name suffix for HTML files (e.g. ".xhtml").
#html_file_suffix = None
# Output file base name for HTML help builder.
htmlhelp_basename = 'Cricketdoc'
# -- Options for LaTeX output --------------------------------------------------
latex_elements = {
# The paper size ('letterpaper' or 'a4paper').
#'papersize': 'letterpaper',
# The font size ('10pt', '11pt' or '12pt').
#'pointsize': '10pt',
# Additional stuff for the LaTeX preamble.
#'preamble': '',
}
# Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title, author, documentclass [howto/manual]).
latex_documents = [
('index', 'Cricket.tex', u'Cricket Documentation',
u'Russell Keith-Magee', 'manual'),
]
# The name of an image file (relative to this directory) to place at the top of
# the title page.
#latex_logo = None
# For "manual" documents, if this is true, then toplevel headings are parts,
# not chapters.
#latex_use_parts = False
# If true, show page references after internal links.
#latex_show_pagerefs = False
# If true, show URL addresses after external links.
#latex_show_urls = False
# Documents to append as an appendix to all manuals.
#latex_appendices = []
# If false, no module index is generated.
#latex_domain_indices = True
# -- Options for manual page output --------------------------------------------
# One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section).
man_pages = [
('index', 'cricket', u'Cricket Documentation',
[u'Russell Keith-Magee'], 1)
]
# If true, show URL addresses after external links.
#man_show_urls = False
# -- Options for Texinfo output ------------------------------------------------
# Grouping the document tree into Texinfo files. List of tuples
# (source start file, target name, title, author,
# dir menu entry, description, category)
texinfo_documents = [
('index', 'Cricket', u'Cricket Documentation',
u'Russell Keith-Magee', 'Cricket', 'A graphical tool to assist running test suites.',
'Miscellaneous'),
]
# Documents to append as an appendix to all manuals.
#texinfo_appendices = []
# If false, no module index is generated.
#texinfo_domain_indices = True
# How to display URL addresses: 'footnote', 'no', or 'inline'.
#texinfo_show_urls = 'footnote'

95
cricket/docs/index.rst Normal file
View file

@ -0,0 +1,95 @@
Cricket
=======
Cricket is part of the `BeeWare suite`_. The project website is `http://pybee.org/cricket`_.
Cricket is a graphical tool that helps projects with large test suites **identify failures without waiting** for all your tests to finish.
Normal unittest test runners dump all output to the console, and provide very
little detail while the suite is running. As a result:
* You can't start looking at failures until the test suite has completed running,
* It isn't a very accessible format for identifying patterns in test failures,
* It can be hard (or cumbersome) to re-run any tests that have failed.
Why the name ``cricket``? `Test Cricket`_ is the most prestigious version of
the game of cricket. Games last for up to 5 days... just like running some
test suites. The usual approach for making cricket watchable is a generous
dose of beer; in programming, `Balmer Peak`_ limits come into effect, so
something else is required...
.. _BeeWare suite: http://pybee.org/
.. _http://pybee.org/cricket: http://pybee.org/cricket
.. _Test Cricket: http://en.wikipedia.org/wiki/Test_cricket
.. _Balmer Peak: http://xkcd.com/323/
Quickstart
----------
At present, Cricket has support for:
* Pre-Django 1.6 project test suites,
* Django 1.6+ project test suites using unittest2-style discovery, and
* unittest project test suites.
In your Django project, install cricket, and then run it::
$ pip install cricket
$ cricket-django
``cricket-django`` will also work in Django's own tests directory -- i.e., you
can use ``cricket-django`` to run Django's own test suite (for Django 1.6 or
later).
In a unittest project, install cricket, and then run it::
$ pip install cricket
$ cricket-unittest
This will pop up a GUI window. Hit "Run all", and watch your test suite
execute.
Problems under Ubuntu
~~~~~~~~~~~~~~~~~~~~~
Ubuntu's packaging of Python omits the ``idlelib`` library from it's base
package. If you're using Python 2.7 on Ubuntu 13.04, you can install
``idlelib`` by running::
$ sudo apt-get install idle-python2.7
For other versions of Python and Ubuntu, you'll need to adjust this as
appropriate.
Problems under Windows
~~~~~~~~~~~~~~~~~~~~~~
If you're running Cricket in a virtualenv, you'll need to set an
environment variable so that Cricket can find the TCL graphics library::
$ set TCL_LIBRARY=c:\Python27\tcl\tcl8.5
You'll need to adjust the exact path to reflect your local Python install.
You may find it helpful to put this line in the ``activate.bat`` script
for your virtual environment so that it is automatically set whenever the
virtualenv is activated.
Contents:
.. toctree::
:maxdepth: 2
:glob:
internals/contributing
internals/backends
internals/roadmap
releases
Indices and tables
==================
* :ref:`genindex`
* :ref:`modindex`
* :ref:`search`

View file

@ -0,0 +1,59 @@
Writing a Cricket backend
=========================
Why you might want to do this
-----------------------------
A number of test execution environments are not necessarily supported. This
includes pytest, GUI test tools, or even custom stuff. The sky is the limit.
Or, you might just want to understand the architecture.
Helicopter Overview of the Architecture
---------------------------------------
The main directory consists of events, executor, model, pipes, view and
widgets. The ones which are the concern of the GUI are events, view and
widgets. The ones which concern the backend are model, executor and pipes.
The one which you need to really understand is pipes, but that's not the best
starting point.
The best starting point is either the unittest or django subdirectory. The GUI
is first built by the relevant backend, and the backend provides standard
callbacks for the GUI.
Layout of a Backend System
--------------------------
A Cricket backend should contain the following 4 files:
* ``__main__.py`` - The entry point for the user.
* ``discoverer.py`` - Generates the list of available tests
* ``executor.py`` - Wraps execution of test functions
* ``model.py`` - Defines the method for executing the discoverer and executor
Requirements of a backend
-------------------------
Both the Django and the unittest backend take advantage of the unittest module
to create and execute test suites. The core file pipes.PipedTestRunner will
run unittest-style tests and provide the appropriately well-formed output
expected by the GUI. However, it is a valid choice for the executor to produce
output of the same for onto stdout itself. The only hard requirement is that
the executor function stream onto stdout a series of well-formed outputs. To
understand the full detail, examine pipes.py.
The Django and the unittest mechanisms for executing tests are different. The
Django backend is a thin hook into the Django test execution machinery. The
unittest backend is a slightly less thin hook into the unittest modele. The
key requirements of the executor backend are:
1. The ability to stream well-formed output to stdout
2. The ability to limit/target test execution according to supplied labels
At the time of writing, sys.argv[1:] will be the list of dotted-namespaced
names of tests which should be run. More complex command-line calls are simply
not supported at this stage. A very useful task would be to do some more
thinking on this interface.

View file

@ -0,0 +1,36 @@
Contributing to Cricket
=======================
If you experience problems with cricket, `log them on GitHub`_. If you want to contribute code, please `fork the code`_ and `submit a pull request`_.
.. _log them on Github: https://github.com/pybee/cricket/issues
.. _fork the code: https://github.com/pybee/cricket
.. _submit a pull request: https://github.com/pybee/cricket/pulls
Setting up your development environment
---------------------------------------
The recommended way of setting up your development envrionment for ``cricket``
is to install a virtual environment, install the required dependencies and
start coding. Assuming that you are using ``virtualenvwrapper``, you only have
to run::
$ git clone git@github.com:pybee/cricket.git
$ cd cricket
$ mkvirtualenv cricket
Cricket uses ``unittest`` (or ``unittest2`` for Python < 2.7) for its own test
suite as well as additional helper modules for testing. To install all the
requirements for cricket, you have to run the following commands within your
virtual environment::
$ pip install -e .
$ pip install -r requirements_dev.txt
In case you are running a python version ``< 2.7`` please use the
``requirements_dev_python2.7.txt`` instead because ``unittest2`` is not part
of the standard library for these version.
Now you are ready to start hacking! Have fun!

View file

@ -0,0 +1,32 @@
Cricket Roadmap
===============
Cricket is a new project - we have lots of things that we'd like to do. If
you'd like to contribute, providing a patch for one of these features:
* Use a standard protocol (e.g., subunit) for communicating between the
executor and the GUI
* Port to Python 3
* Add a pytest backend
* Add a nose backend
* Add a selenium backend, including possibly adding the ability to collect
and store screenshots at the end of each test
* Improve GUI interface, including:
- keyboard shortcuts
- search
* Integrate with coverage, and use tkreadonly to display coverage stats
overlaid on actual code
* Add historical tracking of test results
* Integrate with testr
* Add continuous background testing based on file modifications

190
cricket/docs/make.bat Normal file
View file

@ -0,0 +1,190 @@
@ECHO OFF
REM Command file for Sphinx documentation
if "%SPHINXBUILD%" == "" (
set SPHINXBUILD=sphinx-build
)
set BUILDDIR=_build
set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% .
set I18NSPHINXOPTS=%SPHINXOPTS% .
if NOT "%PAPER%" == "" (
set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS%
set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS%
)
if "%1" == "" goto help
if "%1" == "help" (
:help
echo.Please use `make ^<target^>` where ^<target^> is one of
echo. html to make standalone HTML files
echo. dirhtml to make HTML files named index.html in directories
echo. singlehtml to make a single large HTML file
echo. pickle to make pickle files
echo. json to make JSON files
echo. htmlhelp to make HTML files and a HTML help project
echo. qthelp to make HTML files and a qthelp project
echo. devhelp to make HTML files and a Devhelp project
echo. epub to make an epub
echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter
echo. text to make text files
echo. man to make manual pages
echo. texinfo to make Texinfo files
echo. gettext to make PO message catalogs
echo. changes to make an overview over all changed/added/deprecated items
echo. linkcheck to check all external links for integrity
echo. doctest to run all doctests embedded in the documentation if enabled
goto end
)
if "%1" == "clean" (
for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i
del /q /s %BUILDDIR%\*
goto end
)
if "%1" == "html" (
%SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The HTML pages are in %BUILDDIR%/html.
goto end
)
if "%1" == "dirhtml" (
%SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml.
goto end
)
if "%1" == "singlehtml" (
%SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml.
goto end
)
if "%1" == "pickle" (
%SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle
if errorlevel 1 exit /b 1
echo.
echo.Build finished; now you can process the pickle files.
goto end
)
if "%1" == "json" (
%SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json
if errorlevel 1 exit /b 1
echo.
echo.Build finished; now you can process the JSON files.
goto end
)
if "%1" == "htmlhelp" (
%SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp
if errorlevel 1 exit /b 1
echo.
echo.Build finished; now you can run HTML Help Workshop with the ^
.hhp project file in %BUILDDIR%/htmlhelp.
goto end
)
if "%1" == "qthelp" (
%SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp
if errorlevel 1 exit /b 1
echo.
echo.Build finished; now you can run "qcollectiongenerator" with the ^
.qhcp project file in %BUILDDIR%/qthelp, like this:
echo.^> qcollectiongenerator %BUILDDIR%\qthelp\Django-Cricket.qhcp
echo.To view the help file:
echo.^> assistant -collectionFile %BUILDDIR%\qthelp\Django-Cricket.ghc
goto end
)
if "%1" == "devhelp" (
%SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp
if errorlevel 1 exit /b 1
echo.
echo.Build finished.
goto end
)
if "%1" == "epub" (
%SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The epub file is in %BUILDDIR%/epub.
goto end
)
if "%1" == "latex" (
%SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
if errorlevel 1 exit /b 1
echo.
echo.Build finished; the LaTeX files are in %BUILDDIR%/latex.
goto end
)
if "%1" == "text" (
%SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The text files are in %BUILDDIR%/text.
goto end
)
if "%1" == "man" (
%SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The manual pages are in %BUILDDIR%/man.
goto end
)
if "%1" == "texinfo" (
%SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo.
goto end
)
if "%1" == "gettext" (
%SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The message catalogs are in %BUILDDIR%/locale.
goto end
)
if "%1" == "changes" (
%SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes
if errorlevel 1 exit /b 1
echo.
echo.The overview file is in %BUILDDIR%/changes.
goto end
)
if "%1" == "linkcheck" (
%SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck
if errorlevel 1 exit /b 1
echo.
echo.Link check complete; look for any errors in the above output ^
or in %BUILDDIR%/linkcheck/output.txt.
goto end
)
if "%1" == "doctest" (
%SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest
if errorlevel 1 exit /b 1
echo.
echo.Testing of doctests in the sources finished, look at the ^
results in %BUILDDIR%/doctest/output.txt.
goto end
)
:end

62
cricket/docs/releases.rst Normal file
View file

@ -0,0 +1,62 @@
Release History
===============
0.2.4 - In development
----------------------
0.2.3 - September 26, 2013
--------------------------
* Added ability to generate coverage
* Integration with Duvet
0.2.2 - September 17, 2013
--------------------------
* Corrected a problem with starting unittest2 projects.
* Corrected a error raised when unittest2 was manually installed in
PYTHONPATH.
0.2.1 - September 14, 2013
--------------------------
* Fixed selection of test modules in unittest2-style suite discovery.
* Added ability to run Django's own test suite.
0.2.0 - August 31, 2013
-----------------------
* Relaunch as a PyBee project.
* Added test and help menus.
0.1.3 - July 29, 2013
---------------------
* Corrected problem with Django test executor, masked by pyc caching.
* Improved collection of errors raised during test startup.
0.1.2 - July 26, 2013
---------------------
* Improved handling of button state during test execution.
0.1.1 - July 9, 2013
--------------------
Release incorporating updates from the PyCon AU 2013 sprints, including:
* Unittest2 support
* Improved handling of errors raised during test execution
* Improved reporting of errors caused on Ubuntu
0.1.0 - June 21, 2013
---------------------
Initial public release, at PyCon AU 2013

View file

@ -0,0 +1 @@
mock==1.0.1

8
cricket/setup.cfg Normal file
View file

@ -0,0 +1,8 @@
[bdist_wheel]
universal = 1
[egg_info]
tag_build =
tag_date = 0
tag_svn_revision = 0

52
cricket/setup.py Normal file
View file

@ -0,0 +1,52 @@
#/usr/bin/env python
import io
import re
from setuptools import setup, find_packages
with io.open('./cricket/__init__.py', encoding='utf8') as version_file:
version_match = re.search(r"^__version__ = ['\"]([^'\"]*)['\"]", version_file.read(), re.M)
if version_match:
version = version_match.group(1)
else:
raise RuntimeError("Unable to find version string.")
with io.open('README.rst', encoding='utf8') as readme:
long_description = readme.read()
setup(
name='cricket',
version=version,
description='A graphical tool to assist running test suites.',
long_description=long_description,
author='Russell Keith-Magee',
author_email='russell@keith-magee.com',
url='http://pybee.org/cricket',
packages=find_packages(exclude='tests'),
install_requires=['tkreadonly'],
scripts=[],
entry_points={
'console_scripts': [
'cricket-django = cricket.django.__main__:main',
'cricket-unittest = cricket.unittest.__main__:main',
]
},
license='New BSD',
classifiers=[
'Development Status :: 4 - Beta',
'Intended Audience :: Developers',
'License :: OSI Approved :: BSD License',
'Operating System :: OS Independent',
'Programming Language :: Python :: 2',
'Programming Language :: Python :: 2.7',
'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3.3',
'Programming Language :: Python :: 3.4',
'Topic :: Software Development',
'Topic :: Software Development :: Testing',
'Topic :: Utilities',
],
test_suite='tests'
)

View file

View file

@ -0,0 +1,851 @@
from cricket.compat import unittest
from cricket.model import Project, TestModule, TestCase
class TestProject(unittest.TestCase):
"""Tests for the process of converting the output of the Discoverer
into an internal tree.
"""
def _full_tree(self, node):
"Internal method generating a simple tree version of a project node"
if isinstance(node, TestCase):
return (type(node), node.keys())
else:
return dict(
((type(sub_tree), sub_node), self._full_tree(sub_tree))
for sub_node, sub_tree in node.items()
)
def test_no_tests(self):
"If there are no tests, an empty tree is generated"
project = Project()
project.refresh(test_list=[])
self.assertEqual(project.errors, [])
self.assertEqual(sorted(self._full_tree(project)), sorted({}))
def test_with_tests(self):
"If tests are found, the right tree is created"
project = Project()
project.refresh([
'tests.FunkyTestCase.test_something_unnecessary',
'more_tests.FunkyTestCase.test_this_does_make_sense',
'more_tests.FunkyTestCase.test_this_doesnt_make_sense',
'more_tests.JankyTestCase.test_things',
'deep_tests.package.DeepTestCase.test_doo_hickey',
])
self.assertEqual(project.errors, [])
self.assertEqual(sorted(self._full_tree(project)), sorted({
(TestModule, 'tests'): {
(TestCase, 'FunkyTestCase'): [
'test_something_unnecessary'
]
},
(TestModule, 'more_tests'): {
(TestCase, 'FunkyTestCase'): [
'test_this_doesnt_make_sense',
'test_this_doesnt_make_sense'
],
(TestCase, 'JankyTestCase'): [
'test_things'
]
},
(TestModule, 'deep_tests'): {
(TestModule, 'package'): {
(TestCase, 'DeepTestCase'): [
'test_doo_hickey'
]
}
}
}))
def test_with_tests_and_errors(self):
"If tests *and* errors are found, the tree is still created."
project = Project()
project.refresh([
'tests.FunkyTestCase.test_something_unnecessary',
],
errors=[
'ERROR: you broke it, fool!',
]
)
self.assertEqual(project.errors, [
'ERROR: you broke it, fool!',
])
self.assertEqual(sorted(self._full_tree(project)), sorted({
(TestModule, 'tests'): {
(TestCase, 'FunkyTestCase'): [
'test_something_unnecessary'
]
}
}))
class FindLabelTests(unittest.TestCase):
"Check that naming tests by labels reduces to the right runtime list."
def setUp(self):
super(FindLabelTests, self).setUp()
self.project = Project()
self.project.refresh([
'app1.TestCase.test_method',
'app2.TestCase1.test_method',
'app2.TestCase2.test_method1',
'app2.TestCase2.test_method2',
'app3.tests.TestCase.test_method',
'app4.tests1.TestCase.test_method',
'app4.tests2.TestCase1.test_method',
'app4.tests2.TestCase2.test_method1',
'app4.tests2.TestCase2.test_method2',
'app5.package.tests.TestCase.test_method',
'app6.package1.tests.TestCase.test_method',
'app6.package2.tests1.TestCase.test_method',
'app6.package2.tests2.TestCase1.test_method',
'app6.package2.tests2.TestCase2.test_method1',
'app6.package2.tests2.TestCase2.test_method2',
'app7.package.subpackage.tests.TestCase.test_method',
'app8.package1.subpackage.tests.TestCase.test_method',
'app8.package2.subpackage1.tests.TestCase.test_method',
'app8.package2.subpackage2.tests1.TestCase.test_method',
'app8.package2.subpackage2.tests2.TestCase1.test_method',
'app8.package2.subpackage2.tests2.TestCase2.test_method1',
'app8.package2.subpackage2.tests2.TestCase2.test_method2',
])
def test_single_test_project(self):
"If the project only contains a single test, the reduction is always the full suite"
self.project = Project()
self.project.refresh([
'app.package.tests.TestCase.test_method',
])
self.assertEquals(self.project.find_tests(labels=[
'app.package.tests.TestCase.test_method'
]),
(1, []))
self.assertEquals(self.project.find_tests(labels=[
'app.package.tests.TestCase'
]),
(1, []))
self.assertEquals(self.project.find_tests(labels=[
'app.package.tests'
]),
(1, []))
self.assertEquals(self.project.find_tests(labels=[
'app.package'
]),
(1, []))
self.assertEquals(self.project.find_tests(labels=[
'app'
]),
(1, []))
def test_all_tests(self):
"Without any qualifiers, all tests are run"
self.assertEquals(self.project.find_tests(), (22, []))
def test_method_selection(self):
"Explicitly named test method paths may be trimmed if they are unique"
self.assertEquals(self.project.find_tests(labels=[
'app1.TestCase.test_method'
]),
(1, ['app1']))
self.assertEquals(self.project.find_tests(labels=[
'app2.TestCase1.test_method'
]),
(1, ['app2.TestCase1']))
self.assertEquals(self.project.find_tests(labels=[
'app2.TestCase2.test_method1'
]),
(1, ['app2.TestCase2.test_method1']))
self.assertEquals(self.project.find_tests(labels=[
'app3.tests.TestCase.test_method'
]),
(1, ['app3']))
self.assertEquals(self.project.find_tests(labels=[
'app4.tests1.TestCase.test_method'
]),
(1, ['app4.tests1']))
self.assertEquals(self.project.find_tests(labels=[
'app4.tests2.TestCase1.test_method'
]),
(1, ['app4.tests2.TestCase1']))
self.assertEquals(self.project.find_tests(labels=[
'app4.tests2.TestCase2.test_method1'
]),
(1, ['app4.tests2.TestCase2.test_method1']))
self.assertEquals(self.project.find_tests(labels=[
'app5.package.tests.TestCase.test_method'
]),
(1, ['app5']))
self.assertEquals(self.project.find_tests(labels=[
'app6.package1.tests.TestCase.test_method'
]),
(1, ['app6.package1']))
self.assertEquals(self.project.find_tests(labels=[
'app6.package2.tests1.TestCase.test_method'
]),
(1, ['app6.package2.tests1']))
self.assertEquals(self.project.find_tests(labels=[
'app6.package2.tests2.TestCase1.test_method'
]),
(1, ['app6.package2.tests2.TestCase1']))
self.assertEquals(self.project.find_tests(labels=[
'app6.package2.tests2.TestCase2.test_method1'
]),
(1, ['app6.package2.tests2.TestCase2.test_method1']))
self.assertEquals(self.project.find_tests(labels=[
'app7.package.subpackage.tests.TestCase.test_method'
]),
(1, ['app7']))
self.assertEquals(self.project.find_tests(labels=[
'app8.package1.subpackage.tests.TestCase.test_method'
]),
(1, ['app8.package1']))
self.assertEquals(self.project.find_tests(labels=[
'app8.package2.subpackage1.tests.TestCase.test_method'
]),
(1, ['app8.package2.subpackage1']))
self.assertEquals(self.project.find_tests(labels=[
'app8.package2.subpackage2.tests1.TestCase.test_method'
]),
(1, ['app8.package2.subpackage2.tests1']))
self.assertEquals(self.project.find_tests(labels=[
'app8.package2.subpackage2.tests2.TestCase1.test_method'
]),
(1, ['app8.package2.subpackage2.tests2.TestCase1']))
self.assertEquals(self.project.find_tests(labels=[
'app8.package2.subpackage2.tests2.TestCase2.test_method1'
]),
(1, ['app8.package2.subpackage2.tests2.TestCase2.test_method1']))
def test_testcase_selection(self):
"Explicitly named test case paths may be trimmed if they are unique"
self.assertEquals(self.project.find_tests(labels=[
'app1.TestCase'
]),
(1, ['app1']))
self.assertEquals(self.project.find_tests(labels=[
'app2.TestCase1'
]),
(1, ['app2.TestCase1']))
self.assertEquals(self.project.find_tests(labels=[
'app2.TestCase2'
]),
(2, ['app2.TestCase2']))
self.assertEquals(self.project.find_tests(labels=[
'app3.tests.TestCase'
]),
(1, ['app3']))
self.assertEquals(self.project.find_tests(labels=[
'app4.tests1.TestCase'
]),
(1, ['app4.tests1']))
self.assertEquals(self.project.find_tests(labels=[
'app4.tests2.TestCase1'
]),
(1, ['app4.tests2.TestCase1']))
self.assertEquals(self.project.find_tests(labels=[
'app4.tests2.TestCase2'
]),
(2, ['app4.tests2.TestCase2']))
self.assertEquals(self.project.find_tests(labels=[
'app5.package.tests.TestCase'
]),
(1, ['app5']))
self.assertEquals(self.project.find_tests(labels=[
'app6.package1.tests.TestCase'
]),
(1, ['app6.package1']))
self.assertEquals(self.project.find_tests(labels=[
'app6.package2.tests1.TestCase'
]),
(1, ['app6.package2.tests1']))
self.assertEquals(self.project.find_tests(labels=[
'app6.package2.tests2.TestCase1'
]),
(1, ['app6.package2.tests2.TestCase1']))
self.assertEquals(self.project.find_tests(labels=[
'app6.package2.tests2.TestCase2'
]),
(2, ['app6.package2.tests2.TestCase2']))
self.assertEquals(self.project.find_tests(labels=[
'app7.package.subpackage.tests.TestCase'
]),
(1, ['app7']))
self.assertEquals(self.project.find_tests(labels=[
'app8.package1.subpackage.tests.TestCase'
]),
(1, ['app8.package1']))
self.assertEquals(self.project.find_tests(labels=[
'app8.package2.subpackage1.tests.TestCase'
]),
(1, ['app8.package2.subpackage1']))
self.assertEquals(self.project.find_tests(labels=[
'app8.package2.subpackage2.tests1.TestCase'
]),
(1, ['app8.package2.subpackage2.tests1']))
self.assertEquals(self.project.find_tests(labels=[
'app8.package2.subpackage2.tests2.TestCase1'
]),
(1, ['app8.package2.subpackage2.tests2.TestCase1']))
self.assertEquals(self.project.find_tests(labels=[
'app8.package2.subpackage2.tests2.TestCase2'
]),
(2, ['app8.package2.subpackage2.tests2.TestCase2']))
def test_testmodule_selection(self):
"Explicitly named test module paths may be trimmed if they are unique"
self.assertEquals(self.project.find_tests(labels=[
'app3.tests'
]),
(1, ['app3']))
self.assertEquals(self.project.find_tests(labels=[
'app4.tests1'
]),
(1, ['app4.tests1']))
self.assertEquals(self.project.find_tests(labels=[
'app4.tests2'
]),
(3, ['app4.tests2']))
self.assertEquals(self.project.find_tests(labels=[
'app5.package.tests'
]),
(1, ['app5']))
self.assertEquals(self.project.find_tests(labels=[
'app6.package1.tests'
]),
(1, ['app6.package1']))
self.assertEquals(self.project.find_tests(labels=[
'app6.package2.tests1'
]),
(1, ['app6.package2.tests1']))
self.assertEquals(self.project.find_tests(labels=[
'app6.package2.tests2'
]),
(3, ['app6.package2.tests2']))
self.assertEquals(self.project.find_tests(labels=[
'app7.package.subpackage.tests'
]),
(1, ['app7']))
self.assertEquals(self.project.find_tests(labels=[
'app8.package1.subpackage.tests'
]),
(1, ['app8.package1']))
self.assertEquals(self.project.find_tests(labels=[
'app8.package2.subpackage1.tests'
]),
(1, ['app8.package2.subpackage1']))
self.assertEquals(self.project.find_tests(labels=[
'app8.package2.subpackage2.tests1'
]),
(1, ['app8.package2.subpackage2.tests1']))
self.assertEquals(self.project.find_tests(labels=[
'app8.package2.subpackage2.tests2'
]),
(3, ['app8.package2.subpackage2.tests2']))
def test_package_selection(self):
"Explicitly named test package paths may be trimmed if they are unique"
self.assertEquals(self.project.find_tests(labels=[
'app5.package'
]),
(1, ['app5']))
self.assertEquals(self.project.find_tests(labels=[
'app6.package1'
]),
(1, ['app6.package1']))
self.assertEquals(self.project.find_tests(labels=[
'app6.package2'
]),
(4, ['app6.package2']))
self.assertEquals(self.project.find_tests(labels=[
'app7.package'
]),
(1, ['app7']))
self.assertEquals(self.project.find_tests(labels=[
'app8.package1'
]),
(1, ['app8.package1']))
self.assertEquals(self.project.find_tests(labels=[
'app8.package2'
]),
(5, ['app8.package2']))
def test_subpackage_selection(self):
"Explicitly named test subpackage paths may be trimmed if they are unique"
self.assertEquals(self.project.find_tests(labels=[
'app7.package.subpackage'
]),
(1, ['app7']))
self.assertEquals(self.project.find_tests(labels=[
'app8.package1.subpackage'
]),
(1, ['app8.package1']))
self.assertEquals(self.project.find_tests(labels=[
'app8.package2.subpackage1'
]),
(1, ['app8.package2.subpackage1']))
self.assertEquals(self.project.find_tests(labels=[
'app8.package2.subpackage2'
]),
(4, ['app8.package2.subpackage2']))
def test_app_selection(self):
"Explicitly named app paths return a count of all tests in the app"
self.assertEquals(self.project.find_tests(labels=[
'app1'
]),
(1, ['app1']))
self.assertEquals(self.project.find_tests(labels=[
'app2'
]),
(3, ['app2']))
self.assertEquals(self.project.find_tests(labels=[
'app3'
]),
(1, ['app3']))
self.assertEquals(self.project.find_tests(labels=[
'app4'
]),
(4, ['app4']))
self.assertEquals(self.project.find_tests(labels=[
'app5'
]),
(1, ['app5']))
self.assertEquals(self.project.find_tests(labels=[
'app6'
]),
(5, ['app6']))
self.assertEquals(self.project.find_tests(labels=[
'app7'
]),
(1, ['app7']))
self.assertEquals(self.project.find_tests(labels=[
'app8'
]),
(6, ['app8']))
def test_testcase_collapse(self):
"If all methods in a test are selected, path is trimmed to the case"
self.assertEquals(self.project.find_tests(labels=[
'app2.TestCase2.test_method1',
'app2.TestCase2.test_method2',
]),
(2, ['app2.TestCase2']))
self.assertEquals(self.project.find_tests(labels=[
'app4.tests2.TestCase2.test_method1',
'app4.tests2.TestCase2.test_method2',
]),
(2, ['app4.tests2.TestCase2']))
self.assertEquals(self.project.find_tests(labels=[
'app6.package2.tests2.TestCase2.test_method1',
'app6.package2.tests2.TestCase2.test_method2',
]),
(2, ['app6.package2.tests2.TestCase2']))
self.assertEquals(self.project.find_tests(labels=[
'app8.package2.subpackage2.tests2.TestCase2.test_method1',
'app8.package2.subpackage2.tests2.TestCase2.test_method2',
]),
(2, ['app8.package2.subpackage2.tests2.TestCase2']))
def test_testmethod_collapse(self):
"If all test cases in a test are selected, path is trimmed to the testmethod"
self.assertEquals(self.project.find_tests(labels=[
'app2.TestCase1.test_method',
'app2.TestCase2.test_method1',
'app2.TestCase2.test_method2',
]),
(3, ['app2']))
self.assertEquals(self.project.find_tests(labels=[
'app2.TestCase1.test_method',
'app2.TestCase2',
]),
(3, ['app2']))
self.assertEquals(self.project.find_tests(labels=[
'app2.TestCase1',
'app2.TestCase2',
]),
(3, ['app2']))
self.assertEquals(self.project.find_tests(labels=[
'app4.tests2.TestCase1.test_method',
'app4.tests2.TestCase2.test_method1',
'app4.tests2.TestCase2.test_method2',
]),
(3, ['app4.tests2']))
self.assertEquals(self.project.find_tests(labels=[
'app4.tests2.TestCase1.test_method',
'app4.tests2.TestCase2',
]),
(3, ['app4.tests2']))
self.assertEquals(self.project.find_tests(labels=[
'app4.tests2.TestCase1',
'app4.tests2.TestCase2',
]),
(3, ['app4.tests2']))
self.assertEquals(self.project.find_tests(labels=[
'app6.package2.tests2.TestCase1.test_method',
'app6.package2.tests2.TestCase2.test_method1',
'app6.package2.tests2.TestCase2.test_method2',
]),
(3, ['app6.package2.tests2']))
self.assertEquals(self.project.find_tests(labels=[
'app6.package2.tests2.TestCase1.test_method',
'app6.package2.tests2.TestCase2',
'app6.package2.tests2',
]),
(3, ['app6.package2.tests2']))
self.assertEquals(self.project.find_tests(labels=[
'app6.package2.tests2.TestCase1',
'app6.package2.tests2.TestCase2',
]),
(3, ['app6.package2.tests2']))
self.assertEquals(self.project.find_tests(labels=[
'app8.package2.subpackage2.tests2.TestCase1.test_method',
'app8.package2.subpackage2.tests2.TestCase2.test_method1',
'app8.package2.subpackage2.tests2.TestCase2.test_method2',
]),
(3, ['app8.package2.subpackage2.tests2']))
self.assertEquals(self.project.find_tests(labels=[
'app8.package2.subpackage2.tests2.TestCase1.test_method',
'app8.package2.subpackage2.tests2.TestCase2',
]),
(3, ['app8.package2.subpackage2.tests2']))
self.assertEquals(self.project.find_tests(labels=[
'app8.package2.subpackage2.tests2.TestCase1',
'app8.package2.subpackage2.tests2.TestCase2',
]),
(3, ['app8.package2.subpackage2.tests2']))
def test_package_collapse(self):
"If all test cases in a test pacakge are selected, path is trimmed to the testmethod"
self.assertEquals(self.project.find_tests(labels=[
'app6.package2.tests1.TestCase.test_method',
'app6.package2.tests2.TestCase1.test_method',
'app6.package2.tests2.TestCase2.test_method1',
'app6.package2.tests2.TestCase2.test_method2',
]),
(4, ['app6.package2']))
self.assertEquals(self.project.find_tests(labels=[
'app6.package2.tests1.TestCase.test_method',
'app6.package2.tests2.TestCase1.test_method',
'app6.package2.tests2.TestCase2',
]),
(4, ['app6.package2']))
self.assertEquals(self.project.find_tests(labels=[
'app6.package2.tests1.TestCase',
'app6.package2.tests2.TestCase1',
'app6.package2.tests2.TestCase2',
]),
(4, ['app6.package2']))
self.assertEquals(self.project.find_tests(labels=[
'app8.package2.subpackage1.tests.TestCase.test_method',
'app8.package2.subpackage2.tests1.TestCase.test_method',
'app8.package2.subpackage2.tests2.TestCase1.test_method',
'app8.package2.subpackage2.tests2.TestCase2.test_method1',
'app8.package2.subpackage2.tests2.TestCase2.test_method2',
]),
(5, ['app8.package2']))
self.assertEquals(self.project.find_tests(labels=[
'app8.package2.subpackage1.tests.TestCase.test_method',
'app8.package2.subpackage2.tests1.TestCase.test_method',
'app8.package2.subpackage2.tests2.TestCase1.test_method',
'app8.package2.subpackage2.tests2.TestCase2',
]),
(5, ['app8.package2']))
self.assertEquals(self.project.find_tests(labels=[
'app8.package2.subpackage1.tests.TestCase',
'app8.package2.subpackage2.tests1.TestCase',
'app8.package2.subpackage2.tests2.TestCase1',
'app8.package2.subpackage2.tests2.TestCase2',
]),
(5, ['app8.package2']))
def test_subpackage_collapse(self):
self.assertEquals(self.project.find_tests(labels=[
'app8.package2.subpackage2.tests1.TestCase.test_method',
'app8.package2.subpackage2.tests2.TestCase1.test_method',
'app8.package2.subpackage2.tests2.TestCase2.test_method1',
'app8.package2.subpackage2.tests2.TestCase2.test_method2',
]),
(4, ['app8.package2.subpackage2']))
self.assertEquals(self.project.find_tests(labels=[
'app8.package2.subpackage2.tests1.TestCase.test_method',
'app8.package2.subpackage2.tests2.TestCase1.test_method',
'app8.package2.subpackage2.tests2.TestCase2',
]),
(4, ['app8.package2.subpackage2']))
self.assertEquals(self.project.find_tests(labels=[
'app8.package2.subpackage2.tests1.TestCase',
'app8.package2.subpackage2.tests2.TestCase1',
'app8.package2.subpackage2.tests2.TestCase2',
]),
(4, ['app8.package2.subpackage2']))
def test_app_collapse(self):
self.assertEquals(self.project.find_tests(labels=[
'app2.TestCase1.test_method',
'app2.TestCase2.test_method1',
'app2.TestCase2.test_method2',
]),
(3, ['app2']))
self.assertEquals(self.project.find_tests(labels=[
'app2.TestCase1.test_method',
'app2.TestCase2',
]),
(3, ['app2']))
self.assertEquals(self.project.find_tests(labels=[
'app2.TestCase1',
'app2.TestCase2',
]),
(3, ['app2']))
self.assertEquals(self.project.find_tests(labels=[
'app4.tests1.TestCase.test_method',
'app4.tests2.TestCase1.test_method',
'app4.tests2.TestCase2.test_method1',
'app4.tests2.TestCase2.test_method2',
]),
(4, ['app4']))
self.assertEquals(self.project.find_tests(labels=[
'app4.tests1.TestCase.test_method',
'app4.tests2.TestCase1.test_method',
'app4.tests2.TestCase2',
]),
(4, ['app4']))
self.assertEquals(self.project.find_tests(labels=[
'app4.tests1.TestCase.test_method',
'app4.tests2.TestCase1',
'app4.tests2.TestCase2',
]),
(4, ['app4']))
self.assertEquals(self.project.find_tests(labels=[
'app4.tests1.TestCase.test_method',
'app4.tests2',
]),
(4, ['app4']))
self.assertEquals(self.project.find_tests(labels=[
'app4.tests1.TestCase',
'app4.tests2',
]),
(4, ['app4']))
self.assertEquals(self.project.find_tests(labels=[
'app4.tests1',
'app4.tests2',
]),
(4, ['app4']))
self.assertEquals(self.project.find_tests(labels=[
'app6.package1.tests.TestCase.test_method',
'app6.package2.tests1.TestCase.test_method',
'app6.package2.tests2.TestCase1.test_method',
'app6.package2.tests2.TestCase2.test_method1',
'app6.package2.tests2.TestCase2.test_method2',
]),
(5, ['app6']))
self.assertEquals(self.project.find_tests(labels=[
'app6.package1.tests.TestCase.test_method',
'app6.package2.tests1.TestCase.test_method',
'app6.package2.tests2.TestCase1.test_method',
'app6.package2.tests2.TestCase2',
]),
(5, ['app6']))
self.assertEquals(self.project.find_tests(labels=[
'app6.package1.tests.TestCase.test_method',
'app6.package2.tests1.TestCase.test_method',
'app6.package2.tests2.TestCase1',
'app6.package2.tests2.TestCase2',
]),
(5, ['app6']))
self.assertEquals(self.project.find_tests(labels=[
'app6.package1.tests.TestCase.test_method',
'app6.package2.tests1.TestCase.test_method',
'app6.package2.tests2',
]),
(5, ['app6']))
self.assertEquals(self.project.find_tests(labels=[
'app6.package1.tests.TestCase.test_method',
'app6.package2.tests1.TestCase.test_method',
'app6.package2.tests2.TestCase1',
'app6.package2.tests2.TestCase2',
]),
(5, ['app6']))
self.assertEquals(self.project.find_tests(labels=[
'app6.package1.tests.TestCase.test_method',
'app6.package2.tests1.TestCase.test_method',
'app6.package2.tests2',
]),
(5, ['app6']))
self.assertEquals(self.project.find_tests(labels=[
'app6.package1.tests.TestCase.test_method',
'app6.package2.tests1.TestCase',
'app6.package2.tests2',
]),
(5, ['app6']))
self.assertEquals(self.project.find_tests(labels=[
'app6.package1.tests.TestCase.test_method',
'app6.package2.tests1',
'app6.package2.tests2',
]),
(5, ['app6']))
self.assertEquals(self.project.find_tests(labels=[
'app6.package1.tests.TestCase.test_method',
'app6.package2',
]),
(5, ['app6']))
self.assertEquals(self.project.find_tests(labels=[
'app6.package1.tests.TestCase',
'app6.package2',
]),
(5, ['app6']))
self.assertEquals(self.project.find_tests(labels=[
'app6.package1.tests',
'app6.package2',
]),
(5, ['app6']))
self.assertEquals(self.project.find_tests(labels=[
'app6.package1',
'app6.package2',
]),
(5, ['app6']))
self.assertEquals(self.project.find_tests(labels=[
'app8.package1.subpackage.tests.TestCase.test_method',
'app8.package2.subpackage1.tests.TestCase.test_method',
'app8.package2.subpackage2.tests1.TestCase.test_method',
'app8.package2.subpackage2.tests2.TestCase1.test_method',
'app8.package2.subpackage2.tests2.TestCase2.test_method1',
'app8.package2.subpackage2.tests2.TestCase2.test_method2',
]),
(6, ['app8']))
self.assertEquals(self.project.find_tests(labels=[
'app8.package1.subpackage.tests.TestCase.test_method',
'app8.package2.subpackage1.tests.TestCase.test_method',
'app8.package2.subpackage2.tests1.TestCase.test_method',
'app8.package2.subpackage2.tests2.TestCase1.test_method',
'app8.package2.subpackage2.tests2.TestCase2',
]),
(6, ['app8']))
self.assertEquals(self.project.find_tests(labels=[
'app8.package1.subpackage.tests.TestCase.test_method',
'app8.package2.subpackage1.tests.TestCase.test_method',
'app8.package2.subpackage2.tests1.TestCase.test_method',
'app8.package2.subpackage2.tests2.TestCase1',
'app8.package2.subpackage2.tests2.TestCase2',
]),
(6, ['app8']))
self.assertEquals(self.project.find_tests(labels=[
'app8.package1.subpackage.tests.TestCase.test_method',
'app8.package2.subpackage1.tests.TestCase.test_method',
'app8.package2.subpackage2.tests1.TestCase.test_method',
'app8.package2.subpackage2.tests2',
]),
(6, ['app8']))
self.assertEquals(self.project.find_tests(labels=[
'app8.package1.subpackage.tests.TestCase.test_method',
'app8.package2.subpackage1.tests.TestCase.test_method',
'app8.package2.subpackage2.tests1.TestCase',
'app8.package2.subpackage2.tests2',
]),
(6, ['app8']))
self.assertEquals(self.project.find_tests(labels=[
'app8.package1.subpackage.tests.TestCase.test_method',
'app8.package2.subpackage1.tests.TestCase.test_method',
'app8.package2.subpackage2.tests1',
'app8.package2.subpackage2.tests2',
]),
(6, ['app8']))
self.assertEquals(self.project.find_tests(labels=[
'app8.package1.subpackage.tests.TestCase.test_method',
'app8.package2.subpackage1.tests.TestCase.test_method',
'app8.package2.subpackage2',
]),
(6, ['app8']))
self.assertEquals(self.project.find_tests(labels=[
'app8.package1.subpackage.tests.TestCase.test_method',
'app8.package2.subpackage1.tests.TestCase',
'app8.package2.subpackage2',
]),
(6, ['app8']))
self.assertEquals(self.project.find_tests(labels=[
'app8.package1.subpackage.tests.TestCase.test_method',
'app8.package2.subpackage1.tests',
'app8.package2.subpackage2',
]),
(6, ['app8']))
self.assertEquals(self.project.find_tests(labels=[
'app8.package1.subpackage.tests.TestCase.test_method',
'app8.package2.subpackage1',
'app8.package2.subpackage2',
]),
(6, ['app8']))
self.assertEquals(self.project.find_tests(labels=[
'app8.package1.subpackage.tests.TestCase.test_method',
'app8.package2',
]),
(6, ['app8']))
self.assertEquals(self.project.find_tests(labels=[
'app8.package1.subpackage.tests.TestCase',
'app8.package2',
]),
(6, ['app8']))
self.assertEquals(self.project.find_tests(labels=[
'app8.package1.subpackage.tests',
'app8.package2',
]),
(6, ['app8']))
self.assertEquals(self.project.find_tests(labels=[
'app8.package1.subpackage',
'app8.package2',
]),
(6, ['app8']))
self.assertEquals(self.project.find_tests(labels=[
'app8.package1',
'app8.package2',
]),
(6, ['app8']))

View file

@ -0,0 +1,82 @@
import subprocess
import unittest
import cricket
from cricket.unittest import discoverer
from cricket.unittest import executor
class TestCollection(unittest.TestCase):
def test_testCollection(self):
'''
Confirm that the pytest discovery mechanism is capable of
finding this test
'''
PTD = discoverer.PyTestDiscoverer()
PTD.collect_tests()
tests = str(PTD).split('\n')
test_found = False
for test in tests:
test_found |= 'test_testCollection' in test
self.assertTrue(test_found)
class TestExecutorCmdLine(unittest.TestCase):
def test_labels(self):
'''
Test that the command-line API is respecting the labels
being targetted for testing
'''
labels = ['tests.test_unit_integration.TestCollection']
cmdline = ['python', '-m', 'cricket.unittest.executor'] + labels
runner = subprocess.Popen(
cmdline,
stdin=None,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
shell=False,
)
output = ''
for line in runner.stdout:
output += line.decode('utf-8')
self.assertIn('tests.test_unit_integration.TestCollection',
output)
self.assertNotIn('tests.test_unit_integration.TestExecutorCmdLine',
output)
# This is a magic test which can be un-commented and run manually.
# It recursively calls the text executor, and fouls up normal
# output, so it had to be disabled as I am not smart enough
# to actually understand and fix the issue
# class TestExecutor(unittest.TestCase):
# def test_suite_execution(self):
# '''
# Note, it's hard to test full suite discovery because
# it will include this test and infinite loop. So just
# testing on a single test until I can figure out something
# smarter.
# '''
# run_only = [
# 'tests.test_unit_integration.TestDiscoverer'
# ]
# PTE = test_executor.PyTestExecutor()
# PTE.run_only(run_only)
# PTE.stream_results()
if __name__ == '__main__':
unittest.main()

12
gui-main Executable file
View file

@ -0,0 +1,12 @@
#!/bin/bash -e
ROOT=$(dirname "$(readlink -f "$0")")
export QT_QPA_PLATFORM=wayland
export QT_QPA_PLATFORM_PLUGIN_PATH=/usr/lib/qt/plugins/platforms
export PYTHONPATH=$ROOT:$ROOT/cricket
pushd $ROOT/tests > /dev/null
python -m cricket.unittest
popd > /dev/null

BIN
res/nocturne.wav Normal file

Binary file not shown.

0
tests/__init__.py Normal file
View file

1
tests/auto/__init__.py Normal file
View file

@ -0,0 +1 @@
MODULE_NAME = {'zh': '自动测试项', 'en': 'Auto Test Item'}

View file

@ -0,0 +1,19 @@
from unittest import TestCase
import os
class NVMeSSDTest(TestCase):
LANGUAGES = {
'zh': {
'NVMeSSDTest': 'NVMe固态硬盘',
'test_read_model': '读取型号'
},
'en': {
'NVMeSSDTest': 'NVMe SSD',
'test_read_model': 'Read model'
}
}
def test_read_model(self):
model_file = '/sys/class/nvme/nvme0/model'
self.assertTrue(os.path.exists(model_file))

View file

View file

View file

@ -0,0 +1,20 @@
from unittest import TestCase
import os
class MouseTest(TestCase):
LANGUAGES = {
'zh': {
'MouseTest': '鼠标',
'test_read_product': '识别产品名称'
},
'en': {
'MouseTest': 'Mouse',
'test_read_product': 'Read product name'
}
}
def test_read_product(self):
u2_hub_dir = '/sys/devices/platform/soc/soc:usb3@0/c0a00000.dwc3/xhci-hcd.0.auto/usb2/2-1/'
self.assertTrue(os.path.exists(f'{u2_hub_dir}/product'))
self.assertTrue(os.path.exists(f'{u2_hub_dir}/2-1.1/product'))

View file

@ -0,0 +1,24 @@
from unittest import TestCase
import os
class UDiskTest(TestCase):
LANGUAGES = {
'zh': {
'UDiskTest': 'U盘',
'test_read_product': '识别产品名称'
},
'en': {
'UDiskTest': 'U Disk',
'test_read_product': 'Read product name'
}
}
def test_read_product(self):
u2_hub_dir = '/sys/devices/platform/soc/soc:usb3@0/c0a00000.dwc3/xhci-hcd.0.auto/usb2/2-1/'
u3_hub_dir = '/sys/devices/platform/soc/soc:usb3@0/c0a00000.dwc3/xhci-hcd.0.auto/usb3/3-1/'
self.assertTrue(os.path.exists(f'{u2_hub_dir}/product'))
self.assertTrue(os.path.exists(f'{u3_hub_dir}/product'))
# self.assertTrue(os.path.exists(f'{u3_hub_dir}/3-1.2/product'))
# self.assertTrue(os.path.exists(f'{u3_hub_dir}/3-1.3/product'))
self.assertTrue(os.path.exists(f'{u3_hub_dir}/3-1.4/product'))

View file

@ -0,0 +1,19 @@
from unittest import TestCase
import os
class eMMCTest(TestCase):
LANGUAGES = {
'zh': {
'eMMCTest': 'eMMC',
'test_identify': '识别'
},
'en': {
'eMMCTest': 'eMMC',
'test_identify': 'Identify'
}
}
def test_identify(self):
block_file = '/sys/class/block/mmcblk2'
self.assertTrue(os.path.exists(block_file))

View file

View file

@ -0,0 +1,69 @@
from unittest import TestCase
import os
import time
import socket
import struct
import fcntl
import subprocess
class Eth0Test(TestCase):
LANGUAGES = {
'zh': {
'Eth0Test': '网口1',
'test_ping': 'ping'
},
'en': {
'Eth0Test': '网口1',
'test_ping': 'ping'
}
}
def get_carrier(self, ifname: str):
carrier = f'/sys/class/net/{ifname}/carrier'
with open(carrier, 'r') as f:
return f.readline().strip()
def get_ip(self, ifname: str):
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
return socket.inet_ntoa(fcntl.ioctl(
s.fileno(),
0x8915,
struct.pack('256s', bytes(ifname[:15],'utf-8'))
)[20:24])
def test_ping(self):
ifname = 'eth0'
ping = f'ping -I {ifname} -c 3 baidu.com'
timeout = 10
i = 0
while i < timeout:
if self.get_carrier(ifname) == '1':
break
time.sleep(1)
i += 1
if i == timeout:
self.fail('Get carrier imeout')
i = 0
while i < timeout:
try:
ip = self.get_ip(ifname)
print(f'{ifname}: {ip}')
break
except:
time.sleep(1)
i += 1
if i == timeout:
self.fail('Get ip timeout')
try:
result = subprocess.run(ping, capture_output=True, shell=True, timeout=timeout)
self.assertEqual(result.returncode, 0)
except subprocess.TimeoutExpired:
self.fail('Ping timeout')

View file

@ -0,0 +1,69 @@
from unittest import TestCase
import os
import time
import socket
import struct
import fcntl
import subprocess
class Eth1Test(TestCase):
LANGUAGES = {
'zh': {
'Eth1Test': '网口2',
'test_ping': 'ping'
},
'en': {
'Eth1Test': '网口2',
'test_ping': 'ping'
}
}
def get_carrier(self, ifname: str):
carrier = f'/sys/class/net/{ifname}/carrier'
with open(carrier, 'r') as f:
return f.readline().strip()
def get_ip(self, ifname: str):
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
return socket.inet_ntoa(fcntl.ioctl(
s.fileno(),
0x8915,
struct.pack('256s', bytes(ifname[:15],'utf-8'))
)[20:24])
def test_ping(self):
ifname = 'eth1'
ping = f'ping -I {ifname} -c 3 baidu.com'
timeout = 10
i = 0
while i < timeout:
if self.get_carrier(ifname) == '1':
break
time.sleep(1)
i += 1
if i == timeout:
self.fail('Get carrier imeout')
i = 0
while i < timeout:
try:
ip = self.get_ip(ifname)
print(f'{ifname}: {ip}')
break
except:
time.sleep(1)
i += 1
if i == timeout:
self.fail('Get ip timeout')
try:
result = subprocess.run(ping, capture_output=True, shell=True, timeout=timeout)
self.assertEqual(result.returncode, 0)
except subprocess.TimeoutExpired:
self.fail('Ping timeout')

23
tests/auto/test_08_bt.py Normal file
View file

@ -0,0 +1,23 @@
from unittest import TestCase
import subprocess
class BTTest(TestCase):
LANGUAGES = {
'zh': {
'BTTest': '蓝牙',
'test_scan': '扫描'
},
'en': {
'BTTest': 'Bluetooth',
'test_scan': 'Scan'
}
}
def test_scan(self):
scan = 'hcitool -i hci0 scan'
try:
result = subprocess.run(scan, capture_output=True, shell=True, timeout=10)
self.assertEqual(result.returncode, 0)
except subprocess.TimeoutExpired:
self.fail('Scan timeout')

1
tests/manual/__init__.py Normal file
View file

@ -0,0 +1 @@
MODULE_NAME = {'zh': '手动测试项', 'en': 'Manual Test Item'}

View file

@ -0,0 +1,48 @@
from unittest import TestCase
import subprocess
import threading
from utils.speaker import SpeakerTestWindow, GetTestResult
class SpeakerTest(TestCase):
LANGUAGES = {
'zh': {
'SpeakerTest': '喇叭',
'test_play': '播放一段铃声',
'title': '喇叭',
'test_step': '''1. 点击播放,设备将播放一段铃声,判断从喇叭听到的铃声是否符合预期
'''
},
'en': {
'SpeakerTest': 'Speaker',
'test_play': 'Paly bell',
'title': 'Speaker',
'test_step': '''1. 点击播放,设备将播放一段铃声,判断从喇叭听到的铃声是否符合预期
'''
}
}
def _play_routine(self):
volume = 128
cmd = f'amixer -c 1 cset numid=1,iface=MIXER,name="DAC Playback Volume" {volume}'
result = subprocess.run(cmd, capture_output=True, shell=True)
print(f'amixer: {result.returncode}')
bell_file = '/opt/factorytest/res/nocturne.wav'
cmd = f'aplay -Dhw:1,0 -r 48000 -f S16_LE {bell_file} > /dev/null'
self.play_proc = subprocess.Popen(cmd, shell=True)
def _stop_routine(self):
if self.play_proc.poll() is None:
print('Try to kill play process...')
self.play_proc.kill()
def test_play(self):
t = threading.Thread(target=SpeakerTestWindow,
args=(self._play_routine,
self._stop_routine,
self.LANGUAGES,))
t.start()
t.join()
self.assertTrue(GetTestResult())

View file

@ -0,0 +1,67 @@
from unittest import TestCase
import subprocess
import threading
from utils.mic import MicTestWindow, GetTestResult
class MicTest(TestCase):
LANGUAGES = {
'zh': {
'MicTest': '麦克风',
'test_mic_loop': '录音回放',
'title': '麦克风',
'test_step': '''1. 点击录音对着开发板说测试1、2、3、4、5
2. 点击停止判断从喇叭听到的是否刚才对着开发板说的
'''
},
'en': {
'MicTest': 'Mic',
'test_mic_loop': 'Record and playback',
'title': 'Mic',
'test_step': '''1. 点击录音对着开发板说测试1、2、3、4、5
2. 点击停止判断从喇叭听到的是否刚才对着开发板说的
'''
}
}
def _record_routine(self):
volume = 191
cmd = f'amixer -c 1 cset numid=7,iface=MIXER,name="ADC Capture Volume" {volume},{volume}'
result = subprocess.run(cmd, capture_output=True, shell=True)
print(f'capture amixer: {result.returncode}')
timeout = 10
record_file = '/tmp/record.wav'
cmd = f'arecord -Dhw:1,0 -r 48000 -f S16_LE -d {timeout} {record_file}'
self.record_proc = subprocess.Popen(cmd, shell=True)
def _stop_record(self):
if self.record_proc.poll() is None:
print('Try to kill record process...')
self.record_proc.kill()
def _playback_routine(self):
volume = 191
cmd = f'amixer -c 1 cset numid=1,iface=MIXER,name="DAC Playback Volume" {volume}'
result = subprocess.run(cmd, capture_output=True, shell=True)
print(f'playback amixer: {result.returncode}')
cmd = 'aplay -Dhw:1,0 -r 48000 -f S16_LE /tmp/record.wav'
self.playback_proc = subprocess.Popen(cmd, shell=True)
def _stop_playback(self):
if self.playback_proc.poll() is None:
print('Try to kill playback process...')
self.playback_proc.kill()
def test_mic_loop(self):
t = threading.Thread(target=MicTestWindow,
args=(self._record_routine,
self._stop_record,
self._playback_routine,
self._stop_playback,
self.LANGUAGES),)
t.start()
t.join()
self.assertTrue(GetTestResult())

View file

@ -0,0 +1,50 @@
from unittest import TestCase
import subprocess
import threading
from utils.speaker import SpeakerTestWindow, GetTestResult
class HeadphonesTest(TestCase):
LANGUAGES = {
'zh': {
'HeadphonesTest': '耳机',
'test_play': '播放一段铃声',
'title': '耳机',
'test_step': '''1. 插入耳机
2. 点击播放设备将播放一段铃声判断从喇叭听到的铃声是否符合预期
'''
},
'en': {
'HeadphonesTest': 'Headphones',
'test_play': 'Paly bell',
'title': 'Headphones',
'test_step': '''1. 插入耳机
2. 点击播放设备将播放一段铃声判断从喇叭听到的铃声是否符合预期
'''
}
}
def _play_routine(self):
volume = 128
cmd = f'amixer -c 1 cset numid=1,iface=MIXER,name="DAC Playback Volume" {volume}'
result = subprocess.run(cmd, capture_output=True, shell=True)
print(f'amixer: {result.returncode}')
bell_file = '/opt/factorytest/res/nocturne.wav'
cmd = f'aplay -Dhw:1,0 -r 48000 -f S16_LE {bell_file}'
self.play_proc = subprocess.Popen(cmd, shell=True)
def _stop_routine(self):
if self.play_proc.poll() is None:
print('Try to kill play process...')
self.play_proc.kill()
def test_play(self):
t = threading.Thread(target=SpeakerTestWindow,
args=(self._play_routine,
self._stop_routine,
self.LANGUAGES),)
t.start()
t.join()
self.assertTrue(GetTestResult())

View file

@ -0,0 +1,69 @@
from unittest import TestCase
import subprocess
import threading
from utils.mic import MicTestWindow, GetTestResult
class MicrophoneTest(TestCase):
LANGUAGES = {
'zh': {
'MicrophoneTest': '耳麦',
'test_mic_loop': '录音回放',
'title': '耳麦',
'test_step': '''1. 插入耳机
2. 点击录音对着耳麦说测试12345
3. 点击停止判断从喇叭听到的是否刚才对着开发板说的
'''
},
'en': {
'MicrophoneTest': 'Microphone',
'test_mic_loop': 'Record and playback',
'title': 'Microphone',
'test_step': '''1. 插入耳机
2. 点击录音对着耳麦说测试12345
3. 点击停止判断从喇叭听到的是否刚才对着开发板说的
'''
}
}
def _record_routine(self):
volume = 191
cmd = f'amixer -c 1 cset numid=7,iface=MIXER,name="ADC Capture Volume" {volume},{volume}'
result = subprocess.run(cmd, capture_output=True, shell=True)
print(f'capture amixer: {result.returncode}')
timeout = 10
record_file = '/tmp/record.wav'
cmd = f'arecord -Dhw:1,0 -r 48000 -f S16_LE -d {timeout} {record_file}'
self.record_proc = subprocess.Popen(cmd, shell=True)
def _stop_record(self):
if self.record_proc.poll() is None:
print('Try to kill record process...')
self.record_proc.kill()
def _playback_routine(self):
volume = 160
cmd = f'amixer -c 1 cset numid=1,iface=MIXER,name="DAC Playback Volume" {volume}'
result = subprocess.run(cmd, capture_output=True, shell=True)
print(f'playback amixer: {result.returncode}')
cmd = 'aplay -Dhw:1,0 -r 48000 -f S16_LE /tmp/record.wav'
self.playback_proc = subprocess.Popen(cmd, shell=True)
def _stop_playback(self):
if self.playback_proc.poll() is None:
print('Try to kill playback process...')
self.playback_proc.kill()
def test_mic_loop(self):
t = threading.Thread(target=MicTestWindow,
args=(self._record_routine,
self._stop_record,
self._playback_routine,
self._stop_playback,
self.LANGUAGES),)
t.start()
t.join()
self.assertTrue(GetTestResult())

137
utils/mic.py Normal file
View file

@ -0,0 +1,137 @@
from PyQt5.QtCore import Qt
from PyQt5.QtGui import QGuiApplication
from PyQt5.QtWidgets import (
QApplication,
QMainWindow,
QFrame,
QVBoxLayout,
QGroupBox,
QLabel,
QStatusBar,
QPushButton,
QGridLayout
)
from cricket.lang import SimpleLang
result = False
class MicTestWindow(SimpleLang):
def __init__(self, record_routine, stop_record, playback_routine, stop_playback, languages) -> None:
super().__init__()
self.record_routine = record_routine
self.stop_record = stop_record
self.playback_routine = playback_routine
self.stop_playback = stop_playback
self.languages = languages
self.app = QApplication([])
self.window = QMainWindow()
self.window.setWindowTitle(self._get_text('title'))
# self.window.setWindowFlags(Qt.WindowStaysOnTopHint)
# Main content
content = QFrame(self.window)
content_layout = QVBoxLayout(content)
# Test step
box = QGroupBox(self.get_text('test_step'), content)
box_layout = QVBoxLayout(box)
label = QLabel(self._get_text('test_step'), box)
box_layout.addWidget(label)
content_layout.addWidget(box)
# Status
self.status = QStatusBar(content)
self.status.showMessage('Not running')
content_layout.addWidget(self.status)
# Toolbar
toolbar = QFrame(content)
toolbar_layout = QGridLayout(toolbar)
self.record_button = QPushButton(self.get_text('record_button'), toolbar)
self.record_button.clicked.connect(self.cmd_record)
self.record_button.setFocus()
toolbar_layout.addWidget(self.record_button, 0, 0)
self.playback_button = QPushButton(self.get_text('playback_button'), toolbar)
self.playback_button.setDisabled(True)
self.playback_button.clicked.connect(self.cmd_playback)
toolbar_layout.addWidget(self.playback_button, 0, 1)
self.pass_button = QPushButton(self.get_text('pass_button'), toolbar)
self.pass_button.setDisabled(True)
self.pass_button.clicked.connect(self.cmd_pass)
toolbar_layout.addWidget(self.pass_button, 0, 2)
self.fail_button = QPushButton(self.get_text('fail_button'), toolbar)
self.fail_button.setDisabled(True)
self.fail_button.clicked.connect(self.cmd_fail)
toolbar_layout.addWidget(self.fail_button, 0, 3)
content_layout.addWidget(toolbar)
self.window.setCentralWidget(content)
# Move to center
# screen = QGuiApplication.primaryScreen()
# if screen:
# screen_geometry = screen.geometry()
# window_geometry = self.window.geometry()
# x = (screen_geometry.width() - window_geometry.width()) // 2
# y = (screen_geometry.height() - window_geometry.height()) // 2
# self.window.move(x, y)
# self.window.show()
self.window.showFullScreen()
self.app.exec_()
def _get_text(self, key):
if self.languages:
lang = self.languages.get(self.current_lang)
if lang is not None:
text = lang.get(key)
if text is not None:
return text
return 'Undefined'
def cmd_record(self):
self.status.showMessage('Recording...')
self.record_button.setDisabled(True)
self.record_routine()
self.playback_button.setDisabled(False)
def cmd_playback(self):
self.stop_record()
self.status.showMessage('Playback...')
self.playback_button.setDisabled(True)
self.playback_routine()
self.pass_button.setDisabled(False)
self.fail_button.setDisabled(False)
def cmd_pass(self):
global result
self.status.showMessage('Stopping...')
self.stop_playback()
result = True
self.app.quit()
def cmd_fail(self):
global result
self.status.showMessage('Stopping...')
self.stop_playback()
result = False
self.app.quit()
def GetTestResult():
return result

135
utils/speaker.py Normal file
View file

@ -0,0 +1,135 @@
from PyQt5.QtCore import Qt
from PyQt5.QtGui import QGuiApplication
from PyQt5.QtWidgets import (
QApplication,
QMainWindow,
QFrame,
QVBoxLayout,
QGroupBox,
QLabel,
QStatusBar,
QPushButton,
QGridLayout
)
from cricket.lang import SimpleLang
result = False
class SpeakerTestWindow(SimpleLang):
def __init__(self, play_routine, stop_routine, languages) -> None:
super().__init__()
self.play_routine = play_routine
self.stop_routine = stop_routine
self.languages = languages
self.app = QApplication([])
self.window = QMainWindow()
self.window.setWindowTitle(self._get_text('title'))
# self.window.setWindowFlags(Qt.WindowStaysOnTopHint)
# Main content
content = QFrame(self.window)
content_layout = QVBoxLayout(content)
# Test step
box = QGroupBox(self.get_text('test_step'), content)
box_layout = QVBoxLayout(box)
label = QLabel(self._get_text('test_step'), box)
box_layout.addWidget(label)
content_layout.addWidget(box)
# Status
self.status = QStatusBar(content)
self.status.showMessage('Not running')
content_layout.addWidget(self.status)
# Toolbar
toolbar = QFrame(content)
toolbar_layout = QGridLayout(toolbar)
self.play_button = QPushButton(self.get_text('play_button'), toolbar)
self.play_button.clicked.connect(self.cmd_play)
self.play_button.setFocus()
toolbar_layout.addWidget(self.play_button, 0, 0)
self.stop_button = QPushButton(self.get_text('stop_button'), toolbar)
self.stop_button.setDisabled(True)
self.stop_button.clicked.connect(self.cmd_stop)
toolbar_layout.addWidget(self.stop_button, 0, 1)
self.pass_button = QPushButton(self.get_text('pass_button'), toolbar)
self.pass_button.setDisabled(True)
self.pass_button.clicked.connect(self.cmd_pass)
toolbar_layout.addWidget(self.pass_button, 0, 2)
self.fail_button = QPushButton(self.get_text('fail_button'), toolbar)
self.fail_button.setDisabled(True)
self.fail_button.clicked.connect(self.cmd_fail)
toolbar_layout.addWidget(self.fail_button, 0, 3)
content_layout.addWidget(toolbar)
self.window.setCentralWidget(content)
# Move to center
# screen = QGuiApplication.primaryScreen()
# if screen:
# screen_geometry = screen.geometry()
# window_geometry = self.window.geometry()
# x = (screen_geometry.width() - window_geometry.width()) // 2
# y = (screen_geometry.height() - window_geometry.height()) // 2
# self.window.move(x, y)
# self.window.show()
self.window.showFullScreen()
self.app.exec_()
def _get_text(self, key):
if self.languages:
lang = self.languages.get(self.current_lang)
if lang is not None:
text = lang.get(key)
if text is not None:
return text
return 'Undefined'
def cmd_play(self):
self.status.showMessage('Running...')
self.play_button.setDisabled(True)
self.play_routine()
self.stop_button.setDisabled(False)
self.pass_button.setDisabled(False)
self.fail_button.setDisabled(False)
def cmd_stop(self):
self.status.showMessage('Stopping...')
self.stop_button.setDisabled(True)
self.stop_routine()
self.play_button.setDisabled(False)
self.status.showMessage('Stopped')
def cmd_pass(self):
global result
self.status.showMessage('Stopping...')
self.stop_routine()
result = True
self.app.quit()
def cmd_fail(self):
global result
self.status.showMessage('Stopping...')
self.stop_routine()
result = False
self.app.quit()
def GetTestResult():
return result