Пример #1
0
    def test_ask_with_real_run_function_with_no_log_message_forwarding(self):
        """Test that a service can ask a question to another service that is serving and receive an answer. Use a real
        run function rather than a mock so that the underlying `Runner` instance is used, and check that remote log
        messages aren't forwarded to the local logger.
        """
        child = MockService(backend=BACKEND,
                            run_function=create_run_function())
        parent = MockService(backend=BACKEND, children={child.id: child})

        with self.assertLogs() as logging_context:
            with patch("octue.cloud.pub_sub.service.Topic", new=MockTopic):
                with patch("octue.cloud.pub_sub.service.Subscription",
                           new=MockSubscription):
                    with patch("google.cloud.pubsub_v1.SubscriberClient",
                               new=MockSubscriber):
                        child.serve()

                        answer = self.ask_question_and_wait_for_answer(
                            parent=parent,
                            child=child,
                            input_values={},
                            subscribe_to_logs=False,
                        )

        self.assertEqual(
            answer,
            {
                "output_values": MockAnalysis().output_values,
                "output_manifest": MockAnalysis().output_manifest
            },
        )

        self.assertTrue(
            all("[REMOTE]" not in message
                for message in logging_context.output))
Пример #2
0
    def test_child_can_ask_its_own_child_questions(self):
        """Test that a child can contact its own child while answering a question from a parent."""
        child_of_child = self.make_new_child(
            BACKEND,
            run_function_returnee=DifferentMockAnalysis(),
            use_mock=True)

        def child_run_function(analysis_id, input_values, input_manifest,
                               analysis_log_handler, handle_monitor_message):
            subscription, _ = child.ask(service_id=child_of_child.id,
                                        input_values=input_values)
            return MockAnalysis(output_values={
                input_values["question"]:
                child.wait_for_answer(subscription)
            })

        child = MockService(
            backend=BACKEND,
            run_function=child_run_function,
            children={child_of_child.id: child_of_child},
        )

        parent = MockService(backend=BACKEND, children={child.id: child})

        with patch("octue.cloud.pub_sub.service.Topic", new=MockTopic):
            with patch("octue.cloud.pub_sub.service.Subscription",
                       new=MockSubscription):
                with patch("google.cloud.pubsub_v1.SubscriberClient",
                           new=MockSubscriber):
                    child.serve()
                    child_of_child.serve()

                    answer = self.ask_question_and_wait_for_answer(
                        parent=parent,
                        child=child,
                        input_values={
                            "question": "What does the child of the child say?"
                        },
                    )

        self.assertEqual(
            answer,
            {
                "output_values": {
                    "What does the child of the child say?": {
                        "output_values": DifferentMockAnalysis.output_values,
                        "output_manifest":
                        DifferentMockAnalysis.output_manifest,
                    }
                },
                "output_manifest": None,
            },
        )
Пример #3
0
    def test_ask_with_real_run_function_with_log_message_forwarding(self):
        """Test that a service can ask a question to another service that is serving and receive an answer. Use a real
        run function rather than a mock so that the underlying `Runner` instance is used, and check that remote log
        messages are forwarded to the local logger.
        """
        child = MockService(backend=BACKEND,
                            run_function=create_run_function())
        parent = MockService(backend=BACKEND, children={child.id: child})

        with self.assertLogs() as logs_context_manager:
            with patch("octue.cloud.pub_sub.service.Topic", new=MockTopic):
                with patch("octue.cloud.pub_sub.service.Subscription",
                           new=MockSubscription):
                    with patch("google.cloud.pubsub_v1.SubscriberClient",
                               new=MockSubscriber):
                        child.serve()

                        answer = self.ask_question_and_wait_for_answer(
                            parent=parent,
                            child=child,
                            input_values={},
                            subscribe_to_logs=True,
                            service_name="my-super-service",
                        )

        self.assertEqual(
            answer,
            {
                "output_values": MockAnalysis().output_values,
                "output_manifest": MockAnalysis().output_manifest
            },
        )

        # Check that the two expected remote log messages were logged consecutively in the right order with the service
        # name added as context at the start of the messages.
        start_remote_analysis_message_present = False
        finish_remote_analysis_message_present = False

        for i, log_record in enumerate(logs_context_manager.records):
            if "[my-super-service" in log_record.msg and "Starting analysis." in log_record.msg:
                start_remote_analysis_message_present = True

                if ("[my-super-service"
                        in logs_context_manager.records[i + 1].msg
                        and "Finished analysis."
                        in logs_context_manager.records[i + 1].msg):
                    finish_remote_analysis_message_present = True

                break

        self.assertTrue(start_remote_analysis_message_present)
        self.assertTrue(finish_remote_analysis_message_present)
