|
@@ -0,0 +1,239 @@
|
|
|
+# Base class to be used by all test cases defined in the suite
|
|
|
+#
|
|
|
+# Copyright (C) 2016 Intel Corporation
|
|
|
+#
|
|
|
+# SPDX-License-Identifier: GPL-2.0
|
|
|
+
|
|
|
+import unittest
|
|
|
+import logging
|
|
|
+import json
|
|
|
+import unidiff
|
|
|
+from data import PatchTestInput
|
|
|
+import mailbox
|
|
|
+import collections
|
|
|
+import sys
|
|
|
+import os
|
|
|
+import re
|
|
|
+
|
|
|
+sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'pyparsing'))
|
|
|
+
|
|
|
+logger = logging.getLogger('patchtest')
|
|
|
+debug=logger.debug
|
|
|
+info=logger.info
|
|
|
+warn=logger.warn
|
|
|
+error=logger.error
|
|
|
+
|
|
|
+Commit = collections.namedtuple('Commit', ['author', 'subject', 'commit_message', 'shortlog', 'payload'])
|
|
|
+
|
|
|
+class PatchtestOEError(Exception):
|
|
|
+ """Exception for handling patchtest-oe errors"""
|
|
|
+ def __init__(self, message, exitcode=1):
|
|
|
+ super().__init__(message)
|
|
|
+ self.exitcode = exitcode
|
|
|
+
|
|
|
+class Base(unittest.TestCase):
|
|
|
+ # if unit test fails, fail message will throw at least the following JSON: {"id": <testid>}
|
|
|
+
|
|
|
+ endcommit_messages_regex = re.compile('\(From \w+-\w+ rev:|(?<!\S)Signed-off-by|(?<!\S)---\n')
|
|
|
+ patchmetadata_regex = re.compile('-{3} \S+|\+{3} \S+|@{2} -\d+,\d+ \+\d+,\d+ @{2} \S+')
|
|
|
+
|
|
|
+
|
|
|
+ @staticmethod
|
|
|
+ def msg_to_commit(msg):
|
|
|
+ payload = msg.get_payload()
|
|
|
+ return Commit(subject=msg['subject'].replace('\n', ' ').replace(' ', ' '),
|
|
|
+ author=msg.get('From'),
|
|
|
+ shortlog=Base.shortlog(msg['subject']),
|
|
|
+ commit_message=Base.commit_message(payload),
|
|
|
+ payload=payload)
|
|
|
+
|
|
|
+ @staticmethod
|
|
|
+ def commit_message(payload):
|
|
|
+ commit_message = payload.__str__()
|
|
|
+ match = Base.endcommit_messages_regex.search(payload)
|
|
|
+ if match:
|
|
|
+ commit_message = payload[:match.start()]
|
|
|
+ return commit_message
|
|
|
+
|
|
|
+ @staticmethod
|
|
|
+ def shortlog(shlog):
|
|
|
+ # remove possible prefix (between brackets) before colon
|
|
|
+ start = shlog.find(']', 0, shlog.find(':'))
|
|
|
+ # remove also newlines and spaces at both sides
|
|
|
+ return shlog[start + 1:].replace('\n', '').strip()
|
|
|
+
|
|
|
+ @classmethod
|
|
|
+ def setUpClass(cls):
|
|
|
+
|
|
|
+ # General objects: mailbox.mbox and patchset
|
|
|
+ cls.mbox = mailbox.mbox(PatchTestInput.repo.patch)
|
|
|
+
|
|
|
+ # Patch may be malformed, so try parsing it
|
|
|
+ cls.unidiff_parse_error = ''
|
|
|
+ cls.patchset = None
|
|
|
+ try:
|
|
|
+ cls.patchset = unidiff.PatchSet.from_filename(PatchTestInput.repo.patch, encoding=u'UTF-8')
|
|
|
+ except unidiff.UnidiffParseError as upe:
|
|
|
+ cls.patchset = []
|
|
|
+ cls.unidiff_parse_error = str(upe)
|
|
|
+
|
|
|
+ # Easy to iterate list of commits
|
|
|
+ cls.commits = []
|
|
|
+ for msg in cls.mbox:
|
|
|
+ if msg['subject'] and msg.get_payload():
|
|
|
+ cls.commits.append(Base.msg_to_commit(msg))
|
|
|
+
|
|
|
+ cls.setUpClassLocal()
|
|
|
+
|
|
|
+ @classmethod
|
|
|
+ def tearDownClass(cls):
|
|
|
+ cls.tearDownClassLocal()
|
|
|
+
|
|
|
+ @classmethod
|
|
|
+ def setUpClassLocal(cls):
|
|
|
+ pass
|
|
|
+
|
|
|
+ @classmethod
|
|
|
+ def tearDownClassLocal(cls):
|
|
|
+ pass
|
|
|
+
|
|
|
+ def fail(self, issue, fix=None, commit=None, data=None):
|
|
|
+ """ Convert to a JSON string failure data"""
|
|
|
+ value = {'id': self.id(),
|
|
|
+ 'issue': issue}
|
|
|
+
|
|
|
+ if fix:
|
|
|
+ value['fix'] = fix
|
|
|
+ if commit:
|
|
|
+ value['commit'] = {'subject': commit.subject,
|
|
|
+ 'shortlog': commit.shortlog}
|
|
|
+
|
|
|
+ # extend return value with other useful info
|
|
|
+ if data:
|
|
|
+ value['data'] = data
|
|
|
+
|
|
|
+ return super(Base, self).fail(json.dumps(value))
|
|
|
+
|
|
|
+ def skip(self, issue, data=None):
|
|
|
+ """ Convert the skip string to JSON"""
|
|
|
+ value = {'id': self.id(),
|
|
|
+ 'issue': issue}
|
|
|
+
|
|
|
+ # extend return value with other useful info
|
|
|
+ if data:
|
|
|
+ value['data'] = data
|
|
|
+
|
|
|
+ return super(Base, self).skipTest(json.dumps(value))
|
|
|
+
|
|
|
+ def shortid(self):
|
|
|
+ return self.id().split('.')[-1]
|
|
|
+
|
|
|
+ def __str__(self):
|
|
|
+ return json.dumps({'id': self.id()})
|
|
|
+
|
|
|
+class Metadata(Base):
|
|
|
+ @classmethod
|
|
|
+ def setUpClassLocal(cls):
|
|
|
+ cls.tinfoil = cls.setup_tinfoil()
|
|
|
+
|
|
|
+ # get info about added/modified/remove recipes
|
|
|
+ cls.added, cls.modified, cls.removed = cls.get_metadata_stats(cls.patchset)
|
|
|
+
|
|
|
+ @classmethod
|
|
|
+ def tearDownClassLocal(cls):
|
|
|
+ cls.tinfoil.shutdown()
|
|
|
+
|
|
|
+ @classmethod
|
|
|
+ def setup_tinfoil(cls, config_only=False):
|
|
|
+ """Initialize tinfoil api from bitbake"""
|
|
|
+
|
|
|
+ # import relevant libraries
|
|
|
+ try:
|
|
|
+ scripts_path = os.path.join(PatchTestInput.repodir, 'scripts', 'lib')
|
|
|
+ if scripts_path not in sys.path:
|
|
|
+ sys.path.insert(0, scripts_path)
|
|
|
+ import scriptpath
|
|
|
+ scriptpath.add_bitbake_lib_path()
|
|
|
+ import bb.tinfoil
|
|
|
+ except ImportError:
|
|
|
+ raise PatchtestOEError('Could not import tinfoil module')
|
|
|
+
|
|
|
+ orig_cwd = os.path.abspath(os.curdir)
|
|
|
+
|
|
|
+ # Load tinfoil
|
|
|
+ tinfoil = None
|
|
|
+ try:
|
|
|
+ builddir = os.environ.get('BUILDDIR')
|
|
|
+ if not builddir:
|
|
|
+ logger.warn('Bitbake environment not loaded?')
|
|
|
+ return tinfoil
|
|
|
+ os.chdir(builddir)
|
|
|
+ tinfoil = bb.tinfoil.Tinfoil()
|
|
|
+ tinfoil.prepare(config_only=config_only)
|
|
|
+ except bb.tinfoil.TinfoilUIException as te:
|
|
|
+ if tinfoil:
|
|
|
+ tinfoil.shutdown()
|
|
|
+ raise PatchtestOEError('Could not prepare properly tinfoil (TinfoilUIException)')
|
|
|
+ except Exception as e:
|
|
|
+ if tinfoil:
|
|
|
+ tinfoil.shutdown()
|
|
|
+ raise e
|
|
|
+ finally:
|
|
|
+ os.chdir(orig_cwd)
|
|
|
+
|
|
|
+ return tinfoil
|
|
|
+
|
|
|
+ @classmethod
|
|
|
+ def get_metadata_stats(cls, patchset):
|
|
|
+ """Get lists of added, modified and removed metadata files"""
|
|
|
+
|
|
|
+ def find_pn(data, path):
|
|
|
+ """Find the PN from data"""
|
|
|
+ pn = None
|
|
|
+ pn_native = None
|
|
|
+ for _path, _pn in data:
|
|
|
+ if path in _path:
|
|
|
+ if 'native' in _pn:
|
|
|
+ # store the native PN but look for the non-native one first
|
|
|
+ pn_native = _pn
|
|
|
+ else:
|
|
|
+ pn = _pn
|
|
|
+ break
|
|
|
+ else:
|
|
|
+ # sent the native PN if found previously
|
|
|
+ if pn_native:
|
|
|
+ return pn_native
|
|
|
+
|
|
|
+ # on renames (usually upgrades), we need to check (FILE) base names
|
|
|
+ # because the unidiff library does not provided the new filename, just the modified one
|
|
|
+ # and tinfoil datastore, once the patch is merged, will contain the new filename
|
|
|
+ path_basename = path.split('_')[0]
|
|
|
+ for _path, _pn in data:
|
|
|
+ _path_basename = _path.split('_')[0]
|
|
|
+ if path_basename == _path_basename:
|
|
|
+ pn = _pn
|
|
|
+ return pn
|
|
|
+
|
|
|
+ if not cls.tinfoil:
|
|
|
+ cls.tinfoil = cls.setup_tinfoil()
|
|
|
+
|
|
|
+ added_paths, modified_paths, removed_paths = [], [], []
|
|
|
+ added, modified, removed = [], [], []
|
|
|
+
|
|
|
+ # get metadata filename additions, modification and removals
|
|
|
+ for patch in patchset:
|
|
|
+ if patch.path.endswith('.bb') or patch.path.endswith('.bbappend') or patch.path.endswith('.inc'):
|
|
|
+ if patch.is_added_file:
|
|
|
+ added_paths.append(os.path.join(os.path.abspath(PatchTestInput.repodir), patch.path))
|
|
|
+ elif patch.is_modified_file:
|
|
|
+ modified_paths.append(os.path.join(os.path.abspath(PatchTestInput.repodir), patch.path))
|
|
|
+ elif patch.is_removed_file:
|
|
|
+ removed_paths.append(os.path.join(os.path.abspath(PatchTestInput.repodir), patch.path))
|
|
|
+
|
|
|
+ data = cls.tinfoil.cooker.recipecaches[''].pkg_fn.items()
|
|
|
+
|
|
|
+ added = [find_pn(data,path) for path in added_paths]
|
|
|
+ modified = [find_pn(data,path) for path in modified_paths]
|
|
|
+ removed = [find_pn(data,path) for path in removed_paths]
|
|
|
+
|
|
|
+ return [a for a in added if a], [m for m in modified if m], [r for r in removed if r]
|