diff options
Diffstat (limited to 'meta/lib/oeqa/utils')
-rw-r--r-- | meta/lib/oeqa/utils/__init__.py | 15 | ||||
-rw-r--r-- | meta/lib/oeqa/utils/commands.py | 154 | ||||
-rw-r--r-- | meta/lib/oeqa/utils/decorators.py | 158 | ||||
-rw-r--r-- | meta/lib/oeqa/utils/ftools.py | 27 | ||||
-rw-r--r-- | meta/lib/oeqa/utils/httpserver.py | 35 | ||||
-rw-r--r-- | meta/lib/oeqa/utils/logparser.py | 125 | ||||
-rw-r--r-- | meta/lib/oeqa/utils/qemurunner.py | 237 | ||||
-rw-r--r-- | meta/lib/oeqa/utils/sshcontrol.py | 138 | ||||
-rw-r--r-- | meta/lib/oeqa/utils/targetbuild.py | 132 |
9 files changed, 1021 insertions, 0 deletions
diff --git a/meta/lib/oeqa/utils/__init__.py b/meta/lib/oeqa/utils/__init__.py new file mode 100644 index 0000000000..2260046026 --- /dev/null +++ b/meta/lib/oeqa/utils/__init__.py | |||
@@ -0,0 +1,15 @@ | |||
1 | # Enable other layers to have modules in the same named directory | ||
2 | from pkgutil import extend_path | ||
3 | __path__ = extend_path(__path__, __name__) | ||
4 | |||
5 | |||
6 | # Borrowed from CalledProcessError | ||
7 | |||
8 | class CommandError(Exception): | ||
9 | def __init__(self, retcode, cmd, output = None): | ||
10 | self.retcode = retcode | ||
11 | self.cmd = cmd | ||
12 | self.output = output | ||
13 | def __str__(self): | ||
14 | return "Command '%s' returned non-zero exit status %d with output: %s" % (self.cmd, self.retcode, self.output) | ||
15 | |||
diff --git a/meta/lib/oeqa/utils/commands.py b/meta/lib/oeqa/utils/commands.py new file mode 100644 index 0000000000..802bc2f208 --- /dev/null +++ b/meta/lib/oeqa/utils/commands.py | |||
@@ -0,0 +1,154 @@ | |||
1 | # Copyright (c) 2013-2014 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 | from oeqa.utils import CommandError | ||
18 | from oeqa.utils import ftools | ||
19 | |||
20 | class Command(object): | ||
21 | def __init__(self, command, bg=False, timeout=None, data=None, **options): | ||
22 | |||
23 | self.defaultopts = { | ||
24 | "stdout": subprocess.PIPE, | ||
25 | "stderr": subprocess.STDOUT, | ||
26 | "stdin": None, | ||
27 | "shell": False, | ||
28 | "bufsize": -1, | ||
29 | } | ||
30 | |||
31 | self.cmd = command | ||
32 | self.bg = bg | ||
33 | self.timeout = timeout | ||
34 | self.data = data | ||
35 | |||
36 | self.options = dict(self.defaultopts) | ||
37 | if isinstance(self.cmd, basestring): | ||
38 | self.options["shell"] = True | ||
39 | if self.data: | ||
40 | self.options['stdin'] = subprocess.PIPE | ||
41 | self.options.update(options) | ||
42 | |||
43 | self.status = None | ||
44 | self.output = None | ||
45 | self.error = None | ||
46 | self.thread = None | ||
47 | |||
48 | self.log = logging.getLogger("utils.commands") | ||
49 | |||
50 | def run(self): | ||
51 | self.process = subprocess.Popen(self.cmd, **self.options) | ||
52 | |||
53 | def commThread(): | ||
54 | self.output, self.error = self.process.communicate(self.data) | ||
55 | |||
56 | self.thread = threading.Thread(target=commThread) | ||
57 | self.thread.start() | ||
58 | |||
59 | self.log.debug("Running command '%s'" % self.cmd) | ||
60 | |||
61 | if not self.bg: | ||
62 | self.thread.join(self.timeout) | ||
63 | self.stop() | ||
64 | |||
65 | def stop(self): | ||
66 | if self.thread.isAlive(): | ||
67 | self.process.terminate() | ||
68 | # let's give it more time to terminate gracefully before killing it | ||
69 | self.thread.join(5) | ||
70 | if self.thread.isAlive(): | ||
71 | self.process.kill() | ||
72 | self.thread.join() | ||
73 | |||
74 | self.output = self.output.rstrip() | ||
75 | self.status = self.process.poll() | ||
76 | |||
77 | self.log.debug("Command '%s' returned %d as exit code." % (self.cmd, self.status)) | ||
78 | # logging the complete output is insane | ||
79 | # bitbake -e output is really big | ||
80 | # and makes the log file useless | ||
81 | if self.status: | ||
82 | lout = "\n".join(self.output.splitlines()[-20:]) | ||
83 | self.log.debug("Last 20 lines:\n%s" % lout) | ||
84 | |||
85 | |||
86 | class Result(object): | ||
87 | pass | ||
88 | |||
89 | |||
90 | def runCmd(command, ignore_status=False, timeout=None, assert_error=True, **options): | ||
91 | result = Result() | ||
92 | |||
93 | cmd = Command(command, timeout=timeout, **options) | ||
94 | cmd.run() | ||
95 | |||
96 | result.command = command | ||
97 | result.status = cmd.status | ||
98 | result.output = cmd.output | ||
99 | result.pid = cmd.process.pid | ||
100 | |||
101 | if result.status and not ignore_status: | ||
102 | if assert_error: | ||
103 | raise AssertionError("Command '%s' returned non-zero exit status %d:\n%s" % (command, result.status, result.output)) | ||
104 | else: | ||
105 | raise CommandError(result.status, command, result.output) | ||
106 | |||
107 | return result | ||
108 | |||
109 | |||
110 | def bitbake(command, ignore_status=False, timeout=None, postconfig=None, **options): | ||
111 | |||
112 | if postconfig: | ||
113 | postconfig_file = os.path.join(os.environ.get('BUILDDIR'), 'oeqa-post.conf') | ||
114 | ftools.write_file(postconfig_file, postconfig) | ||
115 | extra_args = "-R %s" % postconfig_file | ||
116 | else: | ||
117 | extra_args = "" | ||
118 | |||
119 | if isinstance(command, basestring): | ||
120 | cmd = "bitbake " + extra_args + " " + command | ||
121 | else: | ||
122 | cmd = [ "bitbake" ] + [a for a in (command + extra_args.split(" ")) if a not in [""]] | ||
123 | |||
124 | try: | ||
125 | return runCmd(cmd, ignore_status, timeout, **options) | ||
126 | finally: | ||
127 | if postconfig: | ||
128 | os.remove(postconfig_file) | ||
129 | |||
130 | |||
131 | def get_bb_env(target=None, postconfig=None): | ||
132 | if target: | ||
133 | return bitbake("-e %s" % target, postconfig=postconfig).output | ||
134 | else: | ||
135 | return bitbake("-e", postconfig=postconfig).output | ||
136 | |||
137 | def get_bb_var(var, target=None, postconfig=None): | ||
138 | val = None | ||
139 | bbenv = get_bb_env(target, postconfig=postconfig) | ||
140 | for line in bbenv.splitlines(): | ||
141 | if line.startswith(var + "="): | ||
142 | val = line.split('=')[1] | ||
143 | val = val.replace('\"','') | ||
144 | break | ||
145 | return val | ||
146 | |||
147 | def get_test_layer(): | ||
148 | layers = get_bb_var("BBLAYERS").split() | ||
149 | testlayer = None | ||
150 | for l in layers: | ||
151 | if "/meta-selftest" in l and os.path.isdir(l): | ||
152 | testlayer = l | ||
153 | break | ||
154 | return testlayer | ||
diff --git a/meta/lib/oeqa/utils/decorators.py b/meta/lib/oeqa/utils/decorators.py new file mode 100644 index 0000000000..40bd4ef2db --- /dev/null +++ b/meta/lib/oeqa/utils/decorators.py | |||
@@ -0,0 +1,158 @@ | |||
1 | # Copyright (C) 2013 Intel Corporation | ||
2 | # | ||
3 | # Released under the MIT license (see COPYING.MIT) | ||
4 | |||
5 | # Some custom decorators that can be used by unittests | ||
6 | # Most useful is skipUnlessPassed which can be used for | ||
7 | # creating dependecies between two test methods. | ||
8 | |||
9 | import os | ||
10 | import logging | ||
11 | import sys | ||
12 | import unittest | ||
13 | |||
14 | #get the "result" object from one of the upper frames provided that one of these upper frames is a unittest.case frame | ||
15 | class getResults(object): | ||
16 | def __init__(self): | ||
17 | #dynamically determine the unittest.case frame and use it to get the name of the test method | ||
18 | upperf = sys._current_frames().values()[0] | ||
19 | while (upperf.f_globals['__name__'] != 'unittest.case'): | ||
20 | upperf = upperf.f_back | ||
21 | |||
22 | def handleList(items): | ||
23 | ret = [] | ||
24 | # items is a list of tuples, (test, failure) or (_ErrorHandler(), Exception()) | ||
25 | for i in items: | ||
26 | s = i[0].id() | ||
27 | #Handle the _ErrorHolder objects from skipModule failures | ||
28 | if "setUpModule (" in s: | ||
29 | ret.append(s.replace("setUpModule (", "").replace(")","")) | ||
30 | else: | ||
31 | ret.append(s) | ||
32 | return ret | ||
33 | self.faillist = handleList(upperf.f_locals['result'].failures) | ||
34 | self.errorlist = handleList(upperf.f_locals['result'].errors) | ||
35 | self.skiplist = handleList(upperf.f_locals['result'].skipped) | ||
36 | |||
37 | def getFailList(self): | ||
38 | return self.faillist | ||
39 | |||
40 | def getErrorList(self): | ||
41 | return self.errorlist | ||
42 | |||
43 | def getSkipList(self): | ||
44 | return self.skiplist | ||
45 | |||
46 | class skipIfFailure(object): | ||
47 | |||
48 | def __init__(self,testcase): | ||
49 | self.testcase = testcase | ||
50 | |||
51 | def __call__(self,f): | ||
52 | def wrapped_f(*args): | ||
53 | res = getResults() | ||
54 | if self.testcase in (res.getFailList() or res.getErrorList()): | ||
55 | raise unittest.SkipTest("Testcase dependency not met: %s" % self.testcase) | ||
56 | return f(*args) | ||
57 | wrapped_f.__name__ = f.__name__ | ||
58 | return wrapped_f | ||
59 | |||
60 | class skipIfSkipped(object): | ||
61 | |||
62 | def __init__(self,testcase): | ||
63 | self.testcase = testcase | ||
64 | |||
65 | def __call__(self,f): | ||
66 | def wrapped_f(*args): | ||
67 | res = getResults() | ||
68 | if self.testcase in res.getSkipList(): | ||
69 | raise unittest.SkipTest("Testcase dependency not met: %s" % self.testcase) | ||
70 | return f(*args) | ||
71 | wrapped_f.__name__ = f.__name__ | ||
72 | return wrapped_f | ||
73 | |||
74 | class skipUnlessPassed(object): | ||
75 | |||
76 | def __init__(self,testcase): | ||
77 | self.testcase = testcase | ||
78 | |||
79 | def __call__(self,f): | ||
80 | def wrapped_f(*args): | ||
81 | res = getResults() | ||
82 | if self.testcase in res.getSkipList() or \ | ||
83 | self.testcase in res.getFailList() or \ | ||
84 | self.testcase in res.getErrorList(): | ||
85 | raise unittest.SkipTest("Testcase dependency not met: %s" % self.testcase) | ||
86 | return f(*args) | ||
87 | wrapped_f.__name__ = f.__name__ | ||
88 | return wrapped_f | ||
89 | |||
90 | class testcase(object): | ||
91 | |||
92 | def __init__(self, test_case): | ||
93 | self.test_case = test_case | ||
94 | |||
95 | def __call__(self, func): | ||
96 | def wrapped_f(*args): | ||
97 | return func(*args) | ||
98 | wrapped_f.test_case = self.test_case | ||
99 | return wrapped_f | ||
100 | |||
101 | class NoParsingFilter(logging.Filter): | ||
102 | def filter(self, record): | ||
103 | return record.levelno == 100 | ||
104 | |||
105 | def LogResults(original_class): | ||
106 | orig_method = original_class.run | ||
107 | |||
108 | #rewrite the run method of unittest.TestCase to add testcase logging | ||
109 | def run(self, result, *args, **kws): | ||
110 | orig_method(self, result, *args, **kws) | ||
111 | passed = True | ||
112 | testMethod = getattr(self, self._testMethodName) | ||
113 | |||
114 | #if test case is decorated then use it's number, else use it's name | ||
115 | try: | ||
116 | test_case = testMethod.test_case | ||
117 | except AttributeError: | ||
118 | test_case = self._testMethodName | ||
119 | |||
120 | #create custom logging level for filtering. | ||
121 | custom_log_level = 100 | ||
122 | logging.addLevelName(custom_log_level, 'RESULTS') | ||
123 | caller = os.path.basename(sys.argv[0]) | ||
124 | |||
125 | def results(self, message, *args, **kws): | ||
126 | if self.isEnabledFor(custom_log_level): | ||
127 | self.log(custom_log_level, message, *args, **kws) | ||
128 | logging.Logger.results = results | ||
129 | |||
130 | logging.basicConfig(filename=os.path.join(os.getcwd(),'results-'+caller+'.log'), | ||
131 | filemode='w', | ||
132 | format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', | ||
133 | datefmt='%H:%M:%S', | ||
134 | level=custom_log_level) | ||
135 | for handler in logging.root.handlers: | ||
136 | handler.addFilter(NoParsingFilter()) | ||
137 | local_log = logging.getLogger(caller) | ||
138 | |||
139 | #check status of tests and record it | ||
140 | for (name, msg) in result.errors: | ||
141 | if self._testMethodName == str(name).split(' ')[0]: | ||
142 | local_log.results("Testcase "+str(test_case)+": ERROR") | ||
143 | local_log.results("Testcase "+str(test_case)+":\n"+msg) | ||
144 | passed = False | ||
145 | for (name, msg) in result.failures: | ||
146 | if self._testMethodName == str(name).split(' ')[0]: | ||
147 | local_log.results("Testcase "+str(test_case)+": FAILED") | ||
148 | local_log.results("Testcase "+str(test_case)+":\n"+msg) | ||
149 | passed = False | ||
150 | for (name, msg) in result.skipped: | ||
151 | if self._testMethodName == str(name).split(' ')[0]: | ||
152 | local_log.results("Testcase "+str(test_case)+": SKIPPED") | ||
153 | passed = False | ||
154 | if passed: | ||
155 | local_log.results("Testcase "+str(test_case)+": PASSED") | ||
156 | |||
157 | original_class.run = run | ||
158 | return original_class | ||
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/meta/lib/oeqa/utils/httpserver.py b/meta/lib/oeqa/utils/httpserver.py new file mode 100644 index 0000000000..76518d8ef9 --- /dev/null +++ b/meta/lib/oeqa/utils/httpserver.py | |||
@@ -0,0 +1,35 @@ | |||
1 | import SimpleHTTPServer | ||
2 | import multiprocessing | ||
3 | import os | ||
4 | |||
5 | class HTTPServer(SimpleHTTPServer.BaseHTTPServer.HTTPServer): | ||
6 | |||
7 | def server_start(self, root_dir): | ||
8 | import signal | ||
9 | signal.signal(signal.SIGTERM, signal.SIG_DFL) | ||
10 | os.chdir(root_dir) | ||
11 | self.serve_forever() | ||
12 | |||
13 | class HTTPRequestHandler(SimpleHTTPServer.SimpleHTTPRequestHandler): | ||
14 | |||
15 | def log_message(self, format_str, *args): | ||
16 | pass | ||
17 | |||
18 | class HTTPService(object): | ||
19 | |||
20 | def __init__(self, root_dir, host=''): | ||
21 | self.root_dir = root_dir | ||
22 | self.host = host | ||
23 | self.port = 0 | ||
24 | |||
25 | def start(self): | ||
26 | self.server = HTTPServer((self.host, self.port), HTTPRequestHandler) | ||
27 | if self.port == 0: | ||
28 | self.port = self.server.server_port | ||
29 | self.process = multiprocessing.Process(target=self.server.server_start, args=[self.root_dir]) | ||
30 | self.process.start() | ||
31 | |||
32 | def stop(self): | ||
33 | self.server.server_close() | ||
34 | self.process.terminate() | ||
35 | self.process.join() | ||
diff --git a/meta/lib/oeqa/utils/logparser.py b/meta/lib/oeqa/utils/logparser.py new file mode 100644 index 0000000000..87b50354cd --- /dev/null +++ b/meta/lib/oeqa/utils/logparser.py | |||
@@ -0,0 +1,125 @@ | |||
1 | #!/usr/bin/env python | ||
2 | |||
3 | import sys | ||
4 | import os | ||
5 | import re | ||
6 | import ftools | ||
7 | |||
8 | |||
9 | # A parser that can be used to identify weather a line is a test result or a section statement. | ||
10 | class Lparser(object): | ||
11 | |||
12 | def __init__(self, test_0_pass_regex, test_0_fail_regex, section_0_begin_regex=None, section_0_end_regex=None, **kwargs): | ||
13 | # Initialize the arguments dictionary | ||
14 | if kwargs: | ||
15 | self.args = kwargs | ||
16 | else: | ||
17 | self.args = {} | ||
18 | |||
19 | # Add the default args to the dictionary | ||
20 | self.args['test_0_pass_regex'] = test_0_pass_regex | ||
21 | self.args['test_0_fail_regex'] = test_0_fail_regex | ||
22 | if section_0_begin_regex: | ||
23 | self.args['section_0_begin_regex'] = section_0_begin_regex | ||
24 | if section_0_end_regex: | ||
25 | self.args['section_0_end_regex'] = section_0_end_regex | ||
26 | |||
27 | self.test_possible_status = ['pass', 'fail', 'error'] | ||
28 | self.section_possible_status = ['begin', 'end'] | ||
29 | |||
30 | self.initialized = False | ||
31 | |||
32 | |||
33 | # Initialize the parser with the current configuration | ||
34 | def init(self): | ||
35 | |||
36 | # extra arguments can be added by the user to define new test and section categories. They must follow a pre-defined pattern: <type>_<category_name>_<status>_regex | ||
37 | self.test_argument_pattern = "^test_(.+?)_(%s)_regex" % '|'.join(map(str, self.test_possible_status)) | ||
38 | self.section_argument_pattern = "^section_(.+?)_(%s)_regex" % '|'.join(map(str, self.section_possible_status)) | ||
39 | |||
40 | # Initialize the test and section regex dictionaries | ||
41 | self.test_regex = {} | ||
42 | self.section_regex ={} | ||
43 | |||
44 | for arg, value in self.args.items(): | ||
45 | if not value: | ||
46 | raise Exception('The value of provided argument %s is %s. Should have a valid value.' % (key, value)) | ||
47 | is_test = re.search(self.test_argument_pattern, arg) | ||
48 | is_section = re.search(self.section_argument_pattern, arg) | ||
49 | if is_test: | ||
50 | if not is_test.group(1) in self.test_regex: | ||
51 | self.test_regex[is_test.group(1)] = {} | ||
52 | self.test_regex[is_test.group(1)][is_test.group(2)] = re.compile(value) | ||
53 | elif is_section: | ||
54 | if not is_section.group(1) in self.section_regex: | ||
55 | self.section_regex[is_section.group(1)] = {} | ||
56 | self.section_regex[is_section.group(1)][is_section.group(2)] = re.compile(value) | ||
57 | else: | ||
58 | # TODO: Make these call a traceback instead of a simple exception.. | ||
59 | raise Exception("The provided argument name does not correspond to any valid type. Please give one of the following types:\nfor tests: %s\nfor sections: %s" % (self.test_argument_pattern, self.section_argument_pattern)) | ||
60 | |||
61 | self.initialized = True | ||
62 | |||
63 | # Parse a line and return a tuple containing the type of result (test/section) and its category, status and name | ||
64 | def parse_line(self, line): | ||
65 | if not self.initialized: | ||
66 | raise Exception("The parser is not initialized..") | ||
67 | |||
68 | for test_category, test_status_list in self.test_regex.items(): | ||
69 | for test_status, status_regex in test_status_list.items(): | ||
70 | test_name = status_regex.search(line) | ||
71 | if test_name: | ||
72 | return ['test', test_category, test_status, test_name.group(1)] | ||
73 | |||
74 | for section_category, section_status_list in self.section_regex.items(): | ||
75 | for section_status, status_regex in section_status_list.items(): | ||
76 | section_name = status_regex.search(line) | ||
77 | if section_name: | ||
78 | return ['section', section_category, section_status, section_name.group(1)] | ||
79 | return None | ||
80 | |||
81 | |||
82 | class Result(object): | ||
83 | |||
84 | def __init__(self): | ||
85 | self.result_dict = {} | ||
86 | |||
87 | def store(self, section, test, status): | ||
88 | if not section in self.result_dict: | ||
89 | self.result_dict[section] = [] | ||
90 | |||
91 | self.result_dict[section].append((test, status)) | ||
92 | |||
93 | # sort tests by the test name(the first element of the tuple), for each section. This can be helpful when using git to diff for changes by making sure they are always in the same order. | ||
94 | def sort_tests(self): | ||
95 | for package in self.result_dict: | ||
96 | sorted_results = sorted(self.result_dict[package], key=lambda tup: tup[0]) | ||
97 | self.result_dict[package] = sorted_results | ||
98 | |||
99 | # Log the results as files. The file name is the section name and the contents are the tests in that section. | ||
100 | def log_as_files(self, target_dir, test_status): | ||
101 | status_regex = re.compile('|'.join(map(str, test_status))) | ||
102 | if not type(test_status) == type([]): | ||
103 | raise Exception("test_status should be a list. Got " + str(test_status) + " instead.") | ||
104 | if not os.path.exists(target_dir): | ||
105 | raise Exception("Target directory does not exist: %s" % target_dir) | ||
106 | |||
107 | for section, test_results in self.result_dict.items(): | ||
108 | prefix = '' | ||
109 | for x in test_status: | ||
110 | prefix +=x+'.' | ||
111 | if (section != ''): | ||
112 | prefix += section | ||
113 | section_file = os.path.join(target_dir, prefix) | ||
114 | # purge the file contents if it exists | ||
115 | open(section_file, 'w').close() | ||
116 | for test_result in test_results: | ||
117 | (test_name, status) = test_result | ||
118 | # we log only the tests with status in the test_status list | ||
119 | match_status = status_regex.search(status) | ||
120 | if match_status: | ||
121 | ftools.append_file(section_file, status + ": " + test_name) | ||
122 | |||
123 | # Not yet implemented! | ||
124 | def log_to_lava(self): | ||
125 | pass | ||
diff --git a/meta/lib/oeqa/utils/qemurunner.py b/meta/lib/oeqa/utils/qemurunner.py new file mode 100644 index 0000000000..f1a7e24ab7 --- /dev/null +++ b/meta/lib/oeqa/utils/qemurunner.py | |||
@@ -0,0 +1,237 @@ | |||
1 | # Copyright (C) 2013 Intel Corporation | ||
2 | # | ||
3 | # Released under the MIT license (see COPYING.MIT) | ||
4 | |||
5 | # This module provides a class for starting qemu images using runqemu. | ||
6 | # It's used by testimage.bbclass. | ||
7 | |||
8 | import subprocess | ||
9 | import os | ||
10 | import time | ||
11 | import signal | ||
12 | import re | ||
13 | import socket | ||
14 | import select | ||
15 | import bb | ||
16 | |||
17 | class QemuRunner: | ||
18 | |||
19 | def __init__(self, machine, rootfs, display, tmpdir, deploy_dir_image, logfile, boottime): | ||
20 | |||
21 | # Popen object for runqemu | ||
22 | self.runqemu = None | ||
23 | # pid of the qemu process that runqemu will start | ||
24 | self.qemupid = None | ||
25 | # target ip - from the command line | ||
26 | self.ip = None | ||
27 | # host ip - where qemu is running | ||
28 | self.server_ip = None | ||
29 | |||
30 | self.machine = machine | ||
31 | self.rootfs = rootfs | ||
32 | self.display = display | ||
33 | self.tmpdir = tmpdir | ||
34 | self.deploy_dir_image = deploy_dir_image | ||
35 | self.logfile = logfile | ||
36 | self.boottime = boottime | ||
37 | |||
38 | self.runqemutime = 60 | ||
39 | |||
40 | self.create_socket() | ||
41 | |||
42 | |||
43 | def create_socket(self): | ||
44 | |||
45 | self.bootlog = '' | ||
46 | self.qemusock = None | ||
47 | |||
48 | try: | ||
49 | self.server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) | ||
50 | self.server_socket.setblocking(0) | ||
51 | self.server_socket.bind(("127.0.0.1",0)) | ||
52 | self.server_socket.listen(2) | ||
53 | self.serverport = self.server_socket.getsockname()[1] | ||
54 | bb.note("Created listening socket for qemu serial console on: 127.0.0.1:%s" % self.serverport) | ||
55 | except socket.error, msg: | ||
56 | self.server_socket.close() | ||
57 | bb.fatal("Failed to create listening socket: %s" %msg[1]) | ||
58 | |||
59 | |||
60 | def log(self, msg): | ||
61 | if self.logfile: | ||
62 | with open(self.logfile, "a") as f: | ||
63 | f.write("%s" % msg) | ||
64 | |||
65 | def start(self, qemuparams = None): | ||
66 | |||
67 | if self.display: | ||
68 | os.environ["DISPLAY"] = self.display | ||
69 | else: | ||
70 | bb.error("To start qemu I need a X desktop, please set DISPLAY correctly (e.g. DISPLAY=:1)") | ||
71 | return False | ||
72 | if not os.path.exists(self.rootfs): | ||
73 | bb.error("Invalid rootfs %s" % self.rootfs) | ||
74 | return False | ||
75 | if not os.path.exists(self.tmpdir): | ||
76 | bb.error("Invalid TMPDIR path %s" % self.tmpdir) | ||
77 | return False | ||
78 | else: | ||
79 | os.environ["OE_TMPDIR"] = self.tmpdir | ||
80 | if not os.path.exists(self.deploy_dir_image): | ||
81 | bb.error("Invalid DEPLOY_DIR_IMAGE path %s" % self.deploy_dir_image) | ||
82 | return False | ||
83 | else: | ||
84 | os.environ["DEPLOY_DIR_IMAGE"] = self.deploy_dir_image | ||
85 | |||
86 | # Set this flag so that Qemu doesn't do any grabs as SDL grabs interact | ||
87 | # badly with screensavers. | ||
88 | os.environ["QEMU_DONT_GRAB"] = "1" | ||
89 | self.qemuparams = 'bootparams="console=tty1 console=ttyS0,115200n8" qemuparams="-serial tcp:127.0.0.1:%s"' % self.serverport | ||
90 | if qemuparams: | ||
91 | self.qemuparams = self.qemuparams[:-1] + " " + qemuparams + " " + '\"' | ||
92 | |||
93 | launch_cmd = 'runqemu %s %s %s' % (self.machine, self.rootfs, self.qemuparams) | ||
94 | self.runqemu = subprocess.Popen(launch_cmd,shell=True,stdout=subprocess.PIPE,stderr=subprocess.STDOUT,preexec_fn=os.setpgrp) | ||
95 | |||
96 | bb.note("runqemu started, pid is %s" % self.runqemu.pid) | ||
97 | bb.note("waiting at most %s seconds for qemu pid" % self.runqemutime) | ||
98 | endtime = time.time() + self.runqemutime | ||
99 | while not self.is_alive() and time.time() < endtime: | ||
100 | time.sleep(1) | ||
101 | |||
102 | if self.is_alive(): | ||
103 | bb.note("qemu started - qemu procces pid is %s" % self.qemupid) | ||
104 | cmdline = '' | ||
105 | with open('/proc/%s/cmdline' % self.qemupid) as p: | ||
106 | cmdline = p.read() | ||
107 | ips = re.findall("((?:[0-9]{1,3}\.){3}[0-9]{1,3})", cmdline.split("ip=")[1]) | ||
108 | if not ips or len(ips) != 3: | ||
109 | bb.note("Couldn't get ip from qemu process arguments! Here is the qemu command line used: %s" % cmdline) | ||
110 | self.stop() | ||
111 | return False | ||
112 | else: | ||
113 | self.ip = ips[0] | ||
114 | self.server_ip = ips[1] | ||
115 | bb.note("Target IP: %s" % self.ip) | ||
116 | bb.note("Server IP: %s" % self.server_ip) | ||
117 | bb.note("Waiting at most %d seconds for login banner" % self.boottime ) | ||
118 | endtime = time.time() + self.boottime | ||
119 | socklist = [self.server_socket] | ||
120 | reachedlogin = False | ||
121 | stopread = False | ||
122 | while time.time() < endtime and not stopread: | ||
123 | sread, swrite, serror = select.select(socklist, [], [], 5) | ||
124 | for sock in sread: | ||
125 | if sock is self.server_socket: | ||
126 | self.qemusock, addr = self.server_socket.accept() | ||
127 | self.qemusock.setblocking(0) | ||
128 | socklist.append(self.qemusock) | ||
129 | socklist.remove(self.server_socket) | ||
130 | bb.note("Connection from %s:%s" % addr) | ||
131 | else: | ||
132 | data = sock.recv(1024) | ||
133 | if data: | ||
134 | self.log(data) | ||
135 | self.bootlog += data | ||
136 | if re.search("qemu.* login:", self.bootlog): | ||
137 | stopread = True | ||
138 | reachedlogin = True | ||
139 | bb.note("Reached login banner") | ||
140 | else: | ||
141 | socklist.remove(sock) | ||
142 | sock.close() | ||
143 | stopread = True | ||
144 | |||
145 | if not reachedlogin: | ||
146 | bb.note("Target didn't reached login boot in %d seconds" % self.boottime) | ||
147 | lines = "\n".join(self.bootlog.splitlines()[-5:]) | ||
148 | bb.note("Last 5 lines of text:\n%s" % lines) | ||
149 | bb.note("Check full boot log: %s" % self.logfile) | ||
150 | self.stop() | ||
151 | return False | ||
152 | else: | ||
153 | bb.note("Qemu pid didn't appeared in %s seconds" % self.runqemutime) | ||
154 | output = self.runqemu.stdout | ||
155 | self.stop() | ||
156 | bb.note("Output from runqemu:\n%s" % output.read()) | ||
157 | return False | ||
158 | |||
159 | return self.is_alive() | ||
160 | |||
161 | def stop(self): | ||
162 | |||
163 | if self.runqemu: | ||
164 | bb.note("Sending SIGTERM to runqemu") | ||
165 | os.killpg(self.runqemu.pid, signal.SIGTERM) | ||
166 | endtime = time.time() + self.runqemutime | ||
167 | while self.runqemu.poll() is None and time.time() < endtime: | ||
168 | time.sleep(1) | ||
169 | if self.runqemu.poll() is None: | ||
170 | bb.note("Sending SIGKILL to runqemu") | ||
171 | os.killpg(self.runqemu.pid, signal.SIGKILL) | ||
172 | self.runqemu = None | ||
173 | if self.server_socket: | ||
174 | self.server_socket.close() | ||
175 | self.server_socket = None | ||
176 | self.qemupid = None | ||
177 | self.ip = None | ||
178 | |||
179 | def restart(self, qemuparams = None): | ||
180 | bb.note("Restarting qemu process") | ||
181 | if self.runqemu.poll() is None: | ||
182 | self.stop() | ||
183 | self.create_socket() | ||
184 | if self.start(qemuparams): | ||
185 | return True | ||
186 | return False | ||
187 | |||
188 | def is_alive(self): | ||
189 | qemu_child = self.find_child(str(self.runqemu.pid)) | ||
190 | if qemu_child: | ||
191 | self.qemupid = qemu_child[0] | ||
192 | if os.path.exists("/proc/" + str(self.qemupid)): | ||
193 | return True | ||
194 | return False | ||
195 | |||
196 | def find_child(self,parent_pid): | ||
197 | # | ||
198 | # Walk the process tree from the process specified looking for a qemu-system. Return its [pid'cmd] | ||
199 | # | ||
200 | ps = subprocess.Popen(['ps', 'axww', '-o', 'pid,ppid,command'], stdout=subprocess.PIPE).communicate()[0] | ||
201 | processes = ps.split('\n') | ||
202 | nfields = len(processes[0].split()) - 1 | ||
203 | pids = {} | ||
204 | commands = {} | ||
205 | for row in processes[1:]: | ||
206 | data = row.split(None, nfields) | ||
207 | if len(data) != 3: | ||
208 | continue | ||
209 | if data[1] not in pids: | ||
210 | pids[data[1]] = [] | ||
211 | |||
212 | pids[data[1]].append(data[0]) | ||
213 | commands[data[0]] = data[2] | ||
214 | |||
215 | if parent_pid not in pids: | ||
216 | return [] | ||
217 | |||
218 | parents = [] | ||
219 | newparents = pids[parent_pid] | ||
220 | while newparents: | ||
221 | next = [] | ||
222 | for p in newparents: | ||
223 | if p in pids: | ||
224 | for n in pids[p]: | ||
225 | if n not in parents and n not in next: | ||
226 | next.append(n) | ||
227 | if p not in parents: | ||
228 | parents.append(p) | ||
229 | newparents = next | ||
230 | #print "Children matching %s:" % str(parents) | ||
231 | for p in parents: | ||
232 | # Need to be careful here since runqemu-internal runs "ldd qemu-system-xxxx" | ||
233 | # Also, old versions of ldd (2.11) run "LD_XXXX qemu-system-xxxx" | ||
234 | basecmd = commands[p].split()[0] | ||
235 | basecmd = os.path.basename(basecmd) | ||
236 | if "qemu-system" in basecmd and "-serial tcp" in commands[p]: | ||
237 | return [int(p),commands[p]] | ||
diff --git a/meta/lib/oeqa/utils/sshcontrol.py b/meta/lib/oeqa/utils/sshcontrol.py new file mode 100644 index 0000000000..1c81795a87 --- /dev/null +++ b/meta/lib/oeqa/utils/sshcontrol.py | |||
@@ -0,0 +1,138 @@ | |||
1 | # Copyright (C) 2013 Intel Corporation | ||
2 | # | ||
3 | # Released under the MIT license (see COPYING.MIT) | ||
4 | |||
5 | # Provides a class for setting up ssh connections, | ||
6 | # running commands and copying files to/from a target. | ||
7 | # It's used by testimage.bbclass and tests in lib/oeqa/runtime. | ||
8 | |||
9 | import subprocess | ||
10 | import time | ||
11 | import os | ||
12 | import select | ||
13 | |||
14 | |||
15 | class SSHProcess(object): | ||
16 | def __init__(self, **options): | ||
17 | |||
18 | self.defaultopts = { | ||
19 | "stdout": subprocess.PIPE, | ||
20 | "stderr": subprocess.STDOUT, | ||
21 | "stdin": None, | ||
22 | "shell": False, | ||
23 | "bufsize": -1, | ||
24 | "preexec_fn": os.setsid, | ||
25 | } | ||
26 | self.options = dict(self.defaultopts) | ||
27 | self.options.update(options) | ||
28 | self.status = None | ||
29 | self.output = None | ||
30 | self.process = None | ||
31 | self.starttime = None | ||
32 | self.logfile = None | ||
33 | |||
34 | def log(self, msg): | ||
35 | if self.logfile: | ||
36 | with open(self.logfile, "a") as f: | ||
37 | f.write("%s" % msg) | ||
38 | |||
39 | def run(self, command, timeout=None, logfile=None): | ||
40 | self.logfile = logfile | ||
41 | self.starttime = time.time() | ||
42 | output = '' | ||
43 | self.process = subprocess.Popen(command, **self.options) | ||
44 | if timeout: | ||
45 | endtime = self.starttime + timeout | ||
46 | eof = False | ||
47 | while time.time() < endtime and not eof: | ||
48 | if select.select([self.process.stdout], [], [], 5)[0] != []: | ||
49 | data = os.read(self.process.stdout.fileno(), 1024) | ||
50 | if not data: | ||
51 | self.process.stdout.close() | ||
52 | eof = True | ||
53 | else: | ||
54 | output += data | ||
55 | self.log(data) | ||
56 | endtime = time.time() + timeout | ||
57 | |||
58 | |||
59 | # process hasn't returned yet | ||
60 | if not eof: | ||
61 | self.process.terminate() | ||
62 | time.sleep(5) | ||
63 | try: | ||
64 | self.process.kill() | ||
65 | except OSError: | ||
66 | pass | ||
67 | lastline = "\nProcess killed - no output for %d seconds. Total running time: %d seconds." % (timeout, time.time() - self.starttime) | ||
68 | self.log(lastline) | ||
69 | output += lastline | ||
70 | else: | ||
71 | output = self.process.communicate()[0] | ||
72 | self.log(output.rstrip()) | ||
73 | |||
74 | self.status = self.process.wait() | ||
75 | self.output = output.rstrip() | ||
76 | return (self.status, self.output) | ||
77 | |||
78 | |||
79 | class SSHControl(object): | ||
80 | def __init__(self, ip, logfile=None, timeout=300, user='root', port=None): | ||
81 | self.ip = ip | ||
82 | self.defaulttimeout = timeout | ||
83 | self.ignore_status = True | ||
84 | self.logfile = logfile | ||
85 | self.user = user | ||
86 | self.ssh_options = [ | ||
87 | '-o', 'UserKnownHostsFile=/dev/null', | ||
88 | '-o', 'StrictHostKeyChecking=no', | ||
89 | '-o', 'LogLevel=ERROR' | ||
90 | ] | ||
91 | self.ssh = ['ssh', '-l', self.user ] + self.ssh_options | ||
92 | self.scp = ['scp'] + self.ssh_options | ||
93 | if port: | ||
94 | self.ssh = self.ssh + [ '-p', port ] | ||
95 | self.scp = self.scp + [ '-P', port ] | ||
96 | |||
97 | def log(self, msg): | ||
98 | if self.logfile: | ||
99 | with open(self.logfile, "a") as f: | ||
100 | f.write("%s\n" % msg) | ||
101 | |||
102 | def _internal_run(self, command, timeout=None, ignore_status = True): | ||
103 | self.log("[Running]$ %s" % " ".join(command)) | ||
104 | |||
105 | proc = SSHProcess() | ||
106 | status, output = proc.run(command, timeout, logfile=self.logfile) | ||
107 | |||
108 | self.log("[Command returned '%d' after %.2f seconds]" % (status, time.time() - proc.starttime)) | ||
109 | |||
110 | if status and not ignore_status: | ||
111 | raise AssertionError("Command '%s' returned non-zero exit status %d:\n%s" % (command, status, output)) | ||
112 | |||
113 | return (status, output) | ||
114 | |||
115 | def run(self, command, timeout=None): | ||
116 | """ | ||
117 | command - ssh command to run | ||
118 | timeout=<val> - kill command if there is no output after <val> seconds | ||
119 | timeout=None - kill command if there is no output after a default value seconds | ||
120 | timeout=0 - no timeout, let command run until it returns | ||
121 | """ | ||
122 | |||
123 | # We need to source /etc/profile for a proper PATH on the target | ||
124 | command = self.ssh + [self.ip, ' . /etc/profile; ' + command] | ||
125 | |||
126 | if timeout is None: | ||
127 | return self._internal_run(command, self.defaulttimeout, self.ignore_status) | ||
128 | if timeout == 0: | ||
129 | return self._internal_run(command, None, self.ignore_status) | ||
130 | return self._internal_run(command, timeout, self.ignore_status) | ||
131 | |||
132 | def copy_to(self, localpath, remotepath): | ||
133 | command = self.scp + [localpath, '%s@%s:%s' % (self.user, self.ip, remotepath)] | ||
134 | return self._internal_run(command, ignore_status=False) | ||
135 | |||
136 | def copy_from(self, remotepath, localpath): | ||
137 | command = self.scp + ['%s@%s:%s' % (self.user, self.ip, remotepath), localpath] | ||
138 | return self._internal_run(command, ignore_status=False) | ||
diff --git a/meta/lib/oeqa/utils/targetbuild.py b/meta/lib/oeqa/utils/targetbuild.py new file mode 100644 index 0000000000..eeb08ba716 --- /dev/null +++ b/meta/lib/oeqa/utils/targetbuild.py | |||
@@ -0,0 +1,132 @@ | |||
1 | # Copyright (C) 2013 Intel Corporation | ||
2 | # | ||
3 | # Released under the MIT license (see COPYING.MIT) | ||
4 | |||
5 | # Provides a class for automating build tests for projects | ||
6 | |||
7 | import os | ||
8 | import re | ||
9 | import bb.utils | ||
10 | import subprocess | ||
11 | from abc import ABCMeta, abstractmethod | ||
12 | |||
13 | class BuildProject(): | ||
14 | |||
15 | __metaclass__ = ABCMeta | ||
16 | |||
17 | def __init__(self, d, uri, foldername=None, tmpdir="/tmp/"): | ||
18 | self.d = d | ||
19 | self.uri = uri | ||
20 | self.archive = os.path.basename(uri) | ||
21 | self.localarchive = os.path.join(tmpdir,self.archive) | ||
22 | self.fname = re.sub(r'.tar.bz2|tar.gz$', '', self.archive) | ||
23 | if foldername: | ||
24 | self.fname = foldername | ||
25 | |||
26 | # Download self.archive to self.localarchive | ||
27 | def _download_archive(self): | ||
28 | |||
29 | exportvars = ['HTTP_PROXY', 'http_proxy', | ||
30 | 'HTTPS_PROXY', 'https_proxy', | ||
31 | 'FTP_PROXY', 'ftp_proxy', | ||
32 | 'FTPS_PROXY', 'ftps_proxy', | ||
33 | 'NO_PROXY', 'no_proxy', | ||
34 | 'ALL_PROXY', 'all_proxy', | ||
35 | 'SOCKS5_USER', 'SOCKS5_PASSWD'] | ||
36 | |||
37 | cmd = '' | ||
38 | for var in exportvars: | ||
39 | val = self.d.getVar(var, True) | ||
40 | if val: | ||
41 | cmd = 'export ' + var + '=\"%s\"; %s' % (val, cmd) | ||
42 | |||
43 | cmd = cmd + "wget -O %s %s" % (self.localarchive, self.uri) | ||
44 | subprocess.check_call(cmd, shell=True) | ||
45 | |||
46 | # This method should provide a way to run a command in the desired environment. | ||
47 | @abstractmethod | ||
48 | def _run(self, cmd): | ||
49 | pass | ||
50 | |||
51 | # The timeout parameter of target.run is set to 0 to make the ssh command | ||
52 | # run with no timeout. | ||
53 | def run_configure(self, configure_args=''): | ||
54 | return self._run('cd %s; ./configure %s' % (self.targetdir, configure_args)) | ||
55 | |||
56 | def run_make(self, make_args=''): | ||
57 | return self._run('cd %s; make %s' % (self.targetdir, make_args)) | ||
58 | |||
59 | def run_install(self, install_args=''): | ||
60 | return self._run('cd %s; make install %s' % (self.targetdir, install_args)) | ||
61 | |||
62 | def clean(self): | ||
63 | self._run('rm -rf %s' % self.targetdir) | ||
64 | subprocess.call('rm -f %s' % self.localarchive, shell=True) | ||
65 | pass | ||
66 | |||
67 | class TargetBuildProject(BuildProject): | ||
68 | |||
69 | def __init__(self, target, d, uri, foldername=None): | ||
70 | self.target = target | ||
71 | self.targetdir = "~/" | ||
72 | BuildProject.__init__(self, d, uri, foldername, tmpdir="/tmp") | ||
73 | |||
74 | def download_archive(self): | ||
75 | |||
76 | self._download_archive() | ||
77 | |||
78 | (status, output) = self.target.copy_to(self.localarchive, self.targetdir) | ||
79 | if status != 0: | ||
80 | raise Exception("Failed to copy archive to target, output: %s" % output) | ||
81 | |||
82 | (status, output) = self.target.run('tar xf %s%s -C %s' % (self.targetdir, self.archive, self.targetdir)) | ||
83 | if status != 0: | ||
84 | raise Exception("Failed to extract archive, output: %s" % output) | ||
85 | |||
86 | #Change targetdir to project folder | ||
87 | self.targetdir = self.targetdir + self.fname | ||
88 | |||
89 | # The timeout parameter of target.run is set to 0 to make the ssh command | ||
90 | # run with no timeout. | ||
91 | def _run(self, cmd): | ||
92 | return self.target.run(cmd, 0)[0] | ||
93 | |||
94 | |||
95 | class SDKBuildProject(BuildProject): | ||
96 | |||
97 | def __init__(self, testpath, sdkenv, d, uri, foldername=None): | ||
98 | self.sdkenv = sdkenv | ||
99 | self.testdir = testpath | ||
100 | self.targetdir = testpath | ||
101 | bb.utils.mkdirhier(testpath) | ||
102 | self.datetime = d.getVar('DATETIME', True) | ||
103 | self.testlogdir = d.getVar("TEST_LOG_DIR", True) | ||
104 | bb.utils.mkdirhier(self.testlogdir) | ||
105 | self.logfile = os.path.join(self.testlogdir, "sdk_target_log.%s" % self.datetime) | ||
106 | BuildProject.__init__(self, d, uri, foldername, tmpdir=testpath) | ||
107 | |||
108 | def download_archive(self): | ||
109 | |||
110 | self._download_archive() | ||
111 | |||
112 | cmd = 'tar xf %s%s -C %s' % (self.targetdir, self.archive, self.targetdir) | ||
113 | subprocess.check_call(cmd, shell=True) | ||
114 | |||
115 | #Change targetdir to project folder | ||
116 | self.targetdir = self.targetdir + self.fname | ||
117 | |||
118 | def run_configure(self, configure_args=''): | ||
119 | return super(SDKBuildProject, self).run_configure(configure_args=(configure_args or '$CONFIGURE_FLAGS')) | ||
120 | |||
121 | def run_install(self, install_args=''): | ||
122 | return super(SDKBuildProject, self).run_install(install_args=(install_args or "DESTDIR=%s/../install" % self.targetdir)) | ||
123 | |||
124 | def log(self, msg): | ||
125 | if self.logfile: | ||
126 | with open(self.logfile, "a") as f: | ||
127 | f.write("%s\n" % msg) | ||
128 | |||
129 | def _run(self, cmd): | ||
130 | self.log("Running source %s; " % self.sdkenv + cmd) | ||
131 | return subprocess.call("source %s; " % self.sdkenv + cmd, shell=True) | ||
132 | |||