1#! /usr/bin/python
2
3# Copyright (c) 2013 The Chromium OS Authors. All rights reserved.
4# Use of this source code is governed by a BSD-style license that can be
5# found in the LICENSE file.
6"""The gdb dejagnu test wrapper."""
7import optparse
8import os
9from os import path
10import re
11import shutil
12import stat
13import sys
14import tempfile
15import time
16
17from cros_utils import command_executer
18from cros_utils import logger
19from cros_utils import misc
20
21from run_dejagnu import TryAcquireMachine
22
23_VALID_TEST_RESULTS = ['FAIL', 'UNRESOLVED', 'XPASS', 'ERROR', 'UNSUPPORTED',
24                       'PASS']
25
26
27def ProcessArguments(argv):
28  """Processing/validating script arguments."""
29  parser = optparse.OptionParser(description=(
30      'Launches gdb dejagnu test in chroot for chromeos toolchain, compares '
31      'the test result with a repository baseline and prints out the result.'),
32                                 usage='run_dejagnu options')
33  parser.add_option('-c',
34                    '--chromeos_root',
35                    dest='chromeos_root',
36                    help='Required. Specify chromeos root')
37  parser.add_option('-m',
38                    '--mount',
39                    dest='mount',
40                    help=('Specify gdb source to mount instead of "auto". '
41                          'Under "auto" mode, which is the default - gdb is '
42                          'checked out and built automatically at default '
43                          'directories. Under "mount" mode '
44                          '- the gdb_source is set to "$chromeos_'
45                          'root/chroot/usr/local/toolchain_root/gdb", which is '
46                          'the mount point for this option value.'))
47  parser.add_option('-b',
48                    '--board',
49                    dest='board',
50                    help=('Required. Specify board.'))
51  parser.add_option('-r',
52                    '--remote',
53                    dest='remote',
54                    help=('Required. Specify addresses/names of the board, '
55                          'seperate each address/name using comma(\',\').'))
56  parser.add_option('--cleanup',
57                    dest='cleanup',
58                    default=None,
59                    help=('Optional. Values to this option could be '
60                          '\'chroot\' (delete chroot) and '
61                          '\'chromeos\' (delete the whole chromeos tree).'))
62
63  options, args = parser.parse_args(argv)
64
65  if not options.chromeos_root:
66    raise SyntaxError('Missing argument for --chromeos_root.')
67  if not options.remote:
68    raise SyntaxError('Missing argument for --remote.')
69  if not options.board:
70    raise SyntaxError('Missing argument for --board.')
71  if options.cleanup == 'mount' and not options.mount:
72    raise SyntaxError('--cleanup=\'mount\' not valid unless --mount is given.')
73  if options.cleanup and not (options.cleanup == 'mount' or
74                              options.cleanup == 'chroot' or
75                              options.cleanup == 'chromeos'):
76    raise SyntaxError('Invalid option value for --cleanup')
77
78  return options
79
80
81class DejagnuExecuter(object):
82  """The class wrapper for dejagnu test executer."""
83
84  def __init__(self, base_dir, source_dir, chromeos_root, remote, board,
85               cleanup):
86    self._l = logger.GetLogger()
87    self._chromeos_root = chromeos_root
88    self._chromeos_chroot = path.join(chromeos_root, 'chroot')
89
90    self._remote = remote
91    self._board = board
92    ## Compute target from board
93    self._target = misc.GetCtargetFromBoard(board, chromeos_root)
94    if not self._target:
95      raise RuntimeError('Unsupported board "%s"' % board)
96    self._executer = command_executer.GetCommandExecuter()
97    self._base_dir = base_dir
98    self._tmp_abs = None
99    self._cleanup = cleanup
100    self._sshflag = ('-o StrictHostKeyChecking=no ' + '-o CheckHostIP=no ' +
101                     '-o UserKnownHostsFile=$(mktemp) ')
102
103    if source_dir:
104      self._source_dir = source_dir
105      self._mount_flag = 'USE="mounted_sources"'
106      self.MountSource()
107    else:
108      self._source_dir = None
109      self._mount_flag = ''
110
111  def SetupTestingDir(self):
112    self._tmp_abs = tempfile.mkdtemp(
113        prefix='dejagnu_',
114        dir=path.join(self._chromeos_chroot, 'tmp'))
115    self._tmp = self._tmp_abs[len(self._chromeos_chroot):]
116    self._tmp_testing_rsa = path.join(self._tmp, 'testing_rsa')
117    self._tmp_testing_rsa_abs = path.join(self._tmp_abs, 'testing_rsa')
118
119  def PrepareTestingRsaKeys(self):
120    if not path.isfile(self._tmp_testing_rsa_abs):
121      shutil.copy(
122          path.join(self._chromeos_root,
123                    'src/scripts/mod_for_test_scripts/ssh_keys/testing_rsa'),
124          self._tmp_testing_rsa_abs)
125      os.chmod(self._tmp_testing_rsa_abs, stat.S_IRUSR)
126
127  def PrepareTestFiles(self):
128    """Prepare site.exp and board exp files."""
129    # Create the boards directory.
130    os.mkdir('%s/boards' % self._tmp_abs)
131
132    # Generate the chromeos.exp file.
133    with open('%s/boards/gdb.exp.in' % self._base_dir, 'r') as template_file:
134      content = template_file.read()
135
136    substitutions = dict({
137        '__boardname__': self._board,
138        '__board_hostname__': self._remote,
139        '__tmp_testing_rsa__': self._tmp_testing_rsa,
140        '__tmp_dir__': self._tmp
141    })
142    for pat, sub in substitutions.items():
143      content = content.replace(pat, sub)
144
145    board_file_name = '%s/boards/%s.exp' % (self._tmp_abs, self._board)
146    with open(board_file_name, 'w') as board_file:
147      board_file.write(content)
148
149    # Generate the site file
150    with open('%s/site.exp' % self._tmp_abs, 'w') as site_file:
151      site_file.write('set target_list "%s"\n' % self._board)
152
153    with open('%s/boards/gdbserver.sh.in' % self._base_dir, 'r') \
154        as template_file:
155      content = template_file.read()
156    substitutions = dict({
157        '__board_hostname__': self._remote,
158        '__tmp_testing_rsa__': self._tmp_testing_rsa,
159        '__tmp_dir__': self._tmp
160    })
161    for pat, sub in substitutions.items():
162      content = content.replace(pat, sub)
163
164    gdbserver_file_name = '%s/boards/gdbserver.sh' % (self._tmp_abs)
165    with open(gdbserver_file_name, 'w') as board_file:
166      board_file.write(content)
167
168    st = os.stat(gdbserver_file_name)
169    os.chmod(gdbserver_file_name, st.st_mode | stat.S_IXGRP | stat.S_IXUSR)
170
171  def PrepareGdb(self):
172    self.PrepareGdbDefault()
173
174  def PrepareGdbDefault(self):
175    ret = self._executer.ChrootRunCommandWOutput(
176        self._chromeos_root, 'equery w cross-%s/gdb' % self._target)[1]
177    ret = path.basename(ret.strip())
178
179    matcher = re.match(r'(.*).ebuild', ret)
180    if matcher:
181      gdb_reversion = matcher.group(1)
182    else:
183      raise RuntimeError('Failed to get gdb reversion.')
184    gdb_version = gdb_reversion.split('-r')[0]
185    gdb_portage_dir = '/var/tmp/portage/cross-%s/%s/work' % (self._target,
186                                                             gdb_reversion)
187    self._gdb_source_dir = path.join(gdb_portage_dir, gdb_version)
188
189    ret = self._executer.ChrootRunCommand(self._chromeos_root, (
190        'sudo %s ebuild $(equery w cross-%s/gdb) clean compile' % (
191            self._mount_flag, self._target)))
192    if ret:
193      raise RuntimeError('ebuild gdb failed.')
194
195  def PrepareGdbserver(self):
196    self.PrepareGdbserverDefault()
197
198  def PrepareGdbserverDefault(self):
199    cmd = ('./setup_board --board {0}; '
200           '{1} emerge-{0} gdb'.format(self._board, self._mount_flag))
201    ret = self._executer.ChrootRunCommand(self._chromeos_root,
202                                          cmd,
203                                          print_to_console=True)
204    if ret:
205      raise RuntimeError('ebuild gdbserver failed.')
206
207    cmd = ('scp -i {0}  {1} '
208           '/build/{2}/usr/bin/gdbserver root@{3}:/usr/local/bin/'.format(
209               self._tmp_testing_rsa, self._sshflag, self._board, self._remote))
210    ret = self._executer.ChrootRunCommand(self._chromeos_root,
211                                          cmd,
212                                          print_to_console=True)
213    if ret:
214      raise RuntimeError('copy gdbserver failed.')
215
216    if self._mount_flag:
217      self.MountSource(unmount=False)
218
219  def Cleanup(self):
220    if not self._cleanup:
221      return
222
223    if self._cleanup == 'chroot' or self._cleanup == 'chromeos':
224      self._l.LogOutput('[Cleanup]: Deleting chroot inside \'{0}\''.format(
225          self._chromeos_root))
226      command = 'cd %s; cros_sdk --delete' % self._chromeos_root
227      rv = self._executer.RunCommand(command)
228      if rv:
229        self._l.LogWarning('Warning - failed to delete chroot.')
230      # Delete .cache - crosbug.com/34956
231      command = 'sudo rm -fr %s' % os.path.join(self._chromeos_root, '.cache')
232      rv = self._executer.RunCommand(command)
233      if rv:
234        self._l.LogWarning('Warning - failed to delete \'.cache\'.')
235
236    if self._cleanup == 'chromeos':
237      self._l.LogOutput('[Cleanup]: Deleting chromeos tree \'{0}\' ...'.format(
238          self._chromeos_root))
239      command = 'rm -fr {0}'.format(self._chromeos_root)
240      rv = self._executer.RunCommand(command)
241      if rv:
242        self._l.LogWarning('Warning - failed to remove chromeos tree.')
243
244  def MakeCheck(self):
245    cmd = ('ssh -i {0} {1}  root@{2} "reboot && exit"'
246           .format(self._tmp_testing_rsa, self._sshflag, self._remote))
247    self._executer.ChrootRunCommand(self._chromeos_root, cmd)
248    time.sleep(40)
249
250    cmd = ('ssh -i {0} {1}  root@{2} '
251           '"iptables -A INPUT -p tcp --dport 1234 -j ACCEPT"'.format(
252               self._tmp_testing_rsa, self._sshflag, self._remote))
253    self._executer.ChrootRunCommand(self._chromeos_root, cmd)
254
255    cmd = ('cd %s ; '
256           'DEJAGNU=%s make check' % (path.join(self._gdb_source_dir, 'gdb'),
257                                      path.join(self._tmp, 'site.exp')))
258    ret = self._executer.ChrootRunCommand(self._chromeos_root, cmd)
259    if ret:
260      raise RuntimeError('Make check failed.')
261
262  # This method ensures necessary mount points before executing chroot comamnd.
263  def MountSource(self, unmount=False):
264    script = os.path.join(self._base_dir, 'build_tc.py')
265    if unmount:
266      mount = '-u'
267    else:
268      mount = '-m'
269    cmd = ('python {0} --chromeos_root={1} '
270           '--gdb_dir={2} --board={3} {4}'.format(script, self._chromeos_root,
271                                                  self._source_dir, self._board,
272                                                  mount))
273    rv = self._executer.RunCommand(cmd)
274    if rv:
275      raise RuntimeError('Mount source failed.')
276
277  def ResultValidate(self):
278    self.PrepareResult()
279    result = []
280    for key, value in self.base_result.items():
281      if 'PASS' not in value:
282        continue
283      if key not in self.test_result:
284        continue
285      test_result = self.test_result[key]
286      if 'PASS' not in test_result:
287        result.append(key)
288    return result
289
290  def PrepareResult(self):
291    test_output = os.path.join(self._gdb_source_dir, 'gdb', 'testsuite',
292                               'gdb.sum')
293    test_output = misc.GetOutsideChrootPath(self._chromeos_root, test_output)
294    base_output = os.path.join(self._base_dir, 'gdb_baseline', self._target)
295
296    self.test_result = self.ParseResult(test_output)
297    self.base_result = self.ParseResult(base_output)
298
299  def ParseResult(self, gdb_sum):
300    result = {}
301    multi_keys = {}
302    with open(gdb_sum) as input_sum:
303      for line in input_sum:
304        line = line.strip()
305        r = line.split(':', 1)
306        if r[0] in _VALID_TEST_RESULTS:
307          key = r[1]
308          if r[1] in result:
309            if r[1] in multi_keys:
310              multi_keys[r[1]] += 1
311            else:
312              multi_keys[r[1]] = 2
313            key = r[1] + '_____{0}_____'.format(multi_keys[r[1]])
314          result[key] = r[0]
315    return result
316
317
318def Main(argv):
319  opts = ProcessArguments(argv)
320  available_machine = TryAcquireMachine(opts.remote)
321  executer = DejagnuExecuter(
322      misc.GetRoot(argv[0])[0], opts.mount, opts.chromeos_root,
323      available_machine._name, opts.board, opts.cleanup)
324  # Return value is a 3- or 4-element tuple
325  #   element#1 - exit code
326  #   element#2 - stdout
327  #   element#3 - stderr
328  #   element#4 - exception infor
329  # Some other scripts need these detailed information.
330  ret = (1, '', '')
331  try:
332    executer.SetupTestingDir()
333    executer.PrepareTestingRsaKeys()
334    executer.PrepareTestFiles()
335    executer.PrepareGdb()
336    executer.PrepareGdbserver()
337    executer.MakeCheck()
338    result = executer.ResultValidate()
339    print result
340    if result:
341      ret = (1, result, '')
342    else:
343      ret = (0, '', '')
344
345  except Exception as e:
346    # At least log the exception on console.
347    print e
348    # The #4 element encodes the runtime exception.
349    ret = (1, '', '', 'Exception happened during execution: \n' + str(e))
350  finally:
351    executer.Cleanup()
352    return ret
353
354
355if __name__ == '__main__':
356  retval = Main(sys.argv)[0]
357  sys.exit(retval)
358