Skip to content
Permalink
Browse files

WIP, have bunch of main.py tests working under pytest-relaxed now

  • Loading branch information...
bitprophet committed Jun 27, 2017
1 parent 82dcc9e commit 1171a0216b8a56501c59359e728ff9b8d13397ba
Showing with 164 additions and 99 deletions.
  1. +1 −0 .gitignore
  2. +4 −4 dev-requirements.txt
  3. +2 −1 fabric/executor.py
  4. +4 −0 setup.cfg
  5. +14 −7 tasks.py
  6. +2 −3 tests/_support/runtime_fabfile.py
  7. +91 −52 tests/_util.py
  8. +9 −0 tests/config.yml
  9. +37 −32 tests/main.py
@@ -17,3 +17,4 @@ TAGS
tox.ini
.idea/
htmlcov
.cache
@@ -1,10 +1,10 @@
# Invoke implicitly required by self/pip install -e .
# Invocations for common project tasks
invocations>=0.17,<2.0
# Spec for test organization/etc
spec==1.3.1
nose==1.3.0
six==1.6.1
# pytest-relaxed for test organization, display etc tweaks
pytest-relaxed==1.0.0
pytest==3.1.2
six==1.10.0
# Mock for test mocking
mock==1.0.1
# Linting!
@@ -34,7 +34,8 @@ def expand_calls(self, calls):
if not hosts:
raise NothingToDo("Was told to run a command, but not given any hosts to run it on!") # noqa
def anonymous(c):
c.run(self.core.remainder)
# TODO: how to make all our tests configure in_stream=False?
c.run(self.core.remainder, in_stream=False)
anon = Call(Task(body=anonymous))
# TODO: see above TODOs about non-parameterized setups, roles etc
# TODO: will likely need to refactor that logic some more so it can
@@ -5,3 +5,7 @@ max-line-length = 79

[metadata]
license_file = LICENSE

[tool:pytest]
testpaths = tests
python_files = *
@@ -1,7 +1,5 @@
from invocations.docs import docs, www, sites, watch_docs
from invocations.testing import (
test, integration, coverage, watch_tests, count_errors,
)
from invocations.pytest import test, integration, coverage
from invocations.packaging import release
from invocations import travis

@@ -10,11 +8,19 @@


