diff options
| -rw-r--r-- | meta/lib/oeqa/selftest/__init__.py | 2 | ||||
| -rw-r--r-- | meta/lib/oeqa/selftest/base.py | 98 | ||||
| -rw-r--r-- | meta/lib/oeqa/utils/commands.py | 137 | ||||
| -rw-r--r-- | meta/lib/oeqa/utils/ftools.py | 27 | ||||
| -rwxr-xr-x | scripts/oe-selftest | 148 |
5 files changed, 412 insertions, 0 deletions
diff --git a/meta/lib/oeqa/selftest/__init__.py b/meta/lib/oeqa/selftest/__init__.py new file mode 100644 index 0000000000..3ad9513f40 --- /dev/null +++ b/meta/lib/oeqa/selftest/__init__.py | |||
| @@ -0,0 +1,2 @@ | |||
| 1 | from pkgutil import extend_path | ||
| 2 | __path__ = extend_path(__path__, __name__) | ||
diff --git a/meta/lib/oeqa/selftest/base.py b/meta/lib/oeqa/selftest/base.py new file mode 100644 index 0000000000..30a71e886f --- /dev/null +++ b/meta/lib/oeqa/selftest/base.py | |||
| @@ -0,0 +1,98 @@ | |||
| 1 | # Copyright (c) 2013 Intel Corporation | ||
| 2 | # | ||
| 3 | # Released under the MIT license (see COPYING.MIT) | ||
| 4 | |||
| 5 | |||
| 6 | # DESCRIPTION | ||
| 7 | # Base class inherited by test classes in meta/lib/selftest | ||
| 8 | |||
| 9 | import unittest | ||
| 10 | import os | ||
| 11 | import sys | ||
| 12 | import logging | ||
| 13 | import errno | ||
| 14 | |||
| 15 | import oeqa.utils.ftools as ftools | ||
| 16 | |||
| 17 | |||
| 18 | class oeSelfTest(unittest.TestCase): | ||
| 19 | |||
| 20 | log = logging.getLogger("selftest.base") | ||
| 21 | longMessage = True | ||
| 22 | |||
| 23 | def __init__(self, methodName="runTest"): | ||
| 24 | self.builddir = os.environ.get("BUILDDIR") | ||
| 25 | self.localconf_path = os.path.join(self.builddir, "conf/local.conf") | ||
| 26 | self.testinc_path = os.path.join(self.builddir, "conf/selftest.inc") | ||
| 27 | self.testlayer_path = oeSelfTest.testlayer_path | ||
| 28 | super(oeSelfTest, self).__init__(methodName) | ||
| 29 | |||
| 30 | def setUp(self): | ||
| 31 | os.chdir(self.builddir) | ||
| 32 | # we don't know what the previous test left around in config or inc files | ||
| 33 | # if it failed so we need a fresh start | ||
| 34 | try: | ||
| 35 | os.remove(self.testinc_path) | ||
| 36 | except OSError as e: | ||
| 37 | if e.errno != errno.ENOENT: | ||
| 38 | raise | ||
| 39 | for root, _, files in os.walk(self.testlayer_path): | ||
| 40 | for f in files: | ||
| 41 | if f == 'test_recipe.inc': | ||
| 42 | os.remove(os.path.join(root, f)) | ||
| 43 | # tests might need their own setup | ||
| 44 | # but if they overwrite this one they have to call | ||
| 45 | # super each time, so let's give them an alternative | ||
| 46 | self.setUpLocal() | ||
| 47 | |||
| 48 | def setUpLocal(self): | ||
| 49 | pass | ||
| 50 | |||
| 51 | def tearDown(self): | ||
| 52 | self.tearDownLocal() | ||
| 53 | |||
| 54 | def tearDownLocal(self): | ||
| 55 | pass | ||
| 56 | |||
| 57 | # write to <builddir>/conf/selftest.inc | ||
| 58 | def write_config(self, data): | ||
| 59 | self.log.debug("Writing to: %s\n%s\n" % (self.testinc_path, data)) | ||
| 60 | ftools.write_file(self.testinc_path, data) | ||
| 61 | |||
| 62 | # append to <builddir>/conf/selftest.inc | ||
| 63 | def append_config(self, data): | ||
| 64 | self.log.debug("Appending to: %s\n%s\n" % (self.testinc_path, data)) | ||
| 65 | ftools.append_file(self.testinc_path, data) | ||
| 66 | |||
| 67 | # remove data from <builddir>/conf/selftest.inc | ||
| 68 | def remove_config(self, data): | ||
| 69 | self.log.debug("Removing from: %s\n\%s\n" % (self.testinc_path, data)) | ||
| 70 | ftools.remove_from_file(self.testinc_path, data) | ||
| 71 | |||
| 72 | # write to meta-sefltest/recipes-test/<recipe>/test_recipe.inc | ||
| 73 | def write_recipeinc(self, recipe, data): | ||
| 74 | inc_file = os.path.join(self.testlayer_path, 'recipes-test', recipe, 'test_recipe.inc') | ||
| 75 | self.log.debug("Writing to: %s\n%s\n" % (inc_file, data)) | ||
| 76 | ftools.write_file(inc_file, data) | ||
| 77 | |||
| 78 | # append data to meta-sefltest/recipes-test/<recipe>/test_recipe.inc | ||
| 79 | def append_recipeinc(self, recipe, data): | ||
| 80 | inc_file = os.path.join(self.testlayer_path, 'recipes-test', recipe, 'test_recipe.inc') | ||
| 81 | self.log.debug("Appending to: %s\n%s\n" % (inc_file, data)) | ||
| 82 | ftools.append_file(inc_file, data) | ||
| 83 | |||
| 84 | # remove data from meta-sefltest/recipes-test/<recipe>/test_recipe.inc | ||
| 85 | def remove_recipeinc(self, recipe, data): | ||
| 86 | inc_file = os.path.join(self.testlayer_path, 'recipes-test', recipe, 'test_recipe.inc') | ||
| 87 | self.log.debug("Removing from: %s\n%s\n" % (inc_file, data)) | ||
| 88 | ftools.remove_from_file(inc_file, data) | ||
| 89 | |||
| 90 | # delete meta-sefltest/recipes-test/<recipe>/test_recipe.inc file | ||
| 91 | def delete_recipeinc(self, recipe): | ||
| 92 | inc_file = os.path.join(self.testlayer_path, 'recipes-test', recipe, 'test_recipe.inc') | ||
| 93 | self.log.debug("Deleting file: %s" % inc_file) | ||
| 94 | try: | ||
| 95 | os.remove(self.testinc_path) | ||
| 96 | except OSError as e: | ||
| 97 | if e.errno != errno.ENOENT: | ||
| 98 | raise | ||
diff --git a/meta/lib/oeqa/utils/commands.py b/meta/lib/oeqa/utils/commands.py new file mode 100644 index 0000000000..9b42620610 --- /dev/null +++ b/meta/lib/oeqa/utils/commands.py | |||
| @@ -0,0 +1,137 @@ | |||
| 1 | # Copyright (c) 2013 Intel Corporation | ||
| 2 | # | ||
| 3 | # Released under the MIT license (see COPYING.MIT) | ||
| 4 | |||
| 5 | # DESCRIPTION | ||
| 6 | # This module is mainly used by scripts/oe-selftest and modules under meta/oeqa/selftest | ||
| 7 | # It provides a class and methods for running commands on the host in a convienent way for tests. | ||
| 8 | |||
| 9 | |||
| 10 | |||
| 11 | import os | ||
| 12 | import sys | ||
| 13 | import signal | ||
| 14 | import subprocess | ||
| 15 | import threading | ||
| 16 | import logging | ||
| 17 | |||
| 18 | class Command(object): | ||
| 19 | def __init__(self, command, bg=False, timeout=None, data=None, **options): | ||
| 20 | |||
| 21 | self.defaultopts = { | ||
| 22 | "stdout": subprocess.PIPE, | ||
| 23 | "stderr": subprocess.STDOUT, | ||
| 24 | "stdin": None, | ||
| 25 | "shell": False, | ||
| 26 | "bufsize": -1, | ||
| 27 | } | ||
| 28 | |||
| 29 | self.cmd = command | ||
| 30 | self.bg = bg | ||
| 31 | self.timeout = timeout | ||
| 32 | self.data = data | ||
| 33 | |||
| 34 | self.options = dict(self.defaultopts) | ||
| 35 | if isinstance(self.cmd, basestring): | ||
| 36 | self.options["shell"] = True | ||
| 37 | if self.data: | ||
| 38 | self.options['stdin'] = subprocess.PIPE | ||
| 39 | self.options.update(options) | ||
| 40 | |||
| 41 | self.status = None | ||
| 42 | self.output = None | ||
| 43 | self.error = None | ||
| 44 | self.thread = None | ||
| 45 | |||
| 46 | self.log = logging.getLogger("utils.commands") | ||
| 47 | |||
| 48 | def run(self): | ||
| 49 | self.process = subprocess.Popen(self.cmd, **self.options) | ||
| 50 | |||
| 51 | def commThread(): | ||
| 52 | self.output, self.error = self.process.communicate(self.data) | ||
| 53 | |||
| 54 | self.thread = threading.Thread(target=commThread) | ||
| 55 | self.thread.start() | ||
| 56 | |||
| 57 | self.log.debug("Running command '%s'" % self.cmd) | ||
| 58 | |||
| 59 | if not self.bg: | ||
| 60 | self.thread.join(self.timeout) | ||
| 61 | self.stop() | ||
| 62 | |||
| 63 | def stop(self): | ||
| 64 | if self.thread.isAlive(): | ||
| 65 | self.process.terminate() | ||
| 66 | # let's give it more time to terminate gracefully before killing it | ||
| 67 | self.thread.join(5) | ||
| 68 | if self.thread.isAlive(): | ||
| 69 | self.process.kill() | ||
| 70 | self.thread.join() | ||
| 71 | |||
| 72 | self.output = self.output.rstrip() | ||
| 73 | self.status = self.process.poll() | ||
| 74 | |||
| 75 | self.log.debug("Command '%s' returned %d as exit code." % (self.cmd, self.status)) | ||
| 76 | # logging the complete output is insane | ||
| 77 | # bitbake -e output is really big | ||
| 78 | # and makes the log file useless | ||
| 79 | if self.status: | ||
| 80 | lout = "\n".join(self.output.splitlines()[-20:]) | ||
| 81 | self.log.debug("Last 20 lines:\n%s" % lout) | ||
| 82 | |||
| 83 | |||
| 84 | class Result(object): | ||
| 85 | pass | ||
| 86 | |||
| 87 | def runCmd(command, ignore_status=False, timeout=None, **options): | ||
| 88 | |||
| 89 | result = Result() | ||
| 90 | |||
| 91 | cmd = Command(command, timeout=timeout, **options) | ||
| 92 | cmd.run() | ||
| 93 | |||
| 94 | result.command = command | ||
| 95 | result.status = cmd.status | ||
| 96 | result.output = cmd.output | ||
| 97 | result.pid = cmd.process.pid | ||
| 98 | |||
| 99 | if result.status and not ignore_status: | ||
| 100 | raise AssertionError("Command '%s' returned non-zero exit status %d:\n%s" % (command, result.status, result.output)) | ||
| 101 | |||
| 102 | return result | ||
| 103 | |||
| 104 | |||
| 105 | def bitbake(command, ignore_status=False, timeout=None, **options): | ||
| 106 | if isinstance(command, basestring): | ||
| 107 | cmd = "bitbake " + command | ||
| 108 | else: | ||
| 109 | cmd = [ "bitbake" ] + command | ||
| 110 | |||
| 111 | return runCmd(cmd, ignore_status, timeout, **options) | ||
| 112 | |||
| 113 | |||
| 114 | def get_bb_env(target=None): | ||
| 115 | if target: | ||
| 116 | return runCmd("bitbake -e %s" % target).output | ||
| 117 | else: | ||
| 118 | return runCmd("bitbake -e").output | ||
| 119 | |||
| 120 | def get_bb_var(var, target=None): | ||
| 121 | val = None | ||
| 122 | bbenv = get_bb_env(target) | ||
| 123 | for line in bbenv.splitlines(): | ||
| 124 | if line.startswith(var + "="): | ||
| 125 | val = line.split('=')[1] | ||
| 126 | val = val.replace('\"','') | ||
| 127 | break | ||
| 128 | return val | ||
| 129 | |||
| 130 | def get_test_layer(): | ||
| 131 | layers = get_bb_var("BBLAYERS").split() | ||
| 132 | testlayer = None | ||
| 133 | for l in layers: | ||
| 134 | if "/meta-selftest" in l and os.path.isdir(l): | ||
| 135 | testlayer = l | ||
| 136 | break | ||
| 137 | return testlayer | ||
diff --git a/meta/lib/oeqa/utils/ftools.py b/meta/lib/oeqa/utils/ftools.py new file mode 100644 index 0000000000..64ebe3d217 --- /dev/null +++ b/meta/lib/oeqa/utils/ftools.py | |||
| @@ -0,0 +1,27 @@ | |||
| 1 | import os | ||
| 2 | import re | ||
| 3 | |||
| 4 | def write_file(path, data): | ||
| 5 | wdata = data.rstrip() + "\n" | ||
| 6 | with open(path, "w") as f: | ||
| 7 | f.write(wdata) | ||
| 8 | |||
| 9 | def append_file(path, data): | ||
| 10 | wdata = data.rstrip() + "\n" | ||
| 11 | with open(path, "a") as f: | ||
| 12 | f.write(wdata) | ||
| 13 | |||
| 14 | def read_file(path): | ||
| 15 | data = None | ||
| 16 | with open(path) as f: | ||
| 17 | data = f.read() | ||
| 18 | return data | ||
| 19 | |||
| 20 | def remove_from_file(path, data): | ||
| 21 | lines = read_file(path).splitlines() | ||
| 22 | rmdata = data.strip().splitlines() | ||
| 23 | for l in rmdata: | ||
| 24 | for c in range(0, lines.count(l)): | ||
| 25 | i = lines.index(l) | ||
| 26 | del(lines[i]) | ||
| 27 | write_file(path, "\n".join(lines)) | ||
diff --git a/scripts/oe-selftest b/scripts/oe-selftest new file mode 100755 index 0000000000..db42e73470 --- /dev/null +++ b/scripts/oe-selftest | |||
| @@ -0,0 +1,148 @@ | |||
| 1 | #!/usr/bin/env python | ||
| 2 | |||
| 3 | # Copyright (c) 2013 Intel Corporation | ||
| 4 | # | ||
| 5 | # This program is free software; you can redistribute it and/or modify | ||
| 6 | # it under the terms of the GNU General Public License version 2 as | ||
| 7 | # published by the Free Software Foundation. | ||
| 8 | # | ||
| 9 | # This program is distributed in the hope that it will be useful, | ||
| 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
| 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
| 12 | # GNU General Public License for more details. | ||
| 13 | # | ||
| 14 | # You should have received a copy of the GNU General Public License along | ||
| 15 | # with this program; if not, write to the Free Software Foundation, Inc., | ||
| 16 | # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. | ||
| 17 | |||
| 18 | # DESCRIPTION | ||
| 19 | # This script runs tests defined in meta/lib/selftest/ | ||
| 20 | # It's purpose is to automate the testing of different bitbake tools. | ||
| 21 | # To use it you just need to source your build environment setup script and | ||
| 22 | # add the meta-selftest layer to your BBLAYERS. | ||
| 23 | # Call the script as: "oe-selftest" to run all the tests in in meta/lib/selftest/ | ||
| 24 | # Call the script as: "oe-selftest <module>.<Class>.<method>" to run just a single test | ||
| 25 | # E.g: "oe-selftest bboutput.BitbakeLayers" will run just the BitbakeLayers class from meta/lib/selftest/bboutput.py | ||
| 26 | |||
| 27 | |||
| 28 | import os | ||
| 29 | import sys | ||
| 30 | import unittest | ||
| 31 | import logging | ||
| 32 | |||
| 33 | sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..', 'meta/lib'))) | ||
| 34 | |||
| 35 | import oeqa.selftest | ||
| 36 | import oeqa.utils.ftools as ftools | ||
| 37 | from oeqa.utils.commands import runCmd, get_bb_var, get_test_layer | ||
| 38 | from oeqa.selftest.base import oeSelfTest | ||
| 39 | |||
| 40 | def logger_create(): | ||
| 41 | log = logging.getLogger("selftest") | ||
| 42 | log.setLevel(logging.DEBUG) | ||
| 43 | |||
| 44 | fh = logging.FileHandler(filename='oe-selftest.log', mode='w') | ||
| 45 | fh.setLevel(logging.DEBUG) | ||
| 46 | |||
| 47 | ch = logging.StreamHandler(sys.stdout) | ||
| 48 | ch.setLevel(logging.INFO) | ||
| 49 | |||
| 50 | formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') | ||
| 51 | fh.setFormatter(formatter) | ||
| 52 | ch.setFormatter(formatter) | ||
| 53 | |||
| 54 | log.addHandler(fh) | ||
| 55 | log.addHandler(ch) | ||
| 56 | |||
| 57 | return log | ||
| 58 | |||
| 59 | log = logger_create() | ||
| 60 | |||
| 61 | def preflight_check(): | ||
| 62 | |||
| 63 | log.info("Checking that everything is in order before running the tests") | ||
| 64 | |||
| 65 | if not os.environ.get("BUILDDIR"): | ||
| 66 | log.error("BUILDDIR isn't set. Did you forget to source your build environment setup script?") | ||
| 67 | return False | ||
| 68 | |||
| 69 | builddir = os.environ.get("BUILDDIR") | ||
| 70 | if os.getcwd() != builddir: | ||
| 71 | log.info("Changing cwd to %s" % builddir) | ||
| 72 | os.chdir(builddir) | ||
| 73 | |||
| 74 | if not "meta-selftest" in get_bb_var("BBLAYERS"): | ||
| 75 | log.error("You don't seem to have the meta-selftest layer in BBLAYERS") | ||
| 76 | return False | ||
| 77 | |||
| 78 | log.info("Running bitbake -p") | ||
| 79 | runCmd("bitbake -p") | ||
| 80 | |||
| 81 | return True | ||
| 82 | |||
| 83 | def add_include(): | ||
| 84 | builddir = os.environ.get("BUILDDIR") | ||
| 85 | if "#include added by oe-selftest.py" \ | ||
| 86 | not in ftools.read_file(os.path.join(builddir, "conf/local.conf")): | ||
| 87 | log.info("Adding: \"include selftest.inc\" in local.conf") | ||
| 88 | ftools.append_file(os.path.join(builddir, "conf/local.conf"), \ | ||
| 89 | "\n#include added by oe-selftest.py\ninclude selftest.inc") | ||
| 90 | |||
| 91 | |||
| 92 | def remove_include(): | ||
| 93 | builddir = os.environ.get("BUILDDIR") | ||
| 94 | if "#include added by oe-selftest.py" \ | ||
| 95 | in ftools.read_file(os.path.join(builddir, "conf/local.conf")): | ||
| 96 | log.info("Removing the include from local.conf") | ||
| 97 | ftools.remove_from_file(os.path.join(builddir, "conf/local.conf"), \ | ||
| 98 | "#include added by oe-selftest.py\ninclude selftest.inc") | ||
| 99 | |||
| 100 | def get_tests(): | ||
| 101 | testslist = [] | ||
| 102 | for x in sys.argv[1:]: | ||
| 103 | testslist.append('oeqa.selftest.' + x) | ||
| 104 | if not testslist: | ||
| 105 | testpath = os.path.abspath(os.path.dirname(oeqa.selftest.__file__)) | ||
| 106 | files = sorted([f for f in os.listdir(testpath) if f.endswith('.py') and not f.startswith('_') and f != 'base.py']) | ||
| 107 | for f in files: | ||
| 108 | module = 'oeqa.selftest.' + f[:-3] | ||
| 109 | testslist.append(module) | ||
| 110 | |||
| 111 | return testslist | ||
| 112 | |||
| 113 | def main(): | ||
| 114 | if not preflight_check(): | ||
| 115 | return 1 | ||
| 116 | |||
| 117 | testslist = get_tests() | ||
| 118 | suite = unittest.TestSuite() | ||
| 119 | loader = unittest.TestLoader() | ||
| 120 | loader.sortTestMethodsUsing = None | ||
| 121 | runner = unittest.TextTestRunner(verbosity=2) | ||
| 122 | # we need to do this here, otherwise just loading the tests | ||
| 123 | # will take 2 minutes (bitbake -e calls) | ||
| 124 | oeSelfTest.testlayer_path = get_test_layer() | ||
| 125 | for test in testslist: | ||
| 126 | log.info("Loading tests from: %s" % test) | ||
| 127 | try: | ||
| 128 | suite.addTests(loader.loadTestsFromName(test)) | ||
| 129 | except AttributeError as e: | ||
| 130 | log.error("Failed to import %s" % test) | ||
| 131 | log.error(e) | ||
| 132 | return 1 | ||
| 133 | add_include() | ||
| 134 | result = runner.run(suite) | ||
| 135 | log.info("Finished") | ||
| 136 | |||
| 137 | return 0 | ||
| 138 | |||
| 139 | if __name__ == "__main__": | ||
| 140 | try: | ||
| 141 | ret = main() | ||
| 142 | except Exception: | ||
| 143 | ret = 1 | ||
| 144 | import traceback | ||
| 145 | traceback.print_exc(5) | ||
| 146 | finally: | ||
| 147 | remove_include() | ||
| 148 | sys.exit(ret) | ||
