Gentoo Archives: gentoo-commits

From: Sebastian Luther <SebastianLuther@×××.de>
To: gentoo-commits@l.g.o
Subject: [gentoo-commits] proj/portage:master commit in: pym/_emerge/resolver/, pym/portage/tests/resolver/
Date: Wed, 05 Feb 2014 19:42:13
Message-Id: 1391629160.ea85cb655918ebdec65a49a2c0b1b4a8d4090208.few@gentoo
1 commit: ea85cb655918ebdec65a49a2c0b1b4a8d4090208
2 Author: Sebastian Luther <SebastianLuther <AT> gmx <DOT> de>
3 AuthorDate: Tue Jan 28 21:29:54 2014 +0000
4 Commit: Sebastian Luther <SebastianLuther <AT> gmx <DOT> de >
5 CommitDate: Wed Feb 5 19:39:20 2014 +0000
6 URL: http://git.overlays.gentoo.org/gitweb/?p=proj/portage.git;a=commit;h=ea85cb65
7
8 Add resolver/package_tracker
9
10 ---
11 pym/_emerge/resolver/package_tracker.py | 301 +++++++++++++++++++++
12 pym/portage/tests/resolver/test_package_tracker.py | 261 ++++++++++++++++++
13 2 files changed, 562 insertions(+)
14
15 diff --git a/pym/_emerge/resolver/package_tracker.py b/pym/_emerge/resolver/package_tracker.py
16 new file mode 100644
17 index 0000000..5982750
18 --- /dev/null
19 +++ b/pym/_emerge/resolver/package_tracker.py
20 @@ -0,0 +1,301 @@
21 +# Copyright 2014 Gentoo Foundation
22 +# Distributed under the terms of the GNU General Public License v2
23 +
24 +from __future__ import print_function
25 +
26 +import collections
27 +
28 +import portage
29 +portage.proxy.lazyimport.lazyimport(globals(),
30 + 'portage.dep:Atom,match_from_list',
31 + 'portage.util:cmp_sort_key',
32 + 'portage.versions:vercmp',
33 +)
34 +
35 +_PackageConflict = collections.namedtuple("_PackageConflict", ["root", "pkgs", "atom", "description"])
36 +
37 +class PackageConflict(_PackageConflict):
38 + """
39 + Class to track the reason for a conflict and the conflicting packages.
40 + """
41 + def __iter__(self):
42 + return iter(self.pkgs)
43 +
44 + def __contains__(self, pkg):
45 + return pkg in self.pkgs
46 +
47 + def __len__(self):
48 + return len(self.pkgs)
49 +
50 +
51 +class PackageTracker(object):
52 + """
53 + This class tracks packages which are currently
54 + installed and packages which have been pulled into
55 + the dependency graph.
56 +
57 + It automatically tracks conflicts between packages.
58 +
59 + Possible conflicts:
60 + 1) Packages that share the same SLOT.
61 + 2) Packages with the same cpv.
62 + Not yet implemented:
63 + 3) Packages that block each other.
64 + """
65 +
66 + def __init__(self):
67 + # Mapping from package keys to set of packages.
68 + self._cp_pkg_map = collections.defaultdict(list)
69 + self._cp_vdb_pkg_map = collections.defaultdict(list)
70 + # List of package keys that may contain conflicts.
71 + # The insetation order must be preserved.
72 + self._multi_pkgs = []
73 +
74 + # Cache for result of conflicts().
75 + self._conflicts_cache = None
76 +
77 + # Records for each pulled package which installed package
78 + # are replaced.
79 + self._replacing = collections.defaultdict(list)
80 + # Records which pulled packages replace this package.
81 + self._replaced_by = collections.defaultdict(list)
82 +
83 + self._match_cache = collections.defaultdict(dict)
84 +
85 + def add_pkg(self, pkg):
86 + """
87 + Add a new package to the tracker. Records conflicts as necessary.
88 + """
89 + cp_key = pkg.root, pkg.cp
90 +
91 + if any(other is pkg for other in self._cp_pkg_map[cp_key]):
92 + return
93 +
94 + self._cp_pkg_map[cp_key].append(pkg)
95 +
96 + if len(self._cp_pkg_map[cp_key]) > 1:
97 + self._conflicts_cache = None
98 + if len(self._cp_pkg_map[cp_key]) == 2:
99 + self._multi_pkgs.append(cp_key)
100 +
101 + self._replacing[pkg] = []
102 + for installed in self._cp_vdb_pkg_map.get(cp_key, []):
103 + if installed.slot_atom == pkg.slot_atom or \
104 + installed.cpv == pkg.cpv:
105 + self._replacing[pkg].append(installed)
106 + self._replaced_by[installed].append(pkg)
107 +
108 + self._match_cache.pop(cp_key, None)
109 +
110 + def add_installed_pkg(self, installed):
111 + """
112 + Add an installed package during vdb load. These packages
113 + are not returned by matched_pull as long as add_pkg hasn't
114 + been called with them. They are only returned by match_final.
115 + """
116 + cp_key = installed.root, installed.cp
117 + if any(other is installed for other in self._cp_vdb_pkg_map[cp_key]):
118 + return
119 +
120 + self._cp_vdb_pkg_map[cp_key].append(installed)
121 +
122 + for pkg in self._cp_pkg_map.get(cp_key, []):
123 + if installed.slot_atom == pkg.slot_atom or \
124 + installed.cpv == pkg.cpv:
125 + self._replacing[pkg].append(installed)
126 + self._replaced_by[installed].append(pkg)
127 +
128 + self._match_cache.pop(cp_key, None)
129 +
130 + def remove_pkg(self, pkg):
131 + """
132 + Removes the package from the tracker.
133 + Raises KeyError if it isn't present.
134 + """
135 + cp_key = pkg.root, pkg.cp
136 + try:
137 + self._cp_pkg_map.get(cp_key, []).remove(pkg)
138 + except ValueError:
139 + raise KeyError(pkg)
140 +
141 + if self._cp_pkg_map[cp_key]:
142 + self._conflicts_cache = None
143 +
144 + if not self._cp_pkg_map[cp_key]:
145 + del self._cp_pkg_map[cp_key]
146 + elif len(self._cp_pkg_map[cp_key]) == 1:
147 + self._multi_pkgs = [other_cp_key for other_cp_key in self._multi_pkgs \
148 + if other_cp_key != cp_key]
149 +
150 + for installed in self._replacing[pkg]:
151 + self._replaced_by[installed].remove(pkg)
152 + if not self._replaced_by[installed]:
153 + del self._replaced_by[installed]
154 + del self._replacing[pkg]
155 +
156 + self._match_cache.pop(cp_key, None)
157 +
158 + def discard_pkg(self, pkg):
159 + """
160 + Removes the package from the tracker.
161 + Does not raises KeyError if it is not present.
162 + """
163 + try:
164 + self.remove_pkg(pkg)
165 + except KeyError:
166 + pass
167 +
168 + def match(self, root, atom, installed=True):
169 + """
170 + Iterates over the packages matching 'atom'.
171 + If 'installed' is True, installed non-replaced
172 + packages may also be returned.
173 + """
174 + cp_key = root, atom.cp
175 + cache_key = root, atom, installed
176 + try:
177 + return iter(self._match_cache.get(cp_key, {})[cache_key])
178 + except KeyError:
179 + pass
180 +
181 + candidates = self._cp_pkg_map.get(cp_key, [])[:]
182 +
183 + if installed:
184 + for installed in self._cp_vdb_pkg_map.get(cp_key, []):
185 + if installed not in self._replaced_by:
186 + candidates.append(installed)
187 +
188 + ret = match_from_list(atom, candidates)
189 + ret.sort(key=cmp_sort_key(lambda x, y: vercmp(x.version, y.version)))
190 + self._match_cache[cp_key][cache_key] = ret
191 +
192 + return iter(ret)
193 +
194 + def conflicts(self):
195 + """
196 + Iterates over the curently existing conflicts.
197 + """
198 + if self._conflicts_cache is None:
199 + self._conflicts_cache = []
200 +
201 + for cp_key in self._multi_pkgs:
202 +
203 + # Categorize packages according to cpv and slot.
204 + slot_map = collections.defaultdict(list)
205 + cpv_map = collections.defaultdict(list)
206 + for pkg in self._cp_pkg_map[cp_key]:
207 + slot_key = pkg.root, pkg.slot_atom
208 + cpv_key = pkg.root, pkg.cpv
209 + slot_map[slot_key].append(pkg)
210 + cpv_map[cpv_key].append(pkg)
211 +
212 + # Slot conflicts.
213 + for slot_key in slot_map:
214 + slot_pkgs = slot_map[slot_key]
215 + if len(slot_pkgs) > 1:
216 + self._conflicts_cache.append(PackageConflict(
217 + description = "slot conflict",
218 + root = slot_key[0],
219 + atom = slot_key[1],
220 + pkgs = tuple(slot_pkgs),
221 + ))
222 +
223 + # CPV conflicts.
224 + for cpv_key in cpv_map:
225 + cpv_pkgs = cpv_map[cpv_key]
226 + if len(cpv_pkgs) > 1:
227 + # Make sure this cpv conflict is not a slot conflict at the same time.
228 + # Ignore it if it is.
229 + slots = set(pkg.slot for pkg in cpv_pkgs)
230 + if len(slots) > 1:
231 + self._conflicts_cache.append(PackageConflict(
232 + description = "cpv conflict",
233 + root = cpv_key[0],
234 + atom = cpv_key[1],
235 + pkgs = tuple(cpv_pkgs),
236 + ))
237 +
238 + return iter(self._conflicts_cache)
239 +
240 + def slot_conflicts(self):
241 + """
242 + Iterates over present slot conflicts.
243 + This is only intended for consumers that haven't been
244 + updated to deal with other kinds of conflicts.
245 + This funcion should be removed once all consumers are updated.
246 + """
247 + return (conflict for conflict in self.conflicts() \
248 + if conflict.description == "slot conflict")
249 +
250 + def all_pkgs(self, root):
251 + """
252 + Iterates over all packages for the given root
253 + present in the tracker, including the installed
254 + packages.
255 + """
256 + for cp_key in self._cp_pkg_map:
257 + if cp_key[0] == root:
258 + for pkg in self._cp_pkg_map[cp_key]:
259 + yield pkg
260 +
261 + for cp_key in self._cp_vdb_pkg_map:
262 + if cp_key[0] == root:
263 + for installed in self._cp_vdb_pkg_map[cp_key]:
264 + if installed not in self._replaced_by:
265 + yield installed
266 +
267 + def contains(self, pkg, installed=True):
268 + """
269 + Checks if the package is in the tracker.
270 + If 'installed' is True, returns True for
271 + non-replaced installed packages.
272 + """
273 + cp_key = pkg.root, pkg.cp
274 + for other in self._cp_pkg_map.get(cp_key, []):
275 + if other is pkg:
276 + return True
277 +
278 + if installed:
279 + for installed in self._cp_vdb_pkg_map.get(cp_key, []):
280 + if installed is pkg and \
281 + installed not in self._replaced_by:
282 + return True
283 +
284 + return False
285 +
286 + def __contains__(self, pkg):
287 + """
288 + Checks if the package is in the tracker.
289 + Returns True for non-replaced installed packages.
290 + """
291 + return self.contains(pkg, installed=True)
292 +
293 +
294 +class PackageTrackerDbapiWrapper(object):
295 + """
296 + A wrpper class that provides parts of the legacy
297 + dbapi interface. Remove it once all consumers have
298 + died.
299 + """
300 + def __init__(self, root, package_tracker):
301 + self._root = root
302 + self._package_tracker = package_tracker
303 +
304 + def cpv_inject(self, pkg):
305 + self._package_tracker.add_pkg(pkg)
306 +
307 + def match_pkgs(self, atom):
308 + if not isinstance(atom, Atom):
309 + atom = Atom(atom)
310 + ret = sorted(self._package_tracker.match(self._root, atom),
311 + key=cmp_sort_key(lambda x, y: vercmp(x.version, y.version)))
312 + return ret
313 +
314 + def __iter__(self):
315 + return self._package_tracker.all_pkgs(self._root)
316 +
317 + def match(self, atom, use_cache=None):
318 + return self.match_pkgs(atom)
319 +
320 + def cp_list(self, cp):
321 + return self.match_pkgs(cp)
322
323 diff --git a/pym/portage/tests/resolver/test_package_tracker.py b/pym/portage/tests/resolver/test_package_tracker.py
324 new file mode 100644
325 index 0000000..8fa3513
326 --- /dev/null
327 +++ b/pym/portage/tests/resolver/test_package_tracker.py
328 @@ -0,0 +1,261 @@
329 +# Copyright 2014 Gentoo Foundation
330 +# Distributed under the terms of the GNU General Public License v2
331 +
332 +import collections
333 +
334 +from portage.dep import Atom
335 +from portage.tests import TestCase
336 +from _emerge.resolver.package_tracker import PackageTracker, PackageTrackerDbapiWrapper
337 +
338 +class PackageTrackerTestCase(TestCase):
339 +
340 + FakePackage = collections.namedtuple("FakePackage",
341 + ["root", "cp", "cpv", "slot", "slot_atom", "version", "repo"])
342 +
343 + FakeConflict = collections.namedtuple("FakeConflict",
344 + ["description", "root", "pkgs"])
345 +
346 + def make_pkg(self, root, atom, repo="test_repo"):
347 + atom = Atom(atom)
348 + slot_atom = Atom("%s:%s" % (atom.cp, atom.slot))
349 + slot = atom.slot
350 +
351 + return self.FakePackage(root=root, cp=atom.cp, cpv=atom.cpv,
352 + slot=slot, slot_atom=slot_atom, version=atom.version, repo=repo)
353 +
354 + def make_conflict(self, description, root, pkgs):
355 + return self.FakeConflict(description=description, root=root, pkgs=pkgs)
356 +
357 + def test_add_remove_discard(self):
358 + p = PackageTracker()
359 +
360 + x1 = self.make_pkg("/", "=dev-libs/X-1:0")
361 + x2 = self.make_pkg("/", "=dev-libs/X-2:0")
362 +
363 + p.add_pkg(x1)
364 + self.assertTrue(x1 in p)
365 + self.assertTrue(p.contains(x1, installed=True))
366 + self.assertTrue(p.contains(x1, installed=False))
367 + p.remove_pkg(x1)
368 + self.assertTrue(x1 not in p)
369 +
370 + p.add_pkg(x1)
371 + self.assertTrue(x1 in p)
372 + p.add_pkg(x1)
373 + self.assertTrue(x1 in p)
374 +
375 + self.assertRaises(KeyError, p.remove_pkg, x2)
376 +
377 + p.add_pkg(x2)
378 + self.assertTrue(x2 in p)
379 + p.remove_pkg(x2)
380 + self.assertTrue(x2 not in p)
381 + p.discard_pkg(x2)
382 + self.assertTrue(x2 not in p)
383 + p.add_pkg(x2)
384 + self.assertTrue(x2 in p)
385 +
386 + all_pkgs = list(p.all_pkgs("/"))
387 + self.assertEqual(len(all_pkgs), 2)
388 + self.assertTrue(all_pkgs[0] is x1 and all_pkgs[1] is x2)
389 +
390 + self.assertEqual(len(list(p.all_pkgs("/"))), 2)
391 + self.assertEqual(len(list(p.all_pkgs("/xxx"))), 0)
392 +
393 + def test_match(self):
394 + p = PackageTracker()
395 + x1 = self.make_pkg("/", "=dev-libs/X-1:0")
396 + x2 = self.make_pkg("/", "=dev-libs/X-2:0")
397 + x3 = self.make_pkg("/", "=dev-libs/X-3:1")
398 +
399 + p.add_pkg(x2)
400 + p.add_pkg(x1)
401 +
402 + matches = list(p.match("/", Atom("=dev-libs/X-1")))
403 + self.assertTrue(x1 in matches)
404 + self.assertEqual(len(matches), 1)
405 +
406 + matches = list(p.match("/", Atom("dev-libs/X")))
407 + self.assertTrue(x1 is matches[0] and x2 is matches[1])
408 + self.assertEqual(len(matches), 2)
409 +
410 + matches = list(p.match("/xxx", Atom("dev-libs/X")))
411 + self.assertEqual(len(matches), 0)
412 +
413 + matches = list(p.match("/", Atom("dev-libs/Y")))
414 + self.assertEqual(len(matches), 0)
415 +
416 + p.add_pkg(x3)
417 + matches = list(p.match("/", Atom("dev-libs/X")))
418 + self.assertTrue(x1 is matches[0] and x2 is matches[1] and x3 is matches[2])
419 + self.assertEqual(len(matches), 3)
420 +
421 + p.remove_pkg(x3)
422 + matches = list(p.match("/", Atom("dev-libs/X")))
423 + self.assertTrue(x1 is matches[0] and x2 is matches[1])
424 + self.assertEqual(len(matches), 2)
425 +
426 + def test_dbapi_interface(self):
427 + p = PackageTracker()
428 + dbapi = PackageTrackerDbapiWrapper("/", p)
429 + installed = self.make_pkg("/", "=dev-libs/X-0:0")
430 + x1 = self.make_pkg("/", "=dev-libs/X-1:0")
431 + x2 = self.make_pkg("/", "=dev-libs/X-2:0")
432 + x3 = self.make_pkg("/", "=dev-libs/X-3:0")
433 + x4 = self.make_pkg("/", "=dev-libs/X-4:6")
434 + x5 = self.make_pkg("/xxx", "=dev-libs/X-5:6")
435 +
436 + def check_dbapi(pkgs):
437 + all_pkgs = set(dbapi)
438 + self.assertEqual(len(all_pkgs), len(pkgs))
439 +
440 + x_atom = "dev-libs/X"
441 + y_atom = "dev-libs/Y"
442 + matches = dbapi.cp_list(x_atom)
443 + for pkg in pkgs:
444 + if pkg.root == "/" and pkg.cp == x_atom:
445 + self.assertTrue(pkg in matches)
446 + self.assertTrue(not dbapi.cp_list(y_atom))
447 + matches = dbapi.match(x_atom)
448 + for pkg in pkgs:
449 + if pkg.root == "/" and pkg.cp == x_atom:
450 + self.assertTrue(pkg in matches)
451 + self.assertTrue(not dbapi.match(y_atom))
452 +
453 + check_dbapi([])
454 +
455 + p.add_installed_pkg(installed)
456 + check_dbapi([installed])
457 +
458 + p.add_pkg(x1)
459 + check_dbapi([x1])
460 +
461 + p.remove_pkg(x1)
462 + check_dbapi([installed])
463 +
464 + dbapi.cpv_inject(x1)
465 + check_dbapi([x1])
466 +
467 + dbapi.cpv_inject(x2)
468 + check_dbapi([x1, x2])
469 +
470 + p.remove_pkg(x1)
471 + check_dbapi([x2])
472 +
473 + p.add_pkg(x5)
474 + check_dbapi([x2])
475 +
476 +
477 + def test_installed(self):
478 + p = PackageTracker()
479 + x1 = self.make_pkg("/", "=dev-libs/X-1:0")
480 + x1b = self.make_pkg("/", "=dev-libs/X-1.1:0")
481 + x2 = self.make_pkg("/", "=dev-libs/X-2:0")
482 + x3 = self.make_pkg("/", "=dev-libs/X-3:1")
483 +
484 + def check_installed(x, should_contain, num_pkgs):
485 + self.assertEqual(x in p, should_contain)
486 + self.assertEqual(p.contains(x), should_contain)
487 + self.assertEqual(p.contains(x1, installed=True), should_contain)
488 + self.assertEqual(p.contains(x1, installed=False), False)
489 + self.assertEqual(len(list(p.all_pkgs("/"))), num_pkgs)
490 +
491 + def check_matches(atom, expected):
492 + matches = list(p.match("/", Atom(atom)))
493 + self.assertEqual(len(matches), len(expected))
494 + for x, y in zip(matches, expected):
495 + self.assertTrue(x is y)
496 +
497 + p.add_installed_pkg(x1)
498 + check_installed(x1, True, 1)
499 + check_matches("dev-libs/X", [x1])
500 +
501 + p.add_installed_pkg(x1)
502 + check_installed(x1, True, 1)
503 + check_matches("dev-libs/X", [x1])
504 +
505 + p.add_pkg(x2)
506 + check_installed(x1, False, 1)
507 + check_matches("dev-libs/X", [x2])
508 +
509 + p.add_installed_pkg(x1)
510 + check_installed(x1, False, 1)
511 + check_matches("dev-libs/X", [x2])
512 +
513 + p.add_installed_pkg(x1b)
514 + check_installed(x1, False, 1)
515 + check_installed(x1b, False, 1)
516 + check_matches("dev-libs/X", [x2])
517 +
518 + p.remove_pkg(x2)
519 + check_installed(x1, True, 2)
520 + check_installed(x1b, True, 2)
521 + check_matches("dev-libs/X", [x1, x1b])
522 +
523 + def test_conflicts(self):
524 + p = PackageTracker()
525 + installed1 = self.make_pkg("/", "=dev-libs/X-0:0")
526 + installed2 = self.make_pkg("/", "=dev-libs/X-0.1:0")
527 + x1 = self.make_pkg("/", "=dev-libs/X-1:0")
528 + x2 = self.make_pkg("/", "=dev-libs/X-2:0")
529 + x3 = self.make_pkg("/", "=dev-libs/X-3:0")
530 + x4 = self.make_pkg("/", "=dev-libs/X-4:4")
531 + x4b = self.make_pkg("/", "=dev-libs/X-4:4b::x-repo")
532 +
533 + def check_conflicts(expected, slot_conflicts_only=False):
534 + if slot_conflicts_only:
535 + conflicts = list(p.slot_conflicts())
536 + else:
537 + conflicts = list(p.conflicts())
538 + self.assertEqual(len(conflicts), len(expected))
539 + for got, exp in zip(conflicts, expected):
540 + self.assertEqual(got.description, exp.description)
541 + self.assertEqual(got.root, exp.root)
542 + self.assertEqual(len(got.pkgs), len(exp.pkgs))
543 + self.assertEqual(len(got), len(exp.pkgs))
544 + for x, y in zip(got.pkgs, exp.pkgs):
545 + self.assertTrue(x is y)
546 + for x, y in zip(got, exp.pkgs):
547 + self.assertTrue(x is y)
548 + for x in exp.pkgs:
549 + self.assertTrue(x in got)
550 +
551 + check_conflicts([])
552 + check_conflicts([])
553 +
554 + p.add_installed_pkg(installed1)
555 + p.add_installed_pkg(installed2)
556 + check_conflicts([])
557 +
558 + p.add_pkg(x1)
559 + check_conflicts([])
560 + p.add_pkg(x2)
561 + check_conflicts([self.make_conflict("slot conflict", "/", [x1, x2])])
562 + p.add_pkg(x3)
563 + check_conflicts([self.make_conflict("slot conflict", "/", [x1, x2, x3])])
564 + p.remove_pkg(x3)
565 + check_conflicts([self.make_conflict("slot conflict", "/", [x1, x2])])
566 + p.remove_pkg(x2)
567 + check_conflicts([])
568 + p.add_pkg(x3)
569 + check_conflicts([self.make_conflict("slot conflict", "/", [x1, x3])])
570 + p.add_pkg(x2)
571 + check_conflicts([self.make_conflict("slot conflict", "/", [x1, x3, x2])])
572 +
573 + p.add_pkg(x4)
574 + check_conflicts([self.make_conflict("slot conflict", "/", [x1, x3, x2])])
575 +
576 + p.add_pkg(x4b)
577 + check_conflicts(
578 + [
579 + self.make_conflict("slot conflict", "/", [x1, x3, x2]),
580 + self.make_conflict("cpv conflict", "/", [x4, x4b]),
581 + ]
582 + )
583 +
584 + check_conflicts(
585 + [
586 + self.make_conflict("slot conflict", "/", [x1, x3, x2]),
587 + ],
588 + slot_conflicts_only=True
589 + )