Esempio n. 1
0
 def load_package_cache(self):
     self.package_cache = PackageCache(os.path.join(self.db_directory, 'package-cache.sqlite3'))
Esempio n. 2
0
 def load_package_cache(self):
     """Prepare a temporary package cache for the duration of a test."""
     self.package_cache = PackageCache(directory=self.db_directory)
Esempio n. 3
0
class DebPkgToolsTestCase(unittest.TestCase):

    def setUp(self):
        coloredlogs.install()
        coloredlogs.set_level(logging.DEBUG)
        self.db_directory = tempfile.mkdtemp()
        self.load_package_cache()
        os.environ['DPT_FORCE_ENTROPY'] = 'yes'

    def load_package_cache(self):
        self.package_cache = PackageCache(os.path.join(self.db_directory, 'package-cache.sqlite3'))

    def tearDown(self):
        self.package_cache.collect_garbage(force=True)
        shutil.rmtree(self.db_directory)
        os.environ.pop('DPT_FORCE_ENTROPY')

    def test_package_cache_error_handling(self):
        self.assertRaises(KeyError, self.package_cache.__getitem__, '/some/random/non-existing/path')

    def test_file_copying(self):
        with Context() as finalizers:
            source_directory = finalizers.mkdtemp()
            target_directory = finalizers.mkdtemp()
            touch(os.path.join(source_directory, '42'))
            copy_package_files(source_directory, target_directory, hard_links=True)
            self.assertEqual(os.stat(os.path.join(source_directory, '42')).st_ino,
                             os.stat(os.path.join(target_directory, '42')).st_ino)

    def test_package_cache_invalidation(self):
        with Context() as finalizers:
            directory = finalizers.mkdtemp()
            package_file = self.test_package_building(directory, overrides=dict(Package='deb-pkg-tools-package-1', Version='1'))
            for i in range(5):
                fields, contents = inspect_package(package_file, cache=self.package_cache)
                if i % 2 == 0:
                    os.utime(package_file, None)
                else:
                    self.load_package_cache()

    def test_find_latest_version(self):
        good = ['name_1.0_all.deb', 'name_0.5_all.deb']
        self.assertEqual(os.path.basename(find_latest_version(good).filename), 'name_1.0_all.deb')
        bad= ['one_1.0_all.deb', 'two_0.5_all.deb']
        self.assertRaises(ValueError, find_latest_version, bad)

    def test_group_by_latest_versions(self):
        packages = ['one_1.0_all.deb', 'one_0.5_all.deb', 'two_1.5_all.deb', 'two_0.1_all.deb']
        self.assertEqual(sorted(os.path.basename(a.filename) for a in group_by_latest_versions(packages).values()),
                         sorted(['one_1.0_all.deb', 'two_1.5_all.deb']))

    def test_control_field_parsing(self):
        deb822_package = Deb822(['Package: python-py2deb',
                                 'Depends: python-deb-pkg-tools, python-pip, python-pip-accel',
                                 'Installed-Size: 42'])
        parsed_info = parse_control_fields(deb822_package)
        self.assertEqual(parsed_info,
                         {'Package': 'python-py2deb',
                          'Depends': RelationshipSet(
                              Relationship(name=u'python-deb-pkg-tools'),
                              Relationship(name=u'python-pip'),
                              Relationship(name=u'python-pip-accel')),
                          'Installed-Size': 42})
        # Test backwards compatibility with the old interface where `Depends'
        # like fields were represented as a list of strings (shallow parsed).
        parsed_info['Depends'] = [unicode(r) for r in parsed_info['Depends']]
        self.assertEqual(unparse_control_fields(parsed_info), deb822_package)
        # Test compatibility with fields like `Depends' containing a string.
        parsed_info['Depends'] = deb822_package['Depends']
        self.assertEqual(unparse_control_fields(parsed_info), deb822_package)

    def test_control_field_merging(self):
        defaults = Deb822(['Package: python-py2deb',
                           'Depends: python-deb-pkg-tools',
                           'Architecture: all'])
        # The field names of the dictionary with overrides are lower case on
        # purpose; control file merging should work properly regardless of
        # field name casing.
        overrides = Deb822(dict(version='1.0',
                                depends='python-pip, python-pip-accel',
                                architecture='amd64'))
        self.assertEqual(merge_control_fields(defaults, overrides),
                         Deb822(['Package: python-py2deb',
                                 'Version: 1.0',
                                 'Depends: python-deb-pkg-tools, python-pip, python-pip-accel',
                                 'Architecture: amd64']))

    def test_control_file_patching_and_loading(self):
        deb822_package = Deb822(['Package: unpatched-example',
                                 'Depends: some-dependency'])
        with Context() as finalizers:
            control_file = tempfile.mktemp()
            finalizers.register(os.unlink, control_file)
            with open(control_file, 'wb') as handle:
                deb822_package.dump(handle)
            call('--patch=%s' % control_file,
                 '--set=Package: patched-example',
                 '--set=Depends: another-dependency')
            patched_fields = load_control_file(control_file)
            self.assertEqual(patched_fields['Package'], 'patched-example')
            self.assertEqual(str(patched_fields['Depends']), 'another-dependency, some-dependency')

    def test_version_comparison(self):
        self.version_comparison_helper()
        if version.have_python_apt:
            version.have_python_apt = False
            self.version_comparison_helper()
            self.assertRaises(NotImplementedError, version.compare_versions_with_python_apt, '0.1', '<<', '0.2')
            version.have_python_apt = True

    def version_comparison_helper(self):
        # V() shortcut for deb_pkg_tools.version.Version().
        V = version.Version
        # Check version sorting implemented on top of `=' and `<<' comparisons.
        expected_order = ['0.1', '0.5', '1.0', '2.0', '3.0', '1:0.4', '2:0.3']
        self.assertNotEqual(list(sorted(expected_order)), expected_order)
        self.assertEqual(list(sorted(map(V, expected_order))), expected_order)
        # Check each individual operator (to make sure the two implementations
        # agree). We use the Version() class for this so that we test both
        # compare_versions() and the Version() wrapper.
        # Test `>'.
        self.assertTrue(V('1.0') > V('0.5')) # usual semantics
        self.assertTrue(V('1:0.5') > V('2.0')) # unusual semantics
        self.assertFalse(V('0.5') > V('2.0')) # sanity check
        # Test `>='.
        self.assertTrue(V('0.75') >= V('0.5')) # usual semantics
        self.assertTrue(V('0.50') >= V('0.5')) # usual semantics
        self.assertTrue(V('1:0.5') >= V('5.0')) # unusual semantics
        self.assertFalse(V('0.2') >= V('0.5')) # sanity check
        # Test `<'.
        self.assertTrue(V('0.5') < V('1.0')) # usual semantics
        self.assertTrue(V('2.0') < V('1:0.5')) # unusual semantics
        self.assertFalse(V('2.0') < V('0.5')) # sanity check
        # Test `<='.
        self.assertTrue(V('0.5') <= V('0.75')) # usual semantics
        self.assertTrue(V('0.5') <= V('0.50')) # usual semantics
        self.assertTrue(V('5.0') <= V('1:0.5')) # unusual semantics
        self.assertFalse(V('0.5') <= V('0.2')) # sanity check
        # Test `=='.
        self.assertTrue(V('42') == V('42')) # usual semantics
        self.assertTrue(V('0.5') == V('0:0.5')) # unusual semantics
        self.assertFalse(V('0.5') == V('1.0')) # sanity check
        # Test `!='.
        self.assertTrue(V('1') != V('0')) # usual semantics
        self.assertFalse(V('0.5') != V('0:0.5')) # unusual semantics

    def test_relationship_parsing(self):
        # Happy path (no parsing errors).
        relationship_set = parse_depends('foo, bar (>= 1) | baz')
        self.assertEqual(relationship_set.relationships[0].name, 'foo')
        self.assertEqual(relationship_set.relationships[1].relationships[0].name, 'bar')
        self.assertEqual(relationship_set.relationships[1].relationships[0].operator, '>=')
        self.assertEqual(relationship_set.relationships[1].relationships[0].version, '1')
        self.assertEqual(relationship_set.relationships[1].relationships[1].name, 'baz')
        self.assertEqual(parse_depends('foo (=1.0)'), RelationshipSet(VersionedRelationship(name='foo', operator='=', version='1.0')))
        # Unhappy path (parsing errors).
        self.assertRaises(ValueError, parse_depends, 'foo (bar) (baz)')
        self.assertRaises(ValueError, parse_depends, 'foo (bar baz qux)')

    def test_relationship_unparsing(self):
        relationship_set = parse_depends('foo, bar(>=1)|baz')
        self.assertEqual(unicode(relationship_set), 'foo, bar (>= 1) | baz')
        self.assertEqual(compact(repr(relationship_set)), "RelationshipSet(Relationship(name='foo'), AlternativeRelationship(VersionedRelationship(name='bar', operator='>=', version='1'), Relationship(name='baz')))")

    def test_relationship_evaluation(self):
        # Relationships without versions.
        relationship_set = parse_depends('python')
        self.assertTrue(relationship_set.matches('python'))
        self.assertFalse(relationship_set.matches('python2.7'))
        self.assertEqual(list(relationship_set.names), ['python'])
        # Alternatives (OR) without versions.
        relationship_set = parse_depends('python2.6 | python2.7')
        self.assertFalse(relationship_set.matches('python2.5'))
        self.assertTrue(relationship_set.matches('python2.6'))
        self.assertTrue(relationship_set.matches('python2.7'))
        self.assertFalse(relationship_set.matches('python3.0'))
        self.assertEqual(sorted(relationship_set.names), ['python2.6', 'python2.7'])
        # Combinations (AND) with versions.
        relationship_set = parse_depends('python (>= 2.6), python (<< 3) | python (>= 3.4)')
        self.assertFalse(relationship_set.matches('python', '2.5'))
        self.assertTrue(relationship_set.matches('python', '2.6'))
        self.assertTrue(relationship_set.matches('python', '2.7'))
        self.assertFalse(relationship_set.matches('python', '3.0'))
        self.assertTrue(relationship_set.matches('python', '3.4'))
        self.assertEqual(list(relationship_set.names), ['python'])
        # Testing for matches without providing a version is valid (should not
        # raise an error) but will never match a relationship with a version.
        relationship_set = parse_depends('python (>= 2.6), python (<< 3)')
        self.assertTrue(relationship_set.matches('python', '2.7'))
        self.assertFalse(relationship_set.matches('python'))
        self.assertEqual(list(relationship_set.names), ['python'])
        # Distinguishing between packages whose name was matched but whose
        # version didn't match vs packages whose name wasn't matched.
        relationship_set = parse_depends('python (>= 2.6), python (<< 3) | python (>= 3.4)')
        self.assertEqual(relationship_set.matches('python', '2.7'), True) # name and version match
        self.assertEqual(relationship_set.matches('python', '2.5'), False) # name matched, version didn't
        self.assertEqual(relationship_set.matches('python2.6'), None) # name didn't match
        self.assertEqual(relationship_set.matches('python', '3.0'), False) # name in alternative matched, version didn't
        self.assertEqual(list(relationship_set.names), ['python'])

    def test_relationship_sorting(self):
        relationship_set = parse_depends('foo | bar, baz | qux')
        self.assertEqual(relationship_set, RelationshipSet(
            AlternativeRelationship(Relationship(name='baz'), Relationship(name='qux')),
            AlternativeRelationship(Relationship(name='foo'), Relationship(name='bar'))))

    def test_custom_pretty_printer(self):
        printer = CustomPrettyPrinter()
        # Test pretty printing of debian.deb822.Deb822 objects.
        self.assertEqual(remove_unicode_prefixes(printer.pformat(deb822_from_string('''
            Package: pretty-printed-control-fields
            Version: 1.0
            Architecture: all
        '''))), remove_unicode_prefixes(dedent('''
            {'Architecture': u'all',
             'Package': u'pretty-printed-control-fields',
             'Version': u'1.0'}
        ''')))
        # Test pretty printing of RelationshipSet objects.
        depends_line = 'python-deb-pkg-tools, python-pip, python-pip-accel'
        self.assertEqual(printer.pformat(parse_depends(depends_line)), dedent('''
            RelationshipSet(Relationship(name='python-deb-pkg-tools'),
                            Relationship(name='python-pip'),
                            Relationship(name='python-pip-accel'))
        '''))

    def test_filename_parsing(self):
        # Test the happy path.
        filename = '/var/cache/apt/archives/python2.7_2.7.3-0ubuntu3.4_amd64.deb'
        components = parse_filename(filename)
        self.assertEqual(components.filename, filename)
        self.assertEqual(components.name, 'python2.7')
        self.assertEqual(components.version, '2.7.3-0ubuntu3.4')
        self.assertEqual(components.architecture, 'amd64')
        # Test the unhappy paths.
        self.assertRaises(ValueError, parse_filename, 'python2.7_2.7.3-0ubuntu3.4_amd64.not-a-deb')
        self.assertRaises(ValueError, parse_filename, 'python2.7.deb')

    def test_boolean_coercion(self):
        for value in ['YES', 'Yes', 'yes', 'TRUE', 'True', 'true', '1']:
            self.assertEqual(coerce_boolean(value), True)
        for value in ['NO', 'No', 'no', 'FALSE', 'False', 'false', '0']:
            self.assertEqual(coerce_boolean(value), False)
        self.assertRaises(ValueError, coerce_boolean, 'not a boolean!')

    def test_package_building(self, repository=None, overrides={}, contents={}):
        with Context() as finalizers:
            build_directory = finalizers.mkdtemp()
            control_fields = merge_control_fields(TEST_PACKAGE_FIELDS, overrides)
            # Create the package template.
            os.mkdir(os.path.join(build_directory, 'DEBIAN'))
            with open(os.path.join(build_directory, 'DEBIAN', 'control'), 'wb') as handle:
                control_fields.dump(handle)
            if contents:
                for filename, data in contents.items():
                    filename = os.path.join(build_directory, filename)
                    directory = os.path.dirname(filename)
                    if not os.path.isdir(directory):
                        os.makedirs(directory)
                    with open(filename, 'w') as handle:
                        handle.write(data)
            else:
                with open(os.path.join(build_directory, 'DEBIAN', 'conffiles'), 'wb') as handle:
                    handle.write(b'/etc/file1\n')
                    handle.write(b'/etc/file2\n')
                # Create the directory with configuration files.
                os.mkdir(os.path.join(build_directory, 'etc'))
                touch(os.path.join(build_directory, 'etc', 'file1'))
                touch(os.path.join(build_directory, 'etc', 'file3'))
                # Create a directory that should be cleaned up by clean_package_tree().
                os.makedirs(os.path.join(build_directory, 'tmp', '.git'))
                # Create a file that should be cleaned up by clean_package_tree().
                with open(os.path.join(build_directory, 'tmp', '.gitignore'), 'w') as handle:
                    handle.write('\n')
            # Build the package (without any contents :-).
            call('--build', build_directory)
            package_file = os.path.join(tempfile.gettempdir(),
                                        '%s_%s_%s.deb' % (control_fields['Package'],
                                                          control_fields['Version'],
                                                          control_fields['Architecture']))
            self.assertTrue(os.path.isfile(package_file))
            if repository:
                shutil.move(package_file, repository)
                return os.path.join(repository, os.path.basename(package_file))
            else:
                finalizers.register(os.unlink, package_file)
                # Verify the package metadata.
                fields, contents = inspect_package(package_file)
                for name in TEST_PACKAGE_FIELDS:
                    self.assertEqual(fields[name], TEST_PACKAGE_FIELDS[name])
                # Verify that the package contains the `/' and `/tmp'
                # directories (since it doesn't contain any actual files).
                self.assertEqual(contents['/'].permissions[0], 'd')
                self.assertEqual(contents['/'].permissions[1:], 'rwxr-xr-x')
                self.assertEqual(contents['/'].owner, 'root')
                self.assertEqual(contents['/'].group, 'root')
                self.assertEqual(contents['/tmp/'].permissions[0], 'd')
                self.assertEqual(contents['/tmp/'].owner, 'root')
                self.assertEqual(contents['/tmp/'].group, 'root')
                # Verify that clean_package_tree() cleaned up properly
                # (`/tmp/.git' and `/tmp/.gitignore' have been cleaned up).
                self.assertFalse('/tmp/.git/' in contents)
                self.assertFalse('/tmp/.gitignore' in contents)
                return package_file

    def test_command_line_interface(self):
        if not SKIP_SLOW_TESTS:
            with Context() as finalizers:
                directory = finalizers.mkdtemp()
                # Test `deb-pkg-tools --inspect PKG'.
                package_file = self.test_package_building(directory)
                lines = call('--verbose', '--inspect', package_file).splitlines()
                for field, value in TEST_PACKAGE_FIELDS.items():
                    self.assertEqual(match('^ - %s: (.+)$' % field, lines), value)
                # Test `deb-pkg-tools --with-repo=DIR CMD' (we simply check whether
                # apt-cache sees the package).
                if os.getuid() == 0:
                    call('--with-repo=%s' % directory, 'apt-cache show %s' % TEST_PACKAGE_NAME)
                # Test `deb-pkg-tools --update=DIR' with a non-existing directory.
                self.assertRaises(SystemExit, call, '--update', '/a/directory/that/will/never/exist')

    def test_check_package(self):
        with Context() as finalizers:
            directory = finalizers.mkdtemp()
            root_package, conflicting_package = self.create_version_conflict(directory)
            # This *should* raise SystemExit.
            self.assertRaises(SystemExit, call, '--check', root_package)
            # Test for lack of duplicate files.
            os.unlink(conflicting_package)
            # This should *not* raise SystemExit.
            call('--check', root_package)

    def test_version_conflicts_check(self):
        with Context() as finalizers:
            # Check that version conflicts raise an exception.
            directory = finalizers.mkdtemp()
            root_package, conflicting_package = self.create_version_conflict(directory)
            packages_to_scan = collect_related_packages(root_package)
            # Test the duplicate files check.
            self.assertRaises(VersionConflictFound, check_version_conflicts, packages_to_scan, self.package_cache)
            # Test for lack of duplicate files.
            os.unlink(conflicting_package)
            self.assertEqual(check_version_conflicts(packages_to_scan, cache=self.package_cache), None)

    def create_version_conflict(self, directory):
        root_package = self.test_package_building(directory, overrides=dict(Package='deb-pkg-tools-package-1', Depends='deb-pkg-tools-package-2 (=1)'))
        self.test_package_building(directory, overrides=dict(Package='deb-pkg-tools-package-2', Version='1'))
        conflicting_package = self.test_package_building(directory, overrides=dict(Package='deb-pkg-tools-package-2', Version='2'))
        return root_package, conflicting_package

    def test_duplicates_check(self):
        with Context() as finalizers:
            # Check that duplicate files raise an exception.
            directory = finalizers.mkdtemp()
            # Build a package containing some files.
            self.test_package_building(directory, overrides=dict(Package='deb-pkg-tools-package-1', Version='1'))
            # Build an unrelated package containing the same files.
            self.test_package_building(directory, overrides=dict(Package='deb-pkg-tools-package-2'))
            # Build two versions of one package.
            duplicate_contents = {'foo/bar': 'some random file'}
            self.test_package_building(directory,
                                       overrides=dict(Package='deb-pkg-tools-package-3', Version='1'),
                                       contents=duplicate_contents)
            self.test_package_building(directory,
                                       overrides=dict(Package='deb-pkg-tools-package-3', Version='2'),
                                       contents=duplicate_contents)
            # Build two packages related by their `Conflicts' and `Provides' fields.
            virtual_package = 'deb-pkg-tools-virtual-package'
            duplicate_contents = {'foo/baz': 'another random file'}
            self.test_package_building(directory,
                                       overrides=dict(Package='deb-pkg-tools-package-4',
                                                      Conflicts=virtual_package,
                                                      Provides=virtual_package),
                                       contents=duplicate_contents)
            self.test_package_building(directory,
                                       overrides=dict(Package='deb-pkg-tools-package-5',
                                                      Conflicts=virtual_package,
                                                      Provides=virtual_package),
                                       contents=duplicate_contents)
            # Test the duplicate files check.
            package_archives = find_package_archives(directory)
            self.assertRaises(DuplicateFilesFound, check_duplicate_files, package_archives, cache=self.package_cache)
            # Verify that invalid arguments are checked.
            self.assertRaises(ValueError, check_duplicate_files, [])

    def test_collect_packages(self):
        with Context() as finalizers:
            source_directory = finalizers.mkdtemp()
            target_directory = finalizers.mkdtemp()
            package1 = self.test_package_building(source_directory, overrides=dict(Package='deb-pkg-tools-package-1', Depends='deb-pkg-tools-package-2'))
            package2 = self.test_package_building(source_directory, overrides=dict(Package='deb-pkg-tools-package-2', Depends='deb-pkg-tools-package-3'))
            package3 = self.test_package_building(source_directory, overrides=dict(Package='deb-pkg-tools-package-3'))
            call('--yes', '--collect=%s' % target_directory, package1)
            self.assertEqual(sorted(os.listdir(target_directory)), sorted(map(os.path.basename, [package1, package2, package3])))

    def test_collect_packages_interactive(self):
        with Context() as finalizers:
            directory = finalizers.mkdtemp()
            package1 = self.test_package_building(directory, overrides=dict(Package='deb-pkg-tools-package-1', Depends='deb-pkg-tools-package-2'))
            package2 = self.test_package_building(directory, overrides=dict(Package='deb-pkg-tools-package-2', Depends='deb-pkg-tools-package-3'))
            self.test_package_building(directory, overrides=dict(Package='deb-pkg-tools-package-3'))
            package4 = self.test_package_building(directory, overrides=dict(Package='deb-pkg-tools-package-3', Version='0.2'))
            self.assertEqual(sorted(p.filename for p in collect_related_packages(package1, cache=self.package_cache)), [package2, package4])


    def test_repository_creation(self, preserve=False):
        if not SKIP_SLOW_TESTS:
            with Context() as finalizers:
                config_dir = tempfile.mkdtemp()
                repo_dir = tempfile.mkdtemp()
                if not preserve:
                    finalizers.register(shutil.rmtree, config_dir)
                    finalizers.register(shutil.rmtree, repo_dir)
                from deb_pkg_tools import config
                config.user_config_directory = config_dir
                with open(os.path.join(config_dir, config.repo_config_file), 'w') as handle:
                    handle.write('[test]\n')
                    handle.write('directory = %s\n' % repo_dir)
                    handle.write('release-origin = %s\n' % TEST_REPO_ORIGIN)
                self.test_package_building(repo_dir)
                update_repository(repo_dir, release_fields=dict(description=TEST_REPO_DESCRIPTION), cache=self.package_cache)
                self.assertTrue(os.path.isfile(os.path.join(repo_dir, 'Packages')))
                self.assertTrue(os.path.isfile(os.path.join(repo_dir, 'Packages.gz')))
                self.assertTrue(os.path.isfile(os.path.join(repo_dir, 'Release')))
                with open(os.path.join(repo_dir, 'Release')) as handle:
                    fields = Deb822(handle)
                    self.assertEqual(fields['Origin'], TEST_REPO_ORIGIN)
                    self.assertEqual(fields['Description'], TEST_REPO_DESCRIPTION)
                if not apt_supports_trusted_option():
                    self.assertTrue(os.path.isfile(os.path.join(repo_dir, 'Release.gpg')))
                return repo_dir

    def test_repository_activation(self):
        if not SKIP_SLOW_TESTS and os.getuid() == 0:
            repository = self.test_repository_creation(preserve=True)
            call('--activate-repo=%s' % repository)
            try:
                handle = os.popen('apt-cache show %s' % TEST_PACKAGE_NAME)
                fields = Deb822(handle)
                self.assertEqual(fields['Package'], TEST_PACKAGE_NAME)
            finally:
                call('--deactivate-repo=%s' % repository)
            # XXX If we skipped the GPG key handling because apt supports the
            # [trusted=yes] option, re-run the test *including* GPG key
            # handling (we want this to be tested...).
            import deb_pkg_tools
            if deb_pkg_tools.repo.apt_supports_trusted_option():
                deb_pkg_tools.repo.trusted_option_supported = False
                self.test_repository_activation()

    def test_gpg_key_generation(self):
        if not SKIP_SLOW_TESTS:
            with Context() as finalizers:
                working_directory = finalizers.mkdtemp()
                secret_key_file = os.path.join(working_directory, 'subdirectory', 'test.sec')
                public_key_file = os.path.join(working_directory, 'subdirectory', 'test.pub')
                # Generate a named GPG key on the spot.
                GPGKey(name="named-test-key",
                       description="GPG key pair generated for unit tests (named key)",
                       secret_key_file=secret_key_file,
                       public_key_file=public_key_file)
                # Generate a default GPG key on the spot.
                default_key = GPGKey(name="default-test-key",
                                     description="GPG key pair generated for unit tests (default key)")
                self.assertEqual(os.path.basename(default_key.secret_key_file), 'secring.gpg')
                self.assertEqual(os.path.basename(default_key.public_key_file), 'pubring.gpg')
                # Test error handling related to GPG keys.
                self.assertRaises(Exception, GPGKey, secret_key_file=secret_key_file)
                self.assertRaises(Exception, GPGKey, public_key_file=public_key_file)
                missing_secret_key_file = '/tmp/deb-pkg-tools-%i.sec' % random.randint(1, 1000)
                missing_public_key_file = '/tmp/deb-pkg-tools-%i.pub' % random.randint(1, 1000)
                self.assertRaises(Exception, GPGKey, key_id='12345', secret_key_file=secret_key_file, public_key_file=missing_public_key_file)
                self.assertRaises(Exception, GPGKey, key_id='12345', secret_key_file=missing_secret_key_file, public_key_file=public_key_file)
                os.unlink(secret_key_file)
                self.assertRaises(Exception, GPGKey, name="test-key", description="Whatever", secret_key_file=secret_key_file, public_key_file=public_key_file)
                touch(secret_key_file)
                os.unlink(public_key_file)
                self.assertRaises(Exception, GPGKey, name="test-key", description="Whatever", secret_key_file=secret_key_file, public_key_file=public_key_file)
                os.unlink(secret_key_file)
                self.assertRaises(Exception, GPGKey, secret_key_file=secret_key_file, public_key_file=public_key_file)
