[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

Re: a new tool fedabipkgdiff



An updated patch with a small fix.

Regards,
Chenxiong Qi

----- Original Message -----
> From: "Chenxiong Qi" <cqi@redhat.com>
> To: libabigail@sourceware.org, dodji@redhat.com
> Sent: Wednesday, February 17, 2016 6:38:31 PM
> Subject: a new tool fedabipkgdiff
> 
> Hi,
> 
> This is a new tool fedabipkgdiff that would be much convenient for
> Fedora packagers to check potential ABI/API differences quickly using
> abipkgdiff shipped with libabigail. This tool came from a cool idea from
> Dodji. Currently, as the first step, it supports following ways,
> 
> fedabipkgdiff --from fc23 ./foo-0.1-1.fc23.x86_64.rpm
> fedabipkgdiff --from fc23 --to fc24 foo
> fedabipkgdiff foo-0.1-1.fc23 foo-0.1-1.fc24
> fedabipkgdiff foo-0.1-1.fc23.i686 foo-0.1-1.fc24.i686
> 
> For more details, please refer to
> https://sourceware.org/bugzilla/show_bug.cgi?id=19428
> 
> Next step is to support the 4th use case mentioned in bug 19428.
> 
> fedabipkgdiff is being under development, still need to improve. Welcome
> any feedback. Thanks.
> 
> Happy hacking :)
> 
> Regards,
> Chenxiong Qi
> 
From 245ade840f7515d0319bb25c6c94c7171f7054f1 Mon Sep 17 00:00:00 2001
From: Chenxiong Qi <cqi@redhat.com>
Date: Tue, 9 Feb 2016 18:05:33 +0800
Subject: [PATCH] new tool of fedabipkgdiff

fedabipkgdiff is a convenient way for Fedora packagers to inspect ABI
compatibility issues quickly.

Currently with the first version of fedabipkgdiff, you can invoke it in
following ways.

fedabipkgdiff --from fc23 foo-0.1-1.fc23.x86_64.rpm
fedabipkgdiff --from fc23 --to fc24 foo
fedabipkgdiff foo-0.1-1.fc23 foo-0.1-1.fc24
fedabipkgdiff foo-0.1-1.fc23.i686 foo-0.1-1.fc24.i686

Bug 19428
---
 .gitignore                    |   3 +
 tests/Makefile.am             |   4 +
 tests/runtestfedabipkgdiff.sh |   5 +
 tools/fedabipkgdiff           | 855 ++++++++++++++++++++++++++++++++++++++++++
 4 files changed, 867 insertions(+)
 create mode 100755 tests/runtestfedabipkgdiff.sh
 create mode 100755 tools/fedabipkgdiff

diff --git a/.gitignore b/.gitignore
index bb7c42a..169400a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -6,6 +6,7 @@ Makefile.in
 *.lo
 *.o
 *~
+*.swp
 
 /aclocal.m4
 /autom4te.cache/
