UnitTests

* Taskd and Taskw classes for testing are now available
 * Testing of server and client can now be performed.
 * The newer test wrappers will eventually replace the BaseTest class
This commit is contained in:
Renato Alves 2014-07-06 02:03:04 +01:00
parent 733561863e
commit 715a414abd
4 changed files with 318 additions and 17 deletions

View file

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
class CommandError(Exception):
def __init__(self, cmd, code, out, err, msg=None):
if msg is None:
self.msg = ("Command '{0}' finished with unexpected exit code "
"'{1}':\nStdout: '{2}'\nStderr: '{3}'")
else:
self.msg = msg
self.cmd = cmd
self.out = out
self.err = err
self.code = code
def __str__(self):
return self.msg.format(self.cmd, self.code, self.out, self.err)
# vim: ai sts=4 et sw=4

183
test/basetest/task.py Normal file
View file

@ -0,0 +1,183 @@
# -*- coding: utf-8 -*-
import os
import tempfile
import shutil
import atexit
from .utils import run_cmd_wait, run_cmd_wait_nofail
from .exceptions import CommandError
class Task(object):
"""Manage a task warrior instance
A temporary folder is used as data store of task warrior.
This class can be instanciated multiple times if multiple taskw clients are
needed.
This class can be given a Taskd instance for simplified configuration.
A taskw client should not be used after being destroyed.
"""
def __init__(self, taskw="task", taskd=None):
"""Initialize a Task warrior (client) that can interact with a taskd
server. The task client runs in a temporary folder.
:arg taskw: Task binary to use as client (defaults: task in PATH)
:arg taskd: Taskd instance for client-server configuration
"""
self.taskw = taskw
self.taskd = taskd
# Configuration of the isolated environment
self._original_pwd = os.getcwd()
self.datadir = tempfile.mkdtemp()
self.taskrc = os.path.join(self.datadir, "test.rc")
# Ensure any instance is properly destroyed at session end
atexit.register(lambda: self.destroy())
# Copy all env variables to avoid clashing subprocess environments
self.env = os.environ.copy()
# Make sure no TASKDDATA is isolated
self.env["TASKDATA"] = self.datadir
# As well as TASKRC
self.env["TASKRC"] = self.taskrc
# Cannot call self.config until confirmation is disabled
with open(self.taskrc, 'w') as rc:
rc.write("data.location={0}\n"
"confirmation=no".format(self.datadir))
# Setup configuration to talk to taskd automatically
if self.taskd is not None:
self.bind_taskd_server(self.taskd)
def __repr__(self):
txt = super(Task, self).__repr__()
return "{0} running from {1}>".format(txt[:-1], self.datadir)
def bind_taskd_server(self, taskd):
"""Configure the present task client to talk to given taskd server
Note that this can be performed automatically by passing taskd when
creating an instance of the current class.
"""
self.taskd = taskd
cert = os.path.join(self.taskd.certpath, "test_client.cert.pem")
key = os.path.join(self.taskd.certpath, "test_client.key.pem")
self.config("taskd.certificate", cert)
self.config("taskd.key", key)
self.config("taskd.ca", self.taskd.ca_cert)
address = ":".join((self.taskd.address, str(self.taskd.port)))
self.config("taskd.server", address)
# Also configure the default user for given taskd server
self.set_taskd_user()
def set_taskd_user(self, taskd_user=None, default=True):
"""Assign a new user user to the present task client
If default==False, a new user will be assigned instead of reusing the
default taskd user for the corresponding instance.
"""
if taskd_user is None:
if default:
user, group, org, userkey = self.taskd.default_user
else:
user, group, org, userkey = self.taskd.create_user()
else:
user, group, org, userkey = taskd_user
self.credentials = "/".join((org, user, userkey))
self.config("taskd.credentials", self.credentials)
def config(self, var, value):
"""Run setup `var` as `value` in taskd config
"""
# Add -- to avoid misinterpretation of - in things like UUIDs
cmd = (self.taskw, "config", "--", var, value)
return run_cmd_wait(cmd, env=self.env)
def runSuccess(self, args=(), input=None, merge_streams=True):
"""Invoke task with the given arguments
Use runError if you want exit_code to be tested automatically and
*not* fail if program finishes abnormally.
If you wish to pass instructions to task such as confirmations or other
input via stdin, you can do so by providing a input string.
Such as input="y\ny".
If merge_streams=True stdout and stderr will be merged into stdout.
Returns (exit_code, stdout, stderr)
"""
command = [self.taskw]
command.extend(args)
return run_cmd_wait(command, input,
merge_streams=merge_streams, env=self.env)
def runError(self, args=(), input=None, merge_streams=True):
"""Same as runSuccess but Invoke task with the given arguments
Use runSuccess if you want exit_code to be tested automatically and
*fail* if program finishes abnormally.
If you wish to pass instructions to task such as confirmations or other
input via stdin, you can do so by providing a input string.
Such as input="y\ny".
If merge_streams=True stdout and stderr will be merged into stdout.
Returns (exit_code, stdout, stderr)
"""
command = [self.taskw]
command.extend(args)
output = run_cmd_wait_nofail(command, input,
merge_streams=merge_streams, env=self.env)
# output[0] is the exit code
if output[0] == 0:
raise CommandError(command, *output)
return output
def destroy(self):
"""Cleanup the data folder and release server port for other instances
"""
try:
shutil.rmtree(self.datadir)
except OSError as e:
if e.errno == 2:
# Directory no longer exists
pass
else:
raise
# Prevent future reuse of this instance
self.runSuccess = self.__destroyed
self.runError = self.__destroyed
# self.destroy will get called when the python session closes.
# If self.destroy was already called, turn the action into a noop
self.destroy = lambda: None
def __destroyed(self, *args, **kwargs):
raise AttributeError("Task instance has been destroyed. "
"Create a new instance if you need a new client.")
def diag(self, out):
"""Diagnostics are just lines preceded with #.
"""
print '# --- diag start ---'
for line in out.split("\n"):
print '#', line
print '# --- diag end ---'
# vim: ai sts=4 et sw=4

