Add daemon communication test cases

This change adds an automated test harness that will spin up an instance
of the X52 daemon, connect to its command socket, send commands and
validate the responses. This first set of test cases simply validates
the basic configuration file handling. Subsequent commits will enhance
the tests to improve code coverage.
reverse-scroll
nirenjan 2022-04-07 23:24:24 -07:00
parent f0ad185421
commit 33e940606c
3 changed files with 251 additions and 0 deletions

View File

@ -95,6 +95,13 @@ EXTRA_DIST += \
daemon/x52dcomm-internal.h \
daemon/x52d.conf
# Test cases
EXTRA_DIST += \
daemon/test_daemon_comm.py \
daemon/tests/comm/config_args.tc
TESTS += daemon/test_daemon_comm.py
if HAVE_SYSTEMD
if !IS_MAKE_DISTCHECK
SED_ARGS = s,%bindir%,$(bindir),g

View File

@ -0,0 +1,181 @@
#!/usr/bin/env python3
"""Test communication with x52d and verify that the behavior is as expected"""
import glob
import os
import os.path
import shlex
import signal
import socket
import subprocess
import tempfile
import time
import sys
class TestCase:
"""TestCase class handles an individual test case"""
def __init__(self, data):
"""Create a new test case"""
self.desc = None
self.in_cmd = None
self.exp_resp = None
self.parse(data)
def parse(self, data):
"""Parses a string of the following form:
<description>
space separated input line, possibly quoted
space separated expected response, possibly quoted
"""
self.desc, in_cmd, exp_resp = data.splitlines()
self.in_cmd = '\0'.join(shlex.split(in_cmd)).encode() + b'\0'
self.exp_resp = '\0'.join(shlex.split(exp_resp)).encode() + b'\0'
def execute(self, index, cmdsock):
"""Execute the test case and return the result in TAP format"""
def dump_failed(name, value):
"""Dump the failed test case"""
print("# {}".format(name))
for argv in value.decode().split('\0'):
print("#\t {}".format(argv))
print()
def print_result(passed):
"""Print the test case result and description"""
out = "ok {} - {}".format(index+1, self.desc)
if not passed:
out = "not " + out
print(out)
cmdsock.send(self.in_cmd)
got_resp = cmdsock.recv(1024)
if got_resp != self.exp_resp:
print_result(False)
dump_failed("Expected", self.exp_resp)
dump_failed("Got", got_resp)
else:
print_result(True)
class Test:
"""Test class runs a series of unit tests"""
def __init__(self):
"""Create a new instance of the Test class"""
self.program = self.find_daemon_program()
self.tmpdir = tempfile.TemporaryDirectory()
self.command = os.path.join(self.tmpdir.name, "x52d.cmd")
self.cmdsock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
self.daemon = None
self.testcases = []
def __enter__(self):
"""Context manager entry"""
self.launch_daemon()
return self
def __exit__(self, *exc):
"""Context manager exit"""
self.terminate_daemon()
self.tmpdir.cleanup()
@staticmethod
def find_daemon_program():
"""Find the daemon program. This script should be run from the
root of the build directory"""
daemon_candidates = glob.glob('**/x52d', recursive=True)
if not daemon_candidates:
print("Bail out! Unable to find X52 daemon.")
sys.exit(1)
return os.path.realpath(daemon_candidates[0])
def launch_daemon(self):
"""Launch an instance of the running daemon"""
if self.daemon is not None:
# We've already started the daemon, check if it is still running
if self.daemon.poll() is None:
return
self.daemon = None
daemon_cmdline = [
self.program,
"-f", # Run in foreground
"-v", # Verbose logging
"-c", os.path.join(self.tmpdir.name, "x52d.cfg"), # Default config file
"-l", os.path.join(self.tmpdir.name, "x52d.log"), # Output logs to log file
"-p", os.path.join(self.tmpdir.name, "x52d.pid"), # PID file
"-s", self.command, # Command socket path
]
# Create empty config file
with open(daemon_cmdline[4], 'w'):
pass
self.daemon = subprocess.Popen(daemon_cmdline)
print("# Sleeping 2 seconds for daemon to start")
time.sleep(2)
self.cmdsock.connect(self.command)
def terminate_daemon(self):
"""Terminate a running daemon"""
if self.daemon is None:
return
# Send a SIGTERM to the daemon
os.kill(self.daemon.pid, signal.SIGTERM)
try:
self.daemon.wait(timeout=15)
except subprocess.TimeoutExpired:
# Forcibly kill the running process
self.daemon.kill()
finally:
self.daemon = None
self.cmdsock.close()
def append(self, testcase):
"""Add one testcase to the test case list"""
self.testcases.append(testcase)
def extend(self, testcases):
"""Add one or more testcases to the test case list"""
self.testcases.extend(testcases)
@staticmethod
def dump_failed(name, value):
"""Dump the failed test case"""
print("# {}".format(name))
for argv in value.decode().split('\0'):
print("#\t {}".format(argv))
print()
def run_tests(self):
"""Run test cases"""
print("1..{}".format(len(self.testcases)))
for index, testcase in enumerate(self.testcases):
testcase.execute(index, self.cmdsock)
def find_and_parse_testcase_files(self):
"""Find and parse *.tc files"""
basedir = os.path.dirname(os.path.realpath(__file__))
pattern = os.path.join(basedir, '**', 'comm', '*.tc')
tc_files = glob.glob(pattern, recursive=True)
for tc_file in tc_files:
with open(tc_file) as tc_fd:
# Test cases are separated by blank lines
testcases = tc_fd.read().split('\n\n')
self.extend(TestCase(tc_data) for tc_data in testcases)
def main():
"""Main routine adds test cases to the Test class and runs them"""
with Test() as test:
test.find_and_parse_testcase_files()
test.run_tests()
if __name__ == '__main__':
main()

View File

@ -0,0 +1,63 @@
Configuration with insufficient arguments
config
ERR "Insufficient arguments for 'config' command"
Load with invalid file argument
config load ''
ERR "Invalid file '' for 'config load' command"
Load with nonexistent file argument
config load /nonexistent
ERR "Invalid file '/nonexistent' for 'config load' command"
Load with empty file argument
config load /dev/null
OK config load /dev/null
Load with extra arguments
config load /dev/null ''
ERR "Unexpected arguments for 'config load' command; got 4, expected 3"
Load with missing argument
config load
ERR "Unexpected arguments for 'config load' command; got 2, expected 3"
Reload configuration
config reload
OK config reload
Reload configuration with extra arguments
config reload ''
ERR "Unexpected arguments for 'config reload' command; got 3, expected 2"
Dump configuration with insufficient arguments
config dump
ERR "Unexpected arguments for 'config dump' command; got 2, expected 3"
Dump configuration with invalid file
config dump ''
ERR "Invalid file '' for 'config dump' command"
Dump configuration with extra arguments
config dump /dev/null ''
ERR "Unexpected arguments for 'config dump' command; got 4, expected 3"
Dump configuration to /dev/null
config dump /dev/null
OK config dump /dev/null
Save configuration
config save
OK config save
Save configuration with extra arguments
config save ''
ERR "Unexpected arguments for 'config save' command; got 3, expected 2"
Config command with empty subcommand
config ''
ERR "Unknown subcommand '' for 'config' command"
Config command with unknown subcommand
config foo
ERR "Unknown subcommand 'foo' for 'config' command"