Esempio n. 4
0
class DebPkgToolsTestCase(TestCase):
    """Container for the `deb-pkg-tools` test suite."""
    def setUp(self):
        """Prepare a temporary package cache."""
        # Set up our superclass.
        super(DebPkgToolsTestCase, self).setUp()
        # Prepare the package cache.
        self.db_directory = tempfile.mkdtemp()
        self.load_package_cache()
        # Try to force entropy generation.
        os.environ['DPT_FORCE_ENTROPY'] = 'yes'

    def load_package_cache(self):
        """Prepare a temporary package cache for the duration of a test."""
        self.package_cache = PackageCache(directory=self.db_directory)

    def tearDown(self):
        """Cleanup the temporary package cache."""
        # Tear down our superclass.
        super(DebPkgToolsTestCase, self).tearDown()
        # Cleanup the package cache.
        self.package_cache.collect_garbage(force=True)
        shutil.rmtree(self.db_directory)
        # Disable entropy generation.
        os.environ.pop('DPT_FORCE_ENTROPY')

    def test_makedirs(self):
        """Test that makedirs() can deal with race conditions."""
        with Context() as finalizers:
            parent = finalizers.mkdtemp()
            child = os.path.join(parent, 'nested')
            # This will create the directory.
            makedirs(child)
            # This should not complain that the directory already exists.
            makedirs(child)

    def test_file_copying(self):
        """Test that file copying using hard links actually works."""
        with Context() as finalizers:
            source_directory = finalizers.mkdtemp()
            target_directory = finalizers.mkdtemp()
            touch(os.path.join(source_directory, '42'))
            copy_package_files(source_directory,
                               target_directory,
                               hard_links=True)
            assert os.stat(os.path.join(source_directory, '42')).st_ino == \
                os.stat(os.path.join(target_directory, '42')).st_ino

    def test_package_cache_invalidation(self):
        """Test that the package cache handles invalidation properly."""
        with Context() as finalizers:
            directory = finalizers.mkdtemp()
            package_file = self.test_package_building(
                directory,
                overrides=dict(
                    Package='deb-pkg-tools-package-1',
                    Version='1',
                ))
            for i in range(5):
                fields, contents = inspect_package(package_file,
                                                   cache=self.package_cache)
                if i % 2 == 0:
                    os.utime(package_file, None)
                else:
                    self.load_package_cache()

    def test_architecture_determination(self):
        """Make sure discovery of the current build architecture works properly."""
        valid_architectures = execute('dpkg-architecture', '-L',
                                      capture=True).splitlines()
        assert find_debian_architecture() in valid_architectures

    def test_find_package_archives(self):
        """Test searching for package archives."""
        with Context() as finalizers:
            directory = finalizers.mkdtemp()
            for filename in 'some-random-file', 'regular-package_1.0_all.deb', 'micro-package_1.5_all.udeb':
                touch(os.path.join(directory, filename))
            matches = find_package_archives(directory)
            assert len(matches) == 2
            assert any(p.name == 'regular-package' and p.version == '1.0'
                       and p.architecture == 'all' for p in matches)
            assert any(p.name == 'micro-package' and p.version == '1.5'
                       and p.architecture == 'all' for p in matches)

    def test_find_latest_version(self):
        """Test the selection of latest versions."""
        good = ['name_1.0_all.deb', 'name_0.5_all.deb']
        assert os.path.basename(
            find_latest_version(good).filename) == 'name_1.0_all.deb'
        bad = ['one_1.0_all.deb', 'two_0.5_all.deb']
        self.assertRaises(ValueError, find_latest_version, bad)

    def test_group_by_latest_versions(self):
        """Test the grouping by latest versions."""
        packages = [
            'one_1.0_all.deb', 'one_0.5_all.deb', 'two_1.5_all.deb',
            'two_0.1_all.deb'
        ]
        assert sorted(os.path.basename(a.filename) for a in group_by_latest_versions(packages).values()) == \
            sorted(['one_1.0_all.deb', 'two_1.5_all.deb'])

    def test_control_field_parsing(self):
        """Test the parsing of control file fields."""
        deb822_package = Deb822([
            'Package: python-py2deb',
            'Depends: python-deb-pkg-tools, python-pip, python-pip-accel',
            'Installed-Size: 42'
        ])
        parsed_info = parse_control_fields(deb822_package)
        assert parsed_info == {
            'Package':
            'python-py2deb',
            'Depends':
            RelationshipSet(Relationship(name=u'python-deb-pkg-tools'),
                            Relationship(name=u'python-pip'),
                            Relationship(name=u'python-pip-accel')),
            'Installed-Size':
            42
        }
        # Test backwards compatibility with the old interface where `Depends'
        # like fields were represented as a list of strings (shallow parsed).
        parsed_info['Depends'] = [text_type(r) for r in parsed_info['Depends']]
        assert unparse_control_fields(parsed_info) == deb822_package
        # Test compatibility with fields like `Depends' containing a string.
        parsed_info['Depends'] = deb822_package['Depends']
        assert unparse_control_fields(parsed_info) == deb822_package

    def test_control_field_merging(self):
        """Test the merging of control file fields."""
        defaults = Deb822([
            'Package: python-py2deb', 'Depends: python-deb-pkg-tools',
            'Architecture: all'
        ])
        # The field names of the dictionary with overrides are lower case on
        # purpose; control file merging should work properly regardless of
        # field name casing.
        overrides = Deb822(
            dict(version='1.0',
                 depends='python-pip, python-pip-accel',
                 architecture='amd64'))
        assert merge_control_fields(defaults, overrides) == \
            Deb822(['Package: python-py2deb',
                    'Version: 1.0',
                    'Depends: python-deb-pkg-tools, python-pip, python-pip-accel',
                    'Architecture: amd64'])

    def test_control_file_creation(self):
        """Test control file creation."""
        with Context() as context:
            directory = context.mkdtemp()
            # Use a non-existing subdirectory to verify that it's created.
            control_file = os.path.join(directory, 'DEBIAN', 'control')
            # Try to create a control file but omit some mandatory fields.
            self.assertRaises(ValueError, create_control_file, control_file,
                              dict(Package='created-from-python'))
            # Now we'll provide all of the required fields to actually create the file.
            create_control_file(
                control_file,
                dict(
                    Package='created-from-python',
                    Description='whatever',
                    Maintainer='Peter Odding',
                    Version='1.0',
                ))
            # Load the control file to verify its contents.
            control_fields = load_control_file(control_file)
            # These fields were provided by us (the caller of create_control_file()).
            assert control_fields['Package'] == 'created-from-python'
            assert control_fields['Description'] == 'whatever'
            # This field was written as a default value.
            assert control_fields['Architecture'] == 'all'

    def test_control_file_patching_and_loading(self):
        """Test patching and loading of control files."""
        deb822_package = Deb822(
            ['Package: unpatched-example', 'Depends: some-dependency'])
        with Context() as finalizers:
            control_file = tempfile.mktemp()
            finalizers.register(os.unlink, control_file)
            with open(control_file, 'wb') as handle:
                deb822_package.dump(handle)
            returncode, output = run_cli(
                main,
                '--patch=%s' % control_file,
                '--set=Package: patched-example',
                '--set=Depends: another-dependency',
            )
            assert returncode == 0
            patched_fields = load_control_file(control_file)
            assert patched_fields['Package'] == 'patched-example'
            assert str(patched_fields['Depends']
                       ) == 'another-dependency, some-dependency'

    def test_version_comparison(self):
        """Test the comparison of version objects (under both implementations)."""
        self.version_comparison_helper()
        if version.have_python_apt:
            with PatchedAttribute(version, 'have_python_apt', False):
                self.version_comparison_helper()
                self.assertRaises(NotImplementedError,
                                  version.compare_versions_with_python_apt,
                                  '0.1', '<<', '0.2')

    def version_comparison_helper(self):
        """Test the comparison of version objects."""
        # V() shortcut for deb_pkg_tools.version.Version().
        V = version.Version
        # Check version sorting implemented on top of `=' and `<<' comparisons.
        expected_order = ['0.1', '0.5', '1.0', '2.0', '3.0', '1:0.4', '2:0.3']
        assert list(sorted(expected_order)) != expected_order
        assert list(sorted(map(V, expected_order))) == expected_order
        # Check each individual operator (to make sure the two implementations
        # agree). We use the Version() class for this so that we test both
        # compare_versions() and the Version() wrapper.
        # Test `>'.
        assert V('1.0') > V('0.5')  # usual semantics
        assert V('1:0.5') > V('2.0')  # unusual semantics
        assert not V('0.5') > V('2.0')  # sanity check
        # Test `>='.
        assert V('0.75') >= V('0.5')  # usual semantics
        assert V('0.50') >= V('0.5')  # usual semantics
        assert V('1:0.5') >= V('5.0')  # unusual semantics
        assert not V('0.2') >= V('0.5')  # sanity check
        # Test `<'.
        assert V('0.5') < V('1.0')  # usual semantics
        assert V('2.0') < V('1:0.5')  # unusual semantics
        assert not V('2.0') < V('0.5')  # sanity check
        # Test `<='.
        assert V('0.5') <= V('0.75')  # usual semantics
        assert V('0.5') <= V('0.50')  # usual semantics
        assert V('5.0') <= V('1:0.5')  # unusual semantics
        assert not V('0.5') <= V('0.2')  # sanity check
        # Test `=='.
        assert V('42') == V('42')  # usual semantics
        assert V('0.5') == V('0:0.5')  # unusual semantics
        assert not V('0.5') == V('1.0')  # sanity check
        # Test `!='.
        assert V('1') != V('0')  # usual semantics
        assert not V('0.5') != V('0:0.5')  # unusual semantics

    def test_relationship_parsing(self):
        """Test the parsing of Debian package relationship declarations."""
        # Happy path (no parsing errors).
        relationship_set = parse_depends('foo, bar (>= 1) | baz')
        assert relationship_set.relationships[0].name == 'foo'
        assert relationship_set.relationships[1].relationships[0].name == 'bar'
        assert relationship_set.relationships[1].relationships[
            0].operator == '>='
        assert relationship_set.relationships[1].relationships[
            0].version == '1'
        assert relationship_set.relationships[1].relationships[1].name == 'baz'
        assert parse_depends('foo (=1.0)') == RelationshipSet(
            VersionedRelationship(
                name='foo',
                operator='=',
                version='1.0',
            ))
        # Unhappy path (parsing errors).
        self.assertRaises(ValueError, parse_depends, 'foo (bar) (baz)')
        self.assertRaises(ValueError, parse_depends, 'foo (bar baz qux)')

    def test_architecture_restriction_parsing(self):
        """Test the parsing of architecture restrictions."""
        relationship_set = parse_depends('qux [i386 amd64]')
        assert relationship_set.relationships[0].name == 'qux'
        assert len(relationship_set.relationships[0].architectures) == 2
        assert 'i386' in relationship_set.relationships[0].architectures
        assert 'amd64' in relationship_set.relationships[0].architectures

    def test_relationship_unparsing(self):
        """Test the unparsing (serialization) of parsed relationship declarations."""
        def strip(text):
            return re.sub(r'\s+', '', text)

        relationship_set = parse_depends('foo, bar(>=1)|baz[i386]')
        assert text_type(relationship_set) == 'foo, bar (>= 1) | baz [i386]'
        assert strip(repr(relationship_set)) == strip("""
            RelationshipSet(
                Relationship(name='foo', architectures=()),
                AlternativeRelationship(
                    VersionedRelationship(name='bar', operator='>=', version='1', architectures=()),
                    Relationship(name='baz', architectures=('i386',))
                )
            )
        """)

    def test_relationship_evaluation(self):
        """Test the evaluation of package relationships."""
        # Relationships without versions.
        relationship_set = parse_depends('python')
        assert relationship_set.matches('python')
        assert not relationship_set.matches('python2.7')
        assert list(relationship_set.names) == ['python']
        # Alternatives (OR) without versions.
        relationship_set = parse_depends('python2.6 | python2.7')
        assert not relationship_set.matches('python2.5')
        assert relationship_set.matches('python2.6')
        assert relationship_set.matches('python2.7')
        assert not relationship_set.matches('python3.0')
        assert sorted(relationship_set.names) == ['python2.6', 'python2.7']
        # Combinations (AND) with versions.
        relationship_set = parse_depends(
            'python (>= 2.6), python (<< 3) | python (>= 3.4)')
        assert not relationship_set.matches('python', '2.5')
        assert relationship_set.matches('python', '2.6')
        assert relationship_set.matches('python', '2.7')
        assert not relationship_set.matches('python', '3.0')
        assert relationship_set.matches('python', '3.4')
        assert list(relationship_set.names) == ['python']
        # Testing for matches without providing a version is valid (should not
        # raise an error) but will never match a relationship with a version.
        relationship_set = parse_depends('python (>= 2.6), python (<< 3)')
        assert relationship_set.matches('python', '2.7')
        assert not relationship_set.matches('python')
        assert list(relationship_set.names) == ['python']
        # Distinguishing between packages whose name was matched but whose
        # version didn't match vs packages whose name wasn't matched.
        relationship_set = parse_depends(
            'python (>= 2.6), python (<< 3) | python (>= 3.4)')
        assert relationship_set.matches(
            'python', '2.7') is True  # name and version match
        assert relationship_set.matches(
            'python', '2.5') is False  # name matched, version didn't
        assert relationship_set.matches(
            'python2.6') is None  # name didn't match
        assert relationship_set.matches(
            'python',
            '3.0') is False  # name in alternative matched, version didn't
        assert list(relationship_set.names) == ['python']

    def test_custom_pretty_printer(self):
        """Test pretty printing of deb822 objects and parsed relationships."""
        printer = CustomPrettyPrinter()
        # Test pretty printing of debian.deb822.Deb822 objects.
        deb822_object = deb822_from_string('''
            Package: pretty-printed-control-fields
            Version: 1.0
            Architecture: all
        ''')
        formatted_object = printer.pformat(deb822_object)
        assert normalize_repr_output(
            formatted_object) == normalize_repr_output('''
            {'Architecture': u'all',
             'Package': u'pretty-printed-control-fields',
             'Version': u'1.0'}
        ''')
        # Test pretty printing of RelationshipSet objects.
        relationship_set = parse_depends(
            'python-deb-pkg-tools, python-pip, python-pip-accel')
        formatted_object = printer.pformat(relationship_set)
        assert normalize_repr_output(
            formatted_object) == normalize_repr_output('''
            RelationshipSet(Relationship(name='python-deb-pkg-tools', architectures=()),
                            Relationship(name='python-pip', architectures=()),
                            Relationship(name='python-pip-accel', architectures=()))
        ''')

    def test_filename_parsing(self):
        """Test filename parsing."""
        # Test the happy path.
        filename = '/var/cache/apt/archives/python2.7_2.7.3-0ubuntu3.4_amd64.deb'
        components = parse_filename(filename)
        assert components.filename == filename
        assert components.name == 'python2.7'
        assert components.version == '2.7.3-0ubuntu3.4'
        assert components.architecture == 'amd64'
        # Test the unhappy paths.
        self.assertRaises(ValueError, parse_filename,
                          'python2.7_2.7.3-0ubuntu3.4_amd64.not-a-deb')
        self.assertRaises(ValueError, parse_filename, 'python2.7.deb')

    def test_find_object_files(self):
        """Test the :func:`deb_pkg_tools.package.find_object_files()` function."""
        with Context() as finalizers:
            directory = finalizers.mkdtemp()
            shutil.copy(__file__, directory)
            shutil.copy(sys.executable, directory)
            object_files = find_object_files(directory)
            assert len(object_files) == 1
            assert object_files[0] == os.path.join(
                directory, os.path.basename(sys.executable))

    def test_find_system_dependencies(self):
        """Test the :func:`deb_pkg_tools.package.find_system_dependencies()` function."""
        dependencies = find_system_dependencies(['/usr/bin/python'])
        assert len(dependencies) >= 1
        assert any(re.match(r'^libc\d+\b', d) for d in dependencies)

    def test_package_building(self,
                              repository=None,
                              overrides={},
                              contents={}):
        """Test building of Debian binary packages."""
        with Context() as finalizers:
            build_directory = finalizers.mkdtemp()
            control_fields = merge_control_fields(TEST_PACKAGE_FIELDS,
                                                  overrides)
            # Create the package template.
            os.mkdir(os.path.join(build_directory, 'DEBIAN'))
            with open(os.path.join(build_directory, 'DEBIAN', 'control'),
                      'wb') as handle:
                control_fields.dump(handle)
            if contents:
                for filename, data in contents.items():
                    filename = os.path.join(build_directory, filename)
                    directory = os.path.dirname(filename)
                    makedirs(directory)
                    with open(filename, 'w') as handle:
                        handle.write(data)
            else:
                with open(os.path.join(build_directory, 'DEBIAN', 'conffiles'),
                          'wb') as handle:
                    handle.write(b'/etc/file1\n')
                    handle.write(b'/etc/file2\n')
                # Create the directory with configuration files.
                os.mkdir(os.path.join(build_directory, 'etc'))
                touch(os.path.join(build_directory, 'etc', 'file1'))
                touch(os.path.join(build_directory, 'etc', 'file3'))
                # Create a directory that should be cleaned up by clean_package_tree().
                makedirs(os.path.join(build_directory, 'tmp', '.git'))
                # Create a file that should be cleaned up by clean_package_tree().
                with open(os.path.join(build_directory, 'tmp', '.gitignore'),
                          'w') as handle:
                    handle.write('\n')
            # Build the package (without any contents :-).
            returncode, output = run_cli(main, '--build', build_directory)
            assert returncode == 0
            package_file = os.path.join(
                tempfile.gettempdir(), '%s_%s_%s.deb' %
                (control_fields['Package'], control_fields['Version'],
                 control_fields['Architecture']))
            assert os.path.isfile(package_file)
            if repository:
                shutil.move(package_file, repository)
                return os.path.join(repository, os.path.basename(package_file))
            else:
                finalizers.register(os.unlink, package_file)
                # Verify the package metadata.
                fields, contents = inspect_package(package_file)
                for name in TEST_PACKAGE_FIELDS:
                    assert fields[name] == TEST_PACKAGE_FIELDS[name]
                # Verify that the package contains the `/' and `/tmp'
                # directories (since it doesn't contain any actual files).
                assert contents['/'].permissions[0] == 'd'
                assert contents['/'].permissions[1:] == 'rwxr-xr-x'
                assert contents['/'].owner == 'root'
                assert contents['/'].group == 'root'
                assert contents['/tmp/'].permissions[0] == 'd'
                assert contents['/tmp/'].owner == 'root'
                assert contents['/tmp/'].group == 'root'
                # Verify that clean_package_tree() cleaned up properly
                # (`/tmp/.git' and `/tmp/.gitignore' have been cleaned up).
                assert '/tmp/.git/' not in contents
                assert '/tmp/.gitignore' not in contents
                return package_file

    def test_command_line_interface(self):
        """Test the command line interface."""
        if SKIP_SLOW_TESTS:
            return self.skipTest("skipping slow tests")
        with Context() as finalizers:
            directory = finalizers.mkdtemp()
            # Test `deb-pkg-tools --inspect PKG'.
            package_file = self.test_package_building(directory)
            returncode, output = run_cli(main, '--verbose', '--inspect',
                                         package_file)
            assert returncode == 0
            lines = output.splitlines()
            for field, value in TEST_PACKAGE_FIELDS.items():
                assert match('^ - %s: (.+)$' % field, lines) == value
            # Test `deb-pkg-tools --update=DIR' with a non-existing directory.
            returncode, output = run_cli(main, '--update',
                                         '/a/directory/that/will/never/exist')
            assert returncode != 0

    def test_with_repo_cli(self):
        """Test ``deb-pkg-tools --with-repo``."""
        if SKIP_SLOW_TESTS:
            return self.skipTest("skipping slow tests")
        elif os.getuid() != 0:
            return self.skipTest("need superuser privileges")
        with Context() as finalizers:
            directory = finalizers.mkdtemp()
            self.test_package_building(directory)
            with CaptureOutput() as capturer:
                run_cli(
                    main,
                    '--with-repo=%s' % directory,
                    'apt-cache show %s' % TEST_PACKAGE_NAME,
                )
                # Check whether apt-cache sees the package.
                expected_line = "Package: %s" % TEST_PACKAGE_NAME
                assert expected_line in capturer.get_lines()

    def test_check_package(self):
        """Test the command line interface for static analysis of package archives."""
        with Context() as finalizers:
            directory = finalizers.mkdtemp()
            root_package, conflicting_package = self.create_version_conflict(
                directory)
            # This *should* raise SystemExit.
            returncode, output = run_cli(main, '--check', root_package)
            assert returncode != 0
            # Test for lack of duplicate files.
            os.unlink(conflicting_package)
            # This should *not* raise SystemExit.
            returncode, output = run_cli(main, '--check', root_package)
            assert returncode == 0

    def test_version_conflicts_check(self):
        """Test static analysis of version conflicts."""
        with Context() as finalizers:
            # Check that version conflicts raise an exception.
            directory = finalizers.mkdtemp()
            root_package, conflicting_package = self.create_version_conflict(
                directory)
            packages_to_scan = collect_related_packages(root_package)
            # Test the duplicate files check.
            self.assertRaises(VersionConflictFound, check_version_conflicts,
                              packages_to_scan, self.package_cache)
            # Test for lack of duplicate files.
            os.unlink(conflicting_package)
            assert check_version_conflicts(packages_to_scan,
                                           cache=self.package_cache) is None

    def create_version_conflict(self, directory):
        """Build a directory of packages with a version conflict."""
        root_package = self.test_package_building(
            directory,
            overrides=dict(
                Package='deb-pkg-tools-package-1',
                Depends='deb-pkg-tools-package-2 (=1)',
            ))
        self.test_package_building(directory,
                                   overrides=dict(
                                       Package='deb-pkg-tools-package-2',
                                       Version='1',
                                   ))
        conflicting_package = self.test_package_building(
            directory,
            overrides=dict(
                Package='deb-pkg-tools-package-2',
                Version='2',
            ))
        return root_package, conflicting_package

    def test_duplicates_check(self):
        """Test static analysis of duplicate files."""
        with Context() as finalizers:
            # Check that duplicate files raise an exception.
            directory = finalizers.mkdtemp()
            # Build a package containing some files.
            self.test_package_building(directory,
                                       overrides=dict(
                                           Package='deb-pkg-tools-package-1',
                                           Version='1'))
            # Build an unrelated package containing the same files.
            self.test_package_building(
                directory, overrides=dict(Package='deb-pkg-tools-package-2'))
            # Build two versions of one package.
            duplicate_contents = {'foo/bar': 'some random file'}
            self.test_package_building(directory,
                                       overrides=dict(
                                           Package='deb-pkg-tools-package-3',
                                           Version='1'),
                                       contents=duplicate_contents)
            self.test_package_building(directory,
                                       overrides=dict(
                                           Package='deb-pkg-tools-package-3',
                                           Version='2'),
                                       contents=duplicate_contents)
            # Build two packages related by their `Conflicts' and `Provides' fields.
            virtual_package = 'deb-pkg-tools-virtual-package'
            duplicate_contents = {'foo/baz': 'another random file'}
            self.test_package_building(directory,
                                       overrides=dict(
                                           Package='deb-pkg-tools-package-4',
                                           Conflicts=virtual_package,
                                           Provides=virtual_package),
                                       contents=duplicate_contents)
            self.test_package_building(directory,
                                       overrides=dict(
                                           Package='deb-pkg-tools-package-5',
                                           Conflicts=virtual_package,
                                           Provides=virtual_package),
                                       contents=duplicate_contents)
            # Test the duplicate files check.
            package_archives = find_package_archives(directory)
            self.assertRaises(DuplicateFilesFound,
                              check_duplicate_files,
                              package_archives,
                              cache=self.package_cache)
            # Verify that invalid arguments are checked.
            self.assertRaises(ValueError, check_duplicate_files, [])

    def test_collect_packages(self):
        """Test the command line interface for collection of related packages."""
        with Context() as finalizers:
            source_directory = finalizers.mkdtemp()
            target_directory = finalizers.mkdtemp()
            package1 = self.test_package_building(
                source_directory,
                overrides=dict(
                    Package='deb-pkg-tools-package-1',
                    Depends='deb-pkg-tools-package-2',
                ))
            package2 = self.test_package_building(
                source_directory,
                overrides=dict(
                    Package='deb-pkg-tools-package-2',
                    Depends='deb-pkg-tools-package-3',
                ))
            package3 = self.test_package_building(
                source_directory,
                overrides=dict(Package='deb-pkg-tools-package-3', ))
            returncode, output = run_cli(
                main,
                '--yes',
                '--collect=%s' % target_directory,
                package1,
            )
            assert returncode == 0
            assert sorted(os.listdir(target_directory)) == \
                sorted(map(os.path.basename, [package1, package2, package3]))

    def test_collect_packages_preference_for_newer_versions(self):
        """Test the preference of package collection for newer versions."""
        with Context() as finalizers:
            directory = finalizers.mkdtemp()
            package1 = self.test_package_building(
                directory,
                overrides=dict(
                    Package='deb-pkg-tools-package-1',
                    Depends='deb-pkg-tools-package-2',
                ))
            package2_1 = self.test_package_building(
                directory,
                overrides=dict(
                    Package='deb-pkg-tools-package-2',
                    Version='1',
                    Depends='deb-pkg-tools-package-3 (= 1)',
                ))
            package2_2 = self.test_package_building(
                directory,
                overrides=dict(
                    Package='deb-pkg-tools-package-2',
                    Version='2',
                    Depends='deb-pkg-tools-package-3 (= 2)',
                ))
            package3_1 = self.test_package_building(
                directory,
                overrides=dict(
                    Package='deb-pkg-tools-package-3',
                    Version='1',
                ))
            package3_2 = self.test_package_building(
                directory,
                overrides=dict(
                    Package='deb-pkg-tools-package-3',
                    Version='2',
                ))
            related_packages = [
                p.filename
                for p in collect_related_packages(package1,
                                                  cache=self.package_cache)
            ]
            # Make sure deb-pkg-tools-package-2 version 1 wasn't collected.
            assert package2_1 not in related_packages
            # Make sure deb-pkg-tools-package-2 version 2 was collected.
            assert package2_2 in related_packages
            # Make sure deb-pkg-tools-package-3 version 1 wasn't collected.
            assert package3_1 not in related_packages
            # Make sure deb-pkg-tools-package-3 version 2 was collected.
            assert package3_2 in related_packages

    def test_collect_packages_with_conflict_resolution(self):
        """Test conflict resolution in collection of related packages."""
        with Context() as finalizers:
            directory = finalizers.mkdtemp()
            # The following names are a bit confusing, this is to enforce
            # implicit sorting on file system level (exposing an otherwise
            # unnoticed bug).
            package_a = self.test_package_building(
                directory,
                overrides=dict(
                    Package='package-a',
                    Depends='package-b, package-c',
                ))
            package_b = self.test_package_building(directory,
                                                   overrides=dict(
                                                       Package='package-b',
                                                       Depends='package-d',
                                                   ))
            package_c = self.test_package_building(
                directory,
                overrides=dict(
                    Package='package-c',
                    Depends='package-d (= 1)',
                ))
            package_d1 = self.test_package_building(directory,
                                                    overrides=dict(
                                                        Package='package-d',
                                                        Version='1',
                                                    ))
            package_d2 = self.test_package_building(directory,
                                                    overrides=dict(
                                                        Package='package-d',
                                                        Version='2',
                                                    ))
            related_packages = [
                p.filename
                for p in collect_related_packages(package_a,
                                                  cache=self.package_cache)
            ]
            # Make sure package-b was collected.
            assert package_b in related_packages
            # Make sure package-c was collected.
            assert package_c in related_packages
            # Make sure package-d1 was collected.
            assert package_d1 in related_packages
            # Make sure package-d2 wasn't collected.
            assert package_d2 not in related_packages

    def test_collect_packages_with_prompt(self):
        """Test the confirmation prompt during interactive package collection."""
        with Context() as finalizers:
            # Temporarily change stdin to respond with `y' (for `yes').
            finalizers.register(setattr, sys, 'stdin', sys.stdin)
            sys.stdin = StringIO('y')
            # Prepare some packages to collect.
            source_directory = finalizers.mkdtemp()
            target_directory = finalizers.mkdtemp()
            package1 = self.test_package_building(
                source_directory,
                overrides=dict(
                    Package='deb-pkg-tools-package-1',
                    Depends='deb-pkg-tools-package-2',
                ))
            package2 = self.test_package_building(
                source_directory,
                overrides=dict(Package='deb-pkg-tools-package-2', ))
            # Run `deb-pkg-tools --collect' ...
            returncode, output = run_cli(main,
                                         '--collect=%s' % target_directory,
                                         package1)
            assert returncode == 0
            assert sorted(os.listdir(target_directory)) == sorted(
                map(os.path.basename, [package1, package2]))

    def test_collect_packages_concurrent(self):
        """Test concurrent collection of related packages."""
        with Context() as finalizers:
            source_directory = finalizers.mkdtemp()
            target_directory = finalizers.mkdtemp()
            # Prepare some packages to collect.
            package1 = self.test_package_building(source_directory,
                                                  overrides=dict(
                                                      Package='package-1',
                                                      Depends='package-3',
                                                  ))
            package2 = self.test_package_building(source_directory,
                                                  overrides=dict(
                                                      Package='package-2',
                                                      Depends='package-4',
                                                  ))
            package3 = self.test_package_building(source_directory,
                                                  overrides=dict(
                                                      Package='package-3', ))
            package4 = self.test_package_building(source_directory,
                                                  overrides=dict(
                                                      Package='package-4', ))
            # Run `deb-pkg-tools --collect' ...
            returncode, output = run_cli(
                main,
                '--collect=%s' % target_directory,
                '--yes',
                package1,
                package2,
            )
            assert returncode == 0
            # Make sure the expected packages were promoted.
            assert sorted(os.listdir(target_directory)) == \
                sorted(map(os.path.basename, [package1, package2, package3, package4]))

    def test_repository_creation(self, preserve=False):
        """Test the creation of trivial repositories."""
        if SKIP_SLOW_TESTS:
            return self.skipTest("skipping slow tests")
        with Context() as finalizers:
            config_dir = tempfile.mkdtemp()
            repo_dir = tempfile.mkdtemp()
            if not preserve:
                finalizers.register(shutil.rmtree, config_dir)
                finalizers.register(shutil.rmtree, repo_dir)
            from deb_pkg_tools import config
            config.user_config_directory = config_dir
            with open(os.path.join(config_dir, config.repo_config_file),
                      'w') as handle:
                handle.write('[test]\n')
                handle.write('directory = %s\n' % repo_dir)
                handle.write('release-origin = %s\n' % TEST_REPO_ORIGIN)
            self.test_package_building(repo_dir)
            update_repository(
                repo_dir,
                release_fields=dict(description=TEST_REPO_DESCRIPTION),
                cache=self.package_cache)
            assert os.path.isfile(os.path.join(repo_dir, 'Packages'))
            assert os.path.isfile(os.path.join(repo_dir, 'Packages.gz'))
            assert os.path.isfile(os.path.join(repo_dir, 'Release'))
            with open(os.path.join(repo_dir, 'Release')) as handle:
                fields = Deb822(handle)
                assert fields['Origin'] == TEST_REPO_ORIGIN
                assert fields['Description'] == TEST_REPO_DESCRIPTION
            if not apt_supports_trusted_option():
                assert os.path.isfile(os.path.join(repo_dir, 'Release.gpg'))
            return repo_dir

    def test_repository_activation(self):
        """Test the activation of trivial repositories."""
        if SKIP_SLOW_TESTS:
            return self.skipTest("skipping slow tests")
        elif os.getuid() != 0:
            return self.skipTest("need superuser privileges")
        repository = self.test_repository_creation(preserve=True)
        returncode, output = run_cli(main, '--activate-repo=%s' % repository)
        assert returncode == 0
        try:
            handle = os.popen('apt-cache show %s' % TEST_PACKAGE_NAME)
            fields = Deb822(handle)
            assert fields['Package'] == TEST_PACKAGE_NAME
        finally:
            returncode, output = run_cli(main,
                                         '--deactivate-repo=%s' % repository)
            assert returncode == 0
        # XXX If we skipped the GPG key handling because apt supports the
        # [trusted=yes] option, re-run the test *including* GPG key
        # handling (we want this to be tested...).
        from deb_pkg_tools import repo
        if repo.apt_supports_trusted_option():
            with PatchedAttribute(repo, 'trusted_option_supported', False):
                self.test_repository_activation()

    def test_gpg_key_generation(self):
        """Test automatic GPG key generation."""
        if SKIP_SLOW_TESTS:
            return self.skipTest("skipping slow tests")
        with Context() as finalizers:
            working_directory = finalizers.mkdtemp()
            secret_key_file = os.path.join(working_directory, 'subdirectory',
                                           'test.sec')
            public_key_file = os.path.join(working_directory, 'subdirectory',
                                           'test.pub')
            # Generate a named GPG key on the spot.
            GPGKey(
                name="named-test-key",
                description="GPG key pair generated for unit tests (named key)",
                secret_key_file=secret_key_file,
                public_key_file=public_key_file)
            # Generate a default GPG key on the spot.
            default_key = GPGKey(
                name="default-test-key",
                description=
                "GPG key pair generated for unit tests (default key)")
            assert os.path.basename(
                default_key.secret_key_file) == 'secring.gpg'
            assert os.path.basename(
                default_key.public_key_file) == 'pubring.gpg'
            # Test error handling related to GPG keys.
            self.assertRaises(Exception,
                              GPGKey,
                              secret_key_file=secret_key_file)
            self.assertRaises(Exception,
                              GPGKey,
                              public_key_file=public_key_file)
            missing_secret_key_file = '/tmp/deb-pkg-tools-%i.sec' % random.randint(
                1, 1000)
            missing_public_key_file = '/tmp/deb-pkg-tools-%i.pub' % random.randint(
                1, 1000)
            self.assertRaises(Exception,
                              GPGKey,
                              key_id='12345',
                              secret_key_file=secret_key_file,
                              public_key_file=missing_public_key_file)
            self.assertRaises(Exception,
                              GPGKey,
                              key_id='12345',
                              secret_key_file=missing_secret_key_file,
                              public_key_file=public_key_file)
            os.unlink(secret_key_file)
            self.assertRaises(Exception,
                              GPGKey,
                              name="test-key",
                              description="Whatever",
                              secret_key_file=secret_key_file,
                              public_key_file=public_key_file)
            touch(secret_key_file)
            os.unlink(public_key_file)
            self.assertRaises(Exception,
                              GPGKey,
                              name="test-key",
                              description="Whatever",
                              secret_key_file=secret_key_file,
                              public_key_file=public_key_file)
            os.unlink(secret_key_file)
            self.assertRaises(Exception,
                              GPGKey,
                              secret_key_file=secret_key_file,
                              public_key_file=public_key_file)
