mirror of
https://gitlab.isc.org/isc-projects/kea
synced 2025-09-01 22:45:18 +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/gen_wiredata.py
|
||||
src/lib/server_common/tests/data_path.h
|
||||
tests/lettuce/setup_intree_bind10.sh
|
||||
tests/system/conf.sh
|
||||
tests/system/run.sh
|
||||
tests/system/glue/setup.sh
|
||||
|
@@ -675,6 +675,8 @@ class BoB:
|
||||
args = ["b10-cmdctl"]
|
||||
if self.cmdctl_port is not None:
|
||||
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)
|
||||
|
||||
def start_all_processes(self):
|
||||
|
@@ -45,6 +45,5 @@ export B10_FROM_BUILD
|
||||
BIND10_MSGQ_SOCKET_FILE=@abs_top_builddir@/msgq_socket
|
||||
export BIND10_MSGQ_SOCKET_FILE
|
||||
|
||||
cd ${BIND10_PATH}
|
||||
exec ${PYTHON_EXEC} -O bind10 "$@"
|
||||
exec ${PYTHON_EXEC} -O ${BIND10_PATH}/bind10 "$@"
|
||||
|
||||
|
@@ -133,19 +133,23 @@ class BindCmdInterpreter(Cmd):
|
||||
'''Parse commands from user and send them to cmdctl. '''
|
||||
try:
|
||||
if not self.login_to_cmdctl():
|
||||
return
|
||||
return 1
|
||||
|
||||
self.cmdloop()
|
||||
print('\nExit from bindctl')
|
||||
return 0
|
||||
except FailToLogin as err:
|
||||
# error already printed when this was raised, ignoring
|
||||
pass
|
||||
return 1
|
||||
except KeyboardInterrupt:
|
||||
print('\nExit from bindctl')
|
||||
return 0
|
||||
except socket.error as err:
|
||||
print('Failed to send request, the connection is closed')
|
||||
return 1
|
||||
except http.client.CannotSendRequest:
|
||||
print('Can not send request, the connection is busy')
|
||||
return 1
|
||||
|
||||
def _get_saved_user_info(self, dir, file_name):
|
||||
''' Read all the available username and password pairs saved in
|
||||
@@ -523,6 +527,7 @@ class BindCmdInterpreter(Cmd):
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
def _get_module_startswith(self, text):
|
||||
return [module
|
||||
for module in self.modules
|
||||
|
@@ -146,4 +146,5 @@ if __name__ == '__main__':
|
||||
tool = BindCmdInterpreter(server_addr, pem_file=options.cert_chain,
|
||||
csv_file_dir=options.csv_file_dir)
|
||||
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
|
||||
socket_err_output = io.StringIO()
|
||||
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",
|
||||
socket_err_output.getvalue())
|
||||
socket_err_output.close()
|
||||
@@ -350,7 +350,7 @@ class TestConfigCommands(unittest.TestCase):
|
||||
# validate log message for http.client.CannotSendRequest
|
||||
cannot_send_output = io.StringIO()
|
||||
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",
|
||||
cannot_send_output.getvalue())
|
||||
cannot_send_output.close()
|
||||
|
@@ -460,6 +460,8 @@ class SecureHTTPServer(socketserver_mixin.NoPollMixIn,
|
||||
socketserver_mixin.NoPollMixIn.__init__(self)
|
||||
try:
|
||||
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:
|
||||
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):
|
||||
''' 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,
|
||||
CommandControl, idle_timeout, verbose)
|
||||
|
||||
httpd.serve_forever()
|
||||
|
||||
def check_port(option, opt_str, value, parser):
|
||||
@@ -607,6 +608,8 @@ if __name__ == '__main__':
|
||||
(options, args) = parser.parse_args()
|
||||
result = 1 # in case of failure
|
||||
try:
|
||||
if options.verbose:
|
||||
logger.set_severity("DEBUG", 99)
|
||||
run(options.addr, options.port, options.idle_timeout, options.verbose)
|
||||
result = 0
|
||||
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
|
||||
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
|
||||
There was a keyboard interrupt signal to stop the cmdctl daemon. The
|
||||
daemon will now shut down.
|
||||
|
@@ -18,6 +18,7 @@ import struct
|
||||
import os
|
||||
import copy
|
||||
import subprocess
|
||||
import copy
|
||||
from isc.log_messages.bind10_messages import *
|
||||
from libutil_io_python import recv_fd
|
||||
|
||||
|
@@ -123,6 +123,7 @@ class ConfigManagerData:
|
||||
output_file_name is not specified, the file used in
|
||||
read_from_file is used."""
|
||||
filename = None
|
||||
|
||||
try:
|
||||
file = tempfile.NamedTemporaryFile(mode='w',
|
||||
prefix="b10-config.db.",
|
||||
|
@@ -37,7 +37,7 @@ class TestConfigManagerData(unittest.TestCase):
|
||||
It shouldn't append the data path to it.
|
||||
"""
|
||||
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(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