Base framework and liblttng-ust-ctl test
authorJonathan Rajotte <jonathan.rajotte-julien@efficios.com>
Mon, 2 Oct 2017 21:57:56 +0000 (17:57 -0400)
committerJonathan Rajotte <jonathan.rajotte-julien@efficios.com>
Mon, 2 Oct 2017 21:57:56 +0000 (17:57 -0400)
Provide building block for building projects and a base primer for
pytest usage.

test_ust_so_name_vs_tools.py uses parametrized tests to ease the
handling of matrix testing.

Note that matrix can also be restricted to only tuples where certains
projects are constituent. This can be set under setting.py for now.

Signed-off-by: Jonathan Rajotte <jonathan.rajotte-julien@efficios.com>
lttng_ivc/__init__.py [new file with mode: 0644]
lttng_ivc/config.yaml [new file with mode: 0644]
lttng_ivc/launch.py [new file with mode: 0644]
lttng_ivc/settings.py [new file with mode: 0644]
lttng_ivc/tests/__init__.py [new file with mode: 0644]
lttng_ivc/tests/ust_soname_vs_tools/__init__.py [new file with mode: 0644]
lttng_ivc/tests/ust_soname_vs_tools/test_ust_so_name_vs_tools.py [new file with mode: 0644]
lttng_ivc/utils/ProjectFactory.py [new file with mode: 0644]
lttng_ivc/utils/project.py [new file with mode: 0644]
lttng_ivc/utils/skip.py [new file with mode: 0644]

