BIND 10 trac1290, updated. 94282a20ab88b590e8b3b2522da2873ca81c632a [1290] put process output in subdir, and don't delete if scenario fails

BIND 10 source code commits bind10-changes at lists.isc.org
Wed Oct 26 15:20:44 UTC 2011


The branch, trac1290 has been updated
       via  94282a20ab88b590e8b3b2522da2873ca81c632a (commit)
       via  4ddb345fdc31614e191b0a0dea282ddd2ebe279e (commit)
       via  18b04945884fbcc1783e52bed845c34395c0acf8 (commit)
      from  673ef8efd5d474d66d62d134348730518160cbf9 (commit)

Those revisions listed above that are new to this repository have
not appeared on any other notification email; so we list those
revisions in full, below.

- Log -----------------------------------------------------------------
commit 94282a20ab88b590e8b3b2522da2873ca81c632a
Author: Jelte Jansen <jelte at isc.org>
Date:   Wed Oct 26 17:19:32 2011 +0200

    [1290] put process output in subdir, and don't delete if scenario fails

commit 4ddb345fdc31614e191b0a0dea282ddd2ebe279e
Author: Jelte Jansen <jelte at isc.org>
Date:   Wed Oct 26 16:59:36 2011 +0200

    [1290] when waiting for process output, give up after some time

commit 18b04945884fbcc1783e52bed845c34395c0acf8
Author: Jelte Jansen <jelte at isc.org>
Date:   Wed Oct 26 16:52:05 2011 +0200

    [1290] move python files to separate subdir

-----------------------------------------------------------------------

Summary of changes:
 .../features/{ => terrain}/bind10_control.py       |    0 
 tests/lettuce/features/{ => terrain}/querying.py   |    0 
 tests/lettuce/features/{ => terrain}/steps.py      |    0 
 tests/lettuce/features/{ => terrain}/terrain.py    |   35 ++++++++++++++++---
 4 files changed, 29 insertions(+), 6 deletions(-)
 rename tests/lettuce/features/{ => terrain}/bind10_control.py (100%)
 rename tests/lettuce/features/{ => terrain}/querying.py (100%)
 rename tests/lettuce/features/{ => terrain}/steps.py (100%)
 rename tests/lettuce/features/{ => terrain}/terrain.py (84%)