Пример #4
0
    def test_monitoring_update_fails_if_schema_not_met(self):
        """Test that an error is raised and sent to the analysis logger if a monitor message fails schema validation,
        but earlier valid monitor messages still make it to the parent's monitoring callback.
        """
        def create_run_function_with_monitoring():
            def mock_app(analysis):
                analysis.send_monitor_message(
                    {"status": "my first monitor message"})
                analysis.send_monitor_message(
                    {"wrong": "my second monitor message"})
                analysis.send_monitor_message(
                    {"status": "my third monitor message"})

            twine = """
                {
                    "input_values_schema": {"type": "object", "required": []},
                    "monitor_message_schema": {
                        "type": "object",
                        "properties": {"status": {"type": "string"}},
                        "required": ["status"]
                    }
                }
            """

            return Runner(app_src=mock_app, twine=twine).run

        child = MockService(backend=BACKEND,
                            run_function=create_run_function_with_monitoring())
        parent = MockService(backend=BACKEND, children={child.id: child})

        with patch("octue.cloud.pub_sub.service.Topic", new=MockTopic):
            with patch("octue.cloud.pub_sub.service.Subscription",
                       new=MockSubscription):
                with patch("google.cloud.pubsub_v1.SubscriberClient",
                           new=MockSubscriber):
                    child.serve()

                    subscription, _ = parent.ask(child.id, input_values={})

                    monitoring_data = []

                    with self.assertRaises(InvalidMonitorMessage):
                        parent.wait_for_answer(
                            subscription,
                            handle_monitor_message=lambda data: monitoring_data
                            .append(data),
                        )

        self.assertEqual(
            monitoring_data,
            [{
                "status": "my first monitor message"
            }],
        )
Пример #5
0
    def test_ask_with_forwarding_exception_log_message(self):
        """Test that exception/error logs are forwarded to the asker successfully."""
        def create_exception_logging_run_function():
            def mock_app(analysis):
                try:
                    raise OSError("This is an OSError.")
                except OSError:
                    logger.exception(
                        "An example exception to log and forward to the parent."
                    )

            return Runner(
                app_src=mock_app,
                twine=
                '{"input_values_schema": {"type": "object", "required": []}}'
            ).run

        child = MockService(
            backend=BACKEND,
            run_function=create_exception_logging_run_function())
        parent = MockService(backend=BACKEND, children={child.id: child})

        with self.assertLogs() as logs_context_manager:
            with patch("octue.cloud.pub_sub.service.Topic", new=MockTopic):
                with patch("octue.cloud.pub_sub.service.Subscription",
                           new=MockSubscription):
                    with patch("google.cloud.pubsub_v1.SubscriberClient",
                               new=MockSubscriber):
                        child.serve()

                        self.ask_question_and_wait_for_answer(
                            parent=parent,
                            child=child,
                            input_values={},
                            subscribe_to_logs=True,
                            service_name="my-super-service",
                        )

        error_logged = False

        for record in logs_context_manager.records:
            if (record.levelno == logging.ERROR and
                    "An example exception to log and forward to the parent."
                    in record.message
                    and "This is an OSError" in record.exc_text):
                error_logged = True
                break

        self.assertTrue(error_logged)
Пример #6
0
    def test_ask_with_output_manifest(self):
        """Test that a service can receive an output manifest as part of the answer to a question."""
        child = self.make_new_child(
            BACKEND,
            run_function_returnee=MockAnalysisWithOutputManifest(),
            use_mock=True)
        parent = MockService(backend=BACKEND, children={child.id: child})

        with patch("octue.cloud.pub_sub.service.Topic", new=MockTopic):
            with patch("octue.cloud.pub_sub.service.Subscription",
                       new=MockSubscription):
                with patch("google.cloud.pubsub_v1.SubscriberClient",
                           new=MockSubscriber):
                    child.serve()

                    answer = self.ask_question_and_wait_for_answer(
                        parent=parent,
                        child=child,
                        input_values={},
                    )

        self.assertEqual(answer["output_values"],
                         MockAnalysisWithOutputManifest.output_values)
        self.assertEqual(answer["output_manifest"].id,
                         MockAnalysisWithOutputManifest.output_manifest.id)
