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

5
Last change on this file since bf58911 was bf58911, checked in by Chris Johns <chrisj@…>, on 10/24/17 at 10:35:21

tester: Refactor to use INI format files for BSP configurations.

  • Add support for user condfigurations files with the --user-config.
  • Add support for a $HOME/.rtemstesterrc for a user configuration.

Closes #3204.

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