1# Copyright 2017 Google Inc.
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7#     http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
15import contextlib
16import logging
17import time
18
19from mobly import asserts
20from mobly import records
21from mobly import signals
22
23# When used outside of a `base_test.BaseTestClass` context, such as when using
24# the `android_device` controller directly, the `expects.recorder`
25# `TestResultRecord` isn't set, which causes `expects` module methods to fail
26# from the missing record, so this provides a default, globally accessible
27# record for `expects` module to use as well as providing a way to get the
28# globally recorded errors.
29DEFAULT_TEST_RESULT_RECORD = records.TestResultRecord('mobly', 'global')
30
31
32class _ExpectErrorRecorder:
33  """Singleton used to store errors caught via `expect_*` functions in test.
34
35  This class is only instantiated once as a singleton. It holds a reference
36  to the record object for the test currently executing.
37  """
38
39  def __init__(self, record=None):
40    self.reset_internal_states(record=record)
41
42  def reset_internal_states(self, record=None):
43    """Resets the internal state of the recorder.
44
45    Args:
46      record: records.TestResultRecord, the test record for a test.
47    """
48    self._record = None
49    self._count = 0
50    self._record = record
51
52  @property
53  def has_error(self):
54    """If any error has been recorded since the last reset."""
55    return self._count > 0
56
57  @property
58  def error_count(self):
59    """The number of errors that have been recorded since last reset."""
60    return self._count
61
62  def add_error(self, error):
63    """Record an error from expect APIs.
64
65    This method generates a position stamp for the expect. The stamp is
66    composed of a timestamp and the number of errors recorded so far.
67
68    Args:
69      error: Exception or signals.ExceptionRecord, the error to add.
70    """
71    self._count += 1
72    self._record.add_error('expect@%s+%s' % (time.time(), self._count), error)
73
74
75def expect_true(condition, msg, extras=None):
76  """Expects an expression evaluates to True.
77
78  If the expectation is not met, the test is marked as fail after its
79  execution finishes.
80
81  Args:
82    expr: The expression that is evaluated.
83    msg: A string explaining the details in case of failure.
84    extras: An optional field for extra information to be included in test
85      result.
86  """
87  try:
88    asserts.assert_true(condition, msg, extras)
89  except signals.TestSignal as e:
90    logging.exception('Expected a `True` value, got `False`.')
91    recorder.add_error(e)
92
93
94def expect_false(condition, msg, extras=None):
95  """Expects an expression evaluates to False.
96
97  If the expectation is not met, the test is marked as fail after its
98  execution finishes.
99
100  Args:
101    expr: The expression that is evaluated.
102    msg: A string explaining the details in case of failure.
103    extras: An optional field for extra information to be included in test
104      result.
105  """
106  try:
107    asserts.assert_false(condition, msg, extras)
108  except signals.TestSignal as e:
109    logging.exception('Expected a `False` value, got `True`.')
110    recorder.add_error(e)
111
112
113def expect_equal(first, second, msg=None, extras=None):
114  """Expects the equality of objects, otherwise fail the test.
115
116  If the expectation is not met, the test is marked as fail after its
117  execution finishes.
118
119  Error message is "first != second" by default. Additional explanation can
120  be supplied in the message.
121
122  Args:
123    first: The first object to compare.
124    second: The second object to compare.
125    msg: A string that adds additional info about the failure.
126    extras: An optional field for extra information to be included in test
127      result.
128  """
129  try:
130    asserts.assert_equal(first, second, msg, extras)
131  except signals.TestSignal as e:
132    logging.exception(
133        'Expected %s equals to %s, but they are not.', first, second
134    )
135    recorder.add_error(e)
136
137
138@contextlib.contextmanager
139def expect_no_raises(message=None, extras=None):
140  """Expects no exception is raised in a context.
141
142  If the expectation is not met, the test is marked as fail after its
143  execution finishes.
144
145  A default message is added to the exception `details`.
146
147  Args:
148    message: string, custom message to add to exception's `details`.
149    extras: An optional field for extra information to be included in test
150      result.
151  """
152  try:
153    yield
154  except Exception as e:
155    e_record = records.ExceptionRecord(e)
156    if extras:
157      e_record.extras = extras
158    msg = message or 'Got an unexpected exception'
159    details = '%s: %s' % (msg, e_record.details)
160    logging.exception(details)
161    e_record.details = details
162    recorder.add_error(e_record)
163
164
165recorder = _ExpectErrorRecorder(DEFAULT_TEST_RESULT_RECORD)
166