aboutsummaryrefslogtreecommitdiff
path: root/mobly/expects.py
blob: 9475758c18ca4ada4e3f9a716568e8b01c3a1124 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
# 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)