From ed4012f0e39f1dbec0113eee066207cf71abbe18 Mon Sep 17 00:00:00 2001 From: Jiri Popelka Date: Nov 15 2023 10:07:00 +0000 Subject: Project has been moved to GitHub https://github.com/fedora-ci/rpmdeplint --- diff --git a/.gitignore b/.gitignore deleted file mode 100644 index 415b161..0000000 --- a/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -*.pyc -/rpmdeplint.egg-info/ -/build/ diff --git a/COPYING b/COPYING deleted file mode 100644 index d159169..0000000 --- a/COPYING +++ /dev/null @@ -1,339 +0,0 @@ - GNU GENERAL PUBLIC LICENSE - Version 2, June 1991 - - Copyright (C) 1989, 1991 Free Software Foundation, Inc., - 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - Everyone is permitted to copy and distribute verbatim copies - of this license document, but changing it is not allowed. - - Preamble - - The licenses for most software are designed to take away your -freedom to share and change it. By contrast, the GNU General Public -License is intended to guarantee your freedom to share and change free -software--to make sure the software is free for all its users. This -General Public License applies to most of the Free Software -Foundation's software and to any other program whose authors commit to -using it. (Some other Free Software Foundation software is covered by -the GNU Lesser General Public License instead.) You can apply it to -your programs, too. - - When we speak of free software, we are referring to freedom, not -price. Our General Public Licenses are designed to make sure that you -have the freedom to distribute copies of free software (and charge for -this service if you wish), that you receive source code or can get it -if you want it, that you can change the software or use pieces of it -in new free programs; and that you know you can do these things. - - To protect your rights, we need to make restrictions that forbid -anyone to deny you these rights or to ask you to surrender the rights. -These restrictions translate to certain responsibilities for you if you -distribute copies of the software, or if you modify it. - - For example, if you distribute copies of such a program, whether -gratis or for a fee, you must give the recipients all the rights that -you have. You must make sure that they, too, receive or can get the -source code. And you must show them these terms so they know their -rights. - - We protect your rights with two steps: (1) copyright the software, and -(2) offer you this license which gives you legal permission to copy, -distribute and/or modify the software. - - Also, for each author's protection and ours, we want to make certain -that everyone understands that there is no warranty for this free -software. If the software is modified by someone else and passed on, we -want its recipients to know that what they have is not the original, so -that any problems introduced by others will not reflect on the original -authors' reputations. - - Finally, any free program is threatened constantly by software -patents. We wish to avoid the danger that redistributors of a free -program will individually obtain patent licenses, in effect making the -program proprietary. To prevent this, we have made it clear that any -patent must be licensed for everyone's free use or not licensed at all. - - The precise terms and conditions for copying, distribution and -modification follow. - - GNU GENERAL PUBLIC LICENSE - TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION - - 0. This License applies to any program or other work which contains -a notice placed by the copyright holder saying it may be distributed -under the terms of this General Public License. The "Program", below, -refers to any such program or work, and a "work based on the Program" -means either the Program or any derivative work under copyright law: -that is to say, a work containing the Program or a portion of it, -either verbatim or with modifications and/or translated into another -language. (Hereinafter, translation is included without limitation in -the term "modification".) Each licensee is addressed as "you". - -Activities other than copying, distribution and modification are not -covered by this License; they are outside its scope. The act of -running the Program is not restricted, and the output from the Program -is covered only if its contents constitute a work based on the -Program (independent of having been made by running the Program). -Whether that is true depends on what the Program does. - - 1. You may copy and distribute verbatim copies of the Program's -source code as you receive it, in any medium, provided that you -conspicuously and appropriately publish on each copy an appropriate -copyright notice and disclaimer of warranty; keep intact all the -notices that refer to this License and to the absence of any warranty; -and give any other recipients of the Program a copy of this License -along with the Program. - -You may charge a fee for the physical act of transferring a copy, and -you may at your option offer warranty protection in exchange for a fee. - - 2. You may modify your copy or copies of the Program or any portion -of it, thus forming a work based on the Program, and copy and -distribute such modifications or work under the terms of Section 1 -above, provided that you also meet all of these conditions: - - a) You must cause the modified files to carry prominent notices - stating that you changed the files and the date of any change. - - b) You must cause any work that you distribute or publish, that in - whole or in part contains or is derived from the Program or any - part thereof, to be licensed as a whole at no charge to all third - parties under the terms of this License. - - c) If the modified program normally reads commands interactively - when run, you must cause it, when started running for such - interactive use in the most ordinary way, to print or display an - announcement including an appropriate copyright notice and a - notice that there is no warranty (or else, saying that you provide - a warranty) and that users may redistribute the program under - these conditions, and telling the user how to view a copy of this - License. (Exception: if the Program itself is interactive but - does not normally print such an announcement, your work based on - the Program is not required to print an announcement.) - -These requirements apply to the modified work as a whole. If -identifiable sections of that work are not derived from the Program, -and can be reasonably considered independent and separate works in -themselves, then this License, and its terms, do not apply to those -sections when you distribute them as separate works. But when you -distribute the same sections as part of a whole which is a work based -on the Program, the distribution of the whole must be on the terms of -this License, whose permissions for other licensees extend to the -entire whole, and thus to each and every part regardless of who wrote it. - -Thus, it is not the intent of this section to claim rights or contest -your rights to work written entirely by you; rather, the intent is to -exercise the right to control the distribution of derivative or -collective works based on the Program. - -In addition, mere aggregation of another work not based on the Program -with the Program (or with a work based on the Program) on a volume of -a storage or distribution medium does not bring the other work under -the scope of this License. - - 3. You may copy and distribute the Program (or a work based on it, -under Section 2) in object code or executable form under the terms of -Sections 1 and 2 above provided that you also do one of the following: - - a) Accompany it with the complete corresponding machine-readable - source code, which must be distributed under the terms of Sections - 1 and 2 above on a medium customarily used for software interchange; or, - - b) Accompany it with a written offer, valid for at least three - years, to give any third party, for a charge no more than your - cost of physically performing source distribution, a complete - machine-readable copy of the corresponding source code, to be - distributed under the terms of Sections 1 and 2 above on a medium - customarily used for software interchange; or, - - c) Accompany it with the information you received as to the offer - to distribute corresponding source code. (This alternative is - allowed only for noncommercial distribution and only if you - received the program in object code or executable form with such - an offer, in accord with Subsection b above.) - -The source code for a work means the preferred form of the work for -making modifications to it. For an executable work, complete source -code means all the source code for all modules it contains, plus any -associated interface definition files, plus the scripts used to -control compilation and installation of the executable. However, as a -special exception, the source code distributed need not include -anything that is normally distributed (in either source or binary -form) with the major components (compiler, kernel, and so on) of the -operating system on which the executable runs, unless that component -itself accompanies the executable. - -If distribution of executable or object code is made by offering -access to copy from a designated place, then offering equivalent -access to copy the source code from the same place counts as -distribution of the source code, even though third parties are not -compelled to copy the source along with the object code. - - 4. You may not copy, modify, sublicense, or distribute the Program -except as expressly provided under this License. Any attempt -otherwise to copy, modify, sublicense or distribute the Program is -void, and will automatically terminate your rights under this License. -However, parties who have received copies, or rights, from you under -this License will not have their licenses terminated so long as such -parties remain in full compliance. - - 5. You are not required to accept this License, since you have not -signed it. However, nothing else grants you permission to modify or -distribute the Program or its derivative works. These actions are -prohibited by law if you do not accept this License. Therefore, by -modifying or distributing the Program (or any work based on the -Program), you indicate your acceptance of this License to do so, and -all its terms and conditions for copying, distributing or modifying -the Program or works based on it. - - 6. Each time you redistribute the Program (or any work based on the -Program), the recipient automatically receives a license from the -original licensor to copy, distribute or modify the Program subject to -these terms and conditions. You may not impose any further -restrictions on the recipients' exercise of the rights granted herein. -You are not responsible for enforcing compliance by third parties to -this License. - - 7. If, as a consequence of a court judgment or allegation of patent -infringement or for any other reason (not limited to patent issues), -conditions are imposed on you (whether by court order, agreement or -otherwise) that contradict the conditions of this License, they do not -excuse you from the conditions of this License. If you cannot -distribute so as to satisfy simultaneously your obligations under this -License and any other pertinent obligations, then as a consequence you -may not distribute the Program at all. For example, if a patent -license would not permit royalty-free redistribution of the Program by -all those who receive copies directly or indirectly through you, then -the only way you could satisfy both it and this License would be to -refrain entirely from distribution of the Program. - -If any portion of this section is held invalid or unenforceable under -any particular circumstance, the balance of the section is intended to -apply and the section as a whole is intended to apply in other -circumstances. - -It is not the purpose of this section to induce you to infringe any -patents or other property right claims or to contest validity of any -such claims; this section has the sole purpose of protecting the -integrity of the free software distribution system, which is -implemented by public license practices. Many people have made -generous contributions to the wide range of software distributed -through that system in reliance on consistent application of that -system; it is up to the author/donor to decide if he or she is willing -to distribute software through any other system and a licensee cannot -impose that choice. - -This section is intended to make thoroughly clear what is believed to -be a consequence of the rest of this License. - - 8. If the distribution and/or use of the Program is restricted in -certain countries either by patents or by copyrighted interfaces, the -original copyright holder who places the Program under this License -may add an explicit geographical distribution limitation excluding -those countries, so that distribution is permitted only in or among -countries not thus excluded. In such case, this License incorporates -the limitation as if written in the body of this License. - - 9. The Free Software Foundation may publish revised and/or new versions -of the General Public License from time to time. Such new versions will -be similar in spirit to the present version, but may differ in detail to -address new problems or concerns. - -Each version is given a distinguishing version number. If the Program -specifies a version number of this License which applies to it and "any -later version", you have the option of following the terms and conditions -either of that version or of any later version published by the Free -Software Foundation. If the Program does not specify a version number of -this License, you may choose any version ever published by the Free Software -Foundation. - - 10. If you wish to incorporate parts of the Program into other free -programs whose distribution conditions are different, write to the author -to ask for permission. For software which is copyrighted by the Free -Software Foundation, write to the Free Software Foundation; we sometimes -make exceptions for this. Our decision will be guided by the two goals -of preserving the free status of all derivatives of our free software and -of promoting the sharing and reuse of software generally. - - NO WARRANTY - - 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY -FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN -OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES -PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED -OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF -MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS -TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE -PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, -REPAIR OR CORRECTION. - - 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING -WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR -REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, -INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING -OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED -TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY -YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER -PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE -POSSIBILITY OF SUCH DAMAGES. - - END OF TERMS AND CONDITIONS - - How to Apply These Terms to Your New Programs - - If you develop a new program, and you want it to be of the greatest -possible use to the public, the best way to achieve this is to make it -free software which everyone can redistribute and change under these terms. - - To do so, attach the following notices to the program. It is safest -to attach them to the start of each source file to most effectively -convey the exclusion of warranty; and each file should have at least -the "copyright" line and a pointer to where the full notice is found. - - - Copyright (C) - - This program is free software; you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation; either version 2 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along - with this program; if not, write to the Free Software Foundation, Inc., - 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. - -Also add information on how to contact you by electronic and paper mail. - -If the program is interactive, make it output a short notice like this -when it starts in an interactive mode: - - Gnomovision version 69, Copyright (C) year name of author - Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. - This is free software, and you are welcome to redistribute it - under certain conditions; type `show c' for details. - -The hypothetical commands `show w' and `show c' should show the appropriate -parts of the General Public License. Of course, the commands you use may -be called something other than `show w' and `show c'; they could even be -mouse-clicks or menu items--whatever suits your program. - -You should also get your employer (if you work as a programmer) or your -school, if any, to sign a "copyright disclaimer" for the program, if -necessary. Here is a sample; alter the names: - - Yoyodyne, Inc., hereby disclaims all copyright interest in the program - `Gnomovision' (which makes passes at compilers) written by James Hacker. - - , 1 April 1989 - Ty Coon, President of Vice - -This General Public License does not permit incorporating your program into -proprietary programs. If your program is a subroutine library, you may -consider it more useful to permit linking proprietary applications with the -library. If this is what you want to do, use the GNU Lesser General -Public License instead of this License. diff --git a/MANIFEST.in b/MANIFEST.in deleted file mode 100644 index e35d7ac..0000000 --- a/MANIFEST.in +++ /dev/null @@ -1,5 +0,0 @@ -include COPYING -recursive-include rpmdeplint/tests/repos * -recursive-include acceptance_tests *.py -recursive-include acceptance_tests/data * -recursive-include docs *.rst *.py diff --git a/README.rst b/README.rst index f2501ac..9786fa3 100644 --- a/README.rst +++ b/README.rst @@ -1,42 +1,4 @@ rpmdeplint ========== -Rpmdeplint is a tool to find errors in RPM packages in the context of their -dependency graph. - -Requirements ------------- - -* Python 2.7 - -External Dependencies ---------------------- - -In order to run the tool, the following pre-requisites need to be installed: - -* rpm, rpm-python -* librepo, python-librepo -* hawkey, python-hawkey - -For development and tests: - -* sphinx -* `rpmfluff `_ -* glibc-devel.i686 and libgcc.i686, for building 32-bit binaries - -Project Links -------------- - -* `Source code on Pagure `__ -* `File a bug `__ - or `view open bugs `__ -* For feedback and discussion join #beaker on irc.freenode.net. -* We use `Gerrit `_ for code review. Patches welcome! -* `Documentation `_ - -Using ------ - -A user guide is provided by the man(1) page shipped with this tool:: - - man rpmdeplint +Project has been moved to `GitHub `. diff --git a/acceptance_tests/conftest.py b/acceptance_tests/conftest.py deleted file mode 100644 index f5ebd09..0000000 --- a/acceptance_tests/conftest.py +++ /dev/null @@ -1,110 +0,0 @@ - -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. - -import os -import os.path -import logging -import wsgiref.simple_server -from threading import Thread -import pytest - -logger = logging.getLogger(__name__) - - -class NonSpewingWSGIRequestHandler(wsgiref.simple_server.WSGIRequestHandler): - - def log_message(self, format, *args): - # The default implementation writes to stderr, which makes a mess in - # the test suite. Use stdlib logging instead so that it can be captured - # with everything else. - logger.info(format, *args) - - -class WSGIServer(Thread): - """ - HTTP server running a WSGI application in its own thread. - - see pytest_localserver.http - """ - - def __init__(self, host='127.0.0.1', port=0, application=None, **kwargs): - self.app = application - self._server = wsgiref.simple_server.make_server(host, port, self.app, - handler_class=NonSpewingWSGIRequestHandler, **kwargs) - self.server_address = self._server.server_address - - super(WSGIServer, self).__init__( - name=self.__class__, - target=self._server.serve_forever) - - def __del__(self): - self.stop() - - def stop(self): - self._server.shutdown() - - @property - def url(self): - host, port = self.server_address - return 'http://%s:%i' % (host, port) - - -class DirServer(WSGIServer): - """ - Small test server which serves directories instead of simple content. - """ - - def __init__(self, host='127.0.0.1', port=0): - super(DirServer, self).__init__(host, port, self) - self.basepath = None - self.num_requests = 0 - - def __call__(self, environ, start_response): - path_info = os.path.normpath(environ['PATH_INFO']) - localpath = os.path.join(self.basepath, path_info.lstrip('/')) - self.num_requests += 1 - - if not os.path.exists(localpath): - start_response('404 Not Found', []) - return [] - if environ['REQUEST_METHOD'] in ('GET', 'HEAD'): - try: - listing = '\n'.join(os.listdir(localpath)) - start_response('200 OK', [('Content-Length', str(len(listing)))]) - return [listing] - except OSError: - start_response('200 OK', [('Content-Length', str(os.path.getsize(localpath)))]) - return wsgiref.util.FileWrapper(open(localpath, 'rb')) - else: - start_response('405 Method Not Allowed', []) - return [] - - -@pytest.fixture -def dir_server(request): - """ - Defines a HTTP test server for listing directory contents. - """ - server = DirServer() - server.start() - request.addfinalizer(server.stop) - return server - - -@pytest.fixture(autouse=True) -def rpmfluff_leak_finder(request): - """ - Adds a finalizer which will fail any test that has left behind - a test-rpmbuild-* directory. These are created by rpmfluff in order to - build dummy packages. Each test case is supposed to clean up all the - rpmfluff build directories it has created. If a test fails to do that, the - directory can pollute subsequent tests because rpmfluff will silently - re-use whatever is in the directory (even if it's wrong). - """ - def _finalize(): - if any(entry.startswith('test-rpmbuild-') for entry in os.listdir('.')): - raise AssertionError('Test failed to clean up rpmfluff build directory') - request.addfinalizer(_finalize) diff --git a/acceptance_tests/data/b-0.1-1.i386.rpm b/acceptance_tests/data/b-0.1-1.i386.rpm deleted file mode 100644 index 9ed61b0..0000000 Binary files a/acceptance_tests/data/b-0.1-1.i386.rpm and /dev/null differ diff --git a/acceptance_tests/data_setup.py b/acceptance_tests/data_setup.py deleted file mode 100644 index 707b8f3..0000000 --- a/acceptance_tests/data_setup.py +++ /dev/null @@ -1,32 +0,0 @@ - -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. - -import os.path -import os -import subprocess - - -def run_rpmdeplint(args, **kwargs): - env = os.environ - env['PYTHONBUFFERED'] = '1' - env['PYTHONIOENCODING'] = 'UTF-8' - - p = subprocess.Popen(args, - stdout=subprocess.PIPE, - stdin=open('/dev/null'), - stderr=subprocess.PIPE, - env=env, - **kwargs) - - max_output = 10240 - out = p.stdout.read(max_output).decode('UTF-8') - if len(out) == max_output: - raise RuntimeError('Output size limit exceeded when invoking {}:\n{}'.format(args, out)) - err = p.stderr.read(max_output).decode('UTF-8') - if len(err) == max_output: - raise RuntimeError('Stderr size limit exceeded when invoking {}:\n{}'.format(args, out)) - p.wait() - return (p.returncode, out, err) diff --git a/acceptance_tests/test_check.py b/acceptance_tests/test_check.py deleted file mode 100644 index 67239a3..0000000 --- a/acceptance_tests/test_check.py +++ /dev/null @@ -1,272 +0,0 @@ - -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. - -import os -import glob -import time -import shutil -import rpmfluff -from data_setup import run_rpmdeplint -from rpmdeplint.repodata import cache_base_path - - -def expected_cache_path(repodir, suffix, old=False): - """ - For the test repo located in *repodir*, return the path within the - rpmdeplint cache where we expect the metadata file with given suffix - to appear after rpmdeplint has downloaded it. - """ - filename, = [filename for filename in os.listdir(os.path.join(repodir, 'repodata')) - if filename.endswith(suffix)] - checksum = filename.split('-', 1)[0] - if old: - return os.path.join(cache_base_path(), checksum[:1], checksum[1:], filename) - return os.path.join(cache_base_path(), checksum[:1], checksum[1:]) - - -def test_finds_all_problems(request, dir_server): - p_newer = rpmfluff.SimpleRpmBuild('a', '5.0', '1', ['i386']) - p_with_content = rpmfluff.SimpleRpmBuild('b', '0.1', '1', ['i386']) - p_with_content.add_installed_file(installPath='usr/share/thing', - sourceFile=rpmfluff.SourceFile('thing', 'content\n')) - p_old_soname = rpmfluff.SimpleRpmBuild('c', '0.1', '1', ['i386']) - p_old_soname.add_provides('libfoo.so.4') - p_depending = rpmfluff.SimpleRpmBuild('d', '0.1', '1', ['i386']) - p_depending.add_requires('libfoo.so.4') - repo_packages = [p_newer, p_with_content, p_old_soname, p_depending] - baserepo = rpmfluff.YumRepoBuild(repo_packages) - baserepo.make('i386') - dir_server.basepath = baserepo.repoDir - - p_older = rpmfluff.SimpleRpmBuild('a', '4.0', '1', ['i386']) - p_older.make() - p_broken = rpmfluff.SimpleRpmBuild('e', '1.0', '1', ['i386']) - p_broken.add_requires('doesnotexist') - p_broken.make() - p_with_different_content = rpmfluff.SimpleRpmBuild('f', '0.1', '1', ['i386']) - p_with_different_content.add_installed_file(installPath='usr/share/thing', - sourceFile=rpmfluff.SourceFile('thing', 'different content\n')) - p_with_different_content.make() - p_soname_changed = rpmfluff.SimpleRpmBuild('c', '0.2', '1', ['i386']) - p_soname_changed.add_provides('libfoo.so.5') - p_soname_changed.make() - test_packages = [p_older, p_broken, p_with_different_content, p_soname_changed] - - def cleanUp(): - shutil.rmtree(baserepo.repoDir) - for p in repo_packages + test_packages: - shutil.rmtree(p.get_base_dir()) - request.addfinalizer(cleanUp) - - exitcode, out, err = run_rpmdeplint( - ['rpmdeplint', 'check', '--repo=base,{}'.format(dir_server.url)] + - [p.get_built_rpm('i386') for p in test_packages]) - assert exitcode == 3 - assert err == ('Problems with dependency set:\n' - 'nothing provides doesnotexist needed by e-1.0-1.i386\n' - 'Dependency problems with repos:\n' - 'package d-0.1-1.i386 requires libfoo.so.4, but none of the providers can be installed\n' - 'Undeclared file conflicts:\n' - 'f-0.1-1.i386 provides /usr/share/thing which is also provided by b-0.1-1.i386\n' - 'Upgrade problems:\n' - 'a-4.0-1.i386 would be upgraded by a-5.0-1.i386 from repo base\n') - - -def test_guesses_arch_when_combined_with_noarch_package(request, dir_server): - # A more realistic case is an archful package with a noarch subpackage, - # but rpmfluff currently can't produce that. - p_noarch = rpmfluff.SimpleRpmBuild('a', '0.1', '1', ['noarch']) - p_noarch.add_requires('libfoo.so.4') - p_noarch.make() - p_archful = rpmfluff.SimpleRpmBuild('b', '0.1', '1', ['i386']) - p_archful.add_requires('libfoo.so.4') - p_archful.make() - - baserepo = rpmfluff.YumRepoBuild([]) - baserepo.make('i386') - dir_server.basepath = baserepo.repoDir - - def cleanUp(): - shutil.rmtree(baserepo.repoDir) - shutil.rmtree(p_noarch.get_base_dir()) - shutil.rmtree(p_archful.get_base_dir()) - request.addfinalizer(cleanUp) - - exitcode, out, err = run_rpmdeplint([ - 'rpmdeplint', 'check', '--repo=base,{}'.format(dir_server.url), - p_noarch.get_built_rpm('noarch'), p_archful.get_built_rpm('i386') - ]) - assert exitcode == 3, err - assert err == ('Problems with dependency set:\n' - 'nothing provides libfoo.so.4 needed by a-0.1-1.noarch\n' - 'nothing provides libfoo.so.4 needed by b-0.1-1.i386\n') - - -def test_cache_is_used_when_available(request, dir_server): - p1 = rpmfluff.SimpleRpmBuild('a', '0.1', '1', ['i386']) - baserepo = rpmfluff.YumRepoBuild((p1,)) - baserepo.make('i386') - dir_server.basepath = baserepo.repoDir - - def cleanUp(): - shutil.rmtree(baserepo.repoDir) - shutil.rmtree(p1.get_base_dir()) - request.addfinalizer(cleanUp) - - # Assuming cache is cleaned first - assert dir_server.num_requests == 0 - - run_rpmdeplint(['rpmdeplint', 'check', '--repo=base,{}'.format( - dir_server.url), p1.get_built_rpm('i386')]) - - cache_path = expected_cache_path(baserepo.repoDir, 'primary.xml.gz') - assert os.path.exists(cache_path) - original_cache_mtime = os.path.getmtime(cache_path) - - # A single run of rpmdeplint with a clean cache should expect network - # requests for - repomd.xml, primary.xml.gz and filelists.xml.gz. Requiring - # a total of 3 - assert dir_server.num_requests == 3 - - run_rpmdeplint(['rpmdeplint', 'check', '--repo=base,{}'.format( - dir_server.url), p1.get_built_rpm('i386')]) - - new_cache_mtime = os.path.getmtime(cache_path) - assert new_cache_mtime > original_cache_mtime - - # Executing 2 subprocesses should expect 4 requests if repodata cache is - # functioning correctly. A single request for each file in the repo - # - repomd.xml, primary.xml.gz, filelists.xml.gz, with an additional - # request from the second process checking metadata. The additional - # single request shows that the files are skipped in the second process - assert dir_server.num_requests == 4 - - -def test_cache_doesnt_grow_unboundedly(request, dir_server): - os.environ['RPMDEPLINT_EXPIRY_SECONDS'] = '1' - - p1 = rpmfluff.SimpleRpmBuild('a', '0.1', '1', ['i386']) - firstrepo = rpmfluff.YumRepoBuild((p1, )) - firstrepo.make('i386') - dir_server.basepath = firstrepo.repoDir - - def cleanup(): - shutil.rmtree(firstrepo.repoDir) - shutil.rmtree(p1.get_base_dir()) - request.addfinalizer(cleanup) - - exitcode, out, err = run_rpmdeplint(['rpmdeplint', 'check', - '--repo=base,{}'.format(dir_server.url), - p1.get_built_rpm('i386')]) - assert exitcode == 0 - - first_primary_cache_path = expected_cache_path(firstrepo.repoDir, 'primary.xml.gz') - first_filelists_cache_path = expected_cache_path(firstrepo.repoDir, 'filelists.xml.gz') - - assert os.path.exists(first_primary_cache_path) - assert os.path.exists(first_filelists_cache_path) - - p2 = rpmfluff.SimpleRpmBuild('b', '0.1', '1', ['i386']) - secondrepo = rpmfluff.YumRepoBuild((p2, )) - secondrepo.make('i386') - dir_server.basepath = secondrepo.repoDir - - def cleanup2(): - shutil.rmtree(secondrepo.repoDir) - shutil.rmtree(p2.get_base_dir()) - request.addfinalizer(cleanup2) - - # ensure time period of cache has expired - time.sleep(2) - - exitcode, out, err = run_rpmdeplint(['rpmdeplint', 'check', - '--repo=base,{}'.format(dir_server.url), - p2.get_built_rpm('i386')]) - assert exitcode == 0 - - second_primary_cache_path = expected_cache_path(secondrepo.repoDir, 'primary.xml.gz') - second_filelists_cache_path = expected_cache_path(secondrepo.repoDir, 'filelists.xml.gz') - - # Ensure the cache only has files from the second one - assert not os.path.exists(first_primary_cache_path) - assert not os.path.exists(first_filelists_cache_path) - assert os.path.exists(second_primary_cache_path) - assert os.path.exists(second_filelists_cache_path) - - -def test_migrates_old_cache_layout(request, dir_server): - p1 = rpmfluff.SimpleRpmBuild('a', '0.1', '1', ['i386']) - repo = rpmfluff.YumRepoBuild([p1]) - repo.make('i386') - dir_server.basepath = repo.repoDir - - def cleanUp(): - shutil.rmtree(repo.repoDir) - shutil.rmtree(p1.get_base_dir()) - request.addfinalizer(cleanUp) - - old_cache_path = expected_cache_path(repo.repoDir, 'primary.xml.gz', old=True) - new_cache_path = expected_cache_path(repo.repoDir, 'primary.xml.gz') - - # Simulate the old cache path left over from an older version of rpmdeplint - os.makedirs(os.path.dirname(old_cache_path)) - with open(old_cache_path, 'w') as f: - f.write('lol\n') - - exitcode, out, err = run_rpmdeplint(['rpmdeplint', 'check', - '--repo=base,{}'.format(dir_server.url), - p1.get_built_rpm('i386')]) - assert exitcode == 0 - assert err == '' - assert not os.path.exists(old_cache_path) - assert os.path.isfile(new_cache_path) - - -def test_prints_error_on_repo_download_failure(request, dir_server): - # Specifically we don't want an unhandled exception, because that triggers abrt. - test_tool_rpm = rpmfluff.SimpleRpmBuild('test-tool', '10', '3.el6', ['x86_64']) - test_tool_rpm.make() - - def cleanUp(): - shutil.rmtree(test_tool_rpm.get_base_dir()) - request.addfinalizer(cleanUp) - - exitcode, out, err = run_rpmdeplint([ - 'rpmdeplint', 'check', '--repo=broken,http://notexist.example/', - test_tool_rpm.get_built_rpm('x86_64') - ]) - - assert exitcode == 1 - assert err.startswith('Failed to download repodata') - assert 'Traceback' not in err - - -def test_prints_error_on_repodata_file_download_failure(request, dir_server): - # Similar to the above, but in this case repomd.xml works but - # primary.xml.gz is broken. We test this case specifically, because the - # code paths for fetching repomd.xml and the other repodata files are - # separate. - p1 = rpmfluff.SimpleRpmBuild('test-tool', '10', '3.el6', ['x86_64']) - p1.add_requires('unsatisfied') - repo = rpmfluff.YumRepoBuild([p1]) - repo.make('x86_64') - for repodata_filename in os.listdir(os.path.join(repo.repoDir, 'repodata')): - if 'primary' in repodata_filename: - os.unlink(os.path.join(repo.repoDir, 'repodata', repodata_filename)) - dir_server.basepath = repo.repoDir - - def cleanUp(): - shutil.rmtree(repo.repoDir) - shutil.rmtree(p1.get_base_dir()) - request.addfinalizer(cleanUp) - - exitcode, out, err = run_rpmdeplint(['rpmdeplint', 'check', - '--repo=base,{}'.format(dir_server.url), p1.get_built_rpm('x86_64')]) - - assert exitcode == 1 - assert err.startswith('Failed to download repodata') - assert '404' in err - assert 'Traceback' not in err diff --git a/acceptance_tests/test_check_conflicts.py b/acceptance_tests/test_check_conflicts.py deleted file mode 100644 index c1fa7c7..0000000 --- a/acceptance_tests/test_check_conflicts.py +++ /dev/null @@ -1,413 +0,0 @@ - -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. - -import shutil -import subprocess -import rpm -import rpmfluff -import os.path -from data_setup import run_rpmdeplint - - -def test_finds_undeclared_file_conflict(request, dir_server): - p2 = rpmfluff.SimpleRpmBuild('b', '0.1', '1', ['i386']) - p2.add_installed_file(installPath='usr/share/thing', - sourceFile=rpmfluff.SourceFile('thing', 'content\n')) - baserepo = rpmfluff.YumRepoBuild([p2]) - baserepo.make('i386') - dir_server.basepath = baserepo.repoDir - - p1 = rpmfluff.SimpleRpmBuild('a', '0.1', '1', ['i386']) - p1.add_installed_file(installPath='usr/share/thing', - sourceFile=rpmfluff.SourceFile('thing', 'different content\n')) - p1.make() - - def cleanUp(): - shutil.rmtree(baserepo.repoDir) - shutil.rmtree(p2.get_base_dir()) - shutil.rmtree(p1.get_base_dir()) - request.addfinalizer(cleanUp) - - exitcode, out, err = run_rpmdeplint(['rpmdeplint', 'check-conflicts', - '--repo=base,{}'.format(dir_server.url), - p1.get_built_rpm('i386')]) - assert exitcode == 3 - assert err == ('Undeclared file conflicts:\n' - 'a-0.1-1.i386 provides /usr/share/thing which is also provided by b-0.1-1.i386\n') - - -def test_finds_undeclared_file_conflict_with_repo_on_local_filesystem(request): - p2 = rpmfluff.SimpleRpmBuild('b', '0.1', '1', ['i386']) - p2.add_installed_file(installPath='usr/share/thing', - sourceFile=rpmfluff.SourceFile('thing', 'content\n')) - baserepo = rpmfluff.YumRepoBuild([p2]) - baserepo.make('i386') - - p1 = rpmfluff.SimpleRpmBuild('a', '0.1', '1', ['i386']) - p1.add_installed_file(installPath='usr/share/thing', - sourceFile=rpmfluff.SourceFile('thing', 'different content\n')) - p1.make() - - def cleanUp(): - shutil.rmtree(baserepo.repoDir) - shutil.rmtree(p2.get_base_dir()) - shutil.rmtree(p1.get_base_dir()) - request.addfinalizer(cleanUp) - - exitcode, out, err = run_rpmdeplint(['rpmdeplint', 'check-conflicts', - '--repo=base,{}'.format(baserepo.repoDir), - p1.get_built_rpm('i386')]) - assert exitcode == 3 - assert err == ('Undeclared file conflicts:\n' - 'a-0.1-1.i386 provides /usr/share/thing which is also provided by b-0.1-1.i386\n') - - -def test_package_does_not_conflict_with_earlier_version_of_itself(request, dir_server): - p2 = rpmfluff.SimpleRpmBuild('a', '0.1', '1', ['i386']) - p2.add_installed_file(installPath='usr/share/thing', - sourceFile=rpmfluff.SourceFile('thing', 'content\n')) - baserepo = rpmfluff.YumRepoBuild([p2]) - baserepo.make('i386') - dir_server.basepath = baserepo.repoDir - - p1 = rpmfluff.SimpleRpmBuild('a', '0.1', '2', ['i386']) - p1.add_installed_file(installPath='usr/share/thing', - sourceFile=rpmfluff.SourceFile('thing', 'different content\n')) - p1.make() - - def cleanUp(): - shutil.rmtree(baserepo.repoDir) - shutil.rmtree(p2.get_base_dir()) - shutil.rmtree(p1.get_base_dir()) - request.addfinalizer(cleanUp) - - exitcode, out, err = run_rpmdeplint(['rpmdeplint', 'check-conflicts', - '--repo=base,{}'.format(dir_server.url), - p1.get_built_rpm('i386')]) - assert exitcode == 0 - - -def test_conflict_is_ignored_for_rpm_level_conflicts(request, dir_server): - # Having two packages intentionally conflict, with a corresponding - # Conflicts declaration at the RPM level, is discouraged by Fedora but - # sometimes necessary. - # https://fedoraproject.org/wiki/Packaging:Conflicts - p2 = rpmfluff.SimpleRpmBuild('mysql', '0.1', '1', ['i386']) - p2.add_installed_file(installPath='usr/bin/mysql', - sourceFile=rpmfluff.SourceFile('mysql', b'\177ELF-mysql', encoding=None)) - baserepo = rpmfluff.YumRepoBuild([p2]) - baserepo.make('i386') - dir_server.basepath = baserepo.repoDir - - p1 = rpmfluff.SimpleRpmBuild('mariadb', '0.1', '1', ['i386']) - p1.add_conflicts('mysql') - p1.add_installed_file(installPath='usr/bin/mysql', - sourceFile=rpmfluff.SourceFile('mysql', b'\177ELF-mariadb', encoding=None)) - p1.make() - - def cleanUp(): - shutil.rmtree(baserepo.repoDir) - shutil.rmtree(p2.get_base_dir()) - shutil.rmtree(p1.get_base_dir()) - request.addfinalizer(cleanUp) - - exitcode, out, err = run_rpmdeplint(['rpmdeplint', 'check-conflicts', - '--repo=base,{}'.format(dir_server.url), - p1.get_built_rpm('i386')]) - assert exitcode == 0 - - -def test_conflict_is_ignored_if_files_match(request, dir_server): - # RPM allows multiple packages to own the same file if the file compares equal - # according to rpmfilesCompare() in both packages -- that is, the same - # owner, group, mode, and contents. - p2 = rpmfluff.SimpleRpmBuild('b', '0.1', '1', ['i386']) - p2.add_installed_file(installPath='usr/share/thing', - sourceFile=rpmfluff.SourceFile('thing', 'same content\n')) - baserepo = rpmfluff.YumRepoBuild([p2]) - baserepo.make('i386') - dir_server.basepath = baserepo.repoDir - - p1 = rpmfluff.SimpleRpmBuild('a', '0.1', '1', ['i386']) - p1.add_installed_file(installPath='usr/share/thing', - sourceFile=rpmfluff.SourceFile('thing', 'same content\n')) - p1.make() - - def cleanUp(): - shutil.rmtree(baserepo.repoDir) - shutil.rmtree(p2.get_base_dir()) - shutil.rmtree(p1.get_base_dir()) - request.addfinalizer(cleanUp) - - exitcode, out, err = run_rpmdeplint(['rpmdeplint', 'check-conflicts', - '--repo=base,{}'.format(dir_server.url), - p1.get_built_rpm('i386')]) - assert exitcode == 0 - - -def test_conflict_not_ignored_if_contents_match_but_perms_differ(request, dir_server): - basepackage = rpmfluff.SimpleRpmBuild('b', '0.1', '1', ['i386']) - basepackage.add_installed_file(installPath='usr/share/thing', - sourceFile=rpmfluff.SourceFile('thing', 'content\n')) - baserepo = rpmfluff.YumRepoBuild([basepackage]) - baserepo.make('i386') - dir_server.basepath = baserepo.repoDir - - different_mode = rpmfluff.SimpleRpmBuild('x', '0.1', '1', ['i386']) - different_mode.add_installed_file(installPath='usr/share/thing', - sourceFile=rpmfluff.SourceFile('thing', 'content\n'), - mode='0600') - different_mode.make() - - different_owner = rpmfluff.SimpleRpmBuild('y', '0.1', '1', ['i386']) - different_owner.add_installed_file(installPath='usr/share/thing', - sourceFile=rpmfluff.SourceFile('thing', 'content\n'), - owner='apache') - different_owner.make() - - different_group = rpmfluff.SimpleRpmBuild('z', '0.1', '1', ['i386']) - different_group.add_installed_file(installPath='usr/share/thing', - sourceFile=rpmfluff.SourceFile('thing', 'content\n'), - group='apache') - different_group.make() - - def cleanUp(): - shutil.rmtree(baserepo.repoDir) - shutil.rmtree(basepackage.get_base_dir()) - shutil.rmtree(different_mode.get_base_dir()) - shutil.rmtree(different_owner.get_base_dir()) - shutil.rmtree(different_group.get_base_dir()) - request.addfinalizer(cleanUp) - - exitcode, out, err = run_rpmdeplint(['rpmdeplint', 'check-conflicts', - '--repo=base,{}'.format(dir_server.url), - different_mode.get_built_rpm('i386'), - different_owner.get_built_rpm('i386'), - different_group.get_built_rpm('i386')]) - assert exitcode == 3 - assert err == ('Undeclared file conflicts:\n' - 'x-0.1-1.i386 provides /usr/share/thing which is also provided by b-0.1-1.i386\n' - 'x-0.1-1.i386 provides /usr/share/thing which is also provided by y-0.1-1.i386\n' - 'x-0.1-1.i386 provides /usr/share/thing which is also provided by z-0.1-1.i386\n' - 'y-0.1-1.i386 provides /usr/share/thing which is also provided by b-0.1-1.i386\n' - 'y-0.1-1.i386 provides /usr/share/thing which is also provided by x-0.1-1.i386\n' - 'y-0.1-1.i386 provides /usr/share/thing which is also provided by z-0.1-1.i386\n' - 'z-0.1-1.i386 provides /usr/share/thing which is also provided by b-0.1-1.i386\n' - 'z-0.1-1.i386 provides /usr/share/thing which is also provided by x-0.1-1.i386\n' - 'z-0.1-1.i386 provides /usr/share/thing which is also provided by y-0.1-1.i386\n' - ) - - -def test_conflict_is_ignored_if_file_colors_are_different(request, dir_server): - # This is part of RPM's multilib support. If two packages own the same file - # but the file color is different in each, the preferred color wins (and - # there is no conflict). This lets both .i386 and .x86_64 packages own - # /bin/bash while installing only the .x86_64 version. - p2 = rpmfluff.SimpleRpmBuild('a', '0.1', '1', ['i386', 'x86_64']) - p2.add_simple_compilation(installPath='usr/bin/thing') - baserepo = rpmfluff.YumRepoBuild([p2]) - baserepo.make('i386', 'x86_64') - dir_server.basepath = baserepo.repoDir - - p1 = rpmfluff.SimpleRpmBuild('a', '0.2', '1', ['i386', 'x86_64']) - p1.add_simple_compilation(installPath='usr/bin/thing') - p1.make() - - def cleanUp(): - shutil.rmtree(baserepo.repoDir) - shutil.rmtree(p2.get_base_dir()) - shutil.rmtree(p1.get_base_dir()) - request.addfinalizer(cleanUp) - - # Make sure we really have different files with different colors - # (this was surprisingly hard to get right) - rpmheader_32 = p1.get_built_rpm_header('i386') - rpmheader_64 = p1.get_built_rpm_header('x86_64') - if hasattr(rpm, 'files'): # rpm 4.12+ - assert 1 == rpm.files(rpmheader_32)['/usr/bin/thing'].color - assert 2 == rpm.files(rpmheader_64)['/usr/bin/thing'].color - else: # sad old rpm < 4.12 - fi_32 = rpm.fi(rpmheader_32) - while fi_32.FN() != '/usr/bin/thing': - fi_32.next() - assert fi_32.FColor() == 1 - fi_64 = rpm.fi(rpmheader_64) - while fi_64.FN() != '/usr/bin/thing': - fi_64.next() - assert fi_64.FColor() == 2 - - exitcode, out, err = run_rpmdeplint(['rpmdeplint', 'check-conflicts', - '--repo=base,{}'.format(dir_server.url), - p1.get_built_rpm('i386')]) - assert exitcode == 0 - - -# https://bugzilla.redhat.com/show_bug.cgi?id=1353757 -def test_does_not_fail_with_signed_rpms(request, dir_server): - p2 = rpmfluff.SimpleRpmBuild('a', '0.1', '1', ['x86_64']) - # Add an undeclared conflict to make rpmdeplint loading the rpms into a - # transaction. That would usually trigger a rpm signature verification. - p2.add_installed_file(installPath='usr/share/thing', - sourceFile=rpmfluff.SourceFile('thing', 'content\n'), - mode='0600') - baserepo = rpmfluff.YumRepoBuild([p2]) - baserepo.make('x86_64') - dir_server.basepath = baserepo.repoDir - - p1 = os.path.join(os.path.dirname(__file__), 'data', 'b-0.1-1.i386.rpm') - - def cleanUp(): - shutil.rmtree(baserepo.repoDir) - shutil.rmtree(p2.get_base_dir()) - request.addfinalizer(cleanUp) - - exitcode, out, err = run_rpmdeplint(['rpmdeplint', 'check-conflicts', - '--repo=base,{}'.format(dir_server.url), - p1]) - assert exitcode == 3 - assert err == ('Undeclared file conflicts:\n' - 'b-0.1-1.i386 provides /usr/share/thing which is also provided by a-0.1-1.x86_64\n') - -# https://bugzilla.redhat.com/show_bug.cgi?id=1412910 -def test_conflict_is_ignored_if_not_installable_concurrently(request, dir_server): - glib_26 = rpmfluff.SimpleRpmBuild('glib', '2.26', '1.el6', ['i686']) - glib_26.add_devel_subpackage() - glib_26.add_installed_file( - installPath="usr/share/gtk-doc/html/gio/annotation-glossary.html", - sourceFile=rpmfluff.SourceFile('annotation-glossary.html', 'something\n'), - subpackageSuffix='devel') - glib_28 = rpmfluff.SimpleRpmBuild('glib', '2.28', '8.el6', ['i686']) - glib_doc = glib_28.add_subpackage('doc') - glib_doc.add_requires('glib = 2.28-8.el6') - glib_28.add_installed_file( - installPath="usr/share/gtk-doc/html/gio/annotation-glossary.html", - sourceFile=rpmfluff.SourceFile('annotation-glossary.html', 'some other content\n'), - subpackageSuffix='doc') - glib_28.make() - - repo = rpmfluff.YumRepoBuild((glib_26,)) - repo.make('i686') - dir_server.basepath = repo.repoDir - - def cleanUp(): - shutil.rmtree(repo.repoDir) - shutil.rmtree(glib_28.get_base_dir()) - shutil.rmtree(glib_26.get_base_dir()) - request.addfinalizer(cleanUp) - - exitcode, out, err = run_rpmdeplint(['rpmdeplint', 'check-conflicts', - '--repo=base,{}'.format(dir_server.url), - glib_28.get_built_rpm('i686'), - glib_28.get_built_rpm('i686', 'glib-doc')]) - assert exitcode == 0 - assert err == '' - assert out == '' - - -# https://bugzilla.redhat.com/show_bug.cgi?id=1465734 -def test_finds_conflicts_in_installonly_packages(request, dir_server): - kernel1 = rpmfluff.SimpleRpmBuild('kernel-core', '0.1', '1', ['i386']) - kernel1.add_installed_file(installPath='usr/share/licenses/kernel-core/COPYING', - sourceFile=rpmfluff.SourceFile('COPYING', 'content\n')) - # The modern mechanism for telling DNF a package is installonly is to add this virtual provide. - kernel1.add_provides('installonlypkg(kernel)') - baserepo = rpmfluff.YumRepoBuild([kernel1]) - baserepo.make('i386') - dir_server.basepath = baserepo.repoDir - - kernel2 = rpmfluff.SimpleRpmBuild('kernel-core', '0.2', '1', ['i386']) - kernel2.add_installed_file(installPath='usr/share/licenses/kernel-core/COPYING', - sourceFile=rpmfluff.SourceFile('COPYING', 'different content\n')) - kernel2.add_provides('installonlypkg(kernel)') - kernel2.make() - - def cleanUp(): - shutil.rmtree(baserepo.repoDir) - shutil.rmtree(kernel1.get_base_dir()) - shutil.rmtree(kernel2.get_base_dir()) - request.addfinalizer(cleanUp) - - exitcode, out, err = run_rpmdeplint(['rpmdeplint', 'check-conflicts', - '--repo=base,{}'.format(dir_server.url), - kernel2.get_built_rpm('i386')]) - assert exitcode == 3 - assert err == ('Undeclared file conflicts:\n' - 'kernel-core-0.2-1.i386 provides /usr/share/licenses/kernel-core/COPYING ' - 'which is also provided by kernel-core-0.1-1.i386\n') - - -# https://bugzilla.redhat.com/show_bug.cgi?id=1502458 -def test_finds_conflict_against_older_subpackage(request, dir_server): - conflicting_path = 'usr/share/man/man1/vim.1.gz' - oldvim = rpmfluff.SimpleRpmBuild('vim', '7.4.1989', '2', ['x86_64']) - oldvim.add_subpackage('common') - oldvim.add_subpackage('minimal') - oldvim.add_installed_file(installPath=conflicting_path, - sourceFile=rpmfluff.SourceFile('vim.1', 'oldcontent\n'), - subpackageSuffix='common') - oldvim.get_subpackage('minimal').section_files += '/%s\n' % conflicting_path - baserepo = rpmfluff.YumRepoBuild([oldvim]) - baserepo.make('x86_64') - dir_server.basepath = baserepo.repoDir - - newvim = rpmfluff.SimpleRpmBuild('vim', '8.0.118', '1', ['x86_64']) - newvim.add_subpackage('common') - newvim.add_subpackage('minimal') - newvim.add_installed_file(installPath=conflicting_path, - sourceFile=rpmfluff.SourceFile('vim.1', 'newcontent\n'), - subpackageSuffix='common') - newvim.get_subpackage('minimal').section_files += '/%s\n' % conflicting_path - newvim.make() - - def cleanUp(): - shutil.rmtree(baserepo.repoDir) - shutil.rmtree(oldvim.get_base_dir()) - shutil.rmtree(newvim.get_base_dir()) - request.addfinalizer(cleanUp) - - exitcode, out, err = run_rpmdeplint(['rpmdeplint', 'check-conflicts', - '--repo=base,{}'.format(dir_server.url), - newvim.get_built_rpm('x86_64', name='vim-common'), - newvim.get_built_rpm('x86_64', name='vim-minimal')]) - assert exitcode == 3 - assert err == ('Undeclared file conflicts:\n' - 'vim-common-8.0.118-1.x86_64 provides /usr/share/man/man1/vim.1.gz ' - 'which is also provided by vim-minimal-7.4.1989-2.x86_64\n' - 'vim-minimal-8.0.118-1.x86_64 provides /usr/share/man/man1/vim.1.gz ' - 'which is also provided by vim-common-7.4.1989-2.x86_64\n') - - -# https://bugzilla.redhat.com/show_bug.cgi?id=1448768 -def test_obeys_xml_base_when_downloading_packages(request, tmpdir, dir_server): - p2 = rpmfluff.SimpleRpmBuild('b', '0.1', '1', ['x86_64']) - p2.add_installed_file(installPath='usr/share/thing', - sourceFile=rpmfluff.SourceFile('thing', 'same content\n')) - p2.make() - - # Set up a repo at http://$dirserver/therepo/ pointing at packages stored - # in http://$dirserver/thepackages/ using xml:base. - dir_server.basepath = tmpdir.strpath - shutil.copy(p2.get_built_rpm('x86_64'), tmpdir.mkdir('thepackages').strpath) - subprocess.check_output(['createrepo_c', - '--baseurl={}/thepackages'.format(dir_server.url), - '--outputdir=.', - '../thepackages'], - stderr=subprocess.STDOUT, cwd=tmpdir.mkdir('therepo').strpath) - - p1 = rpmfluff.SimpleRpmBuild('a', '0.1', '1', ['x86_64']) - p1.add_installed_file(installPath='usr/share/thing', - sourceFile=rpmfluff.SourceFile('thing', 'same content\n')) - p1.make() - - def cleanUp(): - shutil.rmtree(p2.get_base_dir()) - shutil.rmtree(p1.get_base_dir()) - request.addfinalizer(cleanUp) - - exitcode, out, err = run_rpmdeplint(['rpmdeplint', 'check-conflicts', - '--repo=base,{}/therepo'.format(dir_server.url), - p1.get_built_rpm('x86_64')]) - assert exitcode == 0 diff --git a/acceptance_tests/test_check_repoclosure.py b/acceptance_tests/test_check_repoclosure.py deleted file mode 100644 index 346b25f..0000000 --- a/acceptance_tests/test_check_repoclosure.py +++ /dev/null @@ -1,261 +0,0 @@ - -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. - -import shutil -import rpm -import rpmfluff -import os.path -from data_setup import run_rpmdeplint - - -def test_catches_soname_change(request, dir_server): - # This is the classic mistake repoclosure is supposed to find... the - # updated package has changed its soname, causing some other package's - # dependencies to become unresolvable. - p_older = rpmfluff.SimpleRpmBuild('a', '4.0', '1', ['i386']) - p_older.add_provides('libfoo.so.4') - p_depending = rpmfluff.SimpleRpmBuild('b', '0.1', '1', ['i386']) - p_depending.add_requires('libfoo.so.4') - baserepo = rpmfluff.YumRepoBuild([p_older, p_depending]) - baserepo.make('i386') - dir_server.basepath = baserepo.repoDir - - p_newer = rpmfluff.SimpleRpmBuild('a', '5.0', '1', ['i386']) - p_newer.add_provides('libfoo.so.5') - p_newer.make() - - def cleanUp(): - shutil.rmtree(baserepo.repoDir) - shutil.rmtree(p_depending.get_base_dir()) - shutil.rmtree(p_older.get_base_dir()) - shutil.rmtree(p_newer.get_base_dir()) - request.addfinalizer(cleanUp) - - exitcode, out, err = run_rpmdeplint(['rpmdeplint', 'check-repoclosure', - '--repo=base,{}'.format(dir_server.url), - p_newer.get_built_rpm('i386')]) - assert exitcode == 3 - assert err == ('Dependency problems with repos:\n' - 'package b-0.1-1.i386 requires libfoo.so.4, but none of the providers can be installed\n') - - -def test_catches_soname_change_with_package_rename(request, dir_server): - # Slightly more complicated version of the above, where the old provider is - # not being updated but rather obsoleted. - p_older = rpmfluff.SimpleRpmBuild('foolib', '4.0', '1', ['i386']) - p_older.add_provides('libfoo.so.4') - p_depending = rpmfluff.SimpleRpmBuild('b', '0.1', '1', ['i386']) - p_depending.add_requires('libfoo.so.4') - baserepo = rpmfluff.YumRepoBuild([p_older, p_depending]) - baserepo.make('i386') - dir_server.basepath = baserepo.repoDir - - p_newer = rpmfluff.SimpleRpmBuild('libfoo', '5.0', '1', ['i386']) - p_newer.add_obsoletes('foolib < 5.0-1') - p_newer.add_provides('libfoo.so.5') - p_newer.make() - - def cleanUp(): - shutil.rmtree(baserepo.repoDir) - shutil.rmtree(p_depending.get_base_dir()) - shutil.rmtree(p_older.get_base_dir()) - shutil.rmtree(p_newer.get_base_dir()) - request.addfinalizer(cleanUp) - - exitcode, out, err = run_rpmdeplint(['rpmdeplint', 'check-repoclosure', - '--repo=base,{}'.format(dir_server.url), - p_newer.get_built_rpm('i386')]) - assert exitcode == 3 - assert err == ('Dependency problems with repos:\n' - 'package b-0.1-1.i386 requires libfoo.so.4, but none of the providers can be installed\n') - - -def test_ignores_dependency_problems_in_packages_under_test(request, dir_server): - # The check-sat command will find and report these problems, it would be - # redundant for check-repoclosure to also report the same problems. - p2 = rpmfluff.SimpleRpmBuild('b', '0.1', '1', ['i386']) - baserepo = rpmfluff.YumRepoBuild((p2,)) - baserepo.make('i386') - dir_server.basepath = baserepo.repoDir - - p1 = rpmfluff.SimpleRpmBuild('a', '0.1', '1', ['i386']) - p1.add_requires('doesnotexist') - p1.make() - - def cleanUp(): - shutil.rmtree(baserepo.repoDir) - shutil.rmtree(p2.get_base_dir()) - shutil.rmtree(p1.get_base_dir()) - request.addfinalizer(cleanUp) - - exitcode, out, err = run_rpmdeplint(['rpmdeplint', 'check-repoclosure', - '--repo=base,{}'.format(dir_server.url), - p1.get_built_rpm('i386')]) - assert exitcode == 0 - assert err == '' - - -def test_ignores_problems_in_older_packages(request, dir_server): - # We only care if the *latest* version of each package in the repos is - # satisfied. If there are dependency problems with an older version, it is - # irrelevant because nobody will be installing it anyway. - p_older = rpmfluff.SimpleRpmBuild('a', '4.0', '1', ['i386']) - p_older.add_provides('libfoo.so.4') - p_older.add_provides('libfoo.so.5') - p_older_depending = rpmfluff.SimpleRpmBuild('b', '0.1', '1', ['i386']) - p_older_depending.add_requires('libfoo.so.4') - p_newer_depending = rpmfluff.SimpleRpmBuild('b', '0.2', '1', ['i386']) - p_newer_depending.add_requires('libfoo.so.5') - baserepo = rpmfluff.YumRepoBuild([p_older, p_older_depending, p_newer_depending]) - baserepo.make('i386') - dir_server.basepath = baserepo.repoDir - - p_newer = rpmfluff.SimpleRpmBuild('a', '5.0', '1', ['i386']) - p_newer.add_provides('libfoo.so.5') - p_newer.make() - - def cleanUp(): - shutil.rmtree(baserepo.repoDir) - shutil.rmtree(p_older_depending.get_base_dir()) - shutil.rmtree(p_newer_depending.get_base_dir()) - shutil.rmtree(p_older.get_base_dir()) - shutil.rmtree(p_newer.get_base_dir()) - request.addfinalizer(cleanUp) - - exitcode, out, err = run_rpmdeplint(['rpmdeplint', 'check-repoclosure', - '--repo=base,{}'.format(dir_server.url), - p_newer.get_built_rpm('i386')]) - assert exitcode == 0 - - -def test_ignores_problems_in_obsoleted_packages(request, dir_server): - # As above, we also don't care about any dependency problems in packages - # that have been obsoleted. - p_older = rpmfluff.SimpleRpmBuild('a', '4.0', '1', ['i386']) - p_older.add_provides('libfoo.so.4') - p_older.add_provides('libfoo.so.5') - p_obsolete_depending = rpmfluff.SimpleRpmBuild('foofouruser', '1.0', '1', ['i386']) - p_obsolete_depending.add_requires('libfoo.so.4') - p_newer_depending = rpmfluff.SimpleRpmBuild('foofiveuser', '0.1', '1', ['i386']) - p_newer_depending.add_requires('libfoo.so.5') - p_newer_depending.add_obsoletes('foofouruser <= 1.0-1') - baserepo = rpmfluff.YumRepoBuild([p_older, p_obsolete_depending, p_newer_depending]) - baserepo.make('i386') - dir_server.basepath = baserepo.repoDir - - p_newer = rpmfluff.SimpleRpmBuild('a', '5.0', '1', ['i386']) - p_newer.add_provides('libfoo.so.5') - p_newer.make() - - def cleanUp(): - shutil.rmtree(baserepo.repoDir) - shutil.rmtree(p_obsolete_depending.get_base_dir()) - shutil.rmtree(p_newer_depending.get_base_dir()) - shutil.rmtree(p_older.get_base_dir()) - shutil.rmtree(p_newer.get_base_dir()) - request.addfinalizer(cleanUp) - - exitcode, out, err = run_rpmdeplint(['rpmdeplint', 'check-repoclosure', - '--repo=base,{}'.format(dir_server.url), - p_newer.get_built_rpm('i386')]) - assert exitcode == 0 - - -def test_warns_on_preexisting_repoclosure_problems(request, dir_server): - # If the repos have some existing dependency problems, we don't want that - # to be an error -- otherwise a bad repo will make it impossible to get any - # results until the problem is fixed. - p2 = rpmfluff.SimpleRpmBuild('b', '0.1', '1', ['i386']) - p2.add_requires('doesnotexist') - baserepo = rpmfluff.YumRepoBuild((p2,)) - baserepo.make('i386') - dir_server.basepath = baserepo.repoDir - - p1 = rpmfluff.SimpleRpmBuild('a', '0.1', '1', ['i386']) - p1.make() - - def cleanUp(): - shutil.rmtree(baserepo.repoDir) - shutil.rmtree(p2.get_base_dir()) - shutil.rmtree(p1.get_base_dir()) - request.addfinalizer(cleanUp) - - exitcode, out, err = run_rpmdeplint(['rpmdeplint', 'check-repoclosure', - '--repo=base,{}'.format(dir_server.url), - p1.get_built_rpm('i386')]) - assert exitcode == 0 - assert ('Ignoring pre-existing repoclosure problem: ' - 'nothing provides doesnotexist needed by b-0.1-1.i386\n' in err) - - -def test_works_on_different_platform_to_current(request, dir_server): - grep = rpmfluff.SimpleRpmBuild('grep', '2.20', '3.el6', ['ppc64']) - - needs_grep = rpmfluff.SimpleRpmBuild('search-tool-5000', '1.0', '3.el6', ['ppc64']) - needs_grep.add_requires('grep = 2.20-3.el6') - - baserepo = rpmfluff.YumRepoBuild((grep, needs_grep)) - baserepo.make('ppc64') - dir_server.basepath = baserepo.repoDir - - package_to_test = rpmfluff.SimpleRpmBuild('test-tool', '10', '3.el6', ['ppc64']) - package_to_test.make() - - def cleanUp(): - shutil.rmtree(baserepo.repoDir) - shutil.rmtree(grep.get_base_dir()) - shutil.rmtree(needs_grep.get_base_dir()) - shutil.rmtree(package_to_test.get_base_dir()) - - request.addfinalizer(cleanUp) - - exitcode, out, err = run_rpmdeplint(['rpmdeplint', 'check-repoclosure', - '--repo=base,{}'.format(dir_server.url), - package_to_test.get_built_rpm('ppc64')]) - - assert exitcode == 0 - assert out == '' - assert err == '' - - -def test_arch_set_manually_is_passed_to_sack(request, dir_server): - grep = rpmfluff.SimpleRpmBuild('grep', '2.20', '3.el6', ['i686']) - - needs_grep = rpmfluff.SimpleRpmBuild('search-tool-5000', '1.0', '3.el6', ['i686']) - needs_grep.add_requires('grep = 2.20-3.el6') - - package_to_test = rpmfluff.SimpleRpmBuild('test-tool', '10', '3.el6', ['i586']) - package_to_test.make() - - baserepo = rpmfluff.YumRepoBuild((grep, needs_grep)) - baserepo.make('i686') - dir_server.basepath = baserepo.repoDir - - def cleanUp(): - shutil.rmtree(baserepo.repoDir) - shutil.rmtree(grep.get_base_dir()) - shutil.rmtree(needs_grep.get_base_dir()) - shutil.rmtree(package_to_test.get_base_dir()) - - request.addfinalizer(cleanUp) - - exitcode, out, err = run_rpmdeplint(['rpmdeplint', 'check-repoclosure', - '--arch=i586', - '--repo=base,{}'.format(dir_server.url), - package_to_test.get_built_rpm('i586')]) - - assert exitcode == 0 - assert out == '' - assert err == '' - - exitcode, out, err = run_rpmdeplint(['rpmdeplint', 'check-repoclosure', - '--arch=i686', - '--repo=base,{}'.format(dir_server.url), - package_to_test.get_built_rpm('i586')]) - - assert exitcode == 0 - assert out == '' - assert err == '' diff --git a/acceptance_tests/test_check_sat.py b/acceptance_tests/test_check_sat.py deleted file mode 100644 index 4ca218e..0000000 --- a/acceptance_tests/test_check_sat.py +++ /dev/null @@ -1,40 +0,0 @@ - -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. - -import rpmfluff -from data_setup import run_rpmdeplint -import shutil - - -def test_shows_error_for_rpms(request, dir_server): - p2 = rpmfluff.SimpleRpmBuild('b', '0.1', '1', ['i386']) - baserepo = rpmfluff.YumRepoBuild((p2,)) - baserepo.make('i386') - dir_server.basepath = baserepo.repoDir - - p1 = rpmfluff.SimpleRpmBuild('a', '0.1', '1', ['i386']) - p1.add_requires('doesnotexist') - p1.make() - - def cleanUp(): - shutil.rmtree(baserepo.repoDir) - shutil.rmtree(p2.get_base_dir()) - shutil.rmtree(p1.get_base_dir()) - request.addfinalizer(cleanUp) - - exitcode, out, err = run_rpmdeplint(['rpmdeplint', 'check-sat', - '--repo=base,{}'.format(dir_server.url), - p1.get_built_rpm('i386')]) - assert exitcode == 3 - assert err == 'Problems with dependency set:\nnothing provides doesnotexist needed by a-0.1-1.i386\n' - assert out == '' - - -def test_error_if_repository_names_not_provided(tmpdir): - exitcode, out, err = run_rpmdeplint( - ['rpmdeplint', 'check-sat', '--repo={}'.format(tmpdir.dirpath())]) - assert 2 == exitcode - assert "error: argument -r/--repo: Repo '{}' is not in the form ,".format(tmpdir.dirpath()) in err diff --git a/acceptance_tests/test_check_upgrade.py b/acceptance_tests/test_check_upgrade.py deleted file mode 100644 index 1145444..0000000 --- a/acceptance_tests/test_check_upgrade.py +++ /dev/null @@ -1,86 +0,0 @@ - -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. - -import shutil -import rpm -import rpmfluff -import os.path -from data_setup import run_rpmdeplint - - -def test_finds_newer_version_in_repo(request, dir_server): - p2 = rpmfluff.SimpleRpmBuild('anaconda', '19.31.123', '1.el7', ['noarch']) - p2.add_subpackage('user-help') - baserepo = rpmfluff.YumRepoBuild([p2]) - baserepo.make('noarch') - dir_server.basepath = baserepo.repoDir - - p1 = rpmfluff.SimpleRpmBuild('anaconda-user-help', '7.2.2', '1.el7', ['noarch']) - p1.make() - - def cleanUp(): - shutil.rmtree(baserepo.repoDir) - shutil.rmtree(p2.get_base_dir()) - shutil.rmtree(p1.get_base_dir()) - request.addfinalizer(cleanUp) - - exitcode, out, err = run_rpmdeplint(['rpmdeplint', 'check-upgrade', - '--repo=base,{}'.format(dir_server.url), - '--arch=x86_64', - p1.get_built_rpm('noarch')]) - assert exitcode == 3 - assert err == ('Upgrade problems:\n' - 'anaconda-user-help-7.2.2-1.el7.noarch would be upgraded by ' - 'anaconda-user-help-19.31.123-1.el7.noarch from repo base\n') - - -def test_finds_obsoleting_package_in_repo(request, dir_server): - p2 = rpmfluff.SimpleRpmBuild('b', '0.1', '2', ['i386']) - p2.add_obsoletes('a < 0.1-2') - baserepo = rpmfluff.YumRepoBuild([p2]) - baserepo.make('i386') - dir_server.basepath = baserepo.repoDir - - p1 = rpmfluff.SimpleRpmBuild('a', '0.1', '1', ['i386']) - p1.make() - - def cleanUp(): - shutil.rmtree(baserepo.repoDir) - shutil.rmtree(p2.get_base_dir()) - shutil.rmtree(p1.get_base_dir()) - request.addfinalizer(cleanUp) - - exitcode, out, err = run_rpmdeplint(['rpmdeplint', 'check-upgrade', - '--repo=base,{}'.format(dir_server.url), - p1.get_built_rpm('i386')]) - assert exitcode == 3 - assert err == ('Upgrade problems:\n' - 'a-0.1-1.i386 would be obsoleted by b-0.1-2.i386 from repo base\n') - - -def test_epoch(request, dir_server): - p2 = rpmfluff.SimpleRpmBuild('anaconda', '19.31.123', '1.el7', ['noarch']) - p2.add_subpackage('user-help') - baserepo = rpmfluff.YumRepoBuild([p2]) - baserepo.make('noarch') - dir_server.basepath = baserepo.repoDir - - p1 = rpmfluff.SimpleRpmBuild('anaconda-user-help', '7.3.2', '1.el7', ['noarch']) - p1.epoch = 1 - p1.make() - - def cleanUp(): - shutil.rmtree(baserepo.repoDir) - shutil.rmtree(p2.get_base_dir()) - shutil.rmtree(p1.get_base_dir()) - request.addfinalizer(cleanUp) - - exitcode, out, err = run_rpmdeplint(['rpmdeplint', 'check-upgrade', - '--repo=base,{}'.format(dir_server.url), - '--arch=x86_64', - p1.get_built_rpm('noarch')]) - assert exitcode == 0 - assert err == '' diff --git a/acceptance_tests/test_list_deps.py b/acceptance_tests/test_list_deps.py deleted file mode 100644 index 5480b3f..0000000 --- a/acceptance_tests/test_list_deps.py +++ /dev/null @@ -1,125 +0,0 @@ - -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. - -import shutil -import rpmfluff - -from data_setup import run_rpmdeplint - - -def test_lists_dependencies_for_rpms(request, dir_server): - p2 = rpmfluff.SimpleRpmBuild('b', '0.1', '1', ['i386']) - baserepo = rpmfluff.YumRepoBuild((p2,)) - baserepo.make('i386') - dir_server.basepath = baserepo.repoDir - - p1 = rpmfluff.SimpleRpmBuild('a', '0.1', '1', ['i386']) - p1.add_requires('b') - p1.make() - - def cleanUp(): - shutil.rmtree(baserepo.repoDir) - shutil.rmtree(p2.get_base_dir()) - shutil.rmtree(p1.get_base_dir()) - request.addfinalizer(cleanUp) - - exitcode, out, err = run_rpmdeplint(['rpmdeplint', 'list-deps', - '--repo=base,{}'.format(dir_server.url), - p1.get_built_rpm('i386')]) - assert exitcode == 0 - assert err == '' - assert out == ('a-0.1-1.i386 has 2 dependencies:\n' - '\ta-0.1-1.i386\n' - '\tb-0.1-1.i386\n\n') - -def test_lists_dependencies_for_rpms_served_from_filesystem(request): - p2 = rpmfluff.SimpleRpmBuild('b', '0.1', '1', ['i386']) - baserepo = rpmfluff.YumRepoBuild((p2,)) - baserepo.make('i386') - - p1 = rpmfluff.SimpleRpmBuild('a', '0.1', '1', ['i386']) - p1.add_requires('doesnotexist') - p1.make() - - def cleanUp(): - shutil.rmtree(baserepo.repoDir) - shutil.rmtree(p2.get_base_dir()) - shutil.rmtree(p1.get_base_dir()) - request.addfinalizer(cleanUp) - - exitcode, out, err = run_rpmdeplint(['rpmdeplint', 'list-deps', - '--repo=base,{}'.format(baserepo.repoDir), - p1.get_built_rpm('i386')]) - assert exitcode == 3 - - -def test_errors_out_for_unsatisfiable_deps(request, dir_server): - p2 = rpmfluff.SimpleRpmBuild('b', '0.1', '1', ['i386']) - baserepo = rpmfluff.YumRepoBuild((p2,)) - baserepo.make('i386') - dir_server.basepath = baserepo.repoDir - - p1 = rpmfluff.SimpleRpmBuild('a', '0.1', '1', ['i386']) - p1.add_requires('doesnotexist') - p1.make() - - def cleanUp(): - shutil.rmtree(baserepo.repoDir) - shutil.rmtree(p1.get_base_dir()) - shutil.rmtree(p2.get_base_dir()) - request.addfinalizer(cleanUp) - - exitcode, out, err = run_rpmdeplint(['rpmdeplint', 'list-deps', - '--repo=base,{}'.format(dir_server.url), - p1.get_built_rpm('i386')]) - assert exitcode == 3 - - -def test_rpmdeplint_errors_on_unavailble_url(request): - url = 'http://example.test' - p1 = rpmfluff.SimpleRpmBuild('a', '0.1', '1', ['i386']) - p1.make() - - def cleanUp(): - shutil.rmtree(p1.get_base_dir()) - request.addfinalizer(cleanUp) - - exitcode, out, err = run_rpmdeplint(['rpmdeplint', 'list-deps', - '--repo=base,{}'.format(url), - p1.get_built_rpm('i386')]) - - assert exitcode == 1 - - -def test_erroneous_cli_input_errors(): - exitcode, out , err = run_rpmdeplint(['rpmdeplint', 'list-deps', - '--derp']) - - assert exitcode == 2 - - -# https://bugzilla.redhat.com/show_bug.cgi?id=1382531 -def test_handles_invalid_rpm_without_crashing(request, dir_server, tmpdir): - p2 = rpmfluff.SimpleRpmBuild('b', '0.1', '1', ['i386']) - baserepo = rpmfluff.YumRepoBuild([p2]) - baserepo.make('i386') - dir_server.basepath = baserepo.repoDir - - # To trigger this bug, the contents of the invalid RPM are irrelevant but - # the filename must end in '.rpm'. - broken_package = tmpdir.join('broken.rpm') - broken_package.write('lol\n') - - def cleanUp(): - shutil.rmtree(baserepo.repoDir) - shutil.rmtree(p2.get_base_dir()) - request.addfinalizer(cleanUp) - - exitcode, out, err = run_rpmdeplint(['rpmdeplint', 'list-deps', - '--repo=base,{}'.format(dir_server.url), - broken_package.strpath]) - assert exitcode == 1 - assert err == 'Failed to read package: {}: not a rpm\n'.format(broken_package.strpath) diff --git a/acceptance_tests/test_usage.py b/acceptance_tests/test_usage.py deleted file mode 100644 index c5f01fc..0000000 --- a/acceptance_tests/test_usage.py +++ /dev/null @@ -1,25 +0,0 @@ - -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. - -from data_setup import run_rpmdeplint - - -def test_prints_usage_when_no_subcommand_is_given(): - exitcode, out, err = run_rpmdeplint(['rpmdeplint']) - - assert 'usage:' in err - # The first wording is on Python < 3.3, the second wording is on Python 3.3+ - assert ('error: too few arguments' in err or - 'error: the following arguments are required: subcommand' in err) - assert exitcode == 2 - - -# https://bugzilla.redhat.com/show_bug.cgi?id=1537961 -def test_prints_usage_when_no_repos_are_defined(): - exitcode, out, err = run_rpmdeplint(['rpmdeplint', 'check', 'some.rpm']) - assert 'usage:' in err - assert 'error: no repos specified to test against' in err - assert exitcode == 2 diff --git a/docs/CHANGES.rst b/docs/CHANGES.rst deleted file mode 100644 index 7ad62c0..0000000 --- a/docs/CHANGES.rst +++ /dev/null @@ -1,93 +0,0 @@ -Changelog ---------- - -2.0 -~~~ - -* Added yum repository caching which performs regular cleans for files more than - one week old. This expiry period can be modified with the environment - variable ``RPMDEPLINT_EXPIRY_SECONDS``. - -* The :py:class:`rpmdeplint.DependencyAnalyzer` class no longer needs to be - "entered" as a context manager. The class still supports the context manager - protocol as a no-op for backwards compatibility. - -* Added ``--quiet`` option which tells rpmdeplint to only print error messages. - -1.4 -~~~ - -* Fixed handling of the ``xml:base`` attribute in repodata. Previously, if - a repo used ``xml:base`` to refer to packages stored at a different URL, - rpmdeplint would fail to download them when it performed conflict checking - (`RHBZ#1448768 `__). - -* If a package fails to download, a clean error message is now reported. - Previously this would result in an unhandled exception, which triggered abrt - handling - (`RHBZ#1423678 `__). - -* Fixed usage message when no subcommand is given on Python 3.3+ - (`RHBZ#1445990 `__). - -1.3 -~~~ - -* If you are testing only ``noarch`` packages, you must now explicitly pass the - ``--arch`` option to specify the target architecture you are testing against. - Previously the checks would run but produce nonsensical results - (`RHBZ#1392635 `__). - -* The check for undeclared file conflicts has been improved: - - * File conflicts are not reported if the two conflicting packages cannot be - installed together due to Requires relationships - (`RHBZ#1412910 `__). - - * It no longer downloads every potentially conflicting package to - check. Only the first potential conflict is checked, to avoid downloading - a very large number of packages for commonly shared paths such as - :file:`/usr/lib/debug` - (`RHBZ#1400722 `__). - -* A more informative exception is now raised when downloading repodata fails. - -* Added a ``--version`` option to print the installed version of rpmdeplint. - -1.2 -~~~ - -* Added a new option ``--repos-from-system`` for testing against repositories - from the system-wide Yum/DNF configuration. - -* Conflict checking now works correctly with RPM 4.11 (as found on Red Hat - Enterprise Linux 7 and derivatives). Previously it was relying on an API only - present in RPM 4.12+. - -* Fixed spurious errors/warnings from ``check-repoclosure`` when the arch of - the packages being tested did not match the host architecture where - rpmdeplint was run - (`RHBZ#1378253 `__). - -1.1 -~~~ - -* Added ``check-upgrade`` command, to ensure that the given - packages are not upgraded or obsoleted by an existing package - in the repository. - -* Added ``check-repoclosure`` command, to check whether repository - dependencies can still be satisfied with the given packages. - -* Added ``check`` command which performs all the different checks. - -* The command-line interface now uses a specific exit status (3) to indicate - that a check has failed, so that it can be distinguished from other error - conditions. - -1.0 -~~~ - -* Initial release. Supports checking dependency satisfiability and - undeclared file conflicts. - diff --git a/docs/api.rst b/docs/api.rst deleted file mode 100644 index 02b97a6..0000000 --- a/docs/api.rst +++ /dev/null @@ -1,11 +0,0 @@ -API ---- - -.. autoclass:: rpmdeplint.DependencyAnalyzer - :members: - -.. autoclass:: rpmdeplint.DependencySet - :members: - -.. autoclass:: rpmdeplint.repodata.Repo - :members: diff --git a/docs/conf.py b/docs/conf.py deleted file mode 100644 index 1d9ec66..0000000 --- a/docs/conf.py +++ /dev/null @@ -1,52 +0,0 @@ -# -*- coding: utf-8 -*- -extensions = ['sphinx.ext.autodoc'] -templates_path = ['_templates'] -source_suffix = '.rst' -# The master toctree document. -master_doc = 'index' - -# General information about the project. -project = u'rpmdeplint' -copyright = u'2016, Red Hat' -author = u'rpmdeplint contributors' - -version = u'1.0' -release = u'1.0' - -language = None - -exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] - -# The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'sphinx' - -# If true, `todo` and `todoList` produce output, else they produce nothing. -todo_include_todos = False - -autoclass_content = 'both' -autodoc_member_order = 'bysource' - - -# -- Options for HTML output ---------------------------------------------- - -# The theme to use for HTML and HTML Help pages. See the documentation for -# a list of builtin themes. -html_theme = 'default' - -# Add any paths that contain custom static files (such as style sheets) here, -# relative to this directory. They are copied after the builtin static files, -# so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = [] -# Output file base name for HTML help builder. -htmlhelp_basename = 'rpmdeplintdoc' - -# -- Options for manual page output --------------------------------------- - -# One entry per manual page. List of tuples -# (source start file, name, description, authors, manual section). -man_pages = [ - ('rpmdeplint', - 'rpmdeplint', - 'a tool to find errors in RPM packages in the context of their dependency graph', - [author], 1) -] diff --git a/docs/index.rst b/docs/index.rst deleted file mode 100644 index 23f77b0..0000000 --- a/docs/index.rst +++ /dev/null @@ -1,13 +0,0 @@ -Welcome to rpmdeplint's documentation! -====================================== - -Rpmdeplint is a tool to find errors in RPM packages in the context of their -dependency graph. - - -.. toctree:: - :maxdepth: 2 - - rpmdeplint - api - CHANGES diff --git a/docs/rpmdeplint.rst b/docs/rpmdeplint.rst deleted file mode 100644 index 9dea6b3..0000000 --- a/docs/rpmdeplint.rst +++ /dev/null @@ -1,155 +0,0 @@ -rpmdeplint ----------- - -.. program:: rpmdeplint check-sat - -Synopsis -~~~~~~~~ - -| :program:`rpmdeplint` COMMAND [:option:`--repo` NAME,URL] [RPMPATH] - -Description -~~~~~~~~~~~ - -The :program:`rpmdeplint` command will test dependency satisfiability of given -RPM packages against given repositories. - -Options -~~~~~~~ - -.. option:: --repo NAME,URL, -r NAME,URL - - Load yum repo from the given URL. You can also specify a local filesystem - path instead of a URL. - - The NAME is for descriptive purposes only. It has no impact on dependency - resolution. If rpmdeplint finds a dependency problem relating to a package - in this repo, the NAME will appear in the error message. - - Note that the URL should point at a directory containing - ``repodata/repomd.xml``. For example:: - - --repo=fedora,https://download.fedoraproject.org/pub/fedora/linux/development/rawhide/Everything/x86_64/os/ - -.. option:: --repos-from-system, -R - - Use yum repos from the system-wide configuration in :file:`/etc/yum.conf` - and :file:`/etc/yum.repos.d/{*}.repo`. Repos which are disabled in the - configuration (``enabled=0``) are ignored. - - This option can be combined with one or more :option:`--repo` options. - -.. option:: --arch ARCH, -a ARCH - - Only consider packages for ARCH when solving dependencies. If a repo - contains packages for any other arches, they will be ignored. - - Note that the traditional RPM arch compatibility rules are applied, which - means that ``noarch`` packages and "inferior" arch packages are also - included (for example, ``i686`` implicitly includes ``i386``). - - This option is normally *not* required, because distribution repos are - normally split by arch (including the various special cases for multilib). - -Arguments -~~~~~~~~~ - -.. option:: RPMPATH - - Path to an RPM package. This can be a relative or absolute filesystem path. - -Commands -~~~~~~~~ - -check - Performs each of the checks listed below. - -check-sat - Checks for unmet dependencies with the given RPM packages against the given - repositories. - Each unmet dependency is listed. - -check-repoclosure - Checks for unmet dependencies in the given repositories, when considered - together with the given packages. This check is similar to *check-sat*, - except it checks only packages in the repositories, not the packages under - test. - - Packages are only considered to be available for dependency resolution if - they are the latest version and not obsoleted by any other package. - Therefore this check can detect problems where a package under test is - updating an existing package in the repositories, but it no longer provides - a requirement needed by some other package in the repositories. - - In case a pre-existing repoclosure problem is found (that is, the same - problem exists when considering only the repositories without the packages - under test) a warning is printed to stderr, but the check is *not* considered - to have failed. - -check-conflicts - Checks for undeclared file conflicts in the given RPM packages: that is, when - one of the given package contains a file which is also contained in some - other package. - - This command will not report a file as conflicting between two packages if: - - * there is an explicit RPM ``Conflicts`` between the two packages; or - * the file’s checksum, permissions, owner, and group are identical in both - packages (RPM allows both packages to own the file in this case); or - * the file’s color is different between the two packages (RPM will - silently resolve the conflict in favour of the 64-bit file). - -check-upgrade - Checks that there are no existing packages in the repositories which would - upgrade or obsolete the given packages. - - If this check fails, it means that the package under test will never be - installed (since the package manager will always pick the newer or obsoleting - package from the repositories instead) which is not desirable, assuming the - package is intended as an update. - -list-deps - All dependencies will be listed for each given RPM package. - -Exit status -~~~~~~~~~~~ - -0 - Checks ran successfully, and no problems were found - -1 - Error occurred while running checks (including network errors downloading - repodata) - -2 - Command-line usage error - -3 - Problems were found with the packages under test - -Examples -~~~~~~~~ - -Imagine you have produced a new pre-release build of your package, and you want -to check if it will cause dependency errors in Fedora:: - - rpmdeplint check \ - --repo=fedora,https://download.fedoraproject.org/pub/fedora/linux/development/rawhide/Everything/x86_64/os/ \ - greenwave-0.6.1-0.git.2.2529bfb.fc29.noarch.rpm - -You can also use a local filesystem path instead of an absolute URL for the -repos to test against. For example, if you are offline you could re-use your -local dnf cache. (Note that rpmdeplint may need to fetch packages for file -conflict checking and this step will fail if you use an incomplete repo such as -the dnf cache.) - -:: - - rpmdeplint check \ - --repo=rawhide,/var/cache/dnf/rawhide-2d95c80a1fa0a67d/ - greenwave-0.6.1-0.git.2.2529bfb.fc29.noarch.rpm - -Bugs -~~~~ - -Bug reports can be submitted to https://bugzilla.redhat.com/. diff --git a/rpmbuild.sh b/rpmbuild.sh deleted file mode 100755 index 0271adb..0000000 --- a/rpmbuild.sh +++ /dev/null @@ -1,71 +0,0 @@ -#!/bin/bash - -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. - -# Builds a development (S)RPM from HEAD. - -set -e - -if [ $# -eq 0 ] ; then - echo "Usage: $1 -bs|-bb " >&2 - echo "Hint: -bs builds SRPM, -bb builds RPM, refer to rpmbuild(8)" >&2 - exit 1 -fi - -lasttag="$(git describe --abbrev=0 HEAD)" -lastversion="${lasttag##rpmdeplint-}" -if [ "$(git rev-list "$lasttag..HEAD" | wc -l)" -eq 0 ] ; then - # building a tag - rpmver="" - rpmrel="" - version="$lastversion" -else - # git builds count as a pre-release of the next version - version="$lastversion" - version="${version%%[a-z]*}" # strip non-numeric suffixes like "rc1" - # increment the last portion of the version - version="${version%.*}.$((${version##*.} + 1))" - commitcount=$(git rev-list "rpmdeplint-$lastversion..HEAD" | wc -l) - commitsha=$(git rev-parse --short HEAD) - rpmver="${version}" - rpmrel="0.git.${commitcount}.${commitsha}" - version="${version}.git.${commitcount}.${commitsha}" -fi - -workdir="$(mktemp -d)" -trap "rm -rf $workdir" EXIT -outdir="$(readlink -f ./rpmbuild-output)" -mkdir -p "$outdir" - -git archive --format=tar --prefix="rpmdeplint-${version}/" HEAD | gzip >"$workdir/rpmdeplint-${version}.tar.gz" -git show HEAD:rpmdeplint.spec >"$workdir/rpmdeplint.spec" - -if [ -n "$rpmrel" ] ; then - # need to hack the version in the spec - sed --regexp-extended --in-place \ - -e "/%global upstream_version /c\%global upstream_version ${version}" \ - -e "/^Version:/cVersion: ${rpmver}" \ - -e "/^Release:/cRelease: ${rpmrel}%{?dist}" \ - "$workdir/rpmdeplint.spec" - # inject %prep commands to also hack the Python module versions - # (beware the precarious quoting here...) - commands=$(cat <"$workdir/rpmdeplint.spec.injected" - mv "$workdir/rpmdeplint.spec.injected" "$workdir/rpmdeplint.spec" -fi - -rpmbuild \ - --define "_topdir $workdir" \ - --define "_sourcedir $workdir" \ - --define "_specdir $workdir" \ - --define "_rpmdir $outdir" \ - --define "_srcrpmdir $outdir" \ - "$@" "$workdir/rpmdeplint.spec" diff --git a/rpmdeplint.spec b/rpmdeplint.spec deleted file mode 100644 index 95f92eb..0000000 --- a/rpmdeplint.spec +++ /dev/null @@ -1,74 +0,0 @@ - -%global upstream_version 1.4 - -Name: rpmdeplint -Version: 1.4 -Release: 1%{?dist} -Summary: Tool to find errors in RPM packages in the context of their dependency graph -License: GPLv2+ -URL: https://pagure.io/rpmdeplint -Source0: https://files.pythonhosted.org/packages/source/r/%{name}/%{name}-%{upstream_version}.tar.gz -BuildArch: noarch - -# The base package is just the CLI, which pulls in the rpmdeplint -# Python modules to do the real work. -Requires: python3-%{name} = %{version}-%{release} - -%description -Rpmdeplint is a tool to find errors in RPM packages in the context of their -dependency graph. - - -%package -n python3-%{name} -%{?python_provide:%python_provide python3-%{name}} -Summary: %{summary} -BuildRequires: python3-devel -BuildRequires: python3-setuptools -BuildRequires: python3-sphinx -BuildRequires: python3-pytest -BuildRequires: python3-six -BuildRequires: python3-rpm -BuildRequires: python3-hawkey -BuildRequires: python3-librepo -BuildRequires: python3-solv -BuildRequires: python3-rpmfluff -Requires: python3-six -Requires: python3-rpm -Requires: python3-hawkey -Requires: python3-librepo -Requires: python3-solv -Requires: python3-rpmfluff - -%description -n python3-%{name} -Rpmdeplint is a tool to find errors in RPM packages in the context of their -dependency graph. - -This package provides a Python 3 API for performing the checks. - -%prep -%setup -q -n %{name}-%{upstream_version} - - -rm -rf rpmdeplint.egg-info - -%build -%py3_build - -%install -%py3_install - -%check -py.test-3 rpmdeplint -k "not TestDependencyAnalyzer" -# Acceptance tests do not work in mock because they require .i686 packages. - -%files -%{_bindir}/%{name} -%{_mandir}/man1/%{name}.1.* - -%files -n python3-%{name} -%license COPYING -%doc README.rst -%{python3_sitelib}/%{name}/ -%{python3_sitelib}/%{name}*.egg-info - -%changelog diff --git a/rpmdeplint/__init__.py b/rpmdeplint/__init__.py deleted file mode 100644 index 1b62aab..0000000 --- a/rpmdeplint/__init__.py +++ /dev/null @@ -1,472 +0,0 @@ - -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. - -from __future__ import absolute_import - -import os, os.path -from collections import defaultdict -import binascii -import logging -import six -from six.moves import map -import solv -import rpm -import ctypes - - -logger = logging.getLogger(__name__) - - -installonlypkgs = [ - # The default 'installonlypkgs' from dnf - # https://github.com/rpm-software-management/dnf/blob/dnf-2.5.1-1/dnf/const.py.in#L28 - 'kernel', - 'kernel-PAE', - 'installonlypkg(kernel)', - 'installonlypkg(kernel-module)', - 'installonlypkg(vm)', - # Additional names which yum 3.4.3 (RHEL7) has in its default 'installonlypkgs' - # https://github.com/rpm-software-management/yum/blob/cf8a5669165e958d56157abf40d0cdd552c8fbf9/yum/config.py#L650 - 'kernel-bigmem', - 'kernel-enterprise', - 'kernel-smp', - 'kernel-modules', - 'kernel-debug', - 'kernel-unsupported', - 'kernel-source', - 'kernel-devel', - 'kernel-PAE-debug', -] - - -class UnreadablePackageError(Exception): - """ - Raised if an RPM package cannot be read from disk (it's corrupted, or the - file is not a valid RPM package, etc). - """ - pass - - -class DependencySet(object): - """ - Contains dependency information from trying to install the packages under test. - """ - - def __init__(self): - self._packagedeps = defaultdict(lambda: dict(dependencies=[],problems=[])) - self._packages_with_problems = set() - self._overall_problems = set() - - def add_package(self, pkg, dependencies, problems): - nevra = str(pkg) - self._packagedeps[nevra]['dependencies'].extend(map(str, dependencies)) - if len(problems) != 0: - all_problems = [] - # For each problem, find all the problematic RPM "rules" which - # lead to the problem and also include them in - # the `overall_problems` description. - for problem in problems: - all_problems.append(six.text_type(problem)) - for rule in problem.findallproblemrules(): - all_problems.append(rule.info().problemstr()) - self._packagedeps[nevra]['problems'].extend(all_problems) - self._packages_with_problems.add(nevra) - self._overall_problems.update(all_problems) - - @property - def packages(self): - return sorted(self._packagedeps.keys()) - - @property - def overall_problems(self): - """ - List of str dependency problems found (if any) - """ - return sorted(self._overall_problems) - - @property - def packages_with_problems(self): - """ - List of :py:class:`solv.Solvable` which had dependency problems - """ - return sorted(self._packages_with_problems) - - @property - def package_dependencies(self): - """ - Dict in the form {package: {'dependencies': list of packages, 'problems': list of problems}} - """ - return dict(self._packagedeps) - - -class DependencyAnalyzer(object): - """An object which checks packages against provided repos - for dependency satisfiability. - - Construct an instance for a particular set of packages you want to test, - with the repos you want to test against. Then call the individual checking - methods to perform each check. - """ - - def __init__(self, repos, packages, arch=None): - """ - :param repos: An iterable of :py:class:`rpmdeplint.repodata.Repo` instances - :param packages: An iterable of RPM package paths to be tested - """ - # delayed import to avoid circular dependency - from rpmdeplint.repodata import RepoDownloadError - - self.pool = solv.Pool() - self.pool.setarch(arch) - - #: List of :py:class:`solv.Solvable` to be tested (corresponding to *packages* parameter) - self.solvables = [] - self.commandline_repo = self.pool.add_repo('@commandline') - for rpmpath in packages: - solvable = self.commandline_repo.add_rpm(rpmpath) - if solvable is None: - # pool.errstr is already prefixed with the filename - raise UnreadablePackageError('Failed to read package: %s' - % self.pool.errstr) - self.solvables.append(solvable) - - self.repos_by_name = {} #: Mapping of {repo name: :py:class:`rpmdeplint.repodata.Repo`} - for repo in repos: - try: - repo.download_repodata() - except RepoDownloadError as e: - if repo.skip_if_unavailable: - logger.warn('Skipping repo %s: %s', repo.name, e) - continue - else: - raise - solv_repo = self.pool.add_repo(repo.name) - # solv.xfopen does not accept unicode filenames on Python 2 - solv_repo.add_rpmmd(solv.xfopen_fd(str(repo.primary_url), repo.primary.fileno()), - None) - solv_repo.add_rpmmd(solv.xfopen_fd(str(repo.filelists_url), repo.filelists.fileno()), - None, solv.Repo.REPO_EXTEND_SOLVABLES) - self.repos_by_name[repo.name] = repo - - self.pool.addfileprovides() - self.pool.createwhatprovides() - - # Special handling for "installonly" packages: we create jobs to mark - # installonly package names as "multiversion" and then set those as - # pool jobs, which means the jobs are automatically applied whenever we - # run the solver on this pool. - multiversion_jobs = [] - for name in installonlypkgs: - selection = self.pool.select(name, solv.Selection.SELECTION_PROVIDES) - multiversion_jobs.extend(selection.jobs(solv.Job.SOLVER_MULTIVERSION)) - self.pool.setpooljobs(multiversion_jobs) - - # Context manager protocol is only implemented for backwards compatibility. - # There are actually no resources to acquire or release. - - def __enter__(self): - return self - - def __exit__(self, type, value, tb): - return - - def download_package_header(self, solvable): - if solvable in self.solvables: - # It's a package under test, nothing to download - return solvable.lookup_location()[0] - href = solvable.lookup_location()[0] - baseurl = solvable.lookup_str(self.pool.str2id('solvable:mediabase')) - repo = self.repos_by_name[solvable.repo.name] - return repo.download_package_header(href, baseurl) - - def try_to_install_all(self): - """ - Try to solve the goal of installing each of the packages under test, - starting from an empty package set. - - :return: Tuple of (bool ok?, :py:class:`DependencySet`) - """ - solver = self.pool.Solver() - ds = DependencySet() - for solvable in self.solvables: - logger.debug('Solving install jobs for %s', solvable) - jobs = solvable.Selection().jobs(solv.Job.SOLVER_INSTALL) - problems = solver.solve(jobs) - if problems: - ds.add_package(solvable, [], problems) - else: - ds.add_package(solvable, solver.transaction().newsolvables(), []) - - ok = len(ds.overall_problems) == 0 - return ok, ds - - def _select_obsoleted_by(self, solvables): - """ - Returns a solv.Selection matching every solvable which is "obsoleted" - by some solvable in the given list -- either due to an explicit - Obsoletes relationship, or because we have a solvable with the same - name with a higher epoch-version-release. - """ - # Start with an empty selection. - sel = self.pool.Selection() - for solvable in solvables: - # Select every solvable with the same name and lower EVR. - # XXX are there some special cases with arch-noarch upgrades which this does not handle? - sel.add(self.pool.select('{}.{} < {}'.format(solvable.name, solvable.arch, solvable.evr), - solv.Selection.SELECTION_NAME | - solv.Selection.SELECTION_DOTARCH | - solv.Selection.SELECTION_REL)) - for obsoletes_rel in solvable.lookup_deparray(self.pool.str2id('solvable:obsoletes')): - # Select every solvable matching the obsoletes relationship by name. - sel.add(obsoletes_rel.Selection_name()) - return sel - - def find_repoclosure_problems(self): - """ - Checks for any package in the repos which would have unsatisfied - dependencies, if the packages under test were added to the repos. - - This applies some extra constraints to prevent the solver from finding - a solution which involves downgrading or installing an older package, - which is technically a valid solution but is not expected if the - packages are supposed to be updates. - - :return: List of str problem descriptions if any problems were found - """ - problems = [] - solver = self.pool.Solver() - # This selection matches packages obsoleted by our packages under test. - obs_sel = self._select_obsoleted_by(self.solvables) - # This selection matches packages obsoleted by other existing packages in the repo. - existing_obs_sel = self._select_obsoleted_by(s for s in self.pool.solvables - if s.repo.name != '@commandline') - obsoleted = obs_sel.solvables() + existing_obs_sel.solvables() - logger.debug('Excluding the following obsoleted packages:\n%s', - '\n'.join(' {}'.format(s) for s in obsoleted)) - for solvable in self.pool.solvables: - if solvable in self.solvables: - continue # checked by check-sat command instead - if solvable in obsoleted: - continue # no reason to check it - if not self.pool.isknownarch(solvable.archid): - logger.debug( - 'Skipping requirements for package {} arch does not match ' - 'Architecture under test'.format(six.text_type(solvable))) - continue - logger.debug('Checking requires for %s', solvable) - # XXX limit available packages to compatible arches? - # (use libsolv archpolicies somehow) - jobs = (solvable.Selection().jobs(solv.Job.SOLVER_INSTALL) + - obs_sel.jobs(solv.Job.SOLVER_ERASE) + - existing_obs_sel.jobs(solv.Job.SOLVER_ERASE)) - solver_problems = solver.solve(jobs) - if solver_problems: - problem_msgs = [six.text_type(p) for p in solver_problems] - # If it's a pre-existing problem with repos (that is, the - # problem also exists when the packages under test are - # excluded) then warn about it here but don't consider it - # a problem. - jobs = (solvable.Selection().jobs(solv.Job.SOLVER_INSTALL) + - existing_obs_sel.jobs(solv.Job.SOLVER_ERASE)) - existing_problems = solver.solve(jobs) - if existing_problems: - for p in existing_problems: - logger.warn('Ignoring pre-existing repoclosure problem: %s', p) - else: - problems.extend(problem_msgs) - return problems - - def _files_in_solvable(self, solvable): - iterator = solvable.Dataiterator(self.pool.str2id('solvable:filelist'), None, - solv.Dataiterator.SEARCH_FILES | solv.Dataiterator.SEARCH_COMPLETE_FILELIST) - return [match.str for match in iterator] - - def _packages_can_be_installed_together(self, left, right): - """ - Returns True if the given packages can be installed together. - """ - solver = self.pool.Solver() - left_install_jobs = left.Selection().jobs(solv.Job.SOLVER_INSTALL) - right_install_jobs = right.Selection().jobs(solv.Job.SOLVER_INSTALL) - # First check if each one can be installed on its own. If either of - # these fails it is a warning, because it means we have no way to know - # if they can be installed together or not. - problems = solver.solve(left_install_jobs) - if problems: - logger.warn('Ignoring conflict candidate %s ' - 'with pre-existing dependency problems: %s', - left, problems[0]) - return False - problems = solver.solve(right_install_jobs) - if problems: - logger.warn('Ignoring conflict candidate %s ' - 'with pre-existing dependency problems: %s', - right, problems[0]) - return False - problems = solver.solve(left_install_jobs + right_install_jobs) - if problems: - logger.debug('Conflict candidates %s and %s cannot be installed together: %s', - left, right, problems[0]) - return False - return True - - def _file_conflict_is_permitted(self, left, right, filename): - """ - Returns True if rpm would allow both the given packages to share - ownership of the given filename. - """ - if not hasattr(rpm, 'files'): - return self._file_conflict_is_permitted_rpm411(left, right, filename) - - ts = rpm.TransactionSet() - ts.setVSFlags(rpm._RPMVSF_NOSIGNATURES) - - left_hdr = ts.hdrFromFdno(open(left.lookup_location()[0], 'rb')) - right_hdr = ts.hdrFromFdno(open(self.download_package_header(right), 'rb')) - left_files = rpm.files(left_hdr) - right_files = rpm.files(right_hdr) - if left_files[filename].matches(right_files[filename]): - logger.debug('Conflict on %s between %s and %s permitted because files match', - filename, left, right) - return True - if left_files[filename].color != right_files[filename].color: - logger.debug('Conflict on %s between %s and %s permitted because colors differ', - filename, left, right) - return True - return False - - def _file_conflict_is_permitted_rpm411(self, left, right, filename): - # In rpm 4.12+ the rpmfilesCompare() function is exposed nicely as the - # rpm.files.matches Python method. In earlier rpm versions there is - # nothing equivalent in the Python bindings, although we can use ctypes - # to poke around and call the older rpmfiCompare() C API directly... - librpm = ctypes.CDLL('librpm.so.3') - _rpm = ctypes.CDLL(os.path.join(os.path.dirname(rpm.__file__), '_rpm.so')) - class rpmfi_s(ctypes.Structure): pass - librpm.rpmfiCompare.argtypes = [ctypes.POINTER(rpmfi_s), ctypes.POINTER(rpmfi_s)] - librpm.rpmfiCompare.restype = ctypes.c_int - _rpm.fiFromFi.argtypes = [ctypes.py_object] - _rpm.fiFromFi.restype = ctypes.POINTER(rpmfi_s) - - ts = rpm.TransactionSet() - ts.setVSFlags(rpm._RPMVSF_NOSIGNATURES) - - left_hdr = ts.hdrFromFdno(open(left.lookup_location()[0], 'rb')) - right_hdr = ts.hdrFromFdno(open(self.download_package_header(right), 'rb')) - left_fi = rpm.fi(left_hdr) - try: - while left_fi.FN() != filename: - left_fi.next() - except StopIteration: - raise KeyError('Entry %s not found in %s' % (filename, left)) - right_fi = rpm.fi(right_hdr) - try: - while right_fi.FN() != filename: - right_fi.next() - except StopIteration: - raise KeyError('Entry %s not found in %s' % (filename, right)) - if librpm.rpmfiCompare(_rpm.fiFromFi(left_fi), _rpm.fiFromFi(right_fi)) == 0: - logger.debug('Conflict on %s between %s and %s permitted because files match', - filename, left, right) - return True - if left_fi.FColor() != right_fi.FColor(): - logger.debug('Conflict on %s between %s and %s permitted because colors differ', - filename, left, right) - return True - return False - - def find_conflicts(self): - """ - Find undeclared file conflicts in the packages under test. - - :return: List of str describing each conflict found - (or empty list if no conflicts were found) - """ - solver = self.pool.Solver() - problems = [] - for solvable in self.solvables: - logger.debug('Checking all files in %s for conflicts', solvable) - filenames = set(self._files_in_solvable(solvable)) - # In libsolv, iterating all solvables is fast, and listing all - # files in a solvable is fast, but finding solvables which contain - # a given file is *very slow* (see bug 1465736). - # Hence this approach, where we visit each solvable and use Python - # set operations to look for any overlapping filenames. - for conflicting in self.pool.solvables: - # Conflicts cannot happen between identical solvables and also - # between solvables with the same name - such solvables cannot - # be installed next to each other. - if conflicting == solvable or conflicting.name == solvable.name: - continue - conflict_filenames = filenames.intersection(self._files_in_solvable(conflicting)) - if not conflict_filenames: - continue - if not self._packages_can_be_installed_together(solvable, conflicting): - continue - for filename in conflict_filenames: - logger.debug('Considering conflict on %s with %s', filename, conflicting) - if not self._file_conflict_is_permitted(solvable, conflicting, filename): - msg = u'{} provides {} which is also provided by {}'.format( - six.text_type(solvable), filename, six.text_type(conflicting)) - problems.append(msg) - if conflicting not in self.solvables: - # For each filename we are checking, we only want to - # check at most *one* package from the remote - # repositories. This is purely an optimization to save - # network bandwidth and time. We *are* potentially - # missing some real conflicts by doing this, but the - # cost of downloading every package in the distro for - # common directories like /usr/lib/debug is too high. - # Note however that we do always ensure at least one - # *remote* candidate is checked (that is, not from the - # set of packages under test) to catch problems like - # bug 1502458. - logger.debug('Skipping further checks on %s ' - 'to save network bandwidth', filename) - filenames.remove(filename) - return sorted(problems) - - def find_upgrade_problems(self): - """ - Checks for any package in the repos which would upgrade or obsolete the - packages under test. - - :return: List of str describing each upgrade problem found (or - empty list if no problems were found) - """ - # Pretend the packages under test are installed, then solve a distupgrade. - # If any package under test would be erased, then it means some other - # package in the repos is better than it and we have a problem. - self.pool.installed = self.commandline_repo - try: - jobs = self.pool.Selection_all().jobs(solv.Job.SOLVER_UPDATE) - solver = self.pool.Solver() - solver.set_flag(solver.SOLVER_FLAG_ALLOW_UNINSTALL, True) - solver_problems = solver.solve(jobs) - for problem in solver_problems: - # This is a warning, not an error, because it means there are - # some *other* problems with existing packages in the - # repository, not our packages under test. But it means our - # results here might not be valid. - logger.warn('Upgrade candidate has pre-existing dependency problem: %s', problem) - transaction = solver.transaction() - problems = [] - for solvable in self.solvables: - action = transaction.steptype(solvable, transaction.SOLVER_TRANSACTION_SHOW_OBSOLETES) - other = transaction.othersolvable(solvable) - if action == transaction.SOLVER_TRANSACTION_IGNORE: - continue # it's kept, so no problem here - elif action == transaction.SOLVER_TRANSACTION_UPGRADED: - problems.append(u'{} would be upgraded by {} from repo {}'.format( - six.text_type(solvable), six.text_type(other), other.repo.name)) - elif action == transaction.SOLVER_TRANSACTION_OBSOLETED: - problems.append(u'{} would be obsoleted by {} from repo {}'.format( - six.text_type(solvable), six.text_type(other), other.repo.name)) - else: - raise RuntimeError('Unrecognised transaction step type %s' % action) - return problems - finally: - self.pool.installed = None diff --git a/rpmdeplint/cli.py b/rpmdeplint/cli.py deleted file mode 100644 index 841bdce..0000000 --- a/rpmdeplint/cli.py +++ /dev/null @@ -1,240 +0,0 @@ - -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. - -from __future__ import absolute_import - -import sys -import logging -import argparse -import pkg_resources -from rpmdeplint import DependencyAnalyzer, UnreadablePackageError -from rpmdeplint.repodata import Repo, RepoDownloadError, PackageDownloadError - -logger = logging.getLogger(__name__) - -version = pkg_resources.require('rpmdeplint')[0].version - -def cmd_check(args): - """ - Performs all checks on the given packages. - """ - failed = False - with dependency_analyzer_from_args(args) as analyzer: - logger.debug('Performing satisfiability check (check-sat)') - ok, result = analyzer.try_to_install_all() - if not ok: - sys.stderr.write(u'Problems with dependency set:\n') - sys.stderr.write(u'\n'.join(result.overall_problems) + u'\n') - failed = True - logger.debug('Performing repoclosure check (check-repoclosure)') - problems = analyzer.find_repoclosure_problems() - if problems: - sys.stderr.write(u'Dependency problems with repos:\n') - sys.stderr.write(u'\n'.join(problems) + u'\n') - failed = True - logger.debug('Performing file conflict check (check-conflicts)') - conflicts = analyzer.find_conflicts() - if conflicts: - sys.stderr.write(u'Undeclared file conflicts:\n') - sys.stderr.write(u'\n'.join(conflicts) + u'\n') - failed = True - logger.debug('Performing upgrade check (check-upgrade)') - problems = analyzer.find_upgrade_problems() - if problems: - sys.stderr.write(u'Upgrade problems:\n') - sys.stderr.write(u'\n'.join(problems) + u'\n') - failed = True - return 3 if failed else 0 - - -def cmd_check_sat(args): - """ - Checks that all dependencies needed to install the given packages - can be satisfied using the given repos. - """ - with dependency_analyzer_from_args(args) as analyzer: - ok, result = analyzer.try_to_install_all() - - if not ok: - sys.stderr.write(u'Problems with dependency set:\n') - sys.stderr.write(u'\n'.join(result.overall_problems) + u'\n') - return 3 - return 0 - - -def cmd_check_repoclosure(args): - """ - Checks that all dependencies of all packages in the given repos can still - be satisfied, when the given packages are included. - """ - with dependency_analyzer_from_args(args) as analyzer: - problems = analyzer.find_repoclosure_problems() - if problems: - sys.stderr.write(u'Dependency problems with repos:\n') - sys.stderr.write(u'\n'.join(problems) + u'\n') - return 3 - return 0 - - -def cmd_check_conflicts(args): - """ - Checks for undeclared file conflicts in the given packages. - """ - with dependency_analyzer_from_args(args) as analyzer: - conflicts = analyzer.find_conflicts() - if conflicts: - sys.stderr.write(u'Undeclared file conflicts:\n') - sys.stderr.write(u'\n'.join(conflicts) + u'\n') - return 3 - return 0 - - -def cmd_check_upgrade(args): - """ - Checks that the given packages are not older than any other existing - package in the repos. - """ - with dependency_analyzer_from_args(args) as analyzer: - problems = analyzer.find_upgrade_problems() - if problems: - sys.stderr.write(u'Upgrade problems:\n') - sys.stderr.write(u'\n'.join(problems) + u'\n') - return 3 - return 0 - - -def cmd_list_deps(args): - """ - Lists all (transitive) dependencies of the given packages -- that is, - the complete set of dependent packages which are needed - in order to install the packages under test. - """ - with dependency_analyzer_from_args(args) as analyzer: - ok, result = analyzer.try_to_install_all() - if not ok: - sys.stderr.write(u'Problems with dependency set:\n') - sys.stderr.write(u'\n'.join(result.overall_problems) + u'\n') - return 3 - - package_deps = result.package_dependencies - for pkg in package_deps.keys(): - deps = package_deps[pkg]['dependencies'] - sys.stdout.write(u"%s has %s dependencies:\n" % (pkg, len(deps))) - sys.stdout.write(u"\n".join(["\t" + x for x in deps])) - sys.stdout.write(u"\n\n") - return 0 - - -def log_to_stream(stream, level=logging.WARNING): - stream_handler = logging.StreamHandler(stream) - stream_handler.setLevel(level) - stream_handler.setFormatter(logging.Formatter('%(asctime)s %(name)s %(levelname)s %(message)s')) - logging.getLogger().handlers = [stream_handler] - - -def dependency_analyzer_from_args(args): - repos = [] - if args.repos_from_system: - repos.extend(Repo.from_yum_config()) - repos.extend(args.repos) - rpms = list(args.rpms) - arch = args.arch - - return DependencyAnalyzer(repos, rpms, arch=arch) - - -def comma_separated_repo(value): - if ',' not in value: - raise argparse.ArgumentTypeError( - 'Repo %r is not in the form ,' % value) - return Repo(*value.split(',', 1)) - - -def add_common_dependency_analyzer_args(parser): - parser.add_argument('rpms', metavar='RPMPATH', nargs='+', - help='Path to an RPM package to be checked') - parser.add_argument('-r', '--repo', metavar='NAME,URL', - type=comma_separated_repo, - action='append', dest='repos', default=[], - help='Name and URL of a repo to test against') - parser.add_argument('-R', '--repos-from-system', action='store_true', - help='Test against system repos from /etc/yum.repos.d/') - parser.add_argument('-a', '--arch', dest='arch', default=None, - help='Limit dependency resolution to ARCH packages [default: any arch]') - - -def validate_common_dependency_analyzer_args(parser, args): - if not args.repos and not args.repos_from_system: - parser.error('no repos specified to test against\n' - 'Use the --repo option to test against specific repository URLs,\n' - 'or use the --repos-from-system option to load the ' - 'system-wide repos from /etc/yum.repos.d/.') - - -def main(): - parser = argparse.ArgumentParser(description='Checks for errors in ' - 'RPM packages in the context of their dependency graph.', prog='rpmdeplint') - parser.add_argument('--debug', action='store_true', - help='Show detailed progress messages') - parser.add_argument('--quiet', action='store_true', - help='Show only errors') - parser.add_argument('--version', action='version', version='%(prog)s ' + version) - - subparsers = parser.add_subparsers(dest='subcommand', title='subcommands') - subparsers.required = True - - parser_check = subparsers.add_parser('check', - help='Perform all checks', - description=cmd_check.__doc__) - add_common_dependency_analyzer_args(parser_check) - parser_check.set_defaults(func=cmd_check) - - parser_check_sat = subparsers.add_parser('check-sat', - help='Check that dependencies can be satisfied', - description=cmd_check_sat.__doc__) - add_common_dependency_analyzer_args(parser_check_sat) - parser_check_sat.set_defaults(func=cmd_check_sat) - - parser_check_repoclosure = subparsers.add_parser('check-repoclosure', - help='Check that repo dependencies can still be satisfied', - description=cmd_check_repoclosure.__doc__) - add_common_dependency_analyzer_args(parser_check_repoclosure) - parser_check_repoclosure.set_defaults(func=cmd_check_repoclosure) - - parser_check_conflicts = subparsers.add_parser('check-conflicts', - help='Check for undeclared file conflicts', - description=cmd_check_conflicts.__doc__) - add_common_dependency_analyzer_args(parser_check_conflicts) - parser_check_conflicts.set_defaults(func=cmd_check_conflicts) - - parser_check_upgrade = subparsers.add_parser('check-upgrade', - help='Check package is an upgrade', - description=cmd_check_upgrade.__doc__) - add_common_dependency_analyzer_args(parser_check_upgrade) - parser_check_upgrade.set_defaults(func=cmd_check_upgrade) - - parser_list_deps = subparsers.add_parser('list-deps', - help='List all packages needed to satisfy dependencies', - description=cmd_list_deps.__doc__) - add_common_dependency_analyzer_args(parser_list_deps) - parser_list_deps.set_defaults(func=cmd_list_deps) - - args = parser.parse_args() - logging.getLogger().setLevel(logging.DEBUG) - log_to_stream(sys.stderr, level=logging.DEBUG if args.debug else logging.ERROR if args.quiet else logging.WARNING) - - validate_common_dependency_analyzer_args(parser, args) - - try: - return args.func(args) - except argparse.ArgumentTypeError as exc: - parser.error(str(exc)) - except (UnreadablePackageError, RepoDownloadError, PackageDownloadError) as exc: - sys.stderr.write('%s\n' % exc) - return 1 - -if __name__ == '__main__': - sys.exit(main()) diff --git a/rpmdeplint/repodata.py b/rpmdeplint/repodata.py deleted file mode 100644 index fdd8d50..0000000 --- a/rpmdeplint/repodata.py +++ /dev/null @@ -1,384 +0,0 @@ - -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. - -from __future__ import absolute_import - -import os -try: - from os import scandir # Python 3.5+ -except ImportError: - from scandir import scandir -import shutil -import logging -import tempfile -import requests -import errno -import glob -import time -from six.moves import configparser -import librepo -import rpm - -logger = logging.getLogger(__name__) -requests_session = requests.Session() - - -REPO_CACHE_DIR = os.path.join(os.sep, 'var', 'tmp') -REPO_CACHE_NAME_PREFIX = 'rpmdeplint-' - - -class PackageDownloadError(Exception): - """ - Raised if a package is being downloaded for further analysis but the download fails. - """ - pass - -class RepoDownloadError(Exception): - """ - Raised if an error occurs downloading repodata - """ - pass - -def get_yumvars(): - # This is not all the yumvars, but hopefully good enough... - - try: - import dnf.conf, dnf.rpm - except ImportError: - pass - else: - installroot = '' - subst = dnf.conf.Conf().substitutions - subst['releasever'] = dnf.rpm.detect_releasever(installroot) - return subst - - try: - import yum, yum.config, rpmUtils - except ImportError: - pass - else: - return { - 'arch': rpmUtils.arch.getCanonArch(), - 'basearch': rpmUtils.arch.getBaseArch(), - 'releasever': yum.config._getsysver('/', - ['system-release(releasever)', 'redhat-release']), - } - - # Probably not going to work but there's not much else we can do... - return { - 'arch': '$arch', - 'basearch': '$basearch', - 'releasever': '$releasever', - } - - -def substitute_yumvars(s, yumvars): - for name, value in yumvars.items(): - s = s.replace('$' + name, value) - return s - - -def cache_base_path(): - default_cache_home = os.path.join(os.path.expanduser('~'), '.cache') - cache_home = os.environ.get('XDG_CACHE_HOME', default_cache_home) - return os.path.join(cache_home, 'rpmdeplint') - - -def cache_entry_path(checksum): - return os.path.join(cache_base_path(), checksum[:1], checksum[1:]) - - -def clean_cache(): - expiry_time = time.time() - float(os.environ.get('RPMDEPLINT_EXPIRY_SECONDS', '604800')) - try: - subdirs = scandir(cache_base_path()) - except OSError as e: - if e.errno == errno.ENOENT: - return # nothing to do - else: - raise - for subdir in subdirs: - # Should be a subdirectory named after the first checksum letter - if not subdir.is_dir(follow_symlinks=False): - continue - for entry in scandir(subdir.path): - if not entry.is_file(follow_symlinks=False): - continue - if entry.stat().st_mtime < expiry_time: - logger.debug('Purging expired cache file %s', entry.path) - os.unlink(entry.path) - - -class Repo(object): - """ - Represents a Yum ("repomd") package repository to test dependencies against. - """ - - yum_main_config_path = '/etc/yum.conf' - yum_repos_config_glob = '/etc/yum.repos.d/*.repo' - - @classmethod - def from_yum_config(cls): - """ - Yields Repo instances loaded from the system-wide Yum - configuration in :file:`/etc/yum.conf` and :file:`/etc/yum.repos.d/`. - """ - yumvars = get_yumvars() - config = configparser.RawConfigParser() - config.read([cls.yum_main_config_path] + glob.glob(cls.yum_repos_config_glob)) - for section in config.sections(): - if section == 'main': - continue - if (config.has_option(section, 'enabled') and - not config.getboolean(section, 'enabled')): - continue - skip_if_unavailable = False - if config.has_option(section, 'skip_if_unavailable'): - skip_if_unavailable = config.getboolean(section, 'skip_if_unavailable') - if config.has_option(section, 'baseurl'): - baseurl = substitute_yumvars(config.get(section, 'baseurl'), yumvars) - yield cls(section, baseurl=baseurl, skip_if_unavailable=skip_if_unavailable) - elif config.has_option(section, 'metalink'): - metalink = substitute_yumvars(config.get(section, 'metalink'), yumvars) - yield cls(section, metalink=metalink, skip_if_unavailable=skip_if_unavailable) - elif config.has_option(section, 'mirrorlist'): - mirrorlist = substitute_yumvars(config.get(section, 'mirrorlist'), yumvars) - yield cls(section, metalink=mirrorlist, skip_if_unavailable=skip_if_unavailable) - else: - raise ValueError('Yum config section %s has no ' - 'baseurl or metalink or mirrorlist' % section) - - def __init__(self, repo_name, baseurl=None, metalink=None, skip_if_unavailable=False): - """ - :param repo_name: Name of the repository, for example "fedora-updates" - (used in problems and error messages) - :param baseurl: URL or filesystem path to the base of the repository - (there should be a repodata subdirectory under this) - :param metalink: URL to a Metalink file describing mirrors where - the repository can be found - :param skip_if_unavailable: If True, suppress errors downloading - repodata from the repository - - Exactly one of the *baseurl* or *metalink* parameters must be supplied. - """ - self.name = repo_name - if not baseurl and not metalink: - raise RuntimeError('Must specify either baseurl or metalink for repo') - self.baseurl = baseurl - self.metalink = metalink - self.skip_if_unavailable = skip_if_unavailable - - def download_repodata(self): - clean_cache() - logger.debug('Loading repodata for %s from %s', self.name, - self.baseurl or self.metalink) - self.librepo_handle = h = librepo.Handle() - r = librepo.Result() - h.repotype = librepo.LR_YUMREPO - if self.baseurl: - h.urls = [self.baseurl] - if self.metalink: - h.mirrorlist = self.metalink - h.setopt(librepo.LRO_DESTDIR, tempfile.mkdtemp(self.name, - prefix=REPO_CACHE_NAME_PREFIX, dir=REPO_CACHE_DIR)) - h.setopt(librepo.LRO_INTERRUPTIBLE, True) - h.setopt(librepo.LRO_YUMDLIST, []) - if self.baseurl and os.path.isdir(self.baseurl): - self._download_metadata_result(h, r) - self._yum_repomd = r.yum_repomd - self._root_path = self.baseurl - self.primary = open(self.primary_url, 'rb') - self.filelists = open(self.filelists_url, 'rb') - else: - self._root_path = h.destdir = tempfile.mkdtemp(self.name, - prefix=REPO_CACHE_NAME_PREFIX, dir=REPO_CACHE_DIR) - self._download_metadata_result(h, r) - self._yum_repomd = r.yum_repomd - self.primary = self._download_repodata_file( - self.primary_checksum, self.primary_url) - self.filelists = self._download_repodata_file( - self.filelists_checksum, self.filelists_url) - - def _download_metadata_result(self, handle, result): - try: - handle.perform(result) - except librepo.LibrepoException as ex: - raise RepoDownloadError('Failed to download repodata for %r: %s' - % (self, ex.args[1])) - - def _download_repodata_file(self, checksum, url): - """ - Each created file in cache becomes immutable, and is referenced in - the directory tree within XDG_CACHE_HOME as - $XDG_CACHE_HOME/rpmdeplint// - - Both metadata and the files to be cached are written to a tempdir first - then renamed to the cache dir atomically to avoid them potentially being - accessed before written to cache. - """ - filepath_in_cache = cache_entry_path(checksum) - try: - f = open(filepath_in_cache, 'rb') - except IOError as e: - if e.errno == errno.ENOENT: - pass # cache entry does not exist, we will download it - elif e.errno == errno.EISDIR: - # This is the original cache directory layout, merged in commit - # 6f11c3708 although it didn't appear in any released version - # of rpmdeplint. To be helpful we will fix it up, by just - # deleting the directory and letting it be replaced by a file. - shutil.rmtree(filepath_in_cache, ignore_errors=True) - else: - raise - else: - logger.debug('Using cached file %s for %s', filepath_in_cache, url) - # Bump the modtime on the cache file we are using, - # since our cache expiry is LRU based on modtime. - if os.utime in getattr(os, 'supports_fd', []): - os.utime(f.fileno()) # Python 3.3+ - else: - os.utime(filepath_in_cache, None) - return f - try: - os.makedirs(os.path.dirname(filepath_in_cache)) - except OSError as e: - if e.errno != errno.EEXIST: - raise - fd, temp_path = tempfile.mkstemp(dir=os.path.dirname(filepath_in_cache), text=False) - logger.debug('Downloading %s to cache temp file %s', url, temp_path) - try: - f = os.fdopen(fd, 'wb+') - except: - os.close(fd) - raise - try: - try: - response = requests_session.get(url, stream=True) - response.raise_for_status() - for chunk in response.raw.stream(decode_content=False): - f.write(chunk) - response.close() - except IOError as e: - raise RepoDownloadError('Failed to download repodata file %s for %r: %s' - % (os.path.basename(url), self, e)) - f.flush() - f.seek(0) - os.fchmod(f.fileno(), 0o644) - os.rename(temp_path, filepath_in_cache) - logger.debug('Using cached file %s for %s', filepath_in_cache, url) - return f - except: - f.close() - os.unlink(temp_path) - raise - - def _is_header_complete(self, local_path): - """ - Returns `True` if the RPM file `local_path` has complete RPM header. - """ - try: - with open(local_path, 'rb') as f: - try: - ts = rpm.TransactionSet() - ts.setVSFlags(rpm._RPMVSF_NOSIGNATURES) - - # Supress the RPM error message printed to stderr in case - # the header is not complete. Set the log verbosity to CRIT - # to achieve that. This way the critical errors are still - # logged, but the expected "bad header" error is not. - rpm.setVerbosity(rpm.RPMLOG_CRIT) - ts.hdrFromFdno(f) - return True - except rpm.error as e: - return False - finally: - # Revert back to RPMLOG_ERR. - rpm.setVerbosity(rpm.RPMLOG_ERR) - except FileNotFoundError: - return False - - def download_package_header(self, location, baseurl): - """ - Downloads the package header so it can be parsed by `hdrFromFdno`. - - There is no function provided by the Python `rpm` module which would - return the size of RPM header. This method therefore tries to download - first N bytes of the RPM file and checks if the header is complete or - not using the `hdrFromFdno` RPM funtion. - - As the header size can be very different from package to package, it - tries to download first 100KB and if header is not complete, it - fallbacks to 1MB and 5MB. If that is not enough, the final fallback - downloads whole RPM file. - - This strategy still wastes some bandwidth, because we are downloading - first N bytes repeatedly, but because header of typical RPM fits - into first 100KB usually and because the RPM data is much bigger than - what we download repeatedly, it saves lot of time and bandwidth overall. - - Checksums cannot be checked by this method, because checksums work - only when complete RPM file is downloaded. - """ - if self.librepo_handle.local: - logger.debug('Using package %s from local filesystem directly', local_path) - return local_path - - # Check if we already downloaded this file and return it if so. - local_path = os.path.join(self._root_path, os.path.basename(location)) - if self._is_header_complete(local_path): - logger.debug("Using already downloaded package from %s", local_path) - return local_path - - logger.debug('Loading package %s from repo %s', location, self.name) - for byterangeend in [100000, 1000000, 5000000, 0]: - target = librepo.PackageTarget(location, - base_url=baseurl, - dest=self._root_path, - handle=self.librepo_handle, - byterangestart=0, - byterangeend=byterangeend - ) - - if byterangeend: - logger.debug('Download first %s bytes of %s', byterangeend, location) - else: - logger.debug('Download %s', location) - librepo.download_packages([target]) - if target.err and target.err != 'Already downloaded': - raise PackageDownloadError('Failed to download %s from repo %s: %s' - % (location, self.name, target.err)) - else: - if self._is_header_complete(target.local_path): - break - - logger.debug('Saved as %s', target.local_path) - return target.local_path - - @property - def yum_repomd(self): - return self._yum_repomd - @property - def repomd_fn(self): - return os.path.join(self._root_path, 'repodata', 'repomd.xml') - @property - def primary_url(self): - return os.path.join(self.baseurl, self.yum_repomd['primary']['location_href']) - @property - def primary_checksum(self): - return self.yum_repomd['primary']['checksum'] - @property - def filelists_checksum(self): - return self.yum_repomd['filelists']['checksum'] - @property - def filelists_url(self): - return os.path.join(self.baseurl, self.yum_repomd['filelists']['location_href']) - - def __repr__(self): - if self.baseurl: - return '%s(repo_name=%r, baseurl=%r)' % (self.__class__.__name__, self.name, self.baseurl) - if self.metalink: - return '%s(repo_name=%r, metalink=%r)' % (self.__class__.__name__, self.name, self.metalink) - return '%s(repo_name=%r)' % (self.__class__.__name__, self.name) diff --git a/rpmdeplint/tests/__init__.py b/rpmdeplint/tests/__init__.py deleted file mode 100644 index e69de29..0000000 --- a/rpmdeplint/tests/__init__.py +++ /dev/null diff --git a/rpmdeplint/tests/test_dependency_analyzer.py b/rpmdeplint/tests/test_dependency_analyzer.py deleted file mode 100644 index 33c2ecb..0000000 --- a/rpmdeplint/tests/test_dependency_analyzer.py +++ /dev/null @@ -1,83 +0,0 @@ - -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. - -import shutil -from unittest import TestCase -from rpmdeplint import DependencyAnalyzer -from rpmdeplint.repodata import Repo -import os - -try: - from rpmfluff import SimpleRpmBuild - from rpmfluff import YumRepoBuild -except ImportError: - from rpmfluff.rpmbuild import SimpleRpmBuild - from rpmfluff.yumrepobuild import YumRepoBuild - -class TestDependencyAnalyzer(TestCase): - def test_repos(self): - lemon = SimpleRpmBuild('lemon', '1', '3', ['noarch']) - lemon.add_provides('lemon-juice') - lemon.add_provides('lemon-zest') - self.addCleanup(lemon.clean) - peeler = SimpleRpmBuild('peeler', '4', '0', ['x86_64']) - self.addCleanup(peeler.clean) - cinnamon = SimpleRpmBuild('cinnamon', '3', '0', ['noarch']) - self.addCleanup(cinnamon.clean) - apple_pie = SimpleRpmBuild('apple-pie', '1.9', '1', ['x86_64']) - apple_pie.add_requires('apple-lib') - apple_pie.add_requires('lemon-juice') - apple_pie.add_requires('cinnamon >= 2.0') - self.addCleanup(apple_pie.clean) - base_1_repo = YumRepoBuild([lemon, peeler, cinnamon, apple_pie]) - base_1_repo.make('x86_64', 'noarch') - - apple = SimpleRpmBuild('apple', '4.9', '3', ['x86_64']) - apple.add_provides('apple-lib') - apple.add_requires('peeler') - apple.add_requires('lemon-juice') - apple.make() - self.addCleanup(apple.clean) - lemon_meringue_pie = SimpleRpmBuild('lemon-meringue-pie', '1', '0', ['x86_64']) - lemon_meringue_pie.add_requires('lemon-zest') - lemon_meringue_pie.add_requires('lemon-juice') - lemon_meringue_pie.add_requires('egg-whites') - lemon_meringue_pie.add_requires('egg-yolks') - lemon_meringue_pie.add_requires('sugar') - lemon_meringue_pie.make() - self.addCleanup(lemon_meringue_pie.clean) - - da = DependencyAnalyzer( - repos=[Repo(repo_name='base_1', baseurl=base_1_repo.repoDir)], - packages=[apple.get_built_rpm('x86_64'), - lemon_meringue_pie.get_built_rpm('x86_64')]) - - ok, dependency_set = da.try_to_install_all() - self.assertEqual(False, ok) - self.assertEqual(1, len(dependency_set.overall_problems)) - self.assertEqual(['nothing provides egg-whites needed by lemon-meringue-pie-1-0.x86_64', - 'nothing provides egg-whites needed by lemon-meringue-pie-1-0.x86_64'], - dependency_set.package_dependencies['lemon-meringue-pie-1-0.x86_64']['problems']) - - eggs = SimpleRpmBuild('eggs', '1', '3', ['noarch']) - eggs.add_provides('egg-whites') - eggs.add_provides('egg-yolks') - self.addCleanup(eggs.clean) - sugar = SimpleRpmBuild('sugar', '4', '0', ['x86_64']) - self.addCleanup(sugar.clean) - base_2_repo = YumRepoBuild([eggs, sugar]) - base_2_repo.make('x86_64', 'noarch') - - da = DependencyAnalyzer( - repos=[Repo(repo_name='base_1', baseurl=base_1_repo.repoDir), - Repo(repo_name='base_2', baseurl=base_2_repo.repoDir)], - packages=[apple.get_built_rpm('x86_64'), - lemon_meringue_pie.get_built_rpm('x86_64')]) - - ok, dependency_set = da.try_to_install_all() - self.assertEqual(True, ok) - self.assertEqual(4, len(dependency_set.package_dependencies['lemon-meringue-pie-1-0.x86_64']['dependencies'])) - self.assertEqual(3, len(dependency_set.package_dependencies['apple-4.9-3.x86_64']['dependencies'])) diff --git a/rpmdeplint/tests/test_dependency_set.py b/rpmdeplint/tests/test_dependency_set.py deleted file mode 100644 index 6576fb5..0000000 --- a/rpmdeplint/tests/test_dependency_set.py +++ /dev/null @@ -1,81 +0,0 @@ - -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. - -from unittest import TestCase -from rpmdeplint import DependencySet -class test_pkg: - def __init__(self, name, reponame): - self._name = name - self._reponame = reponame - @property - def reponame(self): - return self._reponame - def __str__(self): - return self._name - -class TestDependencySet(TestCase): - _beaker_client_deps = ['basesystem-11-1.fc23.noarch', - 'bash-4.3.42-1.fc23.x86_64', - 'beaker-common-22.1-1.fc22.noarch', - 'bzip2-libs-1.0.6-17.fc23.x86_64', - 'ca-certificates-2015.2.5-1.0.fc23.noarch', - 'chkconfig-1.6-1.fc23.x86_64', - 'coreutils-8.24-4.fc23.x86_64', - 'crypto-policies-20150518-3.gitffe885e.fc23.noarch', - 'expat-2.1.0-12.fc23.x86_64', - 'fedora-release-23-1.noarch', - 'fedora-repos-23-1.noarch', - 'filesystem-3.2-35.fc23.x86_64', - 'gawk-4.1.3-2.fc23.x86_64', - 'gdbm-1.11-6.fc23.x86_64', - 'glibc-2.22-3.fc23.x86_64', - 'glibc-common-2.22-3.fc23.x86_64', - 'gmp-1:6.0.0-12.fc23.x86_64', - 'grep-2.21-7.fc23.x86_64', - 'info-6.0-1.fc23.x86_64', - 'keyutils-libs-1.5.9-7.fc23.x86_64', - 'krb5-libs-1.13.2-11.fc23.x86_64', - 'libacl-2.2.52-10.fc23.x86_64', - 'libattr-2.4.47-14.fc23.x86_64', - 'libcap-2.24-8.fc23.x86_64', - 'libcom_err-1.42.13-3.fc23.x86_64', - 'libdb-5.3.28-13.fc23.x86_64', - 'libffi-3.1-8.fc23.x86_64', - 'libgcc-5.1.1-4.fc23.x86_64', - 'libselinux-2.4-4.fc23.x86_64', - 'libsepol-2.4-1.fc23.x86_64', - 'libstdc++-5.1.1-4.fc23.x86_64', - 'libtasn1-4.5-2.fc23.x86_64', - 'libverto-0.2.6-5.fc23.x86_64', - 'ncurses-5.9-21.20150214.fc23.x86_64', - 'ncurses-base-5.9-21.20150214.fc23.noarch', - 'ncurses-libs-5.9-21.20150214.fc23.x86_64', - 'nss-softokn-freebl-3.20.0-1.0.fc23.x86_64', - 'openssl-libs-1:1.0.2d-2.fc23.x86_64', - 'p11-kit-0.23.1-4.fc23.x86_64', - 'p11-kit-trust-0.23.1-4.fc23.x86_64', - 'pcre-8.37-4.fc23.x86_64', - 'popt-1.16-6.fc23.x86_64', - 'python-2.7.10-8.fc23.x86_64', - 'python-libs-2.7.10-8.fc23.x86_64', - 'python-pip-7.1.0-1.fc23.noarch', - 'python-setuptools-18.0.1-2.fc23.noarch', - 'readline-6.3-6.fc23.x86_64', - 'sed-4.2.2-11.fc23.x86_64', - 'setup-2.9.8-2.fc23.noarch', - 'sqlite-3.8.11.1-1.fc23.x86_64', - 'tzdata-2015g-1.fc23.noarch', - 'zlib-1.2.8-9.fc23.x86_64'] - def test_simple(self): - ds = DependencySet() - beaker_common = 'beaker-common-22.1-1.fc22.noarch' - ds.add_package(beaker_common, - map(lambda x: test_pkg(x, 'fedora_23'), self._beaker_client_deps), - []) - - self.assertEqual(1, len(ds.packages)) - self.assertEqual(beaker_common, ds.packages[0]) - self.assertEqual(len(self._beaker_client_deps), len(ds.package_dependencies[beaker_common]['dependencies'])) diff --git a/rpmdeplint/tests/test_repodata.py b/rpmdeplint/tests/test_repodata.py deleted file mode 100644 index b949307..0000000 --- a/rpmdeplint/tests/test_repodata.py +++ /dev/null @@ -1,121 +0,0 @@ - -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. - -import os -import platform -import pytest -from rpmdeplint.repodata import Repo, RepoDownloadError, get_yumvars - - -@pytest.fixture -def yumdir(tmpdir, monkeypatch): - tmpdir.join('yum.conf').write('[main]\n') - monkeypatch.setattr(Repo, 'yum_main_config_path', str(tmpdir.join('yum.conf'))) - monkeypatch.setattr(Repo, 'yum_repos_config_glob', str(tmpdir.join('yum.repos.d', '*.repo'))) - return tmpdir - - -def test_loads_system_yum_repo_with_baseurl(yumdir): - yumdir.join('yum.repos.d', 'dummy.repo').write( - '[dummy]\nname=Dummy\nbaseurl=http://example.invalid/dummy\n', - ensure=True) - - repos = list(Repo.from_yum_config()) - assert len(repos) == 1 - assert repos[0].name == 'dummy' - assert repos[0].baseurl == 'http://example.invalid/dummy' - assert repos[0].metalink == None - - -def test_loads_system_yum_repo_with_metalink(yumdir): - yumdir.join('yum.repos.d', 'dummy.repo').write( - '[dummy]\nname=Dummy\nmetalink=http://example.invalid/dummy\n', - ensure=True) - - repos = list(Repo.from_yum_config()) - assert len(repos) == 1 - assert repos[0].name == 'dummy' - assert repos[0].baseurl == None - assert repos[0].metalink == 'http://example.invalid/dummy' - - -def test_loads_system_yum_repo_with_mirrorlist(yumdir): - yumdir.join('yum.repos.d', 'dummy.repo').write( - '[dummy]\nname=Dummy\nmirrorlist=http://example.invalid/dummy\n', - ensure=True) - - repos = list(Repo.from_yum_config()) - assert len(repos) == 1 - assert repos[0].name == 'dummy' - assert repos[0].baseurl == None - assert repos[0].metalink == 'http://example.invalid/dummy' - - -def test_skips_disabled_system_yum_repo(yumdir): - yumdir.join('yum.repos.d', 'dummy.repo').write( - '[dummy]\nname=Dummy\nbaseurl=http://example.invalid/dummy\nenabled=0\n', - ensure=True) - - repos = list(Repo.from_yum_config()) - assert len(repos) == 0 - - -def test_loads_system_yum_repo_with_substitutions(yumdir, monkeypatch): - yumdir.join('yum.repos.d', 'dummy.repo').write( - '[dummy]\nname=Dummy\nbaseurl=http://example.invalid/$releasever/$basearch/\n', - ensure=True) - monkeypatch.setattr('rpmdeplint.repodata.get_yumvars', lambda: { - 'releasever': '21', - 'basearch': 's390x', - }) - - repos = list(Repo.from_yum_config()) - assert len(repos) == 1 - assert repos[0].name == 'dummy' - assert repos[0].baseurl == 'http://example.invalid/21/s390x/' - - -def test_yumvars(): - # The expected values are dependent on the system where we are running, and - # also will be different in mock for example (where neither yum nor dnf are - # present). So the best we can do is touch the code path and makes sure it - # gives back some values. - yumvars = get_yumvars() - if 'ID=fedora\nVERSION_ID=25\n' in open('/etc/os-release').read() and \ - os.path.exists('/usr/bin/dnf') and platform.machine() == 'x86_64': - # The common case on developer's machines - assert yumvars['arch'] == 'x86_64' - assert yumvars['basearch'] == 'x86_64' - assert yumvars['releasever'] == '25' - else: - # Everywhere else, just assume it's fine - assert 'arch' in yumvars - assert 'basearch' in yumvars - assert 'releasever' in yumvars - - -def test_bad_repo_url_raises_error(yumdir): - yumdir.join('yum.repos.d', 'dummy.repo').write( - '[dummy]\nname=Dummy\nbaseurl=http://example.invalid/dummy\nenabled=1\n', - ensure=True) - - repos = list(Repo.from_yum_config()) - assert len(repos) == 1 - with pytest.raises(RepoDownloadError) as rde: - repos[0].download_repodata() - assert 'Cannot download repomd.xml' in str(rde.value) - assert "repo_name='dummy'" in str(rde.value) - - -def test_skip_if_unavailable_is_obeyed(yumdir): - yumdir.join('yum.repos.d', 'dummy.repo').write( - '[dummy]\nname=Dummy\nbaseurl=http://example.invalid/dummy\nenabled=1\nskip_if_unavailable=1\n', - ensure=True) - - repos = list(Repo.from_yum_config()) - assert len(repos) == 1 - assert repos[0].name == 'dummy' - assert repos[0].skip_if_unavailable == True diff --git a/setup.py b/setup.py deleted file mode 100644 index e8ec502..0000000 --- a/setup.py +++ /dev/null @@ -1,52 +0,0 @@ -from glob import glob -from setuptools import setup -from distutils.command.build import build as _build - - -# Customization of standard build step: Automatically build the docs as well in -# one build step: python setup.py build -class build(_build): - sub_commands = _build.sub_commands + [('build_sphinx', lambda self: True)] - - -# Note that we have some scripts which programmatically change the version -# declared here. Do not adjust the formatting. -name = 'rpmdeplint' -version = '1.4' -release = version - -setup(name='rpmdeplint', - version=version, - description='Tool to find errors in RPM packages in the context of their dependency graph', - long_description=open('README.rst').read(), - url='https://pagure.io/rpmdeplint', - author='Red Hat, Inc.', - author_email='qa-devel@lists.fedoraproject.org', - classifiers=[ - 'License :: OSI Approved :: GNU General Public License v2 or later (GPLv2+)', - 'Programming Language :: Python :: 2.7', - 'Programming Language :: Python :: 3', - ], - packages=['rpmdeplint', 'rpmdeplint.tests'], - install_requires=['setuptools', 'six'], - tests_require=['pytest'], - data_files = [ - ('/usr/share/man/man1', glob('build/sphinx/man/*.1')), - ], - cmdclass = { - 'build': build, - }, - command_options={ - 'build_sphinx': { - 'builder': ('setup.py', 'man'), - 'project': ('setup.py', name), - 'version': ('setup.py', version), - 'release': ('setup.py', release), - } - }, - entry_points={ - 'console_scripts': [ - 'rpmdeplint = rpmdeplint.cli:main', - ] - }, -) diff --git a/tag-release.sh b/tag-release.sh deleted file mode 100755 index b27c304..0000000 --- a/tag-release.sh +++ /dev/null @@ -1,34 +0,0 @@ -#!/bin/bash - -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. - -set -e - -version="$1" -prerelease="$2" - -if [ -z "$version" ] ; then - echo "Usage: $0 []" >&2 - echo "Example: $0 1.0 rc1" >&2 - exit 1 -fi - -if git status --porcelain | grep -q '^.M' ; then - echo "Work tree has modifications, stash or add before tagging" >&2 - exit 1 -fi - -sed -i -e "/%global upstream_version /c\%global upstream_version ${version}${prerelease}" rpmdeplint.spec -sed -i -e "/^Version:/c\Version: $version" rpmdeplint.spec -if [ -n "$prerelease" ] ; then - sed -i -e "/^Release:/c\Release: 0.$prerelease%{?dist}" rpmdeplint.spec -else - sed -i -e "/^Release:/c\Release: 1%{?dist}" rpmdeplint.spec -fi -sed -i -e "/^version = /c\version = '$version$prerelease'" setup.py -git add setup.py rpmdeplint.spec -git commit -m "Automatic commit of release $version$prerelease" -git tag -a "rpmdeplint-$version$prerelease" -m "Tagging release $version$prerelease"