-----------------------------------------------------------------------
diff --git a/tests/lettuce/features/bind10_control.py b/tests/lettuce/features/bind10_control.py
deleted file mode 100644
index 66e1dfd..0000000
--- a/tests/lettuce/features/bind10_control.py
+++ /dev/null
@@ -1,60 +0,0 @@
-from lettuce import *
-import subprocess
-import re
-
-def check_lines(output, lines):
-    for line in lines:
-        if output.find(line) != -1:
-            return line
-
- at step('start bind10(?: with configuration (\S+))?' +\
-      '(?: with cmdctl port (\d+))?(?: as (\S+))?')
-def start_bind10(step, config_file, cmdctl_port, process_name):
-    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
-    # TODO what to do on failure?
-    message = world.processes.wait_for_stderr_str(process_name,
-                                                  ["BIND10_STARTUP_COMPLETE",
-                                                   "BIND10_STARTUP_ERROR"])
-    assert message == "BIND10_STARTUP_COMPLETE", "Got: " + str(message)
-
- at step('wait for bind10 auth (?:of (\w+) )?to start')
-def wait_for_auth(step, process_name):
-    if process_name is None:
-        process_name = "bind10"
-    world.processes.wait_for_stderr_str(process_name, ['AUTH_SERVER_STARTED'])
-
- at 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')
-
- at step('set bind10 configuration (\S+) to (.*)')
-def set_config_command(step, name, value):
-    args = ['bindctl', '-p', '47805']
-    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)
-
-
diff --git a/tests/lettuce/features/querying.py b/tests/lettuce/features/querying.py
deleted file mode 100644
index 689f653..0000000
--- a/tests/lettuce/features/querying.py
+++ /dev/null
@@ -1,142 +0,0 @@
-from lettuce import *
-import subprocess
-import re
-
-# 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
-#
-# last query should have <property> <value>
-#
-
-#
-# 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
-#
-# 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)
-#
-# 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):
-        args = [ 'dig', '+tries=1', '@' + 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 parse_header(self, 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)
-        elif line == ";; QUESTION SECTION:\n":
-            self.line_handler = self.parse_question
-
-    def parse_question(self, line):
-        if line == ";; ANSWER SECTION:\n":
-            self.line_handler = self.parse_answer
-        elif line != "\n":
-            self.question_section.append(line)
-
-    def parse_answer(self, line):
-        if line == ";; AUTHORITY SECTION:\n":
-            self.line_handler = self.parse_authority
-        elif line != "\n":
-            self.answer_section.append(line)
-
-    def parse_authority(self, line):
-        if line == ";; ADDITIONAL SECTION:\n":
-            self.line_handler = self.parse_additional
-        elif line != "\n":
-            self.additional_section.append(line)
-
-    def parse_authority(self, line):
-        if line.startswith(";; Query time"):
-            self.line_handler = self.parse_footer
-        elif line != "\n":
-            self.additional_section.append(line)
-
-    def parse_footer(self, line):
-        pass
-
- at 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):
-    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
-
- at step('The SOA serial for ([\w.]+) should be ([0-9]+)')
-def query_soa(step, query_name, serial):
-    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
-
- at step('last query should have (\S+) (.+)')
-def check_last_query(step, item, 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)
diff --git a/tests/lettuce/features/steps.py b/tests/lettuce/features/steps.py
deleted file mode 100644
index 0c4d817..0000000
--- a/tests/lettuce/features/steps.py
+++ /dev/null
@@ -1,29 +0,0 @@
-#
-# 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
-
- at step('stop process (\w+)')
-def stop_a_named_process(step, process_name):
-    world.processes.stop_process(process_name)
-
- at step('wait for (new )?(\w+) stderr message (\w+)')
-def wait_for_message(step, new, process_name, message):
-    world.processes.wait_for_stderr_str(process_name, [message], new)
-
- at step('wait for (new )?(\w+) stdout message (\w+)')
-def wait_for_message(step, process_name, message):
-    world.processes.wait_for_stdout_str(process_name, [message], new)
-
- at step('Given I have no database')
-def given_i_have_no_database(step):
-    if os.path.exists("test.db"):
-        os.remove("test.db")
-
- at step('I should see a database file')
-def i_should_see_a_database_file(step):
-    assert os.path.exists("test.db")
-    os.remove("test.db")
diff --git a/tests/lettuce/features/terrain.py b/tests/lettuce/features/terrain.py
deleted file mode 100644
index 14a8883..0000000
--- a/tests/lettuce/features/terrain.py
+++ /dev/null
@@ -1,168 +0,0 @@
-#
-# 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
-
-# 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"]
-]
-
-# class that keeps track of one running process and the files
-# we created for it. This needs to be moved to our framework-framework
-# as it is not specifically for bind10
-class RunningProcess:
-    def __init__(self, step, process_name, args):
-        # set it to none first so destructor won't error if initializer did
-        self.process = None
-        self.step = step
-        self.process_name = process_name
-        self.remove_files_on_exit = True
-        self._create_filenames()
-        self._start_process(args)
-
-    def _start_process(self, args):
-        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):
-        filebase = re.sub("\s+", "_", filebase)
-        filebase = re.sub("[^a-zA-Z.\-_]", "", filebase)
-        return filebase + "." + extension
-
-    def _create_filenames(self):
-        filebase = self.step.scenario.feature.name + "-" +\
-                   self.step.scenario.name + "-" + self.process_name
-        self.stderr_filename = self.mangle_filename(filebase, "stderr")
-        self.stdout_filename = self.mangle_filename(filebase, "stdout")
-
-    def stop_process(self):
-        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):
-        os.remove(self.stderr_filename)
-        os.remove(self.stdout_filename)
-
-    def _wait_for_output_str(self, filename, running_file, strings, only_new):
-        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
-        while True:
-            where = running_file.tell()
-            line = running_file.readline()
-            if line:
-                for string in strings:
-                    if line.find(string) != -1:
-                        return string
-            else:
-                time.sleep(0.5)
-                running_file.seek(where)
-
-    def wait_for_stderr_str(self, strings, only_new = True):
-        return self._wait_for_output_str(self.stderr_filename, self.stderr,
-                                         strings, only_new)
-
-    def wait_for_stdout_str(self, strings, only_new = True):
-        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):
-        self.processes = {}
-    
-    def add_process(self, step, process_name, args):
-        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):
-        assert process_name in self.processes,\
-            "Process " + name + " unknown"
-        return self.processes[process_name]
-
-    def stop_process(self, process_name):
-        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):
-        for process in self.processes.values():
-            process.stop_process()
-    
-    def keep_files(self):
-        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 any of the given strings in the given processes stderr 
-        output. If only_new is True, it will only look at the lines that are 
-        printed to stderr since the last time this method was called. If 
-        False, it will also look at the previously printed lines. This will 
-        block until one of the strings is found. TODO: we may want to put in 
-        a timeout for this... Returns the string that is found"""
-        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 any of the given strings in the given processes stderr 
-        output. If only_new is True, it will only look at the lines that are 
-        printed to stderr since the last time this method was called. If 
-        False, it will also look at the previously printed lines. This will 
-        block until one of the strings is found. TODO: we may want to put in 
-        a timeout for this... Returns the string that is found"""
-        assert process_name in self.processes,\
-           "Process " + process_name + " unknown"
-        return self.processes[process_name].wait_for_stdout_str(strings,
-                                                                only_new)
-
- at before.each_scenario
-def initialize(feature):
-    # 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])
-
- at after.each_scenario
-def cleanup(feature):
-    # Stop any running processes we may have had around
-    world.processes.stop_all_processes()
diff --git a/tests/lettuce/features/terrain/bind10_control.py b/tests/lettuce/features/terrain/bind10_control.py
new file mode 100644
index 0000000..66e1dfd
--- /dev/null
+++ b/tests/lettuce/features/terrain/bind10_control.py
@@ -0,0 +1,60 @@
+from lettuce import *
+import subprocess
+import re
+
+def check_lines(output, lines):
+    for line in lines:
+        if output.find(line) != -1:
+            return line
+
+ at step('start bind10(?: with configuration (\S+))?' +\
+      '(?: with cmdctl port (\d+))?(?: as (\S+))?')
+def start_bind10(step, config_file, cmdctl_port, process_name):
+    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
+    # TODO what to do on failure?
+    message = world.processes.wait_for_stderr_str(process_name,
+                                                  ["BIND10_STARTUP_COMPLETE",
+                                                   "BIND10_STARTUP_ERROR"])
+    assert message == "BIND10_STARTUP_COMPLETE", "Got: " + str(message)
+
+ at step('wait for bind10 auth (?:of (\w+) )?to start')
+def wait_for_auth(step, process_name):
+    if process_name is None:
+        process_name = "bind10"
+    world.processes.wait_for_stderr_str(process_name, ['AUTH_SERVER_STARTED'])
+
+ at 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')
+
+ at step('set bind10 configuration (\S+) to (.*)')
+def set_config_command(step, name, value):
+    args = ['bindctl', '-p', '47805']
+    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)
+
+
diff --git a/tests/lettuce/features/terrain/querying.py b/tests/lettuce/features/terrain/querying.py
new file mode 100644
index 0000000..689f653
--- /dev/null
+++ b/tests/lettuce/features/terrain/querying.py
@@ -0,0 +1,142 @@
+from lettuce import *
+import subprocess
+import re
+
+# 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
+#
+# last query should have <property> <value>
+#
+
+#
+# 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
+#
+# 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)
+#
+# 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):
+        args = [ 'dig', '+tries=1', '@' + 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 parse_header(self, 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)
+        elif line == ";; QUESTION SECTION:\n":
+            self.line_handler = self.parse_question
+
+    def parse_question(self, line):
+        if line == ";; ANSWER SECTION:\n":
+            self.line_handler = self.parse_answer
+        elif line != "\n":
+            self.question_section.append(line)
+
+    def parse_answer(self, line):
+        if line == ";; AUTHORITY SECTION:\n":
+            self.line_handler = self.parse_authority
+        elif line != "\n":
+            self.answer_section.append(line)
+
+    def parse_authority(self, line):
+        if line == ";; ADDITIONAL SECTION:\n":
+            self.line_handler = self.parse_additional
+        elif line != "\n":
+            self.additional_section.append(line)
+
+    def parse_authority(self, line):
+        if line.startswith(";; Query time"):
+            self.line_handler = self.parse_footer
+        elif line != "\n":
+            self.additional_section.append(line)
+
+    def parse_footer(self, line):
+        pass
+
+ at 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):
+    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
+
+ at step('The SOA serial for ([\w.]+) should be ([0-9]+)')
+def query_soa(step, query_name, serial):
+    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
+
+ at step('last query should have (\S+) (.+)')
+def check_last_query(step, item, 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)
diff --git a/tests/lettuce/features/terrain/steps.py b/tests/lettuce/features/terrain/steps.py
new file mode 100644
index 0000000..0c4d817
--- /dev/null
+++ b/tests/lettuce/features/terrain/steps.py
@@ -0,0 +1,29 @@
+#
+# 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
+
+ at step('stop process (\w+)')
+def stop_a_named_process(step, process_name):
+    world.processes.stop_process(process_name)
+
+ at step('wait for (new )?(\w+) stderr message (\w+)')
+def wait_for_message(step, new, process_name, message):
+    world.processes.wait_for_stderr_str(process_name, [message], new)
+
+ at step('wait for (new )?(\w+) stdout message (\w+)')
+def wait_for_message(step, process_name, message):
+    world.processes.wait_for_stdout_str(process_name, [message], new)
+
+ at step('Given I have no database')
+def given_i_have_no_database(step):
+    if os.path.exists("test.db"):
+        os.remove("test.db")
+
+ at step('I should see a database file')
+def i_should_see_a_database_file(step):
+    assert os.path.exists("test.db")
+    os.remove("test.db")
diff --git a/tests/lettuce/features/terrain/terrain.py b/tests/lettuce/features/terrain/terrain.py
new file mode 100644
index 0000000..53bcc58
--- /dev/null
+++ b/tests/lettuce/features/terrain/terrain.py
@@ -0,0 +1,191 @@
+#
+# 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
+
+# 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"]
+]
+
+OUTPUT_WAIT_INTERVAL = 0.5
+OUTPUT_WAIT_MAX_INTERVALS = 10
+
+# class that keeps track of one running process and the files
+# we created for it. This needs to be moved to our framework-framework
+# as it is not specifically for bind10
+class RunningProcess:
+    def __init__(self, step, process_name, args):
+        # set it to none first so destructor won't error if initializer did
+        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):
+        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):
+        filebase = re.sub("\s+", "_", filebase)
+        filebase = re.sub("[^a-zA-Z.\-_]", "", 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/'
+        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):
+        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):
+        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):
+        os.remove(self.stderr_filename)
+        os.remove(self.stdout_filename)
+
+    def _wait_for_output_str(self, filename, running_file, strings, only_new):
+        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):
+        return self._wait_for_output_str(self.stderr_filename, self.stderr,
+                                         strings, only_new)
+
+    def wait_for_stdout_str(self, strings, only_new = True):
+        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):
+        self.processes = {}
+    
+    def add_process(self, step, process_name, args):
+        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):
+        assert process_name in self.processes,\
+            "Process " + name + " unknown"
+        return self.processes[process_name]
+
+    def stop_process(self, process_name):
+        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):
+        for process in self.processes.values():
+            process.stop_process()
+    
+    def keep_files(self):
+        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 any of the given strings in the given processes stderr 
+        output. If only_new is True, it will only look at the lines that are 
+        printed to stderr since the last time this method was called. If 
+        False, it will also look at the previously printed lines. This will 
+        block until one of the strings is found. TODO: we may want to put in 
+        a timeout for this... Returns the string that is found"""
+        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 any of the given strings in the given processes stderr 
+        output. If only_new is True, it will only look at the lines that are 
+        printed to stderr since the last time this method was called. If 
+        False, it will also look at the previously printed lines. This will 
+        block until one of the strings is found. TODO: we may want to put in 
+        a timeout for this... Returns the string that is found"""
+        assert process_name in self.processes,\
+           "Process " + process_name + " unknown"
+        return self.processes[process_name].wait_for_stdout_str(strings,
+                                                                only_new)
+
+ at before.each_scenario
+def initialize(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])
+
+ at after.each_scenario
+def cleanup(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()




More information about the bind10-changes mailing list