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

Reply via email to