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.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.

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_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

Reply via email to