Coverage for tests / unit / tools / core / test_option_validators.py: 100%

83 statements  

« prev     ^ index     » next       coverage.py v7.13.0, created at 2026-04-03 18:53 +0000

1"""Unit tests for option_validators module.""" 

2 

3from __future__ import annotations 

4 

5from typing import Any 

6 

7import pytest 

8from assertpy import assert_that 

9 

10from lintro.tools.core.option_validators import ( 

11 filter_none_options, 

12 normalize_str_or_list, 

13 validate_bool, 

14 validate_int, 

15 validate_list, 

16 validate_positive_int, 

17 validate_str, 

18) 

19 

20# ============================================================================= 

21# validate_bool tests 

22# ============================================================================= 

23 

24 

25@pytest.mark.parametrize( 

26 "value", 

27 [ 

28 pytest.param(True, id="true"), 

29 pytest.param(False, id="false"), 

30 pytest.param(None, id="none"), 

31 ], 

32) 

33def test_validate_bool_accepts_valid_values(value: bool | None) -> None: 

34 """Accept True, False, and None values. 

35 

36 Args: 

37 value: The boolean value to validate. 

38 """ 

39 # Should not raise - test passes if no exception 

40 validate_bool(value, "test") 

41 

42 

43@pytest.mark.parametrize( 

44 ("value", "description"), 

45 [ 

46 pytest.param("true", "string", id="string"), 

47 pytest.param(1, "integer", id="int"), 

48 ], 

49) 

50def test_validate_bool_rejects_invalid_values(value: Any, description: str) -> None: 

51 """Reject non-boolean values like {description}. 

52 

53 Args: 

54 value: The invalid value to test. 

55 description: Description of the test case. 

56 """ 

57 with pytest.raises(ValueError, match="must be a boolean"): 

58 validate_bool(value, "test") 

59 

60 

61# ============================================================================= 

62# validate_str tests 

63# ============================================================================= 

64 

65 

66@pytest.mark.parametrize( 

67 "value", 

68 [ 

69 pytest.param("hello", id="non_empty_string"), 

70 pytest.param("", id="empty_string"), 

71 pytest.param(None, id="none"), 

72 ], 

73) 

74def test_validate_str_accepts_valid_values(value: str | None) -> None: 

75 """Accept string and None values. 

76 

77 Args: 

78 value: The string value to validate. 

79 """ 

80 # Should not raise - test passes if no exception 

81 validate_str(value, "test") 

82 

83 

84@pytest.mark.parametrize( 

85 ("value", "description"), 

86 [ 

87 pytest.param(123, "integer", id="int"), 

88 pytest.param(["a", "b"], "list", id="list"), 

89 ], 

90) 

91def test_validate_str_rejects_invalid_values(value: Any, description: str) -> None: 

92 """Reject non-string values like {description}. 

93 

94 Args: 

95 value: The invalid value to test. 

96 description: Description of the test case. 

97 """ 

98 with pytest.raises(ValueError, match="must be a string"): 

99 validate_str(value, "test") 

100 

101 

102# ============================================================================= 

103# validate_int tests 

104# ============================================================================= 

105 

106 

107@pytest.mark.parametrize( 

108 "value", 

109 [ 

110 pytest.param(42, id="positive_int"), 

111 pytest.param(0, id="zero"), 

112 pytest.param(-5, id="negative_int"), 

113 pytest.param(None, id="none"), 

114 ], 

115) 

116def test_validate_int_accepts_valid_values(value: int | None) -> None: 

117 """Accept integer and None values. 

118 

119 Args: 

120 value: The integer value to validate. 

121 """ 

122 # Should not raise - test passes if no exception 

123 validate_int(value, "test") 

124 

125 

126@pytest.mark.parametrize( 

127 ("value", "description"), 

128 [ 

129 pytest.param("42", "string", id="string"), 

130 pytest.param(3.14, "float", id="float"), 

131 ], 

132) 

133def test_validate_int_rejects_invalid_values(value: Any, description: str) -> None: 

134 """Reject non-integer values like {description}. 

135 

136 Args: 

137 value: The invalid value to test. 

138 description: Description of the test case. 

139 """ 

140 with pytest.raises(ValueError, match="must be an integer"): 

141 validate_int(value, "test") 

142 

143 

144def test_validate_int_with_min_value() -> None: 

145 """Accept values at or above min_value.""" 

146 validate_int(5, "test", min_value=5) # Equal to min 

147 validate_int(10, "test", min_value=5) # Above min 

148 

149 

150def test_validate_int_rejects_below_min_value() -> None: 

151 """Reject values below min_value.""" 

152 with pytest.raises(ValueError, match="must be at least 5"): 

153 validate_int(4, "test", min_value=5) 

154 

155 

156def test_validate_int_with_max_value() -> None: 

157 """Accept values at or below max_value.""" 

158 validate_int(10, "test", max_value=10) # Equal to max 

159 validate_int(5, "test", max_value=10) # Below max 

160 

161 

162def test_validate_int_rejects_above_max_value() -> None: 

163 """Reject values above max_value.""" 

164 with pytest.raises(ValueError, match="must be at most 10"): 

165 validate_int(11, "test", max_value=10) 

166 

167 

168def test_validate_int_with_range() -> None: 

169 """Accept values within min and max range.""" 

170 validate_int(5, "test", min_value=1, max_value=10) 

171 validate_int(1, "test", min_value=1, max_value=10) 

172 validate_int(10, "test", min_value=1, max_value=10) 

173 

174 

175def test_validate_int_none_with_range() -> None: 

176 """Accept None even when range is specified.""" 

