Skip to content

Commit

Permalink
Merge pull request #1 from dave-shawley/first-release
Browse files Browse the repository at this point in the history
First release with some functionality
  • Loading branch information
dave-shawley committed Jul 6, 2019
2 parents 39a072d + 32340c6 commit 18e1de2
Show file tree
Hide file tree
Showing 12 changed files with 488 additions and 1 deletion.
34 changes: 34 additions & 0 deletions .circleci/config.yml
Expand Up @@ -13,6 +13,7 @@ workflows:
- trigger-docs-build:
requires:
- build-docs
- run-tests
release-workflow:
jobs:
- upload-package:
Expand Down Expand Up @@ -75,3 +76,36 @@ jobs:
--data 'branches=master' \
--header 'Email-Address: daveshawley+rtd@gmail.com' \
https://readthedocs.org/api/v2/webhook/cavy/91155/
run-tests:
executor: python-3
steps:
- checkout
- run:
name: Install tools
command: |
pip install -q --user '.[dev]'
- run:
name: Run tests
command: |
export PATH=$PATH:$HOME/.local/bin
mkdir -p build/circleci/nosetests
nosetests --with-coverage \
--with-xunit --xunit-file=build/nosetests.xml
coverage report
coverage xml -o build/coverage.xml
# required for store_test_results
cp build/nosetests.xml build/circleci/nosetests/results.xml
- store_test_results:
path: build/circleci
- run:
name: Upload coverage to coveralls
command: |
export PATH=$PATH:$HOME/.local/bin
if test -n "$COVERALLS_REPO_TOKEN"
then
pip install -q --user coveralls
coveralls
else
echo 'COVERALLS_REPO_TOKEN is not defined, skipping upload.'
fi
2 changes: 2 additions & 0 deletions MANIFEST.in
@@ -1,5 +1,7 @@
include CHANGELOG
include LICENSE
include tox.ini
graft docs
graft tests

global-exclude *.pyc
7 changes: 6 additions & 1 deletion README.rst
@@ -1,4 +1,4 @@
|Version| |Python| |Source| |Docs| |CI|
|Version| |Python| |Source| |Coverage| |Docs| |CI|

This is a *kitchen sink* library of utility classes that simplifies writing
richer tests. I extracted the contents from various projects that I've
Expand All @@ -7,6 +7,8 @@ and paste or rewrite the same code everywhere.

.. |CI| image:: https://img.shields.io/circleci/project/github/dave-shawley/cavy/master.svg
:target: https://circleci.com/gh/dave-shawley/cavy
.. |Coverage| image:: https://coveralls.io/repos/github/dave-shawley/cavy/badge.svg?branch=master
:target: https://coveralls.io/github/dave-shawley/cavy?branch=master
.. |Docs| image:: https://img.shields.io/readthedocs/cavy.svg
:target: https://cavy.readthedocs.io/
.. |Python| image:: https://img.shields.io/pypi/pyversions/cavy.svg
Expand All @@ -15,3 +17,6 @@ and paste or rewrite the same code everywhere.
:target: https://github.com/dave-shawley/cavy
.. |Version| image:: https://img.shields.io/pypi/v/cavy.svg
:target: https://pypi.org/project/cavy



250 changes: 250 additions & 0 deletions cavy/testing.py
@@ -0,0 +1,250 @@
import logging
import os
import unittest


class EnvironmentMixin(unittest.TestCase):
"""Adds safe environment variable manipulation.
Use this instead of manipulating :data:`os.environ` or calling
:meth:`os.setenv` directly. Failing to do so may cause strange
test failures when environment variables leak between test
cases.
"""

def setUp(self):
super().setUp()
self.__logger = logging.getLogger(self.__class__.__name__)
self.__saved_vars = {}

def tearDown(self):
"""Automatically reset the environment during test clenaup."""
super().tearDown()
self.reset_environment()

def reset_environment(self):
"""Undo changes to the environment."""
for name, value in self.__saved_vars.items():
os.environ.pop(name, None)
if value is not None:
os.environ[name] = value
self.__saved_vars.clear()

def setenv(self, name, value):
"""Set an environment variable.
:param str name: names the environment variable to set
:param str value: value to store in the environment
"""
self.__logger.getChild('setenv').info('setting %s=%s', name, value)
self.__saved_vars.setdefault(name, os.environ.get(name))
os.environ[name] = value

def unsetenv(self, name):
"""Clear an environment variable.
:param str name: names the environment variable to clear
"""
self.__logger.getChild('unsetenv').info('clearing %s', name)
self.__saved_vars.setdefault(name, os.environ.get(name))
os.environ.pop(name, None)


class PEP8NamingMixin(unittest.TestCase):
"""PEP8 compliant names for assertions.
Mix this class in over :class:`unittest.TestCase` to provide
:pep:`8` compliant names for the assertions. This makes it possible
to write tests that look like::
class MyTest(PEP8AssertionsMixin, unittest.TestCase):
def test_something(self):
self.assert_equal(1, 2)
instead of::
class MyTest(PEP8AssertionsMixin, unittest.TestCase):
def test_something(self):
self.assertEqual(1, 2)
It is a small thing but the non-PEP8 names in unittest have
always bothered me.
"""

def assert_false(self, expr, msg=None):
self.assertFalse(expr, msg)

def assert_true(self, expr, msg=None):
self.assertTrue(expr, msg)

def assert_raises(self, expected_exception, *args, **kwargs):
return self.assertRaises(expected_exception, *args, **kwargs)

def assert_warns(self, expected_warning, *args, **kwargs):
return self.assertWarns(expected_warning, *args, **kwargs)

def assert_logs(self, logger=None, level=None):
return self.assertLogs(logger, level)

def assert_equal(self, first, second, msg=None):
self.assertEqual(first, second, msg)

def assert_not_equal(self, first, second, msg=None):
self.assertNotEqual(first, second, msg)

def assert_almost_equal(self,
first,
second,
places=None,
msg=None,
delta=None):
self.assertAlmostEqual(first, second, places, msg, delta)

def assert_not_almost_equal(self,
first,
second,
places=None,
msg=None,
delta=None):
self.assertNotAlmostEqual(first, second, places, msg, delta)

def assert_sequence_equal(self, seq1, seq2, msg=None, seq_type=None):
self.assertSequenceEqual(seq1, seq2, msg, seq_type)

def assert_list_equal(self, list1, list2, msg=None):
self.assertListEqual(list1, list2, msg)

def assert_tuple_equal(self, tuple1, tuple2, msg=None):
self.assertTupleEqual(tuple1, tuple2, msg)

def assert_set_equal(self, set1, set2, msg=None):
self.assertSetEqual(set1, set2, msg)

def assert_in(self, member, container, msg=None):
self.assertIn(member, container, msg)

def assert_not_in(self, member, container, msg=None):
self.assertNotIn(member, container, msg)

def assert_is(self, expr1, expr2, msg=None):
self.assertIs(expr1, expr2, msg)

def assert_is_not(self, expr1, expr2, msg=None):
self.assertIsNot(expr1, expr2, msg)

def assert_dict_equal(self, d1, d2, msg=None):
self.assertDictEqual(d1, d2, msg)

def assert_count_equal(self, first, second, msg=None):
self.assertCountEqual(first, second, msg)

def assert_multi_line_equal(self, first, second, msg=None):
self.assertMultiLineEqual(first, second, msg)

def assert_less(self, a, b, msg=None):
self.assertLess(a, b, msg)

def assert_less_equal(self, a, b, msg=None):
self.assertLessEqual(a, b, msg)

def assert_greater(self, a, b, msg=None):
self.assertGreater(a, b, msg)

def assert_greater_equal(self, a, b, msg=None):
self.assertGreaterEqual(a, b, msg)

def assert_is_none(self, obj, msg=None):
self.assertIsNone(obj, msg)

def assert_is_not_none(self, obj, msg=None):
self.assertIsNotNone(obj, msg)

def assert_is_instance(self, obj, cls, msg=None):
self.assertIsInstance(obj, cls, msg)

def assert_not_is_instance(self, obj, cls, msg=None):
self.assertNotIsInstance(obj, cls, msg)

def assert_raises_regex(self, expected_exception, expected_regex, *args,
**kwargs):
return self.assertRaisesRegex(expected_exception, expected_regex,
*args, **kwargs)

def assert_warns_regex(self, expected_warning, expected_regex, *args,
**kwargs):
return self.assertWarnsRegex(expected_warning, expected_regex, *args,
**kwargs)

def assert_regex(self, text, expected_regex, msg=None):
self.assertRegex(text, expected_regex, msg)

def assert_not_regex(self, text, unexpected_regex, msg=None):
self.assertNotRegex(text, unexpected_regex, msg)


class AdditionalAssertionsMixin(unittest.TestCase):
"""Useful assertions that aren't in the Standard Library.
This mix-in includes some assertions that I've found myself
wishing were part of the Standard Library from time to time.
"""

def assert_between(self, value, low, high, msg=None):
"""Assert that `value` is between `low` and `high`.
:param value: value to compare
:param low: inclusive low range endpoint
:param high: inclusive high range endpoint
:param str msg: optional message to use on failure
"""
if msg is None:
self.longMessage = False
msg = '{!r} is not between {!r} and {!r}'.format(value, low, high)
self.assertGreaterEqual(value, low, msg=msg)
self.assertLessEqual(value, high, msg=msg)

def assert_startswith(self, value, prefix, msg=None):
"""Assert that `prefix` is a prefix of `value`.
:param value: value to check
:param prefix: sequence of values that `value` should start with.
:param str msg: optional message to use on failure
Note that `value` and `prefix` ARE NOT required to be of the
same type. The only requirement is that they are ordered
sequences of the same underlying type.
"""
if msg is None:
msg = '{!r} does not start with {!r}'.format(value, prefix)
if len(prefix) > len(value):
self.fail(msg)
for a, b in zip(value, prefix):
if a != b:
self.fail(msg)

def assert_endswith(self, value, suffix, msg=None):
"""Assert that `suffix` is a suffix of `value`.
:param value: value to check
:param suffix: sequence of values that `value` should end with
:param str msg: optional message to use on failure
Note that `value` and `suffix` ARE NOT required to be of the
same type. They are required to be reversible sequences of the
same underlying type.
"""
if msg is None:
msg = '{!r} does not end with {!r}'.format(value, suffix)
if len(suffix) > len(value):
raise AssertionError(msg)
for a, b in zip(reversed(value), reversed(suffix)):
if a != b:
raise AssertionError(msg)
13 changes: 13 additions & 0 deletions docs/index.rst
Expand Up @@ -4,6 +4,19 @@ cavy -- testing helper library

.. include:: ../README.rst

Reference
=========

.. autoclass:: cavy.testing.AdditionalAssertionsMixin
:members:

.. autoclass:: cavy.testing.EnvironmentMixin
:members:

.. autoclass:: cavy.testing.PEP8NamingMixin
:members:
:undoc-members:

Release History
===============

Expand Down
7 changes: 7 additions & 0 deletions setup.cfg
Expand Up @@ -4,6 +4,13 @@ warning-is-error = 1
[coverage:report]
show_missing = 1

[coverage:run]
branch = 1
source = cavy

[flake8]
exclude = build,dist,env

[nosetests]
cover-branches = 1
cover-erase = 1
Expand Down
3 changes: 3 additions & 0 deletions setup.py
Expand Up @@ -16,8 +16,11 @@
packages=['cavy'],
extras_require={
'dev': [
'coverage==4.5.3',
'flake8==3.7.7',
'nose==1.3.7',
'sphinx==2.1.2',
'tox==3.13.2',
'yapf==0.27.0',
],
},
Expand Down
Empty file added tests/__init__.py
Empty file.

0 comments on commit 18e1de2

Please sign in to comment.