base.py 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239
  1. # Base class to be used by all test cases defined in the suite
  2. #
  3. # Copyright (C) 2016 Intel Corporation
  4. #
  5. # SPDX-License-Identifier: GPL-2.0-only
  6. import unittest
  7. import logging
  8. import json
  9. import unidiff
  10. from data import PatchTestInput
  11. import mailbox
  12. import collections
  13. import sys
  14. import os
  15. import re
  16. sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'pyparsing'))
  17. logger = logging.getLogger('patchtest')
  18. debug=logger.debug
  19. info=logger.info
  20. warn=logger.warn
  21. error=logger.error
  22. Commit = collections.namedtuple('Commit', ['author', 'subject', 'commit_message', 'shortlog', 'payload'])
  23. class PatchtestOEError(Exception):
  24. """Exception for handling patchtest-oe errors"""
  25. def __init__(self, message, exitcode=1):
  26. super().__init__(message)
  27. self.exitcode = exitcode
  28. class Base(unittest.TestCase):
  29. # if unit test fails, fail message will throw at least the following JSON: {"id": <testid>}
  30. endcommit_messages_regex = re.compile('\(From \w+-\w+ rev:|(?<!\S)Signed-off-by|(?<!\S)---\n')
  31. patchmetadata_regex = re.compile('-{3} \S+|\+{3} \S+|@{2} -\d+,\d+ \+\d+,\d+ @{2} \S+')
  32. @staticmethod
  33. def msg_to_commit(msg):
  34. payload = msg.get_payload()
  35. return Commit(subject=msg['subject'].replace('\n', ' ').replace(' ', ' '),
  36. author=msg.get('From'),
  37. shortlog=Base.shortlog(msg['subject']),
  38. commit_message=Base.commit_message(payload),
  39. payload=payload)
  40. @staticmethod
  41. def commit_message(payload):
  42. commit_message = payload.__str__()
  43. match = Base.endcommit_messages_regex.search(payload)
  44. if match:
  45. commit_message = payload[:match.start()]
  46. return commit_message
  47. @staticmethod
  48. def shortlog(shlog):
  49. # remove possible prefix (between brackets) before colon
  50. start = shlog.find(']', 0, shlog.find(':'))
  51. # remove also newlines and spaces at both sides
  52. return shlog[start + 1:].replace('\n', '').strip()
  53. @classmethod
  54. def setUpClass(cls):
  55. # General objects: mailbox.mbox and patchset
  56. cls.mbox = mailbox.mbox(PatchTestInput.repo.patch)
  57. # Patch may be malformed, so try parsing it
  58. cls.unidiff_parse_error = ''
  59. cls.patchset = None
  60. try:
  61. cls.patchset = unidiff.PatchSet.from_filename(PatchTestInput.repo.patch, encoding=u'UTF-8')
  62. except unidiff.UnidiffParseError as upe:
  63. cls.patchset = []
  64. cls.unidiff_parse_error = str(upe)
  65. # Easy to iterate list of commits
  66. cls.commits = []
  67. for msg in cls.mbox:
  68. if msg['subject'] and msg.get_payload():
  69. cls.commits.append(Base.msg_to_commit(msg))
  70. cls.setUpClassLocal()
  71. @classmethod
  72. def tearDownClass(cls):
  73. cls.tearDownClassLocal()
  74. @classmethod
  75. def setUpClassLocal(cls):
  76. pass
  77. @classmethod
  78. def tearDownClassLocal(cls):
  79. pass
  80. def fail(self, issue, fix=None, commit=None, data=None):
  81. """ Convert to a JSON string failure data"""
  82. value = {'id': self.id(),
  83. 'issue': issue}
  84. if fix:
  85. value['fix'] = fix
  86. if commit:
  87. value['commit'] = {'subject': commit.subject,
  88. 'shortlog': commit.shortlog}
  89. # extend return value with other useful info
  90. if data:
  91. value['data'] = data
  92. return super(Base, self).fail(json.dumps(value))
  93. def skip(self, issue, data=None):
  94. """ Convert the skip string to JSON"""
  95. value = {'id': self.id(),
  96. 'issue': issue}
  97. # extend return value with other useful info
  98. if data:
  99. value['data'] = data
  100. return super(Base, self).skipTest(json.dumps(value))
  101. def shortid(self):
  102. return self.id().split('.')[-1]
  103. def __str__(self):
  104. return json.dumps({'id': self.id()})
  105. class Metadata(Base):
  106. @classmethod
  107. def setUpClassLocal(cls):
  108. cls.tinfoil = cls.setup_tinfoil()
  109. # get info about added/modified/remove recipes
  110. cls.added, cls.modified, cls.removed = cls.get_metadata_stats(cls.patchset)
  111. @classmethod
  112. def tearDownClassLocal(cls):
  113. cls.tinfoil.shutdown()
  114. @classmethod
  115. def setup_tinfoil(cls, config_only=False):
  116. """Initialize tinfoil api from bitbake"""
  117. # import relevant libraries
  118. try:
  119. scripts_path = os.path.join(PatchTestInput.repodir, 'scripts', 'lib')
  120. if scripts_path not in sys.path:
  121. sys.path.insert(0, scripts_path)
  122. import scriptpath
  123. scriptpath.add_bitbake_lib_path()
  124. import bb.tinfoil
  125. except ImportError:
  126. raise PatchtestOEError('Could not import tinfoil module')
  127. orig_cwd = os.path.abspath(os.curdir)
  128. # Load tinfoil
  129. tinfoil = None
  130. try:
  131. builddir = os.environ.get('BUILDDIR')
  132. if not builddir:
  133. logger.warn('Bitbake environment not loaded?')
  134. return tinfoil
  135. os.chdir(builddir)
  136. tinfoil = bb.tinfoil.Tinfoil()
  137. tinfoil.prepare(config_only=config_only)
  138. except bb.tinfoil.TinfoilUIException as te:
  139. if tinfoil:
  140. tinfoil.shutdown()
  141. raise PatchtestOEError('Could not prepare properly tinfoil (TinfoilUIException)')
  142. except Exception as e:
  143. if tinfoil:
  144. tinfoil.shutdown()
  145. raise e
  146. finally:
  147. os.chdir(orig_cwd)
  148. return tinfoil
  149. @classmethod
  150. def get_metadata_stats(cls, patchset):
  151. """Get lists of added, modified and removed metadata files"""
  152. def find_pn(data, path):
  153. """Find the PN from data"""
  154. pn = None
  155. pn_native = None
  156. for _path, _pn in data:
  157. if path in _path:
  158. if 'native' in _pn:
  159. # store the native PN but look for the non-native one first
  160. pn_native = _pn
  161. else:
  162. pn = _pn
  163. break
  164. else:
  165. # sent the native PN if found previously
  166. if pn_native:
  167. return pn_native
  168. # on renames (usually upgrades), we need to check (FILE) base names
  169. # because the unidiff library does not provided the new filename, just the modified one
  170. # and tinfoil datastore, once the patch is merged, will contain the new filename
  171. path_basename = path.split('_')[0]
  172. for _path, _pn in data:
  173. _path_basename = _path.split('_')[0]
  174. if path_basename == _path_basename:
  175. pn = _pn
  176. return pn
  177. if not cls.tinfoil:
  178. cls.tinfoil = cls.setup_tinfoil()
  179. added_paths, modified_paths, removed_paths = [], [], []
  180. added, modified, removed = [], [], []
  181. # get metadata filename additions, modification and removals
  182. for patch in patchset:
  183. if patch.path.endswith('.bb') or patch.path.endswith('.bbappend') or patch.path.endswith('.inc'):
  184. if patch.is_added_file:
  185. added_paths.append(os.path.join(os.path.abspath(PatchTestInput.repodir), patch.path))
  186. elif patch.is_modified_file:
  187. modified_paths.append(os.path.join(os.path.abspath(PatchTestInput.repodir), patch.path))
  188. elif patch.is_removed_file:
  189. removed_paths.append(os.path.join(os.path.abspath(PatchTestInput.repodir), patch.path))
  190. data = cls.tinfoil.cooker.recipecaches[''].pkg_fn.items()
  191. added = [find_pn(data,path) for path in added_paths]
  192. modified = [find_pn(data,path) for path in modified_paths]
  193. removed = [find_pn(data,path) for path in removed_paths]
  194. return [a for a in added if a], [m for m in modified if m], [r for r in removed if r]