Coverage for tests/test_call_graph/test_unused.py: 100%

1"""Tests for unused function detection."""

2

3 from __future__ import annotations

4

5 from pathlib import Path

6 import tempfile

7

8 from call_graph import build_call_graph, find_unused_functions

9

10

11 def test_entry_point_unused():

12 code = """

13 def calling():

14 called()

15

16 def called():

17 pass

18"""

19 with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as f:

20 f.write(code)

21 f.flush()

22

23 graph = build_call_graph([Path(f.name)])

24 unused = find_unused_functions(graph)

25

26 # 'calling' is an entry point that's never called, so it's unused

27 # 'called' is called by 'calling', so it's not unused

28 unused_names = {u.name for u in unused}

29 assert "calling" in unused_names

30 assert "called" not in unused_names

31

32 Path(f.name).unlink()

33

34

35 def test_mutual_cycle_both_used():

36 code = """

37 def foo():

38 return bar()

39

40 def bar():

41 return foo()

42"""

43 with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as f:

44 f.write(code)

45 f.flush()

46

47 graph = build_call_graph([Path(f.name)])

48 unused = find_unused_functions(graph)

49

50 # In a mutual call cycle, both functions are called (by each other)

51 # So neither should be detected as unused

52 assert len(unused) == 0

53

54 Path(f.name).unlink()

55

56

57 def test_private_filtered():

58 code = """

59 def _helper():

60 pass

61

62 def main():

63 _helper()

64"""

65 with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as f:

66 f.write(code)

67 f.flush()

68

69 graph = build_call_graph([Path(f.name)])

70 unused = find_unused_functions(graph)

71

72 # 'main' is an entry point (never called), so it's unused

73 # '_helper' is called by main, but also filtered because it starts with '_'

74 # So only 'main' appears in unused

75 unused_names = {u.name for u in unused}

76 assert len(unused_names) == 1

77 assert "main" in unused_names

78 assert "_helper" not in unused_names

79

80 Path(f.name).unlink()

81

82

83 def test_chained_usage():

84 code = """

85 def a():

86 b()

87

88 def b():

89 c()

90

91 def c():

92 pass

93"""

94 with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as f:

95 f.write(code)

96 f.flush()

97

98 graph = build_call_graph([Path(f.name)])

99 unused = find_unused_functions(graph)

100

101 # 'a' is the entry point, not called by anything

102 # 'b' and 'c' are called by a and b respectively, so not unused

103 unused_names = {u.name for u in unused}

104 assert "a" in unused_names

105 assert "b" not in unused_names

106 assert "c" not in unused_names

107

108 Path(f.name).unlink()

109

110

111 def test_unused_function_detection():

112 code = """

113 def a():

114 pass

115

116 def b():

117 pass

118"""

119 with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as f:

120 f.write(code)

121 f.flush()

122

123 graph = build_call_graph([Path(f.name)])

124 unused = find_unused_functions(graph)

125

126 # Neither function is called, so both are unused

127 unused_names = {u.name for u in unused}

128 assert len(unused_names) == 2

129 assert "a" in unused_names

130 assert "b" in unused_names

131

132 Path(f.name).unlink()

133

134

135 def test_caller_is_unused():

136 code = """

137 def foo():

138 return bar()

139

140 def bar():

141 return 42

142"""

143 with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as f:

144 f.write(code)

145 f.flush()

146

147 graph = build_call_graph([Path(f.name)])

148 unused = find_unused_functions(graph)

149

150 # 'foo' is not called by anyone, so it's unused

151 # 'bar' IS called by 'foo', so it's not unused

152 unused_names = {u.name for u in unused}

153 assert "foo" in unused_names

154 assert "bar" not in unused_names

155

156 Path(f.name).unlink()