|
3 | 3 |
|
4 | 4 | import pytest
|
5 | 5 | from _pytest.config import Config
|
6 |
| -from _pytest.fixtures import FixtureDef, SubRequest |
| 6 | +from _pytest.fixtures import FixtureDef, FixtureRequest, SubRequest |
7 | 7 | from _pytest.main import Session
|
8 | 8 | from _pytest.nodes import Item, Node
|
9 | 9 | from _pytest.reports import TestReport
|
@@ -103,61 +103,119 @@ def _attributes_from_item(self, item: Item) -> Dict[str, Union[str, int]]:
|
103 | 103 | return attributes
|
104 | 104 |
|
105 | 105 | @pytest.hookimpl(hookwrapper=True)
|
106 |
| - def pytest_runtest_setup(self, item: Item) -> Generator[None, None, None]: |
| 106 | + def pytest_runtest_protocol(self, item: Item) -> Generator[None, None, None]: |
| 107 | + context = trace.set_span_in_context(self.session_span) |
107 | 108 | with tracer.start_as_current_span(
|
108 |
| - 'setup', |
| 109 | + item.nodeid, |
109 | 110 | attributes=self._attributes_from_item(item),
|
| 111 | + context=context, |
110 | 112 | ):
|
111 | 113 | yield
|
112 | 114 |
|
113 | 115 | @pytest.hookimpl(hookwrapper=True)
|
114 |
| - def pytest_fixture_setup( |
115 |
| - self, fixturedef: FixtureDef, request: pytest.FixtureRequest |
116 |
| - ) -> Generator[None, None, None]: |
117 |
| - context: Context = None |
118 |
| - if fixturedef.scope != 'function': |
119 |
| - context = trace.set_span_in_context(self.session_span) |
| 116 | + def pytest_runtest_setup(self, item: Item) -> Generator[None, None, None]: |
| 117 | + with tracer.start_as_current_span( |
| 118 | + f'{item.nodeid}::setup', |
| 119 | + attributes=self._attributes_from_item(item), |
| 120 | + ): |
| 121 | + yield |
120 | 122 |
|
| 123 | + def _attributes_from_fixturedef( |
| 124 | + self, fixturedef: FixtureDef |
| 125 | + ) -> Dict[str, Union[str, int]]: |
| 126 | + return { |
| 127 | + SpanAttributes.CODE_FILEPATH: fixturedef.func.__code__.co_filename, |
| 128 | + SpanAttributes.CODE_FUNCTION: fixturedef.argname, |
| 129 | + SpanAttributes.CODE_LINENO: fixturedef.func.__code__.co_firstlineno, |
| 130 | + "pytest.fixture_scope": fixturedef.scope, |
| 131 | + "pytest.span_type": "fixture", |
| 132 | + } |
| 133 | + |
| 134 | + def _name_from_fixturedef(self, fixturedef: FixtureDef, request: FixtureRequest): |
121 | 135 | if fixturedef.params and 'request' in fixturedef.argnames:
|
122 | 136 | try:
|
123 | 137 | parameter = str(request.param)
|
124 | 138 | except Exception:
|
125 | 139 | parameter = str(
|
126 | 140 | request.param_index if isinstance(request, SubRequest) else '?'
|
127 | 141 | )
|
128 |
| - name = f"{fixturedef.argname}[{parameter}]" |
129 |
| - else: |
130 |
| - name = fixturedef.argname |
131 |
| - |
132 |
| - attributes: Dict[str, Union[str, int]] = { |
133 |
| - SpanAttributes.CODE_FILEPATH: fixturedef.func.__code__.co_filename, |
134 |
| - SpanAttributes.CODE_FUNCTION: fixturedef.argname, |
135 |
| - SpanAttributes.CODE_LINENO: fixturedef.func.__code__.co_firstlineno, |
136 |
| - "pytest.fixture_scope": fixturedef.scope, |
137 |
| - "pytest.span_type": "fixture", |
138 |
| - } |
| 142 | + return f"{fixturedef.argname}[{parameter}]" |
| 143 | + return fixturedef.argname |
139 | 144 |
|
140 |
| - with tracer.start_as_current_span(name, context=context, attributes=attributes): |
| 145 | + @pytest.hookimpl(hookwrapper=True) |
| 146 | + def pytest_fixture_setup( |
| 147 | + self, fixturedef: FixtureDef, request: FixtureRequest |
| 148 | + ) -> Generator[None, None, None]: |
| 149 | + with tracer.start_as_current_span( |
| 150 | + name=f'{self._name_from_fixturedef(fixturedef, request)} setup', |
| 151 | + attributes=self._attributes_from_fixturedef(fixturedef), |
| 152 | + ): |
141 | 153 | yield
|
142 | 154 |
|
143 | 155 | @pytest.hookimpl(hookwrapper=True)
|
144 |
| - def pytest_runtest_protocol(self, item: Item) -> Generator[None, None, None]: |
145 |
| - context = trace.set_span_in_context(self.session_span) |
| 156 | + def pytest_runtest_call(self, item: Item) -> Generator[None, None, None]: |
146 | 157 | with tracer.start_as_current_span(
|
147 |
| - item.name, |
| 158 | + name=f'{item.nodeid}::call', |
148 | 159 | attributes=self._attributes_from_item(item),
|
149 |
| - context=context, |
150 | 160 | ):
|
151 | 161 | yield
|
152 | 162 |
|
153 | 163 | @pytest.hookimpl(hookwrapper=True)
|
154 | 164 | def pytest_runtest_teardown(self, item: Item) -> Generator[None, None, None]:
|
155 | 165 | with tracer.start_as_current_span(
|
156 |
| - 'teardown', |
| 166 | + name=f'{item.nodeid}::teardown', |
157 | 167 | attributes=self._attributes_from_item(item),
|
158 | 168 | ):
|
| 169 | + # Since there is no pytest_fixture_teardown hook, we have to be a |
| 170 | + # little clever to capture the spans for each fixture's teardown. |
| 171 | + # The pytest_fixture_post_finalizer hook is called at the end of a |
| 172 | + # fixture's teardown, but we don't know when the fixture actually |
| 173 | + # began tearing down. |
| 174 | + # |
| 175 | + # Instead start a span here for the first fixture to be torn down, |
| 176 | + # but give it a temporary name, since we don't know which fixture it |
| 177 | + # will be. Then, in pytest_fixture_post_finalizer, when we do know |
| 178 | + # which fixture is being torn down, update the name and attributes |
| 179 | + # to the actual fixture, end the span, and create the span for the |
| 180 | + # next fixture in line to be torn down. |
| 181 | + self._fixture_teardown_span = tracer.start_span("fixture teardown") |
159 | 182 | yield
|
160 | 183 |
|
| 184 | + # The last call to pytest_fixture_post_finalizer will create |
| 185 | + # a span that is unneeded, so delete it. |
| 186 | + del self._fixture_teardown_span |
| 187 | + |
| 188 | + @pytest.hookimpl(hookwrapper=True) |
| 189 | + def pytest_fixture_post_finalizer( |
| 190 | + self, fixturedef: FixtureDef, request: SubRequest |
| 191 | + ) -> Generator[None, None, None]: |
| 192 | + """When the span for a fixture teardown is created by |
| 193 | + pytest_runtest_teardown or a previous pytest_fixture_post_finalizer, we |
| 194 | + need to update the name and attributes now that we know which fixture it |
| 195 | + was for.""" |
| 196 | + |
| 197 | + # If the fixture has already been torn down, then it will have no cached |
| 198 | + # result, so we can skip this one. |
| 199 | + if fixturedef.cached_result is None: |
| 200 | + yield |
| 201 | + # Passing `-x` option to pytest can cause it to exit early so it may not |
| 202 | + # have this span attribute. |
| 203 | + elif not hasattr(self, "_fixture_teardown_span"): # pragma: no cover |
| 204 | + yield |
| 205 | + else: |
| 206 | + # If we've gotten here, we have a real fixture about to be torn down. |
| 207 | + name = f'{self._name_from_fixturedef(fixturedef, request)} teardown' |
| 208 | + self._fixture_teardown_span.update_name(name) |
| 209 | + attributes = self._attributes_from_fixturedef(fixturedef) |
| 210 | + self._fixture_teardown_span.set_attributes(attributes) |
| 211 | + yield |
| 212 | + self._fixture_teardown_span.end() |
| 213 | + |
| 214 | + # Create the span for the next fixture to be torn down. When there are |
| 215 | + # no more fixtures remaining, this will be an empty, useless span, so it |
| 216 | + # needs to be deleted by pytest_runtest_teardown. |
| 217 | + self._fixture_teardown_span = tracer.start_span("fixture teardown") |
| 218 | + |
161 | 219 | @staticmethod
|
162 | 220 | def pytest_exception_interact(
|
163 | 221 | node: Node,
|
|
0 commit comments