Skip to content

Commit f1ef3c4

Browse files
authored
Merge branch 'main' into test-server-cli
2 parents 6932116 + 38e1bf4 commit f1ef3c4

6 files changed

Lines changed: 77 additions & 17 deletions

File tree

python/lib/sift_client/_tests/util/test_test_results_utils.py

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -368,26 +368,38 @@ def test_bad_assert(self, report_context, step):
368368
parent_step = None
369369
substep = None
370370
nested_substep = None
371+
nested_substep_2 = None
371372
sibling_substep = None
372373
with step.substep("Top Level Step", "Should fail") as parent_step_context:
373374
parent_step = parent_step_context.current_step
374375
with parent_step_context.substep("Parent Step", "Should fail") as substep_context:
375376
substep = substep_context.current_step
376377
with substep_context.substep(
377-
"Nested Substep", "Has a bad assert"
378+
"Nested Substep",
379+
"Has a bad assert. Pytest util should nominally mark this as fail instead of error.",
378380
) as nested_substep_context:
379381
nested_substep = nested_substep_context.current_step
380382
nested_substep_context.force_result = True
381383
assert False == True
384+
with substep_context.substep(
385+
"Nested Substep 2",
386+
"Has a bad assert and shows assertion errors. Pytest util should mark this as error.",
387+
) as nested_substep_2_context:
388+
nested_substep_2 = nested_substep_2_context.current_step
389+
nested_substep_2_context.assertion_as_fail_not_error = True
390+
nested_substep_2_context.force_result = True
391+
assert False == True
382392
with substep_context.substep(
383393
"Sibling Substep", "Should pass"
384394
) as sibling_substep_context:
385395
sibling_substep = sibling_substep_context.current_step
386396

387397
assert parent_step.status == TestStatus.FAILED
388398
assert substep.status == TestStatus.FAILED
389-
assert nested_substep.status == TestStatus.ERROR
390-
assert "AssertionError" in nested_substep.error_info.error_message
399+
assert nested_substep.status == TestStatus.FAILED
400+
assert nested_substep.error_info is None
401+
assert nested_substep_2.status == TestStatus.ERROR
402+
assert "AssertionError" in nested_substep_2.error_info.error_message
391403
assert sibling_substep.status == TestStatus.PASSED
392404

393405
# If this test was successful, mark that at a high level.

python/lib/sift_client/resources/__init__.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,14 @@ async def main():
176176
FileAttachmentsAPI,
177177
)
178178