Esempio n. 5
0
 def load_package_cache(self):
     self.package_cache = PackageCache(
         os.path.join(self.db_directory, 'package-cache.sqlite3'))
Esempio n. 6
0
class DebPkgToolsTestCase(unittest.TestCase):
    def setUp(self):
        coloredlogs.install()
        coloredlogs.set_level(logging.DEBUG)
        self.db_directory = tempfile.mkdtemp()
        self.load_package_cache()
        os.environ['DPT_FORCE_ENTROPY'] = 'yes'

    def load_package_cache(self):
        self.package_cache = PackageCache(
            os.path.join(self.db_directory, 'package-cache.sqlite3'))

    def tearDown(self):
        self.package_cache.collect_garbage(force=True)
        shutil.rmtree(self.db_directory)
        os.environ.pop('DPT_FORCE_ENTROPY')

    def test_package_cache_error_handling(self):
        self.assertRaises(KeyError, self.package_cache.__getitem__,
                          '/some/random/non-existing/path')

    def test_file_copying(self):
        with Context() as finalizers:
            source_directory = finalizers.mkdtemp()
            target_directory = finalizers.mkdtemp()
            touch(os.path.join(source_directory, '42'))
            copy_package_files(source_directory,
                               target_directory,
                               hard_links=True)
            self.assertEqual(
                os.stat(os.path.join(source_directory, '42')).st_ino,
                os.stat(os.path.join(target_directory, '42')).st_ino)

    def test_package_cache_invalidation(self):
        with Context() as finalizers:
            directory = finalizers.mkdtemp()
            package_file = self.test_package_building(
                directory,
                overrides=dict(Package='deb-pkg-tools-package-1', Version='1'))
            for i in range(5):
                fields, contents = inspect_package(package_file,
                                                   cache=self.package_cache)
                if i % 2 == 0:
                    os.utime(package_file, None)
                else:
                    self.load_package_cache()

    def test_find_latest_version(self):
        good = ['name_1.0_all.deb', 'name_0.5_all.deb']
        self.assertEqual(os.path.basename(find_latest_version(good).filename),
                         'name_1.0_all.deb')
        bad = ['one_1.0_all.deb', 'two_0.5_all.deb']
        self.assertRaises(ValueError, find_latest_version, bad)

    def test_group_by_latest_versions(self):
        packages = [
            'one_1.0_all.deb', 'one_0.5_all.deb', 'two_1.5_all.deb',
            'two_0.1_all.deb'
        ]
        self.assertEqual(
            sorted(
                os.path.basename(a.filename)
                for a in group_by_latest_versions(packages).values()),
            sorted(['one_1.0_all.deb', 'two_1.5_all.deb']))

    def test_control_field_parsing(self):
        deb822_package = Deb822([
            'Package: python-py2deb',
            'Depends: python-deb-pkg-tools, python-pip, python-pip-accel',
            'Installed-Size: 42'
        ])
        parsed_info = parse_control_fields(deb822_package)
        self.assertEqual(
            parsed_info, {
                'Package':
                'python-py2deb',
                'Depends':
                RelationshipSet(Relationship(name=u'python-deb-pkg-tools'),
                                Relationship(name=u'python-pip'),
                                Relationship(name=u'python-pip-accel')),
                'Installed-Size':
                42
            })
        # Test backwards compatibility with the old interface where `Depends'
        # like fields were represented as a list of strings (shallow parsed).
        parsed_info['Depends'] = [unicode(r) for r in parsed_info['Depends']]
        self.assertEqual(unparse_control_fields(parsed_info), deb822_package)
        # Test compatibility with fields like `Depends' containing a string.
        parsed_info['Depends'] = deb822_package['Depends']
        self.assertEqual(unparse_control_fields(parsed_info), deb822_package)

    def test_control_field_merging(self):
        defaults = Deb822([
            'Package: python-py2deb', 'Depends: python-deb-pkg-tools',
            'Architecture: all'
        ])
        # The field names of the dictionary with overrides are lower case on
        # purpose; control file merging should work properly regardless of
        # field name casing.
        overrides = Deb822(
            dict(version='1.0',
                 depends='python-pip, python-pip-accel',
                 architecture='amd64'))
        self.assertEqual(
            merge_control_fields(defaults, overrides),
            Deb822([
                'Package: python-py2deb', 'Version: 1.0',
                'Depends: python-deb-pkg-tools, python-pip, python-pip-accel',
                'Architecture: amd64'
            ]))

    def test_control_file_patching_and_loading(self):
        deb822_package = Deb822(
            ['Package: unpatched-example', 'Depends: some-dependency'])
        with Context() as finalizers:
            control_file = tempfile.mktemp()
            finalizers.register(os.unlink, control_file)
            with open(control_file, 'wb') as handle:
                deb822_package.dump(handle)
            call('--patch=%s' % control_file, '--set=Package: patched-example',
                 '--set=Depends: another-dependency')
            patched_fields = load_control_file(control_file)
            self.assertEqual(patched_fields['Package'], 'patched-example')
            self.assertEqual(str(patched_fields['Depends']),
                             'another-dependency, some-dependency')

    def test_version_comparison(self):
        self.version_comparison_helper()
        if version.have_python_apt:
            version.have_python_apt = False
            self.version_comparison_helper()
            self.assertRaises(NotImplementedError,
                              version.compare_versions_with_python_apt, '0.1',
                              '<<', '0.2')
            version.have_python_apt = True

    def version_comparison_helper(self):
        # V() shortcut for deb_pkg_tools.version.Version().
        V = version.Version
        # Check version sorting implemented on top of `=' and `<<' comparisons.
        expected_order = ['0.1', '0.5', '1.0', '2.0', '3.0', '1:0.4', '2:0.3']
        self.assertNotEqual(list(sorted(expected_order)), expected_order)
        self.assertEqual(list(sorted(map(V, expected_order))), expected_order)
        # Check each individual operator (to make sure the two implementations
        # agree). We use the Version() class for this so that we test both
        # compare_versions() and the Version() wrapper.
        # Test `>'.
        self.assertTrue(V('1.0') > V('0.5'))  # usual semantics
        self.assertTrue(V('1:0.5') > V('2.0'))  # unusual semantics
        self.assertFalse(V('0.5') > V('2.0'))  # sanity check
        # Test `>='.
        self.assertTrue(V('0.75') >= V('0.5'))  # usual semantics
        self.assertTrue(V('0.50') >= V('0.5'))  # usual semantics
        self.assertTrue(V('1:0.5') >= V('5.0'))  # unusual semantics
        self.assertFalse(V('0.2') >= V('0.5'))  # sanity check
        # Test `<'.
        self.assertTrue(V('0.5') < V('1.0'))  # usual semantics
        self.assertTrue(V('2.0') < V('1:0.5'))  # unusual semantics
        self.assertFalse(V('2.0') < V('0.5'))  # sanity check
        # Test `<='.
        self.assertTrue(V('0.5') <= V('0.75'))  # usual semantics
        self.assertTrue(V('0.5') <= V('0.50'))  # usual semantics
        self.assertTrue(V('5.0') <= V('1:0.5'))  # unusual semantics
        self.assertFalse(V('0.5') <= V('0.2'))  # sanity check
        # Test `=='.
        self.assertTrue(V('42') == V('42'))  # usual semantics
        self.assertTrue(V('0.5') == V('0:0.5'))  # unusual semantics
        self.assertFalse(V('0.5') == V('1.0'))  # sanity check
        # Test `!='.
        self.assertTrue(V('1') != V('0'))  # usual semantics
        self.assertFalse(V('0.5') != V('0:0.5'))  # unusual semantics

    def test_relationship_parsing(self):
        # Happy path (no parsing errors).
        relationship_set = parse_depends('foo, bar (>= 1) | baz')
        self.assertEqual(relationship_set.relationships[0].name, 'foo')
        self.assertEqual(
            relationship_set.relationships[1].relationships[0].name, 'bar')
        self.assertEqual(
            relationship_set.relationships[1].relationships[0].operator, '>=')
        self.assertEqual(
            relationship_set.relationships[1].relationships[0].version, '1')
        self.assertEqual(
            relationship_set.relationships[1].relationships[1].name, 'baz')
        self.assertEqual(
            parse_depends('foo (=1.0)'),
            RelationshipSet(
                VersionedRelationship(name='foo', operator='=',
                                      version='1.0')))
        # Unhappy path (parsing errors).
        self.assertRaises(ValueError, parse_depends, 'foo (bar) (baz)')
        self.assertRaises(ValueError, parse_depends, 'foo (bar baz qux)')

    def test_relationship_unparsing(self):
        relationship_set = parse_depends('foo, bar(>=1)|baz')
        self.assertEqual(unicode(relationship_set), 'foo, bar (>= 1) | baz')
        self.assertEqual(
            compact(repr(relationship_set)),
            "RelationshipSet(Relationship(name='foo'), AlternativeRelationship(VersionedRelationship(name='bar', operator='>=', version='1'), Relationship(name='baz')))"
        )

    def test_relationship_evaluation(self):
        # Relationships without versions.
        relationship_set = parse_depends('python')
        self.assertTrue(relationship_set.matches('python'))
        self.assertFalse(relationship_set.matches('python2.7'))
        self.assertEqual(list(relationship_set.names), ['python'])
        # Alternatives (OR) without versions.
        relationship_set = parse_depends('python2.6 | python2.7')
        self.assertFalse(relationship_set.matches('python2.5'))
        self.assertTrue(relationship_set.matches('python2.6'))
        self.assertTrue(relationship_set.matches('python2.7'))
        self.assertFalse(relationship_set.matches('python3.0'))
        self.assertEqual(sorted(relationship_set.names),
                         ['python2.6', 'python2.7'])
        # Combinations (AND) with versions.
        relationship_set = parse_depends(
            'python (>= 2.6), python (<< 3) | python (>= 3.4)')
        self.assertFalse(relationship_set.matches('python', '2.5'))
        self.assertTrue(relationship_set.matches('python', '2.6'))
        self.assertTrue(relationship_set.matches('python', '2.7'))
        self.assertFalse(relationship_set.matches('python', '3.0'))
        self.assertTrue(relationship_set.matches('python', '3.4'))
        self.assertEqual(list(relationship_set.names), ['python'])
        # Testing for matches without providing a version is valid (should not
        # raise an error) but will never match a relationship with a version.
        relationship_set = parse_depends('python (>= 2.6), python (<< 3)')
        self.assertTrue(relationship_set.matches('python', '2.7'))
        self.assertFalse(relationship_set.matches('python'))
        self.assertEqual(list(relationship_set.names), ['python'])
        # Distinguishing between packages whose name was matched but whose
        # version didn't match vs packages whose name wasn't matched.
        relationship_set = parse_depends(
            'python (>= 2.6), python (<< 3) | python (>= 3.4)')
        self.assertEqual(relationship_set.matches('python', '2.7'),
                         True)  # name and version match
        self.assertEqual(relationship_set.matches('python', '2.5'),
                         False)  # name matched, version didn't
        self.assertEqual(relationship_set.matches('python2.6'),
                         None)  # name didn't match
        self.assertEqual(relationship_set.matches('python', '3.0'),
                         False)  # name in alternative matched, version didn't
        self.assertEqual(list(relationship_set.names), ['python'])

    def test_custom_pretty_printer(self):
        printer = CustomPrettyPrinter()
        # Test pretty printing of debian.deb822.Deb822 objects.
        self.assertEqual(
            remove_unicode_prefixes(
                printer.pformat(
                    deb822_from_string('''
            Package: pretty-printed-control-fields
            Version: 1.0
            Architecture: all
        '''))),
            remove_unicode_prefixes(
                dedent('''
            {'Architecture': u'all',
             'Package': u'pretty-printed-control-fields',
             'Version': u'1.0'}
        ''')))
        # Test pretty printing of RelationshipSet objects.
        depends_line = 'python-deb-pkg-tools, python-pip, python-pip-accel'
        self.assertEqual(
            printer.pformat(parse_depends(depends_line)),
            dedent('''
            RelationshipSet(Relationship(name='python-deb-pkg-tools'),
                            Relationship(name='python-pip'),
                            Relationship(name='python-pip-accel'))
        '''))

    def test_filename_parsing(self):
        # Test the happy path.
        filename = '/var/cache/apt/archives/python2.7_2.7.3-0ubuntu3.4_amd64.deb'
        components = parse_filename(filename)
        self.assertEqual(components.filename, filename)
        self.assertEqual(components.name, 'python2.7')
        self.assertEqual(components.version, '2.7.3-0ubuntu3.4')
        self.assertEqual(components.architecture, 'amd64')
        # Test the unhappy paths.
        self.assertRaises(ValueError, parse_filename,
                          'python2.7_2.7.3-0ubuntu3.4_amd64.not-a-deb')
        self.assertRaises(ValueError, parse_filename, 'python2.7.deb')

    def test_package_building(self,
                              repository=None,
                              overrides={},
                              contents={}):
        with Context() as finalizers:
            build_directory = finalizers.mkdtemp()
            control_fields = merge_control_fields(TEST_PACKAGE_FIELDS,
                                                  overrides)
            # Create the package template.
            os.mkdir(os.path.join(build_directory, 'DEBIAN'))
            with open(os.path.join(build_directory, 'DEBIAN', 'control'),
                      'wb') as handle:
                control_fields.dump(handle)
            if contents:
                for filename, data in contents.items():
                    filename = os.path.join(build_directory, filename)
                    directory = os.path.dirname(filename)
                    if not os.path.isdir(directory):
                        os.makedirs(directory)
                    with open(filename, 'w') as handle:
                        handle.write(data)
            else:
                with open(os.path.join(build_directory, 'DEBIAN', 'conffiles'),
                          'wb') as handle:
                    handle.write(b'/etc/file1\n')
                    handle.write(b'/etc/file2\n')
                # Create the directory with configuration files.
                os.mkdir(os.path.join(build_directory, 'etc'))
                touch(os.path.join(build_directory, 'etc', 'file1'))
                touch(os.path.join(build_directory, 'etc', 'file3'))
                # Create a directory that should be cleaned up by clean_package_tree().
                os.makedirs(os.path.join(build_directory, 'tmp', '.git'))
                # Create a file that should be cleaned up by clean_package_tree().
                with open(os.path.join(build_directory, 'tmp', '.gitignore'),
                          'w') as handle:
                    handle.write('\n')
            # Build the package (without any contents :-).
            call('--build', build_directory)
            package_file = os.path.join(
                tempfile.gettempdir(), '%s_%s_%s.deb' %
                (control_fields['Package'], control_fields['Version'],
                 control_fields['Architecture']))
            self.assertTrue(os.path.isfile(package_file))
            if repository:
                shutil.move(package_file, repository)
                return os.path.join(repository, os.path.basename(package_file))
            else:
                finalizers.register(os.unlink, package_file)
                # Verify the package metadata.
                fields, contents = inspect_package(package_file)
                for name in TEST_PACKAGE_FIELDS:
                    self.assertEqual(fields[name], TEST_PACKAGE_FIELDS[name])
                # Verify that the package contains the `/' and `/tmp'
                # directories (since it doesn't contain any actual files).
                self.assertEqual(contents['/'].permissions[0], 'd')
                self.assertEqual(contents['/'].permissions[1:], 'rwxr-xr-x')
                self.assertEqual(contents['/'].owner, 'root')
                self.assertEqual(contents['/'].group, 'root')
                self.assertEqual(contents['/tmp/'].permissions[0], 'd')
                self.assertEqual(contents['/tmp/'].owner, 'root')
                self.assertEqual(contents['/tmp/'].group, 'root')
                # Verify that clean_package_tree() cleaned up properly
                # (`/tmp/.git' and `/tmp/.gitignore' have been cleaned up).
                self.assertFalse('/tmp/.git/' in contents)
                self.assertFalse('/tmp/.gitignore' in contents)
                return package_file

    def test_command_line_interface(self):
        if not SKIP_SLOW_TESTS:
            with Context() as finalizers:
                directory = finalizers.mkdtemp()
                # Test `deb-pkg-tools --inspect PKG'.
                package_file = self.test_package_building(directory)
                lines = call('--verbose', '--inspect',
                             package_file).splitlines()
                for field, value in TEST_PACKAGE_FIELDS.items():
                    self.assertEqual(match('^ - %s: (.+)$' % field, lines),
                                     value)
                # Test `deb-pkg-tools --with-repo=DIR CMD' (we simply check whether
                # apt-cache sees the package).
                if os.getuid() == 0:
                    call('--with-repo=%s' % directory,
                         'apt-cache show %s' % TEST_PACKAGE_NAME)
                # Test `deb-pkg-tools --update=DIR' with a non-existing directory.
                self.assertRaises(SystemExit, call, '--update',
                                  '/a/directory/that/will/never/exist')

    def test_check_package(self):
        with Context() as finalizers:
            directory = finalizers.mkdtemp()
            root_package, conflicting_package = self.create_version_conflict(
                directory)
            # This *should* raise SystemExit.
            self.assertRaises(SystemExit, call, '--check', root_package)
            # Test for lack of duplicate files.
            os.unlink(conflicting_package)
            # This should *not* raise SystemExit.
            call('--check', root_package)

    def test_version_conflicts_check(self):
        with Context() as finalizers:
            # Check that version conflicts raise an exception.
            directory = finalizers.mkdtemp()
            root_package, conflicting_package = self.create_version_conflict(
                directory)
            packages_to_scan = collect_related_packages(root_package)
            # Test the duplicate files check.
            self.assertRaises(VersionConflictFound, check_version_conflicts,
                              packages_to_scan, self.package_cache)
            # Test for lack of duplicate files.
            os.unlink(conflicting_package)
            self.assertEqual(
                check_version_conflicts(packages_to_scan,
                                        cache=self.package_cache), None)

    def create_version_conflict(self, directory):
        root_package = self.test_package_building(
            directory,
            overrides=dict(Package='deb-pkg-tools-package-1',
                           Depends='deb-pkg-tools-package-2 (=1)'))
        self.test_package_building(directory,
                                   overrides=dict(
                                       Package='deb-pkg-tools-package-2',
                                       Version='1'))
        conflicting_package = self.test_package_building(
            directory,
            overrides=dict(Package='deb-pkg-tools-package-2', Version='2'))
        return root_package, conflicting_package

    def test_duplicates_check(self):
        with Context() as finalizers:
            # Check that duplicate files raise an exception.
            directory = finalizers.mkdtemp()
            # Build a package containing some files.
            self.test_package_building(directory,
                                       overrides=dict(
                                           Package='deb-pkg-tools-package-1',
                                           Version='1'))
            # Build an unrelated package containing the same files.
            self.test_package_building(
                directory, overrides=dict(Package='deb-pkg-tools-package-2'))
            # Build two versions of one package.
            duplicate_contents = {'foo/bar': 'some random file'}
            self.test_package_building(directory,
                                       overrides=dict(
                                           Package='deb-pkg-tools-package-3',
                                           Version='1'),
                                       contents=duplicate_contents)
            self.test_package_building(directory,
                                       overrides=dict(
                                           Package='deb-pkg-tools-package-3',
                                           Version='2'),
                                       contents=duplicate_contents)
            # Build two packages related by their `Conflicts' and `Provides' fields.
            virtual_package = 'deb-pkg-tools-virtual-package'
            duplicate_contents = {'foo/baz': 'another random file'}
            self.test_package_building(directory,
                                       overrides=dict(
                                           Package='deb-pkg-tools-package-4',
                                           Conflicts=virtual_package,
                                           Provides=virtual_package),
                                       contents=duplicate_contents)
            self.test_package_building(directory,
                                       overrides=dict(
                                           Package='deb-pkg-tools-package-5',
                                           Conflicts=virtual_package,
                                           Provides=virtual_package),
                                       contents=duplicate_contents)
            # Test the duplicate files check.
            package_archives = find_package_archives(directory)
            self.assertRaises(DuplicateFilesFound,
                              check_duplicate_files,
                              package_archives,
                              cache=self.package_cache)
            # Verify that invalid arguments are checked.
            self.assertRaises(ValueError, check_duplicate_files, [])

    def test_collect_packages(self):
        with Context() as finalizers:
            source_directory = finalizers.mkdtemp()
            target_directory = finalizers.mkdtemp()
            package1 = self.test_package_building(
                source_directory,
                overrides=dict(Package='deb-pkg-tools-package-1',
                               Depends='deb-pkg-tools-package-2'))
            package2 = self.test_package_building(
                source_directory,
                overrides=dict(Package='deb-pkg-tools-package-2',
                               Depends='deb-pkg-tools-package-3'))
            package3 = self.test_package_building(
                source_directory,
                overrides=dict(Package='deb-pkg-tools-package-3'))
            call('--yes', '--collect=%s' % target_directory, package1)
            self.assertEqual(
                sorted(os.listdir(target_directory)),
                sorted(map(os.path.basename, [package1, package2, package3])))

    def test_collect_packages_interactive(self):
        with Context() as finalizers:
            directory = finalizers.mkdtemp()
            package1 = self.test_package_building(
                directory,
                overrides=dict(Package='deb-pkg-tools-package-1',
                               Depends='deb-pkg-tools-package-2'))
            package2 = self.test_package_building(
                directory,
                overrides=dict(Package='deb-pkg-tools-package-2',
                               Depends='deb-pkg-tools-package-3'))
            self.test_package_building(
                directory, overrides=dict(Package='deb-pkg-tools-package-3'))
            package4 = self.test_package_building(
                directory,
                overrides=dict(Package='deb-pkg-tools-package-3',
                               Version='0.2'))
            self.assertEqual(
                sorted(p.filename for p in collect_related_packages(
                    package1, cache=self.package_cache)), [package2, package4])

    def test_repository_creation(self, preserve=False):
        if not SKIP_SLOW_TESTS:
            with Context() as finalizers:
                config_dir = tempfile.mkdtemp()
                repo_dir = tempfile.mkdtemp()
                if not preserve:
                    finalizers.register(shutil.rmtree, config_dir)
                    finalizers.register(shutil.rmtree, repo_dir)
                from deb_pkg_tools import config
                config.user_config_directory = config_dir
                with open(os.path.join(config_dir, config.repo_config_file),
                          'w') as handle:
                    handle.write('[test]\n')
                    handle.write('directory = %s\n' % repo_dir)
                    handle.write('release-origin = %s\n' % TEST_REPO_ORIGIN)
                self.test_package_building(repo_dir)
                update_repository(
                    repo_dir,
                    release_fields=dict(description=TEST_REPO_DESCRIPTION),
                    cache=self.package_cache)
                self.assertTrue(
                    os.path.isfile(os.path.join(repo_dir, 'Packages')))
                self.assertTrue(
                    os.path.isfile(os.path.join(repo_dir, 'Packages.gz')))
                self.assertTrue(
                    os.path.isfile(os.path.join(repo_dir, 'Release')))
                with open(os.path.join(repo_dir, 'Release')) as handle:
                    fields = Deb822(handle)
                    self.assertEqual(fields['Origin'], TEST_REPO_ORIGIN)
                    self.assertEqual(fields['Description'],
                                     TEST_REPO_DESCRIPTION)
                if not apt_supports_trusted_option():
                    self.assertTrue(
                        os.path.isfile(os.path.join(repo_dir, 'Release.gpg')))
                return repo_dir

    def test_repository_activation(self):
        if not SKIP_SLOW_TESTS and os.getuid() == 0:
            repository = self.test_repository_creation(preserve=True)
            call('--activate-repo=%s' % repository)
            try:
                handle = os.popen('apt-cache show %s' % TEST_PACKAGE_NAME)
                fields = Deb822(handle)
                self.assertEqual(fields['Package'], TEST_PACKAGE_NAME)
            finally:
                call('--deactivate-repo=%s' % repository)
            # XXX If we skipped the GPG key handling because apt supports the
            # [trusted=yes] option, re-run the test *including* GPG key
            # handling (we want this to be tested...).
            import deb_pkg_tools
            if deb_pkg_tools.repo.apt_supports_trusted_option():
                deb_pkg_tools.repo.trusted_option_supported = False
                self.test_repository_activation()

    def test_gpg_key_generation(self):
        if not SKIP_SLOW_TESTS:
            with Context() as finalizers:
                working_directory = finalizers.mkdtemp()
                secret_key_file = os.path.join(working_directory,
                                               'subdirectory', 'test.sec')
                public_key_file = os.path.join(working_directory,
                                               'subdirectory', 'test.pub')
                # Generate a named GPG key on the spot.
                GPGKey(name="named-test-key",
                       description=
                       "GPG key pair generated for unit tests (named key)",
                       secret_key_file=secret_key_file,
                       public_key_file=public_key_file)
                # Generate a default GPG key on the spot.
                default_key = GPGKey(
                    name="default-test-key",
                    description=
                    "GPG key pair generated for unit tests (default key)")
                self.assertEqual(os.path.basename(default_key.secret_key_file),
                                 'secring.gpg')
                self.assertEqual(os.path.basename(default_key.public_key_file),
                                 'pubring.gpg')
                # Test error handling related to GPG keys.
                self.assertRaises(Exception,
                                  GPGKey,
                                  secret_key_file=secret_key_file)
                self.assertRaises(Exception,
                                  GPGKey,
                                  public_key_file=public_key_file)
                missing_secret_key_file = '/tmp/deb-pkg-tools-%i.sec' % random.randint(
                    1, 1000)
                missing_public_key_file = '/tmp/deb-pkg-tools-%i.pub' % random.randint(
                    1, 1000)
                self.assertRaises(Exception,
                                  GPGKey,
                                  key_id='12345',
                                  secret_key_file=secret_key_file,
                                  public_key_file=missing_public_key_file)
                self.assertRaises(Exception,
                                  GPGKey,
                                  key_id='12345',
                                  secret_key_file=missing_secret_key_file,
                                  public_key_file=public_key_file)
                os.unlink(secret_key_file)
                self.assertRaises(Exception,
                                  GPGKey,
                                  name="test-key",
                                  description="Whatever",
                                  secret_key_file=secret_key_file,
                                  public_key_file=public_key_file)
                touch(secret_key_file)
                os.unlink(public_key_file)
                self.assertRaises(Exception,
                                  GPGKey,
                                  name="test-key",
                                  description="Whatever",
                                  secret_key_file=secret_key_file,
                                  public_key_file=public_key_file)
                os.unlink(secret_key_file)
                self.assertRaises(Exception,
                                  GPGKey,
                                  secret_key_file=secret_key_file,
                                  public_key_file=public_key_file)