Пример #7
0
    def test_unknown_exceptions_in_responder_are_handled_and_sent_to_asker(
            self):
        """Test that exceptions not in the exceptions mapping are simply raised as `Exception`s by the asker."""
        class AnUnknownException(Exception):
            pass

        child = self.make_child_service_with_error(
            AnUnknownException("This is an exception unknown to the asker."))
        parent = MockService(backend=BACKEND, children={child.id: child})

        with patch("octue.cloud.pub_sub.service.Topic", new=MockTopic):
            with patch("octue.cloud.pub_sub.service.Subscription",
                       new=MockSubscription):
                with patch("google.cloud.pubsub_v1.SubscriberClient",
                           new=MockSubscriber):
                    child.serve()

                    with self.assertRaises(Exception) as context:
                        self.ask_question_and_wait_for_answer(
                            parent=parent,
                            child=child,
                            input_values={},
                        )

        self.assertEqual(
            type(context.exception).__name__, "AnUnknownException")
        self.assertIn("This is an exception unknown to the asker.",
                      context.exception.args[0])
Пример #8
0
    def test_exceptions_with_multiple_arguments_in_responder_are_handled_and_sent_to_asker(
            self):
        """Test that exceptions with multiple arguments raised in the child service are handled and sent back to
        the asker.
        """
        child = self.make_child_service_with_error(
            FileNotFoundError(2, "No such file or directory: 'blah'"))
        parent = MockService(backend=BACKEND, children={child.id: child})

        with patch("octue.cloud.pub_sub.service.Topic", new=MockTopic):
            with patch("octue.cloud.pub_sub.service.Subscription",
                       new=MockSubscription):
                with patch("google.cloud.pubsub_v1.SubscriberClient",
                           new=MockSubscriber):
                    child.serve()

                    with self.assertRaises(FileNotFoundError) as context:
                        self.ask_question_and_wait_for_answer(
                            parent=parent,
                            child=child,
                            input_values={},
                        )

        self.assertIn("[Errno 2] No such file or directory: 'blah'",
                      format(context.exception))
Пример #9
0
    def test_exceptions_in_responder_are_handled_and_sent_to_asker(self):
        """Test that exceptions raised in the child service are handled and sent back to the asker."""
        child = self.make_child_service_with_error(
            twined.exceptions.InvalidManifestContents(
                "'met_mast_id' is a required property"))

        parent = MockService(backend=BACKEND, children={child.id: child})

        with patch("octue.cloud.pub_sub.service.Topic", new=MockTopic):
            with patch("octue.cloud.pub_sub.service.Subscription",
                       new=MockSubscription):
                with patch("google.cloud.pubsub_v1.SubscriberClient",
                           new=MockSubscriber):
                    child.serve()

                    with self.assertRaises(twined.exceptions.
                                           InvalidManifestContents) as context:
                        self.ask_question_and_wait_for_answer(
                            parent=parent,
                            child=child,
                            input_values={},
                        )

        self.assertIn("'met_mast_id' is a required property",
                      context.exception.args[0])
Пример #10
0
    def test_emit_with_non_json_serialisable_args(self):
        """Test that non-JSON-serialisable arguments to log messages are converted to their string representation
        before being serialised and published to the Pub/Sub topic.
        """
        backend = GCPPubSubBackend(project_name="blah")
        service = MockService(backend=backend)
        topic = MockTopic(name="world-1", namespace="hello", service=service)
        topic.create()

        non_json_serialisable_thing = NonJSONSerialisable()

        # Check that it can't be serialised to JSON.
        with self.assertRaises(TypeError):
            json.dumps(non_json_serialisable_thing)

        record = logging.makeLogRecord({
            "msg": "%r is not JSON-serialisable but can go into a log message",
            "args": (non_json_serialisable_thing, )
        })

        with patch("tests.cloud.pub_sub.mocks.MockPublisher.publish"
                   ) as mock_publish:
            GooglePubSubHandler(service.publisher, topic,
                                "analysis-id").emit(record)

        self.assertEqual(
            json.loads(mock_publish.call_args.kwargs["data"].decode())
            ["log_record"]["msg"],
            "NonJSONSerialisableInstance is not JSON-serialisable but can go into a log message",
        )
