123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561 |
- #
- # Copyright 2018-2022 Elyra Authors
- #
- # Licensed under the Apache License, Version 2.0 (the "License");
- # you may not use this file except in compliance with the License.
- # You may obtain a copy of the License at
- #
- # http://www.apache.org/licenses/LICENSE-2.0
- #
- # Unless required by applicable law or agreed to in writing, software
- # distributed under the License is distributed on an "AS IS" BASIS,
- # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- # See the License for the specific language governing permissions and
- # limitations under the License.
- #
- import copy
- import errno
- import io
- import json
- import os
- from typing import Any
- from typing import Dict
- from typing import List
- from typing import Optional
- from traitlets.config import LoggingConfigurable
- from elyra.metadata.error import MetadataExistsError
- from elyra.metadata.error import MetadataNotFoundError
- from elyra.metadata.metadata import Metadata
- from elyra.metadata.schema import METADATA_TEST_SCHEMASPACE
- from elyra.metadata.schema import METADATA_TEST_SCHEMASPACE_ID
- from elyra.metadata.schema import Schemaspace
- from elyra.metadata.schema import SchemasProvider
- from elyra.metadata.storage import FileMetadataStore
- from elyra.metadata.storage import MetadataStore
- NON_EXISTENT_SCHEMASPACE_ID = "9ab68f6f-000c-470e-814d-2af59ea0956e"
- valid_metadata_json = {
- "schema_name": "metadata-test",
- "display_name": "valid metadata instance",
- "metadata": {
- "uri_test": "http://localhost:31823/v1/models?version=2017-02-13",
- "number_range_test": 8,
- "required_test": "required_value",
- },
- }
- valid_metadata2_json = {
- "schema_name": "metadata-test2",
- "display_name": "valid metadata2 instance",
- "metadata": {
- "uri_test": "http://localhost:31823/v1/models?version=2017-02-13",
- "number_range_test": 8,
- "required_test": "required_value",
- },
- }
- another_metadata_json = {
- "schema_name": "metadata-test",
- "name": "another_instance",
- "display_name": "Another Metadata Instance (2)",
- "metadata": {"uri_test": "http://localhost:8081/", "required_test": "required_value"},
- }
- invalid_metadata_json = {
- "schema_name": "metadata-test",
- "display_name": "Invalid Metadata Instance - bad uri",
- "metadata": {"uri_test": "//localhost:8081/", "required_test": "required_value"},
- }
- invalid_json = "{\
- 'schema_name': 'metadata-test',\
- 'display_name': 'Invalid Metadata Instance - missing comma'\
- 'metadata': {\
- 'uri_test': '//localhost:8081/',\
- 'required_test': 'required_value'\
- }\
- }"
- invalid_no_display_name_json = {
- "schema_name": "metadata-test",
- "metadata": {"uri_test": "//localhost:8081/", "required_test": "required_value"},
- }
- valid_display_name_json = {
- "schema_name": "metadata-test",
- "display_name": '1 teste "rápido"',
- "metadata": {"required_test": "required_value"},
- }
- invalid_schema_name_json = {
- "schema_name": "metadata-testxxx",
- "display_name": "invalid schema name",
- "metadata": {
- "uri_test": "http://localhost:31823/v1/models?version=2017-02-13",
- "number_range_test": 8,
- "required_test": "required_value",
- },
- }
- # Contains all values corresponding to test schema...
- complete_metadata_json = {
- "schema_name": "metadata-test",
- "display_name": "complete metadata instance",
- "metadata": {
- "required_test": "required_value",
- "uri_test": "http://localhost:31823/v1/models?version=2017-02-13",
- "integer_exclusivity_test": 7,
- "integer_multipleOf7_test": 42,
- "number_range_test": 8,
- # purposely missing "number_default_test": 42
- "const_test": 3.14,
- "string_length_test": "1234567",
- "enum_test": "rocks",
- "array_test": ["elyra", "rocks", "the", "world"],
- "object_test": {
- "property1": "first prop",
- "property2": "second_prop",
- "property3": "third prop",
- "property4": "fourth prop",
- },
- "boolean_test": True,
- "null_test": "null",
- },
- }
- # Minimal json to be built upon for each property test. Only
- # required values are specified.
- minimal_metadata_json = {
- "schema_name": "metadata-test",
- "display_name": "complete metadata instance",
- "metadata": {"required_test": "required_value"},
- }
- # Bring-your-own metadata template used to test hierarchical writes. The
- # display_name field will be updated to reflect the location's instance
- # in the hierarchy
- byo_metadata_json = {
- "schema_name": "metadata-test",
- "display_name": "location",
- "metadata": {"required_test": "required_value"},
- }
- # Used in test_install_and_replace_complex to test --file option
- one_of_json = {
- "schema_name": "metadata-test",
- "display_name": "oneOf Testing",
- "metadata": {
- "required_test": "required_value",
- "oneOf_test": {"obj2_prop1": 42, "obj2_prop2": 24, "obj_switch": "obj2"},
- },
- }
- # Used in test_install_and_replace_complex to test --allOf_test option (i.e., ovp option)
- all_of_json = {
- "obj1_prop1": "allOf-test-val1",
- "obj1_prop2": "allOf-test-val2",
- "obj1_switch": "obj1",
- "obj2_prop1": 42,
- "obj2_prop2": 24,
- "obj2_switch": "obj2",
- "obj3_prop1": 42.7,
- "obj3_prop2": True,
- "obj3_switch": "obj3",
- }
- def create_json_file(location: Any, file_name: str, content: Dict) -> str:
- return create_file(location, file_name, json.dumps(content))
- def create_file(location: Any, file_name: str, content: str) -> str:
- try:
- os.makedirs(location)
- except OSError as e:
- if e.errno != errno.EEXIST:
- raise
- resource = os.path.join(location, file_name)
- with open(resource, "w", encoding="utf-8") as f:
- f.write(content)
- return resource
- def create_instance(metadata_store: MetadataStore, location: str, name: str, content: Any) -> str:
- resource = name
- if isinstance(metadata_store, FileMetadataStore):
- if isinstance(content, dict):
- create_json_file(location, name + ".json", content)
- else:
- create_file(location, name + ".json", content)
- resource = os.path.join(location, name + ".json")
- elif isinstance(metadata_store, MockMetadataStore):
- instances = metadata_store.instances
- if instances is None:
- setattr(metadata_store, "instances", dict())
- instances = metadata_store.instances
- if not isinstance(content, dict):
- content = {"display_name": name, "reason": f"JSON failed to load for instance '{name}'"}
- instances[name] = content
- return resource
- def get_instance(instances, field, value):
- """Given a list of instances (dicts), return the dictionary where field == value."""
- for inst in instances:
- if inst[field] == value:
- return inst
- assert False, f"Value '{value}' for field '{field}' was not found in instances!"
- class PropertyTester(object):
- """Helper class used by elyra_md tests to test each of the properties in the test.json schema."""
- name = None # prefixed with 'test_' is test name, post-fixed with '_test' is schema property name
- negative_res = False # expected success of first test
- negative_value = None # value to use in first test (usually negative test)
- negative_stdout = None # expected string to find in first test's stdout
- negative_stderr = None # expected string to find in second test's stdout
- positive_res = True # expected success of second test
- positive_value = None # value to use in second test (usually successful)
- def __init__(self, name):
- self.name = name
- self.property = name + "_test"
- def run(self, script_runner, mock_data_dir):
- expected_file = os.path.join(mock_data_dir, "metadata", METADATA_TEST_SCHEMASPACE, self.name + ".json")
- # Cleanup from any potential previous failures
- if os.path.exists(expected_file):
- os.remove(expected_file)
- # First test
- ret = script_runner.run(
- "elyra-metadata",
- "install",
- METADATA_TEST_SCHEMASPACE,
- "--schema_name=metadata-test",
- "--name=" + self.name,
- "--display_name=" + self.name,
- "--required_test=required_value",
- "--" + self.property + "=" + str(self.negative_value),
- )
- assert ret.success is self.negative_res
- assert self.negative_stdout in ret.stdout
- assert self.negative_stderr in ret.stderr
- # Second test
- ret = script_runner.run(
- "elyra-metadata",
- "install",
- METADATA_TEST_SCHEMASPACE,
- "--schema_name=metadata-test",
- "--name=" + self.name,
- "--display_name=" + self.name,
- "--required_test=required_value",
- "--" + self.property + "=" + str(self.positive_value),
- )
- assert ret.success is self.positive_res
- assert "Metadata instance '" + self.name + "' for schema 'metadata-test' has been written" in ret.stdout
- assert os.path.isdir(os.path.join(mock_data_dir, "metadata", METADATA_TEST_SCHEMASPACE))
- assert os.path.isfile(expected_file)
- with open(expected_file, "r") as fd:
- instance_json = json.load(fd)
- assert instance_json["schema_name"] == "metadata-test"
- assert instance_json["display_name"] == self.name
- assert instance_json["metadata"][self.property] == self.positive_value
- class MockMetadataStore(MetadataStore):
- """Hypothetical class used to demonstrate (and test) use of custom storage classes."""
- def __init__(self, schemaspace: str, **kwargs: Any) -> None:
- super().__init__(schemaspace, **kwargs)
- self.instances = None
- def schemaspace_exists(self) -> bool:
- """Returns True if the schemaspace for this instance exists"""
- return self.instances is not None
- def fetch_instances(self, name: Optional[str] = None, include_invalid: bool = False) -> List[dict]:
- """Fetch metadata instances"""
- if name:
- if self.instances is not None and name in self.instances:
- instance = self.instances.get(name)
- if instance.get("reason"):
- raise ValueError(instance.get("reason"))
- instance["name"] = name
- return [instance]
- raise MetadataNotFoundError(self.schemaspace, name)
- # all instances are wanted, filter based on include-invalid and reason ...
- instance_list = []
- if self.instances is not None:
- for name, instance in self.instances.items():
- if include_invalid or not instance.get("reason"):
- instance["name"] = name
- instance_list.append(instance)
- return instance_list
- def store_instance(self, name: str, metadata: dict, for_update: bool = False) -> dict:
- """Stores the named metadata instance."""
- try:
- instance = self.fetch_instances(name)
- if not for_update: # Create - already exists
- raise MetadataExistsError(self.schemaspace, instance[0].get("resource"))
- except MetadataNotFoundError as mnfe:
- if for_update: # Update - doesn't exist
- raise mnfe from mnfe
- if self.instances is None:
- self.instances = dict()
- self.instances[name] = metadata # persisted, now fetch
- instance = self.fetch_instances(name) # confirm persistence
- return instance[0]
- def delete_instance(self, metadata: dict) -> None:
- """Deletes the metadata instance."""
- name = metadata.get("name")
- self.instances.pop(name)
- class MockMetadataTest(Metadata):
- """Hypothetical class used to demonstrate (and test) use of custom instance classes.
- This class name is referenced in the metadata-test schema.
- """
- pre_property = None
- post_property = None
- def __init__(self, **kwargs: Any) -> None:
- super().__init__(**kwargs)
- self.pre_property = kwargs.get("pre_property")
- self.post_property = kwargs.get("post_property")
- def to_dict(self, trim: bool = False) -> dict:
- d = super().to_dict(trim=trim)
- if self.pre_property is not None:
- d["pre_property"] = self.pre_property
- if self.post_property is not None:
- d["post_property"] = self.post_property
- return d
- def on_load(self, **kwargs: Any) -> None:
- super().on_load(**kwargs)
- self.post_property = self.display_name
- def pre_save(self, **kwargs: Any) -> None:
- super().pre_save(**kwargs)
- self.pre_property = self.metadata["required_test"]
- def post_save(self, **kwargs: Any) -> None:
- super().post_save(**kwargs)
- self.post_property = self.display_name
- def pre_delete(self, **kwargs: Any) -> None:
- super().pre_delete(**kwargs)
- self.pre_property = self.metadata["required_test"]
- def post_delete(self, **kwargs: Any) -> None:
- super().post_delete(**kwargs)
- self.post_property = self.display_name
- class MockMetadataTestRollback(MockMetadataTest):
- """Used by metadata tests to validate rollback behaviors when post-save/delete hooks throw exceptions."""
- def post_save(self, **kwargs: Any) -> None:
- super().post_save(**kwargs)
- for_update = kwargs["for_update"]
- if os.getenv("METADATA_TEST_HOOK_OP", "skipped") == "create" and not for_update:
- raise NotImplementedError
- if os.getenv("METADATA_TEST_HOOK_OP", "skipped") == "update" and for_update:
- raise ModuleNotFoundError
- self.post_property = self.display_name
- def post_delete(self, **kwargs: Any) -> None:
- super().post_delete(**kwargs)
- if os.getenv("METADATA_TEST_HOOK_OP", "skipped") == "delete":
- raise FileNotFoundError
- self.post_property = self.display_name
- class MockMetadataTestInvalid(object):
- """Invalid metadata instance class that doesn't derive from Metadata.
- This requires an update to schema and is only used for manual testing.
- """
- def __init__(self, **kwargs: Any) -> None:
- pass
- class MetadataTestSchemaspace(Schemaspace):
- def __init__(self, *args, **kwargs):
- super().__init__(
- schemaspace_id=METADATA_TEST_SCHEMASPACE_ID,
- name=METADATA_TEST_SCHEMASPACE,
- description="Schemaspace for instances of metadata for testing",
- **kwargs,
- )
- class BYOSchemaspaceBadId(Schemaspace):
- def __init__(self, *args, **kwargs):
- super().__init__(schemaspace_id="byo_schemaspace_bad_id", name="byo-schemaspace-bad-id", **kwargs)
- class BYOSchemaspaceBadName(Schemaspace):
- def __init__(self, *args, **kwargs):
- super().__init__(
- schemaspace_id="b5b391d7-24f5-4b62-93bb-5e5423e651b8", name="byo.schemaspace-bad.name", **kwargs
- )
- class BYOSchemaspaceBadClass(LoggingConfigurable):
- """Class is not a subclass of Schemaspace"""
- def __init__(self, **kwargs):
- super().__init__(**kwargs)
- class BYOSchemaspaceCaseSensitiveName(Schemaspace):
- def __init__(self, *args, **kwargs):
- super().__init__(
- schemaspace_id="1b1e461a-c7fa-40f2-a3a3-bf1f2fd48eeA", name="byo-schemaspace_CaseSensitiveName", **kwargs
- )
- class BYOSchemaspaceThrows(Schemaspace):
- BYO_SCHEMASPACE_ID = "20c98d38-36f6-4f05-a4dc-9b0a6c2cb734"
- BYO_SCHEMASPACE_NAME = "byo-schemaspace-throws"
- def __init__(self, *args, **kwargs):
- super().__init__(
- schemaspace_id=BYOSchemaspace.BYO_SCHEMASPACE_ID, name=BYOSchemaspace.BYO_SCHEMASPACE_NAME, **kwargs
- )
- raise NotImplementedError("Test that throw from constructor is not harmful.")
- class BYOSchemaspace(Schemaspace):
- BYO_SCHEMASPACE_ID = "20c98d38-36f6-4f05-a4dc-9b0a6c2cb733"
- BYO_SCHEMASPACE_NAME = "byo-schemaspace"
- def __init__(self, *args, **kwargs):
- super().__init__(
- schemaspace_id=BYOSchemaspace.BYO_SCHEMASPACE_ID, name=BYOSchemaspace.BYO_SCHEMASPACE_NAME, **kwargs
- )
- class MetadataTestSchemasProvider(SchemasProvider):
- """Returns schemas relative to Runtime Images schemaspace."""
- def get_schemas(self) -> List[Dict]:
- schemas = []
- parent_dir = os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
- schema_dir = os.path.join(parent_dir, "metadata", "schemas")
- schema_files = [
- json_file
- for json_file in os.listdir(schema_dir)
- if json_file.endswith(".json") and json_file.startswith("metadata-test")
- ]
- for json_file in schema_files:
- schema_file = os.path.join(schema_dir, json_file)
- with io.open(schema_file, "r", encoding="utf-8") as f:
- schema_json = json.load(f)
- if json_file == "metadata-test.json": # Apply filtering
- # Update multipleOf from 7 to 6 and and value 'added' to enum-valued property
- multiple_of: int = schema_json["properties"]["metadata"]["properties"]["integer_multiple_test"][
- "multipleOf"
- ]
- assert multiple_of == 7
- schema_json["properties"]["metadata"]["properties"]["integer_multiple_test"]["multipleOf"] = 6
- enum: list = schema_json["properties"]["metadata"]["properties"]["enum_test"]["enum"]
- assert len(enum) == 2
- enum.append("added")
- schema_json["properties"]["metadata"]["properties"]["enum_test"]["enum"] = enum
- schemas.append(schema_json)
- return schemas
- def schema_factory(schemaspace_id: str, schemaspace_name: str, num_good: int, bad_reasons: List[str]) -> List[Dict]:
- # get the metadata test schema as a primary copy
- parent_dir = os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
- schema_file = os.path.join(parent_dir, "metadata", "schemas", "metadata-test.json")
- with io.open(schema_file, "r", encoding="utf-8") as f:
- primary_schema = json.load(f)
- def create_base_schema(primary: Dict, tag: str, ss_name: str, ss_id: str) -> Dict:
- base_schema: Dict = copy.deepcopy(primary)
- base_schema["title"] = f"BYO Test {tag}"
- base_schema["name"] = f"byo-test-{tag}"
- base_schema["display_name"] = base_schema["title"]
- base_schema["schemaspace"] = ss_name
- base_schema["schemaspace_id"] = ss_id
- base_schema["properties"]["schema_name"]["const"] = base_schema["name"]
- base_schema.pop("metadata_class_name")
- return base_schema
- schemas = []
- # Gather bad schemas
- for reason in bad_reasons:
- schema = create_base_schema(primary_schema, reason, schemaspace_name, schemaspace_id)
- if reason == "missing_required": # remove display_name
- schema["properties"].pop("display_name") # This will trigger a validation error
- elif reason == "unknown_schemaspace": # update schemaspace_id to a non-existent schemaspace
- schema["schemaspace_id"] = NON_EXISTENT_SCHEMASPACE_ID
- schemas.append(schema)
- # Gather good schemas
- for i in range(num_good):
- schemas.append(create_base_schema(primary_schema, str(i), schemaspace_name, schemaspace_id))
- return schemas
- class BYOSchemasProvider(SchemasProvider):
- """Test SchemasProvider that loads the metadata-test schema and adjusts its values to match BYOSchemaspace."""
- def get_schemas(self) -> List[Dict]:
- # We'll create 2 good schemas and 2 bad schemas for BYOSchemaspace
- schemas = schema_factory(
- BYOSchemaspace.BYO_SCHEMASPACE_ID,
- BYOSchemaspace.BYO_SCHEMASPACE_NAME,
- 2,
- ["missing_required", "unknown_schemaspace"],
- )
- return schemas
- class BYOSchemasProviderThrows(SchemasProvider):
- """Test SchemasProvider that raises an exception to ensure the exception doesn't mess things up."""
- def get_schemas(self) -> List[Dict]:
- raise ModuleNotFoundError("Exception to ensure bad providers are not side-effecting.")
- class BYOSchemasProviderBadClass(object):
- """Test SchemasProvider that is of the wrong subclass."""
- def get_schemas(self) -> List[Dict]:
- schemas = schema_factory(BYOSchemaspace.BYO_SCHEMASPACE_ID, BYOSchemaspace.BYO_SCHEMASPACE_NAME, 2, [])
- return schemas
|