diff --git a/britney.py b/britney.py index de20218..a64a684 100755 --- a/britney.py +++ b/britney.py @@ -1160,6 +1160,7 @@ class Britney(object): # if no package can satisfy the dependency, add this information to the excuse if not packages: excuse.addhtml("%s/%s unsatisfiable Depends: %s" % (pkg, arch, block_txt.strip())) + excuse.add_unsatisfiable_dep(block_txt.strip(), arch) excuse.addreason("depends") if arch not in self.options.break_arches: is_all_ok = False diff --git a/britney2/excuse.py b/britney2/excuse.py index 72cef0d..e1e107c 100644 --- a/britney2/excuse.py +++ b/britney2/excuse.py @@ -60,6 +60,7 @@ class Excuse(object): self.deps = {} self.sane_deps = [] self.break_deps = [] + self.unsat_deps = {} self.bugs = [] self.newbugs = set() self.oldbugs = set() @@ -123,6 +124,13 @@ class Excuse(object): if (name, arch) not in self.break_deps: self.break_deps.append( (name, arch) ) + def add_unsatisfiable_dep(self, signature, arch): + """Add an unsatisfiable dependency""" + if arch not in self.unsat_deps: + self.unsat_deps[arch] = [] + if signature not in self.unsat_deps[arch]: + self.unsat_deps[arch].append(signature) + def invalidate_dep(self, name): """Invalidate dependency""" if name not in self.invalid_deps: self.invalid_deps.append(name) @@ -246,7 +254,7 @@ class Excuse(object): 'on-architectures': sorted(self.missing_builds), 'on-unimportant-architectures': sorted(self.missing_builds_ood_arch), } - if self.deps or self.invalid_deps or self.break_deps: + if self.deps or self.invalid_deps or self.break_deps or self.unsat_deps: excusedata['dependencies'] = dep_data = {} migrate_after = sorted(x for x in self.deps if x not in self.invalid_deps) break_deps = [x for x, _ in self.break_deps if x not in self.deps] @@ -257,6 +265,12 @@ class Excuse(object): dep_data['migrate-after'] = migrate_after if break_deps: dep_data['unimportant-dependencies'] = sorted(break_deps) + if self.unsat_deps: + dep_data['unsatisfiable-dependencies'] = unsatisfiable = {} + for arch in self.unsat_deps: + unsatisfiable[arch] = [] + for sig in self.unsat_deps[arch]: + unsatisfiable[arch].append("%s" % sig) if self.needs_approval: status = 'not-approved' for h in self.hints: diff --git a/tests/test_yaml.py b/tests/test_yaml.py new file mode 100755 index 0000000..86063e5 --- /dev/null +++ b/tests/test_yaml.py @@ -0,0 +1,132 @@ +#!/usr/bin/python3 +# (C) 2017 Canonical Ltd. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. + +from collections import defaultdict + +import fileinput +import json +import os +import pprint +import sys +import unittest + +import apt_pkg +import yaml + +PROJECT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +sys.path.insert(0, PROJECT_DIR) + +from tests import TestBase, mock_swift + +apt_pkg.init() + + +class YamlTest(TestBase): + ''' Validate generated yaml ''' + def setUp(self): + super().setUp() + self.fake_amqp = os.path.join(self.data.path, 'amqp') + + # Set fake AMQP and Swift server + for line in fileinput.input(self.britney_conf, inplace=True): + if 'ADT_AMQP' in line: + print('ADT_AMQP = file://%s' % self.fake_amqp) + elif 'ADT_SWIFT_URL' in line: + print('ADT_SWIFT_URL = http://localhost:18085') + elif 'ADT_ARCHES' in line: + print('ADT_ARCHES = amd64 i386') + else: + sys.stdout.write(line) + + self.data.add('libc6', False) + self.sourceppa_cache = {} + + self.email_cache = {} + for pkg, vals in self.sourceppa_cache.items(): + for version, empty in vals.items(): + self.email_cache.setdefault(pkg, {}) + self.email_cache[pkg][version] = True + + # create mock Swift server (but don't start it yet, as tests first need + # to poke in results) + self.swift = mock_swift.AutoPkgTestSwiftServer(port=18085) + self.swift.set_results({}) + + def tearDown(self): + del self.swift + + def do_test(self, unstable_add): + '''Run britney with some unstable packages and verify excuses. + + unstable_add is a list of (binpkgname, field_dict, daysold) + + Return excuses_dict. + ''' + age_file = os.path.join(self.data.path, + 'data', + 'series', + 'Dates') + for (pkg, fields, daysold) in unstable_add: + self.data.add(pkg, True, fields, True, None) + self.sourceppa_cache.setdefault(pkg, {}) + if fields['Version'] not in self.sourceppa_cache[pkg]: + self.sourceppa_cache[pkg][fields['Version']] = '' + with open(age_file, 'w') as f: + import time + do = time.time() - (60 * 60 * 24 * daysold) + f.write('%s %s %d' % (pkg, fields['Version'], do)) + + + # Set up sourceppa cache for testing + sourceppa_path = os.path.join(self.data.dirs[True], 'SourcePPA') + with open(sourceppa_path, 'w', encoding='utf-8') as sourceppa: + sourceppa.write(json.dumps(self.sourceppa_cache)) + + (excuses_yaml, excuses_html, out) = self.run_britney() + + # convert excuses to source indexed dict + excuses_dict = {} + for s in yaml.load(excuses_yaml)['sources']: + excuses_dict[s['source']] = s + + if 'SHOW_EXCUSES' in os.environ: + print('------- excuses -----') + pprint.pprint(excuses_dict, width=200) + if 'SHOW_YAML' in os.environ: + print('------- excuses.yaml -----\n%s\n' % excuses_yaml) + if 'SHOW_HTML' in os.environ: + print('------- excuses.html -----\n%s\n' % excuses_html) + if 'SHOW_OUTPUT' in os.environ: + print('------- output -----\n%s\n' % out) + + self.assertNotIn('FIXME', out) + + return excuses_dict + + def test_unsat_deps(self): + '''Test unsatisfiable dependencies list''' + pkg = ('libc6', {'Version': '2', + 'Depends': 'notavailable (>= 2)'}, + 6) + + excuse = self.do_test([pkg]) + self.assertIn('notavailable (>= 2)', excuse['libc6']['dependencies']['unsatisfiable-dependencies']['amd64']) + + def test_epoch_in_deps(self): + '''Test dependencies listing with epoch in versioned dep''' + pkg = ('libc6', {'Version': '2', + 'Depends': 'datefudge (>= 99:1.0-0.1ubuntu8)'}, + 6) + + excuse = self.do_test([pkg]) + self.assertIn('datefudge', excuse['libc6']['dependencies']['unsatisfiable-dependencies']['amd64'][0]) + + + +if __name__ == '__main__': + unittest.main()