Пример #11
0
    def test_service_can_ask_multiple_questions_to_child(self):
        """Test that a service can ask multiple questions to the same child and expect replies to them all."""
        child = self.make_new_child(BACKEND,
                                    run_function_returnee=MockAnalysis(),
                                    use_mock=True)
        parent = MockService(backend=BACKEND, children={child.id: child})

        with patch("octue.cloud.pub_sub.service.Topic", new=MockTopic):
            with patch("octue.cloud.pub_sub.service.Subscription",
                       new=MockSubscription):
                with patch("google.cloud.pubsub_v1.SubscriberClient",
                           new=MockSubscriber):
                    child.serve()

                    answers = []

                    for i in range(5):
                        answers.append(
                            self.ask_question_and_wait_for_answer(
                                parent=parent,
                                child=child,
                                input_values={},
                            ))

        for answer in answers:
            self.assertEqual(
                answer,
                {
                    "output_values": MockAnalysis().output_values,
                    "output_manifest": MockAnalysis().output_manifest
                },
            )
Пример #12
0
 def test_ask_on_non_existent_service_results_in_error(self):
     """Test that trying to ask a question to a non-existent service (i.e. one without a topic in Google Pub/Sub)
     results in an error.
     """
     with patch("octue.cloud.pub_sub.service.Topic", new=MockTopic):
         with self.assertRaises(exceptions.ServiceNotFound):
             MockService(backend=BACKEND).ask(service_id="hello",
                                              input_values=[1, 2, 3, 4])
Пример #13
0
    def test_ask_with_input_manifest_with_local_paths_works_if_allowed_and_child_has_access_to_the_local_paths(
            self):
        """Test that an input manifest referencing local files can be used if the files can be accessed by the child and
        the `allow_local_files` parameter is `True`.
        """
        temporary_local_path = tempfile.NamedTemporaryFile(delete=False).name

        with open(temporary_local_path, "w") as f:
            f.write("This is a local file.")

        local_file = Datafile(path=temporary_local_path)
        self.assertFalse(local_file.exists_in_cloud)

        manifest = Manifest(
            datasets={
                "my-local-dataset":
                Dataset(name="my-local-dataset", files={local_file})
            })

        # Get the child to open the local file itself and return the contents as output.
        def run_function(analysis_id, input_values, input_manifest,
                         analysis_log_handler, handle_monitor_message):
            with open(temporary_local_path) as f:
                return MockAnalysis(output_values=f.read())

        child = MockService(backend=BACKEND, run_function=run_function)
        parent = MockService(backend=BACKEND, children={child.id: child})

        with patch("octue.cloud.pub_sub.service.Topic", new=MockTopic):
            with patch("octue.cloud.pub_sub.service.Subscription",
                       new=MockSubscription):
                with patch("google.cloud.pubsub_v1.SubscriberClient",
                           new=MockSubscriber):
                    child.serve()

                    answer = self.ask_question_and_wait_for_answer(
                        parent=parent,
                        child=child,
                        input_values={},
                        input_manifest=manifest,
                        allow_local_files=True,
                    )

        self.assertEqual(answer["output_values"], "This is a local file.")
Пример #14
0
 def test_ask_with_input_manifest_with_local_paths_raises_error(self):
     """Test that an error is raised if an input manifest whose datasets and/or files are not located in the cloud
     is used in a question.
     """
     with self.assertRaises(exceptions.FileLocationError):
         MockService(backend=BACKEND).ask(
             service_id=str(uuid.uuid4()),
             input_values={},
             input_manifest=self.create_valid_manifest(),
         )