177 validate_int(None, "test", min_value=1, max_value=10) 

178 

179 

180# ============================================================================= 

181# validate_positive_int tests 

182# ============================================================================= 

183 

184 

185@pytest.mark.parametrize( 

186 "value", 

187 [ 

188 pytest.param(42, id="positive_int"), 

189 pytest.param(None, id="none"), 

190 ], 

191) 

192def test_validate_positive_int_accepts_valid_values(value: int | None) -> None: 

193 """Accept positive integer and None values. 

194 

195 Args: 

196 value: The positive integer value to validate. 

197 """ 

198 # Should not raise - test passes if no exception 

199 validate_positive_int(value, "test") 

200 

201 

202@pytest.mark.parametrize( 

203 ("value", "match_pattern"), 

204 [ 

205 pytest.param(0, "must be positive", id="zero"), 

206 pytest.param(-5, "must be positive", id="negative"), 

207 pytest.param("42", "must be an integer", id="string"), 

208 ], 

209) 

210def test_validate_positive_int_rejects_invalid_values( 

211 value: Any, 

212 match_pattern: str, 

213) -> None: 

214 """Reject zero, negative, and non-integer values. 

215 

216 Args: 

217 value: The invalid value to test. 

218 match_pattern: Pattern expected in the error message. 

219 """ 

220 with pytest.raises(ValueError, match=match_pattern): 

221 validate_positive_int(value, "test") 

222 

223 

224# ============================================================================= 

225# validate_list tests 

226# ============================================================================= 

227 

228 

229@pytest.mark.parametrize( 

230 "value", 

231 [ 

232 pytest.param([1, 2, 3], id="non_empty_list"), 

233 pytest.param([], id="empty_list"), 

234 pytest.param(None, id="none"), 

235 ], 

236) 

237def test_validate_list_accepts_valid_values(value: list[Any] | None) -> None: 

238 """Accept list and None values. 

239 

240 Args: 

241 value: The list value to validate. 

242 """ 

243 # Should not raise - test passes if no exception 

244 validate_list(value, "test") 

245 

246 

247@pytest.mark.parametrize( 

248 ("value", "description"), 

249 [ 

250 pytest.param("a,b,c", "string", id="string"), 

251 pytest.param((1, 2, 3), "tuple", id="tuple"), 

252 ], 

253) 

254def test_validate_list_rejects_invalid_values(value: Any, description: str) -> None: 

255 """Reject non-list values like {description}. 

256 

257 Args: 

258 value: The invalid value to test. 

259 description: Description of the test case. 

260 """ 

261 with pytest.raises(ValueError, match="must be a list"): 

262 validate_list(value, "test") 

263 

264 

265# ============================================================================= 

266# normalize_str_or_list tests 

267# ============================================================================= 

268 

269 

270@pytest.mark.parametrize( 

271 ("value", "expected"), 

272 [ 

273 pytest.param(None, None, id="none_returns_none"), 

274 pytest.param("hello", ["hello"], id="string_returns_list"), 

275 pytest.param(["a", "b"], ["a", "b"], id="list_returns_list"), 

276 ], 

277) 

278def test_normalize_str_or_list_valid_values( 

279 value: str | list[str] | None, 

280 expected: list[str] | None, 

281) -> None: 

282 """Normalize string to list and pass through list/None. 

283 

284 Args: 

285 value: The input value to normalize. 

286 expected: The expected normalized result. 

287 """ 

288 result = normalize_str_or_list(value, "test") 

289 if expected is None: 

290 assert_that(result).is_none() 

291 else: 

292 assert_that(result).is_equal_to(expected) 

293 

294 

295@pytest.mark.parametrize( 

296 ("value", "description"), 

297 [ 

298 pytest.param(123, "integer", id="int"), 

299 pytest.param({"key": "value"}, "dict", id="dict"), 

300 ], 

301) 

302def test_normalize_str_or_list_rejects_invalid_values( 

303 value: Any, 

304 description: str, 

305) -> None: 

306 """Reject non-string/list values like {description}. 

307 

308 Args: 

309 value: The invalid value to test. 

310 description: Description of the test case. 

311 """ 

312 with pytest.raises(ValueError, match="must be a string or list"): 

313 normalize_str_or_list(value, "test") 

314 

315 

316# ============================================================================= 

317# filter_none_options tests 

318# ============================================================================= 

319 

320 

321def test_filter_none_options_filters_none_values() -> None: 

322 """Filter out None values.""" 

323 result = filter_none_options(a=1, b=None, c="hello", d=None) 

324 assert_that(result).is_equal_to({"a": 1, "c": "hello"}) 

325 

326 

327def test_filter_none_options_empty_input() -> None: 

328 """Return empty dict for empty input.""" 

329 result = filter_none_options() 

330 assert_that(result).is_empty() 

331 

332 

333def test_filter_none_options_all_none() -> None: 

334 """Return empty dict when all values are None.""" 

335 result = filter_none_options(a=None, b=None) 

336 assert_that(result).is_empty() 

337 

338 

339def test_filter_none_options_no_none() -> None: 

340 """Return all values when none are None.""" 

341 result = filter_none_options(a=1, b=2, c=3) 

342 assert_that(result).is_equal_to({"a": 1, "b": 2, "c": 3}) 

343 

344 

345def test_filter_none_options_preserves_falsy_values() -> None: 

346 """Preserve False, 0, empty string (not None).""" 

347 result = filter_none_options(a=False, b=0, c="", d=None) 

348 assert_that(result).is_equal_to({"a": False, "b": 0, "c": ""})