commit 61c087a0386ad04fd5579ad170b9d7933ed90c8c Author: Jonas Eriksson Date: Fri Apr 25 16:00:12 2014 +0200 Add cyclictest test case Cyclictest is a part of rt-tests used to test the responsiveness of a system. While not developed for non-realtime systems, it can be used to detect unexpected spikes in latencies within virtual machines. Signed-off-by: Jonas Eriksson Upstream-Status: Pending diff --git a/qemu/tests/cfg/cyclictest.cfg b/qemu/tests/cfg/cyclictest.cfg new file mode 100644 index 0000000..d142b7d --- /dev/null +++ b/qemu/tests/cfg/cyclictest.cfg @@ -0,0 +1,25 @@ +- cyclictest: + virt_test_type = qemu + only Linux + type = cyclictest + kill_vm = yes + kill_vm_gracefully = no + backup_image_before_testing = yes + + # It is possible to tune the arguments to cyclictest using the + # cyclictest_args variable. + #cyclictest_args = "-D 30m --smp" + # It is formatted using a dictionary where a number of properties of the + # target is available, for example 'num_cpus' which is the number of cpus, + # and 'mask_all_cpus', which is the string "0-[num_cpus - 1]". See + # cyclictest.py for all available values. The following is for example + # equivalent to "-D 30m --smp": + #cyclictest_args = "-D 30m -t %(num_cpus)s -a %(mask_all_cpus)s" + # Default timeout is 35 min + #cyclictest_timeout_s = 2100 + + # After the run, the test case will check all values against the below + # allowed maximum values of each result. + #allowed_max = 50 + #allowed_avg = 30 + #allowed_act = diff --git a/qemu/tests/cyclictest.py b/qemu/tests/cyclictest.py new file mode 100644 index 0000000..0ad447f --- /dev/null +++ b/qemu/tests/cyclictest.py @@ -0,0 +1,167 @@ +import logging +import re +from autotest.client import utils +from autotest.client.shared import error +from virttest import remote, utils_misc, utils_test + + +# Information about cyclictest is available here: +# https://rt.wiki.kernel.org/index.php/Cyclictest +@error.context_aware +def run(test, params, env): + """ + Test Steps: + + 1. Check availability of cyclictest + 2. Get information about the target + 3. Run cyclictest + 4. Parse and check result + + Params: + :param test: QEMU test object. + :param params: Dictionary with the test parameters. + :param env: Dictionary with test environment. + """ + + def parse_cyclictest(output_string): + """ + Parses the output from a cyclictest run into a nested dictionary. + + Example input: + # /dev/cpu_dma_latency set to 0us + T: 0 ( 7064) P: 0 I:1000 C: 2000 Min: 1 Act: 2 Avg: 3 Max: 751 + T: 1 ( 7065) P: 0 I:1500 C: 1334 Min: 1 Act: 3 Avg: 2 Max: 167 + Example output: + { '0': { 'Act': '2', + 'Avg': '3', + 'C': '2000', + 'I': '1000', + 'Max': '751', + 'Min': '1', + 'P': '0', + 'T': '0 ( 7064)'}, + '1': { 'Act': '3', + 'Avg': '2', + 'C': '1334', + 'I': '1500', + 'Max': '167', + 'Min': '1', + 'P': '0', + 'T': '1 ( 7065)'} + } + + Params: + :param output_string: Output data from cyclictest + :returns: Nested dict of parsed output + """ + output = output_string.split("\n") + result = {} + for row_string in output: + # Only process lines beginning with "T:" + if re.match("^T:", row_string): + # Since the different segments of a line are not comma + # separated (or similar), it is tricky to split them right + # away. Instead, match the "header" of an entry (e.g. "P:"), + # and add a line break before them. Then split the string using + # the added line breaks. + row_list = re.sub("\S+:", "\n\g<0>", row_string).split("\n") + sub_result = {} + for part in row_list: + part = re.sub("\s+$", "", part) + if not part == "": + # The strings are now on the form "P: 0" (for example) + kv = re.split(":\s*", part) + sub_result[kv[0]] = kv[1] + # Use the thread as an index in the outer result dict. Avoid + # using an array to be able to process results in any order and + # with gaps. + thread = (sub_result['T'].split(" "))[0] + result[thread] = sub_result + return result + + def check_val_ceiling(allowed_ceil, val, name, cpu): + """ + Check that the value :val: is not larger than :allowed_ceil:, and if so + log the error using :name: as the name of the value. + + Params: + :allowed_ceil: Maximum allowed vaule of :val:. If None, "" or 0, + just return 0. + :val: The value to be tested + :name: The name of the value, for logging + :cpu: CPU number for error reporting + :returns: Number of errors; 0 if successful. + """ + if allowed_ceil > 0 and val > allowed_ceil: + logging.error("%s value too large: %d for CPU %d" % (name, + val, + cpu)) + return 1 + return 0 + + # Get/setup VM and session + vm_name = params.get("vm_name", "main_vm") + vm = env.get_vm(params[vm_name]) + vm.verify_alive() + timeout = int(params.get("timeout", 2400)) + session = vm.wait_for_login(timeout=timeout) + + try: + error.context("Check availability of cyclictest") + if session.cmd_status("which cyclictest"): + raise error.TestFail("Test application not available") + + error.context("Get information about the target") + # Get the number of CPUs on target + cmd = "grep -c '^processor[[:space:]]*:' /proc/cpuinfo" + output = session.cmd_output(cmd) + num_cpus = int(output) + if num_cpus == 0: + raise error.TestFail("Unable to get number of CPUs on target") + + # Get arguments from the configuration + default_args = "-D 30m --smp" + cyclictest_args = params.get("cyclictest_args", default_args) + fmt_dict = { + 'num_cpus': num_cpus, + 'last_cpu': num_cpus - 1, + 'mask_all_cpus': "0-%d" % (num_cpus - 1), + } + formatted_args = cyclictest_args % fmt_dict + + error.context("Run cyclictest") + timeout_s = params.get("cyclictest_timeout_s", 60*35) + cmd = "cyclictest -q %s" % formatted_args + status, ct_output = session.cmd_status_output(cmd, + timeout=timeout_s) + if status: + logging.error("Cyclic test output: %s" % ct_output) + raise error.TestFail("Cyclictest returned %d" % status) + + error.context("Parse and check result") + results = parse_cyclictest(ct_output) + if not results: + logging.error("Cyclic test output: %s" % ct_output) + raise error.TestFail("Parsing of cyclictest output failed") + + # Get check values. By default, don't check Act. + allowed_max = int(params.get("allowed_max", 50)) + allowed_avg = int(params.get("allowed_avg", 30)) + allowed_act = int(params.get("allowed_act", 0)) + + fails = 0 + for key, result in results.iteritems(): + cpu = int(key) + max_val = int(result['Max']) + avg_val = int(result['Avg']) + act_val = int(result['Act']) + fails += check_val_ceiling(allowed_max, max_val, "Max", cpu) + fails += check_val_ceiling(allowed_avg, avg_val, "Avg", cpu) + fails += check_val_ceiling(allowed_act, act_val, "Act", cpu) + + if fails > 0: + logging.error("Cyclic test output: %s" % ct_output) + raise error.TestFail("Values out of bounds, see log") + + finally: + session.close()