# Copyright 2017 Google Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import contextlib import logging import time from mobly import asserts from mobly import records from mobly import signals # When used outside of a `base_test.BaseTestClass` context, such as when using # the `android_device` controller directly, the `expects.recorder` # `TestResultRecord` isn't set, which causes `expects` module methods to fail # from the missing record, so this provides a default, globally accessible # record for `expects` module to use as well as providing a way to get the # globally recorded errors. DEFAULT_TEST_RESULT_RECORD = records.TestResultRecord('mobly', 'global') class _ExpectErrorRecorder: """Singleton used to store errors caught via `expect_*` functions in test. This class is only instantiated once as a singleton. It holds a reference to the record object for the test currently executing. """ def __init__(self, record=None): self.reset_internal_states(record=record) def reset_internal_states(self, record=None): """Resets the internal state of the recorder. Args: record: records.TestResultRecord, the test record for a test. """ self._record = None self._count = 0 self._record = record @property def has_error(self): """If any error has been recorded since the last reset.""" return self._count > 0 @property def error_count(self): """The number of errors that have been recorded since last reset.""" return self._count def add_error(self, error): """Record an error from expect APIs. This method generates a position stamp for the expect. The stamp is composed of a timestamp and the number of errors recorded so far. Args: error: Exception or signals.ExceptionRecord, the error to add. """ self._count += 1 self._record.add_error('expect@%s+%s' % (time.time(), self._count), error) def expect_true(condition, msg, extras=None): """Expects an expression evaluates to True. If the expectation is not met, the test is marked as fail after its execution finishes. Args: expr: The expression that is evaluated. msg: A string explaining the details in case of failure. extras: An optional field for extra information to be included in test result. """ try: asserts.assert_true(condition, msg, extras) except signals.TestSignal as e: logging.exception('Expected a `True` value, got `False`.') recorder.add_error(e) def expect_false(condition, msg, extras=None): """Expects an expression evaluates to False. If the expectation is not met, the test is marked as fail after its execution finishes. Args: expr: The expression that is evaluated. msg: A string explaining the details in case of failure. extras: An optional field for extra information to be included in test result. """ try: asserts.assert_false(condition, msg, extras) except signals.TestSignal as e: logging.exception('Expected a `False` value, got `True`.') recorder.add_error(e) def expect_equal(first, second, msg=None, extras=None): """Expects the equality of objects, otherwise fail the test. If the expectation is not met, the test is marked as fail after its execution finishes. Error message is "first != second" by default. Additional explanation can be supplied in the message. Args: first: The first object to compare. second: The second object to compare. msg: A string that adds additional info about the failure. extras: An optional field for extra information to be included in test result. """ try: asserts.assert_equal(first, second, msg, extras) except signals.TestSignal as e: logging.exception('Expected %s equals to %s, but they are not.', first, second) recorder.add_error(e) @contextlib.contextmanager def expect_no_raises(message=None, extras=None): """Expects no exception is raised in a context. If the expectation is not met, the test is marked as fail after its execution finishes. A default message is added to the exception `details`. Args: message: string, custom message to add to exception's `details`. extras: An optional field for extra information to be included in test result. """ try: yield except Exception as e: e_record = records.ExceptionRecord(e) if extras: e_record.extras = extras msg = message or 'Got an unexpected exception' details = '%s: %s' % (msg, e_record.details) logging.exception(details) e_record.details = details recorder.add_error(e_record) recorder = _ExpectErrorRecorder(DEFAULT_TEST_RESULT_RECORD)