@@ -17,3 +18,5 @@ Makefile.in
 
 /include/abg-version.h
 /*.pc
+
+.tags
diff --git a/tests/Makefile.am b/tests/Makefile.am
index caf49e6..958995c 100644
--- a/tests/Makefile.am
+++ b/tests/Makefile.am
@@ -31,6 +31,7 @@ runtestlookupsyms		\
 runtestaltdwarf			\
 runtestcorediff			\
 runtestabidiffexit		\
+runtestfedabipkgdiff.sh		\
 $(CXX11_TESTS)
 
 EXTRA_DIST = runtestcanonicalizetypes.sh.in
@@ -114,6 +115,9 @@ printdifftree_LDADD = $(top_builddir)/src/libabigail.la
 runtestcanonicalizetypes_sh_SOURCES =
 runtestcanonicalizetypes.sh$(EXEEXT):
 
+runtestfedabipkgdiff_sh_SOURCES =
+runtestfedabipkgdiff.sh$(EXEEXT):
+
 AM_CPPFLAGS=-I${abs_top_srcdir}/include \
 -I${abs_top_builddir}/include -I${abs_top_srcdir}/tools -fPIC
 
diff --git a/tests/runtestfedabipkgdiff.sh b/tests/runtestfedabipkgdiff.sh
new file mode 100755
index 0000000..4144419
--- /dev/null
+++ b/tests/runtestfedabipkgdiff.sh
@@ -0,0 +1,5 @@
+#!/usr/bin/bash
+
+export FEDABIPKGDIFF_TESTS=1
+
+../../tools/fedabipkgdiff
\ No newline at end of file
diff --git a/tools/fedabipkgdiff b/tools/fedabipkgdiff
new file mode 100755
index 0000000..5d62db2
--- /dev/null
+++ b/tools/fedabipkgdiff
@@ -0,0 +1,855 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+import argparse
+import glob
+import logging
+import os
+import re
+import shlex
+import subprocess
+import sys
+
+from itertools import groupby
+from urlparse import urlparse
+
+import koji
+
+"""
+Find proper RPM packages from koji to run abipkgdiff.
+
+Internal structure is
+
+fc23                                  fc24
+i686                                  i686
+    foo-0.1-1.fc23.i686.rpm               foo-0.2-1.fc24.i686.rpm
+    foo-debuginfo-0.1-1.fc23.i686.rpm     foo-debuginfo-0.2-1.fc23.i686.rpm
+x86_64                                x86_64
+    foo-0.1-1.fc23.x86_64.rpm             foo-0.2-1.fc24.x86_64.rpm
+    foo-debuginfo-0.1-1.fc23.x86_64.rpm   foo-debuginfo-0.2-1.fc23.x86_64.rpm
+"""
+
+DEFAULT_KOJI_TOPDIR = 'https://kojipkgs.fedoraproject.org'
+DEFAULT_KOJI_SERVER = 'http://koji.fedoraproject.org/kojihub'
+
+HOME_DIR = os.path.join('/tmp',
+                        os.path.splitext(os.path.basename(__file__))[0])
+
+global_config = None
+
+logging.basicConfig(format='%(levelname)s %(asctime)s %(name)s %(message)s',
+                    level=logging.DEBUG)
+logger = logging.getLogger(os.path.basename(__file__))
+
+
+class KojiPackageNotFound(Exception):
+    """Package is not found in Koji"""
+
+
+class PackageNotFound(Exception):
+    """Package is not found locally"""
+
+
+class InvalidFedoraDistro(Exception):
+    """Invalid Fedora Distro"""
+
+
+class CannotFindLatestBuildError(Exception):
+    """Cannot find latest build from a package"""
+
+
+def is_fedora_distro(distro):
+    """Adjust if a distro is specific to Fedora
+
+    :param str distro: a string representing a distro value.
+    :return: True if distro is the one specific to Fedora, like fc5, fc24.
+    "rtype: bool
+    """
+    return re.match(r'^fc\d{1,2}$', distro) is not None
+
+
+class Brew(object):
+    """Proxy to kojihub XMLRPC with additional extensions to fedabipkgdiff"""
+
+    def __init__(self, baseurl):
+        self.session = koji.ClientSession(baseurl)
+
+    def listRPMs(self, **kwargs):
+        selector = kwargs.pop('selector')
+        rpms = self.session.listRPMs(**kwargs)
+
+        if selector:
+            rpms = [rpm for rpm in rpms if selector(rpm)]
+
+        return rpms
+
+    def getRPM(self, *args, **kwargs):
+        rpm = self.session.getRPM(*args, **kwargs)
+        if rpm is None:
+            raise KojipackageNotFound()
+        return rpm
+
+    def listBuilds(self, topone=None, selector=None, order_by=None,
+                   reverse=None, **kwargs):
+        """Proxy to kojihub.listBuilds
+
+        Suport additional two keyword parameters:
+
+        - topone: return the top first one
+        - selector: a callable object used to select specific subset of builds
+        """
+        if 'state' not in kwargs:
+            kwargs['state'] = koji.BUILD_STATES['COMPLETE']
+
+        if selector is not None and not hasattr(selector, '__call__'):
+            raise TypeError(
+                '{0} is not a callable object.'.foramt(str(selector)))
+
+        if order_by is not None and not isinstance(order_by, basestring):
+            raise TypeError('order_by {0} is invalid.'.format(order_by))
+
+        builds = self.session.listBuilds(**kwargs)
+        if selector is not None:
+            builds = [build for build in builds if selector(build)]
+        if order_by is not None:
+            # FIXME: is it possible to sort builds by using opts parameter of
+            # listBuilds
+            builds = sorted(builds,
+                            key=lambda item: item[order_by],
+                            reverse=reverse)
+        if topone:
+            builds = builds[0:1]
+
+        return builds
+
+    def getPackage(self, name):
+        package = self.session.getPackage(name)
+        if package is None:
+            package = self.session.getPackage(name.rsplit('-', 1)[0])
+            if package is None:
+                raise KojiPackageNotFound()
+        return package
+
+    def getBuild(self, *args, **kwargs):
+        return self.session.getBuild(*args, **kwargs)
+
+    def get_rpm_build_id(self, name, version, release, arch=None):
+        """Get build ID that contains a rpm with specific nvra
+
+        If arch is omitted, a rpm can be identified easily by its N-V-R-A.
+
+        If arch is omitted, name is used to get associated package, and then
+        to get the build.
+
+        :param str name: name of a rpm
+        :param str version: version of a rpm
+        :param str release: release of a rpm
+        :param arch: arch of a rpm
+        :type arch: str or None
+        :return: the build from where the rpm is built
+        :rtype: dict
+        :raises KojiPackageNotFound: if name is not found from koji when arch
+            is None
+        """
+        if arch is None:
+            package = self.getPackage(name)
+            selector = lambda item: item['version'] == version and \
+                item['release'] == release
+            builds = self.listBuilds(packageID=package['id'],
+                                     selector=selector)
+            return builds[0]['build_id']
+        else:
+            rpm = self.getRPM({'name': name,
+                               'version': version,
+                               'release': release,
+                               'arch': arch,
+                               })
+            return rpm['build_id']
+
+    def get_package_latest_build(self, package_name, distro):
+        """Get latest build from a package
+
+        :param str package_name: from which package to get the latest build
+        :param str distro: which distro the latest build belongs to
+        :return: the found build
+        :rtype: dict or None
+        """
+        package = self.getPackage(package_name)
+        selector = lambda item: item['release'].endswith(distro)
+        builds = self.listBuilds(packageID=package['id'],
+                                 selector=selector,
+                                 order_by='nvr',
+                                 reverse=True)
+        return builds[0] if builds else None
+
+    def select_rpms_from_a_build(self, build_id, package_name, arches=None):
+        """Select specific RPMs within a build
+
+        rpms could be filtered be specific criterias by the parameters.
+
+        :param int build_id: from which build to select rpms.
+        :param str package_name: which rpm to select that matches this name.
+        :param arches: which arches to select. If arches omits, rpms with all
+            arches except noarch and src will be selected.
+        :type arches: list, tuple or None
+        :return: a list of rpms returned from listRPMs
+        :rtype: list
+        """
+        def rpms_selector(package_name, excluded_arches):
+            return lambda rpm: \
+                rpm['arch'] not in excluded_arches and \
+                (rpm['name'] == package_name or
+                 rpm['name'].endswith('-debuginfo'))
+
+        selector = rpms_selector(package_name, ('noarch', 'src'))
+        return self.listRPMs(buildID=build_id,
+                             arches=arches,
+                             selector=selector)
+
+    def get_latest_built_rpms(self, package_name, distro, arches=None):
+        """Get rpms from latest build of a package
+
+        By default, debuginfo rpm is also retrieved.
+
+        :param str package_name: from which package to get the rpms
+        :param str distro: which distro the rpms belong to
+        :param arches: which arches the rpms belong to
+        :type arches: str or None
+        :return: the selected rpms
+        :rtype: list
+        """
+        latest_build = self.get_package_latest_build(package_name, distro)
+
+        # Get rpm and debuginfo rpm from each arch
+        return self.select_rpms_from_a_build(latest_build['build_id'],
+                                             package_name,
+                                             arches=arches)
+
+
+def get_session():
+    return Brew(global_config.koji_server)
+
+
+def get_download_dir():
+    download_dir = os.path.join(HOME_DIR, 'downloads')
+    if not os.path.exists(download_dir):
+        os.makedirs(download_dir)
+    return download_dir
+
+
+def download_rpm(url):
+    cmd = shlex.split('wget -q -P {0} -c {1}'.format(get_download_dir(), url))
+    proc = subprocess.Popen(cmd)
+    s_stdout, s_stderr = proc.communicate()
+    if proc.returncode > 0:
+        logger.error('wget fails. returned code: %d. message: %s',
+                     proc.returncode, s_stderr)
+        return False
+    return True
+
+
+def download_rpms(pkg_info):
+    def _download(pkg_info):
+        if os.path.exists(pkg_info['downloaded_rpm_file']):
+            logger.info('Reuse %s', pkg_info['downloaded_rpm_file'])
+        else:
+            logger.info('Download %s', pkg_info['download_url'])
+            download_rpm(pkg_info['download_url'])
+
+    for arch, rpm_infos in pkg_info.iteritems():
+        map(_download, rpm_infos)
+
+
+def find_rpm_filepath(rpm):
+    """Build RPM download URL"""
+    path_info = koji.PathInfo(topdir=global_config.koji_topdir)
+    session = get_session()
+    build = session.getBuild(rpm['build_id'])
+    return os.path.join(path_info.build(build), path_info.rpm(rpm))
+
+
+def find_local_debuginfo_rpm(rpm_file):
+    """Find debuginfo rpm package from a directory
+
+    :param str rpm_file: the rpm file name
+    :return: the absolute file name of the found debuginfo rpm
+    :rtype: str or None
+    """
+    search_dir = os.path.dirname(os.path.abspath(rpm_file))
+    nvra = koji.parse_NVRA(os.path.basename(rpm_file))
+    debuginfo_rpm_file_glob = os.path.join(
+        search_dir,
+        '%(name)s-debuginfo-%(version)s-%(release)s.%(arch)s.rpm' % nvra)
+
+    try:
+        debuginfo_rpm = glob.glob(debuginfo_rpm_file_glob)[0]
+    except IndexError:
+        return None
+
+    return debuginfo_rpm
+
+
+def abipkgdiff(pkg1_info, pkg2_info):
+    """Run abipkgdiff against found two RPM packages"""
+
+    pkg1_rpm, pkg1_debuginfo_rpm = pkg1_info
+    pkg2_rpm, pkg2_debuginfo_rpm = pkg2_info
+
+    cmd = 'abipkgdiff --d1 {0} --d2 {1} {2} {3}'.format(
+        pkg1_debuginfo_rpm, pkg2_debuginfo_rpm,
+        pkg1_rpm, pkg2_rpm)
+
+    if global_config.dry_run:
+        logger.info('DRY-RUN: %s', cmd)
+        return
+
+    logger.debug('Run: %s', cmd)
+
+    proc = subprocess.Popen(shlex.split(cmd))
+    return proc.wait()
+
+
+def run_abipkgdiff(pkg1_infos, pkg2_infos):
+    """Run abipkgdiff
+
+    If one of the executions finds ABI differences, the return code is the
+    return code from abipkgdiff.
+    """
+    arches = pkg1_infos.keys()
+    arches.sort()
+
+    return_code = 0
+
+    for arch in arches:
+        pkg1_info = pkg1_infos[arch]
+        pkg2_info = pkg2_infos[arch]
+
+        ret = abipkgdiff((pkg1_info[0]['downloaded_rpm_file'],
+                          pkg1_info[1]['downloaded_rpm_file']),
+                         (pkg2_info[0]['downloaded_rpm_file'],
+                          pkg2_info[1]['downloaded_rpm_file']))
+        if ret > 0:
+            return_code = ret
+
+    return return_code
+
+
+def diff_local_rpm_with_latest_rpm_from_koji():
+    """Diff against local rpm and remove latest rpm
+
+    This operation handles a local rpm and debuginfo rpm and remote ones
+    located in remote Koji server, that has specific distro specificed by
+    argument --from.
+
+    1/ Suppose the packager has just locally built a package named
+    foo-3.0.fc24.rpm. To compare the ABI of this locally build package with the
+    latest stable package from Fedora 23, one would do:
+
+    fedabidiff --from f23 ./foo-3.0.fc24.rpm
+    """
+
+    if not is_fedora_distro(global_config.from_distro):
+        raise InvalidFedoraDistro('Invalid Fedora distro {0}'.format(distro))
+
+    local_rpm_file = global_config.NVR[0]
+    if not os.path.exists(local_rpm_file):
+        raise ValueError('{0} does not exist.'.format(local_rpm_file))
+
+    local_debuginfo_rpm = find_local_debuginfo_rpm(local_rpm_file)
+    logger.debug('Found local debuginfo rpm %s', local_debuginfo_rpm)
+    if local_debuginfo_rpm is None:
+        raise ValueError(
+            'debuginfo rpm {0} does not exist.'.format(local_debuginfo_rpm))
+
+    nvra = koji.parse_NVRA(os.path.basename(local_rpm_file))
+    session = get_session()
+    rpms = session.get_latest_built_rpms(nvra['name'],
+                                         global_config.from_distro,
+                                         arches=nvra['arch'])
+    pkg_infos = make_rpms_usable_for_abipkgdiff(rpms)
+    download_rpms(pkg_infos)
+
+    pkg_info = pkg_infos.values()[0]
+    return abipkgdiff((local_rpm_file, local_debuginfo_rpm),
+                      (pkg_info[0]['downloaded_rpm_file'],
+                       pkg_info[1]['downloaded_rpm_file']))
+
+
+def make_rpms_usable_for_abipkgdiff(rpms):
+    """
+    Construct result that contains mappings from arch to download url and
+    downloaded rpm filename of rpm and debuginfo rpm
+
+    :return:  a mapping from an arch to a list of dict objects that contains
+        a URL from where to download the rpm, and an absolute path of a
+        predictable downloaded rpm filename.
+    :rtype: dict
+    """
+
+    result = {}
+
+    rpms_iter = groupby(sorted(rpms, key=lambda rpm: rpm['arch']),
+                        key=lambda item: item['arch'])
+
+    for arch, rpms in rpms_iter:
+        l = []
+        # sorted ensures the order of rpm and associated debuginfo rpm
+        for item in sorted(rpms, key=lambda item: item['name']):
+            download_url = find_rpm_filepath(item)
+            l.append({
+                'download_url': download_url,
+                'downloaded_rpm_file': os.path.join(
+                    get_download_dir(),
+                    os.path.basename(urlparse(download_url).path)),
+                })
+        result[arch] = l
+
+    return result
+
+
+def diff_latest_rpms_based_on_distros():
+    """abipkgdiff rpms based on two distros
+
+    2/ Suppose the packager wants to see how the ABIs of the package foo
+    evolved between fedora 19 and fedora 22. She would thus type the command:
+
+    fedabidiff --from f19 --to f22 foo
+    """
+
+    from_distro = global_config.from_distro
+    to_distro = global_config.to_distro
+
+    if not is_fedora_distro(from_distro):
+        raise InvalidFedoraDistro(
+            'Invalid Fedora distro {0}'.format(from_distro))
+
+    if not is_fedora_distro(to_distro):
+        raise InvalidFedoraDistro(
+            'Invalid Fedora distro {0}'.format(distro))
+
+    package_name = global_config.NVR[0]
+
+    session = get_session()
+
+    rpms = session.get_latest_built_rpms(package_name,
+                                         distro=global_config.from_distro)
+    pkg1_infos = make_rpms_usable_for_abipkgdiff(rpms)
+    download_rpms(pkg1_infos)
+
+    rpms = session.get_latest_built_rpms(package_name,
+                                         distro=global_config.to_distro)
+    pkg2_infos = make_rpms_usable_for_abipkgdiff(rpms)
+    download_rpms(pkg2_infos)
+
+    return run_abipkgdiff(pkg1_infos, pkg2_infos)
+
+
+def diff_rpms_with_nvra(name, version, release, arch=None):
+    session = get_session()
+
+    build_id = session.get_rpm_build_id(name, version, release, arch)
+    rpms = session.select_rpms_from_a_build(build_id, name, arches=arch)
+    return make_rpms_usable_for_abipkgdiff(rpms)
+
+
+def diff_two_nvras_from_koji():
+    """Diff two nvras from koji
+
+    The arch probably omits, that means febabipkgdiff will diff all arches. If
+    specificed, the specific arch will be handled.
+
+    3/ Suppose the packager wants to compare the ABI of two packages designated
+    by their name and version. She would issue a command like this:
+
+    fedabidiff foo-1.0.fc19 foo-3.0.fc24
+    fedabidiff foo-1.0.fc19.i686 foo-1.0.fc24.i686
+    """
+    left_rpm = koji.parse_NVRA(global_config.NVR[0])
+    right_rpm = koji.parse_NVRA(global_config.NVR[1])
+
+    if is_fedora_distro(left_rpm['arch']) and \
+            is_fedora_distro(right_rpm['arch']):
+        nvr = koji.parse_NVR(global_config.NVR[0])
+        params1 = (nvr['name'], nvr['version'], nvr['release'])
+
+        nvr = koji.parse_NVR(global_config.NVR[1])
+        params2 = (nvr['name'], nvr['version'], nvr['release'])
+    else:
+        params1 = (left_rpm['name'],
+                   left_rpm['version'],
+                   left_rpm['release'],
+                   left_rpm['arch'])
+        params2 = (right_rpm['name'],
+                   right_rpm['version'],
+                   right_rpm['release'],
+                   right_rpm['arch'])
+
+    pkg1_infos = diff_rpms_with_nvra(*params1)
+    download_rpms(pkg1_infos)
+
+    pkg2_infos = diff_rpms_with_nvra(*params2)
+    download_rpms(pkg2_infos)
+
+    return run_abipkgdiff(pkg1_infos, pkg2_infos)
+
+
+def build_commandline_args_parser():
+    parser = argparse.ArgumentParser()
+    parser.add_argument('NVR', nargs='+')
+    parser.add_argument('--server', required=False, dest='koji_server',
+                        default=DEFAULT_KOJI_SERVER)
+    parser.add_argument('--topdir', required=False, dest='koji_topdir',
+                        default=DEFAULT_KOJI_TOPDIR)
+    parser.add_argument('--dry-run', required=False, dest='dry_run',
+                        action='store_true')
+    parser.add_argument('--from', required=False, metavar='DISTRO',
+                        dest='from_distro')
+    parser.add_argument('--to', required=False, metavar='DISTRO',
+                        dest='to_distro')
+
+    return parser
+
+
+def main():
+    parser = build_commandline_args_parser()
+
+    args = parser.parse_args()
+
+    global global_config
+    global_config = args
+
+    if global_config.from_distro and global_config.to_distro is None and \
+            global_config.NVR:
+        returncode = diff_local_rpm_with_latest_rpm_from_koji()
+
+    elif global_config.from_distro and global_config.to_distro and \
+            global_config.NVR:
+        returncode = diff_latest_rpms_based_on_distros()
+
+    elif global_config.from_distro is None and \
+            global_config.to_distro is None and len(global_config.NVR) > 1:
+        returncode = diff_two_nvras_from_koji()
+
+    else:
+        print >>sys.stderr, 'Unknown arguments. Please refer to -h.'
+        returncode = 1
+
+    return returncode
+
+
+invoked_from_cmd = __name__ == '__main__'
+
+if 'FEDABIPKGDIFF_TESTS' not in os.environ and invoked_from_cmd:
+    try:
+        sys.exit(main())
+    except Exception as e:
+        print >>sys.stderr, str(e)
+        sys.exit(1)
+
+
+import itertools
+import shutil
+import unittest
+
+
+try:
+    from mock import patch
+except ImportError:
+    print >>sys.stderr, \
+        'mock is not installed. Please install it before running tests.'
+    sys.exit(1)
+
+counter = itertools.count(0)
+
+
+class UtilsTest(unittest.TestCase):
+
+    def test_is_fedora_distro(self):
+        distro = 'fc5'
+        self.assertTrue(is_fedora_distro(distro))
+
+        distro = 'f5'
+        self.assertFalse(is_fedora_distro(distro))
+
+        distro = 'fc23'
+        self.assertTrue(is_fedora_distro(distro))
+
+        distro = 'fc'
+        self.assertFalse(is_fedora_distro(distro))
+
+        distro = 'fc234'
+        self.assertFalse(is_fedora_distro(distro))
+
+
+class RunAbipkgdiffTest(unittest.TestCase):
+    """Test case for method run_abipkgdiff"""
+
+    def setUp(self):
+        self.pkg1_single_info = {
+            'i686': [{'downloaded_rpm_file': 'dummy file path'},
+                     {'downloaded_rpm_file': 'dummy file path'}],
+            }
+
+        self.pkg2_single_info = {
+            'i686': [{'downloaded_rpm_file': 'dummy file path'},
+                     {'downloaded_rpm_file': 'dummy file path'}],
+            }
+
+        self.pkg1_infos = {
+            'i686': [{'downloaded_rpm_file': 'dummy file path'},
+                     {'downloaded_rpm_file': 'dummy file path'}],
+            'x86_64': [{'downloaded_rpm_file': 'dummy file path'},
+                       {'downloaded_rpm_file': 'dummy file path'}],
+            'armv7hl': [{'downloaded_rpm_file': 'dummy file path'},
+                        {'downloaded_rpm_file': 'dummy file path'}],
+            }
+
+        self.pkg2_infos = {
+            'i686': [{'downloaded_rpm_file': 'dummy file path'},
+                     {'downloaded_rpm_file': 'dummy file path'}],
+            'x86_64': [{'downloaded_rpm_file': 'dummy file path'},
+                       {'downloaded_rpm_file': 'dummy file path'}],
+            'armv7hl': [{'downloaded_rpm_file': 'dummy file path'},
+                        {'downloaded_rpm_file': 'dummy file path'}],
+            }
+
+    @patch('__main__.abipkgdiff')
+    def test_all_success(self, mock_abipkgdiff):
+        mock_abipkgdiff.return_value = 0
+
+        result = run_abipkgdiff(self.pkg1_single_info, self.pkg2_single_info)
+        self.assertEquals(0, result)
+
+        result = run_abipkgdiff(self.pkg1_infos, self.pkg2_infos)
+        self.assertEquals(0, result)
+
+    @patch('__main__.abipkgdiff')
+    def test_all_failure(self, mock_abipkgdiff):
+        mock_abipkgdiff.return_value = 4
+
+        result = run_abipkgdiff(self.pkg1_single_info, self.pkg2_single_info)
+        self.assertEquals(4, result)
+
+        result = run_abipkgdiff(self.pkg1_infos, self.pkg2_infos)
+        self.assertEquals(4, result)
+
+    @patch('__main__.abipkgdiff', new=lambda param1, param2: counter.next())
+    def test_partial_failure(self):
+        result = run_abipkgdiff(self.pkg1_infos, self.pkg2_infos)
+        self.assertTrue(result > 0)
+
+
+fake_rpm_file = 'foo-0.1-1.fc24.x86_64.rpm'
+fake_debuginfo_rpm_file = 'foo-debuginfo-0.1-1.fc24.x86_64.rpm'
+
+
+class MockGlobalConfig(object):
+    koji_server = DEFAULT_KOJI_SERVER
+
+
+def mock_get_session():
+    return MockKojiClientSession(baseurl=DEFAULT_KOJI_SERVER)
+
+
+class MockKojiClientSession(object):
+
+    def __init__(self, *args, **kwargs):
+        """Accept arbitrary parameters but do nothing for this mock"""
+        self.args = args
+        self.kwargs = kwargs
+
+    def getPackage(self, *args, **kwargs):
+        return {
+            'id': 1,
+            'name': 'whatever a name of a package',
+        }
+
+    def listRPMs(self, *args, **kwargs):
+        return [{'arch': 'i686',
+                 'name': 'httpd-debuginfo',
+                 'nvr': 'httpd-debuginfo-2.4.18-1.fc22',
+                 'release': '1.fc22',
+                 'version': '2.4.18'},
+                {'arch': 'i686',
+                 'name': 'mod_session',
+                 'nvr': 'mod_session-2.4.18-1.fc22',
+                 'release': '1.fc22',
+                 'version': '2.4.18'},
+                {'arch': 'i686',
+                 'name': 'httpd',
+                 'nvr': 'httpd-2.4.18-1.fc22',
+                 'release': '1.fc22',
+                 'version': '2.4.18'},
+                {'arch': 'i686',
+                 'name': 'mod_proxy_html',
+                 'nvr': 'mod_proxy_html-2.4.18-1.fc22',
+                 'release': '1.fc22',
+                 'version': '2.4.18'},
+                {'arch': 'i686',
+                 'name': 'mod_ldap',
+                 'nvr': 'mod_ldap-2.4.18-1.fc22',
+                 'release': '1.fc22',
+                 'version': '2.4.18'},
+                {'arch': 'i686',
+                 'name': 'mod_ssl',
+                 'nvr': 'mod_ssl-2.4.18-1.fc22',
+                 'release': '1.fc22',
+                 'version': '2.4.18'}]
+
+    def listBuilds(self, *args, **kwargs):
+        return [
+            {'build_id': 720222,
+             'name': 'httpd',
+             'nvr': 'httpd-2.4.18-2.fc24',
+             'release': '2.fc24',
+             'version': '2.4.18'},
+            {'build_id': 708769,
+             'name': 'httpd',
+             'nvr': 'httpd-2.4.18-1.fc22',
+             'release': '1.fc22',
+             'version': '2.4.18'},
+            {'build_id': 708711,
+             'name': 'httpd',
+             'nvr': 'httpd-2.4.18-1.fc23',
+             'release': '1.fc23',
+             'version': '2.4.18'},
+            {'build_id': 705335,
+             'name': 'httpd',
+             'nvr': 'httpd-2.4.18-1.fc24',
+             'release': '1.fc24',
+             'version': '2.4.18'},
+            {'build_id': 704434,
+             'name': 'httpd',
+             'nvr': 'httpd-2.4.17-4.fc24',
+             'release': '4.fc24',
+             'version': '2.4.17'},
+            {'build_id': 704433,
+             'name': 'httpd',
+             'nvr': 'httpd-2.4.17-4.fc23',
+             'release': '4.fc23',
+             'version': '2.4.17'},
+        ]
+
+
+class SelectRpmsFromABuildTest(unittest.TestCase):
+    """Test case for select_rpms_from_a_build"""
+
+    def assert_rpms(self, rpms):
+        for item in rpms:
+            self.assertTrue(item['arch'] in ['i686', 'x86_64'])
+            self.assertTrue(item['name'] in ('httpd', 'httpd-debuginfo'))
+
+    @patch('__main__.Brew.listRPMs')
+    @patch('__main__.global_config', new=MockGlobalConfig)
+    def test_select_rpms_from_all_arches(self, mock_listRPMs):
+        mock_listRPMs.return_value = [
+            {'arch': 'i686',
+             'name': 'httpd-debuginfo',
+             'release': '1.fc22',
+             'version': '2.4.18'},
+            {'arch': 'i686',
+             'name': 'httpd',
+             'release': '1.fc22',
+             'version': '2.4.18'},
+            {'arch': 'x86_64',
+             'name': 'httpd-debuginfo',
+             'release': '1.fc22',
+             'version': '2.4.18'},
+            {'arch': 'x86_64',
+             'name': 'httpd',
+             'release': '1.fc22',
+             'version': '2.4.18'},
+            ]
+
+        session = get_session()
+        rpms = session.select_rpms_from_a_build(1, 'httpd')
+        self.assert_rpms(rpms)
+
+    @patch('__main__.Brew.listRPMs')
+    @patch('__main__.global_config', new=MockGlobalConfig)
+    def test_select_rpms_from_one_arch(self, mock_listRPMs):
+        mock_listRPMs.return_value = [
+            {'arch': 'i686',
+             'name': 'httpd-debuginfo',
+             'release': '1.fc22',
+             'version': '2.4.18'},
+            {'arch': 'i686',
+             'name': 'httpd',
+             'release': '1.fc22',
+             'version': '2.4.18'},
+            ]
+
+        session = get_session()
+        rpms = session.select_rpms_from_a_build(1, 'httpd')
+        self.assert_rpms(rpms)
+
+
+class GetPackageLatestBuildTest(unittest.TestCase):
+    """Test case for get_package_latest_build"""
+
+    @patch('__main__.global_config', new=MockGlobalConfig)
+    @patch('koji.ClientSession', new=MockKojiClientSession)
+    def test_get_latest_one(self):
+        session = get_session()
+        build = session.get_package_latest_build('httpd', 'fc23')
+        self.assertEquals('httpd-2.4.18-1.fc23', build['nvr'])
+
+    @patch('__main__.global_config', new=MockGlobalConfig)
+    @patch('koji.ClientSession', new=MockKojiClientSession)
+    def test_fail_to_find_latest_build(self):
+        session = get_session()
+        latest_build = session.get_package_latest_build('httpd', 'xxxx')
+        self.assertEquals(None, latest_build)
+
+
+class BrewListRPMsTest(unittest.TestCase):
+    """Test case for Brew.listRPMs"""
+
+    @patch('__main__.global_config', new=MockGlobalConfig)
+    @patch('koji.ClientSession', new=MockKojiClientSession)
+    def test_select_specific_rpms(self):
+        session = get_session()
+        selector = lambda rpm: rpm['name'].startswith('httpd')
+        rpms = session.listRPMs(buildID=1000, selector=selector)
+        self.assertTrue(
+            len(rpms) > 0,
+            'More than one rpms should be selected. But, it\'s empty.')
+        for rpm in rpms:
+            self.assertTrue(rpm['name'] in ('httpd', 'httpd-debuginfo'),
+                            '{0} should not be selected'.format(rpm['name']))
+
+
+class FindLocalDebuginfoRPMTest(unittest.TestCase):
+    """Test case for find_local_debuginfo_rpm"""
+
+    def setUp(self):
+        # FIXME: is it possible to patch glob or the underlying methods glob
+        # depends on
+        self.test_dir = './test_dir'
+        os.makedirs(self.test_dir)
+        os.system('touch {0}'.format(
+            os.path.join(self.test_dir, fake_debuginfo_rpm_file)))
+        os.system('touch {0}'.format(
+            os.path.join(self.test_dir,
+                         fake_debuginfo_rpm_file.replace('foo', 'another'))))
+
+    def tearDown(self):
+        shutil.rmtree(self.test_dir)
+
+    def test_find_debuginfo_rpm(self):
+        debuginfo_rpm = find_local_debuginfo_rpm(
+            os.path.join(self.test_dir, fake_rpm_file))
+
+        expected_debuginfo_rpm_file = os.path.abspath(
+            os.path.join(self.test_dir,
+                         fake_debuginfo_rpm_file))
+        self.assertEquals(expected_debuginfo_rpm_file, debuginfo_rpm)
+
+    def test_no_suitable_debuginfo_rpm(self):
+        debuginfo_rpm = find_local_debuginfo_rpm(
+            os.path.join(self.test_dir, 'abc-0.1-1.i686.rpm'))
+        self.assertEquals(None, debuginfo_rpm)
+
+
+if invoked_from_cmd:
+    unittest.main()
-- 
2.5.0