|
| 1 | +from __future__ import annotations |
| 2 | + |
| 3 | +import json |
| 4 | +import os |
| 5 | +import subprocess |
| 6 | +import sys |
| 7 | +from pathlib import Path |
| 8 | +from typing import cast |
| 9 | + |
| 10 | +REPO_ROOT = Path(__file__).resolve().parents[2] |
| 11 | +SRC_ROOT = REPO_ROOT / "src" |
| 12 | + |
| 13 | + |
| 14 | +def _run_python(script: str) -> dict[str, object]: |
| 15 | + env = os.environ.copy() |
| 16 | + pythonpath = env.get("PYTHONPATH") |
| 17 | + if pythonpath: |
| 18 | + env["PYTHONPATH"] = f"{SRC_ROOT}:{pythonpath}" |
| 19 | + else: |
| 20 | + env["PYTHONPATH"] = str(SRC_ROOT) |
| 21 | + |
| 22 | + completed = subprocess.run( |
| 23 | + [sys.executable, "-c", script], |
| 24 | + cwd=REPO_ROOT, |
| 25 | + env=env, |
| 26 | + text=True, |
| 27 | + capture_output=True, |
| 28 | + check=True, |
| 29 | + ) |
| 30 | + payload = json.loads(completed.stdout) |
| 31 | + if not isinstance(payload, dict): |
| 32 | + raise AssertionError("Subprocess payload must be a JSON object.") |
| 33 | + return cast(dict[str, object], payload) |
| 34 | + |
| 35 | + |
| 36 | +def test_import_agents_has_no_tracing_side_effects() -> None: |
| 37 | + payload = _run_python( |
| 38 | + """ |
| 39 | +import gc |
| 40 | +import json |
| 41 | +import httpx |
| 42 | +
|
| 43 | +clients_before = sum(1 for obj in gc.get_objects() if isinstance(obj, httpx.Client)) |
| 44 | +import agents # noqa: F401 |
| 45 | +from agents.tracing import processors as tracing_processors |
| 46 | +from agents.tracing import setup as tracing_setup |
| 47 | +clients_after = sum(1 for obj in gc.get_objects() if isinstance(obj, httpx.Client)) |
| 48 | +
|
| 49 | +print( |
| 50 | + json.dumps( |
| 51 | + { |
| 52 | + "client_delta": clients_after - clients_before, |
| 53 | + "provider_initialized": tracing_setup.GLOBAL_TRACE_PROVIDER is not None, |
| 54 | + "exporter_initialized": tracing_processors._global_exporter is not None, |
| 55 | + "processor_initialized": tracing_processors._global_processor is not None, |
| 56 | + "shutdown_handler_registered": tracing_setup._SHUTDOWN_HANDLER_REGISTERED, |
| 57 | + } |
| 58 | + ) |
| 59 | +) |
| 60 | +""" |
| 61 | + ) |
| 62 | + |
| 63 | + assert payload["client_delta"] == 0 |
| 64 | + assert payload["provider_initialized"] is False |
| 65 | + assert payload["exporter_initialized"] is False |
| 66 | + assert payload["processor_initialized"] is False |
| 67 | + assert payload["shutdown_handler_registered"] is False |
| 68 | + |
| 69 | + |
| 70 | +def test_get_trace_provider_lazily_initializes_defaults() -> None: |
| 71 | + payload = _run_python( |
| 72 | + """ |
| 73 | +import json |
| 74 | +
|
| 75 | +from agents.tracing import setup as tracing_setup |
| 76 | +from agents.tracing import processors as tracing_processors |
| 77 | +
|
| 78 | +provider_before = tracing_setup.GLOBAL_TRACE_PROVIDER |
| 79 | +exporter_before = tracing_processors._global_exporter |
| 80 | +processor_before = tracing_processors._global_processor |
| 81 | +shutdown_before = tracing_setup._SHUTDOWN_HANDLER_REGISTERED |
| 82 | +
|
| 83 | +provider = tracing_setup.get_trace_provider() |
| 84 | +
|
| 85 | +provider_after = tracing_setup.GLOBAL_TRACE_PROVIDER |
| 86 | +exporter_after = tracing_processors._global_exporter |
| 87 | +processor_after = tracing_processors._global_processor |
| 88 | +shutdown_after = tracing_setup._SHUTDOWN_HANDLER_REGISTERED |
| 89 | +
|
| 90 | +print( |
| 91 | + json.dumps( |
| 92 | + { |
| 93 | + "provider_before": provider_before is not None, |
| 94 | + "exporter_before": exporter_before is not None, |
| 95 | + "processor_before": processor_before is not None, |
| 96 | + "shutdown_before": shutdown_before, |
| 97 | + "provider_after": provider_after is not None, |
| 98 | + "exporter_after": exporter_after is not None, |
| 99 | + "processor_after": processor_after is not None, |
| 100 | + "shutdown_after": shutdown_after, |
| 101 | + "provider_matches_global": provider_after is provider, |
| 102 | + } |
| 103 | + ) |
| 104 | +) |
| 105 | +""" |
| 106 | + ) |
| 107 | + |
| 108 | + assert payload["provider_before"] is False |
| 109 | + assert payload["exporter_before"] is False |
| 110 | + assert payload["processor_before"] is False |
| 111 | + assert payload["shutdown_before"] is False |
| 112 | + |
| 113 | + assert payload["provider_after"] is True |
| 114 | + assert payload["exporter_after"] is True |
| 115 | + assert payload["processor_after"] is True |
| 116 | + assert payload["shutdown_after"] is True |
| 117 | + assert payload["provider_matches_global"] is True |
| 118 | + |
| 119 | + |
| 120 | +def test_get_trace_provider_bootstraps_once() -> None: |
| 121 | + payload = _run_python( |
| 122 | + """ |
| 123 | +import json |
| 124 | +
|
| 125 | +from agents.tracing import processors as tracing_processors |
| 126 | +from agents.tracing import setup as tracing_setup |
| 127 | +
|
| 128 | +registrations = [] |
| 129 | +
|
| 130 | +def fake_register(fn): |
| 131 | + registrations.append(fn) |
| 132 | + return fn |
| 133 | +
|
| 134 | +tracing_setup.atexit.register = fake_register |
| 135 | +tracing_setup.GLOBAL_TRACE_PROVIDER = None |
| 136 | +tracing_setup._SHUTDOWN_HANDLER_REGISTERED = False |
| 137 | +tracing_processors._global_exporter = None |
| 138 | +tracing_processors._global_processor = None |
| 139 | +
|
| 140 | +first = tracing_setup.get_trace_provider() |
| 141 | +second = tracing_setup.get_trace_provider() |
| 142 | +
|
| 143 | +print( |
| 144 | + json.dumps( |
| 145 | + { |
| 146 | + "same_provider": first is second, |
| 147 | + "shutdown_registration_count": sum( |
| 148 | + 1 |
| 149 | + for fn in registrations |
| 150 | + if getattr(fn, "__name__", "") == "_shutdown_global_trace_provider" |
| 151 | + ), |
| 152 | + "provider_initialized": tracing_setup.GLOBAL_TRACE_PROVIDER is not None, |
| 153 | + "exporter_initialized": tracing_processors._global_exporter is not None, |
| 154 | + "processor_initialized": tracing_processors._global_processor is not None, |
| 155 | + } |
| 156 | + ) |
| 157 | +) |
| 158 | +""" |
| 159 | + ) |
| 160 | + |
| 161 | + assert payload["same_provider"] is True |
| 162 | + assert payload["shutdown_registration_count"] == 1 |
| 163 | + assert payload["provider_initialized"] is True |
| 164 | + assert payload["exporter_initialized"] is True |
| 165 | + assert payload["processor_initialized"] is True |
| 166 | + |
| 167 | + |
| 168 | +def test_set_trace_provider_skips_default_bootstrap() -> None: |
| 169 | + payload = _run_python( |
| 170 | + """ |
| 171 | +import json |
| 172 | +
|
| 173 | +from agents.tracing import processors as tracing_processors |
| 174 | +from agents.tracing import setup as tracing_setup |
| 175 | +from agents.tracing.provider import DefaultTraceProvider |
| 176 | +
|
| 177 | +registrations = [] |
| 178 | +
|
| 179 | +def fake_register(fn): |
| 180 | + registrations.append(fn) |
| 181 | + return fn |
| 182 | +
|
| 183 | +tracing_setup.atexit.register = fake_register |
| 184 | +tracing_setup.GLOBAL_TRACE_PROVIDER = None |
| 185 | +tracing_setup._SHUTDOWN_HANDLER_REGISTERED = False |
| 186 | +tracing_processors._global_exporter = None |
| 187 | +tracing_processors._global_processor = None |
| 188 | +
|
| 189 | +custom_provider = DefaultTraceProvider() |
| 190 | +tracing_setup.set_trace_provider(custom_provider) |
| 191 | +retrieved_provider = tracing_setup.get_trace_provider() |
| 192 | +
|
| 193 | +print( |
| 194 | + json.dumps( |
| 195 | + { |
| 196 | + "custom_provider_returned": retrieved_provider is custom_provider, |
| 197 | + "shutdown_registration_count": sum( |
| 198 | + 1 |
| 199 | + for fn in registrations |
| 200 | + if getattr(fn, "__name__", "") == "_shutdown_global_trace_provider" |
| 201 | + ), |
| 202 | + "exporter_initialized": tracing_processors._global_exporter is not None, |
| 203 | + "processor_initialized": tracing_processors._global_processor is not None, |
| 204 | + } |
| 205 | + ) |
| 206 | +) |
| 207 | +""" |
| 208 | + ) |
| 209 | + |
| 210 | + assert payload["custom_provider_returned"] is True |
| 211 | + assert payload["shutdown_registration_count"] == 1 |
| 212 | + assert payload["exporter_initialized"] is False |
| 213 | + assert payload["processor_initialized"] is False |
0 commit comments