This commit adds a functional test suite that utilizes lxc containers to guarantee a non-destructive test environment.
The tests can be invoked individually, as a group of related tests, or from automake via the standard 'make check' command. No tests are included as part of this commit. Example test invocations: Run a single test (first cd to tests/ftests): ./001-cgget-basic_cgget.py or ./ftests.py -N 15 # Run test #015 Run a suite of tests (first cd to tests/ftests): ./ftests.py -s cgget # Run all cgget tests Run all the tests by hand ./ftests.py # This may be advantageous over running make check # because it will try to re-use the same lxc # container for all of the tests. This should # provide a significant performance increase Run the tests from automake make check # Then examine the *.trs and *.log files for # specifics regarding each test result Example output from a test run: Test Results: Run Date: Jun 03 13:41:35 Passed: 1 test Skipped: 0 tests Failed: 0 tests ----------------------------------------------------------------- Timing Results: Test Time (sec) --------------------------------------------------------- setup 6.95 001-cgget-basic_cgget.py 0.07 teardown 0.00 --------------------------------------------------------- Total Run Time 7.02 Signed-off-by: Tom Hromatka <tom.hroma...@oracle.com> --- tests/ftests/README.md | 65 ++++++++++++ tests/ftests/config.py | 58 +++++++++++ tests/ftests/consts.py | 6 ++ tests/ftests/ftests.py | 273 +++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 402 insertions(+) create mode 100644 tests/ftests/README.md create mode 100644 tests/ftests/config.py create mode 100755 tests/ftests/ftests.py diff --git a/tests/ftests/README.md b/tests/ftests/README.md new file mode 100644 index 0000000..ed3dcb6 --- /dev/null +++ b/tests/ftests/README.md @@ -0,0 +1,65 @@ +## Functional Test Suite for libcgroup + +This folder contains the functional test suite for libcgroup. +The functional test suite utilizes lxc containers to guarantee +a non-destructive test environment. + +The tests can be invoked individually, as a group of related +tests, or from automake via the standard 'make check' +command. + +## Invocation + +Run a single test (first cd to tests/ftests): + + ./001-cgget-basic_cgget.py + or + ./ftests.py -N 15 # Run test #015 + +Run a suite of tests (first cd to tests/ftests): + + ./ftests.py -s cgget # Run all cgget tests + +Run all the tests by hand + + ./ftests.py + # This may be advantageous over running make check + # because it will try to re-use the same lxc + # container for all of the tests. This should + # provide a significant performance increase + +Run the tests from automake + + make check + # Then examine the *.trs and *.log files for + # specifics regarding each test result + +## Results + +The test suite will generate test results upon completion of +the test run. An example result is below: + +``` +Test Results: + Run Date: Jun 03 13:41:35 + Passed: 1 test + Skipped: 0 tests + Failed: 0 tests +----------------------------------------------------------------- +Timing Results: + Test Time (sec) + --------------------------------------------------------- + setup 6.95 + 001-cgget-basic_cgget.py 0.07 + teardown 0.00 + --------------------------------------------------------- + Total Run Time 7.02 +``` + +A log file can also be generated to help in debugging failed +tests. Run `ftests.py -h` to view the syntax. + +To generate a log file called foo.log at a debug level (8) run +the following: + + ./ftests.py -l 8 -L foo.log diff --git a/tests/ftests/config.py b/tests/ftests/config.py new file mode 100644 index 0000000..2d32aaf --- /dev/null +++ b/tests/ftests/config.py @@ -0,0 +1,58 @@ +# +# Config class for the libcgroup functional tests +# +# Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. +# Author: Tom Hromatka <tom.hroma...@oracle.com> +# + +# +# This library is free software; you can redistribute it and/or modify it +# under the terms of version 2.1 of the GNU Lesser General Public License as +# published by the Free Software Foundation. +# +# This library is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License +# for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this library; if not, see <http://www.gnu.org/licenses>. +# + +import consts +from container import Container +import os + +class Config(object): + def __init__(self, args, container=None): + self.args = args + + if container: + self.container = container + else: + # Use the default container settings + self.container = Container(name=consts.DEFAULT_CONTAINER_NAME, + stop_timeout=args.timeout, arch=None, cfg_path=args.config, + distro=args.distro, release=args.release) + + self.ftest_dir = os.path.dirname(os.path.abspath(__file__)) + self.libcg_dir = os.path.dirname(self.ftest_dir) + + self.test_suite = consts.TESTS_RUN_ALL_SUITES + self.test_num = consts.TESTS_RUN_ALL + self.verbose = False + + def __str__(self): + out_str = "Configuration" + out_str += "\n\tcontainer = {}".format(self.container) + + return out_str + + +class ConfigError(Exception): + def __init__(self, message): + super(ConfigError, self).__init__(message) + + def __str__(self): + out_str = "ConfigError:\n\tmessage = {}".format(self.message) + return out_str diff --git a/tests/ftests/consts.py b/tests/ftests/consts.py index 522cca6..9a1b92b 100644 --- a/tests/ftests/consts.py +++ b/tests/ftests/consts.py @@ -39,3 +39,9 @@ DEFAULT_CONTAINER_CFG_PATH=os.path.join( os.path.dirname(os.path.abspath(__file__)), 'default.conf') TEMP_CONTAINER_CFG_FILE='tmp.conf' + +TESTS_RUN_ALL = -1 +TESTS_RUN_ALL_SUITES = "allsuites" +TEST_PASSED = "passed" +TEST_FAILED = "failed" +TEST_SKIPPED = "skipped" diff --git a/tests/ftests/ftests.py b/tests/ftests/ftests.py new file mode 100755 index 0000000..c8ec0ab --- /dev/null +++ b/tests/ftests/ftests.py @@ -0,0 +1,273 @@ +#!/usr/bin/env python +# +# Main entry point for the libcgroup functional tests +# +# Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. +# Author: Tom Hromatka <tom.hroma...@oracle.com> +# + +# +# This library is free software; you can redistribute it and/or modify it +# under the terms of version 2.1 of the GNU Lesser General Public License as +# published by the Free Software Foundation. +# +# This library is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License +# for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this library; if not, see <http://www.gnu.org/licenses>. +# + +import argparse +from cgroup import Cgroup +from config import Config +import consts +import container +import datetime +import log +from log import Log +import os +from run import Run +import sys +import time + +setup_time = 0.0 +teardown_time = 0.0 + +def parse_args(): + parser = argparse.ArgumentParser("Libcgroup Functional Tests") + parser.add_argument('-n', '--name', + help='name of the container', + required=False, type=str, default=None) + parser.add_argument('-f', '--config', + help='initial configuration file', + required=False, type=str, default=None) + parser.add_argument('-d', '--distro', + help='linux distribution to use as a template', + required=False, type=str, default=None) + parser.add_argument('-r', '--release', + help='distribution release, e.g.\'trusty\'', + required=False, type=str, default=None) + parser.add_argument('-a', '--arch', + help='processor architecture', + required=False, type=str, default=None) + parser.add_argument('-t', '--timeout', + help='wait timeout (sec) before stopping the container', + required=False, type=int, default=None) + + parser.add_argument('-l', '--loglevel', + help='log level', + required=False, type=int, default=None) + parser.add_argument('-L', '--logfile', + help='log file', + required=False, type=str, default=None) + + parser.add_argument('-N', '--num', + help='Test number to run. If unspecified, all tests are run', + required=False, default=consts.TESTS_RUN_ALL, type=int) + parser.add_argument('-s', '--suite', + help='Test suite to run, e.g. cpuset', required=False, + default=consts.TESTS_RUN_ALL_SUITES, type=str) + parser.add_argument('-u', '--unpriv', + help='Run the tests in an unprivileged container', + required=False, action="store_true") + parser.add_argument('-v', '--verbose', + help='Print all information about this test run', + default=True, required=False, action="store_false") + + config = Config(parser.parse_args()) + + if config.args.loglevel: + log.log_level = config.args.loglevel + if config.args.logfile: + log.log_file = config.args.logfile + if config.args.unpriv: + raise ValueError('Unprivileged containers are not currently supported') + config.container.privileged = False + + return config + +def setup(config, do_teardown=True, record_time=False): + global setup_time + start_time = time.time() + if do_teardown: + # belt and suspenders here. In case a previous run wasn't properly + # cleaned up, let's try and clean it up here + try: + teardown(config) + except Exception as e: + # log but ignore all exceptions + Log.log_debug(e) + + config.container.create() + + # make the /libcg directory in the container's rootfs + rootfs = config.container.rootfs() + container_rootfs_path = rootfs.split('=')[1].strip() + Run.run(['sudo', 'mkdir', os.path.join(container_rootfs_path, + consts.LIBCG_MOUNT_POINT)]) + + config.container.start() + + # add the libcgroup library to the container's ld + echo_cmd = ['bash', '-c', 'echo {} >> /etc/ld.so.conf.d/libcgroup.conf'.format( + os.path.join('/', consts.LIBCG_MOUNT_POINT, 'src/.libs'))] + config.container.run(echo_cmd) + config.container.run('ldconfig') + if record_time: + setup_time = time.time() - start_time + +def run_tests(config): + passed_tests = [] + failed_tests = [] + skipped_tests = [] + + for root, dirs, filenames in os.walk(config.ftest_dir): + for filename in filenames: + if os.path.splitext(filename)[-1] != ".py": + # ignore non-python files + continue + + filenum = filename.split('-')[0] + + try: + filenum_int = int(filenum) + except ValueError: + # D'oh. This file must not be a test. Skip it + Log.log_debug('Skipping {}. It doesn\'t start with an int'.format( + filename)) + continue + + try: + filesuite = filename.split('-')[1] + except IndexError: + Log.log_error( + 'Skipping {}. It doesn\'t conform to the filename format'.format( + filename)) + continue + + if config.args.suite == consts.TESTS_RUN_ALL_SUITES or \ + config.args.suite == filesuite: + if config.args.num == consts.TESTS_RUN_ALL or \ + config.args.num == filenum_int: + test = __import__(os.path.splitext(filename)[0]) + + failure_cause = None + start_time = time.time() + try: + Log.log_debug('Running test {}.'.format(filename)) + [ret, failure_cause] = test.main(config) + except Exception as e: + # catch all exceptions. you never know when there's + # a crummy test + failure_cause = e + Log.log_debug(e) + ret = consts.TEST_FAILED + + # if the test does cause an exception, it may not have + # cleaned up after itself. re-create the container + teardown(config) + setup(config, do_teardown=False) + finally: + run_time = time.time() - start_time + if ret == consts.TEST_PASSED: + passed_tests.append([filename, run_time]) + elif ret == consts.TEST_FAILED: + failed_tests.append([filename, run_time]) + elif ret == consts.TEST_SKIPPED: + skipped_tests.append([filename, run_time]) + else: + raise ValueException('Unexpected ret: {}'.format(ret)) + + passed_cnt = len(passed_tests) + failed_cnt = len(failed_tests) + skipped_cnt = len(skipped_tests) + + print("-----------------------------------------------------------------") + print("Test Results:") + date_str = datetime.datetime.now().strftime('%b %d %H:%M:%S') + print('\t{}{}'.format('{0: <30}'.format("Run Date:"), '{0: >15}'.format(date_str))) + + test_str = "{} test(s)".format(passed_cnt) + print('\t{}{}'.format('{0: <30}'.format("Passed:"), '{0: >15}'.format(test_str))) + + test_str = "{} test(s)".format(skipped_cnt) + print('\t{}{}'.format('{0: <30}'.format("Skipped:"), '{0: >15}'.format(test_str))) + + test_str = "{} test(s)".format(failed_cnt) + print('\t{}{}'.format('{0: <30}'.format("Failed:"), '{0: >15}'.format(test_str))) + + for test in failed_tests: + print("\t\tTest:\t\t\t\t{} - {}".format(test[0], str(failure_cause))) + print("-----------------------------------------------------------------") + + global setup_time + global teardown_time + if config.args.verbose: + print("Timing Results:") + print('\t{}{}'.format('{0: <30}'.format("Test"), '{0: >15}'.format("Time (sec)"))) + print("\t---------------------------------------------------------") + time_str = "{0: 2.2f}".format(setup_time) + print('\t{}{}'.format('{0: <30}'.format('setup'), '{0: >15}'.format(time_str))) + for test in passed_tests: + time_str = "{0: 2.2f}".format(test[1]) + print('\t{}{}'.format('{0: <30}'.format(test[0]), '{0: >15}'.format(time_str))) + for test in failed_tests: + time_str = "{0: 2.2f}".format(test[1]) + print('\t{}{}'.format('{0: <30}'.format(test[0]), '{0: >15}'.format(time_str))) + time_str = "{0: 2.2f}".format(teardown_time) + print('\t{}{}'.format('{0: <30}'.format('teardown'), '{0: >15}'.format(time_str))) + + total_run_time = setup_time + teardown_time + for test in passed_tests: + total_run_time += test[1] + for test in failed_tests: + total_run_time += test[1] + total_str = "{0: 5.2f}".format(total_run_time) + print("\t---------------------------------------------------------") + print('\t{}{}'.format('{0: <30}'.format("Total Run Time"), '{0: >15}'.format(total_str))) + + return [passed_cnt, failed_cnt, skipped_cnt] + +def teardown(config, record_time=False): + global teardown_time + start_time = time.time() + try: + config.container.stop() + except Exception as e: + # log but ignore all exceptions + Log.log_debug(e) + try: + config.container.destroy() + except Exception as e: + # log but ignore all exceptions + Log.log_debug(e) + + if record_time: + teardown_time = time.time() - start_time + +def main(config): + AUTOMAKE_SKIPPED = 77 + AUTOMAKE_HARD_ERROR = 99 + AUTOMAKE_PASSED = 0 + + try: + setup(config, record_time=True) + [passed_cnt, failed_cnt, skipped_cnt] = run_tests(config) + finally: + teardown(config, record_time=True) + + if failed_cnt > 0: + return failed_cnt + if skipped_cnt > 0: + return AUTOMAKE_SKIPPED + if passed_cnt > 0: + return AUTOMAKE_PASSED + + return AUTOMAKE_HARD_ERROR + +if __name__ == '__main__': + config = parse_args() + sys.exit(main(config)) -- 1.8.3.1 _______________________________________________ Libcg-devel mailing list Libcg-devel@lists.sourceforge.net https://lists.sourceforge.net/lists/listinfo/libcg-devel