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

5
Last change on this file since c68beb8 was c68beb8, checked in by Chris Johns <chrisj@…>, on 11/03/17 at 06:57:37

tester: Add the rtems-run command.

  • Property mode set to 100644
File size: 17.9 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 . import console
50from . import gdb
51from . import tftp
52
53timeout = 15
54
55class file(config.file):
56    """RTEMS Testing configuration."""
57
58    _directives = ['%execute',
59                   '%gdb',
60                   '%tftp',
61                   '%console']
62
63    def __init__(self, index, total, report, name, opts,
64                 console_prefix = '] ', _directives = _directives):
65        super(file, self).__init__(name, opts, directives = _directives)
66        self.lock = threading.Lock()
67        self.realtime_trace = self.exe_trace('output')
68        self.console_trace = self.exe_trace('console')
69        self.console_prefix = console_prefix
70        self.process = None
71        self.console = None
72        self.output = None
73        self.index = index
74        self.total = total
75        self.report = report
76        self.name = name
77        self.timedout = False
78        self.test_started = False
79        self.kill_good = False
80        self.kill_on_end = False
81        self.test_label = None
82        self.target_reset_regx = None
83        self.target_start_regx = None
84
85    def __del__(self):
86        if self.console:
87            del self.console
88        super(file, self).__del__()
89
90    def _lock(self):
91        self.lock.acquire()
92
93    def _unlock(self):
94        self.lock.release()
95
96    def _timeout(self):
97        self._lock()
98        self.timedout = True
99        self._unlock()
100        self.capture('*** TIMEOUT TIMEOUT')
101
102    def _ok_kill(self):
103        self._lock()
104        self.kill_good = True
105        self._unlock()
106        try:
107            self.process.kill()
108        except:
109            pass
110
111    def _target_regex(self, label):
112        regex = None
113        if self.defined(label):
114            r = self.expand('%%{%s}' % (label))
115            try:
116                regex = re.compile(r, re.MULTILINE)
117            except:
118                msg = 'invalid target regex: %s: %s' % (label, r)
119                raise error.general(msg)
120        return regex
121
122    def _target_command(self, command, bsp_arch = None, bsp = None, exe = None, fexe = None):
123        if self.defined('target_%s_command' % (command)):
124            cmd = self.expand('%%{target_%s_command}' % (command)).strip()
125            if bsp_arch is not None and '@ARCH@' in cmd:
126                cmd = cmd.replace('@ARCH@', bsp_arch)
127            if bsp is not None and '@BSP@' in cmd:
128                cmd = cmd.replace('@BSP@', bsp)
129            if exe is not None and '@EXE@' in cmd:
130                cmd = cmd.replace('@EXE@', exe)
131            if fexe is not None and '@FEXE@' in cmd:
132                cmd = cmd.replace('@FEXE@', exe)
133            if len(cmd) > 0:
134                output = ''
135                if not self.opts.dry_run():
136                    rs_proc = execute.capture_execution()
137                    ec, proc, output = rs_proc.open(cmd, shell = True)
138                self._capture_console('target %s: %s' % (command, cmd))
139                if len(output) > 0:
140                    output = os.linesep.join([' ' + l for l in output.splitlines()])
141                    self._capture_console(output)
142
143    def _target_exe_filter(self, exe):
144        if self.defined('target_exe_filter'):
145            f = self.expand('%{target_exe_filter}').strip()
146            # Be like sed and use the first character as the delmiter.
147            if len(f) > 0:
148                delimiter = f[0]
149                pat = ''
150                repl = ''
151                repl_not_pat = False
152                esc = False
153                for c in f[1:]:
154                    add = True
155                    if not esc and c == '\\':
156                        esc = True
157                        add = False
158                    elif esc:
159                        if c == delimiter:
160                            c = delimiter
161                        else:
162                            c = '\\' + c
163                            esc = False
164                    elif c == delimiter:
165                        if repl_not_pat:
166                            exe = re.sub(r'%s' % (pat), repl, exe)
167                            self._capture_console('target exe filter: find:%s subst:%s -> %s' % \
168                                                  (pat, repl, exe))
169                            return exe
170                        repl_not_pat = True
171                        add = False
172                    if add:
173                        if repl_not_pat:
174                            repl += c
175                        else:
176                            pat += c
177                raise error.general('invalid exe filter: %s' % (f))
178
179    def _output_length(self):
180        self._lock()
181        if self.test_started:
182            l = len(self.output)
183            self._unlock()
184            return l
185        self._unlock()
186        return 0
187
188    def _capture_console(self, text):
189        text = [('=> ', l) for l in text.replace(chr(13), '').splitlines()]
190        if self.output is not None:
191            if self.console_trace:
192                self._realtime_trace(text)
193            self.output += text
194
195    def _dir_console(self, data):
196        if self.console is not None:
197            raise error.general(self._name_line_msg('console already configured'))
198        if len(data) == 0:
199            raise error.general(self._name_line_msg('no console configuration provided'))
200        console_trace = trace = self.exe_trace('console')
201        if not self.opts.dry_run():
202            if data[0] == 'stdio':
203                self.console = console.stdio(trace = console_trace)
204            elif data[0] == 'tty':
205                if len(data) < 2 or len(data) >3:
206                    raise error.general(self._name_line_msg('no tty configuration provided'))
207                if len(data) == 3:
208                    settings = data[2]
209                else:
210                    settings = None
211                self.console = console.tty(data[1],
212                                           output = self.capture,
213                                           setup = settings,
214                                           trace = console_trace)
215            else:
216                raise error.general(self._name_line_msg('invalid console type'))
217
218    def _dir_execute(self, data, total, index, exe, bsp_arch, bsp):
219        self.process = execute.execute(output = self.capture)
220        if self.console:
221            self.console.open()
222        if not self.in_error:
223            self.capture_console('run: %s' % (' '.join(data)))
224            if not self.opts.dry_run():
225                ec, proc = self.process.open(data,
226                                             timeout = (int(self.expand('%{timeout}')),
227                                                        self._timeout))
228                self._lock()
229                if not self.kill_good and ec > 0:
230                    self._error('execute failed: %s: exit-code:%d' % (' '.join(data), ec))
231                elif self.timedout:
232                    self.process.kill()
233                self._unlock()
234            if self.console:
235                self.console.close()
236
237    def _dir_gdb(self, data, total, index, exe, bsp_arch, bsp):
238        if len(data) < 3 or len(data) > 4:
239            raise error.general('invalid %gdb arguments')
240        self.process = gdb.gdb(bsp_arch, bsp,
241                               trace = self.exe_trace('gdb'),
242                               mi_trace = self.exe_trace('gdb-mi'))
243        script = self.expand('%%{%s}' % data[2])
244        if script:
245            script = [l.strip() for l in script.splitlines()]
246        if not self.in_error:
247            if self.console:
248                self.console.open()
249            if not self.opts.dry_run():
250                self.process.open(data[0], data[1],
251                                  script = script,
252                                  output = self.capture,
253                                  gdb_console = self.capture_console,
254                                  timeout = int(self.expand('%{timeout}')))
255            if self.console:
256                self.console.close()
257
258    def _dir_tftp(self, data, total, index, exe, bsp_arch, bsp):
259        if len(data) != 2:
260            raise error.general('invalid %tftp arguments')
261        try:
262            port = int(data[1])
263        except:
264            raise error.general('invalid %tftp port')
265        self.kill_on_end = True
266        if not self.opts.dry_run():
267            self.process = tftp.tftp(bsp_arch, bsp,
268                                     trace = self.exe_trace('tftp'))
269            if not self.in_error:
270                if self.console:
271                    self.console.open()
272                self.process.open(executable = exe,
273                                  port = port,
274                                  output_length = self._output_length,
275                                  console = self.capture_console,
276                                  timeout = (int(self.expand('%{timeout}')),
277                                             self._timeout))
278                if self.console:
279                    self.console.close()
280
281    def _directive_filter(self, results, directive, info, data):
282        if results[0] == 'directive':
283            _directive = results[1]
284            _data = results[2]
285            ds = []
286            if len(_data):
287                ds = [_data[0]]
288                if len(_data) > 1:
289                    ds += _data[1].split()
290            ds = self.expand(ds)
291
292            if _directive == '%console':
293                self._dir_console(ds)
294            else:
295                self._lock()
296                try:
297                    self.output = []
298                    total = int(self.expand('%{test_total}'))
299                    index = int(self.expand('%{test_index}'))
300                    exe = self.expand('%{test_executable}')
301                    bsp_arch = self.expand('%{arch}')
302                    bsp = self.expand('%{bsp}')
303                    fexe = self._target_exe_filter(exe)
304                    if self.report is not None:
305                        self.report.start(index, total, exe, fexe, bsp_arch, bsp)
306                    if self.index == 1:
307                        self._target_command('on', bsp_arch, bsp, exe, fexe)
308                    self._target_command('pretest', bsp_arch, bsp, exe, fexe)
309                finally:
310                    self._unlock()
311                if _directive == '%execute':
312                    self._dir_execute(ds, total, index, fexe, bsp_arch, bsp)
313                elif _directive == '%gdb':
314                    self._dir_gdb(ds, total, index, fexe, bsp_arch, bsp)
315                elif _directive == '%tftp':
316                    self._dir_tftp(ds, total, index, fexe, bsp_arch, bsp)
317                else:
318                    raise error.general(self._name_line_msg('invalid directive'))
319                self._lock()
320                if self.index == self.total:
321                    self._target_command('off', bsp_arch, bsp, exe, fexe)
322                self._target_command('posttest', bsp_arch, bsp, exe, fexe)
323                try:
324                    status = ''
325                    if self.report is not None:
326                        status = self.report.end(exe, self.output)
327                        self._capture_console('test result: %s' % (status))
328                    if status == 'timeout':
329                        if self.index != self.total:
330                            self._target_command('reset', bsp_arch, bsp, exe, fexe)
331                    self.process = None
332                    self.output = None
333                finally:
334                    self._unlock()
335        return None, None, None
336
337    def _realtime_trace(self, text):
338        for l in text:
339            print(''.join(l))
340
341    def run(self):
342        self.target_start_regx = self._target_regex('target_start_regex')
343        self.target_reset_regx = self._target_regex('target_reset_regex')
344        self.load(self.name)
345
346    def capture(self, text):
347        if not self.test_started:
348            s = text.find('*** BEGIN OF TEST ')
349            if s >= 0:
350                self.test_started = True
351                e = text[s + 3:].find('***')
352                if e >= 0:
353                    self.test_label = text[s + len('*** BEGIN OF TEST '):s + e + 3 - 1]
354                self.capture_console('test start: %s' % (self.test_label))
355        ok_to_kill = '*** TEST STATE: USER_INPUT' in text or \
356                     '*** TEST STATE: BENCHMARK' in text
357        if ok_to_kill:
358            reset_target = True
359        else:
360            reset_target = False
361        if self.test_started and self.target_start_regx is not None:
362            if self.target_start_regx.match(text):
363                self.capture_console('target start detected')
364                ok_to_kill = True
365        if not reset_target and self.target_reset_regx is not None:
366            if self.target_reset_regx.match(text):
367                self.capture_console('target reset condition detected')
368                self._target_command('reset')
369        if self.kill_on_end:
370            if not ok_to_kill and '*** END OF TEST ' in text:
371                self.capture_console('test end: %s' % (self.test_label))
372                if self.test_label is not None:
373                    ok_to_kill = '*** END OF TEST %s ***' % (self.test_label) in text
374        text = [(self.console_prefix, l) for l in text.replace(chr(13), '').splitlines()]
375        self._lock()
376        if self.output is not None:
377            if self.realtime_trace:
378                self._realtime_trace(text)
379            self.output += text
380        if reset_target:
381            if self.index == self.total:
382                self._target_command('off')
383            else:
384                self._target_command('reset')
385        self._unlock()
386        if ok_to_kill:
387            self._ok_kill()
388
389    def capture_console(self, text):
390        self._lock()
391        self._capture_console(text)
392        self._unlock()
393
394    def exe_trace(self, flag):
395        dt = self.macros['exe_trace']
396        if dt:
397            if flag in dt.split(','):
398                return True
399        return False
400
401    def kill(self):
402        if self.process:
403            try:
404                self.process.kill()
405            except:
406                pass
407
408def load(bsp, opts):
409    mandatory = ['bsp', 'arch', 'tester']
410    cfg = configuration.configuration()
411    path_ = opts.defaults.expand('%%{_configdir}/bsps/%s.ini' % (bsp))
412    ini_name = path.basename(path_)
413    for p in path.dirname(path_).split(':'):
414        if path.exists(path.join(p, ini_name)):
415            cfg.load(path.join(p, ini_name))
416            if not cfg.has_section(bsp):
417                raise error.general('bsp section not found in ini: [%s]' % (bsp))
418            item_names = cfg.get_item_names(bsp, err = False)
419            for m in mandatory:
420                if m not in item_names:
421                    raise error.general('mandatory item not found in bsp section: %s' % (m))
422            opts.defaults.set_write_map(bsp, add = True)
423            for i in cfg.get_items(bsp, flatten = False):
424                opts.defaults[i[0]] = i[1]
425            if not opts.defaults.set_read_map(bsp):
426                raise error.general('cannot set BSP read map: %s' % (bsp))
427            # Get a copy of the required fields we need
428            requires = cfg.comma_list(bsp, 'requires', err = False)
429            del cfg
430            user_config = opts.find_arg('--user-config')
431            if user_config is not None:
432                user_config = path.expanduser(user_config[1])
433                if not path.exists(user_config):
434                    raise error.general('cannot find user configuration file: %s' % (user_config))
435            else:
436                if 'HOME' in os.environ:
437                    user_config = path.join(os.environ['HOME'], '.rtemstesterrc')
438            if user_config:
439                if path.exists(user_config):
440                    cfg = configuration.configuration()
441                    cfg.load(user_config)
442                    if cfg.has_section(bsp):
443                        for i in cfg.get_items(bsp, flatten = False):
444                            opts.defaults[i[0]] = i[1]
445            # Check for the required values.
446            for r in requires:
447                if opts.defaults.get(r) is None:
448                    raise error.general('user value missing, BSP %s requires \'%s\': missing: %s' % \
449                                        (bsp, ', '.join(requires), r))
450            return opts.defaults['bsp']
451    raise error.general('cannot find bsp configuration file: %s.ini' % (bsp))
Note: See TracBrowser for help on using the repository browser.