Gentoo Archives: gentoo-commits

From: Arthur Zamarin <arthurzam@g.o>
To: gentoo-commits@l.g.o
Subject: [gentoo-commits] proj/pkgcore/pkgdev:main commit in: data/share/bash-completion/completions/, src/pkgdev/scripts/, tests/scripts/
Date: Wed, 01 Mar 2023 19:23:13
Message-Id: 1677698327.6819d87b2e1a65aa57f959f07b8d226578dda634.arthurzam@gentoo
1 commit: 6819d87b2e1a65aa57f959f07b8d226578dda634
2 Author: Arthur Zamarin <arthurzam <AT> gentoo <DOT> org>
3 AuthorDate: Sun Jan 8 20:17:13 2023 +0000
4 Commit: Arthur Zamarin <arthurzam <AT> gentoo <DOT> org>
5 CommitDate: Wed Mar 1 19:18:47 2023 +0000
6 URL: https://gitweb.gentoo.org/proj/pkgcore/pkgdev.git/commit/?id=6819d87b
7
8 pkgdev bugs: new tool for filing stable bugs
9
10 This new tool isn't complete, and any usage should be manually monitored
11 for failures or incorrect results. This tool will be improved in the
12 future, but for now it's a good start.
13
14 Resolves: https://github.com/pkgcore/pkgdev/issues/113
15 Signed-off-by: Arthur Zamarin <arthurzam <AT> gentoo.org>
16
17 data/share/bash-completion/completions/pkgdev | 22 ++
18 src/pkgdev/scripts/pkgdev_bugs.py | 415 ++++++++++++++++++++++++++
19 tests/scripts/test_pkgdev_bugs.py | 104 +++++++
20 3 files changed, 541 insertions(+)
21
22 diff --git a/data/share/bash-completion/completions/pkgdev b/data/share/bash-completion/completions/pkgdev
23 index 37e7a4b..223a7d9 100644
24 --- a/data/share/bash-completion/completions/pkgdev
25 +++ b/data/share/bash-completion/completions/pkgdev
26 @@ -7,6 +7,7 @@ _pkgdev() {
27 _init_completion || return
28
29 local subcommands="
30 + bugs
31 commit
32 manifest
33 mask
34 @@ -229,6 +230,27 @@ _pkgdev() {
35 ;;
36 esac
37 ;;
38 + bugs)
39 + subcmd_options="
40 + --api-key
41 + --auto-cc-arches
42 + --dot
43 + -s --stablereq
44 + -k --keywording
45 + "
46 +
47 + case "${prev}" in
48 + --api-key | --auto-cc-arches)
49 + COMPREPLY=()
50 + ;;
51 + --dot)
52 + COMPREPLY=($(compgen -f -- "${cur}"))
53 + ;;
54 + *)
55 + COMPREPLY+=($(compgen -W "${subcmd_options}" -- "${cur}"))
56 + ;;
57 + esac
58 + ;;
59 esac
60 }
61 complete -F _pkgdev pkgdev
62
63 diff --git a/src/pkgdev/scripts/pkgdev_bugs.py b/src/pkgdev/scripts/pkgdev_bugs.py
64 new file mode 100644
65 index 0000000..9e8938f
66 --- /dev/null
67 +++ b/src/pkgdev/scripts/pkgdev_bugs.py
68 @@ -0,0 +1,415 @@
69 +"""Automatic bugs filler"""
70 +
71 +import json
72 +import urllib.request as urllib
73 +from collections import defaultdict
74 +from functools import partial
75 +from itertools import chain
76 +
77 +from pkgcheck import const as pkgcheck_const
78 +from pkgcheck.addons import ArchesAddon, init_addon
79 +from pkgcheck.addons.profiles import ProfileAddon
80 +from pkgcheck.checks import visibility
81 +from pkgcheck.scripts import argparse_actions
82 +from pkgcore.ebuild.atom import atom
83 +from pkgcore.ebuild.ebuild_src import package
84 +from pkgcore.ebuild.misc import sort_keywords
85 +from pkgcore.repository import multiplex
86 +from pkgcore.restrictions import boolean, packages, values
87 +from pkgcore.test.misc import FakePkg
88 +from pkgcore.util import commandline
89 +from snakeoil.cli import arghparse
90 +from snakeoil.cli.input import userquery
91 +from snakeoil.formatters import Formatter
92 +
93 +from ..cli import ArgumentParser
94 +from .argparsers import _determine_cwd_repo, cwd_repo_argparser
95 +
96 +bugs = ArgumentParser(
97 + prog="pkgdev bugs", description=__doc__, verbose=False, quiet=False,
98 + parents=(cwd_repo_argparser, )
99 +)
100 +bugs.add_argument(
101 + "--api-key",
102 + metavar="KEY",
103 + help="Bugzilla API key",
104 + docs="""
105 + The Bugzilla API key to use for authentication. Used mainly to overcome
106 + rate limiting done by bugzilla server. This tool doesn't perform any
107 + bug editing, just fetching info for the bug.
108 + """,
109 +)
110 +bugs.add_argument(
111 + "targets", metavar="target", nargs="+",
112 + action=commandline.StoreTarget,
113 + help="extended atom matching of packages",
114 +)
115 +bugs.add_argument(
116 + "--dot",
117 + help="path file where to save the graph in dot format",
118 +)
119 +bugs.add_argument(
120 + "--auto-cc-arches",
121 + action=arghparse.CommaSeparatedNegationsAppend,
122 + default=([], []),
123 + help="automatically add CC-ARCHES for the listed email addresses",
124 + docs="""
125 + Comma separated list of email addresses, for which automatically add
126 + CC-ARCHES if one of the maintainers matches the email address. If the
127 + package is maintainer-needed, always add CC-ARCHES.
128 + """,
129 +)
130 +
131 +bugs.add_argument(
132 + "--cache",
133 + action=argparse_actions.CacheNegations,
134 + help=arghparse.SUPPRESS,
135 +)
136 +bugs.add_argument(
137 + "--cache-dir",
138 + type=arghparse.create_dir,
139 + default=pkgcheck_const.USER_CACHE_DIR,
140 + help=arghparse.SUPPRESS,
141 +)
142 +bugs_state = bugs.add_mutually_exclusive_group()
143 +bugs_state.add_argument(
144 + "-s",
145 + "--stablereq",
146 + dest="keywording",
147 + default=None,
148 + action="store_false",
149 + help="File stable request bugs",
150 +)
151 +bugs_state.add_argument(
152 + "-k",
153 + "--keywording",
154 + dest="keywording",
155 + default=None,
156 + action="store_true",
157 + help="File rekeywording bugs",
158 +)
159 +
160 +ArchesAddon.mangle_argparser(bugs)
161 +ProfileAddon.mangle_argparser(bugs)
162 +
163 +
164 +@××××.bind_delayed_default(1500, "target_repo")
165 +def _validate_args(namespace, attr):
166 + _determine_cwd_repo(bugs, namespace)
167 + setattr(namespace, attr, namespace.repo)
168 + setattr(namespace, "verbosity", 1)
169 + setattr(namespace, "search_repo", multiplex.tree(*namespace.repo.trees))
170 + setattr(namespace, "query_caching_freq", "package")
171 +
172 +
173 +@××××.bind_final_check
174 +def _validate_args(parser, namespace):
175 + if namespace.keywording:
176 + parser.error("keywording is not implemented yet, sorry")
177 +
178 +def _get_suggested_keywords(repo, pkg: package):
179 + match_keywords = {
180 + x
181 + for pkgver in repo.match(pkg.unversioned_atom)
182 + for x in pkgver.keywords
183 + if x[0] not in '-~'
184 + }
185 +
186 + # limit stablereq to whatever is ~arch right now
187 + match_keywords.intersection_update(x.lstrip('~') for x in pkg.keywords if x[0] == '~')
188 +
189 + return frozenset({x for x in match_keywords if '-' not in x})
190 +
191 +
192 +class GraphNode:
193 + __slots__ = ("pkgs", "edges", "bugno")
194 +
195 + def __init__(self, pkgs: tuple[tuple[package, set[str]], ...], bugno=None):
196 + self.pkgs = pkgs
197 + self.edges: set[GraphNode] = set()
198 + self.bugno = bugno
199 +
200 + def __eq__(self, __o: object):
201 + return self is __o
202 +
203 + def __hash__(self):
204 + return hash(id(self))
205 +
206 + def __str__(self):
207 + return ", ".join(str(pkg.versioned_atom) for pkg, _ in self.pkgs)
208 +
209 + def __repr__(self):
210 + return str(self)
211 +
212 + def lines(self):
213 + for pkg, keywords in self.pkgs:
214 + yield f"{pkg.versioned_atom} {' '.join(sort_keywords(keywords))}"
215 +
216 + @property
217 + def dot_edge(self):
218 + return f'"{self.pkgs[0][0].versioned_atom}"'
219 +
220 + def cleanup_keywords(self, repo):
221 + previous = frozenset()
222 + for pkg, keywords in self.pkgs:
223 + if keywords == previous:
224 + keywords.clear()
225 + keywords.add("^")
226 + else:
227 + previous = frozenset(keywords)
228 +
229 + for pkg, keywords in self.pkgs:
230 + suggested = _get_suggested_keywords(repo, pkg)
231 + if keywords == set(suggested):
232 + keywords.clear()
233 + keywords.add("*")
234 +
235 + def file_bug(self, api_key: str, auto_cc_arches: frozenset[str], observer=None) -> int:
236 + if self.bugno is not None:
237 + return self.bugno
238 + for dep in self.edges:
239 + if dep.bugno is None:
240 + dep.file_bug(api_key, auto_cc_arches, observer)
241 + maintainers = dict.fromkeys(
242 + maintainer.email
243 + for pkg, _ in self.pkgs
244 + for maintainer in pkg.maintainers
245 + )
246 + if not maintainers or "*" in auto_cc_arches or auto_cc_arches.intersection(maintainers):
247 + keywords = ["CC-ARCHES"]
248 + else:
249 + keywords = []
250 + maintainers = tuple(maintainers) or ("maintainer-needed@g.o", )
251 +
252 + request_data = dict(
253 + Bugzilla_api_key=api_key,
254 + product="Gentoo Linux",
255 + component="Stabilization",
256 + severity="enhancement",
257 + version="unspecified",
258 + summary=f"{', '.join(pkg.versioned_atom.cpvstr for pkg, _ in self.pkgs)}: stablereq",
259 + description="Please stabilize",
260 + keywords=keywords,
261 + cf_stabilisation_atoms="\n".join(self.lines()),
262 + assigned_to=maintainers[0],
263 + cc=maintainers[1:],
264 + depends_on=list({dep.bugno for dep in self.edges}),
265 + )
266 + request = urllib.Request(
267 + url='https://bugs.gentoo.org/rest/bug',
268 + data=json.dumps(request_data).encode('utf-8'),
269 + method='POST',
270 + headers={
271 + "Content-Type": "application/json",
272 + "Accept": "application/json",
273 + },
274 + )
275 + with urllib.urlopen(request, timeout=30) as response:
276 + reply = json.loads(response.read().decode('utf-8'))
277 + self.bugno = int(reply['id'])
278 + if observer is not None:
279 + observer(self)
280 + return self.bugno
281 +
282 +class DependencyGraph:
283 + def __init__(self, out: Formatter, err: Formatter, options):
284 + self.out = out
285 + self.err = err
286 + self.options = options
287 + self.profile_addon: ProfileAddon = init_addon(ProfileAddon, options)
288 +
289 + self.nodes: set[GraphNode] = set()
290 + self.starting_nodes: set[GraphNode] = set()
291 +
292 + def mk_fake_pkg(self, pkg: package, keywords: set[str]):
293 + return FakePkg(
294 + cpv=pkg.cpvstr,
295 + eapi=str(pkg.eapi),
296 + iuse=pkg.iuse,
297 + repo=pkg.repo,
298 + keywords=tuple(keywords),
299 + data={
300 + attr: str(getattr(pkg, attr.lower()))
301 + for attr in pkg.eapi.dep_keys
302 + },
303 + )
304 +
305 + def find_best_match(self, restrict, pkgset: list[package]) -> package:
306 + restrict = boolean.AndRestriction(restrict, packages.PackageRestriction(
307 + "properties", values.ContainmentMatch("live", negate=True)
308 + ))
309 + # prefer using already selected packages in graph
310 + all_pkgs = (pkg for node in self.nodes for pkg, _ in node.pkgs)
311 + if intersect := tuple(filter(restrict.match, all_pkgs)):
312 + return max(intersect)
313 + matches = sorted(filter(restrict.match, pkgset), reverse=True)
314 + for match in matches:
315 + if not all(keyword.startswith("~") for keyword in match.keywords):
316 + return match
317 + return matches[0]
318 +
319 + def _find_dependencies(self, pkg: package, keywords: set[str]):
320 + check = visibility.VisibilityCheck(self.options, profile_addon=self.profile_addon)
321 +
322 + issues: dict[str, dict[str, set[atom]]] = defaultdict(partial(defaultdict, set))
323 + for res in check.feed(self.mk_fake_pkg(pkg, keywords)):
324 + if isinstance(res, visibility.NonsolvableDeps):
325 + for dep in res.deps:
326 + dep = atom(dep).no_usedeps
327 + issues[dep.key][res.keyword.lstrip('~')].add(dep)
328 +
329 + for pkgname, problems in issues.items():
330 + pkgset: list[package] = self.options.repo.match(atom(pkgname))
331 + try:
332 + combined = boolean.AndRestriction(*set().union(*problems.values()))
333 + match = self.find_best_match(combined, pkgset)
334 + yield match, set(problems.keys())
335 + except ValueError:
336 + results: dict[package, set[str]] = defaultdict(set)
337 + for keyword, deps in problems.items():
338 + match = self.find_best_match(deps, pkgset)
339 + results[match].add(keyword)
340 + yield from results.items()
341 +
342 + def build_full_graph(self, targets: list[package]):
343 + check_nodes = [(pkg, set()) for pkg in targets]
344 +
345 + vertices: dict[package, GraphNode] = {}
346 + edges = []
347 + while len(check_nodes):
348 + pkg, keywords = check_nodes.pop(0)
349 + if pkg in vertices:
350 + vertices[pkg].pkgs[0][1].update(keywords)
351 + continue
352 +
353 + keywords.update(_get_suggested_keywords(self.options.repo, pkg))
354 + assert keywords
355 + self.nodes.add(new_node := GraphNode(((pkg, keywords), )))
356 + vertices[pkg] = new_node
357 + self.out.write(f"Checking {pkg.versioned_atom} on {' '.join(sort_keywords(keywords))!r}")
358 + self.out.flush()
359 +
360 + for dep, keywords in self._find_dependencies(pkg, keywords):
361 + edges.append((pkg, dep))
362 + check_nodes.append((dep, keywords))
363 +
364 + for src, dst in edges:
365 + vertices[src].edges.add(vertices[dst])
366 + self.starting_nodes = {vertices[starting_node] for starting_node in targets}
367 +
368 + def output_dot(self, dot_file):
369 + with open(dot_file, "w") as dot:
370 + dot.write("digraph {\n")
371 + dot.write("\trankdir=LR;\n")
372 + for node in self.nodes:
373 + node_text = "\\n".join(node.lines())
374 + dot.write(f'\t{node.dot_edge}[label="{node_text}"];\n')
375 + for other in node.edges:
376 + dot.write(f"\t{node.dot_edge} -> {other.dot_edge};\n")
377 + dot.write("}\n")
378 + dot.close()
379 +
380 + def merge_nodes(self, nodes: tuple[GraphNode, ...]) -> GraphNode:
381 + self.nodes.difference_update(nodes)
382 + self.starting_nodes.difference_update(nodes)
383 + new_node = GraphNode(list(chain.from_iterable(n.pkgs for n in nodes)))
384 +
385 + for node in nodes:
386 + new_node.edges.update(node.edges.difference(nodes))
387 +
388 + for node in self.nodes:
389 + if node.edges.intersection(nodes):
390 + node.edges.difference_update(nodes)
391 + node.edges.add(new_node)
392 +
393 + self.nodes.add(new_node)
394 + return new_node
395 +
396 + @staticmethod
397 + def _find_cycles(nodes: tuple[GraphNode, ...], stack: list[GraphNode]) -> tuple[GraphNode, ...]:
398 + node = stack[-1]
399 + for edge in node.edges:
400 + if edge in stack:
401 + return tuple(stack[stack.index(edge):])
402 + stack.append(edge)
403 + if cycle := DependencyGraph._find_cycles(nodes, stack):
404 + return cycle
405 + stack.pop()
406 + return ()
407 +
408 + def merge_cycles(self):
409 + new_starts = set()
410 + while self.starting_nodes:
411 + starting_node = self.starting_nodes.pop()
412 + assert starting_node in self.nodes
413 + while cycle := self._find_cycles(tuple(self.nodes), [starting_node]):
414 + print("Found cycle:", " -> ".join(str(n) for n in cycle))
415 + new_node = self.merge_nodes(cycle)
416 + if starting_node not in self.nodes:
417 + starting_node = new_node
418 + new_starts.add(starting_node)
419 + self.starting_nodes.update(new_starts)
420 +
421 + def merge_new_keywords_children(self):
422 + repo = self.options.search_repo
423 + found_someone = True
424 + while found_someone:
425 + reverse_edges: dict[GraphNode, set[GraphNode]] = defaultdict(set)
426 + for node in self.nodes:
427 + for dep in node.edges:
428 + reverse_edges[dep].add(node)
429 + found_someone = False
430 + for node, origs in reverse_edges.items():
431 + if len(origs) != 1:
432 + continue
433 + existing_keywords = frozenset().union(*(
434 + pkgver.keywords
435 + for pkg in node.pkgs
436 + for pkgver in repo.match(pkg[0].unversioned_atom)
437 + ))
438 + if existing_keywords & frozenset().union(*(pkg[1] for pkg in node.pkgs)):
439 + continue # not fully new keywords
440 + orig = next(iter(origs))
441 + print(f"Merging {node} into {orig}")
442 + self.merge_nodes((orig, node))
443 + found_someone = True
444 + break
445 +
446 + def file_bugs(self, api_key: str, auto_cc_arches: frozenset[str]):
447 + def observe(node: GraphNode):
448 + self.out.write(
449 + f"https://bugs.gentoo.org/{node.bugno} ",
450 + " | ".join(node.lines()),
451 + " depends on bugs ", {dep.bugno for dep in node.edges}
452 + )
453 + self.out.flush()
454 +
455 + for node in self.starting_nodes:
456 + node.file_bug(api_key, auto_cc_arches, observe)
457 +
458 +
459 +@××××.bind_main_func
460 +def main(options, out: Formatter, err: Formatter):
461 + search_repo = options.search_repo
462 + targets = [max(search_repo.itermatch(target)) for _, target in options.targets]
463 + d = DependencyGraph(out, err, options)
464 + d.build_full_graph(targets)
465 + d.merge_cycles()
466 + d.merge_new_keywords_children()
467 +
468 + for node in d.nodes:
469 + node.cleanup_keywords(search_repo)
470 +
471 + if options.dot is not None:
472 + d.output_dot(options.dot)
473 + out.write(out.fg("green"), f"Dot file written to {options.dot}", out.reset)
474 +
475 + if not userquery(f'Continue and create {len(d.nodes)} stablereq bugs?', out, err, default_answer=False):
476 + return 1
477 +
478 + if options.api_key is None:
479 + err.write(out.fg("red"), "No API key provided, exiting", out.reset)
480 + return 1
481 +
482 + disabled, enabled = options.auto_cc_arches
483 + d.file_bugs(options.api_key, frozenset(enabled).difference(disabled))
484
485 diff --git a/tests/scripts/test_pkgdev_bugs.py b/tests/scripts/test_pkgdev_bugs.py
486 new file mode 100644
487 index 0000000..f23051e
488 --- /dev/null
489 +++ b/tests/scripts/test_pkgdev_bugs.py
490 @@ -0,0 +1,104 @@
491 +import itertools
492 +import os
493 +import sys
494 +import json
495 +import textwrap
496 +from types import SimpleNamespace
497 +from unittest.mock import patch
498 +
499 +import pytest
500 +from pkgcore.ebuild.atom import atom
501 +from pkgcore.test.misc import FakePkg
502 +from pkgdev.scripts import pkgdev_bugs as bugs
503 +from snakeoil.formatters import PlainTextFormatter
504 +from snakeoil.osutils import pjoin
505 +
506 +def mk_pkg(repo, cpvstr, maintainers, **kwargs):
507 + kwargs.setdefault("KEYWORDS", ["~amd64"])
508 + pkgdir = os.path.dirname(repo.create_ebuild(cpvstr, **kwargs))
509 + # stub metadata
510 + with open(pjoin(pkgdir, 'metadata.xml'), 'w') as f:
511 + f.write(textwrap.dedent(f"""\
512 + <?xml version="1.0" encoding="UTF-8"?>
513 + <!DOCTYPE pkgmetadata SYSTEM "https://www.gentoo.org/dtd/metadata.dtd">
514 + <pkgmetadata>
515 + <maintainer type="person">
516 + {' '.join(f'<email>{maintainer}@gentoo.org</email>' for maintainer in maintainers)}
517 + </maintainer>
518 + </pkgmetadata>
519 + """))
520 +
521 +
522 +def mk_repo(repo):
523 + mk_pkg(repo, 'cat/u-0', ['dev1'])
524 + mk_pkg(repo, 'cat/z-0', [], RDEPEND=['cat/u', 'cat/x'])
525 + mk_pkg(repo, 'cat/v-0', ['dev2'], RDEPEND='cat/x')
526 + mk_pkg(repo, 'cat/y-0', ['dev1'], RDEPEND=['cat/z', 'cat/v'])
527 + mk_pkg(repo, 'cat/x-0', ['dev3'], RDEPEND='cat/y')
528 + mk_pkg(repo, 'cat/w-0', ['dev3'], RDEPEND='cat/x')
529 +
530 +
531 +class BugsSession:
532 + def __init__(self):
533 + self.counter = iter(itertools.count(1))
534 + self.calls = []
535 +
536 + def __enter__(self):
537 + return self
538 +
539 + def __exit__(self, *_args):
540 + ...
541 +
542 + def read(self):
543 + return json.dumps({'id': next(self.counter)}).encode('utf-8')
544 +
545 + def __call__(self, request, *_args, **_kwargs):
546 + self.calls.append(json.loads(request.data))
547 + return self
548 +
549 +
550 +class TestBugFiling:
551 + def test_bug_filing(self, repo):
552 + mk_repo(repo)
553 + session = BugsSession()
554 + pkg = max(repo.itermatch(atom('=cat/u-0')))
555 + with patch('pkgdev.scripts.pkgdev_bugs.urllib.urlopen', session):
556 + bugs.GraphNode(((pkg, {'*'}), )).file_bug("API", frozenset())
557 + assert len(session.calls) == 1
558 + call = session.calls[0]
559 + assert call['Bugzilla_api_key'] == 'API'
560 + assert call['summary'] == 'cat/u-0: stablereq'
561 + assert call['assigned_to'] == 'dev1@g.o'
562 + assert not call['cc']
563 + assert call['cf_stabilisation_atoms'] == '=cat/u-0 *'
564 + assert not call['depends_on']
565 +
566 + def test_bug_filing_maintainer_needed(self, repo):
567 + mk_repo(repo)
568 + session = BugsSession()
569 + pkg = max(repo.itermatch(atom('=cat/z-0')))
570 + with patch('pkgdev.scripts.pkgdev_bugs.urllib.urlopen', session):
571 + bugs.GraphNode(((pkg, {'*'}), )).file_bug("API", frozenset())
572 + assert len(session.calls) == 1
573 + call = session.calls[0]
574 + assert call['assigned_to'] == 'maintainer-needed@g.o'
575 + assert not call['cc']
576 +
577 + def test_bug_filing_multiple_pkgs(self, repo):
578 + mk_repo(repo)
579 + session = BugsSession()
580 + pkgX = max(repo.itermatch(atom('=cat/x-0')))
581 + pkgY = max(repo.itermatch(atom('=cat/y-0')))
582 + pkgZ = max(repo.itermatch(atom('=cat/z-0')))
583 + dep = bugs.GraphNode((), 2)
584 + node = bugs.GraphNode(((pkgX, {'*'}), (pkgY, {'*'}), (pkgZ, {'*'})))
585 + node.edges.add(dep)
586 + with patch('pkgdev.scripts.pkgdev_bugs.urllib.urlopen', session):
587 + node.file_bug("API", frozenset())
588 + assert len(session.calls) == 1
589 + call = session.calls[0]
590 + assert call['summary'] == 'cat/x-0, cat/y-0, cat/z-0: stablereq'
591 + assert call['assigned_to'] == 'dev3@g.o'
592 + assert call['cc'] == ['dev1@g.o']
593 + assert call['cf_stabilisation_atoms'] == '=cat/x-0 *\n=cat/y-0 *\n=cat/z-0 *'
594 + assert call['depends_on'] == [2]