Skip to content

Commit f59708b

Browse files
add tests
1 parent 250afc0 commit f59708b

3 files changed

Lines changed: 265 additions & 0 deletions

File tree

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
# Copyright (c) Microsoft Corporation.
2+
# Licensed under the MIT License.
3+
4+
"""Tests for EnrichedReadableSpan."""
5+
6+
import unittest
7+
from unittest.mock import Mock
8+
9+
from microsoft_agents_a365.observability.core.exporters.enriched_span import EnrichedReadableSpan
10+
11+
12+
class TestEnrichedReadableSpan(unittest.TestCase):
13+
"""Test suite for EnrichedReadableSpan."""
14+
15+
def test_attributes_merges_original_and_extra(self):
16+
"""Test that attributes property merges original span attributes with extra attributes."""
17+
# Create mock span with original attributes
18+
mock_span = Mock()
19+
mock_span.attributes = {"original_key": "original_value", "shared_key": "original"}
20+
21+
# Create enriched span with extra attributes
22+
extra_attributes = {"extra_key": "extra_value", "shared_key": "overwritten"}
23+
enriched_span = EnrichedReadableSpan(mock_span, extra_attributes)
24+
25+
# Verify merged attributes
26+
attributes = enriched_span.attributes
27+
self.assertEqual(attributes["original_key"], "original_value")
28+
self.assertEqual(attributes["extra_key"], "extra_value")
29+
self.assertEqual(attributes["shared_key"], "overwritten") # Extra should overwrite original
30+
31+
def test_delegates_all_properties_to_wrapped_span(self):
32+
"""Test that all span properties are delegated to the wrapped span."""
33+
# Create mock span with all properties
34+
mock_span = Mock()
35+
mock_span.name = "test-span"
36+
mock_span.context = Mock(trace_id=123, span_id=456)
37+
mock_span.parent = Mock(span_id=789)
38+
mock_span.start_time = 1000000000
39+
mock_span.end_time = 2000000000
40+
mock_span.status = Mock(status_code="OK", description=None)
41+
mock_span.kind = "INTERNAL"
42+
mock_span.events = []
43+
mock_span.links = []
44+
mock_span.resource = Mock(attributes={"service.name": "test"})
45+
mock_span.instrumentation_scope = Mock(name="test-scope")
46+
mock_span.attributes = {}
47+
48+
enriched_span = EnrichedReadableSpan(mock_span, {})
49+
50+
# Verify all properties delegate correctly
51+
self.assertEqual(enriched_span.name, "test-span")
52+
self.assertEqual(enriched_span.context, mock_span.context)
53+
self.assertEqual(enriched_span.parent, mock_span.parent)
54+
self.assertEqual(enriched_span.start_time, 1000000000)
55+
self.assertEqual(enriched_span.end_time, 2000000000)
56+
self.assertEqual(enriched_span.status, mock_span.status)
57+
self.assertEqual(enriched_span.kind, "INTERNAL")
58+
self.assertEqual(enriched_span.events, [])
59+
self.assertEqual(enriched_span.links, [])
60+
self.assertEqual(enriched_span.resource, mock_span.resource)
61+
self.assertEqual(enriched_span.instrumentation_scope, mock_span.instrumentation_scope)
62+
63+
64+
if __name__ == "__main__":
65+
unittest.main()
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
# Copyright (c) Microsoft Corporation.
2+
# Licensed under the MIT License.
3+
4+
"""Tests for enriching_span_processor module."""
5+
6+
import unittest
7+
from unittest.mock import Mock
8+
9+
from microsoft_agents_a365.observability.core.exporters.enriching_span_processor import (
10+
_EnrichingBatchSpanProcessor,
11+
_span_enrichers,
12+
register_span_enricher,
13+
unregister_span_enricher,
14+
)
15+
16+
17+
class TestSpanEnricherRegistry(unittest.TestCase):
18+
"""Test suite for span enricher registration functions."""
19+
20+
def setUp(self):
21+
"""Clear enrichers before each test."""
22+
_span_enrichers.clear()
23+
24+
def tearDown(self):
25+
"""Clear enrichers after each test."""
26+
_span_enrichers.clear()
27+
28+
def test_register_and_unregister_enricher(self):
29+
"""Test that enrichers can be registered and unregistered."""
30+
31+
# Define a simple enricher
32+
def my_enricher(span):
33+
return span
34+
35+
# Register
36+
register_span_enricher(my_enricher)
37+
self.assertIn(my_enricher, _span_enrichers)
38+
self.assertEqual(len(_span_enrichers), 1)
39+
40+
# Duplicate registration should not add again
41+
register_span_enricher(my_enricher)
42+
self.assertEqual(len(_span_enrichers), 1)
43+
44+
# Unregister
45+
unregister_span_enricher(my_enricher)
46+
self.assertNotIn(my_enricher, _span_enrichers)
47+
self.assertEqual(len(_span_enrichers), 0)
48+
49+
def test_unregister_nonexistent_enricher_does_not_raise(self):
50+
"""Test that unregistering a non-existent enricher doesn't raise an error."""
51+
52+
def my_enricher(span):
53+
return span
54+
55+
# Should not raise
56+
unregister_span_enricher(my_enricher)
57+
self.assertEqual(len(_span_enrichers), 0)
58+
59+
60+
class TestEnrichingBatchSpanProcessor(unittest.TestCase):
61+
"""Test suite for _EnrichingBatchSpanProcessor."""
62+
63+
def setUp(self):
64+
"""Clear enrichers before each test."""
65+
_span_enrichers.clear()
66+
67+
def tearDown(self):
68+
"""Clear enrichers after each test."""
69+
_span_enrichers.clear()
70+
71+
def test_on_end_applies_enrichers_to_span(self):
72+
"""Test that on_end applies all registered enrichers to the span."""
73+
# Create processor with a mock exporter
74+
mock_exporter = Mock()
75+
processor = _EnrichingBatchSpanProcessor(mock_exporter)
76+
77+
# Register an enricher that tracks what it receives and returns
78+
received_spans = []
79+
80+
def enricher(span):
81+
received_spans.append(span)
82+
# Return a mock enriched span
83+
enriched = Mock(name="enriched_span")
84+
enriched.context = span.context
85+
return enriched
86+
87+
register_span_enricher(enricher)
88+
89+
# Create a mock span
90+
original_span = Mock(name="original_span")
91+
original_span.context = Mock()
92+
original_span.context.trace_id = 123
93+
original_span.context.span_id = 456
94+
95+
# Call on_end
96+
processor.on_end(original_span)
97+
98+
# Verify enricher was called with the original span
99+
self.assertEqual(len(received_spans), 1)
100+
self.assertEqual(received_spans[0], original_span)
101+
102+
# Cleanup
103+
processor.shutdown()
104+
105+
def test_on_end_continues_if_enricher_raises_exception(self):
106+
"""Test that on_end continues processing even if an enricher raises an exception."""
107+
mock_exporter = Mock()
108+
processor = _EnrichingBatchSpanProcessor(mock_exporter)
109+
110+
# Track which enrichers were called
111+
called_enrichers = []
112+
113+
def failing_enricher(span):
114+
called_enrichers.append("failing")
115+
raise ValueError("Enricher failed!")
116+
117+
def succeeding_enricher(span):
118+
called_enrichers.append("succeeding")
119+
return span
120+
121+
register_span_enricher(failing_enricher)
122+
register_span_enricher(succeeding_enricher)
123+
124+
# Create a mock span
125+
original_span = Mock(name="original_span")
126+
original_span.context = Mock()
127+
original_span.context.trace_id = 123
128+
original_span.context.span_id = 456
129+
130+
# Should not raise despite failing enricher
131+
processor.on_end(original_span)
132+
133+
# Verify both enrichers were called (failing one didn't stop the chain)
134+
self.assertIn("failing", called_enrichers)
135+
self.assertIn("succeeding", called_enrichers)
136+
137+
# Cleanup
138+
processor.shutdown()
139+
140+
141+
if __name__ == "__main__":
142+
unittest.main()
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
# Copyright (c) Microsoft Corporation.
2+
# Licensed under the MIT License.
3+
4+
"""Tests for Semantic Kernel span enricher."""
5+
6+
import unittest
7+
from unittest.mock import Mock
8+
9+
from microsoft_agents_a365.observability.core.constants import (
10+
GEN_AI_INPUT_MESSAGES_KEY,
11+
GEN_AI_OUTPUT_MESSAGES_KEY,
12+
)
13+
from microsoft_agents_a365.observability.extensions.semantickernel.span_enricher import (
14+
enrich_semantic_kernel_span,
15+
)
16+
17+
18+
class TestSemanticKernelSpanEnricher(unittest.TestCase):
19+
"""Test suite for enrich_semantic_kernel_span function."""
20+
21+
def test_invoke_agent_span_extracts_content_from_messages(self):
22+
"""Test that invoke_agent spans have content extracted from input/output messages."""
23+
# Create a mock span with invoke_agent name and message attributes
24+
mock_span = Mock()
25+
mock_span.name = "invoke_agent test-agent"
26+
mock_span.attributes = {
27+
"gen_ai.agent.invocation_input": '[{"role": "user", "content": "Hello"}]',
28+
"gen_ai.agent.invocation_output": '[{"role": "assistant", "content": "Hi there!"}]',
29+
}
30+
31+
# Enrich the span
32+
enriched = enrich_semantic_kernel_span(mock_span)
33+
34+
# Verify it returns an EnrichedReadableSpan with extracted content
35+
self.assertNotEqual(enriched, mock_span)
36+
attributes = enriched.attributes
37+
# extract_content_as_string_list returns a JSON string
38+
self.assertEqual(attributes[GEN_AI_INPUT_MESSAGES_KEY], '["Hello"]')
39+
self.assertEqual(attributes[GEN_AI_OUTPUT_MESSAGES_KEY], '["Hi there!"]')
40+
41+
def test_non_matching_span_returns_original(self):
42+
"""Test that spans not matching invoke_agent or execute_tool are returned unchanged."""
43+
# Create a mock span with a different operation name
44+
mock_span = Mock()
45+
mock_span.name = "some_other_operation"
46+
mock_span.attributes = {
47+
"some_key": "some_value",
48+
}
49+
50+
# Enrich the span
51+
result = enrich_semantic_kernel_span(mock_span)
52+
53+
# Verify it returns the original span unchanged
54+
self.assertEqual(result, mock_span)
55+
56+
57+
if __name__ == "__main__":
58+
unittest.main()

0 commit comments

Comments
 (0)