View file

@ -4,9 +4,11 @@ import os
import tempfile
import shutil
import signal
import atexit
from time import sleep
from subprocess import Popen
from .utils import find_unused_port, release_port, port_used, run_cmd_wait
from .exceptions import CommandError
try:
from subprocess import DEVNULL
@ -40,17 +42,21 @@ class Taskd(object):
:arg address: Address to bind to
"""
self.taskd = taskd
self.usercount = 0
# Will hold the taskd subprocess if it's running
self.proc = None
self.datadir = tempfile.mkdtemp()
self.tasklog = os.path.join(self.datadir, "taskd.log")
self.taskpid = os.path.join(self.datadir, "taskd.pid")
# Make sure no TASKDDATA is defined
try:
del os.environ["TASKDDATA"]
except KeyError:
pass
# Ensure any instance is properly destroyed at session end
atexit.register(lambda: self.destroy())
# Copy all env variables to avoid clashing subprocess environments
self.env = os.environ.copy()
# Make sure TASKDDATA points to the temporary folder
self.env["TASKDATA"] = self.datadir
if certpath is None:
certpath = DEFAULT_CERT_PATH
@ -69,7 +75,7 @@ class Taskd(object):
# Initialize taskd
cmd = (self.taskd, "init", "--data", self.datadir)
run_cmd_wait(cmd)
run_cmd_wait(cmd, env=self.env)
self.config("server", "{0}:{1}".format(self.address, self.port))
self.config("log", self.tasklog)
@ -85,12 +91,71 @@ class Taskd(object):
self.config("server.crl", self.server_crl)
self.config("ca.cert", self.ca_cert)
self.default_user = self.create_user()
def __repr__(self):
txt = super(Taskd, self).__repr__()
return "{0} running from {1}>".format(txt[:-1], self.datadir)
def create_user(self, user=None, group=None, org=None):
"""Create a user/group in the server and return the user
credentials to use in a taskw client.
"""
if user is None:
# Create a unique user ID
uid = self.usercount
user = "test_user_{0}".format(uid)
# Increment the user_id
self.usercount += 1
if group is None:
group = "default_group"
if org is None:
org = "default_org"
self._add_entity("org", org, ignore_exists=True)
self._add_entity("group", org, group, ignore_exists=True)
userkey = self._add_entity("user", org, user)
return user, group, org, userkey
def _add_entity(self, keyword, org, value=None, ignore_exists=False):
"""Add an organization, group or user to the current server
If a user creation is requested, the user unique ID is returned
"""
cmd = (self.taskd, "add", "--data", self.datadir, keyword, org)
if value is not None:
cmd += (value,)
try:
code, out, err = run_cmd_wait(cmd, env=self.env)
except CommandError as e:
match = False
for line in e.out.splitlines():
if line.endswith("already exists.") and ignore_exists:
match = True
break
# If the error was not "Already exists" report it
if not match:
raise
if keyword == "user":
expected = "New user key: "
for line in out.splitlines():
if line.startswith(expected):
return line.replace(expected, '')
def config(self, var, value):
"""Run setup `var` as `value` in taskd config
"""
cmd = (self.taskd, "config", "--force", "--data", self.datadir, var,
value)
run_cmd_wait(cmd)
run_cmd_wait(cmd, env=self.env)
# If server is running send a SIGHUP to force config reload
if self.proc is not None:
@ -122,7 +187,7 @@ class Taskd(object):
"""
if self.proc is None:
cmd = (self.taskd, "server", "--data", self.datadir)
self.proc = Popen(cmd, stdout=DEVNULL, stdin=DEVNULL)
self.proc = Popen(cmd, stdout=DEVNULL, stdin=DEVNULL, env=self.env)
else:
raise OSError("Taskd server is still running or crashed")
@ -186,7 +251,10 @@ class Taskd(object):
self.start = self.__destroyed
self.config = self.__destroyed
self.stop = self.__destroyed
self.destroy = self.__destroyed
# self.destroy will get called when the python session closes.
# If self.destroy was already called, turn the action into a noop
self.destroy = lambda: None
def __destroyed(self, *args, **kwargs):
raise AttributeError("Taskd instance has been destroyed. "

View file

@ -1,19 +1,49 @@
# -*- coding: utf-8 -*-
import os
import socket
from subprocess import Popen, PIPE
from subprocess import Popen, PIPE, STDOUT
from .exceptions import CommandError
USED_PORTS = set()
def run_cmd_wait(cmd):
def run_cmd_wait(cmd, input=None, stdout=PIPE, stderr=PIPE,
merge_streams=False, env=os.environ):
"Run a subprocess and wait for it to finish"
p = Popen(cmd, stdout=PIPE, stderr=PIPE)
out, err = p.communicate()
if input is None:
stdin = None
else:
stdin = PIPE
if merge_streams:
stderr = STDOUT
else:
stderr = PIPE
p = Popen(cmd, stdin=stdin, stdout=stdout, stderr=stderr, env=env)
out, err = p.communicate(input)
# In python3 we will be able use the following instead of the previous
# line to avoid locking if task is unexpectedly waiting for input
# try:
# out, err = p.communicate(input, timeout=15)
# except TimeoutExpired:
# p.kill()
# out, err = proc.communicate()
if p.returncode != 0:
raise IOError("Failed to run '{0}', exit code was '{1}', stdout"
" '{2}' and stderr '{3}'".format(cmd, p.returncode,
out, err))
raise CommandError(cmd, p.returncode, out, err)
return p.returncode, out, err
def run_cmd_wait_nofail(*args, **kwargs):
"Same as run_cmd_wait but silence the exception if it happens"
try:
return run_cmd_wait(*args, **kwargs)
except CommandError as e:
return e.code, e.out, e.err
def port_used(addr="localhost", port=None):
@ -29,7 +59,7 @@ def port_used(addr="localhost", port=None):
def find_unused_port(addr="localhost", start=53589, track=True):
"""Find an unused port starting at `port`
"""Find an unused port starting at `start` port
If track=False the returned port will not be marked as in-use and the code
will rely entirely on the ability to connect to addr:port as detection