This is an automated email from the git hooks/post-receive script. It was
generated because a ref change was pushed to the repository containing
the project "Tarantool -- an efficient key/value data store".
The branch test-runner has been created
at 1a9bd036e09cf3fdba12d6a70a5365dd14fecac3 (commit)
commit 1a9bd036e09cf3fdba12d6a70a5365dd14fecac3
Author: Konstantin Osipov <[email protected]>
Date: Thu Dec 2 20:42:50 2010 +0300
Initial commit for test-run.py - test-runner.
Implement
https://blueprints.launchpad.net/tarantool/+spec/tarantool-test-runner.
This commit adds an implementation of test running framework
(test-run.py), and a simplistic interactive client for
the administartive console of tarantool (admin.py).
A prototype of the first test suite is added in
directory test/box.
"run" is a convenience symlink to test-run.py.
This commit also adds "make test" goal to the
top-level makefile.
diff --git a/.gitignore b/.gitignore
index a3dc76d..700a6ba 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,11 +2,12 @@
.gdb_history
TAGS
_*
-*.cfg
config.mk
lcov
*.o
*.d
-tarantool*
*.snap
*.xlog
+tarantool_version.h
+tarantool_version.h_
+test/var
diff --git a/Makefile b/Makefile
index 25e9fa8..b0bdb07 100644
--- a/Makefile
+++ b/Makefile
@@ -30,6 +30,11 @@ else
include $(SRCDIR)/scripts/rules.mk
endif
+.PHONY: test
+test:
+ cd ./test && ./test-run.py
+
+
ifeq ("$(origin module)", "command line")
.PHONY: clean
clean:
diff --git a/test/admin.py b/test/admin.py
new file mode 100755
index 0000000..9ecd7ae
--- /dev/null
+++ b/test/admin.py
@@ -0,0 +1,92 @@
+#! /usr/bin/python
+
+__author__ = "Konstantin Osipov <[email protected]>"
+
+import argparse
+import socket
+import sys
+import string
+
+class Options:
+ def __init__(self):
+ """Add all program options, with their defaults."""
+
+ parser = argparse.ArgumentParser(
+ description = "Tarantool regression test suite client.")
+
+ parser.add_argument(
+ "--host",
+ dest = 'host',
+ metavar = "host",
+ default = "localhost",
+ help = "Host to connect to. Default: localhost")
+
+ parser.add_argument(
+ "--port",
+ dest = "port",
+ default = 33015,
+ help = "Server port to connect to. Default: 33015")
+
+ self.args = parser.parse_args()
+
+
+class Connection:
+ def __init__(self, host, port):
+ self.host = host
+ self.port = port
+ self.is_connected = False
+
+ def connect(self):
+ self.socket = socket.socket(
+ socket.AF_INET, socket.SOCK_STREAM)
+ self.socket.connect((self.host, self.port))
+ self.is_connected = True
+
+ def disconnect(self):
+ if self.is_connected:
+ self.socket.close()
+ self.is_connected = False
+
+ def execute(self, command):
+ self.socket.sendall(command)
+
+ bufsiz = 4096
+ res = ""
+
+ while True:
+ buf = self.socket.recv(bufsiz)
+ if not buf:
+ break
+ res+= buf;
+ if res.rfind("---\n"):
+ break
+
+ return res
+
+ def __enter__(self):
+ self.connect()
+ return self
+
+ def __exit__(self, type, value, tb):
+ self.disconnect()
+
+
+def main():
+ options = Options()
+ try:
+ with Connection(options.args.host, options.args.port) as con:
+ res_sep = "r> "
+ for line in iter(sys.stdin.readline, ""):
+ if line.find(res_sep) == 0:
+ continue
+ print line,
+ output = con.execute(line)
+ print res_sep, string.join(output.split("\n"), "\n"+res_sep)
+ return 0
+ except (RuntimeError, socket.error) as e:
+ print "Fatal error: ", repr(e)
+ return -1
+
+if __name__ == "__main__":
+ exit(main())
+
diff --git a/test/box/show.test b/test/box/show.test
new file mode 100644
index 0000000..f98d2d0
--- /dev/null
+++ b/test/box/show.test
@@ -0,0 +1,8 @@
+show stat
+r> statistics:
+r> INSERT: { rps: 0 , total: 0 }
+r> SELECT_LIMIT: { rps: 0 , total: 0 }
+r> SELECT: { rps: 0 , total: 0 }
+r> UPDATE_FIELDS: { rps: 0 , total: 0 }
+r> ---
+r>
diff --git a/test/box/suite.ini b/test/box/suite.ini
new file mode 100644
index 0000000..7afa15a
--- /dev/null
+++ b/test/box/suite.ini
@@ -0,0 +1,5 @@
+[default]
+description = tarantool/silverbox, minimal configuration
+client = admin.py
+config = tarantool.cfg
+pidfile = box.pid
diff --git a/test/box/tarantool.cfg b/test/box/tarantool.cfg
new file mode 100644
index 0000000..c09c23a
--- /dev/null
+++ b/test/box/tarantool.cfg
@@ -0,0 +1,16 @@
+slab_alloc_arena = 0.1
+
+pid_file = "box.pid"
+
+primary_port = 33013
+secondary_port = 33014
+admin_port = 33015
+
+rows_per_wal = 50
+
+namespace[0].enabled = 1
+namespace[0].index[0].type = "HASH"
+namespace[0].index[0].unique = 1
+namespace[0].index[0].key_field[0].fieldno = 0
+namespace[0].index[0].key_field[0].type = "NUM"
+
diff --git a/test/run b/test/run
new file mode 120000
index 0000000..f2e04c0
--- /dev/null
+++ b/test/run
@@ -0,0 +1 @@
+./test-run.py
\ No newline at end of file
diff --git a/test/test-run.py b/test/test-run.py
new file mode 100755
index 0000000..3879af0
--- /dev/null
+++ b/test/test-run.py
@@ -0,0 +1,399 @@
+#! /usr/bin/python
+"""Tarantool regression test suite front-end."""
+
+__author__ = "Konstantin Osipov <[email protected]>"
+
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+# 1. Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# 2. Redistributions in binary form must reproduce the above copyright
+# notice, this list of conditions and the following disclaimer in the
+# documentation and/or other materials provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY AUTHOR AND CONTRIBUTORS ``AS IS'' AND
+# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL AUTHOR OR CONTRIBUTORS BE LIABLE
+# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+# SUCH DAMAGE.
+
+import argparse
+import os
+import os.path
+import signal
+import sys
+import stat
+import glob
+import shutil
+import ConfigParser
+import subprocess
+import pexpect
+import time
+import collections
+import difflib
+import filecmp
+
+#
+# Run a collection of tests.
+#
+# @todo
+# --gdb
+# put class definitions into separate files
+
+############################################################################
+# Class definition
+############################################################################
+
+class TestRunException(RuntimeError):
+ """A common exception to use across the program."""
+ def __init__(self, message):
+ self.message = message
+ def __str__(self):
+ return self.message
+
+class Options:
+ """Handle options of test-runner"""
+ def __init__(self):
+ """Add all program options, with their defaults. We assume
+ that the program is started from the directory where it is
+ located"""
+
+ parser = argparse.ArgumentParser(
+ description = "Tarantool regression test suite front-end. \
+This program must be started from its working directory (" +
+os.path.abspath(os.path.dirname(sys.argv[0])) + ").")
+
+ parser.epilog = "For a complete description, use 'pydoc ./" +\
+ os.path.basename(sys.argv[0]) + "'"
+
+ parser.add_argument(
+ "tests",
+ metavar="list of tests",
+ nargs="*",
+ default = [""],
+ help="""Can be empty. List of tests names, to look for in suites. Each
+ name is used as a substring to look for in the path to test file,
+ e.g. "show" will run all tests that have "show" in their name in all
+ suites, "box/show" will only enable tests starting with "show" in
+ "box" suite. Default: run all tests in all specified suites.""")
+
+ parser.add_argument(
+ "--suite",
+ dest = 'suites',
+ metavar = "suite",
+ nargs="*",
+ default = ["box"],
+ help = """List of tests suites to look for tests in. Default: "box".""")
+
+ parser.add_argument(
+ "--force",
+ dest = "is_force",
+ action = "store_true",
+ default = False,
+ help = "Go on with other tests in case of an individual test failure."
+ " Default: false.")
+
+ parser.add_argument(
+ "--start-and-exit",
+ dest = "start_and_exit",
+ action = "store_true",
+ default = False,
+ help = "Start the server from the first specified suite and"
+ "exit without running any tests. Default: false.")
+
+ parser.add_argument(
+ "--bindir",
+ dest = "bindir",
+ default = "../_debug_box",
+ help = "Path to server binary."
+ " Default: " + "../_debug_box.")
+
+ parser.add_argument(
+ "--vardir",
+ dest = "vardir",
+ default = "var",
+ help = "Path to data directory. Default: var.")
+
+ self.check(parser)
+ self.args = parser.parse_args()
+
+ def check(self, parser):
+ """Check that the program is started from the directory where
+ it is located. This is necessary to minimize potential confusion
+ with absolute paths, since all default paths are relative to the
+ starting directory."""
+
+ if not os.path.exists(os.path.basename(sys.argv[0])):
+ parser.print_help()
+ exit(-1)
+
+
+class Server:
+ """Server represents a single server instance. Normally, the
+ program operates with only one server, but in future we may add
+ replication slaves. The server is started once at the beginning
+ of each suite, and stopped at the end."""
+
+ def __init__(self, args, config, pidfile):
+ self.args = args
+ self.path_to_config = config
+ self.path_to_pidfile = os.path.join(args.vardir, pidfile)
+ self.path_to_exe = None
+ self.abspath_to_exe = None
+ self.is_started = False
+
+ def start(self):
+ """Start server instance: check if the old one exists, kill it
+ if necessary, create necessary directories and files, start
+ the server. Currently this is implemented for tarantool_silverbox
+ only."""
+
+ if not self.is_started:
+ print "Starting the server..."
+
+ if self.path_to_exe == None:
+ self.path_to_exe = self.find_exe()
+ self.abspath_to_exe = os.path.abspath(self.path_to_exe)
+
+ print " Found executable at " + self.path_to_exe + "."
+
+ print " Creating and populating working directory in " +\
+ self.args.vardir + "..."
+
+ if os.access(self.args.vardir, os.F_OK):
+ print " Found old vardir, deleting..."
+ self.kill_old_server()
+ shutil.rmtree(self.args.vardir, ignore_errors = True)
+
+ os.mkdir(self.args.vardir)
+ shutil.copy(self.path_to_config, self.args.vardir)
+
+ subprocess.check_call([self.abspath_to_exe, "--init_storage"],
+ cwd = self.args.vardir,
+ stdout = subprocess.PIPE,
+ stderr = subprocess.PIPE)
+
+ if self.args.start_and_exit:
+ subprocess.check_call([self.abspath_to_exe, "--daemonize"],
+ cwd = self.args.vardir,
+ stdout = subprocess.PIPE,
+ stderr = subprocess.PIPE)
+ else:
+ self.server = pexpect.spawn(self.abspath_to_exe,
+ cwd = self.args.vardir)
+ self.logfile_read = sys.stdout
+ self.server.expect_exact("entering event loop")
+
+ version = subprocess.Popen([self.abspath_to_exe, "--version"],
+ cwd = self.args.vardir,
+ stdout = subprocess.PIPE).stdout.read().rstrip()
+
+ print "Started {0} {1}.".format(os.path.basename(self.abspath_to_exe),
+ version)
+ self.is_started = True
+ else:
+ print "The server is already started."
+
+ def stop(self):
+ """Stop server instance."""
+ if self.is_started:
+ print "Stopping the server..."
+ self.server.terminate()
+ self.server.expect(pexpect.EOF)
+ self.is_started = False
+ else:
+ print "The server is not started."
+
+ def find_exe(self):
+ """Locate server executable in the bindir. We just take
+ the first thing we find."""
+
+ if (os.access(self.args.bindir, os.F_OK) == False or
+ stat.S_ISDIR(os.stat(self.args.bindir).st_mode) == False):
+ raise TestRunException("Directory " + self.args.bindir +
+ " doesn't exist")
+
+ for f in os.listdir(self.args.bindir):
+ f = os.path.join(self.args.bindir, f)
+ st_mode = os.stat(f).st_mode
+ if stat.S_ISREG(st_mode) and st_mode & stat.S_IXUSR:
+ return f
+ raise TestRunException("Can't find server executable in " +
+ self.args.bindir)
+
+ def kill_old_server(self):
+ """Kill old server instance if it exists."""
+ if os.access(self.path_to_pidfile, os.F_OK) == False:
+ return # Nothing to do
+ pid = 0
+ with open(self.path_to_pidfile) as f:
+ pid = int(f.read())
+ print " Found old server, pid {0}, killing...".format(pid)
+ try:
+ os.kill(pid, signal.SIGTERM)
+ while os.kill(pid, 0) != -1:
+ time.sleep(0.01)
+ except OSError:
+ pass
+
+class Test:
+ def __init__(self, name, client):
+ self.name = name
+ self.client = os.path.join(".", client)
+ self.result = name.replace(".test", ".result")
+ self.is_executed = False
+ self.passed = None
+ self.is_equal_result = None
+
+ def run(self, is_force):
+ """Execute the client program, giving it test as stdin,
+ result as stdout. If the client program aborts, print
+ its output to stdout, and raise an exception. Else, comprare
+ result and reject files. If there is a difference, print it to
+ stdout and raise an exception."""
+
+ sys.stdout.write("{0}".format(self.name))
+
+ with open(self.name, "r") as test:
+ with open(self.result, "w+") as result:
+ self.passed = \
+ subprocess.call([self.client], stdin = test, stdout = result) == 0
+
+ self.is_executed = True
+
+ if self.passed:
+ self.is_equal_result = filecmp.cmp(self.name, self.result);
+
+ if self.passed and self.is_equal_result:
+ print "\t\t\t[ pass ]"
+ else:
+ print "\t\t\t[ fail ]"
+ if not self.passed:
+ self.print_diagnostics()
+ else:
+ self.print_unidiff()
+
+ if not is_force:
+ if not self.passed:
+ raise TestRunException("Failed to run test " + self.name +
+ ": client execution aborted")
+ elif not self.is_equal_result:
+ raise TestRunException("Failed to run test " + self.name +
+ ": wrong test output")
+
+ os.remove(self.result)
+
+ def print_diagnostics(self):
+ """Print 10 lines of client program output leading to test
+ failure. Used to diagnose a failure of the client program"""
+
+ print "Test failed! Last 10 lines of the result file:"
+ with open(self.result, "r+") as result:
+ tail_10 = collections.deque(result, 10)
+ for line in tail_10:
+ sys.stdout.write(line)
+
+ def print_unidiff(self):
+ """Print a unified diff between .test and .result files. used
+ to establish the cause of a failure when .test differs
+ from .result"""
+
+ print "Test failed! Result content mismatch:"
+ with open(self.name, "r") as test:
+ with open(self.result, "r") as result:
+ test_time = time.ctime(os.stat(self.name).st_mtime)
+ result_time = time.ctime(os.stat(self.result).st_mtime)
+ for line in difflib.unified_diff(test.readlines(),
+ result.readlines(), self.name, self.result,
+ test_time, result_time):
+ sys.stdout.write(line)
+
+
+class TestSuite:
+# Dont' forget to update epilog
+ """Each test suite contains a number of related tests files,
+ located in the same directory on disk. Each test file has
+ extention .test and contains a listing of server commands,
+ followed by their output. The commands are executed, and
+ obtained results are compared with pre-recorded output. In case
+ of comparision difference, an exception is raised. A test suite
+ must also contain suite.ini, which describes how to start the
+ server for this suite, the client program to execute individual
+ tests and other suite properties. The server is started once per
+ suite."""
+
+ def __init__(self, suite_path, args):
+ """Initialize a test suite: check that it exists and contains
+ a syntactically correct configuration file. Then create
+ a test instance for each found test."""
+ self.path = suite_path
+ self.args = args
+ self.tests = []
+
+ if os.access(self.path, os.F_OK) == False:
+ raise TestRunException("Suite \"" + self.path + "\" doesn't exist")
+
+ config = ConfigParser.ConfigParser()
+ config.read(os.path.join(self.path, "suite.ini"))
+ self.ini = dict(config.items("default"))
+ print "Collecting tests in \"" + self.path + "\": " +\
+ self.ini["description"] + "."
+
+ for test_name in glob.glob(os.path.join(self.path, "*.test")):
+ for test_pattern in self.args.tests:
+ if test_name.find(test_pattern) != -1:
+ self.tests.append(Test(test_name, self.ini["client"]))
+ print "Found " + str(len(self.tests)) + " tests."
+
+ def run_all(self):
+ """For each file in the test suite, run client program
+ assuming each file represents an individual test."""
+ server = Server(self.args, os.path.join(self.path, self.ini["config"]),
+ self.ini["pidfile"])
+ server.start()
+ if self.args.start_and_exit:
+ print " Start and exit requested, exiting..."
+ exit(0)
+
+ longsep =
"=============================================================================="
+ shortsep = "------------------------------------------------------------"
+ print longsep
+ print "TEST\t\t\t\tRESULT"
+ print shortsep
+
+ for test in self.tests:
+ test.run(self.args.is_force)
+
+ print shortsep
+ server.stop();
+
+#######################################################################
+# Program body
+#######################################################################
+
+def main():
+ options = Options()
+
+ try:
+ print "Started", "".join(sys.argv)
+ suites = []
+ for suite_name in options.args.suites:
+ suites.append(TestSuite(suite_name, options.args))
+
+ for suite in suites:
+ suite.run_all()
+ except RuntimeError as e:
+ print "\nFatal error: ", e, ". Execution aborted."
+ return (-1)
+
+ return 0
+
+if __name__ == "__main__":
+ exit(main())
--
Tarantool -- an efficient key/value data store
_______________________________________________
Mailing list: https://launchpad.net/~tarantool-developers
Post to : [email protected]
Unsubscribe : https://launchpad.net/~tarantool-developers
More help : https://help.launchpad.net/ListHelp