179+
import sys
180+
181+
if "pytest" in sys.modules:
182+
# These are not test classes, so we need to set __test__ to False to avoid pytest warnings.
183+
# Do this here because for some reason our docs generation doesn't like it when done in the classes themselves.
184+
TestResultsAPI.__test__ = False # type: ignore
185+
TestResultsAPIAsync.__test__ = False # type: ignore
186+
179187
__all__ = [
180188
"AssetsAPI",
181189
"AssetsAPIAsync",

python/lib/sift_client/sift_types/__init__.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,8 @@
129129
```
130130
"""
131131

132+
import sys
133+
132134
from sift_client.sift_types.asset import Asset, AssetUpdate
133135
from sift_client.sift_types.calculated_channel import (
134136
CalculatedChannel,
@@ -164,6 +166,7 @@
164166
TestMeasurement,
165167
TestMeasurementCreate,
166168
TestMeasurementType,
169+
TestMeasurementUpdate,
167170
TestReport,
168171
TestReportCreate,
169172
TestReportUpdate,
@@ -173,6 +176,21 @@
173176
TestStepType,
174177
)
175178

179+
if "pytest" in sys.modules:
180+
# These are not test classes, so we need to set __test__ to False to avoid pytest warnings.
181+
# Do this here because for some reason our docs generation doesn't like it when done in the classes themselves.
182+
TestStepType.__test__ = False # type: ignore
183+
TestMeasurementType.__test__ = False # type: ignore
184+
TestMeasurement.__test__ = False # type: ignore
185+
TestMeasurementCreate.__test__ = False # type: ignore
186+
TestMeasurementUpdate.__test__ = False # type: ignore
187+
TestStatus.__test__ = False # type: ignore
188+
TestStep.__test__ = False # type: ignore
189+
TestStepCreate.__test__ = False # type: ignore
190+
TestReport.__test__ = False # type: ignore
191+
TestReportCreate.__test__ = False # type: ignore
192+
TestReportUpdate.__test__ = False # type: ignore
193+
176194
__all__ = [
177195
"Asset",
178196
"AssetUpdate",

python/lib/sift_client/sift_types/_base.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ def _update(self, other: BaseType[ProtoT, SelfT]) -> BaseType[ProtoT, SelfT]:
5757
"""Update this instance with the values from another instance."""
5858
# This bypasses the frozen status of the model
5959
for key in other.__class__.model_fields.keys():
60-
if key in self.model_fields:
60+
if key in self.__class__.model_fields:
6161
self.__dict__.update({key: getattr(other, key)})
6262

6363
# Make sure we also update the proto since it is excluded
@@ -68,7 +68,7 @@ def _update(self, other: BaseType[ProtoT, SelfT]) -> BaseType[ProtoT, SelfT]:
6868
@model_validator(mode="after")
6969
def _validate_timezones(self):
7070
"""Validate datetime fiels have timezone information."""
71-
for field_name in self.model_fields.keys():
71+
for field_name in self.__class__.model_fields.keys():
7272
val = getattr(self, field_name)
7373
if isinstance(val, datetime) and val.tzinfo is None:
7474
raise ValueError(f"{field_name} must have timezone information")

python/lib/sift_client/util/test_results/context_manager.py

Lines changed: 31 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -94,9 +94,16 @@ def __exit__(self, exc_type, exc_value, traceback):
9494
self.report.update(update)
9595
return True
9696

97-
def new_step(self, name: str, description: str | None = None) -> NewStep:
97+
def new_step(
98+
self, name: str, description: str | None = None, assertion_as_fail_not_error: bool = True
99+
) -> NewStep:
98100
"""Alias to return a new step context manager from this report context. Use create_step for actually creating a TestStep in the current context."""
99-
return NewStep(self, name=name, description=description)
101+
return NewStep(
102+
self,
103+
name=name,
104+
description=description,
105+
assertion_as_fail_not_error=assertion_as_fail_not_error,
106+
)
100107

101108
def get_next_step_path(self) -> str:
102109
"""Get the next step path for the current depth."""
@@ -191,24 +198,28 @@ class NewStep(AbstractContextManager):
191198

192199
report_context: ReportContext
193200
client: SiftClient
201+
assertion_as_fail_not_error: bool = True
194202
current_step: TestStep | None = None
195203

196204
def __init__(
197205
self,
198206
report_context: ReportContext,
199207
name: str,
200208
description: str | None = None,
209+
assertion_as_fail_not_error: bool = True,
201210
):
202211
"""Initialize a new step context.
203212
204213
Args:
205214
report_context: The report context to create the step in.
206215
name: The name of the step.
207216
description: The description of the step.
217+
assertion_as_fail_not_error: Mark steps with assertion errors as failed instead of error+traceback (some users want assertions to work as simple failures especially when using pytest).
208218
"""
209219
self.report_context = report_context
210220
self.client = report_context.report.client
211221
self.current_step = self.report_context.create_step(name, description)
222+
self.assertion_as_fail_not_error = assertion_as_fail_not_error
212223

213224
def __enter__(self):
214225
"""Enter the context manager to create a new step.
@@ -233,15 +244,19 @@ def update_step_from_result(
233244
returns: The false if step failed or errored, true otherwise.
234245
"""
235246
error_info = None
236-
if exc:
237-
stack = traceback.format_exception(exc, exc_value, tb) # type: ignore
238-
stack = [stack[0], *stack[-10:]] if len(stack) > 10 else stack
239-
trace = "".join(stack)
240-
error_info = ErrorInfo(
241-
error_code=1,
242-
error_message=trace,
243-
)
244247
assert self.current_step is not None
248+
if exc:
249+
if isinstance(exc_value, AssertionError) and not self.assertion_as_fail_not_error:
250+
# If we're not showing assertion errors (i.e. pytest), mark step as failed but don't set error info.
251+
self.report_context.record_step_outcome(False, self.current_step)
252+
else:
253+
stack = traceback.format_exception(exc, exc_value, tb) # type: ignore
254+
stack = [stack[0], *stack[-10:]] if len(stack) > 10 else stack
255+
trace = "".join(stack)
256+
error_info = ErrorInfo(
257+
error_code=1,
258+
error_message=trace,
259+
)
245260

246261
# Resolve the status of this step (i.e. fail if children failed) and propagate the result to the parent step.
247262
result = self.report_context.resolve_and_propagate_step_result(
@@ -272,6 +287,7 @@ def __exit__(self, exc, exc_value, tb):
272287
self.report_context.exit_step(self.current_step)
273288

274289
# Test only attribute (hence not public class variable)
290+
# This changes the result after the status and error info are set.
275291
if hasattr(self, "force_result"):
276292
result = self.force_result
277293

@@ -421,4 +437,8 @@ def report_outcome(self, name: str, result: bool, reason: str | None = None) ->
421437

422438
def substep(self, name: str, description: str | None = None) -> NewStep:
423439
"""Alias to return a new step context manager from the current step. The ReportContext will manage nesting of steps."""
424-
return self.report_context.new_step(name=name, description=description)
440+
return self.report_context.new_step(
441+
name=name,
442+
description=description,
443+
assertion_as_fail_not_error=self.assertion_as_fail_not_error,
444+
)

python/lib/sift_client/util/test_results/pytest_util.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,9 @@ def _step_impl(
6363
) -> Generator[NewStep | None, None, None]:
6464
name = str(request.node.name)
6565
existing_docstring = request.node.obj.__doc__ or None
66-
with report_context.new_step(name=name, description=existing_docstring) as new_step:
66+
with report_context.new_step(
67+
name=name, description=existing_docstring, assertion_as_fail_not_error=False
68+
) as new_step:
6769
yield new_step
6870
if hasattr(request.node, "rep_call") and request.node.rep_call.excinfo:
6971
new_step.update_step_from_result(

0 commit comments

Comments
 (0)