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