Files
mlflow--mlflow/tests/entities/test_span_attachments.py
2026-07-13 13:22:34 +08:00

146 lines
4.6 KiB
Python

from unittest import mock
from mlflow.entities.span import LiveSpan, Span
from mlflow.tracing.attachments import Attachment
def _make_live_span(trace_id="tr-test123"):
from opentelemetry.sdk.trace import TracerProvider
tracer = TracerProvider().get_tracer("test")
otel_span = tracer.start_span("test_span")
return LiveSpan(otel_span, trace_id=trace_id)
def test_set_inputs_extracts_attachment():
span = _make_live_span()
att = Attachment(content_type="image/png", content_bytes=b"img")
span.set_inputs({"image": att})
assert att.id in span._attachments
assert span._attachments[att.id] is att
inputs = span.inputs
assert isinstance(inputs["image"], str)
assert inputs["image"].startswith("mlflow-attachment://")
parsed = Attachment.parse_ref(inputs["image"])
assert parsed["trace_id"] == "tr-test123"
assert parsed["attachment_id"] == att.id
def test_set_outputs_extracts_attachment():
span = _make_live_span()
att = Attachment(content_type="audio/wav", content_bytes=b"audio")
span.set_outputs({"sound": att})
assert att.id in span._attachments
outputs = span.outputs
assert isinstance(outputs["sound"], str)
parsed = Attachment.parse_ref(outputs["sound"])
assert parsed["content_type"] == "audio/wav"
def test_nested_dict_extraction():
span = _make_live_span()
att = Attachment(content_type="image/jpeg", content_bytes=b"jpg")
span.set_inputs({"nested": {"deep": att}})
assert att.id in span._attachments
inputs = span.inputs
assert isinstance(inputs["nested"]["deep"], str)
assert att.id in inputs["nested"]["deep"]
def test_list_extraction():
span = _make_live_span()
att1 = Attachment(content_type="image/png", content_bytes=b"a")
att2 = Attachment(content_type="image/png", content_bytes=b"b")
span.set_inputs({"images": [att1, att2]})
assert att1.id in span._attachments
assert att2.id in span._attachments
inputs = span.inputs
assert len(inputs["images"]) == 2
assert all(isinstance(v, str) for v in inputs["images"])
def test_mixed_values_only_replaces_attachments():
span = _make_live_span()
att = Attachment(content_type="image/png", content_bytes=b"img")
span.set_inputs({"image": att, "text": "hello", "number": 42})
assert att.id in span._attachments
inputs = span.inputs
assert inputs["text"] == "hello"
assert inputs["number"] == 42
assert isinstance(inputs["image"], str)
def test_non_dict_input_without_attachment():
span = _make_live_span()
span.set_inputs("plain string")
assert span._attachments == {}
assert span.inputs == "plain string"
def test_tuple_extraction():
span = _make_live_span()
att = Attachment(content_type="image/png", content_bytes=b"img")
span.set_inputs({"images": (att, "text")})
assert att.id in span._attachments
inputs = span.inputs
assert isinstance(inputs["images"], list)
assert isinstance(inputs["images"][0], str)
assert inputs["images"][0].startswith("mlflow-attachment://")
assert inputs["images"][1] == "text"
def test_set_inputs_twice_accumulates_attachments():
span = _make_live_span()
att1 = Attachment(content_type="image/png", content_bytes=b"first")
att2 = Attachment(content_type="image/jpeg", content_bytes=b"second")
span.set_inputs({"img": att1})
span.set_outputs({"img": att2})
assert att1.id in span._attachments
assert att2.id in span._attachments
assert len(span._attachments) == 2
def test_to_immutable_span_propagates_attachments():
span = _make_live_span()
att = Attachment(content_type="image/png", content_bytes=b"img")
span.set_inputs({"image": att})
immutable = span.to_immutable_span()
assert isinstance(immutable, Span)
assert att.id in immutable._attachments
assert immutable._attachments[att.id] is att
def test_set_inputs_after_end_does_not_store_attachment():
span = _make_live_span()
span.end()
att = Attachment(content_type="image/png", content_bytes=b"img")
with mock.patch("mlflow.entities.span._logger.debug") as mock_debug:
span.set_inputs({"image": att})
assert att.id not in span._attachments
assert span.inputs is None
mock_debug.assert_called_once_with(
"Skipping attachment storage because the span is no longer recording."
)
def test_set_outputs_after_end_does_not_store_attachment():
span = _make_live_span()
span.end()
att = Attachment(content_type="audio/wav", content_bytes=b"audio")
span.set_outputs({"sound": att})
assert att.id not in span._attachments
assert span.outputs is None