mirror of
https://github.com/GothenburgBitFactory/taskwarrior.git
synced 2025-08-19 00:43:07 +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 tempfile
|
||||||
import shutil
|
import shutil
|
||||||
import signal
|
import signal
|
||||||
|
import atexit
|
||||||
from time import sleep
|
from time import sleep
|
||||||
from subprocess import Popen
|
from subprocess import Popen
|
||||||
from .utils import find_unused_port, release_port, port_used, run_cmd_wait
|
from .utils import find_unused_port, release_port, port_used, run_cmd_wait
|
||||||
|
from .exceptions import CommandError
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from subprocess import DEVNULL
|
from subprocess import DEVNULL
|
||||||
|
@ -40,17 +42,21 @@ class Taskd(object):
|
||||||
:arg address: Address to bind to
|
:arg address: Address to bind to
|
||||||
"""
|
"""
|
||||||
self.taskd = taskd
|
self.taskd = taskd
|
||||||
|
self.usercount = 0
|
||||||
|
|
||||||
# Will hold the taskd subprocess if it's running
|
# Will hold the taskd subprocess if it's running
|
||||||
self.proc = None
|
self.proc = None
|
||||||
self.datadir = tempfile.mkdtemp()
|
self.datadir = tempfile.mkdtemp()
|
||||||
self.tasklog = os.path.join(self.datadir, "taskd.log")
|
self.tasklog = os.path.join(self.datadir, "taskd.log")
|
||||||
self.taskpid = os.path.join(self.datadir, "taskd.pid")
|
self.taskpid = os.path.join(self.datadir, "taskd.pid")
|
||||||
|
|
||||||
# Make sure no TASKDDATA is defined
|
# Ensure any instance is properly destroyed at session end
|
||||||
try:
|
atexit.register(lambda: self.destroy())
|
||||||
del os.environ["TASKDDATA"]
|
|
||||||
except KeyError:
|
# Copy all env variables to avoid clashing subprocess environments
|
||||||
pass
|
self.env = os.environ.copy()
|
||||||
|
# Make sure TASKDDATA points to the temporary folder
|
||||||
|
self.env["TASKDATA"] = self.datadir
|
||||||
|
|
||||||
if certpath is None:
|
if certpath is None:
|
||||||
certpath = DEFAULT_CERT_PATH
|
certpath = DEFAULT_CERT_PATH
|
||||||
|
@ -69,7 +75,7 @@ class Taskd(object):
|
||||||
|
|
||||||
# Initialize taskd
|
# Initialize taskd
|
||||||
cmd = (self.taskd, "init", "--data", self.datadir)
|
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("server", "{0}:{1}".format(self.address, self.port))
|
||||||
self.config("log", self.tasklog)
|
self.config("log", self.tasklog)
|
||||||
|
@ -85,12 +91,71 @@ class Taskd(object):
|
||||||
self.config("server.crl", self.server_crl)
|
self.config("server.crl", self.server_crl)
|
||||||
self.config("ca.cert", self.ca_cert)
|
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):
|
def config(self, var, value):
|
||||||
"""Run setup `var` as `value` in taskd config
|
"""Run setup `var` as `value` in taskd config
|
||||||
"""
|
"""
|
||||||
cmd = (self.taskd, "config", "--force", "--data", self.datadir, var,
|
cmd = (self.taskd, "config", "--force", "--data", self.datadir, var,
|
||||||
value)
|
value)
|
||||||
run_cmd_wait(cmd)
|
run_cmd_wait(cmd, env=self.env)
|
||||||
|
|
||||||
# If server is running send a SIGHUP to force config reload
|
# If server is running send a SIGHUP to force config reload
|
||||||
if self.proc is not None:
|
if self.proc is not None:
|
||||||
|
@ -122,7 +187,7 @@ class Taskd(object):
|
||||||
"""
|
"""
|
||||||
if self.proc is None:
|
if self.proc is None:
|
||||||
cmd = (self.taskd, "server", "--data", self.datadir)
|
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:
|
else:
|
||||||
raise OSError("Taskd server is still running or crashed")
|
raise OSError("Taskd server is still running or crashed")
|
||||||
|
|
||||||
|
@ -186,7 +251,10 @@ class Taskd(object):
|
||||||
self.start = self.__destroyed
|
self.start = self.__destroyed
|
||||||
self.config = self.__destroyed
|
self.config = self.__destroyed
|
||||||
self.stop = 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):
|
def __destroyed(self, *args, **kwargs):
|
||||||
raise AttributeError("Taskd instance has been destroyed. "
|
raise AttributeError("Taskd instance has been destroyed. "
|
||||||
|
|
|
@ -1,19 +1,49 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
import os
|
||||||
import socket
|
import socket
|
||||||
from subprocess import Popen, PIPE
|
from subprocess import Popen, PIPE, STDOUT
|
||||||
|
from .exceptions import CommandError
|
||||||
|
|
||||||
USED_PORTS = set()
|
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"
|
"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:
|
if p.returncode != 0:
|
||||||
raise IOError("Failed to run '{0}', exit code was '{1}', stdout"
|
raise CommandError(cmd, p.returncode, out, err)
|
||||||
" '{2}' and stderr '{3}'".format(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):
|
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):
|
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
|
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
|
will rely entirely on the ability to connect to addr:port as detection
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue