test_metadata.py 40 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012
  1. #
  2. # Copyright 2018-2022 Elyra Authors
  3. #
  4. # Licensed under the Apache License, Version 2.0 (the "License");
  5. # you may not use this file except in compliance with the License.
  6. # You may obtain a copy of the License at
  7. #
  8. # http://www.apache.org/licenses/LICENSE-2.0
  9. #
  10. # Unless required by applicable law or agreed to in writing, software
  11. # distributed under the License is distributed on an "AS IS" BASIS,
  12. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. # See the License for the specific language governing permissions and
  14. # limitations under the License.
  15. #
  16. from collections import OrderedDict
  17. import copy
  18. import json
  19. import os
  20. import shutil
  21. import time
  22. from jsonschema import ValidationError
  23. import pytest
  24. from elyra.metadata.error import MetadataExistsError
  25. from elyra.metadata.error import MetadataNotFoundError
  26. from elyra.metadata.error import SchemaNotFoundError
  27. from elyra.metadata.manager import MetadataManager
  28. from elyra.metadata.metadata import Metadata
  29. from elyra.metadata.schema import METADATA_TEST_SCHEMASPACE
  30. from elyra.metadata.schema import METADATA_TEST_SCHEMASPACE_ID
  31. from elyra.metadata.storage import FileMetadataCache
  32. from elyra.metadata.storage import FileMetadataStore
  33. from elyra.metadata.storage import MetadataStore
  34. from elyra.tests.metadata.test_utils import byo_metadata_json
  35. from elyra.tests.metadata.test_utils import create_instance
  36. from elyra.tests.metadata.test_utils import create_json_file
  37. from elyra.tests.metadata.test_utils import invalid_metadata_json
  38. from elyra.tests.metadata.test_utils import invalid_no_display_name_json
  39. from elyra.tests.metadata.test_utils import MockMetadataStore
  40. from elyra.tests.metadata.test_utils import valid_display_name_json
  41. from elyra.tests.metadata.test_utils import valid_metadata2_json
  42. from elyra.tests.metadata.test_utils import valid_metadata_json
  43. os.environ["METADATA_TESTING"] = "1" # Enable metadata-tests schemaspace
  44. # ########################## MetadataManager Tests ###########################
  45. def test_manager_add_invalid(tests_manager):
  46. with pytest.raises(ValueError):
  47. MetadataManager(schemaspace="invalid")
  48. # Attempt with non Metadata instance
  49. with pytest.raises(TypeError):
  50. tests_manager.create(valid_metadata_json)
  51. # and invalid parameters
  52. with pytest.raises(TypeError):
  53. tests_manager.create(None, invalid_no_display_name_json)
  54. with pytest.raises(ValueError):
  55. tests_manager.create("foo", None)
  56. def test_manager_add_no_name(tests_manager, schemaspace_location):
  57. metadata_name = "valid_metadata_instance"
  58. metadata = Metadata.from_dict(METADATA_TEST_SCHEMASPACE_ID, {**valid_metadata_json})
  59. instance = tests_manager.create(None, metadata)
  60. assert instance is not None
  61. assert instance.name == metadata_name
  62. assert instance.pre_property == instance.metadata.get("required_test")
  63. assert instance.post_property == instance.display_name
  64. # Ensure file was created using store_manager
  65. instance_list = tests_manager.metadata_store.fetch_instances(metadata_name)
  66. assert len(instance_list) == 1
  67. instance = Metadata.from_dict(METADATA_TEST_SCHEMASPACE, instance_list[0])
  68. metadata_location = _compose_instance_location(tests_manager.metadata_store, schemaspace_location, metadata_name)
  69. assert instance.resource == metadata_location
  70. assert instance.pre_property == instance.metadata.get("required_test")
  71. # This will be None because the hooks don't get called when fetched directly from the store
  72. assert instance.post_property is None
  73. # And finally, remove it.
  74. tests_manager.remove(metadata_name)
  75. # Verify removal using metadata_store
  76. with pytest.raises(MetadataNotFoundError):
  77. tests_manager.metadata_store.fetch_instances(metadata_name)
  78. def test_manager_add_short_name(tests_manager, schemaspace_location):
  79. # Found that single character names were failing validation
  80. metadata_name = "a"
  81. metadata = Metadata(**valid_metadata_json)
  82. instance = tests_manager.create(metadata_name, metadata)
  83. assert instance is not None
  84. assert instance.name == metadata_name
  85. # Ensure file was created using store_manager
  86. instance_list = tests_manager.metadata_store.fetch_instances(metadata_name)
  87. assert len(instance_list) == 1
  88. instance = Metadata.from_dict(METADATA_TEST_SCHEMASPACE_ID, instance_list[0])
  89. metadata_location = _compose_instance_location(tests_manager.metadata_store, schemaspace_location, metadata_name)
  90. assert instance.resource == metadata_location
  91. # And finally, remove it.
  92. tests_manager.remove(metadata_name)
  93. # Verify removal using metadata_store
  94. with pytest.raises(MetadataNotFoundError):
  95. tests_manager.metadata_store.fetch_instances(metadata_name)
  96. def test_manager_add_empty_display_name(tests_manager):
  97. # Found that empty display_name values were passing validation, so minLength=1 was added
  98. metadata_name = "empty_display_name"
  99. metadata = Metadata(**valid_metadata_json)
  100. metadata.display_name = ""
  101. with pytest.raises(ValidationError):
  102. tests_manager.create(metadata_name, metadata)
  103. # Ensure file was not created using storage manager
  104. with pytest.raises(MetadataNotFoundError):
  105. tests_manager.metadata_store.fetch_instances(metadata_name)
  106. def test_manager_add_display_name(tests_manager, schemaspace_location):
  107. metadata_display_name = '1 teste "rápido"'
  108. metadata_name = "a_1_teste_rpido"
  109. metadata = Metadata(**valid_display_name_json)
  110. instance = tests_manager.create(None, metadata)
  111. assert instance is not None
  112. assert instance.name == metadata_name
  113. assert instance.display_name == metadata_display_name
  114. # Ensure file was created using store_manager
  115. instance_list = tests_manager.metadata_store.fetch_instances(metadata_name)
  116. assert len(instance_list) == 1
  117. instance = Metadata.from_dict(METADATA_TEST_SCHEMASPACE, instance_list[0])
  118. metadata_location = _compose_instance_location(tests_manager.metadata_store, schemaspace_location, metadata_name)
  119. assert instance.resource == metadata_location
  120. assert instance.display_name == metadata_display_name
  121. # And finally, remove it.
  122. tests_manager.remove(metadata_name)
  123. # Verify removal using metadata_store
  124. with pytest.raises(MetadataNotFoundError):
  125. tests_manager.metadata_store.fetch_instances(metadata_name)
  126. @pytest.mark.parametrize(
  127. "complex_string, valid",
  128. [
  129. ("", False),
  130. (" ", False),
  131. (" starting-whitespace", False),
  132. ("ending-whitespace ", False),
  133. (" whitespace-both-ends ", False),
  134. ("whitespace in between", True),
  135. ("no-whitespace", True),
  136. ],
  137. )
  138. def test_manager_complex_string_schema(tests_manager, schemaspace_location, complex_string, valid):
  139. metadata_name = "valid_metadata_instance"
  140. metadata_dict = {**valid_metadata_json}
  141. metadata_dict["metadata"]["string_complex_test"] = complex_string
  142. metadata = Metadata.from_dict(METADATA_TEST_SCHEMASPACE_ID, metadata_dict)
  143. if not valid:
  144. with pytest.raises(ValidationError):
  145. tests_manager.create(metadata_name, metadata)
  146. else:
  147. instance = tests_manager.create(metadata_name, metadata)
  148. assert instance.metadata.get("string_complex_test") == complex_string
  149. # And finally, remove it.
  150. tests_manager.remove(metadata_name)
  151. # Verify removal using metadata_store
  152. with pytest.raises(MetadataNotFoundError):
  153. tests_manager.metadata_store.fetch_instances(metadata_name)
  154. def test_manager_get_include_invalid(tests_manager):
  155. metadata_list = tests_manager.get_all(include_invalid=False)
  156. assert len(metadata_list) == 2
  157. metadata_list = tests_manager.get_all(include_invalid=True)
  158. assert len(metadata_list) == 5
  159. def test_manager_get_of_schema(tests_manager):
  160. metadata_list = tests_manager.get_all(include_invalid=True)
  161. assert len(metadata_list) == 5
  162. metadata_list = tests_manager.get_all(include_invalid=True, of_schema="metadata-test")
  163. assert len(metadata_list) == 3 # does not include metadata with schema {unknown} and metadata-testxxx
  164. def test_manager_get_bad_json(tests_manager):
  165. with pytest.raises(ValueError) as ve:
  166. tests_manager.get("bad")
  167. assert "JSON failed to load for instance 'bad'" in str(ve.value)
  168. def test_manager_get_all(tests_manager):
  169. metadata_list = tests_manager.get_all()
  170. assert len(metadata_list) == 2
  171. # Ensure name is getting derived from resource and not from contents
  172. for metadata in metadata_list:
  173. if metadata.display_name == "Another Metadata Instance (2)":
  174. assert metadata.name == "another"
  175. else:
  176. assert metadata.name == "valid"
  177. def test_manager_get_none(tests_manager, schemaspace_location):
  178. # Attempt to get a metadata instance using `None` (error expected)
  179. with pytest.raises(ValueError, match="The 'name' parameter requires a value."):
  180. tests_manager.get(name=None)
  181. def test_manager_get_all_none(tests_manager, schemaspace_location):
  182. # Delete the schemaspace contents and attempt listing metadata
  183. _remove_schemaspace(tests_manager.metadata_store, schemaspace_location)
  184. assert tests_manager.schemaspace_exists() is False
  185. _create_schemaspace(tests_manager.metadata_store, schemaspace_location)
  186. assert tests_manager.schemaspace_exists()
  187. metadata_list = tests_manager.get_all()
  188. assert len(metadata_list) == 0
  189. def test_manager_add_remove_valid(tests_manager, schemaspace_location):
  190. metadata_name = "valid_add_remove"
  191. # Remove schemaspace_location and ensure it gets created
  192. _remove_schemaspace(tests_manager.metadata_store, schemaspace_location)
  193. metadata = Metadata(**valid_metadata_json)
  194. instance = tests_manager.create(metadata_name, metadata)
  195. assert instance is not None
  196. # Attempt to create again w/o replace, then replace it.
  197. with pytest.raises(MetadataExistsError):
  198. tests_manager.create(metadata_name, metadata)
  199. instance = tests_manager.update(metadata_name, metadata)
  200. assert instance is not None
  201. # And finally, remove it.
  202. tests_manager.remove(metadata_name)
  203. # Verify removal using metadata_store
  204. with pytest.raises(MetadataNotFoundError):
  205. tests_manager.metadata_store.fetch_instances(metadata_name)
  206. def test_manager_remove_invalid(tests_manager, schemaspace_location):
  207. # Ensure invalid metadata file isn't validated and is removed.
  208. create_instance(tests_manager.metadata_store, schemaspace_location, "remove_invalid", invalid_metadata_json)
  209. metadata_name = "remove_invalid"
  210. tests_manager.remove(metadata_name)
  211. # Verify removal using metadata_store
  212. with pytest.raises(MetadataNotFoundError):
  213. tests_manager.metadata_store.fetch_instances(metadata_name)
  214. def test_manager_remove_missing(tests_manager):
  215. # Ensure removal of missing metadata file is handled.
  216. metadata_name = "missing"
  217. with pytest.raises(MetadataNotFoundError):
  218. tests_manager.remove(metadata_name)
  219. def test_manager_read_valid_by_name(tests_manager, schemaspace_location):
  220. metadata_name = "valid"
  221. some_metadata = tests_manager.get(metadata_name)
  222. assert some_metadata.name == metadata_name
  223. assert some_metadata.schema_name == "metadata-test"
  224. metadata_location = _compose_instance_location(tests_manager.metadata_store, schemaspace_location, metadata_name)
  225. assert metadata_location == some_metadata.resource
  226. def test_manager_read_invalid_by_name(tests_manager):
  227. metadata_name = "invalid"
  228. with pytest.raises(ValidationError):
  229. tests_manager.get(metadata_name)
  230. def test_manager_read_missing_by_name(tests_manager):
  231. metadata_name = "missing"
  232. with pytest.raises(MetadataNotFoundError):
  233. tests_manager.get(metadata_name)
  234. def test_manager_rollback_create(tests_manager):
  235. metadata_name = "rollback_create"
  236. metadata = Metadata(**valid_metadata2_json)
  237. os.environ["METADATA_TEST_HOOK_OP"] = "create" # Tell test class which op to raise
  238. # Create post-save hook will throw NotImplementedError
  239. with pytest.raises(NotImplementedError):
  240. tests_manager.create(metadata_name, metadata)
  241. # Ensure nothing got created
  242. with pytest.raises(MetadataNotFoundError):
  243. tests_manager.get(metadata_name)
  244. os.environ.pop("METADATA_TEST_HOOK_OP") # Restore normal operation
  245. instance = tests_manager.create(metadata_name, metadata)
  246. instance2 = tests_manager.get(metadata_name)
  247. assert instance.name == instance2.name
  248. assert instance.schema_name == instance2.schema_name
  249. assert instance.post_property == instance2.post_property
  250. def test_manager_rollback_update(tests_manager):
  251. metadata_name = "rollback_update"
  252. metadata = Metadata(**valid_metadata2_json)
  253. # Create the instance
  254. instance = tests_manager.create(metadata_name, metadata)
  255. original_display_name = instance.display_name
  256. instance.display_name = "Updated_" + original_display_name
  257. os.environ["METADATA_TEST_HOOK_OP"] = "update" # Tell test class which op to raise
  258. # Update post-save hook will throw ModuleNotFoundError
  259. with pytest.raises(ModuleNotFoundError):
  260. tests_manager.update(metadata_name, instance)
  261. # Ensure the display_name is still the original value.
  262. instance2 = tests_manager.get(metadata_name)
  263. assert instance2.display_name == original_display_name
  264. os.environ.pop("METADATA_TEST_HOOK_OP") # Restore normal operation
  265. # Ensure we can still update
  266. instance = tests_manager.update(metadata_name, instance)
  267. assert instance.display_name == "Updated_" + original_display_name
  268. def test_manager_rollback_delete(tests_manager):
  269. metadata_name = "rollback_delete"
  270. metadata = Metadata(**valid_metadata2_json)
  271. # Create the instance
  272. instance = tests_manager.create(metadata_name, metadata)
  273. os.environ["METADATA_TEST_HOOK_OP"] = "delete" # Tell test class which op to raise
  274. # Delete post-save hook will throw FileNotFoundError
  275. with pytest.raises(FileNotFoundError):
  276. tests_manager.remove(metadata_name)
  277. # Ensure the instance still exists
  278. instance2 = tests_manager.get(metadata_name)
  279. assert instance2.display_name == instance.display_name
  280. os.environ.pop("METADATA_TEST_HOOK_OP") # Restore normal operation
  281. # Ensure we can still delete
  282. tests_manager.remove(metadata_name)
  283. # Ensure the instance was deleted
  284. with pytest.raises(MetadataNotFoundError):
  285. tests_manager.get(metadata_name)
  286. def test_manager_hierarchy_fetch(tests_hierarchy_manager, factory_location, shared_location, schemaspace_location):
  287. # fetch initial instances, only factory data should be present
  288. metadata_list = tests_hierarchy_manager.get_all()
  289. assert len(metadata_list) == 3
  290. # Ensure these are all factory instances
  291. for metadata in metadata_list:
  292. assert metadata.display_name == "factory"
  293. byo_3 = tests_hierarchy_manager.get("byo_3")
  294. assert byo_3.resource.startswith(str(factory_location))
  295. # add a shared instance and confirm list count is still the same, but
  296. # only that instance is present in shared directory...
  297. byo_instance = byo_metadata_json
  298. byo_instance["display_name"] = "shared"
  299. create_json_file(shared_location, "byo_3.json", byo_instance)
  300. metadata_list = tests_hierarchy_manager.get_all()
  301. assert len(metadata_list) == 3
  302. # Ensure the proper instances exist
  303. for metadata in metadata_list:
  304. if metadata.name == "byo_3":
  305. assert metadata.display_name == "shared"
  306. else:
  307. assert metadata.display_name == "factory"
  308. byo_3 = tests_hierarchy_manager.get("byo_3")
  309. assert byo_3.resource.startswith(str(shared_location))
  310. # add a shared and a user instance confirm list count is still the same, but
  311. # both the user and shared instances are correct.
  312. byo_instance = byo_metadata_json
  313. byo_instance["display_name"] = "shared"
  314. create_json_file(shared_location, "byo_2.json", byo_instance)
  315. byo_instance["display_name"] = "user"
  316. create_json_file(schemaspace_location, "byo_2.json", byo_instance)
  317. metadata_list = tests_hierarchy_manager.get_all()
  318. assert len(metadata_list) == 3
  319. # Ensure the proper instances exist
  320. for metadata in metadata_list:
  321. if metadata.name == "byo_1":
  322. assert metadata.display_name == "factory"
  323. if metadata.name == "byo_2":
  324. assert metadata.display_name == "user"
  325. if metadata.name == "byo_3":
  326. assert metadata.display_name == "shared"
  327. byo_2 = tests_hierarchy_manager.get("byo_2")
  328. assert byo_2.resource.startswith(str(schemaspace_location))
  329. # delete the user instance and ensure its shared copy is now exposed
  330. tests_hierarchy_manager.metadata_store.delete_instance(byo_2.to_dict())
  331. metadata_list = tests_hierarchy_manager.get_all()
  332. assert len(metadata_list) == 3
  333. # Ensure the proper instances exist
  334. for metadata in metadata_list:
  335. if metadata.name == "byo_1":
  336. assert metadata.display_name == "factory"
  337. if metadata.name == "byo_2":
  338. assert metadata.display_name == "shared"
  339. if metadata.name == "byo_3":
  340. assert metadata.display_name == "shared"
  341. byo_2 = tests_hierarchy_manager.get("byo_2")
  342. assert byo_2.resource.startswith(str(shared_location))
  343. # delete both shared copies and ensure only factory is left
  344. # Note: because we can only delete user instances via the APIs, this
  345. # code is metadata_store-sensitive. If other stores implement this
  346. # hierachy scheme, similar storage-specific code will be necessary.
  347. if isinstance(tests_hierarchy_manager.metadata_store, FileMetadataStore):
  348. os.remove(os.path.join(shared_location, "byo_2.json"))
  349. os.remove(os.path.join(shared_location, "byo_3.json"))
  350. # fetch initial instances, only factory data should be present
  351. metadata_list = tests_hierarchy_manager.get_all()
  352. assert len(metadata_list) == 3
  353. # Ensure these are all factory instances
  354. for metadata in metadata_list:
  355. assert metadata.display_name == "factory"
  356. byo_2 = tests_hierarchy_manager.get("byo_2")
  357. assert byo_2.resource.startswith(str(factory_location))
  358. def test_manager_hierarchy_create(tests_hierarchy_manager, schemaspace_location):
  359. # Note, this is really more of an update test (replace = True), since you cannot "create" an
  360. # instance if it already exists - which, in this case, it exists in the factory area
  361. metadata = Metadata(**byo_metadata_json)
  362. metadata.display_name = "user"
  363. with pytest.raises(MetadataExistsError):
  364. tests_hierarchy_manager.create("byo_2", metadata)
  365. instance = tests_hierarchy_manager.update("byo_2", metadata)
  366. assert instance is not None
  367. assert instance.resource.startswith(str(schemaspace_location))
  368. metadata_list = tests_hierarchy_manager.get_all()
  369. assert len(metadata_list) == 3
  370. # Ensure the proper instances exist
  371. for metadata in metadata_list:
  372. if metadata.name == "byo_1":
  373. assert metadata.display_name == "factory"
  374. if metadata.name == "byo_2":
  375. assert metadata.display_name == "user"
  376. if metadata.name == "byo_3":
  377. assert metadata.display_name == "factory"
  378. byo_2 = tests_hierarchy_manager.get("byo_2")
  379. assert byo_2.resource.startswith(str(schemaspace_location))
  380. metadata = Metadata(**byo_metadata_json)
  381. metadata.display_name = "user"
  382. instance = tests_hierarchy_manager.update("byo_3", metadata)
  383. assert instance is not None
  384. assert instance.resource.startswith(str(schemaspace_location))
  385. metadata_list = tests_hierarchy_manager.get_all()
  386. assert len(metadata_list) == 3
  387. # Ensure the proper instances exist
  388. for metadata in metadata_list:
  389. if metadata.name == "byo_1":
  390. assert metadata.display_name == "factory"
  391. if metadata.name == "byo_2":
  392. assert metadata.display_name == "user"
  393. if metadata.name == "byo_3":
  394. assert metadata.display_name == "user"
  395. byo_2 = tests_hierarchy_manager.get("byo_2")
  396. assert byo_2.resource.startswith(str(schemaspace_location))
  397. def test_manager_hierarchy_update(tests_hierarchy_manager, factory_location, shared_location, schemaspace_location):
  398. # Create a copy of existing factory instance and ensure its in the user area
  399. byo_2 = tests_hierarchy_manager.get("byo_2")
  400. assert byo_2.resource.startswith(str(factory_location))
  401. byo_2.display_name = "user"
  402. with pytest.raises(MetadataExistsError):
  403. tests_hierarchy_manager.create("byo_2", byo_2)
  404. # Repeat with replacement enabled
  405. instance = tests_hierarchy_manager.update("byo_2", byo_2)
  406. assert instance is not None
  407. assert instance.resource.startswith(str(schemaspace_location))
  408. # now "slip in" a shared instance behind the updated version and ensure
  409. # the updated version is what's returned.
  410. byo_instance = byo_metadata_json
  411. byo_instance["display_name"] = "shared"
  412. create_json_file(shared_location, "byo_2.json", byo_instance)
  413. byo_2 = tests_hierarchy_manager.get("byo_2")
  414. assert byo_2.resource.startswith(str(schemaspace_location))
  415. # now remove the updated instance and ensure the shared instance appears
  416. tests_hierarchy_manager.remove("byo_2")
  417. byo_2 = tests_hierarchy_manager.get("byo_2")
  418. assert byo_2.resource.startswith(str(shared_location))
  419. def test_manager_update(tests_hierarchy_manager, schemaspace_location):
  420. # Create some metadata, then attempt to update it with a known schema violation
  421. # and ensure the previous copy still exists...
  422. # Create a user instance...
  423. metadata = Metadata.from_dict(METADATA_TEST_SCHEMASPACE_ID, {**byo_metadata_json})
  424. metadata.display_name = "user1"
  425. instance = tests_hierarchy_manager.create("update", metadata)
  426. assert instance is not None
  427. assert instance.resource.startswith(str(schemaspace_location))
  428. assert instance.pre_property == instance.metadata["required_test"]
  429. assert instance.post_property == instance.display_name
  430. # Now update the user instance - add a field - and ensure that the original renamed file is not present.
  431. instance2 = tests_hierarchy_manager.get("update")
  432. instance2.display_name = "user2"
  433. instance2.metadata["number_range_test"] = 7
  434. instance = tests_hierarchy_manager.update("update", instance2)
  435. assert instance.pre_property == instance.metadata["required_test"]
  436. assert instance.post_property == instance2.display_name
  437. _ensure_single_instance(tests_hierarchy_manager, schemaspace_location, "update.json")
  438. instance2 = tests_hierarchy_manager.get("update")
  439. assert instance2.display_name == "user2"
  440. assert instance2.metadata["number_range_test"] == 7
  441. def test_manager_default_value(tests_hierarchy_manager, schemaspace_location):
  442. # Create some metadata, then attempt to update it with a known schema violation
  443. # and ensure the previous copy still exists...
  444. # Create a user instance...
  445. metadata = Metadata.from_dict(METADATA_TEST_SCHEMASPACE, {**byo_metadata_json})
  446. metadata.display_name = "user1"
  447. instance = tests_hierarchy_manager.create("default_value", metadata)
  448. assert instance.metadata["number_default_test"] == 42 # Ensure default value was applied when not present
  449. instance2 = tests_hierarchy_manager.get("default_value")
  450. instance2.metadata["number_default_test"] = 37
  451. tests_hierarchy_manager.update("default_value", instance2)
  452. instance3 = tests_hierarchy_manager.get("default_value")
  453. assert instance3.metadata["number_default_test"] == 37
  454. # Now remove the updated value and ensure it comes back with the default
  455. instance3.metadata.pop("number_default_test")
  456. assert "number_default_test" not in instance3.metadata
  457. tests_hierarchy_manager.update("default_value", instance3)
  458. instance4 = tests_hierarchy_manager.get("default_value")
  459. assert instance4.metadata["number_default_test"] == 42
  460. def test_manager_bad_update(tests_hierarchy_manager, schemaspace_location):
  461. # Create some metadata, then attempt to update it with a known schema violation
  462. # and ensure the previous copy still exists...
  463. # Create a user instance...
  464. metadata = Metadata(**byo_metadata_json)
  465. metadata.display_name = "user1"
  466. instance = tests_hierarchy_manager.create("bad_update", metadata)
  467. assert instance is not None
  468. assert instance.resource.startswith(str(schemaspace_location))
  469. # Now, attempt to update the user instance, but include a schema violation.
  470. # Verify the update failed, but also ensure the previous instance is still there.
  471. instance2 = tests_hierarchy_manager.get("bad_update")
  472. instance2.display_name = "user2"
  473. instance2.metadata["number_range_test"] = 42 # number is out of range
  474. with pytest.raises(ValidationError):
  475. tests_hierarchy_manager.update("bad_update", instance2)
  476. _ensure_single_instance(tests_hierarchy_manager, schemaspace_location, "bad_update.json")
  477. instance2 = tests_hierarchy_manager.get("bad_update")
  478. assert instance2.display_name == instance.display_name
  479. assert "number_range_test" not in instance2.metadata
  480. # Now try update without providing a name, ValueError expected
  481. instance2 = tests_hierarchy_manager.get("bad_update")
  482. instance2.display_name = "user update with no name"
  483. with pytest.raises(ValueError):
  484. tests_hierarchy_manager.update(None, instance2)
  485. _ensure_single_instance(tests_hierarchy_manager, schemaspace_location, "bad_update.json")
  486. def test_manager_hierarchy_remove(tests_hierarchy_manager, factory_location, shared_location, schemaspace_location):
  487. # Create additional instances in shared and user areas
  488. byo_2 = byo_metadata_json
  489. byo_2["display_name"] = "shared"
  490. create_json_file(shared_location, "byo_2.json", byo_2)
  491. metadata = Metadata(**byo_metadata_json)
  492. metadata.display_name = "user"
  493. instance = tests_hierarchy_manager.update("byo_2", metadata)
  494. assert instance is not None
  495. assert instance.resource.startswith(str(schemaspace_location))
  496. # Confirm on in user is found...
  497. metadata_list = tests_hierarchy_manager.get_all()
  498. assert len(metadata_list) == 3
  499. # Ensure the proper instances exist
  500. for metadata in metadata_list:
  501. if metadata.name == "byo_1":
  502. assert metadata.display_name == "factory"
  503. if metadata.name == "byo_2":
  504. assert metadata.display_name == "user"
  505. if metadata.name == "byo_3":
  506. assert metadata.display_name == "factory"
  507. byo_2 = tests_hierarchy_manager.get("byo_2")
  508. assert byo_2.resource.startswith(str(schemaspace_location))
  509. # Now remove instance. Should be allowed since it resides in user area
  510. tests_hierarchy_manager.remove("byo_2")
  511. _ensure_single_instance(tests_hierarchy_manager, schemaspace_location, "byo_2.json", expected_count=0)
  512. # Attempt to remove instance from shared area and its protected
  513. with pytest.raises(PermissionError) as pe:
  514. tests_hierarchy_manager.remove("byo_2")
  515. assert "Removal of instance 'byo_2'" in str(pe.value)
  516. # Ensure the one that exists is the one in the shared area
  517. byo_2 = tests_hierarchy_manager.get("byo_2")
  518. assert byo_2.resource.startswith(str(shared_location))
  519. # Attempt to remove instance from factory area and its protected as well
  520. with pytest.raises(PermissionError) as pe:
  521. tests_hierarchy_manager.remove("byo_1")
  522. assert "Removal of instance 'byo_1'" in str(pe.value)
  523. byo_1 = tests_hierarchy_manager.get("byo_1")
  524. assert byo_1.resource.startswith(str(factory_location))
  525. @pytest.mark.skipif(
  526. os.getenv("TEST_VALIDATION_PERFORMANCE", "0") != "1",
  527. reason="test_validation_performance - enable via env TEST_VALIDATION_PERFORMANCE=1",
  528. )
  529. def test_validation_performance():
  530. import psutil
  531. metadata_mgr = MetadataManager(schemaspace=METADATA_TEST_SCHEMASPACE)
  532. metadata_dict = {**valid_metadata_json}
  533. metadata = Metadata.from_dict(METADATA_TEST_SCHEMASPACE_ID, metadata_dict)
  534. process = psutil.Process(os.getpid())
  535. # warm up
  536. metadata_mgr.validate("perf_test", metadata)
  537. iterations = 10000
  538. memory_start = process.memory_info()
  539. t0 = time.time()
  540. for _ in range(0, iterations):
  541. metadata_mgr.validate("perf_test", metadata)
  542. t1 = time.time()
  543. memory_end = process.memory_info()
  544. diff = (memory_end.rss - memory_start.rss) / 1024
  545. print(
  546. f"Memory: {diff:,} kb, Start: {memory_start.rss / 1024 / 1024:,.3f} mb, "
  547. f"End: {memory_end.rss / 1024 / 1024:,.3f} mb., "
  548. f"Elapsed time: {t1-t0:.3f}s over {iterations} iterations."
  549. )
  550. # ########################## MetadataStore Tests ###########################
  551. def test_store_schemaspace(store_manager, schemaspace_location):
  552. # Delete the metadata dir contents and attempt listing metadata
  553. _remove_schemaspace(store_manager, schemaspace_location)
  554. assert store_manager.schemaspace_exists() is False
  555. # create some metadata
  556. store_manager.store_instance("ensure_schemaspace_exists", Metadata(**valid_metadata_json).prepare_write())
  557. assert store_manager.schemaspace_exists()
  558. def test_store_fetch_instances(store_manager):
  559. instances_list = store_manager.fetch_instances()
  560. assert len(instances_list) == 4
  561. def test_store_fetch_no_schemaspace(store_manager, schemaspace_location):
  562. # Delete the schemaspace contents and attempt listing metadata
  563. _remove_schemaspace(store_manager, schemaspace_location)
  564. instance_list = store_manager.fetch_instances()
  565. assert len(instance_list) == 0
  566. def test_store_fetch_by_name(store_manager):
  567. metadata_name = "valid"
  568. instance_list = store_manager.fetch_instances(name=metadata_name)
  569. assert instance_list[0].get("name") == metadata_name
  570. def test_store_fetch_missing(store_manager):
  571. metadata_name = "missing"
  572. with pytest.raises(MetadataNotFoundError):
  573. store_manager.fetch_instances(name=metadata_name)
  574. def test_store_store_instance(store_manager, schemaspace_location):
  575. # Remove schemaspace to test raw creation and confirm perms
  576. _remove_schemaspace(store_manager, schemaspace_location)
  577. metadata_name = "persist"
  578. metadata = Metadata(**valid_metadata_json)
  579. metadata_dict = metadata.prepare_write()
  580. instance = store_manager.store_instance(metadata_name, metadata_dict)
  581. assert instance is not None
  582. if isinstance(store_manager, FileMetadataStore):
  583. dir_mode = oct(os.stat(schemaspace_location).st_mode & 0o777777) # Be sure to include other attributes
  584. assert dir_mode == "0o40700" # and ensure this is a directory with only rwx by owner enabled
  585. # Ensure file was created
  586. metadata_file = os.path.join(schemaspace_location, "persist.json")
  587. assert os.path.exists(metadata_file)
  588. file_mode = oct(os.stat(metadata_file).st_mode & 0o777777) # Be sure to include other attributes
  589. assert file_mode == "0o100600" # and ensure this is a regular file with only rw by owner enabled
  590. with open(metadata_file, "r", encoding="utf-8") as f:
  591. valid_add = json.loads(f.read())
  592. assert "resource" not in valid_add
  593. assert "name" not in valid_add
  594. assert "display_name" in valid_add
  595. assert valid_add["display_name"] == "valid metadata instance"
  596. assert "schema_name" in valid_add
  597. assert valid_add["schema_name"] == "metadata-test"
  598. # Attempt to create again w/o replace, then replace it.
  599. with pytest.raises(MetadataExistsError):
  600. store_manager.store_instance(metadata_name, metadata.prepare_write())
  601. metadata.metadata["number_range_test"] = 10
  602. instance = store_manager.store_instance(metadata_name, metadata.prepare_write(), for_update=True)
  603. assert instance is not None
  604. assert instance.get("metadata")["number_range_test"] == 10
  605. def test_store_delete_instance(store_manager, schemaspace_location):
  606. metadata_name = "valid"
  607. instance_list = store_manager.fetch_instances(name=metadata_name)
  608. metadata = instance_list[0]
  609. store_manager.delete_instance(metadata)
  610. with pytest.raises(MetadataNotFoundError):
  611. store_manager.fetch_instances(name=metadata_name)
  612. if isinstance(store_manager, FileMetadataStore):
  613. # Ensure file was physically deleted
  614. metadata_file = os.path.join(schemaspace_location, "valid.json")
  615. assert not os.path.exists(metadata_file)
  616. # ########################## Error Tests ###########################
  617. def test_error_metadata_not_found():
  618. schemaspace = METADATA_TEST_SCHEMASPACE
  619. resource = "missing_metadata"
  620. try:
  621. raise MetadataNotFoundError(schemaspace, resource)
  622. except MetadataNotFoundError as mnfe:
  623. assert str(mnfe) == f"No such instance named '{resource}' was found in the {schemaspace} schemaspace."
  624. def test_error_metadata_exists():
  625. schemaspace = METADATA_TEST_SCHEMASPACE
  626. resource = "existing_metadata"
  627. try:
  628. raise MetadataExistsError(schemaspace, resource)
  629. except MetadataExistsError as mee:
  630. assert str(mee) == f"An instance named '{resource}' already exists in the {schemaspace} schemaspace."
  631. def test_error_schema_not_found():
  632. schemaspace = METADATA_TEST_SCHEMASPACE
  633. resource = "missing_schema"
  634. try:
  635. raise SchemaNotFoundError(schemaspace, resource)
  636. except SchemaNotFoundError as snfe:
  637. assert str(snfe) == f"No such schema named '{resource}' was found in the {schemaspace} schemaspace."
  638. def test_cache_init():
  639. FileMetadataCache.clear_instance()
  640. cache = FileMetadataCache.instance()
  641. assert cache.max_size == 128
  642. FileMetadataCache.clear_instance()
  643. cache = FileMetadataCache.instance(max_size=3)
  644. assert cache.max_size == 3
  645. FileMetadataCache.clear_instance()
  646. def test_cache_ops(tests_manager, schemaspace_location):
  647. FileMetadataCache.clear_instance()
  648. test_items = OrderedDict({"a": 3, "b": 4, "c": 5, "d": 6, "e": 7})
  649. test_resources = {}
  650. test_content = {}
  651. # Setup test data
  652. for name, number in test_items.items():
  653. content = copy.deepcopy(valid_metadata_json)
  654. content["display_name"] = name
  655. content["metadata"]["number_range_test"] = number
  656. resource = create_instance(tests_manager.metadata_store, schemaspace_location, name, content)
  657. test_resources[name] = resource
  658. test_content[name] = content
  659. # Add initial entries
  660. cache = FileMetadataCache.instance(max_size=3)
  661. for name in test_items: # Add the items to the cache
  662. cache.add_item(test_resources[name], test_content[name])
  663. assert len(cache) == 3
  664. assert cache.trims == 2
  665. assert cache.get_item(test_resources.get("a")) is None
  666. assert cache.get_item(test_resources.get("b")) is None
  667. assert cache.get_item(test_resources.get("c")) is not None
  668. assert cache.get_item(test_resources.get("d")) is not None
  669. assert cache.get_item(test_resources.get("e")) is not None
  670. assert cache.misses == 2
  671. assert cache.hits == 3
  672. cache.add_item(test_resources.get("a"), test_content.get("a"))
  673. assert len(cache) == 3
  674. assert cache.trims == 3
  675. assert cache.get_item(test_resources.get("c")) is None # since 'c' was aged out
  676. assert cache.get_item(test_resources.get("a")) is not None
  677. assert cache.misses == 3
  678. assert cache.hits == 4
  679. e_val = cache.remove_item(test_resources.get("e"))
  680. assert len(cache) == 2
  681. assert e_val["metadata"]["number_range_test"] == test_items.get("e")
  682. assert cache.get_item(test_resources.get("e")) is None
  683. assert cache.misses == 4
  684. assert cache.hits == 4
  685. assert cache.trims == 3
  686. a_val = cache.remove_item(test_resources.get("a"))
  687. assert len(cache) == 1
  688. assert a_val["metadata"]["number_range_test"] == test_items.get("a")
  689. assert cache.get_item(test_resources.get("a")) is None
  690. assert cache.misses == 5
  691. assert cache.hits == 4
  692. assert cache.trims == 3
  693. d_val = cache.get_item(test_resources.get("d"))
  694. assert len(cache) == 1
  695. assert d_val["metadata"]["number_range_test"] == test_items.get("d")
  696. assert cache.misses == 5
  697. assert cache.hits == 5
  698. assert cache.trims == 3
  699. if isinstance(tests_manager.metadata_store, FileMetadataStore):
  700. # Exercise delete from filesystem and ensure cached item is removed
  701. assert os.path.exists(test_resources.get("d"))
  702. os.remove(test_resources.get("d"))
  703. recorded = 0.0
  704. for i in range(1, 6): # allow up to a second for delete to record in cache
  705. time.sleep(0.2) # initial tests are showing only one sub-second delay is necessary
  706. recorded += 0.2
  707. if len(cache) == 0:
  708. break
  709. assert len(cache) == 0
  710. print(f"\ntest_cache_ops: Delete recorded after {recorded} seconds")
  711. assert cache.get_item(test_resources.get("d")) is None
  712. assert cache.misses == 6
  713. assert cache.hits == 5
  714. assert cache.trims == 3
  715. def test_cache_disabled(tests_manager, schemaspace_location):
  716. FileMetadataCache.clear_instance()
  717. test_items = OrderedDict({"a": 3, "b": 4, "c": 5, "d": 6, "e": 7})
  718. test_resources = {}
  719. test_content = {}
  720. # Setup test data
  721. for name, number in test_items.items():
  722. content = copy.deepcopy(valid_metadata_json)
  723. content["display_name"] = name
  724. content["metadata"]["number_range_test"] = number
  725. resource = create_instance(tests_manager.metadata_store, schemaspace_location, name, content)
  726. test_resources[name] = resource
  727. test_content[name] = content
  728. # Add initial entries
  729. cache = FileMetadataCache.instance(max_size=3, enabled=False)
  730. assert hasattr(cache, "observer") is False
  731. assert hasattr(cache, "observed_dirs") is False
  732. for name in test_items: # Add the items to the cache
  733. cache.add_item(test_resources[name], test_content[name])
  734. assert len(cache) == 0
  735. assert cache.trims == 0
  736. assert cache.get_item(test_resources.get("a")) is None
  737. assert cache.get_item(test_resources.get("b")) is None
  738. assert cache.get_item(test_resources.get("c")) is None
  739. assert cache.get_item(test_resources.get("d")) is None
  740. assert cache.get_item(test_resources.get("e")) is None
  741. assert cache.misses == 0
  742. assert cache.hits == 0
  743. def _ensure_single_instance(tests_hierarchy_manager, schemaspace_location, name, expected_count=1):
  744. """Because updates can trigger the copy of the original, this methods ensures that
  745. only the named instance (`name`) exists after the operation. The expected_count
  746. can be altered so that it can also be used to ensure clean removals.
  747. """
  748. if isinstance(tests_hierarchy_manager.metadata_store, FileMetadataStore):
  749. # Ensure only the actual metadata file exists. The renamed instance will start with 'name' but have
  750. # a timestamp appended to it.
  751. count = 0
  752. actual = 0
  753. for f in os.listdir(str(schemaspace_location)):
  754. if name in f:
  755. count = count + 1
  756. if name == f:
  757. actual = actual + 1
  758. assert count == expected_count, "Temporarily renamed file was not removed"
  759. assert actual == expected_count
  760. def _create_schemaspace(store_manager: MetadataStore, schemaspace_location: str):
  761. """Creates schemaspace in a storage-independent manner"""
  762. if isinstance(store_manager, FileMetadataStore):
  763. os.makedirs(schemaspace_location)
  764. elif isinstance(store_manager, MockMetadataStore):
  765. instances = store_manager.instances
  766. if instances is None:
  767. setattr(store_manager, "instances", dict())
  768. def _remove_schemaspace(store_manager: MetadataStore, schemaspace_location: str):
  769. """Removes schemaspace in a storage-independent manner"""
  770. if isinstance(store_manager, FileMetadataStore):
  771. shutil.rmtree(schemaspace_location)
  772. elif isinstance(store_manager, MockMetadataStore):
  773. setattr(store_manager, "instances", None)
  774. def _compose_instance_location(store_manager: MetadataStore, location: str, name: str) -> str:
  775. """Compose location of the named instance in a storage-independent manner"""
  776. if isinstance(store_manager, FileMetadataStore):
  777. location = os.path.join(location, f"{name}.json")
  778. elif isinstance(store_manager, MockMetadataStore):
  779. location = None
  780. return location