From f77a70aa31d642efe39b696521f048bc9b5deb1e Mon Sep 17 00:00:00 2001 From: KerstenBreuer Date: Thu, 7 Dec 2023 10:55:40 +0000 Subject: [PATCH] test transform frame using null transformation --- tests/schemapack_/transform/__init__.py | 17 ++ tests/schemapack_/transform/test_base.py | 221 +++++++++++++++++++ tests/schemapack_/transform/test_handling.py | 94 ++++++++ tests/schemapack_/transform/test_main.py | 94 ++++++++ 4 files changed, 426 insertions(+) create mode 100644 tests/schemapack_/transform/__init__.py create mode 100644 tests/schemapack_/transform/test_base.py create mode 100644 tests/schemapack_/transform/test_handling.py create mode 100644 tests/schemapack_/transform/test_main.py diff --git a/tests/schemapack_/transform/__init__.py b/tests/schemapack_/transform/__init__.py new file mode 100644 index 00000000..46489375 --- /dev/null +++ b/tests/schemapack_/transform/__init__.py @@ -0,0 +1,17 @@ +# Copyright 2021 - 2023 Universität Tübingen, DKFZ, EMBL, and Universität zu Köln +# for the German Human Genome-Phenome Archive (GHGA) +# +# 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. +# + +"""Test the transform sub-package.""" diff --git a/tests/schemapack_/transform/test_base.py b/tests/schemapack_/transform/test_base.py new file mode 100644 index 00000000..d6d0d68b --- /dev/null +++ b/tests/schemapack_/transform/test_base.py @@ -0,0 +1,221 @@ +# Copyright 2021 - 2023 Universität Tübingen, DKFZ, EMBL, and Universität zu Köln +# for the German Human Genome-Phenome Archive (GHGA) +# +# 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. +# + +"""Test the base module.""" + + +import pytest +from pydantic import ValidationError + +from metldata.schemapack_.builtin_transformations.null import NULL_TRANSFORMATION +from metldata.schemapack_.transform.base import ( + WorkflowDefinition, + WorkflowStep, +) + + +def test_workflow_definition_invalid_step_refs(): + """Test that an invalid step reference raises an error.""" + with pytest.raises(ValidationError): + WorkflowDefinition( + description="A workflow for testing.", + steps={ + "step1": WorkflowStep( + description="A dummy step.", + transformation_definition=NULL_TRANSFORMATION, + input=None, + ), + "step2": WorkflowStep( + description="Another dummy step.", + transformation_definition=NULL_TRANSFORMATION, + input="non_existing_step", + ), + }, + artifacts={ + "step1_output": "step1", + "step2_output": "step2", + }, + ) + + +def test_workflow_definition_invalid_multiple_first_steps(): + """Test that specifing multiple steps without input raises an exeception.""" + with pytest.raises(ValidationError): + WorkflowDefinition( + description="A workflow for testing.", + steps={ + "step1": WorkflowStep( + description="A dummy step.", + transformation_definition=NULL_TRANSFORMATION, + input=None, + ), + "step2": WorkflowStep( + description="Another dummy step.", + transformation_definition=NULL_TRANSFORMATION, + input=None, + ), + }, + artifacts={ + "step1_output": "step1", + "step2_output": "step2", + }, + ) + + +def test_workflow_definition_invalid_artifacts(): + """Test that artifacts referencing non-existing steps raise an exception.""" + with pytest.raises(ValidationError): + WorkflowDefinition( + description="A workflow for testing.", + steps={ + "step1": WorkflowStep( + description="A dummy step.", + transformation_definition=NULL_TRANSFORMATION, + input=None, + ), + "step2": WorkflowStep( + description="Another dummy step.", + transformation_definition=NULL_TRANSFORMATION, + input="step1", + ), + }, + artifacts={ + "step1_output": "non_existing_step", + "step2_output": "step2", + }, + ) + + +def test_workflow_definition_step_order_happy(): + """Test that the step order is correctly inferred from the workflow definition.""" + + workflow_definition = WorkflowDefinition( + description="A workflow for testing.", + steps={ + "step3": WorkflowStep( + description="A test step.", + transformation_definition=NULL_TRANSFORMATION, + input="step2", + ), + "step2": WorkflowStep( + description="A test step.", + transformation_definition=NULL_TRANSFORMATION, + input="step1", + ), + "step1": WorkflowStep( + description="A test step.", + transformation_definition=NULL_TRANSFORMATION, + input=None, + ), + "step4": WorkflowStep( + description="A test step.", + transformation_definition=NULL_TRANSFORMATION, + input="step2", + ), + }, + artifacts={ + "output3": "step3", + "output4": "step4", + }, + ) + + assert workflow_definition.step_order in ( + [ + "step1", + "step2", + "step3", + "step4", + ], + [ + "step1", + "step2", + "step4", + "step3", + ], + ) + + +def test_workflow_definition_step_order_circular(): + """Test that initialization of a WorkflowDefinition with a circularly dependent + steps fails.""" + + workflow_definition = WorkflowDefinition( + description="A workflow for testing.", + steps={ + "step1": WorkflowStep( + description="A test step.", + transformation_definition=NULL_TRANSFORMATION, + input=None, + ), + "step2": WorkflowStep( + description="A test step.", + transformation_definition=NULL_TRANSFORMATION, + input="step4", + ), + "step3": WorkflowStep( + description="A test step.", + transformation_definition=NULL_TRANSFORMATION, + input="step2", + ), + "step4": WorkflowStep( + description="A test step.", + transformation_definition=NULL_TRANSFORMATION, + input="step3", + ), + }, + artifacts={ + "output3": "step3", + "output4": "step4", + }, + ) + + with pytest.raises(RuntimeError): + _ = workflow_definition.step_order + + +def test_workflow_definition_config_cls(): + """Test that the config_cls of the WorkflowDefinition generates a concatenated + config class correctly.""" + + null_workflow = WorkflowDefinition( + description="A workflow for testing.", + steps={ + "step1": WorkflowStep( + description="A dummy step.", + transformation_definition=NULL_TRANSFORMATION, + input=None, + ), + "step2": WorkflowStep( + description="Another dummy step.", + transformation_definition=NULL_TRANSFORMATION, + input="step1", + ), + }, + artifacts={ + "step1_output": "step1", + "step2_output": "step2", + }, + ) + + config_fields = null_workflow.config_cls.model_fields + + assert "step1" in config_fields + assert "step2" in config_fields + assert ( + config_fields["step1"].annotation + == config_fields["step2"].annotation + == NULL_TRANSFORMATION.config_cls + ) diff --git a/tests/schemapack_/transform/test_handling.py b/tests/schemapack_/transform/test_handling.py new file mode 100644 index 00000000..00a781bc --- /dev/null +++ b/tests/schemapack_/transform/test_handling.py @@ -0,0 +1,94 @@ +# Copyright 2021 - 2023 Universität Tübingen, DKFZ, EMBL, and Universität zu Köln +# for the German Human Genome-Phenome Archive (GHGA) +# +# 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. +# + +"""Test the handling module. Only edge cases that are not covered by tests +with builtin transformations are tested here.""" + +import pytest + +from metldata.builtin_transformations.infer_references.main import ( + REFERENCE_INFERENCE_TRANSFORMATION, + ReferenceInferenceConfig, +) +from metldata.model_utils.essentials import MetadataModel +from metldata.transform.base import ( + MetadataModelAssumptionError, + MetadataModelTransformationError, + TransformationDefinition, +) +from metldata.transform.handling import TransformationHandler +from tests.fixtures.metadata_models import VALID_ADVANCED_METADATA_MODEL + +VALID_EXAMPLE_CONFIG = ReferenceInferenceConfig( + inferred_ref_map={ + "Experiment": { + "files": { # type: ignore + "path": "Experiment(samples)>Sample(files)>File", + "multivalued": True, + } + } + } +) + + +def test_transformation_handler_assumption_error(): + """Test using the TransformationHandling when model assumptions are not met.""" + + # make transformation definition always raise an MetadataModelAssumptionError: + def always_failing_assumptions( + model: MetadataModel, config: ReferenceInferenceConfig + ): + """A function that always raises a MetadataModelAssumptionError.""" + raise MetadataModelAssumptionError + + transformation = TransformationDefinition( + config_cls=REFERENCE_INFERENCE_TRANSFORMATION.config_cls, + check_model_assumptions=always_failing_assumptions, + transform_model=REFERENCE_INFERENCE_TRANSFORMATION.transform_model, + metadata_transformer_factory=REFERENCE_INFERENCE_TRANSFORMATION.metadata_transformer_factory, + ) + + with pytest.raises(MetadataModelAssumptionError): + _ = TransformationHandler( + transformation_definition=transformation, + transformation_config=VALID_EXAMPLE_CONFIG, + original_model=VALID_ADVANCED_METADATA_MODEL, + ) + + +def test_transformation_handler_model_transformation_error(): + """Test using the TransformationHandling when model transformation fails.""" + + # make transformation definition always raise an MetadataModelAssumptionError: + def always_failing_transformation( + original_model: MetadataModel, config: ReferenceInferenceConfig + ): + """A function that always raises a MetadataModelTransformationError.""" + raise MetadataModelTransformationError + + transformation = TransformationDefinition( + config_cls=REFERENCE_INFERENCE_TRANSFORMATION.config_cls, + check_model_assumptions=REFERENCE_INFERENCE_TRANSFORMATION.check_model_assumptions, + transform_model=always_failing_transformation, + metadata_transformer_factory=REFERENCE_INFERENCE_TRANSFORMATION.metadata_transformer_factory, + ) + + with pytest.raises(MetadataModelTransformationError): + _ = TransformationHandler( + transformation_definition=transformation, + transformation_config=VALID_EXAMPLE_CONFIG, + original_model=VALID_ADVANCED_METADATA_MODEL, + ) diff --git a/tests/schemapack_/transform/test_main.py b/tests/schemapack_/transform/test_main.py new file mode 100644 index 00000000..d04ad5d2 --- /dev/null +++ b/tests/schemapack_/transform/test_main.py @@ -0,0 +1,94 @@ +# Copyright 2021 - 2023 Universität Tübingen, DKFZ, EMBL, and Universität zu Köln +# for the German Human Genome-Phenome Archive (GHGA) +# +# 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. +# + +"""Test the main module.""" + +import json + +import pytest + +from metldata.event_handling.artifact_events import get_artifact_topic +from metldata.event_handling.models import SubmissionEventPayload +from metldata.transform.main import ( + TransformationEventHandlingConfig, + run_workflow_on_all_source_events, +) +from tests.fixtures.event_handling import ( + Event, + FileSystemEventFixture, + file_system_event_fixture, # noqa: F401 +) +from tests.fixtures.workflows import ( + EXAMPLE_WORKFLOW_DEFINITION, + EXAMPLE_WORKFLOW_TEST_CASE, +) + + +@pytest.mark.asyncio +async def test_run_workflow_on_all_source_events( + file_system_event_fixture: FileSystemEventFixture, # noqa: F811 +): + """Test the happy path of using the run_workflow_on_all_source_events function.""" + + event_config = TransformationEventHandlingConfig( + artifact_topic_prefix="artifacts", + source_event_topic="source-events", + source_event_type="source-event", + **file_system_event_fixture.config.model_dump(), + ) + + submission_id = "some-submission-id" + source_event = Event( + topic=event_config.source_event_topic, + type_=event_config.source_event_type, + key=submission_id, + payload=json.loads( + SubmissionEventPayload( + submission_id=submission_id, + content=EXAMPLE_WORKFLOW_TEST_CASE.original_metadata, + annotation=EXAMPLE_WORKFLOW_TEST_CASE.submission_annotation, + ).model_dump_json() + ), + ) + await file_system_event_fixture.publish_events(events=[source_event]) + + expected_events = [ + Event( + topic=get_artifact_topic( + artifact_topic_prefix=event_config.artifact_topic_prefix, + artifact_type=artifact_type, + ), + type_=artifact_type, + key=submission_id, + payload=json.loads( + SubmissionEventPayload( + submission_id=submission_id, + content=artifact, + annotation=EXAMPLE_WORKFLOW_TEST_CASE.submission_annotation, + ).model_dump_json() + ), + ) + for artifact_type, artifact in EXAMPLE_WORKFLOW_TEST_CASE.artifact_metadata.items() + ] + + await run_workflow_on_all_source_events( + event_config=event_config, + workflow_definition=EXAMPLE_WORKFLOW_DEFINITION, + worflow_config=EXAMPLE_WORKFLOW_TEST_CASE.config, + original_model=EXAMPLE_WORKFLOW_TEST_CASE.original_model, + ) + + file_system_event_fixture.expect_events(expected_events=expected_events)