test_variable_factory.py 32 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890
  1. import math
  2. from dataclasses import dataclass
  3. from typing import Any
  4. from uuid import uuid4
  5. import pytest
  6. from hypothesis import given
  7. from hypothesis import strategies as st
  8. from core.file import File, FileTransferMethod, FileType
  9. from core.variables import (
  10. ArrayNumberVariable,
  11. ArrayObjectVariable,
  12. ArrayStringVariable,
  13. FloatVariable,
  14. IntegerVariable,
  15. SecretVariable,
  16. StringVariable,
  17. )
  18. from core.variables.exc import VariableError
  19. from core.variables.segments import (
  20. ArrayAnySegment,
  21. ArrayFileSegment,
  22. ArrayNumberSegment,
  23. ArrayObjectSegment,
  24. ArrayStringSegment,
  25. BooleanSegment,
  26. FileSegment,
  27. FloatSegment,
  28. IntegerSegment,
  29. NoneSegment,
  30. ObjectSegment,
  31. Segment,
  32. StringSegment,
  33. )
  34. from core.variables.types import SegmentType
  35. from factories import variable_factory
  36. from factories.variable_factory import TypeMismatchError, build_segment, build_segment_with_type
  37. def test_string_variable():
  38. test_data = {"value_type": "string", "name": "test_text", "value": "Hello, World!"}
  39. result = variable_factory.build_conversation_variable_from_mapping(test_data)
  40. assert isinstance(result, StringVariable)
  41. def test_integer_variable():
  42. test_data = {"value_type": "number", "name": "test_int", "value": 42}
  43. result = variable_factory.build_conversation_variable_from_mapping(test_data)
  44. assert isinstance(result, IntegerVariable)
  45. def test_float_variable():
  46. test_data = {"value_type": "number", "name": "test_float", "value": 3.14}
  47. result = variable_factory.build_conversation_variable_from_mapping(test_data)
  48. assert isinstance(result, FloatVariable)
  49. def test_secret_variable():
  50. test_data = {"value_type": "secret", "name": "test_secret", "value": "secret_value"}
  51. result = variable_factory.build_conversation_variable_from_mapping(test_data)
  52. assert isinstance(result, SecretVariable)
  53. def test_invalid_value_type():
  54. test_data = {"value_type": "unknown", "name": "test_invalid", "value": "value"}
  55. with pytest.raises(VariableError):
  56. variable_factory.build_conversation_variable_from_mapping(test_data)
  57. def test_build_a_blank_string():
  58. result = variable_factory.build_conversation_variable_from_mapping(
  59. {
  60. "value_type": "string",
  61. "name": "blank",
  62. "value": "",
  63. }
  64. )
  65. assert isinstance(result, StringVariable)
  66. assert result.value == ""
  67. def test_build_a_object_variable_with_none_value():
  68. var = variable_factory.build_segment(
  69. {
  70. "key1": None,
  71. }
  72. )
  73. assert isinstance(var, ObjectSegment)
  74. assert var.value["key1"] is None
  75. def test_object_variable():
  76. mapping = {
  77. "id": str(uuid4()),
  78. "value_type": "object",
  79. "name": "test_object",
  80. "description": "Description of the variable.",
  81. "value": {
  82. "key1": "text",
  83. "key2": 2,
  84. },
  85. }
  86. variable = variable_factory.build_conversation_variable_from_mapping(mapping)
  87. assert isinstance(variable, ObjectSegment)
  88. assert isinstance(variable.value["key1"], str)
  89. assert isinstance(variable.value["key2"], int)
  90. def test_array_string_variable():
  91. mapping = {
  92. "id": str(uuid4()),
  93. "value_type": "array[string]",
  94. "name": "test_array",
  95. "description": "Description of the variable.",
  96. "value": [
  97. "text",
  98. "text",
  99. ],
  100. }
  101. variable = variable_factory.build_conversation_variable_from_mapping(mapping)
  102. assert isinstance(variable, ArrayStringVariable)
  103. assert isinstance(variable.value[0], str)
  104. assert isinstance(variable.value[1], str)
  105. def test_array_number_variable():
  106. mapping = {
  107. "id": str(uuid4()),
  108. "value_type": "array[number]",
  109. "name": "test_array",
  110. "description": "Description of the variable.",
  111. "value": [
  112. 1,
  113. 2.0,
  114. ],
  115. }
  116. variable = variable_factory.build_conversation_variable_from_mapping(mapping)
  117. assert isinstance(variable, ArrayNumberVariable)
  118. assert isinstance(variable.value[0], int)
  119. assert isinstance(variable.value[1], float)
  120. def test_build_segment_scalar_values():
  121. @dataclass
  122. class TestCase:
  123. value: Any
  124. expected: Segment
  125. description: str
  126. cases = [
  127. TestCase(
  128. value=True,
  129. expected=BooleanSegment(value=True),
  130. description="build_segment with boolean should yield BooleanSegment",
  131. )
  132. ]
  133. for idx, c in enumerate(cases, 1):
  134. seg = build_segment(c.value)
  135. assert seg == c.expected, f"Test case {idx} failed: {c.description}"
  136. def test_array_object_variable():
  137. mapping = {
  138. "id": str(uuid4()),
  139. "value_type": "array[object]",
  140. "name": "test_array",
  141. "description": "Description of the variable.",
  142. "value": [
  143. {
  144. "key1": "text",
  145. "key2": 1,
  146. },
  147. {
  148. "key1": "text",
  149. "key2": 1,
  150. },
  151. ],
  152. }
  153. variable = variable_factory.build_conversation_variable_from_mapping(mapping)
  154. assert isinstance(variable, ArrayObjectVariable)
  155. assert isinstance(variable.value[0], dict)
  156. assert isinstance(variable.value[1], dict)
  157. assert isinstance(variable.value[0]["key1"], str)
  158. assert isinstance(variable.value[0]["key2"], int)
  159. assert isinstance(variable.value[1]["key1"], str)
  160. assert isinstance(variable.value[1]["key2"], int)
  161. def test_variable_cannot_large_than_200_kb():
  162. with pytest.raises(VariableError):
  163. variable_factory.build_conversation_variable_from_mapping(
  164. {
  165. "id": str(uuid4()),
  166. "value_type": "string",
  167. "name": "test_text",
  168. "value": "a" * 1024 * 201,
  169. }
  170. )
  171. def test_array_none_variable():
  172. var = variable_factory.build_segment([None, None, None, None])
  173. assert isinstance(var, ArrayAnySegment)
  174. assert var.value == [None, None, None, None]
  175. def test_build_segment_none_type():
  176. """Test building NoneSegment from None value."""
  177. segment = variable_factory.build_segment(None)
  178. assert isinstance(segment, NoneSegment)
  179. assert segment.value is None
  180. assert segment.value_type == SegmentType.NONE
  181. def test_build_segment_none_type_properties():
  182. """Test NoneSegment properties and methods."""
  183. segment = variable_factory.build_segment(None)
  184. assert segment.text == ""
  185. assert segment.log == ""
  186. assert segment.markdown == ""
  187. assert segment.to_object() is None
  188. def test_build_segment_array_file_single_file():
  189. """Test building ArrayFileSegment from list with single file."""
  190. file = File(
  191. id="test_file_id",
  192. tenant_id="test_tenant_id",
  193. type=FileType.IMAGE,
  194. transfer_method=FileTransferMethod.REMOTE_URL,
  195. remote_url="https://test.example.com/test-file.png",
  196. filename="test-file",
  197. extension=".png",
  198. mime_type="image/png",
  199. size=1000,
  200. )
  201. segment = variable_factory.build_segment([file])
  202. assert isinstance(segment, ArrayFileSegment)
  203. assert len(segment.value) == 1
  204. assert segment.value[0] == file
  205. assert segment.value_type == SegmentType.ARRAY_FILE
  206. def test_build_segment_array_file_multiple_files():
  207. """Test building ArrayFileSegment from list with multiple files."""
  208. file1 = File(
  209. id="test_file_id_1",
  210. tenant_id="test_tenant_id",
  211. type=FileType.IMAGE,
  212. transfer_method=FileTransferMethod.REMOTE_URL,
  213. remote_url="https://test.example.com/test-file1.png",
  214. filename="test-file1",
  215. extension=".png",
  216. mime_type="image/png",
  217. size=1000,
  218. )
  219. file2 = File(
  220. id="test_file_id_2",
  221. tenant_id="test_tenant_id",
  222. type=FileType.DOCUMENT,
  223. transfer_method=FileTransferMethod.LOCAL_FILE,
  224. related_id="test_relation_id",
  225. filename="test-file2",
  226. extension=".txt",
  227. mime_type="text/plain",
  228. size=500,
  229. )
  230. segment = variable_factory.build_segment([file1, file2])
  231. assert isinstance(segment, ArrayFileSegment)
  232. assert len(segment.value) == 2
  233. assert segment.value[0] == file1
  234. assert segment.value[1] == file2
  235. assert segment.value_type == SegmentType.ARRAY_FILE
  236. def test_build_segment_array_file_empty_list():
  237. """Test building ArrayFileSegment from empty list should create ArrayAnySegment."""
  238. segment = variable_factory.build_segment([])
  239. assert isinstance(segment, ArrayAnySegment)
  240. assert segment.value == []
  241. assert segment.value_type == SegmentType.ARRAY_ANY
  242. def test_build_segment_array_any_mixed_types():
  243. """Test building ArrayAnySegment from list with mixed types."""
  244. mixed_values = ["string", 42, 3.14, {"key": "value"}, None]
  245. segment = variable_factory.build_segment(mixed_values)
  246. assert isinstance(segment, ArrayAnySegment)
  247. assert segment.value == mixed_values
  248. assert segment.value_type == SegmentType.ARRAY_ANY
  249. def test_build_segment_array_any_with_nested_arrays():
  250. """Test building ArrayAnySegment from list containing arrays."""
  251. nested_values = [["nested", "array"], [1, 2, 3], "string"]
  252. segment = variable_factory.build_segment(nested_values)
  253. assert isinstance(segment, ArrayAnySegment)
  254. assert segment.value == nested_values
  255. assert segment.value_type == SegmentType.ARRAY_ANY
  256. def test_build_segment_array_any_mixed_with_files():
  257. """Test building ArrayAnySegment from list with files and other types."""
  258. file = File(
  259. id="test_file_id",
  260. tenant_id="test_tenant_id",
  261. type=FileType.IMAGE,
  262. transfer_method=FileTransferMethod.REMOTE_URL,
  263. remote_url="https://test.example.com/test-file.png",
  264. filename="test-file",
  265. extension=".png",
  266. mime_type="image/png",
  267. size=1000,
  268. )
  269. mixed_values = [file, "string", 42]
  270. segment = variable_factory.build_segment(mixed_values)
  271. assert isinstance(segment, ArrayAnySegment)
  272. assert segment.value == mixed_values
  273. assert segment.value_type == SegmentType.ARRAY_ANY
  274. def test_build_segment_array_any_all_none_values():
  275. """Test building ArrayAnySegment from list with all None values."""
  276. none_values = [None, None, None]
  277. segment = variable_factory.build_segment(none_values)
  278. assert isinstance(segment, ArrayAnySegment)
  279. assert segment.value == none_values
  280. assert segment.value_type == SegmentType.ARRAY_ANY
  281. def test_build_segment_array_file_properties():
  282. """Test ArrayFileSegment properties and methods."""
  283. file1 = File(
  284. id="test_file_id_1",
  285. tenant_id="test_tenant_id",
  286. type=FileType.IMAGE,
  287. transfer_method=FileTransferMethod.REMOTE_URL,
  288. remote_url="https://test.example.com/test-file1.png",
  289. filename="test-file1",
  290. extension=".png",
  291. mime_type="image/png",
  292. size=1000,
  293. )
  294. file2 = File(
  295. id="test_file_id_2",
  296. tenant_id="test_tenant_id",
  297. type=FileType.DOCUMENT,
  298. transfer_method=FileTransferMethod.REMOTE_URL,
  299. remote_url="https://test.example.com/test-file2.txt",
  300. filename="test-file2",
  301. extension=".txt",
  302. mime_type="text/plain",
  303. size=500,
  304. )
  305. segment = variable_factory.build_segment([file1, file2])
  306. # Test properties
  307. assert segment.text == "" # ArrayFileSegment text property returns empty string
  308. assert segment.log == "" # ArrayFileSegment log property returns empty string
  309. assert segment.markdown == f"{file1.markdown}\n{file2.markdown}"
  310. assert segment.to_object() == [file1, file2]
  311. def test_build_segment_array_any_properties():
  312. """Test ArrayAnySegment properties and methods."""
  313. mixed_values = ["string", 42, None]
  314. segment = variable_factory.build_segment(mixed_values)
  315. # Test properties
  316. assert segment.text == str(mixed_values)
  317. assert segment.log == str(mixed_values)
  318. assert segment.markdown == "string\n42\nNone"
  319. assert segment.to_object() == mixed_values
  320. def test_build_segment_edge_cases():
  321. """Test edge cases for build_segment function."""
  322. # Test with complex nested structures
  323. complex_structure = [{"nested": {"deep": [1, 2, 3]}}, [{"inner": "value"}], "mixed"]
  324. segment = variable_factory.build_segment(complex_structure)
  325. assert isinstance(segment, ArrayAnySegment)
  326. assert segment.value == complex_structure
  327. # Test with single None in list
  328. single_none = [None]
  329. segment = variable_factory.build_segment(single_none)
  330. assert isinstance(segment, ArrayAnySegment)
  331. assert segment.value == single_none
  332. def test_build_segment_file_array_with_different_file_types():
  333. """Test ArrayFileSegment with different file types."""
  334. image_file = File(
  335. id="image_id",
  336. tenant_id="test_tenant_id",
  337. type=FileType.IMAGE,
  338. transfer_method=FileTransferMethod.REMOTE_URL,
  339. remote_url="https://test.example.com/image.png",
  340. filename="image",
  341. extension=".png",
  342. mime_type="image/png",
  343. size=1000,
  344. )
  345. video_file = File(
  346. id="video_id",
  347. tenant_id="test_tenant_id",
  348. type=FileType.VIDEO,
  349. transfer_method=FileTransferMethod.LOCAL_FILE,
  350. related_id="video_relation_id",
  351. filename="video",
  352. extension=".mp4",
  353. mime_type="video/mp4",
  354. size=5000,
  355. )
  356. audio_file = File(
  357. id="audio_id",
  358. tenant_id="test_tenant_id",
  359. type=FileType.AUDIO,
  360. transfer_method=FileTransferMethod.LOCAL_FILE,
  361. related_id="audio_relation_id",
  362. filename="audio",
  363. extension=".mp3",
  364. mime_type="audio/mpeg",
  365. size=3000,
  366. )
  367. segment = variable_factory.build_segment([image_file, video_file, audio_file])
  368. assert isinstance(segment, ArrayFileSegment)
  369. assert len(segment.value) == 3
  370. assert segment.value[0].type == FileType.IMAGE
  371. assert segment.value[1].type == FileType.VIDEO
  372. assert segment.value[2].type == FileType.AUDIO
  373. @st.composite
  374. def _generate_file(draw) -> File:
  375. file_type, mime_type, extension = draw(
  376. st.sampled_from(
  377. [
  378. (FileType.IMAGE, "image/png", ".png"),
  379. (FileType.VIDEO, "video/mp4", ".mp4"),
  380. (FileType.DOCUMENT, "text/plain", ".txt"),
  381. (FileType.AUDIO, "audio/mpeg", ".mp3"),
  382. ]
  383. )
  384. )
  385. filename = "test-file"
  386. size = draw(st.integers(min_value=0, max_value=1024 * 1024))
  387. transfer_method = draw(st.sampled_from(list(FileTransferMethod)))
  388. if transfer_method == FileTransferMethod.REMOTE_URL:
  389. url = "https://test.example.com/test-file"
  390. file = File(
  391. id="test_file_id",
  392. tenant_id="test_tenant_id",
  393. type=file_type,
  394. transfer_method=transfer_method,
  395. remote_url=url,
  396. related_id=None,
  397. filename=filename,
  398. extension=extension,
  399. mime_type=mime_type,
  400. size=size,
  401. )
  402. else:
  403. relation_id = draw(st.uuids(version=4))
  404. file = File(
  405. id="test_file_id",
  406. tenant_id="test_tenant_id",
  407. type=file_type,
  408. transfer_method=transfer_method,
  409. related_id=str(relation_id),
  410. filename=filename,
  411. extension=extension,
  412. mime_type=mime_type,
  413. size=size,
  414. )
  415. return file
  416. def _scalar_value() -> st.SearchStrategy[int | float | str | File | None]:
  417. return st.one_of(
  418. st.none(),
  419. st.integers(),
  420. st.floats(),
  421. st.text(),
  422. _generate_file(),
  423. )
  424. @given(_scalar_value())
  425. def test_build_segment_and_extract_values_for_scalar_types(value):
  426. seg = variable_factory.build_segment(value)
  427. # nan == nan yields false, so we need to use `math.isnan` to check `seg.value` here.
  428. if isinstance(value, float) and math.isnan(value):
  429. assert math.isnan(seg.value)
  430. else:
  431. assert seg.value == value
  432. @given(st.lists(_scalar_value()))
  433. def test_build_segment_and_extract_values_for_array_types(values):
  434. seg = variable_factory.build_segment(values)
  435. assert seg.value == values
  436. def test_build_segment_type_for_scalar():
  437. @dataclass(frozen=True)
  438. class TestCase:
  439. value: int | float | str | File
  440. expected_type: SegmentType
  441. file = File(
  442. id="test_file_id",
  443. tenant_id="test_tenant_id",
  444. type=FileType.IMAGE,
  445. transfer_method=FileTransferMethod.REMOTE_URL,
  446. remote_url="https://test.example.com/test-file.png",
  447. filename="test-file",
  448. extension=".png",
  449. mime_type="image/png",
  450. size=1000,
  451. )
  452. cases = [
  453. TestCase(0, SegmentType.INTEGER),
  454. TestCase(0.0, SegmentType.FLOAT),
  455. TestCase("", SegmentType.STRING),
  456. TestCase(file, SegmentType.FILE),
  457. ]
  458. for idx, c in enumerate(cases, 1):
  459. segment = variable_factory.build_segment(c.value)
  460. assert segment.value_type == c.expected_type, f"test case {idx} failed."
  461. class TestBuildSegmentWithType:
  462. """Test cases for build_segment_with_type function."""
  463. def test_string_type(self):
  464. """Test building a string segment with correct type."""
  465. result = build_segment_with_type(SegmentType.STRING, "hello")
  466. assert isinstance(result, StringSegment)
  467. assert result.value == "hello"
  468. assert result.value_type == SegmentType.STRING
  469. def test_number_type_integer(self):
  470. """Test building a number segment with integer value."""
  471. result = build_segment_with_type(SegmentType.NUMBER, 42)
  472. assert isinstance(result, IntegerSegment)
  473. assert result.value == 42
  474. assert result.value_type == SegmentType.INTEGER
  475. def test_number_type_float(self):
  476. """Test building a number segment with float value."""
  477. result = build_segment_with_type(SegmentType.NUMBER, 3.14)
  478. assert isinstance(result, FloatSegment)
  479. assert result.value == 3.14
  480. assert result.value_type == SegmentType.FLOAT
  481. def test_object_type(self):
  482. """Test building an object segment with correct type."""
  483. test_obj = {"key": "value", "nested": {"inner": 123}}
  484. result = build_segment_with_type(SegmentType.OBJECT, test_obj)
  485. assert isinstance(result, ObjectSegment)
  486. assert result.value == test_obj
  487. assert result.value_type == SegmentType.OBJECT
  488. def test_file_type(self):
  489. """Test building a file segment with correct type."""
  490. test_file = File(
  491. id="test_file_id",
  492. tenant_id="test_tenant_id",
  493. type=FileType.IMAGE,
  494. transfer_method=FileTransferMethod.REMOTE_URL,
  495. remote_url="https://test.example.com/test-file.png",
  496. filename="test-file",
  497. extension=".png",
  498. mime_type="image/png",
  499. size=1000,
  500. storage_key="test_storage_key",
  501. )
  502. result = build_segment_with_type(SegmentType.FILE, test_file)
  503. assert isinstance(result, FileSegment)
  504. assert result.value == test_file
  505. assert result.value_type == SegmentType.FILE
  506. def test_none_type(self):
  507. """Test building a none segment with None value."""
  508. result = build_segment_with_type(SegmentType.NONE, None)
  509. assert isinstance(result, NoneSegment)
  510. assert result.value is None
  511. assert result.value_type == SegmentType.NONE
  512. def test_empty_array_string(self):
  513. """Test building an empty array[string] segment."""
  514. result = build_segment_with_type(SegmentType.ARRAY_STRING, [])
  515. assert isinstance(result, ArrayStringSegment)
  516. assert result.value == []
  517. assert result.value_type == SegmentType.ARRAY_STRING
  518. def test_empty_array_number(self):
  519. """Test building an empty array[number] segment."""
  520. result = build_segment_with_type(SegmentType.ARRAY_NUMBER, [])
  521. assert isinstance(result, ArrayNumberSegment)
  522. assert result.value == []
  523. assert result.value_type == SegmentType.ARRAY_NUMBER
  524. def test_empty_array_object(self):
  525. """Test building an empty array[object] segment."""
  526. result = build_segment_with_type(SegmentType.ARRAY_OBJECT, [])
  527. assert isinstance(result, ArrayObjectSegment)
  528. assert result.value == []
  529. assert result.value_type == SegmentType.ARRAY_OBJECT
  530. def test_empty_array_file(self):
  531. """Test building an empty array[file] segment."""
  532. result = build_segment_with_type(SegmentType.ARRAY_FILE, [])
  533. assert isinstance(result, ArrayFileSegment)
  534. assert result.value == []
  535. assert result.value_type == SegmentType.ARRAY_FILE
  536. def test_empty_array_any(self):
  537. """Test building an empty array[any] segment."""
  538. result = build_segment_with_type(SegmentType.ARRAY_ANY, [])
  539. assert isinstance(result, ArrayAnySegment)
  540. assert result.value == []
  541. assert result.value_type == SegmentType.ARRAY_ANY
  542. def test_array_with_values(self):
  543. """Test building array segments with actual values."""
  544. # Array of strings
  545. result = build_segment_with_type(SegmentType.ARRAY_STRING, ["hello", "world"])
  546. assert isinstance(result, ArrayStringSegment)
  547. assert result.value == ["hello", "world"]
  548. assert result.value_type == SegmentType.ARRAY_STRING
  549. # Array of numbers
  550. result = build_segment_with_type(SegmentType.ARRAY_NUMBER, [1, 2, 3.14])
  551. assert isinstance(result, ArrayNumberSegment)
  552. assert result.value == [1, 2, 3.14]
  553. assert result.value_type == SegmentType.ARRAY_NUMBER
  554. # Array of objects
  555. result = build_segment_with_type(SegmentType.ARRAY_OBJECT, [{"a": 1}, {"b": 2}])
  556. assert isinstance(result, ArrayObjectSegment)
  557. assert result.value == [{"a": 1}, {"b": 2}]
  558. assert result.value_type == SegmentType.ARRAY_OBJECT
  559. def test_type_mismatch_string_to_number(self):
  560. """Test type mismatch when expecting number but getting string."""
  561. with pytest.raises(TypeMismatchError) as exc_info:
  562. build_segment_with_type(SegmentType.NUMBER, "not_a_number")
  563. assert "Type mismatch" in str(exc_info.value)
  564. assert "expected number" in str(exc_info.value)
  565. assert "str" in str(exc_info.value)
  566. def test_type_mismatch_number_to_string(self):
  567. """Test type mismatch when expecting string but getting number."""
  568. with pytest.raises(TypeMismatchError) as exc_info:
  569. build_segment_with_type(SegmentType.STRING, 123)
  570. assert "Type mismatch" in str(exc_info.value)
  571. assert "expected string" in str(exc_info.value)
  572. assert "int" in str(exc_info.value)
  573. def test_type_mismatch_none_to_string(self):
  574. """Test type mismatch when expecting string but getting None."""
  575. with pytest.raises(TypeMismatchError) as exc_info:
  576. build_segment_with_type(SegmentType.STRING, None)
  577. assert "expected string, but got None" in str(exc_info.value)
  578. def test_type_mismatch_empty_list_to_non_array(self):
  579. """Test type mismatch when expecting non-array type but getting empty list."""
  580. with pytest.raises(TypeMismatchError) as exc_info:
  581. build_segment_with_type(SegmentType.STRING, [])
  582. assert "expected string, but got empty list" in str(exc_info.value)
  583. def test_type_mismatch_object_to_array(self):
  584. """Test type mismatch when expecting array but getting object."""
  585. with pytest.raises(TypeMismatchError) as exc_info:
  586. build_segment_with_type(SegmentType.ARRAY_STRING, {"key": "value"})
  587. assert "Type mismatch" in str(exc_info.value)
  588. assert "expected array[string]" in str(exc_info.value)
  589. def test_compatible_number_types(self):
  590. """Test that int and float are both compatible with NUMBER type."""
  591. # Integer should work
  592. result_int = build_segment_with_type(SegmentType.NUMBER, 42)
  593. assert isinstance(result_int, IntegerSegment)
  594. assert result_int.value_type == SegmentType.INTEGER
  595. # Float should work
  596. result_float = build_segment_with_type(SegmentType.NUMBER, 3.14)
  597. assert isinstance(result_float, FloatSegment)
  598. assert result_float.value_type == SegmentType.FLOAT
  599. @pytest.mark.parametrize(
  600. ("segment_type", "value", "expected_class"),
  601. [
  602. (SegmentType.STRING, "test", StringSegment),
  603. (SegmentType.INTEGER, 42, IntegerSegment),
  604. (SegmentType.FLOAT, 3.14, FloatSegment),
  605. (SegmentType.OBJECT, {}, ObjectSegment),
  606. (SegmentType.NONE, None, NoneSegment),
  607. (SegmentType.ARRAY_STRING, [], ArrayStringSegment),
  608. (SegmentType.ARRAY_NUMBER, [], ArrayNumberSegment),
  609. (SegmentType.ARRAY_OBJECT, [], ArrayObjectSegment),
  610. (SegmentType.ARRAY_ANY, [], ArrayAnySegment),
  611. ],
  612. )
  613. def test_parametrized_valid_types(self, segment_type, value, expected_class):
  614. """Parametrized test for valid type combinations."""
  615. result = build_segment_with_type(segment_type, value)
  616. assert isinstance(result, expected_class)
  617. assert result.value == value
  618. assert result.value_type == segment_type
  619. @pytest.mark.parametrize(
  620. ("segment_type", "value"),
  621. [
  622. (SegmentType.STRING, 123),
  623. (SegmentType.NUMBER, "not_a_number"),
  624. (SegmentType.OBJECT, "not_an_object"),
  625. (SegmentType.ARRAY_STRING, "not_an_array"),
  626. (SegmentType.STRING, None),
  627. (SegmentType.NUMBER, None),
  628. ],
  629. )
  630. def test_parametrized_type_mismatches(self, segment_type, value):
  631. """Parametrized test for type mismatches that should raise TypeMismatchError."""
  632. with pytest.raises(TypeMismatchError):
  633. build_segment_with_type(segment_type, value)
  634. # Test cases for ValueError scenarios in build_segment function
  635. class TestBuildSegmentValueErrors:
  636. """Test cases for ValueError scenarios in the build_segment function."""
  637. @dataclass(frozen=True)
  638. class ValueErrorTestCase:
  639. """Test case data for ValueError scenarios."""
  640. name: str
  641. description: str
  642. test_value: Any
  643. def _get_test_cases(self):
  644. """Get all test cases for ValueError scenarios."""
  645. # Define inline classes for complex test cases
  646. class CustomType:
  647. pass
  648. def unsupported_function():
  649. return "test"
  650. def gen():
  651. yield 1
  652. yield 2
  653. return [
  654. self.ValueErrorTestCase(
  655. name="unsupported_custom_type",
  656. description="custom class that doesn't match any supported type",
  657. test_value=CustomType(),
  658. ),
  659. self.ValueErrorTestCase(
  660. name="unsupported_set_type",
  661. description="set (unsupported collection type)",
  662. test_value={1, 2, 3},
  663. ),
  664. self.ValueErrorTestCase(
  665. name="unsupported_tuple_type", description="tuple (unsupported type)", test_value=(1, 2, 3)
  666. ),
  667. self.ValueErrorTestCase(
  668. name="unsupported_bytes_type",
  669. description="bytes (unsupported type)",
  670. test_value=b"hello world",
  671. ),
  672. self.ValueErrorTestCase(
  673. name="unsupported_function_type",
  674. description="function (unsupported type)",
  675. test_value=unsupported_function,
  676. ),
  677. self.ValueErrorTestCase(
  678. name="unsupported_module_type", description="module (unsupported type)", test_value=math
  679. ),
  680. self.ValueErrorTestCase(
  681. name="array_with_unsupported_element_types",
  682. description="array with unsupported element types",
  683. test_value=[CustomType()],
  684. ),
  685. self.ValueErrorTestCase(
  686. name="mixed_array_with_unsupported_types",
  687. description="array with mix of supported and unsupported types",
  688. test_value=["valid_string", 42, CustomType()],
  689. ),
  690. self.ValueErrorTestCase(
  691. name="nested_unsupported_types",
  692. description="nested structures containing unsupported types",
  693. test_value=[{"valid": "data"}, CustomType()],
  694. ),
  695. self.ValueErrorTestCase(
  696. name="complex_number_type",
  697. description="complex number (unsupported type)",
  698. test_value=3 + 4j,
  699. ),
  700. self.ValueErrorTestCase(
  701. name="range_type", description="range object (unsupported type)", test_value=range(10)
  702. ),
  703. self.ValueErrorTestCase(
  704. name="generator_type",
  705. description="generator (unsupported type)",
  706. test_value=gen(),
  707. ),
  708. self.ValueErrorTestCase(
  709. name="exception_message_contains_value",
  710. description="set to verify error message contains the actual unsupported value",
  711. test_value={1, 2, 3},
  712. ),
  713. self.ValueErrorTestCase(
  714. name="array_with_mixed_unsupported_segment_types",
  715. description="array processing with unsupported segment types in match",
  716. test_value=[CustomType()],
  717. ),
  718. self.ValueErrorTestCase(
  719. name="frozenset_type",
  720. description="frozenset (unsupported type)",
  721. test_value=frozenset([1, 2, 3]),
  722. ),
  723. self.ValueErrorTestCase(
  724. name="memoryview_type",
  725. description="memoryview (unsupported type)",
  726. test_value=memoryview(b"hello"),
  727. ),
  728. self.ValueErrorTestCase(
  729. name="slice_type", description="slice object (unsupported type)", test_value=slice(1, 10, 2)
  730. ),
  731. self.ValueErrorTestCase(name="type_object", description="type object (unsupported type)", test_value=type),
  732. self.ValueErrorTestCase(
  733. name="generic_object", description="generic object (unsupported type)", test_value=object()
  734. ),
  735. ]
  736. def test_build_segment_unsupported_types(self):
  737. """Table-driven test for all ValueError scenarios in build_segment function."""
  738. test_cases = self._get_test_cases()
  739. for index, test_case in enumerate(test_cases, 1):
  740. # Use test value directly
  741. test_value = test_case.test_value
  742. with pytest.raises(ValueError) as exc_info: # noqa: PT012
  743. segment = variable_factory.build_segment(test_value)
  744. pytest.fail(f"Test case {index} ({test_case.name}) should raise ValueError but not, result={segment}")
  745. error_message = str(exc_info.value)
  746. assert "not supported value" in error_message, (
  747. f"Test case {index} ({test_case.name}): Expected 'not supported value' in error message, "
  748. f"but got: {error_message}"
  749. )
  750. def test_build_segment_boolean_type(self):
  751. """Test that Boolean values are correctly handled as boolean type, not integers."""
  752. # Boolean values should now be processed as BooleanSegment, not IntegerSegment
  753. # This is because the bool check now comes before the int check in build_segment
  754. true_segment = variable_factory.build_segment(True)
  755. false_segment = variable_factory.build_segment(False)
  756. # Verify they are processed as booleans, not integers
  757. assert true_segment.value is True, "Test case 1 (boolean_true): Expected True to be processed as boolean True"
  758. assert false_segment.value is False, (
  759. "Test case 2 (boolean_false): Expected False to be processed as boolean False"
  760. )
  761. assert true_segment.value_type == SegmentType.BOOLEAN
  762. assert false_segment.value_type == SegmentType.BOOLEAN
  763. # Test array of booleans
  764. bool_array_segment = variable_factory.build_segment([True, False, True])
  765. assert bool_array_segment.value_type == SegmentType.ARRAY_BOOLEAN
  766. assert bool_array_segment.value == [True, False, True]