from contextlib import contextmanager import platform import sys from tempfile import NamedTemporaryFile try: from unittest import mock except ImportError: import mock import pytest import virtualenv import pipdeptree as p # Tests for DAG classes def mock_pkgs(simple_graph): for node, children in simple_graph.items(): nk, nv = node p = mock.Mock(key=nk, project_name=nk, version=nv) as_req = mock.Mock(key=nk, project_name=nk, specs=[('==', nv)]) p.as_requirement = mock.Mock(return_value=as_req) reqs = [] for child in children: ck, cv = child r = mock.Mock(key=ck, project_name=ck, specs=cv) reqs.append(r) p.requires = mock.Mock(return_value=reqs) yield p def mock_PackageDAG(simple_graph): pkgs = list(mock_pkgs(simple_graph)) return p.PackageDAG.from_pkgs(pkgs) # util for comparing tree contents with a simple graph def dag_to_dict(g): return {k.key: [v.key for v in vs] for k, vs in g._obj.items()} def sort_map_values(m): return {k: sorted(v) for k, v in m.items()} t = mock_PackageDAG({ ('a', '3.4.0'): [('b', [('>=', '2.0.0')]), ('c', [('>=', '5.7.1')])], ('b', '2.3.1'): [('d', [('>=', '2.30'), ('<', '2.42')])], ('c', '5.10.0'): [('d', [('>=', '2.30')]), ('e', [('>=', '0.12.1')])], ('d', '2.35'): [('e', [('>=', '0.9.0')])], ('e', '0.12.1'): [], ('f', '3.1'): [('b', [('>=', '2.1.0')])], ('g', '6.8.3rc1'): [('e', [('>=', '0.9.0')]), ('f', [('>=', '3.0.0')])] }) def test_PackageDAG__get_node_as_parent(): assert 'b' == t.get_node_as_parent('b').key assert 'c' == t.get_node_as_parent('c').key def test_PackageDAG_filter(): # When both show_only and exclude are not specified, same tree # object is returned assert t.filter(None, None) is t # when show_only is specified g1 = dag_to_dict(t.filter(set(['a', 'd']), None)) expected = {'a': ['b', 'c'], 'b': ['d'], 'c': ['d', 'e'], 'd': ['e'], 'e': []} assert expected == g1 # when exclude is specified g2 = dag_to_dict(t.filter(None, ['d'])) expected = {'a': ['b', 'c'], 'b': [], 'c': ['e'], 'e': [], 'f': ['b'], 'g': ['e', 'f']} assert expected == g2 # when both show_only and exclude are specified g3 = dag_to_dict(t.filter(set(['a', 'g']), set(['d', 'e']))) expected = {'a': ['b', 'c'], 'b': [], 'c': [], 'f': ['b'], 'g': ['f']} assert expected == g3 # when conflicting values in show_only and exclude, AssertionError # is raised with pytest.raises(AssertionError): dag_to_dict(t.filter(set(['d']), set(['D', 'e']))) def test_PackageDAG_reverse(): t1 = t.reverse() expected = {'a': [], 'b': ['a', 'f'], 'c': ['a'], 'd': ['b', 'c'], 'e': ['c', 'd', 'g'], 'f': ['g'], 'g': []} assert isinstance(t1, p.ReversedPackageDAG) assert sort_map_values(expected) == sort_map_values(dag_to_dict(t1)) assert all([isinstance(k, p.ReqPackage) for k in t1.keys()]) assert all([isinstance(v, p.DistPackage) for v in p.flatten(t1.values())]) # testing reversal of ReversedPackageDAG instance expected = {'a': ['b', 'c'], 'b': ['d'], 'c': ['d', 'e'], 'd': ['e'], 'e': [], 'f': ['b'], 'g': ['e', 'f']} t2 = t1.reverse() assert isinstance(t2, p.PackageDAG) assert sort_map_values(expected) == sort_map_values(dag_to_dict(t2)) assert all([isinstance(k, p.DistPackage) for k in t2.keys()]) assert all([isinstance(v, p.ReqPackage) for v in p.flatten(t2.values())]) # Tests for Package classes # # Note: For all render methods, we are only testing for frozen=False # as mocks with frozen=True are a lot more complicated def test_DistPackage__render_as_root(): foo = mock.Mock(key='foo', project_name='foo', version='20.4.1') dp = p.DistPackage(foo) is_frozen = False assert 'foo==20.4.1' == dp.render_as_root(is_frozen) def test_DistPackage__render_as_branch(): foo = mock.Mock(key='foo', project_name='foo', version='20.4.1') bar = mock.Mock(key='bar', project_name='bar', version='4.1.0') bar_req = mock.Mock(key='bar', project_name='bar', version='4.1.0', specs=[('>=', '4.0')]) rp = p.ReqPackage(bar_req, dist=bar) dp = p.DistPackage(foo).as_parent_of(rp) is_frozen = False assert 'foo==20.4.1 [requires: bar>=4.0]' == dp.render_as_branch(is_frozen) def test_DistPackage__as_parent_of(): foo = mock.Mock(key='foo', project_name='foo', version='20.4.1') dp = p.DistPackage(foo) assert dp.req is None bar = mock.Mock(key='bar', project_name='bar', version='4.1.0') bar_req = mock.Mock(key='bar', project_name='bar', version='4.1.0', specs=[('>=', '4.0')]) rp = p.ReqPackage(bar_req, dist=bar) dp1 = dp.as_parent_of(rp) assert dp1._obj == dp._obj assert dp1.req is rp dp2 = dp.as_parent_of(None) assert dp2 is dp def test_DistPackage__as_dict(): foo = mock.Mock(key='foo', project_name='foo', version='1.3.2b1') dp = p.DistPackage(foo) result = dp.as_dict() expected = {'key': 'foo', 'package_name': 'foo', 'installed_version': '1.3.2b1'} assert expected == result def test_ReqPackage__render_as_root(): bar = mock.Mock(key='bar', project_name='bar', version='4.1.0') bar_req = mock.Mock(key='bar', project_name='bar', version='4.1.0', specs=[('>=', '4.0')]) rp = p.ReqPackage(bar_req, dist=bar) is_frozen = False assert 'bar==4.1.0' == rp.render_as_root(is_frozen) def test_ReqPackage__render_as_branch(): bar = mock.Mock(key='bar', project_name='bar', version='4.1.0') bar_req = mock.Mock(key='bar', project_name='bar', version='4.1.0', specs=[('>=', '4.0')]) rp = p.ReqPackage(bar_req, dist=bar) is_frozen = False assert 'bar [required: >=4.0, installed: 4.1.0]' == rp.render_as_branch(is_frozen) def test_ReqPackage__as_dict(): bar = mock.Mock(key='bar', project_name='bar', version='4.1.0') bar_req = mock.Mock(key='bar', project_name='bar', version='4.1.0', specs=[('>=', '4.0')]) rp = p.ReqPackage(bar_req, dist=bar) result = rp.as_dict() expected = {'key': 'bar', 'package_name': 'bar', 'installed_version': '4.1.0', 'required_version': '>=4.0'} assert expected == result # Tests for render_text # # @NOTE: These tests use mocked tree and it's not easy to test for # frozen=True with mocks. Hence those tests are covered only in # end-to-end tests. Check the ./e2e-tests script. @pytest.mark.parametrize( "list_all,reverse,expected_output", [ ( True, False, [ 'a==3.4.0', ' - b [required: >=2.0.0, installed: 2.3.1]', ' - d [required: >=2.30,<2.42, installed: 2.35]', ' - e [required: >=0.9.0, installed: 0.12.1]', ' - c [required: >=5.7.1, installed: 5.10.0]', ' - d [required: >=2.30, installed: 2.35]', ' - e [required: >=0.9.0, installed: 0.12.1]', ' - e [required: >=0.12.1, installed: 0.12.1]', 'b==2.3.1', ' - d [required: >=2.30,<2.42, installed: 2.35]', ' - e [required: >=0.9.0, installed: 0.12.1]', 'c==5.10.0', ' - d [required: >=2.30, installed: 2.35]', ' - e [required: >=0.9.0, installed: 0.12.1]', ' - e [required: >=0.12.1, installed: 0.12.1]', 'd==2.35', ' - e [required: >=0.9.0, installed: 0.12.1]', 'e==0.12.1', 'f==3.1', ' - b [required: >=2.1.0, installed: 2.3.1]', ' - d [required: >=2.30,<2.42, installed: 2.35]', ' - e [required: >=0.9.0, installed: 0.12.1]', 'g==6.8.3rc1', ' - e [required: >=0.9.0, installed: 0.12.1]', ' - f [required: >=3.0.0, installed: 3.1]', ' - b [required: >=2.1.0, installed: 2.3.1]', ' - d [required: >=2.30,<2.42, installed: 2.35]', ' - e [required: >=0.9.0, installed: 0.12.1]' ] ), ( True, True, [ 'a==3.4.0', 'b==2.3.1', ' - a==3.4.0 [requires: b>=2.0.0]', ' - f==3.1 [requires: b>=2.1.0]', ' - g==6.8.3rc1 [requires: f>=3.0.0]', 'c==5.10.0', ' - a==3.4.0 [requires: c>=5.7.1]', 'd==2.35', ' - b==2.3.1 [requires: d>=2.30,<2.42]', ' - a==3.4.0 [requires: b>=2.0.0]', ' - f==3.1 [requires: b>=2.1.0]', ' - g==6.8.3rc1 [requires: f>=3.0.0]', ' - c==5.10.0 [requires: d>=2.30]', ' - a==3.4.0 [requires: c>=5.7.1]', 'e==0.12.1', ' - c==5.10.0 [requires: e>=0.12.1]', ' - a==3.4.0 [requires: c>=5.7.1]', ' - d==2.35 [requires: e>=0.9.0]', ' - b==2.3.1 [requires: d>=2.30,<2.42]', ' - a==3.4.0 [requires: b>=2.0.0]', ' - f==3.1 [requires: b>=2.1.0]', ' - g==6.8.3rc1 [requires: f>=3.0.0]', ' - c==5.10.0 [requires: d>=2.30]', ' - a==3.4.0 [requires: c>=5.7.1]', ' - g==6.8.3rc1 [requires: e>=0.9.0]', 'f==3.1', ' - g==6.8.3rc1 [requires: f>=3.0.0]', 'g==6.8.3rc1' ] ), ( False, False, [ 'a==3.4.0', ' - b [required: >=2.0.0, installed: 2.3.1]', ' - d [required: >=2.30,<2.42, installed: 2.35]', ' - e [required: >=0.9.0, installed: 0.12.1]', ' - c [required: >=5.7.1, installed: 5.10.0]', ' - d [required: >=2.30, installed: 2.35]', ' - e [required: >=0.9.0, installed: 0.12.1]', ' - e [required: >=0.12.1, installed: 0.12.1]', 'g==6.8.3rc1', ' - e [required: >=0.9.0, installed: 0.12.1]', ' - f [required: >=3.0.0, installed: 3.1]', ' - b [required: >=2.1.0, installed: 2.3.1]', ' - d [required: >=2.30,<2.42, installed: 2.35]', ' - e [required: >=0.9.0, installed: 0.12.1]', ] ), ( False, True, [ 'e==0.12.1', ' - c==5.10.0 [requires: e>=0.12.1]', ' - a==3.4.0 [requires: c>=5.7.1]', ' - d==2.35 [requires: e>=0.9.0]', ' - b==2.3.1 [requires: d>=2.30,<2.42]', ' - a==3.4.0 [requires: b>=2.0.0]', ' - f==3.1 [requires: b>=2.1.0]', ' - g==6.8.3rc1 [requires: f>=3.0.0]', ' - c==5.10.0 [requires: d>=2.30]', ' - a==3.4.0 [requires: c>=5.7.1]', ' - g==6.8.3rc1 [requires: e>=0.9.0]', ] ) ] ) def test_render_text(capsys, list_all, reverse, expected_output): tree = t.reverse() if reverse else t p.render_text(tree, list_all=list_all, frozen=False) captured = capsys.readouterr() assert '\n'.join(expected_output).strip() == captured.out.strip() # Tests for graph outputs def test_render_pdf(): output = p.dump_graphviz(t, output_format='pdf') @contextmanager def redirect_stdout(new_target): old_target, sys.stdout = sys.stdout, new_target try: yield new_target finally: sys.stdout = old_target with NamedTemporaryFile(delete=True) as f: with redirect_stdout(f): p.print_graphviz(output) rf = open(f.name, 'rb') assert b'%PDF' == rf.read()[:4] # @NOTE: rf is not closed to avoid "bad filedescriptor" error def test_render_svg(capsys): output = p.dump_graphviz(t, output_format='svg') p.print_graphviz(output) out, _ = capsys.readouterr() assert out.startswith('') # Test for conflicting deps @pytest.mark.parametrize( "mpkgs,expected_keys,expected_output", [ ( { ('a', '1.0.1'): [('b', [('>=', '2.3.0')])], ('b', '1.9.1'): [] }, {'a': ['b']}, [ 'Warning!!! Possibly conflicting dependencies found:', '* a==1.0.1', ' - b [required: >=2.3.0, installed: 1.9.1]' ] ), ( { ('a', '1.0.1'): [('c', [('>=', '9.4.1')])], ('b', '2.3.0'): [('c', [('>=', '7.0')])], ('c', '8.0.1'): [] }, {'a': ['c']}, [ 'Warning!!! Possibly conflicting dependencies found:', '* a==1.0.1', ' - c [required: >=9.4.1, installed: 8.0.1]' ] ), ( { ('a', '1.0.1'): [('c', [('>=', '9.4.1')])], ('b', '2.3.0'): [('c', [('>=', '9.4.0')])] }, {'a': ['c'], 'b': ['c']}, [ 'Warning!!! Possibly conflicting dependencies found:', '* a==1.0.1', ' - c [required: >=9.4.1, installed: ?]', '* b==2.3.0', ' - c [required: >=9.4.0, installed: ?]' ] ), ( { ('a', '1.0.1'): [('c', [('>=', '9.4.1')])], ('b', '2.3.0'): [('c', [('>=', '7.0')])], ('c', '9.4.1'): [] }, {}, [] ) ] ) def test_conflicting_deps(capsys, mpkgs, expected_keys, expected_output): tree = mock_PackageDAG(mpkgs) result = p.conflicting_deps(tree) result_keys = {k.key: [v.key for v in vs] for k, vs in result.items()} assert expected_keys == result_keys p.render_conflicts_text(result) captured = capsys.readouterr() assert '\n'.join(expected_output).strip() == captured.err.strip() # Tests for cyclic deps @pytest.mark.parametrize( "mpkgs,expected_keys,expected_output", [ ( { ('a', '1.0.1'): [('b', [('>=', '2.0.0')])], ('b', '2.3.0'): [('a', [('>=', '1.0.1')])], ('c', '4.5.0'): [('d', [('==', '2.0')])], ('d', '2.0'): [] }, [('a', 'b', 'a'), ('b', 'a', 'b')], [ 'Warning!! Cyclic dependencies found:', '* b => a => b', '* a => b => a' ] ), ( # if a dependency isn't installed, cannot verify cycles { ('a', '1.0.1'): [('b', [('>=', '2.0.0')])], }, [], [] # no output expected ) ] ) def test_cyclic_deps(capsys, mpkgs, expected_keys, expected_output): tree = mock_PackageDAG(mpkgs) result = p.cyclic_deps(tree) result_keys = [(a.key, b.key, c.key) for (a, b, c) in result] assert sorted(expected_keys) == sorted(result_keys) p.render_cycles_text(result) captured = capsys.readouterr() assert '\n'.join(expected_output).strip() == captured.err.strip() # Tests for the argparse parser def test_parser_default(): parser = p.get_parser() args = parser.parse_args([]) assert not args.json assert args.output_format is None def test_parser_j(): parser = p.get_parser() args = parser.parse_args(['-j']) assert args.json assert args.output_format is None def test_parser_json(): parser = p.get_parser() args = parser.parse_args(['--json']) assert args.json assert args.output_format is None def test_parser_json_tree(): parser = p.get_parser() args = parser.parse_args(['--json-tree']) assert args.json_tree assert not args.json assert args.output_format is None def test_parser_pdf(): parser = p.get_parser() args = parser.parse_args(['--graph-output', 'pdf']) assert args.output_format == 'pdf' assert not args.json def test_parser_svg(): parser = p.get_parser() args = parser.parse_args(['--graph-output', 'svg']) assert args.output_format == 'svg' assert not args.json @pytest.mark.parametrize("args_joined", [True, False]) def test_custom_interpreter(tmp_path, monkeypatch, capfd, args_joined): result = virtualenv.cli_run([str(tmp_path), "--activators", ""]) cmd = [sys.executable] cmd += ["--python={}".format(result.creator.exe)] if args_joined else ["--python", str(result.creator.exe)] monkeypatch.setattr(sys, "argv", cmd) p.main() out, _ = capfd.readouterr() found = {i.split("==")[0] for i in out.splitlines()} implementation = platform.python_implementation() if implementation == "CPython": expected = {"pip", "setuptools", "wheel"} elif implementation == "PyPy": expected = {"cffi", "greenlet", "pip", "readline", "setuptools", "wheel"} else: raise ValueError(implementation) assert found == expected, out monkeypatch.setattr(sys, "argv", cmd + ["--graph-output", "something"]) with pytest.raises(SystemExit) as context: p.main() out, err = capfd.readouterr() assert context.value.code == 1 assert not out assert err == "graphviz functionality is not supported when querying" " non-host python\n"