Пример #15
0
    def test_with_monitor_message_handler(self):
        """Test that monitor messages can be sent from a child app and handled by the parent's monitor message handler."""
        def create_run_function_with_monitoring():
            def mock_app(analysis):
                analysis.send_monitor_message(
                    {"status": "my first monitor message"})
                analysis.send_monitor_message(
                    {"status": "my second monitor message"})

            twine = """
                {
                    "input_values_schema": {"type": "object", "required": []},
                    "monitor_message_schema": {
                        "type": "object",
                        "properties": {"status": {"type": "string"}},
                        "required": ["status"]
                    }
                }
            """

            return Runner(app_src=mock_app, twine=twine).run

        child = MockService(backend=BACKEND,
                            run_function=create_run_function_with_monitoring())
        parent = MockService(backend=BACKEND, children={child.id: child})

        with patch("octue.cloud.pub_sub.service.Topic", new=MockTopic):
            with patch("octue.cloud.pub_sub.service.Subscription",
                       new=MockSubscription):
                with patch("google.cloud.pubsub_v1.SubscriberClient",
                           new=MockSubscriber):
                    child.serve()

                    subscription, _ = parent.ask(child.id, input_values={})

                    monitoring_data = []
                    parent.wait_for_answer(subscription,
                                           handle_monitor_message=lambda data:
                                           monitoring_data.append(data))

        self.assertEqual(monitoring_data,
                         [{
                             "status": "my first monitor message"
                         }, {
                             "status": "my second monitor message"
                         }])
Пример #16
0
    def test_service_can_ask_questions_to_multiple_children(self):
        """Test that a service can ask questions to different children and expect replies to them all."""
        child_1 = self.make_new_child(BACKEND,
                                      run_function_returnee=MockAnalysis(),
                                      use_mock=True)
        child_2 = self.make_new_child(
            BACKEND,
            run_function_returnee=DifferentMockAnalysis(),
            use_mock=True)

        parent = MockService(backend=BACKEND,
                             children={
                                 child_1.id: child_1,
                                 child_2.id: child_2
                             })

        with patch("octue.cloud.pub_sub.service.Topic", new=MockTopic):
            with patch("octue.cloud.pub_sub.service.Subscription",
                       new=MockSubscription):
                with patch("google.cloud.pubsub_v1.SubscriberClient",
                           new=MockSubscriber):
                    child_1.serve()
                    child_2.serve()

                    answer_1 = self.ask_question_and_wait_for_answer(
                        parent=parent,
                        child=child_1,
                        input_values={},
                    )

                    answer_2 = self.ask_question_and_wait_for_answer(
                        parent=parent,
                        child=child_2,
                        input_values={},
                    )

        self.assertEqual(
            answer_1,
            {
                "output_values": MockAnalysis().output_values,
                "output_manifest": MockAnalysis().output_manifest
            },
        )

        self.assertEqual(
            answer_2,
            {
                "output_values": DifferentMockAnalysis.output_values,
                "output_manifest": DifferentMockAnalysis.output_manifest,
            },
        )
Пример #17
0
    def test_child_can_be_asked_multiple_questions(self):
        """Test that a child can be asked multiple questions."""
        backend = GCPPubSubBackend(project_name="blah")

        def run_function(analysis_id, input_values, input_manifest,
                         analysis_log_handler, handle_monitor_message):
            return MockAnalysis(output_values=input_values)

        responding_service = MockService(backend=backend,
                                         service_id="testing/wind-speed",
                                         run_function=run_function)

        with patch("octue.cloud.pub_sub.service.Topic", new=MockTopic):
            with patch("octue.cloud.pub_sub.service.Subscription",
                       new=MockSubscription):
                with patch("octue.resources.child.BACKEND_TO_SERVICE_MAPPING",
                           {"GCPPubSubBackend": MockService}):
                    with patch("google.cloud.pubsub_v1.SubscriberClient",
                               new=MockSubscriber):
                        responding_service.serve()

                        child = Child(
                            id=responding_service.id,
                            backend={
                                "name": "GCPPubSubBackend",
                                "project_name": "blah"
                            },
                        )

                        # Make sure the child's underlying mock service knows how to access the mock responding service.
                        child._service.children[
                            responding_service.id] = responding_service
                        self.assertEqual(
                            child.ask([1, 2, 3, 4])["output_values"],
                            [1, 2, 3, 4])
                        self.assertEqual(
                            child.ask([5, 6, 7, 8])["output_values"],
                            [5, 6, 7, 8])
Пример #18
0
    def setUpClass(cls):
        """Set up the test class with a mock subscription.

        :return None:
        """
        service = MockService(backend=BACKEND)
        mock_topic = MockTopic(name="world", namespace="hello", service=service)
        cls.mock_subscription = MockSubscription(
            name="world",
            topic=mock_topic,
            namespace="hello",
            project_name=TEST_PROJECT_NAME,
            subscriber=MockSubscriber(),
        )
