mirror of
https://github.com/GothenburgBitFactory/taskwarrior.git
synced 2025-06-26 10:54:26 +02:00
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:
parent
733561863e
commit
715a414abd
4 changed files with 318 additions and 17 deletions
20
test/basetest/exceptions.py
Normal file
20
test/basetest/exceptions.py
Normal 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
183
test/basetest/task.py
Normal 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
|
|
@ -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. "
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue