source: rtems-tools/tester/rt/config.py @ e6bf128

Last change on this file since e6bf128 was e6bf128, checked in by Chris Johns <chrisj@…>, on Feb 5, 2019 at 5:07:24 AM

rtemstoolkit/config: Unlock when printing to avoid blocking.

  • Property mode set to 100644
File size: 18.7 KB
Line 
1#
2# RTEMS Tools Project (http://www.rtems.org/)
3# Copyright 2013-2017 Chris Johns (chrisj@rtems.org)
4# All rights reserved.
5#
6# This file is part of the RTEMS Tools package in 'rtems-tools'.
7#
8# Redistribution and use in source and binary forms, with or without
9# modification, are permitted provided that the following conditions are met:
10#
11# 1. Redistributions of source code must retain the above copyright notice,
12# this list of conditions and the following disclaimer.
13#
14# 2. Redistributions in binary form must reproduce the above copyright notice,
15# this list of conditions and the following disclaimer in the documentation
16# and/or other materials provided with the distribution.
17#
18# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
19# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
20# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
21# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
22# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
23# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
24# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
25# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
26# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
27# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
28# POSSIBILITY OF SUCH DAMAGE.
29#
30
31#
32# RTEMS Testing Config
33#
34
35from __future__ import print_function
36
37import datetime
38import os
39import re
40import threading
41
42from rtemstoolkit import configuration
43from rtemstoolkit import config
44from rtemstoolkit import error
45from rtemstoolkit import execute
46from rtemstoolkit import log
47from rtemstoolkit import path
48
49from rtemstoolkit import stacktraces
50
51import console
52import gdb
53import tftp
54
55timeout = 15
56
57class file(config.file):
58    """RTEMS Testing configuration."""
59
60    _directives = ['%execute',
61                   '%gdb',
62                   '%tftp',
63                   '%console']
64
65    def __init__(self, index, total, report, name, opts,
66                 console_prefix = ']', _directives = _directives):
67        super(file, self).__init__(name, opts, directives = _directives)
68        self.lock = threading.Lock()
69        self.realtime_trace = self.exe_trace('output')
70        self.console_trace = self.exe_trace('console')
71        self.console_prefix = console_prefix
72        self.show_header = not self.defined('test_disable_header')
73        self.process = None
74        self.console = None
75        self.output = None
76        self.index = index
77        self.total = total
78        self.report = report
79        self.name = name
80        self.timedout = False
81        self.test_started = False
82        self.kill_good = False
83        self.kill_on_end = False
84        self.test_label = None
85        self.target_reset_regx = None
86        self.target_start_regx = None
87
88    def __del__(self):
89        if self.console:
90            del self.console
91        super(file, self).__del__()
92
93    def _lock(self):
94        self.lock.acquire()
95
96    def _unlock(self):
97        self.lock.release()
98
99    def _timeout(self):
100        self._lock()
101        self.timedout = True
102        self._unlock()
103        self.capture('*** TIMEOUT TIMEOUT')
104
105    def _ok_kill(self):
106        self._lock()
107        self.kill_good = True
108        self._unlock()
109        try:
110            self.process.kill()
111        except:
112            pass
113
114    def _target_regex(self, label):
115        regex = None
116        if self.defined(label):
117            r = self.expand('%%{%s}' % (label))
118            try:
119                regex = re.compile(r, re.MULTILINE)
120            except:
121                msg = 'invalid target regex: %s: %s' % (label, r)
122                raise error.general(msg)
123        return regex
124
125    def _target_command(self, command, bsp_arch = None, bsp = None, exe = None, fexe = None):
126        if self.defined('target_%s_command' % (command)):
127            cmd = self.expand('%%{target_%s_command}' % (command)).strip()
128            if bsp_arch is not None and '@ARCH@' in cmd:
129                cmd = cmd.replace('@ARCH@', bsp_arch)
130            if bsp is not None and '@BSP@' in cmd:
131                cmd = cmd.replace('@BSP@', bsp)
132            if exe is not None and '@EXE@' in cmd:
133                cmd = cmd.replace('@EXE@', exe)
134            if fexe is not None and '@FEXE@' in cmd:
135                cmd = cmd.replace('@FEXE@', exe)
136            if len(cmd) > 0:
137                output = ''
138                if not self.opts.dry_run():
139                    rs_proc = execute.capture_execution()
140                    ec, proc, output = rs_proc.open(cmd, shell = True)
141                self._capture_console('target %s: %s' % (command, cmd))
142                if len(output) > 0:
143                    output = os.linesep.join([' ' + l for l in output.splitlines()])
144                    self._capture_console(output)
145
146    def _target_exe_filter(self, exe):
147        if self.defined('target_exe_filter'):
148            f = self.expand('%{target_exe_filter}').strip()
149            # Be like sed and use the first character as the delmiter.
150            if len(f) > 0:
151                delimiter = f[0]
152                pat = ''
153                repl = ''
154                repl_not_pat = False
155                esc = False
156                for c in f[1:]:
157                    add = True
158                    if not esc and c == '\\':
159                        esc = True
160                        add = False
161                    elif esc:
162                        if c == delimiter:
163                            c = delimiter
164                        else:
165                            c = '\\' + c
166                            esc = False
167                    elif c == delimiter:
168                        if repl_not_pat:
169                            exe = re.sub(r'%s' % (pat), repl, exe)
170                            self._capture_console('target exe filter: find:%s subst:%s -> %s' % \
171                                                  (pat, repl, exe))
172                            return exe
173                        repl_not_pat = True
174                        add = False
175                    if add:
176                        if repl_not_pat:
177                            repl += c
178                        else:
179                            pat += c
180                raise error.general('invalid exe filter: %s' % (f))
181
182    def _output_length(self):
183        self._lock()
184        if self.test_started:
185            l = len(self.output)
186            self._unlock()
187            return l
188        self._unlock()
189        return 0
190
191    def _capture_console(self, text):
192        text = [('=> ', l) for l in text.replace(chr(13), '').splitlines()]
193        if self.output is not None:
194            if self.console_trace:
195                self._realtime_trace(text)
196            self.output += text
197
198    def _dir_console(self, data):
199        if self.console is not None:
200            raise error.general(self._name_line_msg('console already configured'))
201        if len(data) == 0:
202            raise error.general(self._name_line_msg('no console configuration provided'))
203        console_trace = trace = self.exe_trace('console')
204        if not self.opts.dry_run():
205            if data[0] == 'stdio':
206                self.console = console.stdio(trace = console_trace)
207            elif data[0] == 'tty':
208                if len(data) < 2 or len(data) >3:
209                    raise error.general(self._name_line_msg('no tty configuration provided'))
210                if len(data) == 3:
211                    settings = data[2]
212                else:
213                    settings = None
214                self.console = console.tty(data[1],
215                                           output = self.capture,
216                                           setup = settings,
217                                           trace = console_trace)
218            else:
219                raise error.general(self._name_line_msg('invalid console type'))
220
221    def _dir_execute(self, data, total, index, exe, bsp_arch, bsp):
222        self.process = execute.execute(output = self.capture)
223        if self.console:
224            self.console.open()
225        if not self.in_error:
226            self.capture_console('run: %s' % (' '.join(data)))
227            if not self.opts.dry_run():
228                ec, proc = self.process.open(data,
229                                             timeout = (int(self.expand('%{timeout}')),
230                                                        self._timeout))
231                self._lock()
232                if not self.kill_good and ec > 0:
233                    self._error('execute failed: %s: exit-code:%d' % (' '.join(data), ec))
234                elif self.timedout:
235                    self.process.kill()
236                self._unlock()
237            if self.console:
238                self.console.close()
239
240    def _dir_gdb(self, data, total, index, exe, bsp_arch, bsp):
241        if len(data) < 3 or len(data) > 4:
242            raise error.general('invalid %gdb arguments')
243        self.process = gdb.gdb(bsp_arch, bsp,
244                               trace = self.exe_trace('gdb'),
245                               mi_trace = self.exe_trace('gdb-mi'))
246        script = self.expand('%%{%s}' % data[2])
247        if script:
248            script = [l.strip() for l in script.splitlines()]
249        if not self.in_error:
250            if self.console:
251                self.console.open()
252            if not self.opts.dry_run():
253                self.process.open(data[0], data[1],
254                                  script = script,
255                                  output = self.capture,
256                                  gdb_console = self.capture_console,
257                                  timeout = int(self.expand('%{timeout}')))
258            if self.console:
259                self.console.close()
260
261    def _dir_tftp(self, data, total, index, exe, bsp_arch, bsp):
262        if len(data) != 2:
263            raise error.general('invalid %tftp arguments')
264        try:
265            port = int(data[1])
266        except:
267            raise error.general('invalid %tftp port')
268        self.kill_on_end = True
269        if not self.opts.dry_run():
270            self.process = tftp.tftp(bsp_arch, bsp,
271                                     trace = self.exe_trace('tftp'))
272            if not self.in_error:
273                if self.console:
274                    self.console.open()
275                self.process.open(executable = exe,
276                                  port = port,
277                                  output_length = self._output_length,
278                                  console = self.capture_console,
279                                  timeout = (int(self.expand('%{timeout}')),
280                                             self._timeout))
281                if self.console:
282                    self.console.close()
283
284    def _directive_filter(self, results, directive, info, data):
285        if results[0] == 'directive':
286            _directive = results[1]
287            _data = results[2]
288            ds = []
289            if len(_data):
290                ds = [_data[0]]
291                if len(_data) > 1:
292                    ds += _data[1].split()
293            ds = self.expand(ds)
294
295            if _directive == '%console':
296                self._dir_console(ds)
297            else:
298                self._lock()
299                try:
300                    self.output = []
301                    total = int(self.expand('%{test_total}'))
302                    index = int(self.expand('%{test_index}'))
303                    exe = self.expand('%{test_executable}')
304                    bsp_arch = self.expand('%{arch}')
305                    bsp = self.expand('%{bsp}')
306                    fexe = self._target_exe_filter(exe)
307                    self.report.start(index, total, exe, fexe,
308                                      bsp_arch, bsp, self.show_header)
309                    if self.index == 1:
310                        self._target_command('on', bsp_arch, bsp, exe, fexe)
311                    self._target_command('pretest', bsp_arch, bsp, exe, fexe)
312                finally:
313                    self._unlock()
314                if _directive == '%execute':
315                    self._dir_execute(ds, total, index, fexe, bsp_arch, bsp)
316                elif _directive == '%gdb':
317                    self._dir_gdb(ds, total, index, fexe, bsp_arch, bsp)
318                elif _directive == '%tftp':
319                    self._dir_tftp(ds, total, index, fexe, bsp_arch, bsp)
320                else:
321                    raise error.general(self._name_line_msg('invalid directive'))
322                self._lock()
323                if self.index == self.total:
324                    self._target_command('off', bsp_arch, bsp, exe, fexe)
325                self._target_command('posttest', bsp_arch, bsp, exe, fexe)
326                try:
327                    status = self.report.end(exe, self.output, self.console_prefix)
328                    version = self.report.get_config('version', not_found = 'n/p')
329                    build = self.report.get_config('build', not_found = 'n/p')
330                    tools = self.report.get_config('tools', not_found = 'n/p')
331                    self._capture_console('test result: %s' % (status))
332                    self._capture_console('test version: %s' % (version))
333                    self._capture_console('test build: %s' % (build))
334                    self._capture_console('test tools: %s' % (tools))
335                    if status == 'timeout':
336                        if self.index != self.total:
337                            self._target_command('reset', bsp_arch, bsp, exe, fexe)
338                    self.process = None
339                    self.output = None
340                finally:
341                    self._unlock()
342        return None, None, None
343
344    def _realtime_trace(self, text):
345        for l in text:
346            self._unlock()
347            try:
348                print(''.join(l))
349            except:
350                stacktraces.trace()
351                raise
352            finally:
353                self._lock()
354
355    def run(self):
356        self.target_start_regx = self._target_regex('target_start_regex')
357        self.target_reset_regx = self._target_regex('target_reset_regex')
358        self.load(self.name)
359
360    def capture(self, text):
361        self._lock()
362        try:
363            if not self.test_started:
364                s = text.find('*** BEGIN OF TEST ')
365                if s >= 0:
366                    self.test_started = True
367                    e = text[s + 3:].find('***')
368                    if e >= 0:
369                        self.test_label = text[s + len('*** BEGIN OF TEST '):s + e + 3 - 1]
370                    self._capture_console('test start: %s' % (self.test_label))
371            ok_to_kill = '*** TEST STATE: USER_INPUT' in text or \
372                         '*** TEST STATE: BENCHMARK' in text
373            if ok_to_kill:
374                reset_target = True
375            else:
376                reset_target = False
377            if self.test_started and self.target_start_regx is not None:
378                if self.target_start_regx.match(text):
379                    self._capture_console('target start detected')
380                    ok_to_kill = True
381            if not reset_target and self.target_reset_regx is not None:
382                if self.target_reset_regx.match(text):
383                    self._capture_console('target reset condition detected')
384                    self._target_command('reset')
385            if self.kill_on_end:
386                if not ok_to_kill and '*** END OF TEST ' in text:
387                    self._capture_console('test end: %s' % (self.test_label))
388                    if self.test_label is not None:
389                        ok_to_kill = '*** END OF TEST %s ***' % (self.test_label) in text
390            text = [(self.console_prefix, l) for l in text.replace(chr(13), '').splitlines()]
391            if self.output is not None:
392                if self.realtime_trace:
393                    self._realtime_trace(text)
394                self.output += text
395            if reset_target:
396                if self.index == self.total:
397                    self._target_command('off')
398                else:
399                    self._target_command('reset')
400        finally:
401            self._unlock()
402        if ok_to_kill:
403            self._ok_kill()
404
405    def capture_console(self, text):
406        self._lock()
407        try:
408            self._capture_console(text)
409        finally:
410            self._unlock()
411
412    def exe_trace(self, flag):
413        dt = self.macros['exe_trace']
414        if dt:
415            if flag in dt.split(','):
416                return True
417        return False
418
419    def kill(self):
420        if self.process:
421            try:
422                self.process.kill()
423            except:
424                pass
425
426def load(bsp, opts):
427    mandatory = ['bsp', 'arch', 'tester']
428    cfg = configuration.configuration()
429    path_ = opts.defaults.expand('%%{_configdir}/bsps/%s.ini' % (bsp))
430    ini_name = path.basename(path_)
431    for p in path.dirname(path_).split(':'):
432        if path.exists(path.join(p, ini_name)):
433            cfg.load(path.join(p, ini_name))
434            if not cfg.has_section(bsp):
435                raise error.general('bsp section not found in ini: [%s]' % (bsp))
436            item_names = cfg.get_item_names(bsp, err = False)
437            for m in mandatory:
438                if m not in item_names:
439                    raise error.general('mandatory item not found in bsp section: %s' % (m))
440            opts.defaults.set_write_map(bsp, add = True)
441            for i in cfg.get_items(bsp, flatten = False):
442                opts.defaults[i[0]] = i[1]
443            if not opts.defaults.set_read_map(bsp):
444                raise error.general('cannot set BSP read map: %s' % (bsp))
445            # Get a copy of the required fields we need
446            requires = cfg.comma_list(bsp, 'requires', err = False)
447            del cfg
448            user_config = opts.find_arg('--user-config')
449            if user_config is not None:
450                user_config = path.expanduser(user_config[1])
451                if not path.exists(user_config):
452                    raise error.general('cannot find user configuration file: %s' % (user_config))
453            else:
454                if 'HOME' in os.environ:
455                    user_config = path.join(os.environ['HOME'], '.rtemstesterrc')
456            if user_config:
457                if path.exists(user_config):
458                    cfg = configuration.configuration()
459                    cfg.load(user_config)
460                    if cfg.has_section(bsp):
461                        for i in cfg.get_items(bsp, flatten = False):
462                            opts.defaults[i[0]] = i[1]
463            # Check for the required values.
464            for r in requires:
465                if opts.defaults.get(r) is None:
466                    raise error.general('user value missing, BSP %s requires \'%s\': missing: %s' % \
467                                        (bsp, ', '.join(requires), r))
468            return opts.defaults['bsp']
469    raise error.general('cannot find bsp configuration file: %s.ini' % (bsp))
Note: See TracBrowser for help on using the repository browser.