Coverage for tests / test_common.py: 100%

98 statements  

« prev     ^ index     » next       coverage.py v7.13.3, created at 2026-02-08 15:04 -0800

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

2 

3import math 

4 

5from common import compute_halstead, compute_ldi 

6from models import HalsteadMetrics 

7 

8 

9def 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 

41def 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 

57def 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 

72def 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 

86def 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 

95def 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 

103def 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 

111def 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 

122def 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 

130def 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 

138def 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 

144def 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 

151def 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