@@ -355,6 +355,8 @@ def __rich_console__(
355355 yield Rule (status ["title" ], style = f"{ status ['color' ]} bold" )
356356
357357 for step in self .steps :
358+ if step .error is not None :
359+ yield step .error .rich_line (status ["color" ])
358360 for result in step .results :
359361 yield from result .__rich_console__ (console , options )
360362
@@ -382,6 +384,28 @@ class TestCaseStatus(str, Enum):
382384 SKIP = "skip"
383385
384386
387+ class TestCaseError (BaseModel , frozen = True ):
388+ """Captures why a test case failed to execute."""
389+
390+ message : str
391+ exception_type : str
392+ traceback : str | None = None
393+ phase : str | None = None
394+
395+ def summary (self ) -> str :
396+ """One-line description: ``<ExceptionType>[ during <phase>]: <message>``."""
397+ phase = f" during { self .phase } " if self .phase else ""
398+ return f"{ self .exception_type } { phase } : { self .message } "
399+
400+ def rich_line (self , color : str ) -> str :
401+ """Rich-markup row rendering this error under a step, in the given color."""
402+ return (
403+ f"[{ color } bold]Test case[/{ color } bold]\t "
404+ f"[{ color } ]ERROR[/{ color } ]\t "
405+ f"{ self .summary ()} "
406+ )
407+
408+
385409class TestCaseResult (BaseResult , frozen = True ):
386410 """Immutable summary of a test case execution with full run history.
387411
@@ -396,6 +420,9 @@ class TestCaseResult(BaseResult, frozen=True):
396420 this step added before its checks ran. ``None`` when the step added no
397421 interactions (e.g. skipped). Consumers (such as the Giskard Hub upload
398422 flow) use this to attribute check results to a specific interaction.
423+ error : TestCaseError | None
424+ Execution error that prevented the test case from running normally,
425+ such as an input-generation failure.
399426 status : TestCaseStatus
400427 Aggregated outcome of the test case derived from its results.
401428 passed : bool
@@ -417,11 +444,19 @@ class TestCaseResult(BaseResult, frozen=True):
417444 "interacts before checks ran; None when no interactions were added."
418445 ),
419446 )
447+ error : TestCaseError | None = Field (
448+ default = None ,
449+ description = (
450+ "Execution error that prevented this test case from running normally."
451+ ),
452+ )
420453
421454 @computed_field
422455 @property
423456 def status (self ) -> TestCaseStatus :
424457 """The status of the test case."""
458+ if self .error is not None :
459+ return TestCaseStatus .ERROR
425460 if not self .results :
426461 return TestCaseStatus .PASS
427462
@@ -470,6 +505,8 @@ def format_failures(self) -> list[str]:
470505 the check name/kind and the failure reason.
471506 """
472507 failure_messages : list [str ] = []
508+ if self .error is not None :
509+ failure_messages .append (f"Test case ERRORED: { self .error .summary ()} " )
473510 for result in self .results :
474511 if result .failed or result .errored :
475512 check_name : str = result .details .get (
@@ -508,11 +545,15 @@ def __rich_console__(
508545 status = STATUS_MAPPING [self .status ]
509546 yield Rule (status ["title" ], style = f"{ status ['color' ]} bold" )
510547
548+ if self .error is not None :
549+ yield self .error .rich_line (status ["color" ])
550+
511551 for result in self .results :
512552 yield from result .__rich_console__ (console , options )
513553
514554 status_counts = {
515- "error" : sum (1 for r in self .results if r .errored ),
555+ "error" : sum (1 for r in self .results if r .errored )
556+ + (1 if self .error is not None else 0 ),
516557 "fail" : sum (1 for r in self .results if r .failed ),
517558 "skip" : sum (1 for r in self .results if r .skipped ),
518559 "pass" : sum (1 for r in self .results if r .passed ),
0 commit comments