Esempio n. 7
0
 def load_package_cache(self):
     """Prepare a temporary package cache for the duration of a test."""
     self.package_cache = PackageCache(directory=self.db_directory)
Esempio n. 8
0
class DebPkgToolsTestCase(TestCase):

    """Container for the `deb-pkg-tools` test suite."""

    def setUp(self):
        """Prepare a temporary package cache."""
        # Set up our superclass.
        super(DebPkgToolsTestCase, self).setUp()
        # Prepare the package cache.
        self.db_directory = tempfile.mkdtemp()
        self.load_package_cache()
        # Try to force entropy generation.
        os.environ['DPT_FORCE_ENTROPY'] = 'yes'

    def load_package_cache(self):
        """Prepare a temporary package cache for the duration of a test."""
        self.package_cache = PackageCache(directory=self.db_directory)

    def tearDown(self):
        """Cleanup the temporary package cache."""
        # Tear down our superclass.
        super(DebPkgToolsTestCase, self).tearDown()
        # Cleanup the package cache.
        self.package_cache.collect_garbage(force=True)
        shutil.rmtree(self.db_directory)
        # Disable entropy generation.
        os.environ.pop('DPT_FORCE_ENTROPY')

    def test_makedirs(self):
        """Test that makedirs() can deal with race conditions."""
        with Context() as finalizers:
            parent = finalizers.mkdtemp()
            child = os.path.join(parent, 'nested')
            # This will create the directory.
            makedirs(child)
            # This should not complain that the directory already exists.
            makedirs(child)

    def test_file_copying(self):
        """Test that file copying using hard links actually works."""
        with Context() as finalizers:
            source_directory = finalizers.mkdtemp()
            target_directory = finalizers.mkdtemp()
            touch(os.path.join(source_directory, '42'))
            copy_package_files(source_directory, target_directory, hard_links=True)
            assert os.stat(os.path.join(source_directory, '42')).st_ino == \
                os.stat(os.path.join(target_directory, '42')).st_ino

    def test_package_cache_invalidation(self):
        """Test that the package cache handles invalidation properly."""
        with Context() as finalizers:
            directory = finalizers.mkdtemp()
            package_file = self.test_package_building(directory, overrides=dict(
                Package='deb-pkg-tools-package-1',
                Version='1',
            ))
            for i in range(5):
                fields, contents = inspect_package(package_file, cache=self.package_cache)
                if i % 2 == 0:
                    os.utime(package_file, None)
                else:
                    self.load_package_cache()

    def test_architecture_determination(self):
        """Make sure discovery of the current build architecture works properly."""
        valid_architectures = execute('dpkg-architecture', '-L', capture=True).splitlines()
        assert find_debian_architecture() in valid_architectures

    def test_find_package_archives(self):
        """Test searching for package archives."""
        with Context() as finalizers:
            directory = finalizers.mkdtemp()
            for filename in 'some-random-file', 'regular-package_1.0_all.deb', 'micro-package_1.5_all.udeb':
                touch(os.path.join(directory, filename))
            matches = find_package_archives(directory)
            assert len(matches) == 2
            assert any(p.name == 'regular-package' and
                       p.version == '1.0' and
                       p.architecture == 'all'
                       for p in matches)
            assert any(p.name == 'micro-package' and
                       p.version == '1.5' and
                       p.architecture == 'all'
                       for p in matches)

    def test_find_latest_version(self):
        """Test the selection of latest versions."""
        good = ['name_1.0_all.deb', 'name_0.5_all.deb']
        assert os.path.basename(find_latest_version(good).filename) == 'name_1.0_all.deb'
        bad = ['one_1.0_all.deb', 'two_0.5_all.deb']
        self.assertRaises(ValueError, find_latest_version, bad)

    def test_group_by_latest_versions(self):
        """Test the grouping by latest versions."""
        packages = ['one_1.0_all.deb', 'one_0.5_all.deb', 'two_1.5_all.deb', 'two_0.1_all.deb']
        assert sorted(os.path.basename(a.filename) for a in group_by_latest_versions(packages).values()) == \
            sorted(['one_1.0_all.deb', 'two_1.5_all.deb'])

    def test_control_field_parsing(self):
        """Test the parsing of control file fields."""
        deb822_package = Deb822(['Package: python-py2deb',
                                 'Depends: python-deb-pkg-tools, python-pip, python-pip-accel',
                                 'Installed-Size: 42'])
        parsed_info = parse_control_fields(deb822_package)
        assert parsed_info == {'Package': 'python-py2deb',
                               'Depends': RelationshipSet(
                                   Relationship(name=u'python-deb-pkg-tools'),
                                   Relationship(name=u'python-pip'),
                                   Relationship(name=u'python-pip-accel')),
                               'Installed-Size': 42}
        # Test backwards compatibility with the old interface where `Depends'
        # like fields were represented as a list of strings (shallow parsed).
        parsed_info['Depends'] = [text_type(r) for r in parsed_info['Depends']]
        assert unparse_control_fields(parsed_info) == deb822_package
        # Test compatibility with fields like `Depends' containing a string.
        parsed_info['Depends'] = deb822_package['Depends']
        assert unparse_control_fields(parsed_info) == deb822_package

    def test_control_field_merging(self):
        """Test the merging of control file fields."""
        defaults = Deb822(['Package: python-py2deb',
                           'Depends: python-deb-pkg-tools',
                           'Architecture: all'])
        # The field names of the dictionary with overrides are lower case on
        # purpose; control file merging should work properly regardless of
        # field name casing.
        overrides = Deb822(dict(version='1.0',
                                depends='python-pip, python-pip-accel',
                                architecture='amd64'))
        assert merge_control_fields(defaults, overrides) == \
            Deb822(['Package: python-py2deb',
                    'Version: 1.0',
                    'Depends: python-deb-pkg-tools, python-pip, python-pip-accel',
                    'Architecture: amd64'])

    def test_control_file_creation(self):
        """Test control file creation."""
        with Context() as context:
            directory = context.mkdtemp()
            # Use a non-existing subdirectory to verify that it's created.
            control_file = os.path.join(directory, 'DEBIAN', 'control')
            # Try to create a control file but omit some mandatory fields.
            self.assertRaises(ValueError, create_control_file, control_file, dict(Package='created-from-python'))
            # Now we'll provide all of the required fields to actually create the file.
            create_control_file(control_file, dict(
                Package='created-from-python',
                Description='whatever',
                Maintainer='Peter Odding',
                Version='1.0',
            ))
            # Load the control file to verify its contents.
            control_fields = load_control_file(control_file)
            # These fields were provided by us (the caller of create_control_file()).
            assert control_fields['Package'] == 'created-from-python'
            assert control_fields['Description'] == 'whatever'
            # This field was written as a default value.
            assert control_fields['Architecture'] == 'all'

    def test_control_file_patching_and_loading(self):
        """Test patching and loading of control files."""
        deb822_package = Deb822(['Package: unpatched-example',
                                 'Depends: some-dependency'])
        with Context() as finalizers:
            control_file = tempfile.mktemp()
            finalizers.register(os.unlink, control_file)
            with open(control_file, 'wb') as handle:
                deb822_package.dump(handle)
            returncode, output = run_cli(
                main, '--patch=%s' % control_file,
                '--set=Package: patched-example',
                '--set=Depends: another-dependency',
            )
            assert returncode == 0
            patched_fields = load_control_file(control_file)
            assert patched_fields['Package'] == 'patched-example'
            assert str(patched_fields['Depends']) == 'another-dependency, some-dependency'

    def test_version_comparison(self):
        """Test the comparison of version objects (under both implementations)."""
        self.version_comparison_helper()
        if version.have_python_apt:
            with PatchedAttribute(version, 'have_python_apt', False):
                self.version_comparison_helper()
                self.assertRaises(NotImplementedError, version.compare_versions_with_python_apt, '0.1', '<<', '0.2')

    def version_comparison_helper(self):
        """Test the comparison of version objects."""
        # V() shortcut for deb_pkg_tools.version.Version().
        V = version.Version
        # Check version sorting implemented on top of `=' and `<<' comparisons.
        expected_order = ['0.1', '0.5', '1.0', '2.0', '3.0', '1:0.4', '2:0.3']
        assert list(sorted(expected_order)) != expected_order
        assert list(sorted(map(V, expected_order))) == expected_order
        # Check each individual operator (to make sure the two implementations
        # agree). We use the Version() class for this so that we test both
        # compare_versions() and the Version() wrapper.
        # Test `>'.
        assert V('1.0') > V('0.5')      # usual semantics
        assert V('1:0.5') > V('2.0')    # unusual semantics
        assert not V('0.5') > V('2.0')  # sanity check
        # Test `>='.
        assert V('0.75') >= V('0.5')     # usual semantics
        assert V('0.50') >= V('0.5')     # usual semantics
        assert V('1:0.5') >= V('5.0')    # unusual semantics
        assert not V('0.2') >= V('0.5')  # sanity check
        # Test `<'.
        assert V('0.5') < V('1.0')      # usual semantics
        assert V('2.0') < V('1:0.5')    # unusual semantics
        assert not V('2.0') < V('0.5')  # sanity check
        # Test `<='.
        assert V('0.5') <= V('0.75')     # usual semantics
        assert V('0.5') <= V('0.50')     # usual semantics
        assert V('5.0') <= V('1:0.5')    # unusual semantics
        assert not V('0.5') <= V('0.2')  # sanity check
        # Test `=='.
        assert V('42') == V('42')        # usual semantics
        assert V('0.5') == V('0:0.5')    # unusual semantics
        assert not V('0.5') == V('1.0')  # sanity check
        # Test `!='.
        assert V('1') != V('0')            # usual semantics
        assert not V('0.5') != V('0:0.5')  # unusual semantics

    def test_relationship_parsing(self):
        """Test the parsing of Debian package relationship declarations."""
        # Happy path (no parsing errors).
        relationship_set = parse_depends('foo, bar (>= 1) | baz')
        assert relationship_set.relationships[0].name == 'foo'
        assert relationship_set.relationships[1].relationships[0].name == 'bar'
        assert relationship_set.relationships[1].relationships[0].operator == '>='
        assert relationship_set.relationships[1].relationships[0].version == '1'
        assert relationship_set.relationships[1].relationships[1].name == 'baz'
        assert parse_depends('foo (=1.0)') == RelationshipSet(VersionedRelationship(
            name='foo',
            operator='=',
            version='1.0',
        ))
        # Unhappy path (parsing errors).
        self.assertRaises(ValueError, parse_depends, 'foo (bar) (baz)')
        self.assertRaises(ValueError, parse_depends, 'foo (bar baz qux)')

    def test_architecture_restriction_parsing(self):
        """Test the parsing of architecture restrictions."""
        relationship_set = parse_depends('qux [i386 amd64]')
        assert relationship_set.relationships[0].name == 'qux'
        assert len(relationship_set.relationships[0].architectures) == 2
        assert 'i386' in relationship_set.relationships[0].architectures
        assert 'amd64' in relationship_set.relationships[0].architectures

    def test_relationship_unparsing(self):
        """Test the unparsing (serialization) of parsed relationship declarations."""
        def strip(text):
            return re.sub(r'\s+', '', text)
        relationship_set = parse_depends('foo, bar(>=1)|baz[i386]')
        assert text_type(relationship_set) == 'foo, bar (>= 1) | baz [i386]'
        assert strip(repr(relationship_set)) == strip("""
            RelationshipSet(
                Relationship(name='foo', architectures=()),
                AlternativeRelationship(
                    VersionedRelationship(name='bar', operator='>=', version='1', architectures=()),
                    Relationship(name='baz', architectures=('i386',))
                )
            )
        """)

    def test_relationship_evaluation(self):
        """Test the evaluation of package relationships."""
        # Relationships without versions.
        relationship_set = parse_depends('python')
        assert relationship_set.matches('python')
        assert not relationship_set.matches('python2.7')
        assert list(relationship_set.names) == ['python']
        # Alternatives (OR) without versions.
        relationship_set = parse_depends('python2.6 | python2.7')
        assert not relationship_set.matches('python2.5')
        assert relationship_set.matches('python2.6')
        assert relationship_set.matches('python2.7')
        assert not relationship_set.matches('python3.0')
        assert sorted(relationship_set.names) == ['python2.6', 'python2.7']
        # Combinations (AND) with versions.
        relationship_set = parse_depends('python (>= 2.6), python (<< 3) | python (>= 3.4)')
        assert not relationship_set.matches('python', '2.5')
        assert relationship_set.matches('python', '2.6')
        assert relationship_set.matches('python', '2.7')
        assert not relationship_set.matches('python', '3.0')
        assert relationship_set.matches('python', '3.4')
        assert list(relationship_set.names) == ['python']
        # Testing for matches without providing a version is valid (should not
        # raise an error) but will never match a relationship with a version.
        relationship_set = parse_depends('python (>= 2.6), python (<< 3)')
        assert relationship_set.matches('python', '2.7')
        assert not relationship_set.matches('python')
        assert list(relationship_set.names) == ['python']
        # Distinguishing between packages whose name was matched but whose
        # version didn't match vs packages whose name wasn't matched.
        relationship_set = parse_depends('python (>= 2.6), python (<< 3) | python (>= 3.4)')
        assert relationship_set.matches('python', '2.7') is True  # name and version match
        assert relationship_set.matches('python', '2.5') is False  # name matched, version didn't
        assert relationship_set.matches('python2.6') is None  # name didn't match
        assert relationship_set.matches('python', '3.0') is False  # name in alternative matched, version didn't
        assert list(relationship_set.names) == ['python']

    def test_custom_pretty_printer(self):
        """Test pretty printing of deb822 objects and parsed relationships."""
        printer = CustomPrettyPrinter()
        # Test pretty printing of debian.deb822.Deb822 objects.
        deb822_object = deb822_from_string('''
            Package: pretty-printed-control-fields
            Version: 1.0
            Architecture: all
        ''')
        formatted_object = printer.pformat(deb822_object)
        assert normalize_repr_output(formatted_object) == normalize_repr_output('''
            {'Architecture': u'all',
             'Package': u'pretty-printed-control-fields',
             'Version': u'1.0'}
        ''')
        # Test pretty printing of RelationshipSet objects.
        relationship_set = parse_depends('python-deb-pkg-tools, python-pip, python-pip-accel')
        formatted_object = printer.pformat(relationship_set)
        assert normalize_repr_output(formatted_object) == normalize_repr_output('''
            RelationshipSet(Relationship(name='python-deb-pkg-tools', architectures=()),
                            Relationship(name='python-pip', architectures=()),
                            Relationship(name='python-pip-accel', architectures=()))
        ''')

    def test_filename_parsing(self):
        """Test filename parsing."""
        # Test the happy path.
        filename = '/var/cache/apt/archives/python2.7_2.7.3-0ubuntu3.4_amd64.deb'
        components = parse_filename(filename)
        assert components.filename == filename
        assert components.name == 'python2.7'
        assert components.version == '2.7.3-0ubuntu3.4'
        assert components.architecture == 'amd64'
        # Test the unhappy paths.
        self.assertRaises(ValueError, parse_filename, 'python2.7_2.7.3-0ubuntu3.4_amd64.not-a-deb')
        self.assertRaises(ValueError, parse_filename, 'python2.7.deb')

    def test_find_object_files(self):
        """Test the :func:`deb_pkg_tools.package.find_object_files()` function."""
        with Context() as finalizers:
            directory = finalizers.mkdtemp()
            shutil.copy(__file__, directory)
            shutil.copy(sys.executable, directory)
            object_files = find_object_files(directory)
            assert len(object_files) == 1
            assert object_files[0] == os.path.join(directory, os.path.basename(sys.executable))

    def test_find_system_dependencies(self):
        """Test the :func:`deb_pkg_tools.package.find_system_dependencies()` function."""
        dependencies = find_system_dependencies(['/usr/bin/python'])
        assert len(dependencies) >= 1
        assert any(re.match(r'^libc\d+\b', d) for d in dependencies)

    def test_package_building(self, repository=None, overrides={}, contents={}):
        """Test building of Debian binary packages."""
        with Context() as finalizers:
            build_directory = finalizers.mkdtemp()
            control_fields = merge_control_fields(TEST_PACKAGE_FIELDS, overrides)
            # Create the package template.
            os.mkdir(os.path.join(build_directory, 'DEBIAN'))
            with open(os.path.join(build_directory, 'DEBIAN', 'control'), 'wb') as handle:
                control_fields.dump(handle)
            if contents:
                for filename, data in contents.items():
                    filename = os.path.join(build_directory, filename)
                    directory = os.path.dirname(filename)
                    makedirs(directory)
                    with open(filename, 'w') as handle:
                        handle.write(data)
            else:
                with open(os.path.join(build_directory, 'DEBIAN', 'conffiles'), 'wb') as handle:
                    handle.write(b'/etc/file1\n')
                    handle.write(b'/etc/file2\n')
                # Create the directory with configuration files.
                os.mkdir(os.path.join(build_directory, 'etc'))
                touch(os.path.join(build_directory, 'etc', 'file1'))
                touch(os.path.join(build_directory, 'etc', 'file3'))
                # Create a directory that should be cleaned up by clean_package_tree().
                makedirs(os.path.join(build_directory, 'tmp', '.git'))
                # Create a file that should be cleaned up by clean_package_tree().
                with open(os.path.join(build_directory, 'tmp', '.gitignore'), 'w') as handle:
                    handle.write('\n')
            # Build the package (without any contents :-).
            returncode, output = run_cli(main, '--build', build_directory)
            assert returncode == 0
            package_file = os.path.join(tempfile.gettempdir(),
                                        '%s_%s_%s.deb' % (control_fields['Package'],
                                                          control_fields['Version'],
                                                          control_fields['Architecture']))
            assert os.path.isfile(package_file)
            if repository:
                shutil.move(package_file, repository)
                return os.path.join(repository, os.path.basename(package_file))
            else:
                finalizers.register(os.unlink, package_file)
                # Verify the package metadata.
                fields, contents = inspect_package(package_file)
                for name in TEST_PACKAGE_FIELDS:
                    assert fields[name] == TEST_PACKAGE_FIELDS[name]
                # Verify that the package contains the `/' and `/tmp'
                # directories (since it doesn't contain any actual files).
                assert contents['/'].permissions[0] == 'd'
                assert contents['/'].permissions[1:] == 'rwxr-xr-x'
                assert contents['/'].owner == 'root'
                assert contents['/'].group == 'root'
                assert contents['/tmp/'].permissions[0] == 'd'
                assert contents['/tmp/'].owner == 'root'
                assert contents['/tmp/'].group == 'root'
                # Verify that clean_package_tree() cleaned up properly
                # (`/tmp/.git' and `/tmp/.gitignore' have been cleaned up).
                assert '/tmp/.git/' not in contents
                assert '/tmp/.gitignore' not in contents
                return package_file

    def test_command_line_interface(self):
        """Test the command line interface."""
        if SKIP_SLOW_TESTS:
            return self.skipTest("skipping slow tests")
        with Context() as finalizers:
            directory = finalizers.mkdtemp()
            # Test `deb-pkg-tools --inspect PKG'.
            package_file = self.test_package_building(directory)
            returncode, output = run_cli(main, '--verbose', '--inspect', package_file)
            assert returncode == 0
            lines = output.splitlines()
            for field, value in TEST_PACKAGE_FIELDS.items():
                assert match('^ - %s: (.+)$' % field, lines) == value
            # Test `deb-pkg-tools --update=DIR' with a non-existing directory.
            returncode, output = run_cli(main, '--update', '/a/directory/that/will/never/exist')
            assert returncode != 0

    def test_with_repo_cli(self):
        """Test ``deb-pkg-tools --with-repo``."""
        if SKIP_SLOW_TESTS:
            return self.skipTest("skipping slow tests")
        elif os.getuid() != 0:
            return self.skipTest("need superuser privileges")
        with Context() as finalizers:
            directory = finalizers.mkdtemp()
            self.test_package_building(directory)
            with CaptureOutput() as capturer:
                run_cli(
                    main, '--with-repo=%s' % directory,
                    'apt-cache show %s' % TEST_PACKAGE_NAME,
                )
                # Check whether apt-cache sees the package.
                expected_line = "Package: %s" % TEST_PACKAGE_NAME
                assert expected_line in capturer.get_lines()

    def test_check_package(self):
        """Test the command line interface for static analysis of package archives."""
        with Context() as finalizers:
            directory = finalizers.mkdtemp()
            root_package, conflicting_package = self.create_version_conflict(directory)
            # This *should* raise SystemExit.
            returncode, output = run_cli(main, '--check', root_package)
            assert returncode != 0
            # Test for lack of duplicate files.
            os.unlink(conflicting_package)
            # This should *not* raise SystemExit.
            returncode, output = run_cli(main, '--check', root_package)
            assert returncode == 0

    def test_version_conflicts_check(self):
        """Test static analysis of version conflicts."""
        with Context() as finalizers:
            # Check that version conflicts raise an exception.
            directory = finalizers.mkdtemp()
            root_package, conflicting_package = self.create_version_conflict(directory)
            packages_to_scan = collect_related_packages(root_package)
            # Test the duplicate files check.
            self.assertRaises(VersionConflictFound, check_version_conflicts, packages_to_scan, self.package_cache)
            # Test for lack of duplicate files.
            os.unlink(conflicting_package)
            assert check_version_conflicts(packages_to_scan, cache=self.package_cache) is None

    def create_version_conflict(self, directory):
        """Build a directory of packages with a version conflict."""
        root_package = self.test_package_building(directory, overrides=dict(
            Package='deb-pkg-tools-package-1',
            Depends='deb-pkg-tools-package-2 (=1)',
        ))
        self.test_package_building(directory, overrides=dict(
            Package='deb-pkg-tools-package-2',
            Version='1',
        ))
        conflicting_package = self.test_package_building(directory, overrides=dict(
            Package='deb-pkg-tools-package-2',
            Version='2',
        ))
        return root_package, conflicting_package

    def test_duplicates_check(self):
        """Test static analysis of duplicate files."""
        with Context() as finalizers:
            # Check that duplicate files raise an exception.
            directory = finalizers.mkdtemp()
            # Build a package containing some files.
            self.test_package_building(directory, overrides=dict(Package='deb-pkg-tools-package-1', Version='1'))
            # Build an unrelated package containing the same files.
            self.test_package_building(directory, overrides=dict(Package='deb-pkg-tools-package-2'))
            # Build two versions of one package.
            duplicate_contents = {'foo/bar': 'some random file'}
            self.test_package_building(directory,
                                       overrides=dict(Package='deb-pkg-tools-package-3', Version='1'),
                                       contents=duplicate_contents)
            self.test_package_building(directory,
                                       overrides=dict(Package='deb-pkg-tools-package-3', Version='2'),
                                       contents=duplicate_contents)
            # Build two packages related by their `Conflicts' and `Provides' fields.
            virtual_package = 'deb-pkg-tools-virtual-package'
            duplicate_contents = {'foo/baz': 'another random file'}
            self.test_package_building(directory,
                                       overrides=dict(Package='deb-pkg-tools-package-4',
                                                      Conflicts=virtual_package,
                                                      Provides=virtual_package),
                                       contents=duplicate_contents)
            self.test_package_building(directory,
                                       overrides=dict(Package='deb-pkg-tools-package-5',
                                                      Conflicts=virtual_package,
                                                      Provides=virtual_package),
                                       contents=duplicate_contents)
            # Test the duplicate files check.
            package_archives = find_package_archives(directory)
            self.assertRaises(DuplicateFilesFound, check_duplicate_files, package_archives, cache=self.package_cache)
            # Verify that invalid arguments are checked.
            self.assertRaises(ValueError, check_duplicate_files, [])

    def test_collect_packages(self):
        """Test the command line interface for collection of related packages."""
        with Context() as finalizers:
            source_directory = finalizers.mkdtemp()
            target_directory = finalizers.mkdtemp()
            package1 = self.test_package_building(source_directory, overrides=dict(
                Package='deb-pkg-tools-package-1',
                Depends='deb-pkg-tools-package-2',
            ))
            package2 = self.test_package_building(source_directory, overrides=dict(
                Package='deb-pkg-tools-package-2',
                Depends='deb-pkg-tools-package-3',
            ))
            package3 = self.test_package_building(source_directory, overrides=dict(
                Package='deb-pkg-tools-package-3',
            ))
            returncode, output = run_cli(
                main, '--yes',
                '--collect=%s' % target_directory,
                package1,
            )
            assert returncode == 0
            assert sorted(os.listdir(target_directory)) == \
                sorted(map(os.path.basename, [package1, package2, package3]))

    def test_collect_packages_preference_for_newer_versions(self):
        """Test the preference of package collection for newer versions."""
        with Context() as finalizers:
            directory = finalizers.mkdtemp()
            package1 = self.test_package_building(directory, overrides=dict(
                Package='deb-pkg-tools-package-1',
                Depends='deb-pkg-tools-package-2',
            ))
            package2_1 = self.test_package_building(directory, overrides=dict(
                Package='deb-pkg-tools-package-2',
                Version='1',
                Depends='deb-pkg-tools-package-3 (= 1)',
            ))
            package2_2 = self.test_package_building(directory, overrides=dict(
                Package='deb-pkg-tools-package-2',
                Version='2',
                Depends='deb-pkg-tools-package-3 (= 2)',
            ))
            package3_1 = self.test_package_building(directory, overrides=dict(
                Package='deb-pkg-tools-package-3',
                Version='1',
            ))
            package3_2 = self.test_package_building(directory, overrides=dict(
                Package='deb-pkg-tools-package-3',
                Version='2',
            ))
            related_packages = [p.filename for p in collect_related_packages(package1, cache=self.package_cache)]
            # Make sure deb-pkg-tools-package-2 version 1 wasn't collected.
            assert package2_1 not in related_packages
            # Make sure deb-pkg-tools-package-2 version 2 was collected.
            assert package2_2 in related_packages
            # Make sure deb-pkg-tools-package-3 version 1 wasn't collected.
            assert package3_1 not in related_packages
            # Make sure deb-pkg-tools-package-3 version 2 was collected.
            assert package3_2 in related_packages

    def test_collect_packages_with_conflict_resolution(self):
        """Test conflict resolution in collection of related packages."""
        with Context() as finalizers:
            directory = finalizers.mkdtemp()
            # The following names are a bit confusing, this is to enforce
            # implicit sorting on file system level (exposing an otherwise
            # unnoticed bug).
            package_a = self.test_package_building(directory, overrides=dict(
                Package='package-a',
                Depends='package-b, package-c',
            ))
            package_b = self.test_package_building(directory, overrides=dict(
                Package='package-b',
                Depends='package-d',
            ))
            package_c = self.test_package_building(directory, overrides=dict(
                Package='package-c',
                Depends='package-d (= 1)',
            ))
            package_d1 = self.test_package_building(directory, overrides=dict(
                Package='package-d',
                Version='1',
            ))
            package_d2 = self.test_package_building(directory, overrides=dict(
                Package='package-d',
                Version='2',
            ))
            related_packages = [p.filename for p in collect_related_packages(package_a, cache=self.package_cache)]
            # Make sure package-b was collected.
            assert package_b in related_packages
            # Make sure package-c was collected.
            assert package_c in related_packages
            # Make sure package-d1 was collected.
            assert package_d1 in related_packages
            # Make sure package-d2 wasn't collected.
            assert package_d2 not in related_packages

    def test_collect_packages_with_prompt(self):
        """Test the confirmation prompt during interactive package collection."""
        with Context() as finalizers:
            # Temporarily change stdin to respond with `y' (for `yes').
            finalizers.register(setattr, sys, 'stdin', sys.stdin)
            sys.stdin = StringIO('y')
            # Prepare some packages to collect.
            source_directory = finalizers.mkdtemp()
            target_directory = finalizers.mkdtemp()
            package1 = self.test_package_building(source_directory, overrides=dict(
                Package='deb-pkg-tools-package-1',
                Depends='deb-pkg-tools-package-2',
            ))
            package2 = self.test_package_building(source_directory, overrides=dict(
                Package='deb-pkg-tools-package-2',
            ))
            # Run `deb-pkg-tools --collect' ...
            returncode, output = run_cli(main, '--collect=%s' % target_directory, package1)
            assert returncode == 0
            assert sorted(os.listdir(target_directory)) == sorted(map(os.path.basename, [package1, package2]))

    def test_collect_packages_concurrent(self):
        """Test concurrent collection of related packages."""
        with Context() as finalizers:
            source_directory = finalizers.mkdtemp()
            target_directory = finalizers.mkdtemp()
            # Prepare some packages to collect.
            package1 = self.test_package_building(source_directory, overrides=dict(
                Package='package-1',
                Depends='package-3',
            ))
            package2 = self.test_package_building(source_directory, overrides=dict(
                Package='package-2',
                Depends='package-4',
            ))
            package3 = self.test_package_building(source_directory, overrides=dict(
                Package='package-3',
            ))
            package4 = self.test_package_building(source_directory, overrides=dict(
                Package='package-4',
            ))
            # Run `deb-pkg-tools --collect' ...
            returncode, output = run_cli(
                main, '--collect=%s' % target_directory,
                '--yes', package1, package2,
            )
            assert returncode == 0
            # Make sure the expected packages were promoted.
            assert sorted(os.listdir(target_directory)) == \
                sorted(map(os.path.basename, [package1, package2, package3, package4]))

    def test_repository_creation(self, preserve=False):
        """Test the creation of trivial repositories."""
        if SKIP_SLOW_TESTS:
            return self.skipTest("skipping slow tests")
        with Context() as finalizers:
            config_dir = tempfile.mkdtemp()
            repo_dir = tempfile.mkdtemp()
            if not preserve:
                finalizers.register(shutil.rmtree, config_dir)
                finalizers.register(shutil.rmtree, repo_dir)
            from deb_pkg_tools import config
            config.user_config_directory = config_dir
            with open(os.path.join(config_dir, config.repo_config_file), 'w') as handle:
                handle.write('[test]\n')
                handle.write('directory = %s\n' % repo_dir)
                handle.write('release-origin = %s\n' % TEST_REPO_ORIGIN)
            self.test_package_building(repo_dir)
            update_repository(repo_dir,
                              release_fields=dict(description=TEST_REPO_DESCRIPTION),
                              cache=self.package_cache)
            assert os.path.isfile(os.path.join(repo_dir, 'Packages'))
            assert os.path.isfile(os.path.join(repo_dir, 'Packages.gz'))
            assert os.path.isfile(os.path.join(repo_dir, 'Release'))
            with open(os.path.join(repo_dir, 'Release')) as handle:
                fields = Deb822(handle)
                assert fields['Origin'] == TEST_REPO_ORIGIN
                assert fields['Description'] == TEST_REPO_DESCRIPTION
            if not apt_supports_trusted_option():
                assert os.path.isfile(os.path.join(repo_dir, 'Release.gpg'))
            return repo_dir

    def test_repository_activation(self):
        """Test the activation of trivial repositories."""
        if SKIP_SLOW_TESTS:
            return self.skipTest("skipping slow tests")
        elif os.getuid() != 0:
            return self.skipTest("need superuser privileges")
        repository = self.test_repository_creation(preserve=True)
        returncode, output = run_cli(main, '-vv', '--activate-repo=%s' % repository)
        assert returncode == 0
        try:
            handle = os.popen('apt-cache show %s' % TEST_PACKAGE_NAME)
            fields = Deb822(handle)
            assert fields['Package'] == TEST_PACKAGE_NAME
        finally:
            returncode, output = run_cli(main, '-vv', '--deactivate-repo=%s' % repository)
            assert returncode == 0

    def test_repository_activation_fallback(self):
        """Test the activation of trivial repositories using the fall-back mechanism."""
        # If we skipped the GPG key handling in test_repository_activation()
        # because apt supports the [trusted=yes] option, we re-run the test
        # *including* GPG key handling because we want this to be tested...
        from deb_pkg_tools import repo
        if repo.apt_supports_trusted_option():
            with PatchedAttribute(repo, 'apt_supports_trusted_option', lambda: False):
                self.test_repository_activation()

    def test_gpg_key_generation(self):
        """Test automatic GPG key generation."""
        if SKIP_SLOW_TESTS:
            return self.skipTest("skipping slow tests")
        with Context() as finalizers:
            directory = finalizers.mkdtemp()
            # Generate a named GPG key on the spot.
            key = GPGKey(
                description="GPG key pair generated for unit tests",
                directory=directory,
                name="deb-pkg-tools test suite",
            )
            # Make sure a key pair was generated.
            assert key.existing_files
            # Make sure an identifier can be extracted from the key.
            assert re.match('^[0-9A-Fa-f]{10,}$', key.identifier)

    def test_gpg_key_error_handling(self):
        """Test explicit error handling of GPG key generation."""
        from deb_pkg_tools import gpg
        with PatchedAttribute(gpg, 'have_updated_gnupg', lambda: False):
            with Context() as finalizers:
                directory = finalizers.mkdtemp()
                options = dict(
                    key_id='12345',
                    public_key_file=os.path.join(directory, 'test.pub'),
                    secret_key_file=os.path.join(directory, 'test.sec'),
                )
                touch(options['public_key_file'])
                self.assertRaises(EnvironmentError, GPGKey, **options)
                os.unlink(options['public_key_file'])
                touch(options['secret_key_file'])
                self.assertRaises(EnvironmentError, GPGKey, **options)