Пример #19
0
    def test_emit(self):
        """Test the log message is published when `GooglePubSubHandler.emit` is called."""
        backend = GCPPubSubBackend(project_name="blah")
        service = MockService(backend=backend)
        topic = MockTopic(name="world", namespace="hello", service=service)
        topic.create()

        log_record = makeLogRecord({"msg": "Starting analysis."})
        GooglePubSubHandler(service.publisher, topic,
                            "analysis-id").emit(log_record)

        self.assertEqual(
            json.loads(
                MESSAGES[topic.name][0].data.decode())["log_record"]["msg"],
            "Starting analysis.")
Пример #20
0
    def test_ask_with_input_manifest(self):
        """Test that a service can ask a question including an input manifest to another service that is serving and
        receive an answer.
        """
        child = self.make_new_child(BACKEND,
                                    run_function_returnee=MockAnalysis(),
                                    use_mock=True)
        parent = MockService(backend=BACKEND, children={child.id: child})

        dataset_path = f"gs://{TEST_BUCKET_NAME}/my-dataset"

        input_manifest = Manifest(
            datasets={
                "my-dataset":
                Dataset(
                    files=[
                        f"{dataset_path}/hello.txt",
                        f"{dataset_path}/goodbye.csv"
                    ],
                    path=dataset_path,
                )
            })

        with patch("octue.cloud.pub_sub.service.Topic", new=MockTopic):
            with patch("octue.cloud.pub_sub.service.Subscription",
                       new=MockSubscription):
                with patch("google.cloud.pubsub_v1.SubscriberClient",
                           new=MockSubscriber):
                    child.serve()

                    with patch(
                            "google.cloud.storage.blob.Blob.generate_signed_url",
                            new=mock_generate_signed_url):
                        answer = self.ask_question_and_wait_for_answer(
                            parent=parent,
                            child=child,
                            input_values={},
                            input_manifest=input_manifest,
                        )

        self.assertEqual(
            answer,
            {
                "output_values": MockAnalysis().output_values,
                "output_manifest": MockAnalysis().output_manifest
            },
        )
Пример #21
0
    def make_new_child(backend, run_function_returnee, use_mock=False):
        """Make and return a new child service that returns the given run function returnee when its run function is
        executed.

        :param octue.resources.service_backends.ServiceBackend backend:
        :param any run_function_returnee:
        :param bool use_mock:
        :return tests.cloud.pub_sub.mocks.MockService:
        """
        run_function = (lambda analysis_id, input_values, input_manifest,
                        analysis_log_handler, handle_monitor_message:
                        run_function_returnee)

        if use_mock:
            return MockService(backend=backend, run_function=run_function)

        return Service(backend=backend, run_function=run_function)
Пример #22
0
    def test_warning_issued_if_child_and_parent_sdk_versions_incompatible(
            self):
        """Test that a warning is logged if the parent and child's Octue SDK versions are potentially incompatible."""
        child = self.make_new_child(backend=BACKEND,
                                    run_function_returnee=MockAnalysis(),
                                    use_mock=True)
        parent = MockService(backend=BACKEND, children={child.id: child})

        with patch("octue.cloud.pub_sub.service.Topic", new=MockTopic):
            with patch("octue.cloud.pub_sub.service.Subscription",
                       new=MockSubscription):
                with patch("google.cloud.pubsub_v1.SubscriberClient",
                           new=MockSubscriber):
                    child.serve()

                    for parent_sdk_version, child_sdk_version in (
                        ("0.1.0", "0.2.0"),
                        ("0.2.0", "0.1.0"),
                        ("0.1.0", "1.1.0"),
                        ("1.1.0", "0.1.0"),
                    ):
                        with self.subTest(
                                parent_sdk_version=parent_sdk_version,
                                child_sdk_version=child_sdk_version):
                            with patch("pkg_resources.Distribution.version",
                                       child_sdk_version):
                                with self.assertLogs() as logging_context:
                                    self.ask_question_and_wait_for_answer(
                                        parent=parent,
                                        child=child,
                                        input_values={
                                            "question":
                                            "What does the child of the child say?"
                                        },
                                        parent_sdk_version=parent_sdk_version,
                                    )

                                    self.assertIn(
                                        f"The parent's Octue SDK version {parent_sdk_version} may not be compatible "
                                        f"with the local Octue SDK version {child_sdk_version}",
                                        logging_context.output[3],
                                    )