From dbb6176aa2c1e92cb5b43cdfee0f226ba0482fca Mon Sep 17 00:00:00 2001 From: Hector Hernandez <39923391+hectorhdzg@users.noreply.github.com> Date: Thu, 9 Apr 2026 16:49:10 -0700 Subject: [PATCH 1/2] Add test coverage --- .coverage | Bin 0 -> 53248 bytes dev_requirements.txt | 1 + pyproject.toml | 8 + .../_browser_sdk_loader/django_middleware.py | 3 +- .../browserSdkLoader/test_init.py | 176 ++++++++++++++ .../browserSdkLoader/test_snippet_injector.py | 170 ++++++++++++++ tests/azure_monitor/test_configure.py | 217 ++++++++---------- .../utils/test_instrumentation.py | 127 ++++++++++ tests/conftest.py | 5 - tests/test_distro.py | 61 +++++ tests/test_instrumentation.py | 127 ++++++++++ tox.ini | 2 +- 12 files changed, 769 insertions(+), 128 deletions(-) create mode 100644 .coverage create mode 100644 tests/azure_monitor/browserSdkLoader/test_init.py create mode 100644 tests/azure_monitor/utils/test_instrumentation.py create mode 100644 tests/test_instrumentation.py diff --git a/.coverage b/.coverage new file mode 100644 index 0000000000000000000000000000000000000000..b80673b400249dd2fdae8e7cbd45f9cb6140d9f6 GIT binary patch literal 53248 zcmeI4eT>}16~OIX?}zupbKiTL0Ou6a%fa1+E2Js3kc2b}l$0a{B8o0JzFps*lU;jb zdvkX{dTSC2YDJMyB@|Ig{;5Dzs`#fs)zTbaZJ|Y|{85l7Ar<9AAOa1gfR1GQ#@;>e zCVL1G2b$co?sn|)%zN{G^X83bcV{un>NG>1)iCp5}@t-HY#a|m&BmMj!+`t0~AOR$R z1Wp?Q_Qpgcl}@v_?6TyMykZ%0Mlsyi3`NT*rrU}oYPK)dw|goUNwv4Lw<(TI zvXOzmc!M>N;hA#9C}}}I8gkmBZmp80?kFj8d zHTxYiG`(M>Mt}z_j)zkhwX+O(r7~ge$it(PB$q7Rxt5^wr6G7p>}icgQawHFZ95!C zp?IVb)iP&K_y(t$&7Q+`=>56E{uhiRUW1$V=NM0Xl^fkw0Oa%rA#DZF}(UO*-L&c;vWb~biA?K9A>RmT1eASKw zt~11kQ)_zWa-CThkcMVYV|F?mNiAH+-oiU>RPC*55@~RuT2D3!qqSQFMK()DEs%Ux zkgV14qGWB1RPTT}1CelQ)xx>Vsg{$5swYu9!bMV@oviJS2pX5@f2hF-n&S#%*IBkM7L|?;uS^`d!Zx1^33lQ|_f3Ts+G{{{~%OiTpa{QNGI_^p591X%sSh#SvZ4E3)R!4N&6>6LhMk=(OSZS!snQ!acFDp$Qi3+d#e$!ik_ z6Zr%me=z=?*i*4#eulq;Pet#Io*Q{Ma#8r%@H(i12NFO6NB{|(q6Dr=bBw)c^~J+i zD@ENL){6>kN6#y;gUy&6$f~Ag=mW(`YfRUMO(Qd0P&0;Z>Z4X;$zjQwEGi~!*t@wK zr7_)^fU3P_x;*G7j69L3<$u z+9lt$mn=1J4yzi_C=~z@{1$<(fx4TzK;2Egt7~Gcsj0=HVo9pDP00WeP;PXBa@BX` z?pF2&ONU)Q0G&HJKxfW(omo}RX}W2t8FSbKx+N1jIR`)904gWiL1oEzmCekAjmo2H zu4KqAV5r;Jk2fT?iyDJ8_A}&&5}BAi3|vCD%La`XIs4*MeG-tKN{XC@Yet7U7Wqk2YFv?{ok!|w&cI| zxz_)wn>qU?|J5~luxzsa?=m@C^&@VoV~?=t#hpZTeoudYX7x)5Bx1T&fe(1g5%czWSp~i_^+ql zGrjY=^}ld(y35nmu>Mcpz}YMOx3aeWPmFN(TK~n@FR9`uCu%hbncn(8W^wj*|IKQq z(_5JFCynLo`agPd2CrvBWFu!U_5qw14#Gd+>}5VkJbwKj+Qiv?K1i;2R&E1lpY4Op z+4(c*6Hu><0+(?%e}e0O{QW;%2p|C@fCP{L5ldWp?3K3e+b zr$?W=X#dn}%zvl$?%m7(?4H8jN)Ks;iXtiBE$(^i&ig8#aC{47bR^1$Dr}O#=@}j6 z)P)~Rz50BlOMuHkp`5z>y;mN4e&&c>IZ#>l@sS^flaMbY+13Q4=fuk_%N(s#dJYsH zN-mE>RxvtVIfG%=eeIUpMV2=1?Z^MR?(qAQNA<(4mCB_(+;rvMXHrad_u96{XZZ5I z-MK~V%2?C{08{}28m%IeO6u65#Nv@}e@IV4c00|%gB!C2v01`j~NB{{S0VFVQ3D8A3 z8%{e(y4y)YDJKbbISJS4BrP3Il4^I7);1?0txl3?agq+=B!aWhXOl_iDxPqXXxvG< zVouV{J4r0+Bz(k4I>S!V9&(a4&PgIcCkX^Ncmu%s{GVx=w|#|%BLO6U1dsp{Kmter z2_OL^fCP{L5||eR=;!~q{+}0JjYc8?B!C2v01`j~NB{{S0VIF~kia}9K==RS`hOlf e0Gf^jkN^@u0!RP}AOR$R1dsp{KmzlE!2bX|+$7!r literal 0 HcmV?d00001 diff --git a/dev_requirements.txt b/dev_requirements.txt index f48cbaee..1398bb12 100644 --- a/dev_requirements.txt +++ b/dev_requirements.txt @@ -1,2 +1,3 @@ pytest>=8.0 +pytest-cov>=5.0 black>=24.0 diff --git a/pyproject.toml b/pyproject.toml index f83b342a..73272e48 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,6 +29,7 @@ dependencies = [ [project.optional-dependencies] test = [ "pytest>=8.0", + "pytest-cov>=5.0", ] dev = [ "black>=24.0", @@ -36,6 +37,7 @@ dev = [ "mypy>=1.10", "pyright>=1.1", "pytest>=8.0", + "pytest-cov>=5.0", ] [tool.setuptools] @@ -55,6 +57,12 @@ py-version = "3.10" [tool.pylint.format] max-line-length = 120 +[tool.coverage.run] +source = ["microsoft"] + +[tool.coverage.report] +show_missing = true + [tool.pylint.messages_control] disable = [ "missing-function-docstring", diff --git a/src/microsoft/opentelemetry/_azure_monitor/_browser_sdk_loader/django_middleware.py b/src/microsoft/opentelemetry/_azure_monitor/_browser_sdk_loader/django_middleware.py index b0cfaf51..acf3db55 100644 --- a/src/microsoft/opentelemetry/_azure_monitor/_browser_sdk_loader/django_middleware.py +++ b/src/microsoft/opentelemetry/_azure_monitor/_browser_sdk_loader/django_middleware.py @@ -42,7 +42,8 @@ def __init__(self, get_response: Callable[[HttpRequest], HttpResponse]) -> None: :type get_response: Callable[[HttpRequest], HttpResponse] :rtype: None """ - super().__init__(get_response) + if MiddlewareMixin is not object: + super().__init__(get_response) self.get_response = get_response self._injector: Optional[WebSnippetInjector] = None if not DJANGO_AVAILABLE: diff --git a/tests/azure_monitor/browserSdkLoader/test_init.py b/tests/azure_monitor/browserSdkLoader/test_init.py new file mode 100644 index 00000000..08be25c3 --- /dev/null +++ b/tests/azure_monitor/browserSdkLoader/test_init.py @@ -0,0 +1,176 @@ +# ------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# -------------------------------------------------------------------------- + +import unittest +from unittest.mock import MagicMock, patch + +from microsoft.opentelemetry._azure_monitor._browser_sdk_loader._config import BrowserSDKConfig + + +class TestSetupSnippetInjection(unittest.TestCase): + def setUp(self): + self.config = BrowserSDKConfig( + enabled=True, + connection_string="InstrumentationKey=test;IngestionEndpoint=https://test.in.ai.azure.com/", + ) + + @patch( + "microsoft.opentelemetry._azure_monitor._browser_sdk_loader._setup_django_injection" + ) + def test_setup_snippet_injection_calls_django(self, mock_django): + from microsoft.opentelemetry._azure_monitor._browser_sdk_loader import setup_snippet_injection + + setup_snippet_injection(self.config) + mock_django.assert_called_once_with(self.config) + + @patch( + "microsoft.opentelemetry._azure_monitor._browser_sdk_loader._setup_django_injection", + side_effect=Exception("boom"), + ) + def test_setup_snippet_injection_handles_exception(self, mock_django): + from microsoft.opentelemetry._azure_monitor._browser_sdk_loader import setup_snippet_injection + + # Should not raise + setup_snippet_injection(self.config) + + +class TestSetupDjangoInjection(unittest.TestCase): + def setUp(self): + self.config = BrowserSDKConfig( + enabled=True, + connection_string="InstrumentationKey=test;IngestionEndpoint=https://test.in.ai.azure.com/", + ) + + @patch.dict("sys.modules", {"django": None, "django.conf": None}) + def test_django_not_available(self): + from microsoft.opentelemetry._azure_monitor._browser_sdk_loader import _setup_django_injection + + # Should not raise when django is not importable + _setup_django_injection(self.config) + + @patch( + "microsoft.opentelemetry._azure_monitor._browser_sdk_loader._register_django_middleware" + ) + def test_django_available_and_configured(self, mock_register): + settings_mock = MagicMock() + settings_mock.configured = True + + with patch.dict("sys.modules", { + "django": MagicMock(), + "django.conf": MagicMock(settings=settings_mock), + }): + from microsoft.opentelemetry._azure_monitor._browser_sdk_loader import _setup_django_injection + + _setup_django_injection(self.config) + mock_register.assert_called_once_with(self.config) + + def test_django_not_configured(self): + settings_mock = MagicMock() + settings_mock.configured = False + + with patch.dict("sys.modules", { + "django": MagicMock(), + "django.conf": MagicMock(settings=settings_mock), + }): + from microsoft.opentelemetry._azure_monitor._browser_sdk_loader import _setup_django_injection + + # Should return early without raising + _setup_django_injection(self.config) + + +class TestRegisterDjangoMiddleware(unittest.TestCase): + def setUp(self): + self.config = BrowserSDKConfig( + enabled=True, + connection_string="InstrumentationKey=test;IngestionEndpoint=https://test.in.ai.azure.com/", + ) + + def test_register_with_middleware_attr(self): + settings_mock = MagicMock() + settings_mock.MIDDLEWARE = ["django.middleware.common.CommonMiddleware"] + # Ensure MIDDLEWARE_CLASSES doesn't exist + del settings_mock.MIDDLEWARE_CLASSES + + with patch.dict("sys.modules", { + "django": MagicMock(), + "django.conf": MagicMock(settings=settings_mock), + }): + from microsoft.opentelemetry._azure_monitor._browser_sdk_loader import _register_django_middleware + + _register_django_middleware(self.config) + # Our middleware should have been appended + middleware_list = settings_mock.MIDDLEWARE + self.assertIn( + "microsoft.opentelemetry._azure_monitor._browser_sdk_loader.django_middleware.ApplicationInsightsWebSnippetMiddleware", + middleware_list, + ) + + def test_register_with_middleware_classes_attr(self): + settings_mock = MagicMock(spec=[]) + settings_mock.MIDDLEWARE_CLASSES = ["django.middleware.common.CommonMiddleware"] + + with patch.dict("sys.modules", { + "django": MagicMock(), + "django.conf": MagicMock(settings=settings_mock), + }): + from microsoft.opentelemetry._azure_monitor._browser_sdk_loader import _register_django_middleware + + _register_django_middleware(self.config) + self.assertIn( + "microsoft.opentelemetry._azure_monitor._browser_sdk_loader.django_middleware.ApplicationInsightsWebSnippetMiddleware", + settings_mock.MIDDLEWARE_CLASSES, + ) + + def test_already_registered(self): + middleware_path = "microsoft.opentelemetry._azure_monitor._browser_sdk_loader.django_middleware.ApplicationInsightsWebSnippetMiddleware" + settings_mock = MagicMock() + settings_mock.MIDDLEWARE = [middleware_path] + + with patch.dict("sys.modules", { + "django": MagicMock(), + "django.conf": MagicMock(settings=settings_mock), + }): + from microsoft.opentelemetry._azure_monitor._browser_sdk_loader import _register_django_middleware + + _register_django_middleware(self.config) + # Should not duplicate + self.assertEqual(settings_mock.MIDDLEWARE.count(middleware_path), 1) + + +class TestStoreDjangoConfig(unittest.TestCase): + def setUp(self): + self.config = BrowserSDKConfig( + enabled=True, + connection_string="InstrumentationKey=test;IngestionEndpoint=https://test.in.ai.azure.com/", + ) + + def test_store_config(self): + settings_mock = MagicMock(spec=[]) + # No AZURE_MONITOR_WEB_SNIPPET_CONFIG attribute yet + + with patch.dict("sys.modules", { + "django": MagicMock(), + "django.conf": MagicMock(settings=settings_mock), + }): + from microsoft.opentelemetry._azure_monitor._browser_sdk_loader import _store_django_config + + _store_django_config(self.config) + self.assertEqual(settings_mock.AZURE_MONITOR_WEB_SNIPPET_CONFIG, self.config) + + def test_store_config_already_set(self): + existing_config = BrowserSDKConfig(enabled=False) + settings_mock = MagicMock() + settings_mock.AZURE_MONITOR_WEB_SNIPPET_CONFIG = existing_config + + with patch.dict("sys.modules", { + "django": MagicMock(), + "django.conf": MagicMock(settings=settings_mock), + }): + from microsoft.opentelemetry._azure_monitor._browser_sdk_loader import _store_django_config + + _store_django_config(self.config) + # Should not overwrite + self.assertEqual(settings_mock.AZURE_MONITOR_WEB_SNIPPET_CONFIG, existing_config) diff --git a/tests/azure_monitor/browserSdkLoader/test_snippet_injector.py b/tests/azure_monitor/browserSdkLoader/test_snippet_injector.py index 02797fab..2a1915fe 100644 --- a/tests/azure_monitor/browserSdkLoader/test_snippet_injector.py +++ b/tests/azure_monitor/browserSdkLoader/test_snippet_injector.py @@ -377,6 +377,176 @@ def test_should_inject_script_url_detection(self): result = self.injector.should_inject("GET", "text/html", content) self.assertFalse(result, f"Should detect existing script URL in: {content}") + def test_mark_browser_loader_feature_disabled(self): + """Test _mark_browser_loader_feature does nothing when disabled.""" + from microsoft.opentelemetry._azure_monitor._browser_sdk_loader.snippet_injector import ( + _mark_browser_loader_feature, + ) + + # Should not raise + _mark_browser_loader_feature(False) + + def test_mark_browser_loader_feature_enabled_statsbeat_available(self): + """Test _mark_browser_loader_feature when statsbeat is available.""" + from microsoft.opentelemetry._azure_monitor._browser_sdk_loader.snippet_injector import ( + _mark_browser_loader_feature, + ) + + mock_module = unittest.mock.MagicMock() + mock_module.is_statsbeat_enabled.return_value = True + mock_module.get_statsbeat_shutdown.return_value = False + mock_module.get_statsbeat_browser_sdk_loader_feature_set.return_value = False + with patch.dict("sys.modules", { + "azure.monitor.opentelemetry.exporter.statsbeat._state": mock_module, + }): + _mark_browser_loader_feature(True) + mock_module.set_statsbeat_browser_sdk_loader_feature_set.assert_called_once() + + def test_mark_browser_loader_feature_enabled_statsbeat_exception(self): + """Test _mark_browser_loader_feature handles statsbeat exception.""" + from microsoft.opentelemetry._azure_monitor._browser_sdk_loader.snippet_injector import ( + _mark_browser_loader_feature, + ) + + mock_module = unittest.mock.MagicMock() + mock_module.is_statsbeat_enabled.side_effect = Exception("boom") + with patch.dict("sys.modules", { + "azure.monitor.opentelemetry.exporter.statsbeat._state": mock_module, + }): + # Should not raise + _mark_browser_loader_feature(True) + + def test_inject_snippet_no_insertion_point(self): + """Test inject_snippet returns original content when no insertion point found.""" + content = b"just some text without html tags" + result = self.injector.inject_snippet(content) + self.assertEqual(result, content) + + def test_inject_snippet_exception(self): + """Test inject_snippet handles exceptions gracefully.""" + content = b"" + with patch.object(self.injector, "_find_insertion_point", side_effect=Exception("boom")): + result = self.injector.inject_snippet(content) + self.assertEqual(result, content) + + def test_find_insertion_point_body_fallback(self): + """Test _find_insertion_point falls back to when no .""" + html = "

content

" + idx = self.injector._find_insertion_point(html) + self.assertEqual(idx, html.lower().find(" when no or .""" + html = "

content

" + idx = self.injector._find_insertion_point(html) + self.assertEqual(idx, 0) + + def test_find_insertion_point_no_tags(self): + """Test _find_insertion_point returns -1 when no tags found.""" + html = "just plain text" + idx = self.injector._find_insertion_point(html) + self.assertEqual(idx, -1) + + def test_has_existing_web_sdk_plain(self): + """Test _has_existing_web_sdk with plain content.""" + content = b"" + self.assertTrue(self.injector._has_existing_web_sdk(content)) + + def test_has_existing_web_sdk_no_sdk(self): + """Test _has_existing_web_sdk with no SDK present.""" + content = b"hello" + self.assertFalse(self.injector._has_existing_web_sdk(content)) + + def test_has_existing_web_sdk_gzip(self): + """Test _has_existing_web_sdk with gzip compressed content.""" + content = gzip.compress(b"") + self.assertTrue(self.injector._has_existing_web_sdk(content, "gzip")) + + def test_appears_compressed_gzip(self): + """Test _appears_compressed detects gzip magic bytes.""" + self.assertTrue(self.injector._appears_compressed(b"\x1f\x8b\x08rest")) + + def test_appears_compressed_zlib(self): + """Test _appears_compressed detects zlib magic bytes.""" + self.assertTrue(self.injector._appears_compressed(b"\x78\x9c\x00rest")) + + def test_appears_compressed_short_content(self): + """Test _appears_compressed returns False for short content.""" + self.assertFalse(self.injector._appears_compressed(b"ab")) + + def test_appears_compressed_plain(self): + """Test _appears_compressed returns False for plain content.""" + self.assertFalse(self.injector._appears_compressed(b"hello")) + + def test_decompress_content_deflate(self): + """Test _decompress_content with deflate encoding.""" + import zlib + + original = b"hello" + compressed = zlib.compress(original) + result = self.injector._decompress_content(compressed, "deflate") + self.assertEqual(result, original) + + def test_compress_content_deflate(self): + """Test _compress_content with deflate encoding.""" + import zlib + + original = b"hello" + result = self.injector._compress_content(original, "deflate") + decompressed = zlib.decompress(result) + self.assertEqual(decompressed, original) + + @patch("microsoft.opentelemetry._azure_monitor._browser_sdk_loader.snippet_injector.HAS_ZLIB", False) + def test_decompress_content_deflate_not_available(self): + """Test _decompress_content when zlib is not available.""" + content = b"some content" + result = self.injector._decompress_content(content, "deflate") + self.assertEqual(result, content) + + @patch("microsoft.opentelemetry._azure_monitor._browser_sdk_loader.snippet_injector.HAS_ZLIB", False) + def test_compress_content_deflate_not_available(self): + """Test _compress_content when zlib is not available.""" + content = b"some content" + result = self.injector._compress_content(content, "deflate") + self.assertEqual(result, content) + + def test_format_config_value_dict(self): + """Test _format_config_value with dict value.""" + result = self.injector._format_config_value({"key": "val"}) + self.assertIn("key", result) + self.assertIn('"val"', result) + + def test_format_config_value_other(self): + """Test _format_config_value with unsupported type.""" + result = self.injector._format_config_value([1, 2, 3]) + self.assertEqual(result, '"[1, 2, 3]"') + + def test_get_decompressed_content_caching(self): + """Test _get_decompressed_content uses cache on second call.""" + content = gzip.compress(b"hello") + # First call populates cache + result1 = self.injector._get_decompressed_content(content, "gzip") + # Second call should use cache + result2 = self.injector._get_decompressed_content(content, "gzip") + self.assertEqual(result1, result2) + self.assertEqual(result1, b"hello") + + def test_get_decompressed_content_auto_detect_gzip(self): + """Test _get_decompressed_content auto-detects gzip without encoding.""" + original = b"hello" + content = gzip.compress(original) + result = self.injector._get_decompressed_content(content, None) + self.assertEqual(result, original) + + def test_inject_with_compression_gzip_recompress(self): + """Test inject_with_compression recompresses after injection.""" + html = b"Test" + compressed = gzip.compress(html) + result_content, result_encoding = self.injector.inject_with_compression(compressed, "gzip") + self.assertEqual(result_encoding, "gzip") + decompressed = gzip.decompress(result_content) + self.assertIn(b"appInsightsSDK", decompressed) + if __name__ == "__main__": unittest.main() diff --git a/tests/azure_monitor/test_configure.py b/tests/azure_monitor/test_configure.py index 87cafb0c..d128ee73 100644 --- a/tests/azure_monitor/test_configure.py +++ b/tests/azure_monitor/test_configure.py @@ -18,7 +18,8 @@ from microsoft.opentelemetry._azure_monitor._configure import ( _send_attach_warning, - _setup_instrumentations, + _setup_azure_instrumentations, + _setup_browser_sdk_loader, _setup_live_metrics, _setup_logging, _setup_metrics, @@ -26,6 +27,7 @@ configure_azure_monitor, ) from microsoft.opentelemetry._azure_monitor._diagnostics.diagnostic_logging import _DISTRO_DETECTS_ATTACH +from microsoft.opentelemetry._distro import _setup_instrumentations TEST_RESOURCE = Resource({"foo": "bar"}) @@ -36,7 +38,10 @@ class TestConfigure(unittest.TestCase): "microsoft.opentelemetry._azure_monitor._configure._send_attach_warning", ) @patch( - "microsoft.opentelemetry._azure_monitor._configure._setup_instrumentations", + "microsoft.opentelemetry._azure_monitor._configure._setup_browser_sdk_loader", + ) + @patch( + "microsoft.opentelemetry._azure_monitor._configure._setup_azure_instrumentations", ) @patch( "microsoft.opentelemetry._azure_monitor._configure._setup_live_metrics", @@ -56,7 +61,8 @@ def test_configure_azure_monitor( logging_mock, metrics_mock, live_metrics_mock, - instrumentation_mock, + azure_instr_mock, + browser_sdk_mock, detect_attach_mock, ): kwargs = { @@ -67,11 +73,15 @@ def test_configure_azure_monitor( logging_mock.assert_called_once() metrics_mock.assert_called_once() live_metrics_mock.assert_called_once() - instrumentation_mock.assert_called_once() + azure_instr_mock.assert_called_once() + browser_sdk_mock.assert_called_once() detect_attach_mock.assert_called_once() @patch( - "microsoft.opentelemetry._azure_monitor._configure._setup_instrumentations", + "microsoft.opentelemetry._azure_monitor._configure._setup_browser_sdk_loader", + ) + @patch( + "microsoft.opentelemetry._azure_monitor._configure._setup_azure_instrumentations", ) @patch( "microsoft.opentelemetry._azure_monitor._configure._setup_live_metrics", @@ -95,7 +105,8 @@ def test_configure_azure_monitor_disable_tracing( logging_mock, metrics_mock, live_metrics_mock, - instrumentation_mock, + azure_instr_mock, + browser_sdk_mock, ): configurations = { "connection_string": "test_cs", @@ -122,12 +133,15 @@ def test_configure_azure_monitor_disable_tracing( logging_mock.assert_called_once_with(configurations) metrics_mock.assert_called_once_with(configurations) live_metrics_mock.assert_called_once_with(configurations) - instrumentation_mock.assert_called_once_with(configurations) + azure_instr_mock.assert_called_once_with(configurations) # Assert setup_metrics is called before setup_logging self.assertLess(call_order.index("metrics"), call_order.index("logging")) @patch( - "microsoft.opentelemetry._azure_monitor._configure._setup_instrumentations", + "microsoft.opentelemetry._azure_monitor._configure._setup_browser_sdk_loader", + ) + @patch( + "microsoft.opentelemetry._azure_monitor._configure._setup_azure_instrumentations", ) @patch( "microsoft.opentelemetry._azure_monitor._configure._setup_live_metrics", @@ -151,7 +165,8 @@ def test_configure_azure_monitor_disable_logging( logging_mock, metrics_mock, live_metrics_mock, - instrumentation_mock, + azure_instr_mock, + browser_sdk_mock, ): configurations = { "connection_string": "test_cs", @@ -173,12 +188,15 @@ def test_configure_azure_monitor_disable_logging( logging_mock.assert_not_called() metrics_mock.assert_called_once_with(configurations) live_metrics_mock.assert_called_once_with(configurations) - instrumentation_mock.assert_called_once_with(configurations) + azure_instr_mock.assert_called_once_with(configurations) # Assert setup_metrics is called before setup_tracing self.assertLess(call_order.index("metrics"), call_order.index("tracing")) @patch( - "microsoft.opentelemetry._azure_monitor._configure._setup_instrumentations", + "microsoft.opentelemetry._azure_monitor._configure._setup_browser_sdk_loader", + ) + @patch( + "microsoft.opentelemetry._azure_monitor._configure._setup_azure_instrumentations", ) @patch( "microsoft.opentelemetry._azure_monitor._configure._setup_live_metrics", @@ -202,7 +220,8 @@ def test_configure_azure_monitor_disable_metrics( logging_mock, metrics_mock, live_metrics_mock, - instrumentation_mock, + azure_instr_mock, + browser_sdk_mock, ): configurations = { "connection_string": "test_cs", @@ -219,10 +238,13 @@ def test_configure_azure_monitor_disable_metrics( logging_mock.assert_called_once_with(configurations) metrics_mock.assert_not_called() live_metrics_mock.assert_called_once_with(configurations) - instrumentation_mock.assert_called_once_with(configurations) + azure_instr_mock.assert_called_once_with(configurations) @patch( - "microsoft.opentelemetry._azure_monitor._configure._setup_instrumentations", + "microsoft.opentelemetry._azure_monitor._configure._setup_browser_sdk_loader", + ) + @patch( + "microsoft.opentelemetry._azure_monitor._configure._setup_azure_instrumentations", ) @patch( "microsoft.opentelemetry._azure_monitor._configure._setup_live_metrics", @@ -246,7 +268,8 @@ def test_configure_azure_monitor_enable_live_metrics( logging_mock, metrics_mock, live_metrics_mock, - instrumentation_mock, + azure_instr_mock, + browser_sdk_mock, ): configurations = { "connection_string": "test_cs", @@ -268,13 +291,16 @@ def test_configure_azure_monitor_enable_live_metrics( logging_mock.assert_called_once_with(configurations) metrics_mock.assert_called_once_with(configurations) live_metrics_mock.assert_called_once_with(configurations) - instrumentation_mock.assert_called_once_with(configurations) + azure_instr_mock.assert_called_once_with(configurations) # Assert setup_metrics is called before setup_logging and setup_tracing self.assertLess(call_order.index("metrics"), call_order.index("logging")) self.assertLess(call_order.index("metrics"), call_order.index("tracing")) @patch( - "microsoft.opentelemetry._azure_monitor._configure._setup_instrumentations", + "microsoft.opentelemetry._azure_monitor._configure._setup_browser_sdk_loader", + ) + @patch( + "microsoft.opentelemetry._azure_monitor._configure._setup_azure_instrumentations", ) @patch( "microsoft.opentelemetry._azure_monitor._configure._setup_live_metrics", @@ -298,7 +324,8 @@ def test_configure_azure_monitor_disable_perf_counters( logging_mock, metrics_mock, live_metrics_mock, - instrumentation_mock, + azure_instr_mock, + browser_sdk_mock, ): configurations = { "connection_string": "test_cs", @@ -315,7 +342,7 @@ def test_configure_azure_monitor_disable_perf_counters( logging_mock.assert_called_once_with(configurations) metrics_mock.assert_called_once_with(configurations) live_metrics_mock.assert_not_called() - instrumentation_mock.assert_called_once_with(configurations) + azure_instr_mock.assert_called_once_with(configurations) @patch( "microsoft.opentelemetry._azure_monitor._configure._PerformanceCountersSpanProcessor", @@ -368,27 +395,17 @@ def test_setup_tracing( "span_processors": [custom_sp], "resource": TEST_RESOURCE, } - with patch("microsoft.opentelemetry._azure_monitor._configure._is_instrumentation_enabled") as instr_mock: - instr_mock.return_value = True - with patch.dict( - "sys.modules", - { - "azure.core.settings": Mock(settings=settings_mock), - "azure.core.tracing.ext.opentelemetry_span": Mock(OpenTelemetrySpan=opentelemetry_span_mock), - }, - ): - _setup_tracing(configurations) - sampler_mock.assert_called_once_with(sampling_ratio=0.5) - tp_mock.assert_called_once_with(sampler=sampler_init_mock, resource=TEST_RESOURCE) - set_tracer_provider_mock.assert_called_once_with(tp_init_mock) - trace_exporter_mock.assert_called_once_with(**configurations) - bsp_mock.assert_called_once_with(trace_exp_init_mock) - self.assertEqual(tp_init_mock.add_span_processor.call_count, 3) - tp_init_mock.add_span_processor.assert_has_calls( - [call(custom_sp), call(pcsp_init_mock), call(bsp_init_mock)] - ) - self.assertEqual(settings_mock.tracing_implementation, opentelemetry_span_mock) - pcsp_mock.assert_called_once_with() + _setup_tracing(configurations) + sampler_mock.assert_called_once_with(sampling_ratio=0.5) + tp_mock.assert_called_once_with(sampler=sampler_init_mock, resource=TEST_RESOURCE) + set_tracer_provider_mock.assert_called_once_with(tp_init_mock) + trace_exporter_mock.assert_called_once_with(**configurations) + bsp_mock.assert_called_once_with(trace_exp_init_mock) + self.assertEqual(tp_init_mock.add_span_processor.call_count, 3) + tp_init_mock.add_span_processor.assert_has_calls( + [call(custom_sp), call(pcsp_init_mock), call(bsp_init_mock)] + ) + pcsp_mock.assert_called_once_with() @patch( "microsoft.opentelemetry._azure_monitor._configure._PerformanceCountersSpanProcessor", @@ -441,27 +458,17 @@ def test_setup_tracing_rate_limited_sampler( "span_processors": [custom_sp], "resource": TEST_RESOURCE, } - with patch("microsoft.opentelemetry._azure_monitor._configure._is_instrumentation_enabled") as instr_mock: - instr_mock.return_value = True - with patch.dict( - "sys.modules", - { - "azure.core.settings": Mock(settings=settings_mock), - "azure.core.tracing.ext.opentelemetry_span": Mock(OpenTelemetrySpan=opentelemetry_span_mock), - }, - ): - _setup_tracing(configurations) - sampler_mock.assert_called_once_with(target_spans_per_second_limit=2.0) - tp_mock.assert_called_once_with(sampler=sampler_init_mock, resource=TEST_RESOURCE) - set_tracer_provider_mock.assert_called_once_with(tp_init_mock) - trace_exporter_mock.assert_called_once_with(**configurations) - bsp_mock.assert_called_once_with(trace_exp_init_mock) - self.assertEqual(tp_init_mock.add_span_processor.call_count, 3) - tp_init_mock.add_span_processor.assert_has_calls( - [call(custom_sp), call(pcsp_init_mock), call(bsp_init_mock)] - ) - self.assertEqual(settings_mock.tracing_implementation, opentelemetry_span_mock) - pcsp_mock.assert_called_once_with() + _setup_tracing(configurations) + sampler_mock.assert_called_once_with(target_spans_per_second_limit=2.0) + tp_mock.assert_called_once_with(sampler=sampler_init_mock, resource=TEST_RESOURCE) + set_tracer_provider_mock.assert_called_once_with(tp_init_mock) + trace_exporter_mock.assert_called_once_with(**configurations) + bsp_mock.assert_called_once_with(trace_exp_init_mock) + self.assertEqual(tp_init_mock.add_span_processor.call_count, 3) + tp_init_mock.add_span_processor.assert_has_calls( + [call(custom_sp), call(pcsp_init_mock), call(bsp_init_mock)] + ) + pcsp_mock.assert_called_once_with() @patch( "microsoft.opentelemetry._azure_monitor._configure._PerformanceCountersSpanProcessor", @@ -514,25 +521,15 @@ def test_setup_tracing_perf_counters_disabled( "span_processors": [custom_sp], "resource": TEST_RESOURCE, } - with patch("microsoft.opentelemetry._azure_monitor._configure._is_instrumentation_enabled") as instr_mock: - instr_mock.return_value = True - with patch.dict( - "sys.modules", - { - "azure.core.settings": Mock(settings=settings_mock), - "azure.core.tracing.ext.opentelemetry_span": Mock(OpenTelemetrySpan=opentelemetry_span_mock), - }, - ): - _setup_tracing(configurations) - sampler_mock.assert_called_once_with(sampling_ratio=0.5) - tp_mock.assert_called_once_with(sampler=sampler_init_mock, resource=TEST_RESOURCE) - set_tracer_provider_mock.assert_called_once_with(tp_init_mock) - trace_exporter_mock.assert_called_once_with(**configurations) - bsp_mock.assert_called_once_with(trace_exp_init_mock) - self.assertEqual(tp_init_mock.add_span_processor.call_count, 2) - tp_init_mock.add_span_processor.assert_has_calls([call(custom_sp), call(bsp_init_mock)]) - self.assertEqual(settings_mock.tracing_implementation, opentelemetry_span_mock) - pcsp_mock.assert_not_called() + _setup_tracing(configurations) + sampler_mock.assert_called_once_with(sampling_ratio=0.5) + tp_mock.assert_called_once_with(sampler=sampler_init_mock, resource=TEST_RESOURCE) + set_tracer_provider_mock.assert_called_once_with(tp_init_mock) + trace_exporter_mock.assert_called_once_with(**configurations) + bsp_mock.assert_called_once_with(trace_exp_init_mock) + self.assertEqual(tp_init_mock.add_span_processor.call_count, 2) + tp_init_mock.add_span_processor.assert_has_calls([call(custom_sp), call(bsp_init_mock)]) + pcsp_mock.assert_not_called() @patch("microsoft.opentelemetry._azure_monitor._configure._PerformanceCountersLogRecordProcessor") @patch("microsoft.opentelemetry._azure_monitor._configure.getLogger") @@ -876,10 +873,10 @@ def test_setup_live_metrics( enable_live_metrics_mock.assert_called_once_with(**configurations) - @patch("microsoft.opentelemetry._azure_monitor._configure._ALL_SUPPORTED_INSTRUMENTED_LIBRARIES", ("test_instr2")) - @patch("microsoft.opentelemetry._azure_monitor._configure._is_instrumentation_enabled") - @patch("microsoft.opentelemetry._azure_monitor._configure.get_dist_dependency_conflicts") - @patch("microsoft.opentelemetry._azure_monitor._configure.entry_points") + @patch("microsoft.opentelemetry._distro._SUPPORTED_INSTRUMENTED_LIBRARIES", ("test_instr2",)) + @patch("microsoft.opentelemetry._distro._is_instrumentation_enabled") + @patch("microsoft.opentelemetry._distro.get_dist_dependency_conflicts") + @patch("microsoft.opentelemetry._distro.entry_points") def test_setup_instrumentations_lib_not_supported( self, iter_mock, @@ -898,34 +895,15 @@ def test_setup_instrumentations_lib_not_supported( dep_mock.return_value = None enabled_mock.return_value = True _setup_instrumentations({}) - dep_mock.assert_called_with(ep2_mock.dist) ep_mock.load.assert_not_called() ep2_mock.load.assert_called_once() instrumentor_mock.instrument.assert_called_once() - @patch("microsoft.opentelemetry._azure_monitor._configure._setup_additional_azure_sdk_instrumentations") - @patch("microsoft.opentelemetry._azure_monitor._configure._ALL_SUPPORTED_INSTRUMENTED_LIBRARIES", ("azure_sdk")) - @patch("microsoft.opentelemetry._azure_monitor._configure._is_instrumentation_enabled") - @patch("microsoft.opentelemetry._azure_monitor._configure.entry_points") - def test_setup_instrumentations_additional_azure( - self, - iter_mock, - enabled_mock, - additional_instrumentations_mock, - ): - ep_mock = Mock() - ep_mock.name = "azure_sdk" - iter_mock.return_value = (ep_mock,) - - enabled_mock.return_value = True - _setup_instrumentations({}) - additional_instrumentations_mock.assert_called_once() - - @patch("microsoft.opentelemetry._azure_monitor._configure._ALL_SUPPORTED_INSTRUMENTED_LIBRARIES", ("test_instr")) - @patch("microsoft.opentelemetry._azure_monitor._configure._is_instrumentation_enabled") - @patch("microsoft.opentelemetry._azure_monitor._configure._logger") - @patch("microsoft.opentelemetry._azure_monitor._configure.get_dist_dependency_conflicts") - @patch("microsoft.opentelemetry._azure_monitor._configure.entry_points") + @patch("microsoft.opentelemetry._distro._SUPPORTED_INSTRUMENTED_LIBRARIES", ("test_instr",)) + @patch("microsoft.opentelemetry._distro._is_instrumentation_enabled") + @patch("microsoft.opentelemetry._distro._logger") + @patch("microsoft.opentelemetry._distro.get_dist_dependency_conflicts") + @patch("microsoft.opentelemetry._distro.entry_points") def test_setup_instrumentations_conflict( self, iter_mock, @@ -943,16 +921,15 @@ def test_setup_instrumentations_conflict( dep_mock.return_value = True enabled_mock.return_value = True _setup_instrumentations({}) - dep_mock.assert_called_with(ep_mock.dist) ep_mock.load.assert_not_called() instrumentor_mock.instrument.assert_not_called() logger_mock.debug.assert_called_once() - @patch("microsoft.opentelemetry._azure_monitor._configure._ALL_SUPPORTED_INSTRUMENTED_LIBRARIES", ("test_instr")) - @patch("microsoft.opentelemetry._azure_monitor._configure._is_instrumentation_enabled") - @patch("microsoft.opentelemetry._azure_monitor._configure._logger") - @patch("microsoft.opentelemetry._azure_monitor._configure.get_dist_dependency_conflicts") - @patch("microsoft.opentelemetry._azure_monitor._configure.entry_points") + @patch("microsoft.opentelemetry._distro._SUPPORTED_INSTRUMENTED_LIBRARIES", ("test_instr",)) + @patch("microsoft.opentelemetry._distro._is_instrumentation_enabled") + @patch("microsoft.opentelemetry._distro._logger") + @patch("microsoft.opentelemetry._distro.get_dist_dependency_conflicts") + @patch("microsoft.opentelemetry._distro.entry_points") def test_setup_instrumentations_exception( self, iter_mock, @@ -970,19 +947,18 @@ def test_setup_instrumentations_exception( dep_mock.return_value = None enabled_mock.return_value = True _setup_instrumentations({}) - dep_mock.assert_called_with(ep_mock.dist) ep_mock.load.assert_called_once() instrumentor_mock.instrument.assert_not_called() logger_mock.warning.assert_called_once() @patch( - "microsoft.opentelemetry._azure_monitor._configure._ALL_SUPPORTED_INSTRUMENTED_LIBRARIES", + "microsoft.opentelemetry._distro._SUPPORTED_INSTRUMENTED_LIBRARIES", ("test_instr1", "test_instr2"), ) - @patch("microsoft.opentelemetry._azure_monitor._configure._is_instrumentation_enabled") - @patch("microsoft.opentelemetry._azure_monitor._configure._logger") - @patch("microsoft.opentelemetry._azure_monitor._configure.get_dist_dependency_conflicts") - @patch("microsoft.opentelemetry._azure_monitor._configure.entry_points") + @patch("microsoft.opentelemetry._distro._is_instrumentation_enabled") + @patch("microsoft.opentelemetry._distro._logger") + @patch("microsoft.opentelemetry._distro.get_dist_dependency_conflicts") + @patch("microsoft.opentelemetry._distro.entry_points") def test_setup_instrumentations_disabled( self, iter_mock, @@ -1002,7 +978,6 @@ def test_setup_instrumentations_disabled( dep_mock.return_value = None enabled_mock.side_effect = [False, True] _setup_instrumentations({}) - dep_mock.assert_called_with(ep2_mock.dist) ep_mock.load.assert_not_called() ep2_mock.load.assert_called_once() instrumentor_mock.instrument.assert_called_once() diff --git a/tests/azure_monitor/utils/test_instrumentation.py b/tests/azure_monitor/utils/test_instrumentation.py new file mode 100644 index 00000000..381bbf84 --- /dev/null +++ b/tests/azure_monitor/utils/test_instrumentation.py @@ -0,0 +1,127 @@ +# ------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# -------------------------------------------------------------------------- + +import unittest +from unittest.mock import MagicMock, patch + +from packaging.requirements import Requirement + +from microsoft.opentelemetry._azure_monitor._utils.instrumentation import ( + DependencyConflict, + _get_dependency_conflicts_any, + get_dependency_conflicts, + get_dist_dependency_conflicts, +) + + +class TestDependencyConflict(unittest.TestCase): + def test_str_required(self): + c = DependencyConflict(required="foo>=1.0", found="foo 0.9") + self.assertIn("foo>=1.0", str(c)) + self.assertIn("foo 0.9", str(c)) + + def test_str_required_any(self): + c = DependencyConflict(required_any=["a", "b"], found_any=["a 0.1"]) + result = str(c) + self.assertIn("requested any", result) + self.assertIn("['a', 'b']", result) + + def test_str_no_required_with_found_any(self): + c = DependencyConflict(found_any=["x 1.0"]) + result = str(c) + self.assertIn("requested any", result) + + +class TestGetDistDependencyConflicts(unittest.TestCase): + def test_no_requires(self): + dist = MagicMock() + dist.requires = None + self.assertIsNone(get_dist_dependency_conflicts(dist)) + + def test_empty_requires(self): + dist = MagicMock() + dist.requires = [] + self.assertIsNone(get_dist_dependency_conflicts(dist)) + + def test_no_matching_extras(self): + dist = MagicMock() + dist.requires = ["some-package>=1.0"] + self.assertIsNone(get_dist_dependency_conflicts(dist)) + + def test_instruments_deps(self): + dist = MagicMock() + # Simulate a dep with 'extra == "instruments"' marker + dist.requires = ['pytest>=8.0; extra == "instruments"'] + result = get_dist_dependency_conflicts(dist) + # pytest is installed, so no conflict + self.assertIsNone(result) + + def test_instruments_any_deps(self): + dist = MagicMock() + dist.requires = ['pytest>=8.0; extra == "instruments-any"'] + result = get_dist_dependency_conflicts(dist) + self.assertIsNone(result) + + +class TestGetDependencyConflicts(unittest.TestCase): + def test_no_deps(self): + self.assertIsNone(get_dependency_conflicts([])) + + def test_requirement_object(self): + req = Requirement("pytest>=8.0") + self.assertIsNone(get_dependency_conflicts([req])) + + def test_string_dep_satisfied(self): + self.assertIsNone(get_dependency_conflicts(["pytest>=8.0"])) + + def test_string_dep_not_found(self): + result = get_dependency_conflicts(["nonexistent-package-xyz>=1.0"]) + self.assertIsNotNone(result) + self.assertIn("nonexistent-package-xyz", str(result)) + + def test_string_dep_wrong_version(self): + result = get_dependency_conflicts(["pytest>=99999.0"]) + self.assertIsNotNone(result) + self.assertIsNotNone(result.found) + + def test_invalid_requirement(self): + result = get_dependency_conflicts(["this is not valid!!!"]) + self.assertIsNotNone(result) + + def test_with_deps_any(self): + result = get_dependency_conflicts([], ["pytest>=8.0"]) + self.assertIsNone(result) + + +class TestGetDependencyConflictsAny(unittest.TestCase): + def test_empty(self): + self.assertIsNone(_get_dependency_conflicts_any([])) + + def test_one_satisfied(self): + self.assertIsNone(_get_dependency_conflicts_any(["pytest>=8.0"])) + + def test_one_not_found(self): + result = _get_dependency_conflicts_any(["nonexistent-xyz>=1.0"]) + self.assertIsNotNone(result) + self.assertIsNotNone(result.required_any) + + def test_one_wrong_version(self): + result = _get_dependency_conflicts_any(["pytest>=99999.0"]) + self.assertIsNotNone(result) + self.assertTrue(len(result.found_any) > 0) + + def test_mixed_one_satisfied(self): + # One not found + one satisfied = no conflict + result = _get_dependency_conflicts_any(["nonexistent-xyz>=1.0", "pytest>=8.0"]) + self.assertIsNone(result) + + def test_requirement_object(self): + req = Requirement("pytest>=8.0") + self.assertIsNone(_get_dependency_conflicts_any([req])) + + def test_invalid_requirement(self): + result = _get_dependency_conflicts_any(["this is not valid!!!"]) + self.assertIsNotNone(result) diff --git a/tests/conftest.py b/tests/conftest.py index 466e715e..8b137891 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,6 +1 @@ -import os -# Exclude tests/azure_monitor from collection — these tests require -# azure-monitor-opentelemetry and related packages that are not -# installed as part of the base test dependencies. -collect_ignore = [os.path.join(os.path.dirname(__file__), "azure_monitor")] diff --git a/tests/test_distro.py b/tests/test_distro.py index 1ffacbdf..c715c88e 100644 --- a/tests/test_distro.py +++ b/tests/test_distro.py @@ -23,6 +23,7 @@ use_microsoft_opentelemetry, _setup_tracing, _setup_metrics, + _setup_logging, ) TEST_RESOURCE = Resource({"service.name": "test-service"}) @@ -359,5 +360,65 @@ def test_all_options_end_to_end(self, tracing_mock, metrics_mock, logging_mock, self.assertNotIn("enable_azure_monitor", actual) +class TestSetupLogging(unittest.TestCase): + """Tests for _setup_logging().""" + + @patch("opentelemetry._logs.set_logger_provider") + def test_creates_logger_provider(self, set_lp_mock): + """_setup_logging creates and registers a LoggerProvider.""" + lp = _setup_logging(TEST_RESOURCE, {}) + self.assertIsNotNone(lp) + set_lp_mock.assert_called_once() + + @patch("opentelemetry._logs.set_logger_provider") + def test_adds_log_record_processors(self, set_lp_mock): + """_setup_logging adds user-supplied log record processors.""" + lrp = MagicMock() + lp = _setup_logging(TEST_RESOURCE, {"log_record_processors": [lrp]}) + self.assertIsNotNone(lp) + + @patch("opentelemetry._logs.set_logger_provider") + def test_attaches_handler_for_logger_name(self, set_lp_mock): + """_setup_logging attaches a LoggingHandler when logger_name is specified.""" + import logging + + test_logger_name = "test_distro_logging_handler" + test_logger = logging.getLogger(test_logger_name) + original_handlers = list(test_logger.handlers) + + try: + _setup_logging(TEST_RESOURCE, {"logger_name": test_logger_name}) + # Should have added a handler + self.assertGreater(len(test_logger.handlers), len(original_handlers)) + finally: + # Cleanup: remove any handlers we added + for h in test_logger.handlers: + if h not in original_handlers: + test_logger.removeHandler(h) + + @patch("opentelemetry._logs.set_logger_provider") + def test_attaches_handler_with_formatter(self, set_lp_mock): + """_setup_logging sets formatter on handler when logging_formatter is provided.""" + import logging + + test_logger_name = "test_distro_logging_formatter" + test_logger = logging.getLogger(test_logger_name) + original_handlers = list(test_logger.handlers) + formatter = logging.Formatter("%(message)s") + + try: + _setup_logging(TEST_RESOURCE, { + "logger_name": test_logger_name, + "logging_formatter": formatter, + }) + new_handlers = [h for h in test_logger.handlers if h not in original_handlers] + self.assertTrue(len(new_handlers) > 0) + self.assertEqual(new_handlers[0].formatter, formatter) + finally: + for h in test_logger.handlers: + if h not in original_handlers: + test_logger.removeHandler(h) + + if __name__ == "__main__": unittest.main() diff --git a/tests/test_instrumentation.py b/tests/test_instrumentation.py new file mode 100644 index 00000000..00754e96 --- /dev/null +++ b/tests/test_instrumentation.py @@ -0,0 +1,127 @@ +# ------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# -------------------------------------------------------------------------- + +import unittest +from unittest.mock import MagicMock, patch + +from packaging.requirements import Requirement + +from microsoft.opentelemetry._instrumentation import ( + DependencyConflict, + _get_dependency_conflicts_any, + get_dependency_conflicts, + get_dist_dependency_conflicts, +) + + +class TestDependencyConflict(unittest.TestCase): + def test_str_required(self): + c = DependencyConflict(required="foo>=1.0", found="foo 0.9") + self.assertIn("foo>=1.0", str(c)) + self.assertIn("foo 0.9", str(c)) + + def test_str_required_any(self): + c = DependencyConflict(required_any=["a", "b"], found_any=["a 0.1"]) + result = str(c) + self.assertIn("requested any", result) + + def test_str_no_required_with_found_any(self): + c = DependencyConflict(found_any=["x 1.0"]) + result = str(c) + self.assertIn("requested any", result) + + def test_str_default(self): + c = DependencyConflict(required="pkg>=1.0") + self.assertIn("pkg>=1.0", str(c)) + self.assertIn("None", str(c)) + + +class TestGetDistDependencyConflicts(unittest.TestCase): + def test_no_requires(self): + dist = MagicMock() + dist.requires = None + self.assertIsNone(get_dist_dependency_conflicts(dist)) + + def test_empty_requires(self): + dist = MagicMock() + dist.requires = [] + self.assertIsNone(get_dist_dependency_conflicts(dist)) + + def test_instruments_dep_satisfied(self): + dist = MagicMock() + dist.requires = ['pytest>=8.0; extra == "instruments"'] + result = get_dist_dependency_conflicts(dist) + self.assertIsNone(result) + + def test_instruments_any_dep_satisfied(self): + dist = MagicMock() + dist.requires = ['pytest>=8.0; extra == "instruments-any"'] + result = get_dist_dependency_conflicts(dist) + self.assertIsNone(result) + + def test_no_extra_marker(self): + dist = MagicMock() + dist.requires = ["pytest>=8.0"] + self.assertIsNone(get_dist_dependency_conflicts(dist)) + + +class TestGetDependencyConflicts(unittest.TestCase): + def test_no_deps(self): + self.assertIsNone(get_dependency_conflicts([])) + + def test_requirement_object(self): + req = Requirement("pytest>=8.0") + self.assertIsNone(get_dependency_conflicts([req])) + + def test_string_dep_satisfied(self): + self.assertIsNone(get_dependency_conflicts(["pytest>=8.0"])) + + def test_string_dep_not_found(self): + result = get_dependency_conflicts(["nonexistent-xyz-package>=1.0"]) + self.assertIsNotNone(result) + + def test_string_dep_wrong_version(self): + result = get_dependency_conflicts(["pytest>=99999.0"]) + self.assertIsNotNone(result) + self.assertIsNotNone(result.found) + + def test_invalid_requirement(self): + result = get_dependency_conflicts(["this is not valid!!!"]) + self.assertIsNotNone(result) + + def test_with_deps_any_satisfied(self): + result = get_dependency_conflicts([], ["pytest>=8.0"]) + self.assertIsNone(result) + + +class TestGetDependencyConflictsAny(unittest.TestCase): + def test_empty(self): + self.assertIsNone(_get_dependency_conflicts_any([])) + + def test_one_satisfied(self): + self.assertIsNone(_get_dependency_conflicts_any(["pytest>=8.0"])) + + def test_one_not_found(self): + result = _get_dependency_conflicts_any(["nonexistent-xyz>=1.0"]) + self.assertIsNotNone(result) + self.assertIsNotNone(result.required_any) + + def test_one_wrong_version(self): + result = _get_dependency_conflicts_any(["pytest>=99999.0"]) + self.assertIsNotNone(result) + self.assertTrue(len(result.found_any) > 0) + + def test_mixed_one_satisfied(self): + result = _get_dependency_conflicts_any(["nonexistent-xyz>=1.0", "pytest>=8.0"]) + self.assertIsNone(result) + + def test_requirement_object(self): + req = Requirement("pytest>=8.0") + self.assertIsNone(_get_dependency_conflicts_any([req])) + + def test_invalid_requirement(self): + result = _get_dependency_conflicts_any(["this is not valid!!!"]) + self.assertIsNotNone(result) diff --git a/tox.ini b/tox.ini index 6ed738ab..2c668cc6 100644 --- a/tox.ini +++ b/tox.ini @@ -55,4 +55,4 @@ deps = pytest>=8.0 commands = python -m pip install -e {tox_root} - python -m pytest {tox_root}/tests {posargs} + python -m pytest --cov --cov-report=term-missing {tox_root}/tests {posargs} From 8aa5ec859d14461920dafb6d37ff5b9749c05c4a Mon Sep 17 00:00:00 2001 From: Hector Hernandez <39923391+hectorhdzg@users.noreply.github.com> Date: Fri, 10 Apr 2026 09:38:08 -0700 Subject: [PATCH 2/2] Update --- .../browserSdkLoader/test_init.py | 106 ++++++++++-------- .../browserSdkLoader/test_snippet_injector.py | 18 ++- tests/azure_monitor/test_configure.py | 19 +--- .../utils/test_instrumentation.py | 2 +- tests/conftest.py | 1 - tests/test_distro.py | 15 ++- tests/test_instrumentation.py | 2 +- 7 files changed, 85 insertions(+), 78 deletions(-) delete mode 100644 tests/conftest.py diff --git a/tests/azure_monitor/browserSdkLoader/test_init.py b/tests/azure_monitor/browserSdkLoader/test_init.py index 08be25c3..c52870a9 100644 --- a/tests/azure_monitor/browserSdkLoader/test_init.py +++ b/tests/azure_monitor/browserSdkLoader/test_init.py @@ -9,6 +9,11 @@ from microsoft.opentelemetry._azure_monitor._browser_sdk_loader._config import BrowserSDKConfig +_MW_PATH = ( + "microsoft.opentelemetry._azure_monitor._browser_sdk_loader" + ".django_middleware.ApplicationInsightsWebSnippetMiddleware" +) + class TestSetupSnippetInjection(unittest.TestCase): def setUp(self): @@ -17,9 +22,7 @@ def setUp(self): connection_string="InstrumentationKey=test;IngestionEndpoint=https://test.in.ai.azure.com/", ) - @patch( - "microsoft.opentelemetry._azure_monitor._browser_sdk_loader._setup_django_injection" - ) + @patch("microsoft.opentelemetry._azure_monitor._browser_sdk_loader._setup_django_injection") def test_setup_snippet_injection_calls_django(self, mock_django): from microsoft.opentelemetry._azure_monitor._browser_sdk_loader import setup_snippet_injection @@ -51,17 +54,18 @@ def test_django_not_available(self): # Should not raise when django is not importable _setup_django_injection(self.config) - @patch( - "microsoft.opentelemetry._azure_monitor._browser_sdk_loader._register_django_middleware" - ) + @patch("microsoft.opentelemetry._azure_monitor._browser_sdk_loader._register_django_middleware") def test_django_available_and_configured(self, mock_register): settings_mock = MagicMock() settings_mock.configured = True - with patch.dict("sys.modules", { - "django": MagicMock(), - "django.conf": MagicMock(settings=settings_mock), - }): + with patch.dict( + "sys.modules", + { + "django": MagicMock(), + "django.conf": MagicMock(settings=settings_mock), + }, + ): from microsoft.opentelemetry._azure_monitor._browser_sdk_loader import _setup_django_injection _setup_django_injection(self.config) @@ -71,10 +75,13 @@ def test_django_not_configured(self): settings_mock = MagicMock() settings_mock.configured = False - with patch.dict("sys.modules", { - "django": MagicMock(), - "django.conf": MagicMock(settings=settings_mock), - }): + with patch.dict( + "sys.modules", + { + "django": MagicMock(), + "django.conf": MagicMock(settings=settings_mock), + }, + ): from microsoft.opentelemetry._azure_monitor._browser_sdk_loader import _setup_django_injection # Should return early without raising @@ -89,50 +96,51 @@ def setUp(self): ) def test_register_with_middleware_attr(self): - settings_mock = MagicMock() + settings_mock = MagicMock(spec_set=["MIDDLEWARE"]) settings_mock.MIDDLEWARE = ["django.middleware.common.CommonMiddleware"] - # Ensure MIDDLEWARE_CLASSES doesn't exist - del settings_mock.MIDDLEWARE_CLASSES - with patch.dict("sys.modules", { - "django": MagicMock(), - "django.conf": MagicMock(settings=settings_mock), - }): + with patch.dict( + "sys.modules", + { + "django": MagicMock(), + "django.conf": MagicMock(settings=settings_mock), + }, + ): from microsoft.opentelemetry._azure_monitor._browser_sdk_loader import _register_django_middleware _register_django_middleware(self.config) # Our middleware should have been appended middleware_list = settings_mock.MIDDLEWARE - self.assertIn( - "microsoft.opentelemetry._azure_monitor._browser_sdk_loader.django_middleware.ApplicationInsightsWebSnippetMiddleware", - middleware_list, - ) + self.assertIn(_MW_PATH, middleware_list) def test_register_with_middleware_classes_attr(self): settings_mock = MagicMock(spec=[]) settings_mock.MIDDLEWARE_CLASSES = ["django.middleware.common.CommonMiddleware"] - with patch.dict("sys.modules", { - "django": MagicMock(), - "django.conf": MagicMock(settings=settings_mock), - }): + with patch.dict( + "sys.modules", + { + "django": MagicMock(), + "django.conf": MagicMock(settings=settings_mock), + }, + ): from microsoft.opentelemetry._azure_monitor._browser_sdk_loader import _register_django_middleware _register_django_middleware(self.config) - self.assertIn( - "microsoft.opentelemetry._azure_monitor._browser_sdk_loader.django_middleware.ApplicationInsightsWebSnippetMiddleware", - settings_mock.MIDDLEWARE_CLASSES, - ) + self.assertIn(_MW_PATH, settings_mock.MIDDLEWARE_CLASSES) def test_already_registered(self): - middleware_path = "microsoft.opentelemetry._azure_monitor._browser_sdk_loader.django_middleware.ApplicationInsightsWebSnippetMiddleware" + middleware_path = _MW_PATH settings_mock = MagicMock() settings_mock.MIDDLEWARE = [middleware_path] - with patch.dict("sys.modules", { - "django": MagicMock(), - "django.conf": MagicMock(settings=settings_mock), - }): + with patch.dict( + "sys.modules", + { + "django": MagicMock(), + "django.conf": MagicMock(settings=settings_mock), + }, + ): from microsoft.opentelemetry._azure_monitor._browser_sdk_loader import _register_django_middleware _register_django_middleware(self.config) @@ -151,10 +159,13 @@ def test_store_config(self): settings_mock = MagicMock(spec=[]) # No AZURE_MONITOR_WEB_SNIPPET_CONFIG attribute yet - with patch.dict("sys.modules", { - "django": MagicMock(), - "django.conf": MagicMock(settings=settings_mock), - }): + with patch.dict( + "sys.modules", + { + "django": MagicMock(), + "django.conf": MagicMock(settings=settings_mock), + }, + ): from microsoft.opentelemetry._azure_monitor._browser_sdk_loader import _store_django_config _store_django_config(self.config) @@ -165,10 +176,13 @@ def test_store_config_already_set(self): settings_mock = MagicMock() settings_mock.AZURE_MONITOR_WEB_SNIPPET_CONFIG = existing_config - with patch.dict("sys.modules", { - "django": MagicMock(), - "django.conf": MagicMock(settings=settings_mock), - }): + with patch.dict( + "sys.modules", + { + "django": MagicMock(), + "django.conf": MagicMock(settings=settings_mock), + }, + ): from microsoft.opentelemetry._azure_monitor._browser_sdk_loader import _store_django_config _store_django_config(self.config) diff --git a/tests/azure_monitor/browserSdkLoader/test_snippet_injector.py b/tests/azure_monitor/browserSdkLoader/test_snippet_injector.py index 2a1915fe..d5335fb4 100644 --- a/tests/azure_monitor/browserSdkLoader/test_snippet_injector.py +++ b/tests/azure_monitor/browserSdkLoader/test_snippet_injector.py @@ -396,9 +396,12 @@ def test_mark_browser_loader_feature_enabled_statsbeat_available(self): mock_module.is_statsbeat_enabled.return_value = True mock_module.get_statsbeat_shutdown.return_value = False mock_module.get_statsbeat_browser_sdk_loader_feature_set.return_value = False - with patch.dict("sys.modules", { - "azure.monitor.opentelemetry.exporter.statsbeat._state": mock_module, - }): + with patch.dict( + "sys.modules", + { + "azure.monitor.opentelemetry.exporter.statsbeat._state": mock_module, + }, + ): _mark_browser_loader_feature(True) mock_module.set_statsbeat_browser_sdk_loader_feature_set.assert_called_once() @@ -410,9 +413,12 @@ def test_mark_browser_loader_feature_enabled_statsbeat_exception(self): mock_module = unittest.mock.MagicMock() mock_module.is_statsbeat_enabled.side_effect = Exception("boom") - with patch.dict("sys.modules", { - "azure.monitor.opentelemetry.exporter.statsbeat._state": mock_module, - }): + with patch.dict( + "sys.modules", + { + "azure.monitor.opentelemetry.exporter.statsbeat._state": mock_module, + }, + ): # Should not raise _mark_browser_loader_feature(True) diff --git a/tests/azure_monitor/test_configure.py b/tests/azure_monitor/test_configure.py index d128ee73..80228a22 100644 --- a/tests/azure_monitor/test_configure.py +++ b/tests/azure_monitor/test_configure.py @@ -18,8 +18,6 @@ from microsoft.opentelemetry._azure_monitor._configure import ( _send_attach_warning, - _setup_azure_instrumentations, - _setup_browser_sdk_loader, _setup_live_metrics, _setup_logging, _setup_metrics, @@ -384,9 +382,6 @@ def test_setup_tracing( pcsp_mock.return_value = pcsp_init_mock custom_sp = Mock() - settings_mock = Mock() - opentelemetry_span_mock = Mock() - configurations = { "connection_string": "test_cs", "enable_performance_counters": True, @@ -402,9 +397,7 @@ def test_setup_tracing( trace_exporter_mock.assert_called_once_with(**configurations) bsp_mock.assert_called_once_with(trace_exp_init_mock) self.assertEqual(tp_init_mock.add_span_processor.call_count, 3) - tp_init_mock.add_span_processor.assert_has_calls( - [call(custom_sp), call(pcsp_init_mock), call(bsp_init_mock)] - ) + tp_init_mock.add_span_processor.assert_has_calls([call(custom_sp), call(pcsp_init_mock), call(bsp_init_mock)]) pcsp_mock.assert_called_once_with() @patch( @@ -447,9 +440,6 @@ def test_setup_tracing_rate_limited_sampler( pcsp_mock.return_value = pcsp_init_mock custom_sp = Mock() - settings_mock = Mock() - opentelemetry_span_mock = Mock() - configurations = { "connection_string": "test_cs", "enable_performance_counters": True, @@ -465,9 +455,7 @@ def test_setup_tracing_rate_limited_sampler( trace_exporter_mock.assert_called_once_with(**configurations) bsp_mock.assert_called_once_with(trace_exp_init_mock) self.assertEqual(tp_init_mock.add_span_processor.call_count, 3) - tp_init_mock.add_span_processor.assert_has_calls( - [call(custom_sp), call(pcsp_init_mock), call(bsp_init_mock)] - ) + tp_init_mock.add_span_processor.assert_has_calls([call(custom_sp), call(pcsp_init_mock), call(bsp_init_mock)]) pcsp_mock.assert_called_once_with() @patch( @@ -510,9 +498,6 @@ def test_setup_tracing_perf_counters_disabled( pcsp_mock.return_value = pcsp_init_mock custom_sp = Mock() - settings_mock = Mock() - opentelemetry_span_mock = Mock() - configurations = { "connection_string": "test_cs", "enable_performance_counters": False, diff --git a/tests/azure_monitor/utils/test_instrumentation.py b/tests/azure_monitor/utils/test_instrumentation.py index 381bbf84..bd8d8779 100644 --- a/tests/azure_monitor/utils/test_instrumentation.py +++ b/tests/azure_monitor/utils/test_instrumentation.py @@ -5,7 +5,7 @@ # -------------------------------------------------------------------------- import unittest -from unittest.mock import MagicMock, patch +from unittest.mock import MagicMock from packaging.requirements import Requirement diff --git a/tests/conftest.py b/tests/conftest.py deleted file mode 100644 index 8b137891..00000000 --- a/tests/conftest.py +++ /dev/null @@ -1 +0,0 @@ - diff --git a/tests/test_distro.py b/tests/test_distro.py index c715c88e..fe946822 100644 --- a/tests/test_distro.py +++ b/tests/test_distro.py @@ -392,7 +392,7 @@ def test_attaches_handler_for_logger_name(self, set_lp_mock): self.assertGreater(len(test_logger.handlers), len(original_handlers)) finally: # Cleanup: remove any handlers we added - for h in test_logger.handlers: + for h in list(test_logger.handlers): if h not in original_handlers: test_logger.removeHandler(h) @@ -407,15 +407,18 @@ def test_attaches_handler_with_formatter(self, set_lp_mock): formatter = logging.Formatter("%(message)s") try: - _setup_logging(TEST_RESOURCE, { - "logger_name": test_logger_name, - "logging_formatter": formatter, - }) + _setup_logging( + TEST_RESOURCE, + { + "logger_name": test_logger_name, + "logging_formatter": formatter, + }, + ) new_handlers = [h for h in test_logger.handlers if h not in original_handlers] self.assertTrue(len(new_handlers) > 0) self.assertEqual(new_handlers[0].formatter, formatter) finally: - for h in test_logger.handlers: + for h in list(test_logger.handlers): if h not in original_handlers: test_logger.removeHandler(h) diff --git a/tests/test_instrumentation.py b/tests/test_instrumentation.py index 00754e96..2a47f2b1 100644 --- a/tests/test_instrumentation.py +++ b/tests/test_instrumentation.py @@ -5,7 +5,7 @@ # -------------------------------------------------------------------------- import unittest -from unittest.mock import MagicMock, patch +from unittest.mock import MagicMock from packaging.requirements import Requirement