diff --git a/lttng_ivc/__init__.py b/lttng_ivc/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/lttng_ivc/config.yaml b/lttng_ivc/config.yaml
new file mode 100644 (file)
index 0000000..bc0281b
--- /dev/null
@@ -0,0 +1,24 @@
+lttng-tools:
+        - marker: lttng-tools-2.7
+          url: https://github.com/lttng/lttng-tools
+          ref: stable-2.7
+        - marker: lttng-tools-2.8
+          url: https://github.com/lttng/lttng-tools
+          ref: stable-2.8
+lttng-ust:
+        - marker: lttng-ust-2.7
+          url: https://github.com/lttng/lttng-ust
+          ref: stable-2.7
+        - marker: lttng-ust-2.10
+          url: https://github.com/lttng/lttng-ust
+          ref: stable-2.7
+
+lttng-modules:
+        - marker: lttng-modules-2.7
+          url: https://github.com/lttng/lttng-modules
+          ref: stable-2.7
+
+babeltrace:
+        - marker: babeltrace-master
+          url: https://github.com/efficios/babeltrace
+          ref: master
diff --git a/lttng_ivc/launch.py b/lttng_ivc/launch.py
new file mode 100644 (file)
index 0000000..fcbde6e
--- /dev/null
@@ -0,0 +1,136 @@
+import pytest
+import os
+import yaml
+import logging
+import urllib.parse
+
+from git import Repo
+
+default_git_remote_dir = "./git_remote"
+
+
+def is_ref_branch(repo, ref):
+    try:
+        repo.remote().refs[ref]
+        is_branch = True
+    except:
+        is_branch = False
+
+    return is_branch
+
+
+def is_ref_tag(repo, ref):
+    try:
+        repo.tags[ref]
+        is_tag = True
+    except:
+        is_tag = False
+
+    return is_tag
+
+
+def is_ref_commit(repo, ref):
+    try:
+        Repo.rev_parse(repo, ref)
+        is_commit = True
+    except:
+        is_commit = False
+
+    return is_commit
+
+
+def logging_setup():
+    logger_format = '%(asctime)s %(name)-12s %(levelname)-8s %(message)s'
+    logging.basicConfig(level=logging.DEBUG,
+                        format=logger_format,
+                        datefmt='%m-%d %H:%M',
+                        filename='./debug.log',
+                        filemode='w')
+    # define a Handler which writes INFO messages or higher to the sys.stderr
+    console = logging.StreamHandler()
+    console.setLevel(logging.DEBUG)
+    # set a format which is simpler for console use
+    formatter = logging.Formatter('%(name)-12s: %(levelname)-8s %(message)s')
+    # tell the handler to use this format
+    console.setFormatter(formatter)
+    # add the handler to the root logger
+    logging.getLogger('').addHandler(console)
+
+
+logging_setup()
+
+# Remote setup
+logger_git = logging.getLogger('setup.git')
+
+# Fetch local base repository
+with open("config.yaml", 'r') as stream:
+    config = yaml.load(stream)
+
+# Retrieve all possibles remotes and clean url for path
+remotes = {}
+for project, markers in config.items():
+    if markers is None:
+        continue
+    for marker in markers:
+        url = marker['url']
+        url2path = urllib.parse.quote_plus(url)
+        path = os.path.abspath(default_git_remote_dir + '/' + url2path)
+        remotes[url] = path
+
+logger_git.info('Remotes to be fetched {}'.format(remotes))
+
+if not os.path.isdir(default_git_remote_dir):
+    os.mkdir(default_git_remote_dir)
+
+# Fetch the remote
+for url, path in remotes.items():
+    if os.path.exists(path):
+        if not os.path.isdir(path):
+            logger_git.error('Remote path {} exists and is not a folder'.format(path))
+            exit()
+        repo = Repo(path)
+    else:
+        repo = Repo.clone_from(url, path)
+
+    # TODO: might be necessary to actually update the base branch, to validate
+    repo.remote().fetch()
+
+# Create marker definition for test runners
+runnable_markers = {}
+for project, markers in config.items():
+    if markers is None:
+        continue
+    for marker in markers:
+        name = marker['marker']
+        ref = marker['ref']
+        url = marker['url']
+        path = remotes[url]
+        repo = Repo(path)
+
+        git_object = None
+        if is_ref_branch(repo, ref):
+            git_object = Repo.rev_parse(repo, repo.remote().refs[ref].name)
+        elif is_ref_tag(repo, ref):
+            git_object = repo.tags[ref].commit
+        elif is_ref_commit(repo, ref):
+            git_object = repo.commit(ref)
+
+        if git_object is None:
+            logger_git.error('Invalid git reference for marker "{}"'.format(name))
+            exit(1)
+
+        logger_git.info('Marker:{: <30}  Sha1 {: <20}'.format(name, git_object.hexsha))
+
+        if name in runnable_markers:
+            logger_git.error('Duplicate for entry for marker "{}"'.format(name))
+            exit(1)
+
+        runnable_markers[name] = {
+                'project': project,
+                'sha1': git_object.hexsha,
+                'url': url,
+                'path': path
+        }
+
+with open('run_configuration.yaml', 'w') as run_configuration:
+    yaml.dump(runnable_markers, run_configuration, default_flow_style=False)
diff --git a/lttng_ivc/settings.py b/lttng_ivc/settings.py
new file mode 100644 (file)
index 0000000..d005bd4
--- /dev/null
@@ -0,0 +1,2 @@
+# All tests are run if empty
+test_only = {"lttng-ust-2.7"}
diff --git a/lttng_ivc/tests/__init__.py b/lttng_ivc/tests/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/lttng_ivc/tests/ust_soname_vs_tools/__init__.py b/lttng_ivc/tests/ust_soname_vs_tools/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/lttng_ivc/tests/ust_soname_vs_tools/test_ust_so_name_vs_tools.py b/lttng_ivc/tests/ust_soname_vs_tools/test_ust_so_name_vs_tools.py
new file mode 100644 (file)
index 0000000..d3b1f1a
--- /dev/null
@@ -0,0 +1,123 @@
+import pytest
+import subprocess
+
+import lttng_ivc.utils.ProjectFactory as ProjectFactory
+import lttng_ivc.settings as project_settings
+
+"""
+TODO: Document how the tests is donne and what it tests
+
+At configure time
+At build time
+At run time
+
+Always include a documentation matrix:
+FC: Fully Compatible
+BC: Backward Compatible
+I: Incompatible
+
++------------------------------------------------------------------+
+| LTTng UST control library vs LTTng Tools                         |
++-----------------------------------------+-----+-----+-----+------+
+| LTTng UST Control(soname) / LTTng Tools | 2.7 | 2.8 | 2.9 | 2.10 |
++-----------------------------------------+-----+-----+-----+------+
+| 2.7 (2.0.0)                             | FC  | I   | I   | I    |
+| 2.8 (2.0.0)                             | BC  | FC  | I   | I    |
+| 2.9 (2.0.0)                             | BC  | BC  | FC  | I    |
+| 2.10 (4.0.0)                            | I   | I   | I   | FC   |
++-----------------------------------------+-----+-----+-----+------+
+
+In this scenario:
+
+FC and BC must pass configure, build and run time
+I must fail at configure, build and run time.
+
+"""
+
+"""
+First tuple member: lttng-ust label
+Second tuple member: lttng-tool label
+Third tuple member: Success.
+                    True -> expect success
+                    False -> expect failure
+"""
+
+test_matrix_label = [
+    ("lttng-ust-2.7", "lttng-tools-2.7", "lttng-ust-2.7", True),
+    ("lttng-ust-2.7", "lttng-tools-2.8", "lttng-ust-2.8", False),
+    ("lttng-ust-2.7", "lttng-tools-2.9", "lttng-ust-2.9", False),
+    ("lttng-ust-2.7", "lttng-tools-2.10", "lttng-ust-2.10", False),
+    ("lttng-ust-2.8", "lttng-tools-2.7", "lttng-ust-2.7", True),
+    ("lttng-ust-2.8", "lttng-tools-2.8", "lttng-ust-2.8", True),
+    ("lttng-ust-2.8", "lttng-tools-2.9", "lttng-ust-2.9", False),
+    ("lttng-ust-2.8", "lttng-tools-2.10", "lttng-ust-2.10", False),
+    ("lttng-ust-2.9", "lttng-tools-2.7", "lttng-ust-2.7", True),
+    ("lttng-ust-2.9", "lttng-tools-2.8", "lttng-ust-2.8", True),
+    ("lttng-ust-2.9", "lttng-tools-2.9", "lttng-ust-2.9", True),
+    ("lttng-ust-2.9", "lttng-tools-2.10", "lttng-ust-2.10", False),
+    ("lttng-ust-2.10", "lttng-tools-2.7", "lttng-ust-2.7", False),
+    ("lttng-ust-2.10", "lttng-tools-2.8", "lttng-ust-2.8", False),
+    ("lttng-ust-2.10", "lttng-tools-2.9", "lttng-ust-2.9", False),
+    ("lttng-ust-2.10", "lttng-tools-2.10", "lttng-ust-2.10", True),
+]
+
+runtime_matrix_label = []
+if not project_settings.test_only:
+    runtime_matrix_label = test_matrix_label
+else:
+    for tup in test_matrix_label:
+        ust_label, tools_label = tup[0], tup[1]
+        if (ust_label in project_settings.test_only or tools_label in
+                project_settings.test_only):
+            runtime_matrix_label.append(tup)
+
+
+@pytest.mark.parametrize("ust_label,tools_label,base_tools_ust_dep,should_pass", runtime_matrix_label)
+def test_soname_configure(tmpdir, ust_label, tools_label, base_tools_ust_dep, should_pass):
+    ust = ProjectFactory.get(ust_label, str(tmpdir.mkdir("lttng-ust")))
+    tools = ProjectFactory.get(tools_label, str(tmpdir.mkdir("lttng-tools")))
+
+    ust.autobuild()
+
+    tools.dependencies.append(ust)
+    # TODO: Propose fixes to upstream regarding the check
+    if not should_pass:
+        # Making sure we get a error here
+        pytest.xfail("passing configure but should fail See todo")
+        with pytest.raises(subprocess.CalledProcessError) as error:
+            tools.configure()
+        print(error)
+    else:
+        # An exception is thrown on errors
+        # TODO MAYBE: wrap around a try and perform error printing + save
+        # stdout stderr etc. Or move all this handling inside the function and
+        # reraise the error (bubble up)
+        tools.configure()
+
+
+@pytest.mark.parametrize("ust_label,tools_label,base_tools_ust_dep,should_pass", runtime_matrix_label)
+def test_soname_build(tmpdir, ust_label, tools_label, base_tools_ust_dep, should_pass):
+    ust = ProjectFactory.get(ust_label, str(tmpdir.mkdir("lttng-ust")))
+    tools = ProjectFactory.get(tools_label, str(tmpdir.mkdir("lttng-tools")))
+    ust_configure_mockup = ProjectFactory.get(ust_label, str(tmpdir.mkdir("lttng-ust-base")))
+
+    ust.autobuild()
+    ust_configure_mockup.autobuild()
+
+    # Fool configure
+    tools.dependencies.append(ust_configure_mockup)
+    tools.configure()
+
+    # Use ust under test
+    tools.special_env_variables["CPPFLAGS"] = ust.get_cppflags()
+    tools.special_env_variables["LDFLAGS"] = ust.get_ldflags()
+    tools.special_env_variables["LD_LIBRARY_PATH"] = ust.get_ld_library_path()
+
+    if not should_pass:
+        # Making sure we get a error here
+        with pytest.raises(subprocess.CalledProcessError) as error:
+            tools.build()
+        print(error)
+    else:
+        # An exception is thrown on errors
+        tools.build()
diff --git a/lttng_ivc/utils/ProjectFactory.py b/lttng_ivc/utils/ProjectFactory.py
new file mode 100644 (file)
index 0000000..93b32c1
--- /dev/null
@@ -0,0 +1,33 @@
+import os
+import logging
+import yaml
+
+import lttng_ivc.utils.project as Project
+
+
+_logger = logging.getLogger('project.factory')
+_conf_file = os.path.dirname(os.path.abspath(__file__)) + "/../run_configuration.yaml"
+_project_constructor = {
+        'babeltrace': Project.Babeltrace,
+        'lttng-modules': Project.Lttng_modules,
+        'lttng-tools': Project.Lttng_tools,
+        'lttng-ust': Project.Lttng_ust,
+}
+
+_markers = None
+with open(_conf_file, 'r') as stream:
+    # This is voluntary static across call, no need to perform this
+    # every time.
+    _markers = yaml.load(stream)
+
+
+def get(label, tmpdir):
+    if label not in _markers:
+        # TODO: specialized exception, handle it caller-side so the caller
+        # can decide to skip or fail test.
+        raise Exception('Label is no present')
+    marker = _markers[label]
+    constructor = _project_constructor[marker['project']]
+    path = marker['path']
+    sha1 = marker['sha1']
+    return constructor(label, path, sha1, tmpdir)
diff --git a/lttng_ivc/utils/project.py b/lttng_ivc/utils/project.py
new file mode 100644 (file)
index 0000000..fb8c5c8
--- /dev/null
@@ -0,0 +1,235 @@
+import os
+import shutil
+import git
+import subprocess
+import logging
+
+
+class Project(object):
+
+    def __init__(self, label, git_path, sha1, tmpdir):
+        self.label = label
+        self.git_path = git_path
+        self.sha1 = sha1
+
+        """ Custom configure flags in the for of ['-x', 'arg']"""
+        self.custom_configure_flags = []
+        ccache = shutil.which("ccache")
+        if ccache is not None:
+            self.custom_configure_flags.append("CC={} gcc".format(ccache))
+            self.custom_configure_flags.append("CXX={} g++".format(ccache))
+
+        """ A collection of Project dependencies """
+        self.dependencies = []
+
+        # State
+        self.isCheckedOut = False
+        self.isBootStrapped = False
+        self.isBuilt = False
+        self.isConfigured = False
+        self.isInstalled = False
+
+        self.source_path = tmpdir + "/source"
+        self.installation_path = tmpdir + "/install"
+        os.makedirs(self.source_path)
+        os.makedirs(self.installation_path)
+        self.logger = logging.getLogger('project.{}'.format(self.label))
+
+        self.special_env_variables = {}
+
+        # Init the repo for work
+        self.checkout()
+        self.bootstrap()
+
+    def get_cppflags(self):
+        return " -I{}/include".format(self.installation_path)
+
+    def get_ldflags(self):
+        return " -L{}/lib".format(self.installation_path)
+
+    def get_ld_library_path(self):
+        return "{}/lib".format(self.installation_path)
+
+    def get_env(self):
+        """Modify environment to reflect dependency"""
+        cpp_flags = ""
+        ld_flags = ""
+        ld_library_path = ""
+
+        env = os.environ.copy()
+
+        for var, value in self.special_env_variables.items():
+            if var in env:
+                # TODO: WARNING log point
+                # Raise for now since no special cases is known
+                self.logger.warning("Special var % is already defined", var)
+                raise Exception("Multiple definition of a special environment variable")
+            else:
+                env[var] = value
+
+        for dep in self.dependencies:
+            # Extra space just in case
+            cpp_flags += " {}".format(dep.get_cppflags())
+            ld_flags += " {}".format(dep.get_ldflags())
+            ld_library_path += "{}:".format(dep.get_ld_library_path())
+            for var, value in dep.special_env_variables.items():
+                if var in env:
+                    # TODO: WARNING log point
+                    # Raise for now since no special cases is known
+                    self.logger.warning("Special var % is already defined", var)
+                    raise Exception("Multiple definition of a special environment variable")
+                else:
+                    env[var] = value
+
+        # TODO: INFO log point for each variable with project information
+        if cpp_flags:
+            if 'CPPFLAGS' in env:
+                cpp_flags = env['CPPFLAGS'] + cpp_flags
+            env['CPPFLAGS'] = cpp_flags
+        if ld_flags:
+            if 'LDFLAGS' in env:
+                ld_flags = env['LDFLAGS'] + ld_flags
+            env['LDFLAGS'] = ld_flags
+        if ld_library_path:
+            if 'LD_LIBRARY_PATH' in env:
+                ld_library_path = env['LD_LIBRARY_PATH'] + ":" + ld_library_path
+            env['LD_LIBRARY_PATH'] = ld_library_path
+        return env
+
+    def autobuild(self):
+        """
+        Perform the bootstrap, configuration, build and install the
+        project. Build dependencies if not already built
+        """
+        for dep in self.dependencies:
+            dep.autobuild()
+
+        if self.isCheckedOut ^ self.isBootStrapped ^ self.isBootStrapped ^ self.isBuilt ^ self.isConfigured ^ self.isInstalled:
+            raise Exception("Project steps where manually triggered. Can't autobuild")
+
+        self.configure()
+        self.build()
+        self.install()
+
+    def checkout(self):
+        repo = git.Repo.clone_from(self.git_path, self.source_path)
+        commit = repo.commit(self.sha1)
+        repo.head.reference = commit
+        assert repo.head.is_detached
+        repo.head.reset(index=True, working_tree=True)
+
+    def bootstrap(self):
+        """
+        Bootstap the project. Raise subprocess.CalledProcessError on
+        bootstrap error.
+        """
+        os.chdir(self.source_path)
+        p = subprocess.run(['./bootstrap'], stdout=subprocess.PIPE,
+                           stderr=subprocess.PIPE)
+        p.check_returncode()
+        return p
+
+    def configure(self):
+        """
+        Configure the project.
+        Raises subprocess.CalledProcessError on configure error
+        """
+        # Check that all our dependencies were actually installed
+        for dep in self.dependencies:
+            if not dep.isInstalled:
+                # TODO: Custom exception here Dependency Error
+                raise Exception("Dependency project flagged as not installed")
+
+        os.chdir(self.source_path)
+        args = ['./configure']
+        prefix = '--prefix={}'.format(self.installation_path)
+        args.append(prefix)
+        args.extend(self.custom_configure_flags)
+
+        # TODO: log output and add INFO log point
+        p = subprocess.run(args, env=self.get_env(), stdout=subprocess.PIPE,
+                           stderr=subprocess.PIPE)
+        p.check_returncode()
+        self.isConfigured = True
+        return p
+
+    def build(self):
+        """
+        Build the project. Raise subprocess.CalledProcessError on build
+        error.
+        """
+        os.chdir(self.source_path)
+        args = ['make']
+        env = self.get_env()
+        env['CFLAGS'] = '-g -O0'
+
+        # Number of usable cpu
+        # https://docs.python.org/3/library/os.html#os.cpu_count
+        num_cpu = str(len(os.sched_getaffinity(0)))
+        args.append('-j')
+        args.append(num_cpu)
+
+        # TODO: log output and add INFO log point with args
+        p = subprocess.run(args, env=env, stdout=subprocess.PIPE,
+                           stderr=subprocess.PIPE)
+        p.check_returncode()
+        self.isBuilt = True
+        return p
+
+    def install(self):
+        """
+        Install the project. Raise subprocess.CalledProcessError on
+        bootstrap error
+        """
+        os.chdir(self.source_path)
+        args = ['make', 'install']
+
+        # TODO: log output and add INFO log point
+        p = subprocess.run(args, env=self.get_env(), stdout=subprocess.PIPE,
+                           stderr=subprocess.PIPE)
+        p.check_returncode()
+        self.isInstalled = True
+        return p
+
+    def cleanup(self):
+        if os.path.exists(self.source_path):
+            shutil.rmtree(self.source_path)
+        if os.path.exists(self.installation_path):
+            shutil.rmtree(self.installation_path)
+
+
+class Lttng_modules(Project):
+    def bootstrap(self):
+        pass
+
+    def configure(self):
+        pass
+
+    def install(self):
+        os.chdir(self.source_path)
+        args = ['make', 'INSTALL_MOD_PATH={}'.format(self.installation_path),
+                'modules_install']
+        p = subprocess.run(args, env=self.get_env(), stdout=subprocess.PIPE,
+                           stderr=subprocess.PIPE)
+        p.check_returncode()
+
+        # Perform a local depmod
+        args = ['depmod', '-b', self.installation_path]
+        p = subprocess.run(args, env=self.get_env())
+        p.check_returncode()
+        self.isInstalled = True
+
+
+class Lttng_ust(Project):
+    def __init__(self, label, git_path, sha1, tmpdir):
+        super(Lttng_ust, self).__init__(label=label, git_path=git_path,
+                                        sha1=sha1, tmpdir=tmpdir)
+        self.custom_configure_flags.extend(['--disable-man-pages'])
+
+
+class Lttng_tools(Project):
+    pass
+
+
+class Babeltrace(Project):
+    pass
diff --git a/lttng_ivc/utils/skip.py b/lttng_ivc/utils/skip.py
new file mode 100644 (file)
index 0000000..10f92d3
--- /dev/null
@@ -0,0 +1,4 @@
+import os
+import pytest
+
+root = pytest.mark.skipif(os.geteuid() != 0, reason="Must be run as root")
This page took 0.030813 seconds and 5 git commands to generate.