Skip to content

Preserve original test failure status and termination signal when teardown_test() fails, and expose test failure state during teardown #1036

Description

@Puneetha-Ramachandra

Description

In hardware/device end-to-end testing, when a test case (test_*) fails, for example, because the device under test crashed, disconnected, or became unresponsive, subsequent cleanup operations in teardown_test() (or teardown_class()) frequently fail when attempting to communicate with the unhealthy device.

Currently, BaseTestClass.exec_one_test in mobly/base_test.py has several issues in how teardown_test() interacts with an already-failed test:

  1. self.current_test_info.record does not reflect the test failure during teardown_test():
    In exec_one_test(), self._teardown_test(test_name) is executed inside an inner finally: block before the outer except blocks catch the exception raised by test_method() and update tr_record:

    try:
      try:
        self._setup_test(test_name)
        test_method()
      ...
      finally:
        # tr_record.result and tr_record.termination_signal are still None here!
        self._teardown_test(test_name)
    except (signals.TestFailure, AssertionError) as e:
      tr_record.test_fail(e)

    Because tr_record is only populated after _teardown_test() finishes, teardown_test() implementations cannot inspect self.current_test_info.record to determine if the test has already failed (forcing users to rely on fragile workarounds like inspecting sys.exc_info() and expects.recorder.has_error).

  2. mobly.expects failures are overridden as ERROR instead of FAIL if teardown_test() fails:
    At the end of exec_one_test():

    else:
      if expects.recorder.has_error and not teardown_test_failed:
        tr_record.test_fail()
      elif not teardown_test_failed:
        tr_record.test_pass()

    If test_method() records a failure via expects.expect_true(False, ...) and teardown_test() subsequently raises an exception (or records another expect failure), teardown_test_failed is True. As a result, tr_record.test_fail() is skipped and the test is marked as ERROR rather than FAIL.

  3. Raising TestAbortSignal (TestAbortClass / TestAbortAll) in teardown_test() overwrites the original test failure:
    Inside the finally: block of exec_one_test():

    try:
      self._teardown_test(test_name)
    except signals.TestAbortSignal:
      raise

    If test_method() raises AssertionError / TestFailure (or another exception) and teardown_test() detects that the device is dead and raises TestAbortClass or TestAbortAll, re-raising TestAbortSignal inside finally: replaces the active exception from test_method(). The outer except signals.TestAbortSignal as e: then records the teardown abort signal as tr_record.termination_signal, masking the root-cause failure from test_method().

Steps to Reproduce

from mobly import asserts
from mobly import base_test
from mobly import expects
from mobly import signals
from mobly import test_runner


class TeardownOverrideTest(base_test.BaseTestClass):

  def teardown_test(self):
    # Note: self.current_test_info.record.result is None here even if the test failed.
    # If teardown detects a broken device and aborts the class, or fails after an `expect`:
    raise signals.TestAbortClass("Device unreachable during teardown_test")

  def test_assertion_failure_masked_by_teardown_abort(self):
    asserts.assert_true(False, "Root cause: test assertion failed")

  def test_expect_failure_overridden_by_teardown_error(self):
    expects.expect_true(False, "Root cause: soft expectation failed")


if __name__ == "__main__":
  test_runner.main()

Expected Behavior

  1. Record test failure before running teardown_test():
    Catch and record the exception from setup_test() / test_method() (and any expects.recorder errors) onto tr_record before invoking self._teardown_test(test_name), so that self.current_test_info.record accurately reflects the test's status and termination_signal during teardown_test().
  2. Never overwrite a test's primary failure with a teardown failure:
    • If test_method() already failed (via exception or mobly.expects), any exception or TestAbortSignal raised in teardown_test() should be recorded in tr_record.extra_errors['teardown_test'] without overwriting tr_record.termination_signal or changing a FAIL result to ERROR.
    • If teardown_test() raises TestAbortClass or TestAbortAll after test_method() already failed, the class/run should still abort, but the failed test's termination_signal should remain the original exception from test_method().

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions