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> --- configure.in | 1 + tests/Makefile.am | 2 +- tests/ftests/.gitignore | 5 + tests/ftests/Makefile.am | 27 ++++ tests/ftests/__init__.py | 0 tests/ftests/cgroup.py | 186 +++++++++++++++++++++++ tests/ftests/config.py | 56 +++++++ tests/ftests/consts.py | 49 +++++++ tests/ftests/container.py | 236 ++++++++++++++++++++++++++++++ tests/ftests/default.conf | 29 ++++ tests/ftests/ftests.py | 301 ++++++++++++++++++++++++++++++++++++++ tests/ftests/log.py | 67 +++++++++ tests/ftests/run.py | 73 +++++++++ 13 files changed, 1031 insertions(+), 1 deletion(-) create mode 100644 tests/ftests/.gitignore create mode 100644 tests/ftests/Makefile.am create mode 100644 tests/ftests/__init__.py create mode 100644 tests/ftests/cgroup.py create mode 100644 tests/ftests/config.py create mode 100644 tests/ftests/consts.py create mode 100644 tests/ftests/container.py create mode 100644 tests/ftests/default.conf create mode 100755 tests/ftests/ftests.py create mode 100644 tests/ftests/log.py create mode 100644 tests/ftests/run.py diff --git a/configure.in b/configure.in index 81949b1..fafd245 100644 --- a/configure.in +++ b/configure.in @@ -198,6 +198,7 @@ fi AC_CONFIG_FILES([Makefile tests/Makefile + tests/ftests/Makefile tests/gunit/Makefile tests/tools/testenv.sh tests/tools/Makefile diff --git a/tests/Makefile.am b/tests/Makefile.am index 7f1a071..6d748dc 100644 --- a/tests/Makefile.am +++ b/tests/Makefile.am @@ -1,4 +1,4 @@ -SUBDIRS = gunit tools +SUBDIRS = ftests gunit tools INCLUDES = -I$(top_srcdir)/include LDADD = $(top_builddir)/src/.libs/libcgroup.la diff --git a/tests/ftests/.gitignore b/tests/ftests/.gitignore new file mode 100644 index 0000000..9a54287 --- /dev/null +++ b/tests/ftests/.gitignore @@ -0,0 +1,5 @@ +tmp.conf +*.log +*.pyc +*.swp +*.trs diff --git a/tests/ftests/Makefile.am b/tests/ftests/Makefile.am new file mode 100644 index 0000000..ede06b4 --- /dev/null +++ b/tests/ftests/Makefile.am @@ -0,0 +1,27 @@ +# +# libcgroup functional tests Makefile.am +# +# 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>. +# + +TESTS = + +clean-local: clean-local-check +.PHONY: clean-local-check +clean-local-check: + -rm -f *.pyc diff --git a/tests/ftests/__init__.py b/tests/ftests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/ftests/cgroup.py b/tests/ftests/cgroup.py new file mode 100644 index 0000000..34f054b --- /dev/null +++ b/tests/ftests/cgroup.py @@ -0,0 +1,186 @@ +#!/usr/bin/env python +# +# Cgroup 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>. +# +# SPDX-License-Identifier: GPL-2.0 + +import consts +import os +from run import Run +import types + +class Cgroup(object): + @staticmethod + def build_cmd_path(in_container, cmd): + if in_container: + return os.path.join('/', consts.LIBCG_MOUNT_POINT, + 'src/tools/.libs/%s' % cmd) + else: + return cmd + + @staticmethod + def concatenate_controllers(controller_list): + if type(controller_list) is types.StringType: + # controller is already a string. return it as is + return controller_list + + out_str = "" + for controller in controller_list: + out_str += "%s," % controller + + # remove the trailing "," + out_str = out_str[:-1] + return out_str + + # TODO - add support for all of the cgcreate options + @staticmethod + def create(config, controller_list, cgname, in_container=True): + cmd = list() + cmd.append(Cgroup.build_cmd_path(in_container, 'cgcreate')) + + controllers_and_path = "%s:%s" % \ + (Cgroup.concatenate_controllers(controller_list), cgname) + + cmd.append('-g') + cmd.append(controllers_and_path) + + if in_container: + config.container.run(cmd) + else: + Run.run(cmd) + + @staticmethod + def delete(config, controller_list, cgname, in_container=True, recursive=False): + cmd = list() + cmd.append(Cgroup.build_cmd_path(in_container, 'cgdelete')) + + if recursive: + cmd.append('-r') + + controllers_and_path = "%s:%s" % \ + (Cgroup.concatenate_controllers(controller_list), cgname) + + cmd.append('-g') + cmd.append(controllers_and_path) + + if in_container: + config.container.run(cmd) + else: + Run.run(cmd) + + @staticmethod + def set(config, cgname, setting, value, in_container=True): + cmd = list() + cmd.append(Cgroup.build_cmd_path(in_container, 'cgset')) + + if type(setting) is types.StringType and \ + type(value) is types.StringType: + cmd.append('-r') + cmd.append('%s=%s' % (setting, value)) + elif type(setting) is types.ListType and \ + type(value) is types.ListType: + if len(setting) != len(value): + raise ValueError('Settings list length must equal values list length') + + for idx, stg in enumerate(setting): + cmd.append('-r') + cmd.append('%s=%s' % (stg, value[idx])) + + cmd.append(cgname) + + if in_container: + config.container.run(cmd) + else: + Run.run(cmd) + + @staticmethod + # valid cpuset commands: + # Read one setting: + # cgget -r cpuset.cpus tomcpuset + # Read two settings: + # cgget -r cpuset.cpus -r cpuset.cpu_exclusive tomcpuset + # Read one setting from two cgroups: + # cgget -r cpuset.cpu_exclusive tomcgroup1 tomcgroup2 + # Read two settings from two cgroups: + # cgget -r cpuset.cpu_exclusive -r cpuset.cpu_exclusive tomcgroup1 tomcgroup2 + # + # Read all of the settings in a cgroup + # cgget -g cpuset tomcpuset + # Read all of the settings in multiple controllers + # cgget -g cpuset -g cpu -g memory tomcgroup + # Read all of the settings from a cgroup at a specific path + # cgget -g memory:tomcgroup/tomcgroup + def get(config, controller=None, cgname=None, setting=None, + in_container=True, print_headers=True, values_only=False, + all_controllers=False): + cmd = list() + cmd.append(Cgroup.build_cmd_path(in_container, 'cgget')) + + if not print_headers: + cmd.append('-n') + if values_only: + cmd.append('-v') + + if setting is not None: + if type(setting) is types.StringType: + # the user provided a simple string. use it as is + cmd.append('-r') + cmd.append(setting) + elif type(setting) is types.ListType: + for sttng in setting: + cmd.append('-r') + cmd.append(sttng) + else: + raise ValueError('Unsupported setting value') + + if controller is not None: + if type(controller) is types.StringType and \ + ':' in controller: + # the user provided a controller:cgroup. use it as is + cmd.append('-g') + cmd.append(controller) + elif type(controller) is types.StringType: + # the user provided a controller only. use it as is + cmd.append('-g') + cmd.append(controller) + elif type(controller) is types.ListType: + for ctrl in controller: + cmd.append('-g') + cmd.append(ctrl) + else: + raise ValueError('Unsupported controller value') + + if all_controllers: + cmd.append('-a') + + if cgname is not None: + if type(cgname) is types.StringType: + # use the string as is + cmd.append(cgname) + elif type(cgname) is types.ListType: + for cg in cgname: + cmd.append(cg) + + if in_container: + ret = config.container.run(cmd) + else: + ret = Run.run(cmd) + + return ret diff --git a/tests/ftests/config.py b/tests/ftests/config.py new file mode 100644 index 0000000..20157f3 --- /dev/null +++ b/tests/ftests/config.py @@ -0,0 +1,56 @@ +#!/usr/bin/env python +# +# 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>. +# +# SPDX-License-Identifier: GPL-2.0 + +import consts +from container import Container +import os + +class Config(object): + def __init__(self, container=None): + if container: + self.container = container + else: + # Use the default container settings + self.container = Container(consts.DEFAULT_CONTAINER_NAME) + + 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 = %d" % 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 = %s" % self.message + return out_str diff --git a/tests/ftests/consts.py b/tests/ftests/consts.py new file mode 100644 index 0000000..6ac2eb3 --- /dev/null +++ b/tests/ftests/consts.py @@ -0,0 +1,49 @@ +#!/usr/bin/env python +# +# Constants 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>. +# +# SPDX-License-Identifier: GPL-2.0 + +import os + +DEFAULT_CONTAINER_NAME = 'test_libcg' +DEFAULT_CONTAINER_DISTRO = 'oracle' +DEFAULT_CONTAINER_RELEASE = '7' +DEFAULT_CONTAINER_ARCH = 'amd64' +DEFAULT_CONTAINER_STOP_TIMEOUT = 5 +DEFAULT_CONTAINER_CFG_PATH=os.path.join( + os.path.dirname(os.path.abspath(__file__)), + 'default.conf') +TEMP_CONTAINER_CFG_FILE='tmp.conf' + +DEFAULT_LOG_FILE = 'libcgroup-ftests.log' + +LOG_CRITICAL = 1 +LOG_WARNING = 5 +LOG_DEBUG = 8 +DEFAULT_LOG_LEVEL = 5 + +LIBCG_MOUNT_POINT = 'libcg' + +TESTS_RUN_ALL = -1 +TESTS_RUN_ALL_SUITES = "allsuites" +TEST_PASSED = "passed" +TEST_FAILED = "failed" +TEST_SKIPPED = "skipped" diff --git a/tests/ftests/container.py b/tests/ftests/container.py new file mode 100644 index 0000000..bc1e6de --- /dev/null +++ b/tests/ftests/container.py @@ -0,0 +1,236 @@ +#!/usr/bin/env python +# +# Container 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>. +# +# SPDX-License-Identifier: GPL-2.0 + +import consts +import getpass +from log import Log +import os +from run import Run +import types + +class Container(object): + def __init__(self, name, stop_timeout=None, arch=None, cfg_path=None, + distro=None, release=None): + self.name = name + self.privileged = True + + if stop_timeout: + self.stop_timeout = stop_timeout + else: + self.stop_timeout = consts.DEFAULT_CONTAINER_STOP_TIMEOUT + + if arch: + self.arch = arch + else: + self.arch = consts.DEFAULT_CONTAINER_ARCH + + if cfg_path: + self.cfg_path = cfg_path + else: + self.cfg_path = consts.DEFAULT_CONTAINER_CFG_PATH + + if distro: + self.distro = distro + else: + self.distro = consts.DEFAULT_CONTAINER_DISTRO + + if release: + self.release = release + else: + self.release = consts.DEFAULT_CONTAINER_RELEASE + + ftest_dir = os.path.dirname(os.path.abspath(__file__)) + tests_dir = os.path.dirname(ftest_dir) + libcg_dir = os.path.dirname(tests_dir) + + self.tmp_cfg_path = os.path.join(ftest_dir, consts.TEMP_CONTAINER_CFG_FILE) + try: + Run.run(['rm', '-f', self.tmp_cfg_path]) + except: + pass + + Run.run(['cp', self.cfg_path, self.tmp_cfg_path]) + + conf_line = 'lxc.arch = %s' % self.arch + Run.run(['echo', conf_line, '>>', self.tmp_cfg_path], shell_bool=True) + + conf_line = 'lxc.mount.entry = %s %s none bind,ro 0 0' % \ + (libcg_dir, consts.LIBCG_MOUNT_POINT) + Run.run(['echo', conf_line, '>>', self.tmp_cfg_path], shell_bool=True) + + if not self.privileged: + conf_line = 'lxc.idmap = u 0 100000 65536' + Run.run(['echo', conf_line, '>>', self.tmp_cfg_path], shell_bool=True) + conf_line = 'lxc.idmap = g 0 100000 65536' + Run.run(['echo', conf_line, '>>', self.tmp_cfg_path], shell_bool=True) + + def __str__(self): + out_str = "%s" % self.name + out_str += "\n\tdistro = %s" % self.distro + out_str += "\n\trelease = %s" % self.release + out_str += "\n\tarch = %s" % self.arch + out_str += "\n\tcfg_path = %s" % self.cfg_path + out_str += "\n\tstop_timeout = %d" % self.stop_timeout + + return out_str + + def create(self): + cmd = list() + + if self.privileged: + cmd.append('sudo') + + cmd.append('lxc-create') + cmd.append('-t') + cmd.append( 'download') + + cmd.append('-n') + cmd.append(self.name) + + if self.privileged: + cmd.append('sudo') + cmd.append('-f') + cmd.append(self.tmp_cfg_path) + + cmd.append('--') + + cmd.append('-d') + cmd.append(self.distro) + + cmd.append('-r') + cmd.append(self.release) + + cmd.append('-a') + cmd.append(self.arch) + + return Run.run(cmd) + + def destroy(self): + cmd = list() + + if self.privileged: + cmd.append('sudo') + + cmd.append('lxc-destroy') + + cmd.append('-n') + cmd.append(self.name) + + return Run.run(cmd) + + def info(self, cfgname): + cmd = list() + + if self.privileged: + cmd.append('sudo') + + cmd.append('lxc-info') + + cmd.append('--config=%s' % cfgname) + + cmd.append('-n') + cmd.append(self.name) + + return Run.run(cmd) + + def rootfs(self): + # try to read lxc.rootfs.path first + ret = self.info('lxc.rootfs.path') + if len(ret.strip()) > 0: + return ret + + # older versions of lxc used lxc.rootfs. Try that. + ret = self.info('lxc.rootfs') + if len(ret.strip()) == 0: + # we failed to get the rootfs + raise ContainerError('Failed to get the rootfs') + return ret + + def run(self, cntnr_cmd): + cmd = list() + + if self.privileged: + cmd.append('sudo') + + cmd.append('lxc-attach') + + cmd.append('-n') + cmd.append(self.name) + + cmd.append('--') + + # concatenate the lxc-attach command with the command to be run + # inside the container + if type(cntnr_cmd) is types.StringType: + cmd.append(cntnr_cmd) + elif type(cntnr_cmd) is types.ListType: + cmd = cmd + cntnr_cmd + else: + raise ContainerError('Unsupported command type') + + return Run.run(cmd) + + def start(self): + cmd = list() + + if self.privileged: + cmd.append('sudo') + + cmd.append('lxc-start') + cmd.append('-d') + + cmd.append('-n') + cmd.append(self.name) + + return Run.run(cmd) + + def stop(self, kill=True): + cmd = list() + + if self.privileged: + cmd.append('sudo') + + cmd.append('lxc-stop') + + cmd.append('-n') + cmd.append(self.name) + + if kill: + cmd.append('-k') + else: + cmd.append('-t') + cmd.append(str(self.stop_timeout)) + + return Run.run(cmd) + + def version(self): + cmd = ['lxc-create', '--version'] + return Run.run(cmd) + +class ContainerError(Exception): + def __init__(self, message, ret): + super(RunError, self).__init__(message) + + def __str__(self): + out_str = "ContainerError:\n\tmessage = %s" % (self.message) + return out_str diff --git a/tests/ftests/default.conf b/tests/ftests/default.conf new file mode 100644 index 0000000..c999f75 --- /dev/null +++ b/tests/ftests/default.conf @@ -0,0 +1,29 @@ +# Template used to create this container: /usr/share/lxc/templates/lxc-download +# +# Default lxc configuration file for 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>. +# +# SPDX-License-Identifier: GPL-2.0 + +# Distribution configuration +lxc.include = /usr/share/lxc/config/common.conf +lxc.include = /usr/share/lxc/config/userns.conf + +# Container specific configuration +lxc.include = /etc/lxc/default.conf diff --git a/tests/ftests/ftests.py b/tests/ftests/ftests.py new file mode 100755 index 0000000..92b1245 --- /dev/null +++ b/tests/ftests/ftests.py @@ -0,0 +1,301 @@ +#!/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>. +# +# SPDX-License-Identifier: GPL-2.0 + +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") + + args = parser.parse_args() + + config = Config() + + if args.name: + config.name = args.name + if args.config: + config.container.cfg_path = args.config + if args.distro: + config.container.distro = args.distro + if args.release: + config.container.release = args.release + if args.arch: + config.container.arch = args.arch + if args.timeout: + config.container.stop_timeout = args.timeout + if args.loglevel: + log.log_level = args.loglevel + if args.logfile: + log.log_file = args.logfile + if args.num: + config.test_num = args.num + if args.suite: + config.test_suite = args.suite + if args.unpriv: + raise ValueError('Unprivileged containers are not currently supported') + config.container.privileged = False + config.verbose = args.verbose + + 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 %s >> /etc/ld.so.conf.d/libcgroup.conf' % \ + 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 %s. It doesn\'t start with an int' % + filename) + continue + + try: + filesuite = filename.split('-')[1] + except IndexError: + Log.log_error('Skipping %s. It doesn\'t conform to the filename format' % + filename) + continue + + if config.test_suite == consts.TESTS_RUN_ALL_SUITES or \ + config.test_suite == filesuite: + if config.test_num == consts.TESTS_RUN_ALL or \ + config.test_num == filenum_int: + test = __import__(os.path.splitext(filename)[0]) + + failure_cause = None + start_time = time.time() + try: + [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: %s' % 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%s%s' % ('{0: <30}'.format("Run Date:"), '{0: >15}'.format(date_str))) + if passed_cnt == 1: + test_str = "1 test" + print('\t%s%s' % ('{0: <30}'.format("Passed:"), '{0: >15}'.format(test_str))) + else: + test_str = "%d tests" % passed_cnt + print('\t%s%s' % ('{0: <30}'.format("Passed:"), '{0: >15}'.format(test_str))) + + if skipped_cnt == 1: + test_str = "1 test" + print('\t%s%s' % ('{0: <30}'.format("Skipped:"), '{0: >15}'.format(test_str))) + else: + test_str = "%d tests" % skipped_cnt + print('\t%s%s' % ('{0: <30}'.format("Skipped:"), '{0: >15}'.format(test_str))) + + if failed_cnt == 1: + test_str = "1 test" + print('\t%s%s' % ('{0: <30}'.format("Failed:"), '{0: >15}'.format(test_str))) + else: + test_str = "%d tests" % failed_cnt + print('\t%s%s' % ('{0: <30}'.format("Failed:"), '{0: >15}'.format(test_str))) + for test in failed_tests: + print("\t\tTest:\t\t\t\t%s - %s" % (test[0], str(failure_cause))) + print("-----------------------------------------------------------------") + + global setup_time + global teardown_time + if config.verbose: + print("Timing Results:") + print('\t%s%s' % ('{0: <30}'.format("Test"), '{0: >15}'.format("Time (sec)"))) + print("\t---------------------------------------------------------") + time_str = "%2.2f" % setup_time + print('\t%s%s' % ('{0: <30}'.format('setup'), '{0: >15}'.format(time_str))) + for test in passed_tests: + time_str = "%2.2f" % test[1] + print('\t%s%s' % ('{0: <30}'.format(test[0]), '{0: >15}'.format(time_str))) + for test in failed_tests: + time_str = "%2.2f" % test[1] + print('\t%s%s' % ('{0: <30}'.format(test[0]), '{0: >15}'.format(time_str))) + time_str = "%2.2f" % teardown_time + print('\t%s%s' % ('{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 = "%5.2f" % total_run_time + print("\t---------------------------------------------------------") + print('\t%s%s' % ('{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)) diff --git a/tests/ftests/log.py b/tests/ftests/log.py new file mode 100644 index 0000000..104aa20 --- /dev/null +++ b/tests/ftests/log.py @@ -0,0 +1,67 @@ +#!/usr/bin/env python +# +# Log 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>. +# +# SPDX-License-Identifier: GPL-2.0 + +import consts +import datetime +import log + +log_level = consts.DEFAULT_LOG_LEVEL +log_file = consts.DEFAULT_LOG_FILE +log_fd = None + + +class Log(object): + + @staticmethod + def log(msg, msg_level=consts.DEFAULT_LOG_LEVEL): + if log_level >= msg_level: + if log.log_fd is None: + Log.open_logfd(log.log_file) + + timestamp = datetime.datetime.now().strftime('%b %d %H:%M:%S') + log_fd.write("%s: %s\n" % (timestamp, msg)) + + @staticmethod + def open_logfd(log_file): + log.log_fd = open(log_file, "a") + + @staticmethod + def log_critical(msg): + Log.log("CRITICAL: %s" % msg, consts.LOG_CRITICAL) + + @staticmethod + def log_warning(msg): + Log.log("WARNING: %s" % msg, consts.LOG_WARNING) + + @staticmethod + def log_debug(msg): + Log.log("DEBUG: %s" % msg, consts.LOG_DEBUG) + + +class LogError(Exception): + def __init__(self, message): + super(LogError, self).__init__(message) + + def __str__(self): + out_str = "LogError:\n\tmessage = %s" % self.message + return out_str diff --git a/tests/ftests/run.py b/tests/ftests/run.py new file mode 100644 index 0000000..c3ddfc3 --- /dev/null +++ b/tests/ftests/run.py @@ -0,0 +1,73 @@ +#!/usr/bin/env python +# +# Run 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>. +# +# SPDX-License-Identifier: GPL-2.0 +from log import Log +import subprocess +import time +import types + + +class Run(object): + @staticmethod + def run(command, shell_bool=False): + if shell_bool: + if type(command) is types.StringType: + # nothing to do. command is already formatted as a string + pass + elif type(command) is types.ListType: + command = " ".join(command) + else: + raise ValueError('Unsupported command type') + + subproc = subprocess.Popen(command, shell=shell_bool, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE) + out, err = subproc.communicate() + ret = subproc.returncode + + out = out.strip() + err = err.strip() + + if shell_bool: + Log.log_debug("run:\n\tcommand = %s\n\tret = %d\n\tstdout = %s\n\tstderr = %s" % + (command, ret, out, err)) + else: + Log.log_debug("run:\n\tcommand = %s\n\tret = %d\n\tstdout = %s\n\tstderr = %s" % + (" ".join(command), ret, out, err)) + + if ret != 0: + raise RunError("Command '%s' failed" % " ".join(command), ret, out, err) + + return out + +class RunError(Exception): + def __init__(self, message, ret, stdout, stderr): + super(RunError, self).__init__(message) + + self.ret = ret + self.stdout = stdout + self.stderr = stderr + + def __str__(self): + out_str = "RunError:\n\tmessage = %s\n\tret = %d" % (self.message, self.ret) + out_str += "\n\tstdout = %s\n\tstderr = %s" % (self.stdout, self.stderr) + return out_str -- 2.21.0 _______________________________________________ Libcg-devel mailing list Libcg-devel@lists.sourceforge.net https://lists.sourceforge.net/lists/listinfo/libcg-devel