On 6/10/19 11:56 AM, Dhaval Giani wrote:
On Tue, Jun 4, 2019 at 1:34 PM Tom Hromatka <tom.hroma...@oracle.com> wrote: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.0a. This is wrong, this is LGPL, not GPL b. This is also inconsistent. You use SPDX identifier here, but not elsewhere. I would remove it for now, and we will probably do a project wide change in the future.
Good spot. I'll remove SPDX everywhere.
+ +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):Does it make sense to put some default names here? Something we expect is being used for most tests (for example, cgroup name), but changing it for specific cases where we need to?
I've done this in the past and gotten burned pretty badly. It led to some really poorly written tests. Then if/when a bad test failed, it wreaked havoc on subsequent innocent tests because they were sharing a cgroup. At this point, I would lean against that, but I'm open to revisiting it if it becomes a burden.
+ 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) +So I see a lot of catch things, make it really flexible here. It is quite cool, but I question if it is needed right now? Could we just make it inflexible and expect the caller to do the right thing? It also makes the test more debuggable.
Good point. This logic is hidden within the framework and should actually help make the tests easier to write and more readable. For example, with the logic above, tests can do any of the following: Cgroup.set(config, 'TomsCgroup', 'cpu.shares', '2048') # not recommended but will work fine Cgroup.set(config, 'TomsCgroup', ['cpu.shares'], ['2048']) Cgroup.set(config, 'TomsCgroup', ['memory.soft_limit_in_bytes', 'memory.limit_in_bytes'], ['1G', '2G']) Without the fancy checking, the first example call above would fail, even though it's the most Pythonic way to do it for a single setting.
+ 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 +Same here, not GPL, but LGPL+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 +ditto+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_strThis lxc stuff should really be a patch by itself. Haven't reviewed this too closely yet. (Will wait for your split patchset)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 +ditto+# 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 +ditto [Willcontinue reviewing from here. Please split this patch, it is very big, and multiple discrete bits]+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
_______________________________________________ Libcg-devel mailing list Libcg-devel@lists.sourceforge.net https://lists.sourceforge.net/lists/listinfo/libcg-devel