Coverage for tests/test_common.py: 100%

1"""Tests for shared Halstead computation logic."""

2

3 import math

4

5 from common import compute_halstead, compute_ldi

6 from models import HalsteadMetrics

7

8

9 def test_known_input():

10 """Verify computed metrics against hand-calculated values."""

11 # 3 distinct operators, 4 distinct operands

12 # 5 total operators, 7 total operands

13 operators = ["+", "+", "*", "*", "="]

14 operands = ["x", "y", "z", "x", "y", "z", "w"]

15

16 h = compute_halstead(operators, operands)

17

18 assert h.n1 == 3 # distinct operators: +, *, =

19 assert h.n2 == 4 # distinct operands: x, y, z, w

20 assert h.N1 == 5 # total operators

21 assert h.N2 == 7 # total operands

22 assert h.vocabulary == 7 # n = 3 + 4

23 assert h.length == 12 # N = 5 + 7

24

25 expected_volume = 12 * math.log2(7)

26 assert h.volume == round(expected_volume, 2)

27

28 expected_difficulty = (3 / 2) * (7 / 4)

29 assert h.difficulty == round(expected_difficulty, 2)

30

31 expected_effort = expected_difficulty * expected_volume

32 assert h.effort == round(expected_effort, 2)

33

34 expected_time = expected_effort / 18

35 assert h.time == round(expected_time, 2)

36

37 expected_bugs = expected_effort ** (2 / 3) / 3000

38 assert h.bugs == round(expected_bugs, 4)

39

40

41 def test_empty_inputs():

42 """Empty operator/operand lists produce zeroed metrics."""

43 h = compute_halstead([], [])

44 assert h.n1 == 0

45 assert h.n2 == 0

46 assert h.N1 == 0

47 assert h.N2 == 0

48 assert h.vocabulary == 0

49 assert h.length == 0

50 assert h.volume == 0.0

51 assert h.difficulty == 0.0

52 assert h.effort == 0.0

53 assert h.time == 0.0

54 assert h.bugs == 0.0

55

56

57 def test_zero_operands_guard():

58 """Operators only (no operands) should not crash -- guards n2=0 division."""

59 h = compute_halstead(["+", "-"], [])

60 assert h.n1 == 2

61 assert h.n2 == 0

62 assert h.N1 == 2

63 assert h.N2 == 0

64 assert h.vocabulary == 2

65 assert h.length == 2

66 # Derived measures should be zero since n2=0

67 assert h.volume == 0.0

68 assert h.difficulty == 0.0

69 assert h.effort == 0.0

70

71

72 def test_zero_operators_guard():

73 """Operands only (no operators) should not crash."""

74 h = compute_halstead([], ["x", "y"])

75 assert h.n1 == 0

76 assert h.n2 == 2

77 assert h.N1 == 0

78 assert h.N2 == 2

79 assert h.vocabulary == 2

80 assert h.length == 2

81 # n=2 and n2=2 so derived measures can be computed, but n1=0 means D=0

82 assert h.volume == round(2 * math.log2(2), 2)

83 assert h.difficulty == 0.0 # (0/2) * (2/2) = 0

84

85

86 def test_effort_equals_d_times_v():

87 """E = D * V relationship holds."""

88 operators = ["=", "+", "*"]

89 operands = ["a", "b", "c", "a"]

90 h = compute_halstead(operators, operands)

91 # Verify E = D * V within rounding tolerance

92 assert abs(h.effort - h.difficulty * h.volume) < 0.1

93

94

95 def test_time_equals_effort_over_18():

96 """T = E / 18 relationship holds."""

97 operators = ["=", "+", "-", "="]

98 operands = ["x", "y", "z", "result"]

99 h = compute_halstead(operators, operands)

100 assert abs(h.time - h.effort / 18) < 0.1

101

102

103 def test_dataclass_defaults():

104 """HalsteadMetrics defaults are all zero."""

105 h = HalsteadMetrics()

106 assert h.n1 == 0

107 assert h.volume == 0.0

108 assert h.bugs == 0.0

109

110

111 def test_single_operator_single_operand():

112 """Minimal non-empty program."""

113 h = compute_halstead(["="], ["x"])

114 assert h.n1 == 1

115 assert h.n2 == 1

116 assert h.vocabulary == 2

117 assert h.length == 2

118 assert h.volume == round(2 * math.log2(2), 2)

119 assert h.difficulty == round(0.5 * 1.0, 2)

120

121

122 def test_ldi_basic():

123 """Verify LDI computation with known values."""

124 # Test case: vocabulary=10, n2=5, volume=8, difficulty=2.5

125 # Expected: (10/100 * 40) + (2.5/60 * 40) + (5/8 * 200) = 4 + 1.67 + 125 = 130.67, capped at 100

126 ldi = compute_ldi(volume=8, vocabulary=10, n2=5, difficulty=2.5)

127 assert ldi == 100.0

128

129

130 def test_ldi_within_range():

131 """Verify LDI stays within 0-100 range."""

132 # Lower vocabulary/difficulty should produce smaller LDI

133 h = compute_halstead(["=", "+", "*"], ["a", "b", "c", "a"])

134 ldi = compute_ldi(h.volume, h.vocabulary, h.n2, h.difficulty)

135 assert 0 <= ldi <= 100

136

137

138 def test_ldi_zero_volume():

139 """Zero volume returns 0 LDI."""

140 ldi = compute_ldi(volume=0, vocabulary=10, n2=5, difficulty=2.5)

141 assert ldi == 0.0

142

143

144 def test_ldi_formula_components():

145 """Verify individual components of LDI formula."""

146 ldi = compute_ldi(volume=50, vocabulary=20, n2=10, difficulty=30)

147 # (20/100 * 40) = 8, (30/60 * 40) = 20, (10/50 * 200) = 40 = 68

148 assert round(ldi, 2) == 68.0

149

150

151 def test_ldi_known_input():

152 """Calculate LDI from known Halstead metrics."""

153 h = compute_halstead(["=", "+", "*"], ["a", "b", "c", "a"])

154 # LDI should be computed without error

155 ldi = compute_ldi(h.volume, h.vocabulary, h.n2, h.difficulty)

156 assert isinstance(ldi, float)

157 assert 0 <= ldi <= 100