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.0
a. 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. > + > +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? > + 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. > + 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_str This 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