mirror of
https://gitlab.isc.org/isc-projects/kea
synced 2025-09-03 15:35:17 +00:00
Merge branch 'trac1290'
This commit is contained in:
@@ -991,6 +991,7 @@ AC_OUTPUT([doc/version.ent
|
|||||||
src/lib/util/python/mkpywrapper.py
|
src/lib/util/python/mkpywrapper.py
|
||||||
src/lib/util/python/gen_wiredata.py
|
src/lib/util/python/gen_wiredata.py
|
||||||
src/lib/server_common/tests/data_path.h
|
src/lib/server_common/tests/data_path.h
|
||||||
|
tests/lettuce/setup_intree_bind10.sh
|
||||||
tests/system/conf.sh
|
tests/system/conf.sh
|
||||||
tests/system/run.sh
|
tests/system/run.sh
|
||||||
tests/system/glue/setup.sh
|
tests/system/glue/setup.sh
|
||||||
|
@@ -675,6 +675,8 @@ class BoB:
|
|||||||
args = ["b10-cmdctl"]
|
args = ["b10-cmdctl"]
|
||||||
if self.cmdctl_port is not None:
|
if self.cmdctl_port is not None:
|
||||||
args.append("--port=" + str(self.cmdctl_port))
|
args.append("--port=" + str(self.cmdctl_port))
|
||||||
|
if self.verbose:
|
||||||
|
args.append("-v")
|
||||||
self.start_process("b10-cmdctl", args, c_channel_env, self.cmdctl_port)
|
self.start_process("b10-cmdctl", args, c_channel_env, self.cmdctl_port)
|
||||||
|
|
||||||
def start_all_processes(self):
|
def start_all_processes(self):
|
||||||
|
@@ -45,6 +45,5 @@ export B10_FROM_BUILD
|
|||||||
BIND10_MSGQ_SOCKET_FILE=@abs_top_builddir@/msgq_socket
|
BIND10_MSGQ_SOCKET_FILE=@abs_top_builddir@/msgq_socket
|
||||||
export BIND10_MSGQ_SOCKET_FILE
|
export BIND10_MSGQ_SOCKET_FILE
|
||||||
|
|
||||||
cd ${BIND10_PATH}
|
exec ${PYTHON_EXEC} -O ${BIND10_PATH}/bind10 "$@"
|
||||||
exec ${PYTHON_EXEC} -O bind10 "$@"
|
|
||||||
|
|
||||||
|
@@ -133,19 +133,23 @@ class BindCmdInterpreter(Cmd):
|
|||||||
'''Parse commands from user and send them to cmdctl. '''
|
'''Parse commands from user and send them to cmdctl. '''
|
||||||
try:
|
try:
|
||||||
if not self.login_to_cmdctl():
|
if not self.login_to_cmdctl():
|
||||||
return
|
return 1
|
||||||
|
|
||||||
self.cmdloop()
|
self.cmdloop()
|
||||||
print('\nExit from bindctl')
|
print('\nExit from bindctl')
|
||||||
|
return 0
|
||||||
except FailToLogin as err:
|
except FailToLogin as err:
|
||||||
# error already printed when this was raised, ignoring
|
# error already printed when this was raised, ignoring
|
||||||
pass
|
return 1
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
print('\nExit from bindctl')
|
print('\nExit from bindctl')
|
||||||
|
return 0
|
||||||
except socket.error as err:
|
except socket.error as err:
|
||||||
print('Failed to send request, the connection is closed')
|
print('Failed to send request, the connection is closed')
|
||||||
|
return 1
|
||||||
except http.client.CannotSendRequest:
|
except http.client.CannotSendRequest:
|
||||||
print('Can not send request, the connection is busy')
|
print('Can not send request, the connection is busy')
|
||||||
|
return 1
|
||||||
|
|
||||||
def _get_saved_user_info(self, dir, file_name):
|
def _get_saved_user_info(self, dir, file_name):
|
||||||
''' Read all the available username and password pairs saved in
|
''' Read all the available username and password pairs saved in
|
||||||
@@ -523,6 +527,7 @@ class BindCmdInterpreter(Cmd):
|
|||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def _get_module_startswith(self, text):
|
def _get_module_startswith(self, text):
|
||||||
return [module
|
return [module
|
||||||
for module in self.modules
|
for module in self.modules
|
||||||
|
@@ -146,4 +146,5 @@ if __name__ == '__main__':
|
|||||||
tool = BindCmdInterpreter(server_addr, pem_file=options.cert_chain,
|
tool = BindCmdInterpreter(server_addr, pem_file=options.cert_chain,
|
||||||
csv_file_dir=options.csv_file_dir)
|
csv_file_dir=options.csv_file_dir)
|
||||||
prepare_config_commands(tool)
|
prepare_config_commands(tool)
|
||||||
tool.run()
|
result = tool.run()
|
||||||
|
sys.exit(result)
|
||||||
|
@@ -342,7 +342,7 @@ class TestConfigCommands(unittest.TestCase):
|
|||||||
# validate log message for socket.err
|
# validate log message for socket.err
|
||||||
socket_err_output = io.StringIO()
|
socket_err_output = io.StringIO()
|
||||||
sys.stdout = socket_err_output
|
sys.stdout = socket_err_output
|
||||||
self.assertRaises(None, self.tool.run())
|
self.assertEqual(1, self.tool.run())
|
||||||
self.assertEqual("Failed to send request, the connection is closed\n",
|
self.assertEqual("Failed to send request, the connection is closed\n",
|
||||||
socket_err_output.getvalue())
|
socket_err_output.getvalue())
|
||||||
socket_err_output.close()
|
socket_err_output.close()
|
||||||
@@ -350,7 +350,7 @@ class TestConfigCommands(unittest.TestCase):
|
|||||||
# validate log message for http.client.CannotSendRequest
|
# validate log message for http.client.CannotSendRequest
|
||||||
cannot_send_output = io.StringIO()
|
cannot_send_output = io.StringIO()
|
||||||
sys.stdout = cannot_send_output
|
sys.stdout = cannot_send_output
|
||||||
self.assertRaises(None, self.tool.run())
|
self.assertEqual(1, self.tool.run())
|
||||||
self.assertEqual("Can not send request, the connection is busy\n",
|
self.assertEqual("Can not send request, the connection is busy\n",
|
||||||
cannot_send_output.getvalue())
|
cannot_send_output.getvalue())
|
||||||
cannot_send_output.close()
|
cannot_send_output.close()
|
||||||
|
@@ -460,6 +460,8 @@ class SecureHTTPServer(socketserver_mixin.NoPollMixIn,
|
|||||||
socketserver_mixin.NoPollMixIn.__init__(self)
|
socketserver_mixin.NoPollMixIn.__init__(self)
|
||||||
try:
|
try:
|
||||||
http.server.HTTPServer.__init__(self, server_address, RequestHandlerClass)
|
http.server.HTTPServer.__init__(self, server_address, RequestHandlerClass)
|
||||||
|
logger.debug(DBG_CMDCTL_MESSAGING, CMDCTL_STARTED,
|
||||||
|
server_address[0], server_address[1])
|
||||||
except socket.error as err:
|
except socket.error as err:
|
||||||
raise CmdctlException("Error creating server, because: %s \n" % str(err))
|
raise CmdctlException("Error creating server, because: %s \n" % str(err))
|
||||||
|
|
||||||
@@ -566,10 +568,9 @@ def set_signal_handler():
|
|||||||
|
|
||||||
def run(addr = 'localhost', port = 8080, idle_timeout = 1200, verbose = False):
|
def run(addr = 'localhost', port = 8080, idle_timeout = 1200, verbose = False):
|
||||||
''' Start cmdctl as one https server. '''
|
''' Start cmdctl as one https server. '''
|
||||||
if verbose:
|
|
||||||
sys.stdout.write("[b10-cmdctl] starting on %s port:%d\n" %(addr, port))
|
|
||||||
httpd = SecureHTTPServer((addr, port), SecureHTTPRequestHandler,
|
httpd = SecureHTTPServer((addr, port), SecureHTTPRequestHandler,
|
||||||
CommandControl, idle_timeout, verbose)
|
CommandControl, idle_timeout, verbose)
|
||||||
|
|
||||||
httpd.serve_forever()
|
httpd.serve_forever()
|
||||||
|
|
||||||
def check_port(option, opt_str, value, parser):
|
def check_port(option, opt_str, value, parser):
|
||||||
@@ -607,6 +608,8 @@ if __name__ == '__main__':
|
|||||||
(options, args) = parser.parse_args()
|
(options, args) = parser.parse_args()
|
||||||
result = 1 # in case of failure
|
result = 1 # in case of failure
|
||||||
try:
|
try:
|
||||||
|
if options.verbose:
|
||||||
|
logger.set_severity("DEBUG", 99)
|
||||||
run(options.addr, options.port, options.idle_timeout, options.verbose)
|
run(options.addr, options.port, options.idle_timeout, options.verbose)
|
||||||
result = 0
|
result = 0
|
||||||
except isc.cc.SessionError as err:
|
except isc.cc.SessionError as err:
|
||||||
|
@@ -64,6 +64,9 @@ be set up. The specific error is given in the log message. Possible
|
|||||||
causes may be that the ssl request itself was bad, or the local key or
|
causes may be that the ssl request itself was bad, or the local key or
|
||||||
certificate file could not be read.
|
certificate file could not be read.
|
||||||
|
|
||||||
|
% CMDCTL_STARTED cmdctl is listening for connections on %1:%2
|
||||||
|
The cmdctl daemon has started and is now listening for connections.
|
||||||
|
|
||||||
% CMDCTL_STOPPED_BY_KEYBOARD keyboard interrupt, shutting down
|
% CMDCTL_STOPPED_BY_KEYBOARD keyboard interrupt, shutting down
|
||||||
There was a keyboard interrupt signal to stop the cmdctl daemon. The
|
There was a keyboard interrupt signal to stop the cmdctl daemon. The
|
||||||
daemon will now shut down.
|
daemon will now shut down.
|
||||||
|
@@ -18,6 +18,7 @@ import struct
|
|||||||
import os
|
import os
|
||||||
import copy
|
import copy
|
||||||
import subprocess
|
import subprocess
|
||||||
|
import copy
|
||||||
from isc.log_messages.bind10_messages import *
|
from isc.log_messages.bind10_messages import *
|
||||||
from libutil_io_python import recv_fd
|
from libutil_io_python import recv_fd
|
||||||
|
|
||||||
|
@@ -123,6 +123,7 @@ class ConfigManagerData:
|
|||||||
output_file_name is not specified, the file used in
|
output_file_name is not specified, the file used in
|
||||||
read_from_file is used."""
|
read_from_file is used."""
|
||||||
filename = None
|
filename = None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
file = tempfile.NamedTemporaryFile(mode='w',
|
file = tempfile.NamedTemporaryFile(mode='w',
|
||||||
prefix="b10-config.db.",
|
prefix="b10-config.db.",
|
||||||
|
@@ -37,7 +37,7 @@ class TestConfigManagerData(unittest.TestCase):
|
|||||||
It shouldn't append the data path to it.
|
It shouldn't append the data path to it.
|
||||||
"""
|
"""
|
||||||
abs_path = self.data_path + os.sep + "b10-config-imaginary.db"
|
abs_path = self.data_path + os.sep + "b10-config-imaginary.db"
|
||||||
data = ConfigManagerData(os.getcwd(), abs_path)
|
data = ConfigManagerData(self.data_path, abs_path)
|
||||||
self.assertEqual(abs_path, data.db_filename)
|
self.assertEqual(abs_path, data.db_filename)
|
||||||
self.assertEqual(self.data_path, data.data_path)
|
self.assertEqual(self.data_path, data.data_path)
|
||||||
|
|
||||||
|
127
tests/lettuce/README
Normal file
127
tests/lettuce/README
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
BIND10 system testing with Lettuce
|
||||||
|
or: to BDD or not to BDD
|
||||||
|
|
||||||
|
In this directory, we define a set of behavioral tests for BIND 10. Currently,
|
||||||
|
these tests are specific for BIND10, but we are keeping in mind that RFC-related
|
||||||
|
tests could be separated, so that we can test other systems as well.
|
||||||
|
|
||||||
|
Prerequisites:
|
||||||
|
- Installed version of BIND 10 (but see below how to run it from source tree)
|
||||||
|
- dig
|
||||||
|
- lettuce (http://lettuce.it)
|
||||||
|
|
||||||
|
To install lettuce, if you have the python pip installation tool, simply do
|
||||||
|
pip install lettuce
|
||||||
|
See http://lettuce.it/intro/install.html
|
||||||
|
|
||||||
|
Most systems have the pip tool in a separate package; on Debian-based systems
|
||||||
|
it is called python-pip. On FreeBSD the port is devel/py-pip.
|
||||||
|
|
||||||
|
Running the tests
|
||||||
|
-----------------
|
||||||
|
|
||||||
|
At this moment, we have a fixed port for local tests in our setups, port 47806.
|
||||||
|
This port must be free. (TODO: can we make this run-time discovered?).
|
||||||
|
Port 47805 is used for cmdctl, and must also be available.
|
||||||
|
(note, we will need to extend this to a range, or if possible, we will need to
|
||||||
|
do some on-the-fly available port finding)
|
||||||
|
|
||||||
|
The bind10 main program, bindctl, and dig must all be in the default search
|
||||||
|
path of your environment, and BIND 10 must not be running if you use the
|
||||||
|
installed version when you run the tests.
|
||||||
|
|
||||||
|
If you want to test an installed version of bind 10, just run 'lettuce' in
|
||||||
|
this directory.
|
||||||
|
|
||||||
|
We have provided a script that sets up the shell environment to run the tests
|
||||||
|
with the build tree version of bind. If your shell uses export to set
|
||||||
|
environment variables, you can source the script setup_intree_bind10.sh, then
|
||||||
|
run lettuce.
|
||||||
|
|
||||||
|
Due to the default way lettuce prints its output, it is advisable to run it
|
||||||
|
in a terminal that is wide than the default. If you see a lot of lines twice
|
||||||
|
in different colors, the terminal is not wide enough.
|
||||||
|
|
||||||
|
If you just want to run one specific feature test, use
|
||||||
|
lettuce features/<feature file>
|
||||||
|
|
||||||
|
To run a specific scenario from a feature, use
|
||||||
|
lettuce features/<feature file> -s <scenario number>
|
||||||
|
|
||||||
|
We have set up the tests to assume that lettuce is run from this directory,
|
||||||
|
so even if you specify a specific feature file, you should do it from this
|
||||||
|
directory.
|
||||||
|
|
||||||
|
What to do when a test fails
|
||||||
|
----------------------------
|
||||||
|
|
||||||
|
First of all, look at the error it printed and see what step it occurred in.
|
||||||
|
If written well, the output should explain most of what went wrong.
|
||||||
|
|
||||||
|
The stacktrace that is printed is *not* of bind10, but of the testing
|
||||||
|
framework; this helps in finding more information about what exactly the test
|
||||||
|
tried to achieve when it failed (as well as help debug the tests themselves).
|
||||||
|
|
||||||
|
Furthermore, if any scenario fails, the output from long-running processes
|
||||||
|
will be stored in the directory output/. The name of the files will be
|
||||||
|
<Feature name>-<Scenario name>-<Process name>.stdout and
|
||||||
|
<Feature name>-<Scenario name>-<Process name>.stderr
|
||||||
|
Where spaces and other non-standard characters are replaced by an underscore.
|
||||||
|
The process name is either the standard name for said process (e.g. 'bind10'),
|
||||||
|
or the name given to it by the test ('when i run bind10 as <name>').
|
||||||
|
|
||||||
|
These files *will* be overwritten or deleted if the same scenarios are run
|
||||||
|
again, so if you want to inspect them after a failed test, either do so
|
||||||
|
immediately or move the files.
|
||||||
|
|
||||||
|
Adding and extending tests
|
||||||
|
--------------------------
|
||||||
|
|
||||||
|
If you want to add tests, it is advisable to first go through the examples to
|
||||||
|
see what is possible, and read the documentation on http://www.lettuce.it
|
||||||
|
|
||||||
|
There is also a README.tutorial file here.
|
||||||
|
|
||||||
|
We have a couple of conventions to keep things manageable.
|
||||||
|
|
||||||
|
Configuration files go into the configurations/ directory.
|
||||||
|
Data files go into the data/ directory.
|
||||||
|
Step definition go into the features/terrain/ directory (the name terrain is
|
||||||
|
chosen for the same reason Lettuce chose terrain.py, this is the place the
|
||||||
|
tests 'live' in).
|
||||||
|
Feature definitions go directly into the features/ directory.
|
||||||
|
|
||||||
|
These directories are currently not divided further; we may want to consider
|
||||||
|
this as the set grows. Due to a (current?) limitation of Lettuce, for
|
||||||
|
feature files this is currently not possible; the python files containing
|
||||||
|
steps and terrain must be below or at the same level of the feature files.
|
||||||
|
|
||||||
|
Long-running processes should be started through the world.RunningProcesses
|
||||||
|
instance. If you want to add a process (e.g. bind9), create start, stop and
|
||||||
|
control steps in terrain/<base_name>_control.py, and let it use the
|
||||||
|
RunningProcesses API (defined in terrain.py). See bind10_control.py for an
|
||||||
|
example.
|
||||||
|
|
||||||
|
For sending queries and checking the results, steps have been defined in
|
||||||
|
terrain/querying.py. These use dig and store the results split up into text
|
||||||
|
strings. This is intentionally not parsed through our own library (as that way
|
||||||
|
we might run into a 'symmetric bug'). If you need something more advanced from
|
||||||
|
query results, define it here.
|
||||||
|
|
||||||
|
Some very general steps are defined in terrain/steps.py.
|
||||||
|
Initialization code, cleanup code, and helper classes are defined in
|
||||||
|
terrain/terrain.py.
|
||||||
|
|
||||||
|
To find the right steps, case insensitive matching is used. Parameters taken
|
||||||
|
from the steps are case-sensitive though. So a step defined as
|
||||||
|
'do foo with value (bar)' will be matched when using
|
||||||
|
'Do Foo with value xyz', but xyz will be taken as given.
|
||||||
|
|
||||||
|
If you need to add steps that are very particular to one test, create a new
|
||||||
|
file with a name relevant for that test in terrain. We may want to consider
|
||||||
|
creating a specific subdirectory for these, but at this moment it is unclear
|
||||||
|
whether we need to.
|
||||||
|
|
||||||
|
We should try to keep steps as general as possible, while not making them to
|
||||||
|
complex and error-prone.
|
||||||
|
|
157
tests/lettuce/README.tutorial
Normal file
157
tests/lettuce/README.tutorial
Normal file
@@ -0,0 +1,157 @@
|
|||||||
|
Quick tutorial and overview
|
||||||
|
---------------------------
|
||||||
|
|
||||||
|
Lettuce is a framework for doing Behaviour Driven Development (BDD).
|
||||||
|
|
||||||
|
The idea behind BDD is that you first write down your requirements in
|
||||||
|
the form of scenarios, then implement their behaviour.
|
||||||
|
|
||||||
|
We do not plan on doing full BDD, but such a system should also help
|
||||||
|
us make system tests. And, hopefully, being able to better identify
|
||||||
|
what exactly is going wrong when a test fails.
|
||||||
|
|
||||||
|
Lettuce is a python implementation of the Cucumber framework, which is
|
||||||
|
a ruby system. So far we chose lettuce because we already need python
|
||||||
|
anyway, so chances are higher that any system we want to run it on
|
||||||
|
supports it. It only supports a subset of cucumber, but more cucumber
|
||||||
|
features are planned. As I do not know much details of cucumber, I
|
||||||
|
can't really say what is there and what is not.
|
||||||
|
|
||||||
|
A slight letdown is that the current version does not support python 3.
|
||||||
|
However, as long as the tool-calling glue is python2, this should not
|
||||||
|
cause any problems, since these aren't unit tests; We do not plan to use
|
||||||
|
our libraries directly, but only through the runnable scripts and
|
||||||
|
executables.
|
||||||
|
|
||||||
|
-----
|
||||||
|
|
||||||
|
Features, Scenarios, Steps.
|
||||||
|
|
||||||
|
Lettuce makes a distinction between features, scenarios, and steps.
|
||||||
|
|
||||||
|
Features are general, well, features. Each 'feature' has its own file
|
||||||
|
ending in .feature. A feature file contains a description and a number
|
||||||
|
of scenarios. Each scenario tests one or more particular parts of the
|
||||||
|
feature. Each scenario consists of a number of steps.
|
||||||
|
|
||||||
|
So let's open up a simple one.
|
||||||
|
|
||||||
|
-- example.feature
|
||||||
|
Feature: showing off BIND 10
|
||||||
|
This is to show BIND 10 running and that it answer queries
|
||||||
|
|
||||||
|
Scenario: Starting bind10
|
||||||
|
# steps go here
|
||||||
|
--
|
||||||
|
|
||||||
|
I have predefined a number of steps we can use, as we build test we
|
||||||
|
will need to expand these, but we will look at them shortly.
|
||||||
|
|
||||||
|
This file defines a feature, just under the feature name we can
|
||||||
|
provide a description of the feature.
|
||||||
|
|
||||||
|
The one scenario we have no has no steps, so if we run it we should
|
||||||
|
see something like:
|
||||||
|
|
||||||
|
-- output
|
||||||
|
> lettuce
|
||||||
|
Feature: showing off BIND 10
|
||||||
|
This is to show BIND 10 running and that it answer queries
|
||||||
|
|
||||||
|
Scenario: Starting bind10
|
||||||
|
|
||||||
|
1 feature (1 passed)
|
||||||
|
1 scenario (1 passed)
|
||||||
|
0 step (0 passed)
|
||||||
|
--
|
||||||
|
|
||||||
|
Let's first add some steps that send queries.
|
||||||
|
|
||||||
|
--
|
||||||
|
A query for www.example.com should have rcode REFUSED
|
||||||
|
A query for www.example.org should have rcode NOERROR
|
||||||
|
--
|
||||||
|
|
||||||
|
Since we didn't start any bind10, dig will time out and the result
|
||||||
|
should be an error saying it got no answer. Errors are in the
|
||||||
|
form of stack traces (trigger by failed assertions), so we can find
|
||||||
|
out easily where in the tests they occurred. Especially when the total
|
||||||
|
set of steps gets bigger we might need that.
|
||||||
|
|
||||||
|
So let's add a step that starts bind10.
|
||||||
|
|
||||||
|
--
|
||||||
|
When I start bind10 with configuration example.org.config
|
||||||
|
--
|
||||||
|
|
||||||
|
This is not good enough; it will fire of the process, but setting up
|
||||||
|
b10-auth may take a few moments, so we need to add a step to wait for
|
||||||
|
it to be started before we continue.
|
||||||
|
|
||||||
|
--
|
||||||
|
Then wait for bind10 auth to start
|
||||||
|
--
|
||||||
|
|
||||||
|
And let's run the tests again.
|
||||||
|
|
||||||
|
--
|
||||||
|
> lettuce
|
||||||
|
|
||||||
|
Feature: showing off BIND 10
|
||||||
|
This is to show BIND 10 running and that it answer queries
|
||||||
|
|
||||||
|
Scenario: Starting bind10
|
||||||
|
When I start bind10 with configuration example.org.config
|
||||||
|
Then wait for bind10 auth to start
|
||||||
|
A query for www.example.com should have rcode REFUSED
|
||||||
|
A query for www.example.org should have rcode NOERROR
|
||||||
|
|
||||||
|
1 feature (1 passed)
|
||||||
|
1 scenario (1 passed)
|
||||||
|
4 steps (4 passed)
|
||||||
|
(finished within 2 seconds)
|
||||||
|
--
|
||||||
|
|
||||||
|
So take a look at one of those steps, let's pick the first one.
|
||||||
|
|
||||||
|
A step is defined through a python decorator, which in essence is a regular
|
||||||
|
expression; lettuce searches through all defined steps to find one that
|
||||||
|
matches. These are 'partial' matches (unless specified otherwise in the
|
||||||
|
regular expression itself), so if the step is defined with "do foo bar", the
|
||||||
|
scenario can add words for readability "When I do foo bar".
|
||||||
|
|
||||||
|
Each captured group will be passed as an argument to the function we define.
|
||||||
|
For bind10, i defined a configuration file, a cmdctl port, and a process
|
||||||
|
name. The first two should be self-evident, and the process name is an
|
||||||
|
optional name we give it, should we want to address it in the rest of the
|
||||||
|
tests. This is most useful if we want to start multiple instances. In the
|
||||||
|
next step (the wait for auth to start), I added a 'of <instance>'. So if we
|
||||||
|
define the bind10 'as b10_second_instance', we can specify that one here as
|
||||||
|
'of b10_second_instance'.
|
||||||
|
|
||||||
|
--
|
||||||
|
When I start bind10 with configuration second.config
|
||||||
|
with cmdctl port 12345 as b10_second_instance
|
||||||
|
--
|
||||||
|
(line wrapped for readability)
|
||||||
|
|
||||||
|
But notice how we needed two steps, which we probably always need (but
|
||||||
|
not entirely always)? We can also combine steps; for instance:
|
||||||
|
|
||||||
|
--
|
||||||
|
@step('have bind10 running(?: with configuration ([\w.]+))?')
|
||||||
|
def have_bind10_running(step, config_file):
|
||||||
|
step.given('start bind10 with configuration ' + config_file)
|
||||||
|
step.given('wait for bind10 auth to start')
|
||||||
|
--
|
||||||
|
|
||||||
|
Now we can replace the two steps with one:
|
||||||
|
|
||||||
|
--
|
||||||
|
Given I have bind10 running
|
||||||
|
--
|
||||||
|
|
||||||
|
That's it for the quick overview. For some more examples, with comments,
|
||||||
|
take a look at features/example.feature. You can read more about lettuce and
|
||||||
|
its features on http://www.lettuce.it, and if you plan on adding tests and
|
||||||
|
scenarios, please consult the last section of the main README first.
|
17
tests/lettuce/configurations/example.org.config.orig
Normal file
17
tests/lettuce/configurations/example.org.config.orig
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"version": 2,
|
||||||
|
"Logging": {
|
||||||
|
"loggers": [ {
|
||||||
|
"debuglevel": 99,
|
||||||
|
"severity": "DEBUG",
|
||||||
|
"name": "auth"
|
||||||
|
} ]
|
||||||
|
},
|
||||||
|
"Auth": {
|
||||||
|
"database_file": "data/example.org.sqlite3",
|
||||||
|
"listen_on": [ {
|
||||||
|
"port": 47806,
|
||||||
|
"address": "127.0.0.1"
|
||||||
|
} ]
|
||||||
|
}
|
||||||
|
}
|
18
tests/lettuce/configurations/example2.org.config
Normal file
18
tests/lettuce/configurations/example2.org.config
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"version": 2,
|
||||||
|
"Logging": {
|
||||||
|
"loggers": [ {
|
||||||
|
"severity": "DEBUG",
|
||||||
|
"name": "auth",
|
||||||
|
"debuglevel": 99
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Auth": {
|
||||||
|
"database_file": "data/example.org.sqlite3",
|
||||||
|
"listen_on": [ {
|
||||||
|
"port": 47807,
|
||||||
|
"address": "127.0.0.1"
|
||||||
|
} ]
|
||||||
|
}
|
||||||
|
}
|
10
tests/lettuce/configurations/no_db_file.config
Normal file
10
tests/lettuce/configurations/no_db_file.config
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"version": 2,
|
||||||
|
"Auth": {
|
||||||
|
"database_file": "data/test_nonexistent_db.sqlite3",
|
||||||
|
"listen_on": [ {
|
||||||
|
"port": 47806,
|
||||||
|
"address": "127.0.0.1"
|
||||||
|
} ]
|
||||||
|
}
|
||||||
|
}
|
BIN
tests/lettuce/data/empty_db.sqlite3
Normal file
BIN
tests/lettuce/data/empty_db.sqlite3
Normal file
Binary file not shown.
BIN
tests/lettuce/data/example.org.sqlite3
Normal file
BIN
tests/lettuce/data/example.org.sqlite3
Normal file
Binary file not shown.
142
tests/lettuce/features/example.feature
Normal file
142
tests/lettuce/features/example.feature
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
Feature: Example feature
|
||||||
|
This is an example Feature set. Is is mainly intended to show
|
||||||
|
our use of the lettuce tool and our own framework for it
|
||||||
|
The first scenario is to show what a simple test would look like, and
|
||||||
|
is intentionally uncommented.
|
||||||
|
The later scenarios have comments to show what the test steps do and
|
||||||
|
support
|
||||||
|
|
||||||
|
Scenario: A simple example
|
||||||
|
Given I have bind10 running with configuration example.org.config
|
||||||
|
A query for www.example.org should have rcode NOERROR
|
||||||
|
A query for www.doesnotexist.org should have rcode REFUSED
|
||||||
|
The SOA serial for example.org should be 1234
|
||||||
|
|
||||||
|
Scenario: New database
|
||||||
|
# This test checks whether a database file is automatically created
|
||||||
|
# Underwater, we take advantage of our intialization routines so
|
||||||
|
# that we are sure this file does not exist, see
|
||||||
|
# features/terrain/terrain.py
|
||||||
|
|
||||||
|
# Standard check to test (non-)existence of a file
|
||||||
|
# This file is actually automatically
|
||||||
|
The file data/test_nonexistent_db.sqlite3 should not exist
|
||||||
|
|
||||||
|
# In the first scenario, we used 'given I have bind10 running', which
|
||||||
|
# is actually a compound step consisting of the following two
|
||||||
|
# one to start the server
|
||||||
|
When I start bind10 with configuration no_db_file.config
|
||||||
|
# And one to wait until it reports that b10-auth has started
|
||||||
|
Then wait for bind10 auth to start
|
||||||
|
|
||||||
|
# This is a general step to stop a named process. By convention,
|
||||||
|
# the default name for any process is the same as the one we
|
||||||
|
# use in the start step (for bind 10, that is 'I start bind10 with')
|
||||||
|
# See scenario 'Multiple instances' for more.
|
||||||
|
Then stop process bind10
|
||||||
|
|
||||||
|
# Now we use the first step again to see if the file has been created
|
||||||
|
The file data/test_nonexistent_db.sqlite3 should exist
|
||||||
|
|
||||||
|
Scenario: example.org queries
|
||||||
|
# This scenario performs a number of queries and inspects the results
|
||||||
|
# Simple queries have already been show, but after we have sent a query,
|
||||||
|
# we can also do more extensive checks on the result.
|
||||||
|
# See querying.py for more information on these steps.
|
||||||
|
|
||||||
|
# note: lettuce can group similar checks by using tables, but we
|
||||||
|
# intentionally do not make use of that here
|
||||||
|
|
||||||
|
# This is a compound statement that starts and waits for the
|
||||||
|
# started message
|
||||||
|
Given I have bind10 running with configuration example.org.config
|
||||||
|
|
||||||
|
# Some simple queries that is not examined further
|
||||||
|
A query for www.example.com should have rcode REFUSED
|
||||||
|
A query for www.example.org should have rcode NOERROR
|
||||||
|
|
||||||
|
# A query where we look at some of the result properties
|
||||||
|
A query for www.example.org should have rcode NOERROR
|
||||||
|
The last query response should have qdcount 1
|
||||||
|
The last query response should have ancount 1
|
||||||
|
The last query response should have nscount 3
|
||||||
|
The last query response should have adcount 0
|
||||||
|
# The answer section can be inspected in its entirety; in the future
|
||||||
|
# we may add more granular inspection steps
|
||||||
|
The answer section of the last query response should be
|
||||||
|
"""
|
||||||
|
www.example.org. 3600 IN A 192.0.2.1
|
||||||
|
"""
|
||||||
|
|
||||||
|
A query for example.org type NS should have rcode NOERROR
|
||||||
|
The answer section of the last query response should be
|
||||||
|
"""
|
||||||
|
example.org. 3600 IN NS ns1.example.org.
|
||||||
|
example.org. 3600 IN NS ns2.example.org.
|
||||||
|
example.org. 3600 IN NS ns3.example.org.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# We have a specific step for checking SOA serial numbers
|
||||||
|
The SOA serial for example.org should be 1234
|
||||||
|
|
||||||
|
# Another query where we look at some of the result properties
|
||||||
|
A query for doesnotexist.example.org should have rcode NXDOMAIN
|
||||||
|
The last query response should have qdcount 1
|
||||||
|
The last query response should have ancount 0
|
||||||
|
The last query response should have nscount 1
|
||||||
|
The last query response should have adcount 0
|
||||||
|
# When checking flags, we must pass them exactly as they appear in
|
||||||
|
# the output of dig.
|
||||||
|
The last query response should have flags qr aa rd
|
||||||
|
|
||||||
|
A query for www.example.org type TXT should have rcode NOERROR
|
||||||
|
The last query response should have ancount 0
|
||||||
|
|
||||||
|
# Some queries where we specify more details about what to send and
|
||||||
|
# where
|
||||||
|
A query for www.example.org class CH should have rcode REFUSED
|
||||||
|
A query for www.example.org to 127.0.0.1 should have rcode NOERROR
|
||||||
|
A query for www.example.org to 127.0.0.1:47806 should have rcode NOERROR
|
||||||
|
A query for www.example.org type A class IN to 127.0.0.1:47806 should have rcode NOERROR
|
||||||
|
|
||||||
|
Scenario: changing database
|
||||||
|
# This scenario contains a lot of 'wait for' steps
|
||||||
|
# If those are not present, the asynchronous nature of the application
|
||||||
|
# can cause some of the things we send to be handled out of order;
|
||||||
|
# for instance auth could still be serving the old zone when we send
|
||||||
|
# the new query, or already respond from the new database.
|
||||||
|
# Therefore we wait for specific log messages after each operation
|
||||||
|
#
|
||||||
|
# This scenario outlines every single step, and does not use
|
||||||
|
# 'steps of steps' (e.g. Given I have bind10 running)
|
||||||
|
# We can do that but as an example this is probably better to learn
|
||||||
|
# the system
|
||||||
|
|
||||||
|
When I start bind10 with configuration example.org.config
|
||||||
|
Then wait for bind10 auth to start
|
||||||
|
Wait for bind10 stderr message CMDCTL_STARTED
|
||||||
|
A query for www.example.org should have rcode NOERROR
|
||||||
|
Wait for new bind10 stderr message AUTH_SEND_NORMAL_RESPONSE
|
||||||
|
Then set bind10 configuration Auth/database_file to data/empty_db.sqlite3
|
||||||
|
And wait for new bind10 stderr message DATASRC_SQLITE_OPEN
|
||||||
|
A query for www.example.org should have rcode REFUSED
|
||||||
|
Wait for new bind10 stderr message AUTH_SEND_NORMAL_RESPONSE
|
||||||
|
Then set bind10 configuration Auth/database_file to data/example.org.sqlite3
|
||||||
|
And wait for new bind10 stderr message DATASRC_SQLITE_OPEN
|
||||||
|
A query for www.example.org should have rcode NOERROR
|
||||||
|
|
||||||
|
Scenario: two bind10 instances
|
||||||
|
# This is more a test of the test system, start 2 bind10's
|
||||||
|
When I start bind10 with configuration example.org.config as bind10_one
|
||||||
|
And I start bind10 with configuration example2.org.config with cmdctl port 47804 as bind10_two
|
||||||
|
|
||||||
|
Then wait for bind10 auth of bind10_one to start
|
||||||
|
Then wait for bind10 auth of bind10_two to start
|
||||||
|
A query for www.example.org to 127.0.0.1:47806 should have rcode NOERROR
|
||||||
|
A query for www.example.org to 127.0.0.1:47807 should have rcode NOERROR
|
||||||
|
|
||||||
|
Then set bind10 configuration Auth/database_file to data/empty_db.sqlite3
|
||||||
|
And wait for bind10_one stderr message DATASRC_SQLITE_OPEN
|
||||||
|
|
||||||
|
A query for www.example.org to 127.0.0.1:47806 should have rcode REFUSED
|
||||||
|
A query for www.example.org to 127.0.0.1:47807 should have rcode NOERROR
|
108
tests/lettuce/features/terrain/bind10_control.py
Normal file
108
tests/lettuce/features/terrain/bind10_control.py
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
# Copyright (C) 2011 Internet Systems Consortium.
|
||||||
|
#
|
||||||
|
# Permission to use, copy, modify, and distribute this software for any
|
||||||
|
# purpose with or without fee is hereby granted, provided that the above
|
||||||
|
# copyright notice and this permission notice appear in all copies.
|
||||||
|
#
|
||||||
|
# THE SOFTWARE IS PROVIDED "AS IS" AND INTERNET SYSTEMS CONSORTIUM
|
||||||
|
# DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL
|
||||||
|
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL
|
||||||
|
# INTERNET SYSTEMS CONSORTIUM BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||||
|
# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING
|
||||||
|
# FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
|
||||||
|
# NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION
|
||||||
|
# WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||||
|
|
||||||
|
from lettuce import *
|
||||||
|
import subprocess
|
||||||
|
import re
|
||||||
|
|
||||||
|
@step('start bind10(?: with configuration (\S+))?' +\
|
||||||
|
'(?: with cmdctl port (\d+))?(?: as (\S+))?')
|
||||||
|
def start_bind10(step, config_file, cmdctl_port, process_name):
|
||||||
|
"""
|
||||||
|
Start BIND 10 with the given optional config file, cmdctl port, and
|
||||||
|
store the running process in world with the given process name.
|
||||||
|
Parameters:
|
||||||
|
config_file ('with configuration <file>', optional): this configuration
|
||||||
|
will be used. The path is relative to the base lettuce
|
||||||
|
directory.
|
||||||
|
cmdctl_port ('with cmdctl port <portnr>', optional): The port on which
|
||||||
|
b10-cmdctl listens for bindctl commands. Defaults to 47805.
|
||||||
|
process_name ('as <name>', optional). This is the name that can be used
|
||||||
|
in the following steps of the scenario to refer to this
|
||||||
|
BIND 10 instance. Defaults to 'bind10'.
|
||||||
|
This call will block until BIND10_STARTUP_COMPLETE or BIND10_STARTUP_ERROR
|
||||||
|
is logged. In the case of the latter, or if it times out, the step (and
|
||||||
|
scenario) will fail.
|
||||||
|
It will also fail if there is a running process with the given process_name
|
||||||
|
already.
|
||||||
|
"""
|
||||||
|
args = [ 'bind10', '-v' ]
|
||||||
|
if config_file is not None:
|
||||||
|
args.append('-p')
|
||||||
|
args.append("configurations/")
|
||||||
|
args.append('-c')
|
||||||
|
args.append(config_file)
|
||||||
|
if cmdctl_port is None:
|
||||||
|
args.append('--cmdctl-port=47805')
|
||||||
|
else:
|
||||||
|
args.append('--cmdctl-port=' + cmdctl_port)
|
||||||
|
if process_name is None:
|
||||||
|
process_name = "bind10"
|
||||||
|
else:
|
||||||
|
args.append('-m')
|
||||||
|
args.append(process_name + '_msgq.socket')
|
||||||
|
|
||||||
|
world.processes.add_process(step, process_name, args)
|
||||||
|
|
||||||
|
# check output to know when startup has been completed
|
||||||
|
message = world.processes.wait_for_stderr_str(process_name,
|
||||||
|
["BIND10_STARTUP_COMPLETE",
|
||||||
|
"BIND10_STARTUP_ERROR"])
|
||||||
|
assert message == "BIND10_STARTUP_COMPLETE", "Got: " + str(message)
|
||||||
|
|
||||||
|
@step('wait for bind10 auth (?:of (\w+) )?to start')
|
||||||
|
def wait_for_auth(step, process_name):
|
||||||
|
"""Wait for b10-auth to run. This is done by blocking until the message
|
||||||
|
AUTH_SERVER_STARTED is logged.
|
||||||
|
Parameters:
|
||||||
|
process_name ('of <name', optional): The name of the BIND 10 instance
|
||||||
|
to wait for. Defaults to 'bind10'.
|
||||||
|
"""
|
||||||
|
if process_name is None:
|
||||||
|
process_name = "bind10"
|
||||||
|
world.processes.wait_for_stderr_str(process_name, ['AUTH_SERVER_STARTED'],
|
||||||
|
False)
|
||||||
|
|
||||||
|
@step('have bind10 running(?: with configuration ([\w.]+))?')
|
||||||
|
def have_bind10_running(step, config_file):
|
||||||
|
"""
|
||||||
|
Compound convenience step for running bind10, which consists of
|
||||||
|
start_bind10 and wait_for_auth.
|
||||||
|
Currently only supports the 'with configuration' option.
|
||||||
|
"""
|
||||||
|
step.given('start bind10 with configuration ' + config_file)
|
||||||
|
step.given('wait for bind10 auth to start')
|
||||||
|
|
||||||
|
@step('set bind10 configuration (\S+) to (.*)(?: with cmdctl port (\d+))?')
|
||||||
|
def set_config_command(step, name, value, cmdctl_port):
|
||||||
|
"""
|
||||||
|
Run bindctl, set the given configuration to the given value, and commit it.
|
||||||
|
Parameters:
|
||||||
|
name ('configuration <name>'): Identifier of the configuration to set
|
||||||
|
value ('to <value>'): value to set it to.
|
||||||
|
cmdctl_port ('with cmdctl port <portnr>', optional): cmdctl port to send
|
||||||
|
the command to. Defaults to 47805.
|
||||||
|
Fails if cmdctl does not exit with status code 0.
|
||||||
|
"""
|
||||||
|
if cmdctl_port is None:
|
||||||
|
cmdctl_port = '47805'
|
||||||
|
args = ['bindctl', '-p', cmdctl_port]
|
||||||
|
bindctl = subprocess.Popen(args, 1, None, subprocess.PIPE,
|
||||||
|
subprocess.PIPE, None)
|
||||||
|
bindctl.stdin.write("config set " + name + " " + value + "\n")
|
||||||
|
bindctl.stdin.write("config commit\n")
|
||||||
|
bindctl.stdin.write("quit\n")
|
||||||
|
result = bindctl.wait()
|
||||||
|
assert result == 0, "bindctl exit code: " + str(result)
|
279
tests/lettuce/features/terrain/querying.py
Normal file
279
tests/lettuce/features/terrain/querying.py
Normal file
@@ -0,0 +1,279 @@
|
|||||||
|
# Copyright (C) 2011 Internet Systems Consortium.
|
||||||
|
#
|
||||||
|
# Permission to use, copy, modify, and distribute this software for any
|
||||||
|
# purpose with or without fee is hereby granted, provided that the above
|
||||||
|
# copyright notice and this permission notice appear in all copies.
|
||||||
|
#
|
||||||
|
# THE SOFTWARE IS PROVIDED "AS IS" AND INTERNET SYSTEMS CONSORTIUM
|
||||||
|
# DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL
|
||||||
|
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL
|
||||||
|
# INTERNET SYSTEMS CONSORTIUM BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||||
|
# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING
|
||||||
|
# FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
|
||||||
|
# NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION
|
||||||
|
# WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||||
|
|
||||||
|
# This script provides querying functionality
|
||||||
|
# The most important step is
|
||||||
|
#
|
||||||
|
# query for <name> [type X] [class X] [to <addr>[:port]] should have rcode <rc>
|
||||||
|
#
|
||||||
|
# By default, it will send queries to 127.0.0.1:47806 unless specified
|
||||||
|
# otherwise. The rcode is always checked. If the result is not NO_ANSWER,
|
||||||
|
# the result will be stored in last_query_result, which can then be inspected
|
||||||
|
# more closely, for instance with the step
|
||||||
|
#
|
||||||
|
# "the last query response should have <property> <value>"
|
||||||
|
#
|
||||||
|
# Also see example.feature for some examples
|
||||||
|
|
||||||
|
from lettuce import *
|
||||||
|
import subprocess
|
||||||
|
import re
|
||||||
|
|
||||||
|
#
|
||||||
|
# define a class to easily access different parts
|
||||||
|
# We may consider using our full library for this, but for now
|
||||||
|
# simply store several parts of the response as text values in
|
||||||
|
# this structure.
|
||||||
|
# (this actually has the advantage of not relying on our own libraries
|
||||||
|
# to test our own, well, libraries)
|
||||||
|
#
|
||||||
|
# The following attributes are 'parsed' from the response, all as strings,
|
||||||
|
# and end up as direct attributes of the QueryResult object:
|
||||||
|
# opcode, rcode, id, flags, qdcount, ancount, nscount, adcount
|
||||||
|
# (flags is one string with all flags, in the order they appear in the
|
||||||
|
# response packet.)
|
||||||
|
#
|
||||||
|
# this will set 'rcode' as the result code, we 'define' one additional
|
||||||
|
# rcode, "NO_ANSWER", if the dig process returned an error code itself
|
||||||
|
# In this case none of the other attributes will be set.
|
||||||
|
#
|
||||||
|
# The different sections will be lists of strings, one for each RR in the
|
||||||
|
# section. The question section will start with ';', as per dig output
|
||||||
|
#
|
||||||
|
# See server_from_sqlite3.feature for various examples to perform queries
|
||||||
|
class QueryResult(object):
|
||||||
|
status_re = re.compile("opcode: ([A-Z])+, status: ([A-Z]+), id: ([0-9]+)")
|
||||||
|
flags_re = re.compile("flags: ([a-z ]+); QUERY: ([0-9]+), ANSWER: " +
|
||||||
|
"([0-9]+), AUTHORITY: ([0-9]+), ADDITIONAL: ([0-9]+)")
|
||||||
|
|
||||||
|
def __init__(self, name, qtype, qclass, address, port):
|
||||||
|
"""
|
||||||
|
Constructor. This fires of a query using dig.
|
||||||
|
Parameters:
|
||||||
|
name: The domain name to query
|
||||||
|
qtype: The RR type to query. Defaults to A if it is None.
|
||||||
|
qclass: The RR class to query. Defaults to IN if it is None.
|
||||||
|
address: The IP adress to send the query to.
|
||||||
|
port: The port number to send the query to.
|
||||||
|
All parameters must be either strings or have the correct string
|
||||||
|
representation.
|
||||||
|
Only one query attempt will be made.
|
||||||
|
"""
|
||||||
|
args = [ 'dig', '+tries=1', '@' + str(address), '-p', str(port) ]
|
||||||
|
if qtype is not None:
|
||||||
|
args.append('-t')
|
||||||
|
args.append(str(qtype))
|
||||||
|
if qclass is not None:
|
||||||
|
args.append('-c')
|
||||||
|
args.append(str(qclass))
|
||||||
|
args.append(name)
|
||||||
|
dig_process = subprocess.Popen(args, 1, None, None, subprocess.PIPE,
|
||||||
|
None)
|
||||||
|
result = dig_process.wait()
|
||||||
|
if result != 0:
|
||||||
|
self.rcode = "NO_ANSWER"
|
||||||
|
else:
|
||||||
|
self.rcode = None
|
||||||
|
parsing = "HEADER"
|
||||||
|
self.question_section = []
|
||||||
|
self.answer_section = []
|
||||||
|
self.authority_section = []
|
||||||
|
self.additional_section = []
|
||||||
|
self.line_handler = self.parse_header
|
||||||
|
for out in dig_process.stdout:
|
||||||
|
self.line_handler(out)
|
||||||
|
|
||||||
|
def _check_next_header(self, line):
|
||||||
|
"""
|
||||||
|
Returns true if we found a next header, and sets the internal
|
||||||
|
line handler to the appropriate value.
|
||||||
|
"""
|
||||||
|
if line == ";; ANSWER SECTION:\n":
|
||||||
|
self.line_handler = self.parse_answer
|
||||||
|
elif line == ";; AUTHORITY SECTION:\n":
|
||||||
|
self.line_handler = self.parse_authority
|
||||||
|
elif line == ";; ADDITIONAL SECTION:\n":
|
||||||
|
self.line_handler = self.parse_additional
|
||||||
|
elif line.startswith(";; Query time"):
|
||||||
|
self.line_handler = self.parse_footer
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
def parse_header(self, line):
|
||||||
|
"""
|
||||||
|
Parse the header lines of the query response.
|
||||||
|
Parameters:
|
||||||
|
line: The current line of the response.
|
||||||
|
"""
|
||||||
|
if not self._check_next_header(line):
|
||||||
|
status_match = self.status_re.search(line)
|
||||||
|
flags_match = self.flags_re.search(line)
|
||||||
|
if status_match is not None:
|
||||||
|
self.opcode = status_match.group(1)
|
||||||
|
self.rcode = status_match.group(2)
|
||||||
|
elif flags_match is not None:
|
||||||
|
self.flags = flags_match.group(1)
|
||||||
|
self.qdcount = flags_match.group(2)
|
||||||
|
self.ancount = flags_match.group(3)
|
||||||
|
self.nscount = flags_match.group(4)
|
||||||
|
self.adcount = flags_match.group(5)
|
||||||
|
|
||||||
|
def parse_question(self, line):
|
||||||
|
"""
|
||||||
|
Parse the question section lines of the query response.
|
||||||
|
Parameters:
|
||||||
|
line: The current line of the response.
|
||||||
|
"""
|
||||||
|
if not self._check_next_header(line):
|
||||||
|
if line != "\n":
|
||||||
|
self.question_section.append(line.strip())
|
||||||
|
|
||||||
|
def parse_answer(self, line):
|
||||||
|
"""
|
||||||
|
Parse the answer section lines of the query response.
|
||||||
|
Parameters:
|
||||||
|
line: The current line of the response.
|
||||||
|
"""
|
||||||
|
if not self._check_next_header(line):
|
||||||
|
if line != "\n":
|
||||||
|
self.answer_section.append(line.strip())
|
||||||
|
|
||||||
|
def parse_authority(self, line):
|
||||||
|
"""
|
||||||
|
Parse the authority section lines of the query response.
|
||||||
|
Parameters:
|
||||||
|
line: The current line of the response.
|
||||||
|
"""
|
||||||
|
if not self._check_next_header(line):
|
||||||
|
if line != "\n":
|
||||||
|
self.authority_section.append(line.strip())
|
||||||
|
|
||||||
|
def parse_additional(self, line):
|
||||||
|
"""
|
||||||
|
Parse the additional section lines of the query response.
|
||||||
|
Parameters:
|
||||||
|
line: The current line of the response.
|
||||||
|
"""
|
||||||
|
if not self._check_next_header(line):
|
||||||
|
if line != "\n":
|
||||||
|
self.additional_section.append(line.strip())
|
||||||
|
|
||||||
|
def parse_footer(self, line):
|
||||||
|
"""
|
||||||
|
Parse the footer lines of the query response.
|
||||||
|
Parameters:
|
||||||
|
line: The current line of the response.
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
@step('A query for ([\w.]+) (?:type ([A-Z]+) )?(?:class ([A-Z]+) )?' +
|
||||||
|
'(?:to ([^:]+)(?::([0-9]+))? )?should have rcode ([\w.]+)')
|
||||||
|
def query(step, query_name, qtype, qclass, addr, port, rcode):
|
||||||
|
"""
|
||||||
|
Run a query, check the rcode of the response, and store the query
|
||||||
|
result in world.last_query_result.
|
||||||
|
Parameters:
|
||||||
|
query_name ('query for <name>'): The domain name to query.
|
||||||
|
qtype ('type <type>', optional): The RR type to query. Defaults to A.
|
||||||
|
qclass ('class <class>', optional): The RR class to query. Defaults to IN.
|
||||||
|
addr ('to <address>', optional): The IP address of the nameserver to query.
|
||||||
|
Defaults to 127.0.0.1.
|
||||||
|
port (':<port>', optional): The port number of the nameserver to query.
|
||||||
|
Defaults to 47806.
|
||||||
|
rcode ('should have rcode <rcode>'): The expected rcode of the answer.
|
||||||
|
"""
|
||||||
|
if qtype is None:
|
||||||
|
qtype = "A"
|
||||||
|
if qclass is None:
|
||||||
|
qclass = "IN"
|
||||||
|
if addr is None:
|
||||||
|
addr = "127.0.0.1"
|
||||||
|
if port is None:
|
||||||
|
port = 47806
|
||||||
|
query_result = QueryResult(query_name, qtype, qclass, addr, port)
|
||||||
|
assert query_result.rcode == rcode,\
|
||||||
|
"Expected: " + rcode + ", got " + query_result.rcode
|
||||||
|
world.last_query_result = query_result
|
||||||
|
|
||||||
|
@step('The SOA serial for ([\w.]+) should be ([0-9]+)')
|
||||||
|
def query_soa(step, query_name, serial):
|
||||||
|
"""
|
||||||
|
Convenience function to check the SOA SERIAL value of the given zone at
|
||||||
|
the nameserver at the default address (127.0.0.1:47806).
|
||||||
|
Parameters:
|
||||||
|
query_name ('for <name>'): The zone to find the SOA record for.
|
||||||
|
serial ('should be <number>'): The expected value of the SOA SERIAL.
|
||||||
|
If the rcode is not NOERROR, or the answer section does not contain the
|
||||||
|
SOA record, this step fails.
|
||||||
|
"""
|
||||||
|
query_result = QueryResult(query_name, "SOA", "IN", "127.0.0.1", "47806")
|
||||||
|
assert "NOERROR" == query_result.rcode,\
|
||||||
|
"Got " + query_result.rcode + ", expected NOERROR"
|
||||||
|
assert len(query_result.answer_section) == 1,\
|
||||||
|
"Too few or too many answers in SOA response"
|
||||||
|
soa_parts = query_result.answer_section[0].split()
|
||||||
|
assert serial == soa_parts[6],\
|
||||||
|
"Got SOA serial " + soa_parts[6] + ", expected " + serial
|
||||||
|
|
||||||
|
@step('last query response should have (\S+) (.+)')
|
||||||
|
def check_last_query(step, item, value):
|
||||||
|
"""
|
||||||
|
Check a specific value in the reponse from the last successful query sent.
|
||||||
|
Parameters:
|
||||||
|
item: The item to check the value of
|
||||||
|
value: The expected value.
|
||||||
|
This performs a very simple direct string comparison of the QueryResult
|
||||||
|
member with the given item name and the given value.
|
||||||
|
Fails if the item is unknown, or if its value does not match the expected
|
||||||
|
value.
|
||||||
|
"""
|
||||||
|
assert world.last_query_result is not None
|
||||||
|
assert item in world.last_query_result.__dict__
|
||||||
|
lq_val = world.last_query_result.__dict__[item]
|
||||||
|
assert str(value) == str(lq_val),\
|
||||||
|
"Got: " + str(lq_val) + ", expected: " + str(value)
|
||||||
|
|
||||||
|
@step('([a-zA-Z]+) section of the last query response should be')
|
||||||
|
def check_last_query_section(step, section):
|
||||||
|
"""
|
||||||
|
Check the entire contents of the given section of the response of the last
|
||||||
|
query.
|
||||||
|
Parameters:
|
||||||
|
section ('<section> section'): The name of the section (QUESTION, ANSWER,
|
||||||
|
AUTHORITY or ADDITIONAL).
|
||||||
|
The expected response is taken from the multiline part of the step in the
|
||||||
|
scenario. Differing whitespace is ignored, but currently the order is
|
||||||
|
significant.
|
||||||
|
Fails if they do not match.
|
||||||
|
"""
|
||||||
|
response_string = None
|
||||||
|
if section.lower() == 'question':
|
||||||
|
response_string = "\n".join(world.last_query_result.question_section)
|
||||||
|
elif section.lower() == 'answer':
|
||||||
|
response_string = "\n".join(world.last_query_result.answer_section)
|
||||||
|
elif section.lower() == 'authority':
|
||||||
|
response_string = "\n".join(world.last_query_result.answer_section)
|
||||||
|
elif section.lower() == 'additional':
|
||||||
|
response_string = "\n".join(world.last_query_result.answer_section)
|
||||||
|
else:
|
||||||
|
assert False, "Unknown section " + section
|
||||||
|
# replace whitespace of any length by one space
|
||||||
|
response_string = re.sub("[ \t]+", " ", response_string)
|
||||||
|
expect = re.sub("[ \t]+", " ", step.multiline)
|
||||||
|
assert response_string.strip() == expect.strip(),\
|
||||||
|
"Got:\n'" + response_string + "'\nExpected:\n'" + step.multiline +"'"
|
||||||
|
|
||||||
|
|
73
tests/lettuce/features/terrain/steps.py
Normal file
73
tests/lettuce/features/terrain/steps.py
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
# Copyright (C) 2011 Internet Systems Consortium.
|
||||||
|
#
|
||||||
|
# Permission to use, copy, modify, and distribute this software for any
|
||||||
|
# purpose with or without fee is hereby granted, provided that the above
|
||||||
|
# copyright notice and this permission notice appear in all copies.
|
||||||
|
#
|
||||||
|
# THE SOFTWARE IS PROVIDED "AS IS" AND INTERNET SYSTEMS CONSORTIUM
|
||||||
|
# DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL
|
||||||
|
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL
|
||||||
|
# INTERNET SYSTEMS CONSORTIUM BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||||
|
# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING
|
||||||
|
# FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
|
||||||
|
# NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION
|
||||||
|
# WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||||
|
|
||||||
|
#
|
||||||
|
# This file contains a number of common steps that are general and may be used
|
||||||
|
# By a lot of feature files.
|
||||||
|
#
|
||||||
|
|
||||||
|
from lettuce import *
|
||||||
|
import os
|
||||||
|
|
||||||
|
@step('stop process (\w+)')
|
||||||
|
def stop_a_named_process(step, process_name):
|
||||||
|
"""
|
||||||
|
Stop the process with the given name.
|
||||||
|
Parameters:
|
||||||
|
process_name ('process <name>'): Name of the process to stop.
|
||||||
|
"""
|
||||||
|
world.processes.stop_process(process_name)
|
||||||
|
|
||||||
|
@step('wait for (new )?(\w+) stderr message (\w+)')
|
||||||
|
def wait_for_message(step, new, process_name, message):
|
||||||
|
"""
|
||||||
|
Block until the given message is printed to the given process's stderr
|
||||||
|
output.
|
||||||
|
Parameter:
|
||||||
|
new: (' new', optional): Only check the output printed since last time
|
||||||
|
this step was used for this process.
|
||||||
|
process_name ('<name> stderr'): Name of the process to check the output of.
|
||||||
|
message ('message <message>'): Output (part) to wait for.
|
||||||
|
Fails if the message is not found after 10 seconds.
|
||||||
|
"""
|
||||||
|
world.processes.wait_for_stderr_str(process_name, [message], new)
|
||||||
|
|
||||||
|
@step('wait for (new )?(\w+) stdout message (\w+)')
|
||||||
|
def wait_for_message(step, process_name, message):
|
||||||
|
"""
|
||||||
|
Block until the given message is printed to the given process's stdout
|
||||||
|
output.
|
||||||
|
Parameter:
|
||||||
|
new: (' new', optional): Only check the output printed since last time
|
||||||
|
this step was used for this process.
|
||||||
|
process_name ('<name> stderr'): Name of the process to check the output of.
|
||||||
|
message ('message <message>'): Output (part) to wait for.
|
||||||
|
Fails if the message is not found after 10 seconds.
|
||||||
|
"""
|
||||||
|
world.processes.wait_for_stdout_str(process_name, [message], new)
|
||||||
|
|
||||||
|
@step('the file (\S+) should (not )?exist')
|
||||||
|
def check_existence(step, file_name, should_not_exist):
|
||||||
|
"""
|
||||||
|
Check the existence of the given file.
|
||||||
|
Parameters:
|
||||||
|
file_name ('file <name>'): File to check existence of.
|
||||||
|
should_not_exist ('not', optional): Whether it should or should not exist.
|
||||||
|
Fails if the file should exist and does not, or vice versa.
|
||||||
|
"""
|
||||||
|
if should_not_exist is None:
|
||||||
|
assert os.path.exists(file_name), file_name + " does not exist"
|
||||||
|
else:
|
||||||
|
assert not os.path.exists(file_name), file_name + " exists"
|
360
tests/lettuce/features/terrain/terrain.py
Normal file
360
tests/lettuce/features/terrain/terrain.py
Normal file
@@ -0,0 +1,360 @@
|
|||||||
|
# Copyright (C) 2011 Internet Systems Consortium.
|
||||||
|
#
|
||||||
|
# Permission to use, copy, modify, and distribute this software for any
|
||||||
|
# purpose with or without fee is hereby granted, provided that the above
|
||||||
|
# copyright notice and this permission notice appear in all copies.
|
||||||
|
#
|
||||||
|
# THE SOFTWARE IS PROVIDED "AS IS" AND INTERNET SYSTEMS CONSORTIUM
|
||||||
|
# DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL
|
||||||
|
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL
|
||||||
|
# INTERNET SYSTEMS CONSORTIUM BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||||
|
# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING
|
||||||
|
# FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
|
||||||
|
# NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION
|
||||||
|
# WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||||
|
|
||||||
|
#
|
||||||
|
# This is the 'terrain' in which the lettuce lives. By convention, this is
|
||||||
|
# where global setup and teardown is defined.
|
||||||
|
#
|
||||||
|
# We declare some attributes of the global 'world' variables here, so the
|
||||||
|
# tests can safely assume they are present.
|
||||||
|
#
|
||||||
|
# We also use it to provide scenario invariants, such as resetting data.
|
||||||
|
#
|
||||||
|
|
||||||
|
from lettuce import *
|
||||||
|
import subprocess
|
||||||
|
import os.path
|
||||||
|
import shutil
|
||||||
|
import re
|
||||||
|
import time
|
||||||
|
|
||||||
|
# In order to make sure we start all tests with a 'clean' environment,
|
||||||
|
# We perform a number of initialization steps, like restoring configuration
|
||||||
|
# files, and removing generated data files.
|
||||||
|
|
||||||
|
# This approach may not scale; if so we should probably provide specific
|
||||||
|
# initialization steps for scenarios. But until that is shown to be a problem,
|
||||||
|
# It will keep the scenarios cleaner.
|
||||||
|
|
||||||
|
# This is a list of files that are freshly copied before each scenario
|
||||||
|
# The first element is the original, the second is the target that will be
|
||||||
|
# used by the tests that need them
|
||||||
|
copylist = [
|
||||||
|
["configurations/example.org.config.orig", "configurations/example.org.config"]
|
||||||
|
]
|
||||||
|
|
||||||
|
# This is a list of files that, if present, will be removed before a scenario
|
||||||
|
removelist = [
|
||||||
|
"data/test_nonexistent_db.sqlite3"
|
||||||
|
]
|
||||||
|
|
||||||
|
# When waiting for output data of a running process, use OUTPUT_WAIT_INTERVAL
|
||||||
|
# as the interval in which to check again if it has not been found yet.
|
||||||
|
# If we have waited OUTPUT_WAIT_MAX_INTERVALS times, we will abort with an
|
||||||
|
# error (so as not to hang indefinitely)
|
||||||
|
OUTPUT_WAIT_INTERVAL = 0.5
|
||||||
|
OUTPUT_WAIT_MAX_INTERVALS = 20
|
||||||
|
|
||||||
|
# class that keeps track of one running process and the files
|
||||||
|
# we created for it.
|
||||||
|
class RunningProcess:
|
||||||
|
def __init__(self, step, process_name, args):
|
||||||
|
# set it to none first so destructor won't error if initializer did
|
||||||
|
"""
|
||||||
|
Initialize the long-running process structure, and start the process.
|
||||||
|
Parameters:
|
||||||
|
step: The scenario step it was called from. This is used for
|
||||||
|
determining the output files for redirection of stdout
|
||||||
|
and stderr.
|
||||||
|
process_name: The name to refer to this running process later.
|
||||||
|
args: Array of arguments to pass to Popen().
|
||||||
|
"""
|
||||||
|
self.process = None
|
||||||
|
self.step = step
|
||||||
|
self.process_name = process_name
|
||||||
|
self.remove_files_on_exit = True
|
||||||
|
self._check_output_dir()
|
||||||
|
self._create_filenames()
|
||||||
|
self._start_process(args)
|
||||||
|
|
||||||
|
def _start_process(self, args):
|
||||||
|
"""
|
||||||
|
Start the process.
|
||||||
|
Parameters:
|
||||||
|
args:
|
||||||
|
Array of arguments to pass to Popen().
|
||||||
|
"""
|
||||||
|
stderr_write = open(self.stderr_filename, "w")
|
||||||
|
stdout_write = open(self.stdout_filename, "w")
|
||||||
|
self.process = subprocess.Popen(args, 1, None, subprocess.PIPE,
|
||||||
|
stdout_write, stderr_write)
|
||||||
|
# open them again, this time for reading
|
||||||
|
self.stderr = open(self.stderr_filename, "r")
|
||||||
|
self.stdout = open(self.stdout_filename, "r")
|
||||||
|
|
||||||
|
def mangle_filename(self, filebase, extension):
|
||||||
|
"""
|
||||||
|
Remove whitespace and non-default characters from a base string,
|
||||||
|
and return the substituted value. Whitespace is replaced by an
|
||||||
|
underscore. Any other character that is not an ASCII letter, a
|
||||||
|
number, a dot, or a hyphen or underscore is removed.
|
||||||
|
Parameter:
|
||||||
|
filebase: The string to perform the substitution and removal on
|
||||||
|
extension: An extension to append to the result value
|
||||||
|
Returns the modified filebase with the given extension
|
||||||
|
"""
|
||||||
|
filebase = re.sub("\s+", "_", filebase)
|
||||||
|
filebase = re.sub("[^a-zA-Z0-9.\-_]", "", filebase)
|
||||||
|
return filebase + "." + extension
|
||||||
|
|
||||||
|
def _check_output_dir(self):
|
||||||
|
# We may want to make this overridable by the user, perhaps
|
||||||
|
# through an environment variable. Since we currently expect
|
||||||
|
# lettuce to be run from our lettuce dir, we shall just use
|
||||||
|
# the relative path 'output/'
|
||||||
|
"""
|
||||||
|
Make sure the output directory for stdout/stderr redirection
|
||||||
|
exists.
|
||||||
|
Fails if it exists but is not a directory, or if it does not
|
||||||
|
and we are unable to create it.
|
||||||
|
"""
|
||||||
|
self._output_dir = os.getcwd() + os.sep + "output"
|
||||||
|
if not os.path.exists(self._output_dir):
|
||||||
|
os.mkdir(self._output_dir)
|
||||||
|
assert os.path.isdir(self._output_dir),\
|
||||||
|
self._output_dir + " is not a directory."
|
||||||
|
|
||||||
|
def _create_filenames(self):
|
||||||
|
"""
|
||||||
|
Derive the filenames for stdout/stderr redirection from the
|
||||||
|
feature, scenario, and process name. The base will be
|
||||||
|
"<Feature>-<Scenario>-<process name>.[stdout|stderr]"
|
||||||
|
"""
|
||||||
|
filebase = self.step.scenario.feature.name + "-" +\
|
||||||
|
self.step.scenario.name + "-" + self.process_name
|
||||||
|
self.stderr_filename = self._output_dir + os.sep +\
|
||||||
|
self.mangle_filename(filebase, "stderr")
|
||||||
|
self.stdout_filename = self._output_dir + os.sep +\
|
||||||
|
self.mangle_filename(filebase, "stdout")
|
||||||
|
|
||||||
|
def stop_process(self):
|
||||||
|
"""
|
||||||
|
Stop this process by calling terminate(). Blocks until process has
|
||||||
|
exited. If remove_files_on_exit is True, redirected output files
|
||||||
|
are removed.
|
||||||
|
"""
|
||||||
|
if self.process is not None:
|
||||||
|
self.process.terminate()
|
||||||
|
self.process.wait()
|
||||||
|
self.process = None
|
||||||
|
if self.remove_files_on_exit:
|
||||||
|
self._remove_files()
|
||||||
|
|
||||||
|
def _remove_files(self):
|
||||||
|
"""
|
||||||
|
Remove the files created for redirection of stdout/stderr output.
|
||||||
|
"""
|
||||||
|
os.remove(self.stderr_filename)
|
||||||
|
os.remove(self.stdout_filename)
|
||||||
|
|
||||||
|
def _wait_for_output_str(self, filename, running_file, strings, only_new):
|
||||||
|
"""
|
||||||
|
Wait for a line of output in this process. This will (if only_new is
|
||||||
|
False) first check all previous output from the process, and if not
|
||||||
|
found, check all output since the last time this method was called.
|
||||||
|
For each line in the output, the given strings array is checked. If
|
||||||
|
any output lines checked contains one of the strings in the strings
|
||||||
|
array, that string (not the line!) is returned.
|
||||||
|
Parameters:
|
||||||
|
filename: The filename to read previous output from, if applicable.
|
||||||
|
running_file: The open file to read new output from.
|
||||||
|
strings: Array of strings to look for.
|
||||||
|
only_new: If true, only check output since last time this method was
|
||||||
|
called. If false, first check earlier output.
|
||||||
|
Returns the matched string.
|
||||||
|
Fails if none of the strings was read after 10 seconds
|
||||||
|
(OUTPUT_WAIT_INTERVAL * OUTPUT_WAIT_MAX_INTERVALS).
|
||||||
|
"""
|
||||||
|
if not only_new:
|
||||||
|
full_file = open(filename, "r")
|
||||||
|
for line in full_file:
|
||||||
|
for string in strings:
|
||||||
|
if line.find(string) != -1:
|
||||||
|
full_file.close()
|
||||||
|
return string
|
||||||
|
wait_count = 0
|
||||||
|
while wait_count < OUTPUT_WAIT_MAX_INTERVALS:
|
||||||
|
where = running_file.tell()
|
||||||
|
line = running_file.readline()
|
||||||
|
if line:
|
||||||
|
for string in strings:
|
||||||
|
if line.find(string) != -1:
|
||||||
|
return string
|
||||||
|
else:
|
||||||
|
wait_count += 1
|
||||||
|
time.sleep(OUTPUT_WAIT_INTERVAL)
|
||||||
|
running_file.seek(where)
|
||||||
|
assert False, "Timeout waiting for process output: " + str(strings)
|
||||||
|
|
||||||
|
def wait_for_stderr_str(self, strings, only_new = True):
|
||||||
|
"""
|
||||||
|
Wait for one of the given strings in this process's stderr output.
|
||||||
|
Parameters:
|
||||||
|
strings: Array of strings to look for.
|
||||||
|
only_new: If true, only check output since last time this method was
|
||||||
|
called. If false, first check earlier output.
|
||||||
|
Returns the matched string.
|
||||||
|
Fails if none of the strings was read after 10 seconds
|
||||||
|
(OUTPUT_WAIT_INTERVAL * OUTPUT_WAIT_MAX_INTERVALS).
|
||||||
|
"""
|
||||||
|
return self._wait_for_output_str(self.stderr_filename, self.stderr,
|
||||||
|
strings, only_new)
|
||||||
|
|
||||||
|
def wait_for_stdout_str(self, strings, only_new = True):
|
||||||
|
"""
|
||||||
|
Wait for one of the given strings in this process's stdout output.
|
||||||
|
Parameters:
|
||||||
|
strings: Array of strings to look for.
|
||||||
|
only_new: If true, only check output since last time this method was
|
||||||
|
called. If false, first check earlier output.
|
||||||
|
Returns the matched string.
|
||||||
|
Fails if none of the strings was read after 10 seconds
|
||||||
|
(OUTPUT_WAIT_INTERVAL * OUTPUT_WAIT_MAX_INTERVALS).
|
||||||
|
"""
|
||||||
|
return self._wait_for_output_str(self.stdout_filename, self.stdout,
|
||||||
|
strings, only_new)
|
||||||
|
|
||||||
|
# Container class for a number of running processes
|
||||||
|
# i.e. servers like bind10, etc
|
||||||
|
# one-shot programs like dig or bindctl are started and closed separately
|
||||||
|
class RunningProcesses:
|
||||||
|
def __init__(self):
|
||||||
|
"""
|
||||||
|
Initialize with no running processes.
|
||||||
|
"""
|
||||||
|
self.processes = {}
|
||||||
|
|
||||||
|
def add_process(self, step, process_name, args):
|
||||||
|
"""
|
||||||
|
Start a process with the given arguments, and store it under the given
|
||||||
|
name.
|
||||||
|
Parameters:
|
||||||
|
step: The scenario step it was called from. This is used for
|
||||||
|
determining the output files for redirection of stdout
|
||||||
|
and stderr.
|
||||||
|
process_name: The name to refer to this running process later.
|
||||||
|
args: Array of arguments to pass to Popen().
|
||||||
|
Fails if a process with the given name is already running.
|
||||||
|
"""
|
||||||
|
assert process_name not in self.processes,\
|
||||||
|
"Process " + name + " already running"
|
||||||
|
self.processes[process_name] = RunningProcess(step, process_name, args)
|
||||||
|
|
||||||
|
def get_process(self, process_name):
|
||||||
|
"""
|
||||||
|
Return the Process with the given process name.
|
||||||
|
Parameters:
|
||||||
|
process_name: The name of the process to return.
|
||||||
|
Fails if the process is not running.
|
||||||
|
"""
|
||||||
|
assert process_name in self.processes,\
|
||||||
|
"Process " + name + " unknown"
|
||||||
|
return self.processes[process_name]
|
||||||
|
|
||||||
|
def stop_process(self, process_name):
|
||||||
|
"""
|
||||||
|
Stop the Process with the given process name.
|
||||||
|
Parameters:
|
||||||
|
process_name: The name of the process to return.
|
||||||
|
Fails if the process is not running.
|
||||||
|
"""
|
||||||
|
assert process_name in self.processes,\
|
||||||
|
"Process " + name + " unknown"
|
||||||
|
self.processes[process_name].stop_process()
|
||||||
|
del self.processes[process_name]
|
||||||
|
|
||||||
|
def stop_all_processes(self):
|
||||||
|
"""
|
||||||
|
Stop all running processes.
|
||||||
|
"""
|
||||||
|
for process in self.processes.values():
|
||||||
|
process.stop_process()
|
||||||
|
|
||||||
|
def keep_files(self):
|
||||||
|
"""
|
||||||
|
Keep the redirection files for stdout/stderr output of all processes
|
||||||
|
instead of removing them when they are stopped later.
|
||||||
|
"""
|
||||||
|
for process in self.processes.values():
|
||||||
|
process.remove_files_on_exit = False
|
||||||
|
|
||||||
|
def wait_for_stderr_str(self, process_name, strings, only_new = True):
|
||||||
|
"""
|
||||||
|
Wait for one of the given strings in the given process's stderr output.
|
||||||
|
Parameters:
|
||||||
|
process_name: The name of the process to check the stderr output of.
|
||||||
|
strings: Array of strings to look for.
|
||||||
|
only_new: If true, only check output since last time this method was
|
||||||
|
called. If false, first check earlier output.
|
||||||
|
Returns the matched string.
|
||||||
|
Fails if none of the strings was read after 10 seconds
|
||||||
|
(OUTPUT_WAIT_INTERVAL * OUTPUT_WAIT_MAX_INTERVALS).
|
||||||
|
Fails if the process is unknown.
|
||||||
|
"""
|
||||||
|
assert process_name in self.processes,\
|
||||||
|
"Process " + process_name + " unknown"
|
||||||
|
return self.processes[process_name].wait_for_stderr_str(strings,
|
||||||
|
only_new)
|
||||||
|
|
||||||
|
def wait_for_stdout_str(self, process_name, strings, only_new = True):
|
||||||
|
"""
|
||||||
|
Wait for one of the given strings in the given process's stdout output.
|
||||||
|
Parameters:
|
||||||
|
process_name: The name of the process to check the stdout output of.
|
||||||
|
strings: Array of strings to look for.
|
||||||
|
only_new: If true, only check output since last time this method was
|
||||||
|
called. If false, first check earlier output.
|
||||||
|
Returns the matched string.
|
||||||
|
Fails if none of the strings was read after 10 seconds
|
||||||
|
(OUTPUT_WAIT_INTERVAL * OUTPUT_WAIT_MAX_INTERVALS).
|
||||||
|
Fails if the process is unknown.
|
||||||
|
"""
|
||||||
|
assert process_name in self.processes,\
|
||||||
|
"Process " + process_name + " unknown"
|
||||||
|
return self.processes[process_name].wait_for_stdout_str(strings,
|
||||||
|
only_new)
|
||||||
|
|
||||||
|
@before.each_scenario
|
||||||
|
def initialize(scenario):
|
||||||
|
"""
|
||||||
|
Global initialization for each scenario.
|
||||||
|
"""
|
||||||
|
# Keep track of running processes
|
||||||
|
world.processes = RunningProcesses()
|
||||||
|
|
||||||
|
# Convenience variable to access the last query result from querying.py
|
||||||
|
world.last_query_result = None
|
||||||
|
|
||||||
|
# Some tests can modify the settings. If the tests fail half-way, or
|
||||||
|
# don't clean up, this can leave configurations or data in a bad state,
|
||||||
|
# so we copy them from originals before each scenario
|
||||||
|
for item in copylist:
|
||||||
|
shutil.copy(item[0], item[1])
|
||||||
|
|
||||||
|
for item in removelist:
|
||||||
|
if os.path.exists(item):
|
||||||
|
os.remove(item)
|
||||||
|
|
||||||
|
@after.each_scenario
|
||||||
|
def cleanup(scenario):
|
||||||
|
"""
|
||||||
|
Global cleanup for each scenario.
|
||||||
|
"""
|
||||||
|
# Keep output files if the scenario failed
|
||||||
|
if not scenario.passed:
|
||||||
|
world.processes.keep_files()
|
||||||
|
# Stop any running processes we may have had around
|
||||||
|
world.processes.stop_all_processes()
|
||||||
|
|
46
tests/lettuce/setup_intree_bind10.sh.in
Executable file
46
tests/lettuce/setup_intree_bind10.sh.in
Executable file
@@ -0,0 +1,46 @@
|
|||||||
|
#! /bin/sh
|
||||||
|
|
||||||
|
# Copyright (C) 2010 Internet Systems Consortium.
|
||||||
|
#
|
||||||
|
# Permission to use, copy, modify, and distribute this software for any
|
||||||
|
# purpose with or without fee is hereby granted, provided that the above
|
||||||
|
# copyright notice and this permission notice appear in all copies.
|
||||||
|
#
|
||||||
|
# THE SOFTWARE IS PROVIDED "AS IS" AND INTERNET SYSTEMS CONSORTIUM
|
||||||
|
# DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL
|
||||||
|
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL
|
||||||
|
# INTERNET SYSTEMS CONSORTIUM BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||||
|
# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING
|
||||||
|
# FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
|
||||||
|
# NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION
|
||||||
|
# WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||||
|
|
||||||
|
PYTHON_EXEC=${PYTHON_EXEC:-@PYTHON@}
|
||||||
|
export PYTHON_EXEC
|
||||||
|
|
||||||
|
BIND10_PATH=@abs_top_builddir@/src/bin/bind10
|
||||||
|
|
||||||
|
PATH=@abs_top_builddir@/src/bin/bind10:@abs_top_builddir@/src/bin/bindctl:@abs_top_builddir@/src/bin/msgq:@abs_top_builddir@/src/bin/auth:@abs_top_builddir@/src/bin/resolver:@abs_top_builddir@/src/bin/cfgmgr:@abs_top_builddir@/src/bin/cmdctl:@abs_top_builddir@/src/bin/stats:@abs_top_builddir@/src/bin/xfrin:@abs_top_builddir@/src/bin/xfrout:@abs_top_builddir@/src/bin/zonemgr:@abs_top_builddir@/src/bin/dhcp6:@abs_top_builddir@/src/bin/sockcreator:$PATH
|
||||||
|
export PATH
|
||||||
|
|
||||||
|
PYTHONPATH=@abs_top_builddir@/src/bin:@abs_top_builddir@/src/lib/python/isc/log_messages:@abs_top_builddir@/src/lib/python:@abs_top_builddir@/src/lib/dns/python/.libs:@abs_top_builddir@/src/lib/xfr/.libs:@abs_top_builddir@/src/lib/log/.libs:@abs_top_builddir@/src/lib/util/io/.libs:@abs_top_builddir@/src/lib/python/isc/config:@abs_top_builddir@/src/lib/python/isc/acl/.libs:@abs_top_builddir@/src/lib/python/isc/datasrc/.libs
|
||||||
|
export PYTHONPATH
|
||||||
|
|
||||||
|
# If necessary (rare cases), explicitly specify paths to dynamic libraries
|
||||||
|
# required by loadable python modules.
|
||||||
|
SET_ENV_LIBRARY_PATH=@SET_ENV_LIBRARY_PATH@
|
||||||
|
if test $SET_ENV_LIBRARY_PATH = yes; then
|
||||||
|
@ENV_LIBRARY_PATH@=@abs_top_builddir@/src/lib/dns/.libs:@abs_top_builddir@/src/lib/dns/python/.libs:@abs_top_builddir@/src/lib/cryptolink/.libs:@abs_top_builddir@/src/lib/cc/.libs:@abs_top_builddir@/src/lib/config/.libs:@abs_top_builddir@/src/lib/log/.libs:@abs_top_builddir@/src/lib/acl/.libs:@abs_top_builddir@/src/lib/util/.libs:@abs_top_builddir@/src/lib/util/io/.libs:@abs_top_builddir@/src/lib/exceptions/.libs:@abs_top_builddir@/src/lib/datasrc/.libs:$@ENV_LIBRARY_PATH@
|
||||||
|
export @ENV_LIBRARY_PATH@
|
||||||
|
fi
|
||||||
|
|
||||||
|
B10_FROM_SOURCE=@abs_top_srcdir@
|
||||||
|
export B10_FROM_SOURCE
|
||||||
|
# TODO: We need to do this feature based (ie. no general from_source)
|
||||||
|
# But right now we need a second one because some spec files are
|
||||||
|
# generated and hence end up under builddir
|
||||||
|
B10_FROM_BUILD=@abs_top_builddir@
|
||||||
|
export B10_FROM_BUILD
|
||||||
|
|
||||||
|
BIND10_MSGQ_SOCKET_FILE=@abs_top_builddir@/msgq_socket
|
||||||
|
export BIND10_MSGQ_SOCKET_FILE
|
Reference in New Issue
Block a user