ns = Collection(
docs, www, test, coverage, integration, sites, watch_docs,
watch_tests, count_errors, release, travis,
coverage,
docs,
integration,
release,
sites,
test,
travis,
watch_docs,
www,
)
ns.configure({
'tests': {
# TODO: have pytest tasks honor these?
'package': 'fabric',
'logformat': LOG_FORMAT,
},
@@ -23,8 +29,9 @@
'wheel': True,
'check_desc': True,
},
# TODO: perhaps move this into a tertiary, non automatically loaded, conf
# file so that both this & the code under test can reference it? Meh.
# TODO: perhaps move this into a tertiary, non automatically loaded,
# conf file so that both this & the code under test can reference it?
# Meh.
'travis': {
'sudo': {
'user': 'sudouser',
@@ -1,14 +1,13 @@
from invoke import task
from spec import eq_


@task
def runtime_ssh_config(c):
# NOTE: assumes it's run with host='runtime' + ssh_configs/runtime.conf
# TODO: SSHConfig should really learn to turn certain things into ints
# automatically...
eq_(c.ssh_config['port'], '666')
eq_(c.port, 666)
assert c.ssh_config['port'] == '666'
assert c.port == 666


@task
@@ -1,16 +1,20 @@
from itertools import chain, repeat
from io import BytesIO
import os
import re
import sys
import types

from invoke.vendor.six import wraps, iteritems

from mock import patch, Mock, PropertyMock, call, ANY
from pytest import fixture
from pytest_relaxed import trap

from fabric import Connection
from fabric.main import program as fab_program
from fabric.transfer import Transfer
from mock import patch, Mock, PropertyMock, call, ANY
from spec import eq_, trap, Spec



support_path = os.path.join(
@@ -19,13 +23,26 @@
)


# TODO: figure out a non shite way to share Invoke's more beefy copy of same.
# TODO: revert to asserts
def eq_(got, expected):
assert got == expected


@trap
def expect(invocation, out, program=None, test=None):
def expect(invocation, out, program=None, test='equals'):
if program is None:
program = fab_program
program.run("fab {}".format(invocation), exit=False)
(test or eq_)(sys.stdout.getvalue(), out)
output = sys.stdout.getvalue()
if test == 'equals':
assert output == out
elif test == 'contains':
assert out in output
elif test == 'regex':
assert re.match(out, output)
else:
err = "Don't know how to expect that <stdout> {} <expected>!"
assert False, err.format(test)


class Command(object):
@@ -247,41 +264,39 @@ class MockRemote(object):
Class representing mocked remote state.
Set up for start/stop style patching (so it can be used in situations
requiring setup/teardown semantics); is then wrapped by e.g. `mock_remote`
to provide decorator, etc style use.
"""
# TODO: make it easier to assume one session w/ >1 command?
def __init__(self, cmd=None, out=None, err=None, in_=None, exit=None,
commands=None, sessions=None, autostart=True):
"""
Create & start new remote state.
Multiple ways to instantiate:
requiring setup/teardown semantics); is then wrapped by the `remote`
fixture.
- no args, for basic "don't explode / touch network" stubbing
- pass Session args directly for a one-off anonymous session
- pass ``commands`` kwarg with explicit commands (put into an anonymous
session)
- pass ``sessions`` kwarg with explicit sessions
Defaults to a single anonymous `Session`, so it can be used as a "request &
forget" pytest fixture. Users requiring detailed remote session
expectations can call methods like `expect`, which wipe that anonymous
Session & set up a new one instead.
"""
def __init__(self):
self.expect_session(Session())

Combining these approaches is not well defined.
# TODO: make it easier to assume one session w/ >1 command?

Will automatically call `start` by default; say ``autostart=False`` to
disable.
# TODO: look at use of MockRemote; now that it's being used as a fixture,
# suspect we can do away with the "list of sessions OR args for a single
# session", just have 2x methods instead
def expect(self, host, cmd):
"""
if commands:
sessions = [Session(commands=commands)]
elif not sessions:
if cmd or out or err or exit:
session = Session(
cmd=cmd, out=out, err=err, in_=in_, exit=exit,
)
else:
session = Session()
sessions = [session]
self.sessions = sessions
if autostart:
self.start()
Set up and start mocking a remote session.
"""
session = Session(
host=host, cmd=cmd, # out=out, err=err, in_=in_, exit=exit,
)
self.expect_session(session)

def expect_session(self, session):
# First, stop the default session to clean up its state, if it seems to
# be running.
self.stop()
# Update sessions list with new session
self.sessions = [session]
# And start patching again
self.start()

def start(self):
"""
@@ -306,6 +321,9 @@ def stop(self):
"""
Stop patching SSHClient.
"""
# Short circuit if we don't seem to have start()ed yet.
if not hasattr(self, 'patcher'):
return
# Stop patching SSHClient
self.patcher.stop()

@@ -318,6 +336,22 @@ def sanity(self):
session.sanity_check()


@fixture
def remote():
"""
Fixture allowing setup of a mocked remote session & access to sub-mocks.
Yields a `MockRemote` object (which may need to be updated via
`MockRemote.expect_session`; otherwise a default session will be used) &
calls `MockRemote.stop` on teardown.
"""
# TODO: how to disable capturing inside here, or otherwise tell pytest not
# to raise its frickin IOError?
remote = MockRemote()
yield remote
remote.stop()


def mock_remote(*sessions):
"""
Mock & expect one or more remote connections & command executions.
@@ -472,19 +506,24 @@ def wrapper(self, **kwargs):
# TODO: mostly copied from invoke's suite; unify sometime
support = os.path.join(os.path.dirname(__file__), '_support')

class IntegrationSpec(Spec):
def setup(self):
# Preserve environment for later restore
self.old_environ = os.environ.copy()

def teardown(self):
# Nuke changes to environ
os.environ.clear()
os.environ.update(self.old_environ)
# Strip any test-support task collections from sys.modules to prevent
# state bleed between tests; otherwise tests can incorrectly pass
# despite not explicitly loading/cd'ing to get the tasks they call
# loaded.
for name, module in iteritems(sys.modules.copy()):
if module and support in getattr(module, '__file__', ''):
del sys.modules[name]
#class IntegrationSpec(Spec):
# def setup(self):
# # TODO: move that environ fixture in relaxed's own test suite to be
# # part of its public API, then apply fixture to all tests doing environ
# # shit.
# # Preserve environment for later restore
# self.old_environ = os.environ.copy()
#
# def teardown(self):
# # Nuke changes to environ
# os.environ.clear()
# os.environ.update(self.old_environ)
# # TODO: make this another fixture? global setup/teardown? what is
# # 'pytest-y' for this?
# # Strip any test-support task collections from sys.modules to prevent
# # state bleed between tests; otherwise tests can incorrectly pass
# # despite not explicitly loading/cd'ing to get the tasks they call
# # loaded.
# for name, module in iteritems(sys.modules.copy()):
# if module and support in getattr(module, '__file__', ''):
# del sys.modules[name]
@@ -0,0 +1,9 @@
#
# Settings overrides for test-executed Invoke code. Test code typically tries
# specifying this via the -f CLI flag or the runtime arguments to Config().
#

run:
# Disable all stdin mirroring by default. Otherwise, pytest's capture plugin
# gets all upset. It looks difficult to change that, too.
in_stream: false
Oops, something went wrong.

0 comments on commit 1171a02

Please sign in to comment.
You can’t perform that action at this time.
You signed in with another tab or window. Reload to refresh your session. You signed out in another tab or window. Reload to refresh your session.