core.py 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280
  1. # -*- coding: utf-8 -*-
  2. # <sure - utility belt for automated testing in python>
  3. # Copyright (C) <2010-2017> Gabriel Falcão <gabriel@nacaolivre.org>
  4. #
  5. # This program is free software: you can redistribute it and/or modify
  6. # it under the terms of the GNU General Public License as published by
  7. # the Free Software Foundation, either version 3 of the License, or
  8. # (at your option) any later version.
  9. #
  10. # This program is distributed in the hope that it will be useful,
  11. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  12. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  13. # GNU General Public License for more details.
  14. #
  15. # You should have received a copy of the GNU General Public License
  16. # along with this program. If not, see <http://www.gnu.org/licenses/>.
  17. from __future__ import unicode_literals
  18. import os
  19. try:
  20. from mock import _CallList
  21. except ImportError:
  22. from mock.mock import _CallList
  23. import inspect
  24. from six import (
  25. text_type, integer_types, string_types, binary_type,
  26. get_function_code
  27. )
  28. from sure.terminal import red, green, yellow
  29. from sure.compat import safe_repr, OrderedDict
  30. class Anything(object):
  31. """Represents any possible value. Its existence is solely for
  32. idiomatic purposes.
  33. """
  34. def __eq__(self, _):
  35. return True
  36. anything = Anything()
  37. class DeepExplanation(text_type):
  38. def get_header(self, X, Y, suffix):
  39. params = (safe_repr(X), safe_repr(Y), text_type(suffix))
  40. header = "given\nX = {0}\n and\nY = {1}\n{2}".format(*params)
  41. return yellow(header).strip()
  42. def get_assertion(self, X, Y):
  43. return AssertionError(self.get_header(X, Y, self))
  44. def as_assertion(self, X, Y):
  45. raise self.get_assertion(X, Y)
  46. class DeepComparison(object):
  47. def __init__(self, X, Y, epsilon=None, parent=None):
  48. self.complex_cmp_funcs = {
  49. float: self.compare_floats,
  50. dict: self.compare_dicts,
  51. list: self.compare_iterables,
  52. tuple: self.compare_iterables,
  53. OrderedDict: self.compare_ordereddict
  54. }
  55. self.operands = X, Y
  56. self.epsilon = epsilon
  57. self.parent = parent
  58. self._context = None
  59. def is_simple(self, obj):
  60. return isinstance(obj, (
  61. string_types, integer_types, binary_type, Anything
  62. ))
  63. def is_complex(self, obj):
  64. return isinstance(obj, tuple(self.complex_cmp_funcs.keys()))
  65. def compare_complex_stuff(self, X, Y):
  66. return self.complex_cmp_funcs.get(type(X), self.compare_generic)(X, Y)
  67. def compare_generic(self, X, Y, msg_format='X{0} != Y{1}'):
  68. c = self.get_context()
  69. if X == Y:
  70. return True
  71. else:
  72. m = msg_format.format(red(c.current_X_keys), green(c.current_Y_keys))
  73. return DeepExplanation(m)
  74. def compare_floats(self, X, Y):
  75. c = self.get_context()
  76. if self.epsilon is None:
  77. return self.compare_generic(X, Y)
  78. if abs(X - Y) <= self.epsilon:
  79. return True
  80. else:
  81. m = 'X{0}±{1} != Y{2}±{3}'.format(
  82. red(c.current_X_keys), self.epsilon, green(c.current_Y_keys), self.epsilon)
  83. return DeepExplanation(m)
  84. def compare_dicts(self, X, Y):
  85. c = self.get_context()
  86. x_keys = list(sorted(X.keys()))
  87. y_keys = list(sorted(Y.keys()))
  88. diff_x = list(set(x_keys).difference(set(y_keys)))
  89. diff_y = list(set(y_keys).difference(set(x_keys)))
  90. if diff_x:
  91. msg = "X{0} has the key {1!r} whereas Y{2} does not".format(
  92. red(c.current_X_keys),
  93. safe_repr(diff_x[0]),
  94. green(c.current_Y_keys))
  95. return DeepExplanation(msg)
  96. elif diff_y:
  97. msg = "X{0} does not have the key {1!r} whereas Y{2} has it".format(
  98. red(c.current_X_keys),
  99. safe_repr(diff_y[0]),
  100. green(c.current_Y_keys))
  101. return DeepExplanation(msg)
  102. elif X == Y:
  103. return True
  104. else:
  105. for key_X, key_Y in zip(x_keys, y_keys):
  106. self.key_X = key_X
  107. self.key_Y = key_Y
  108. value_X = X[key_X]
  109. value_Y = Y[key_Y]
  110. child = DeepComparison(
  111. value_X,
  112. value_Y,
  113. epsilon=self.epsilon,
  114. parent=self,
  115. ).compare()
  116. if isinstance(child, DeepExplanation):
  117. return child
  118. def compare_ordereddict(self, X, Y):
  119. """Compares two instances of an OrderedDict."""
  120. # check if OrderedDict instances have the same keys and values
  121. child = self.compare_dicts(X, Y)
  122. if isinstance(child, DeepExplanation):
  123. return child
  124. # check if the order of the keys is the same
  125. for i, j in zip(X.items(), Y.items()):
  126. if i[0] != j[0]:
  127. c = self.get_context()
  128. msg = "X{0} and Y{1} are in a different order".format(
  129. red(c.current_X_keys), green(c.current_Y_keys)
  130. )
  131. return DeepExplanation(msg)
  132. return True
  133. def get_context(self):
  134. if self._context:
  135. return self._context
  136. X_keys = []
  137. Y_keys = []
  138. comp = self
  139. while comp.parent:
  140. X_keys.insert(0, comp.parent.key_X)
  141. Y_keys.insert(0, comp.parent.key_Y)
  142. comp = comp.parent
  143. def get_keys(i):
  144. if not i:
  145. return ''
  146. return '[{0}]'.format(']['.join(map(safe_repr, i)))
  147. class ComparisonContext:
  148. current_X_keys = get_keys(X_keys)
  149. current_Y_keys = get_keys(Y_keys)
  150. parent = comp
  151. self._context = ComparisonContext()
  152. return self._context
  153. def compare_iterables(self, X, Y):
  154. len_X, len_Y = map(len, (X, Y))
  155. if len_X > len_Y:
  156. msg = "X has {0} items whereas Y has only {1}".format(len_X, len_Y)
  157. return DeepExplanation(msg)
  158. elif len_X < len_Y:
  159. msg = "Y has {0} items whereas X has only {1}".format(len_Y, len_X)
  160. return DeepExplanation(msg)
  161. elif X == Y:
  162. return True
  163. else:
  164. for i, (value_X, value_Y) in enumerate(zip(X, Y)):
  165. self.key_X = self.key_Y = i
  166. child = DeepComparison(
  167. value_X,
  168. value_Y,
  169. epsilon=self.epsilon,
  170. parent=self,
  171. ).compare()
  172. if isinstance(child, DeepExplanation):
  173. return child
  174. def compare(self):
  175. X, Y = self.operands
  176. if isinstance(X, _CallList):
  177. X = list(X)
  178. if isinstance(Y, _CallList):
  179. X = list(Y)
  180. c = self.get_context()
  181. if self.is_complex(X) and type(X) is type(Y):
  182. return self.compare_complex_stuff(X, Y)
  183. def safe_format_repr(string):
  184. "Escape '{' and '}' in string for use with str.format()"
  185. if not isinstance(string, (string_types, binary_type)):
  186. return string
  187. orig_str_type = type(string)
  188. if isinstance(string, binary_type):
  189. safe_repr = string.replace(b'{', b'{{').replace(b'}', b'}}')
  190. else:
  191. safe_repr = string.replace('{', '{{').replace('}', '}}')
  192. # NOTE: str.replace() automatically converted the 'string' to 'unicode' in Python 2
  193. return orig_str_type(safe_repr)
  194. # get safe representation for X and Y
  195. safe_X, safe_Y = safe_format_repr(X), safe_format_repr(Y)
  196. # maintaining backwards compatability between error messages
  197. kwargs = {}
  198. if self.is_simple(X) and self.is_simple(Y):
  199. kwargs['msg_format'] = 'X{{0}} is {0!r} whereas Y{{1}} is {1!r}'.format(safe_X, safe_Y)
  200. elif type(X) is not type(Y):
  201. kwargs['msg_format'] = 'X{{0}} is a {0} and Y{{1}} is a {1} instead'.format(
  202. type(X).__name__, type(Y).__name__)
  203. exp = self.compare_generic(X, Y, **kwargs)
  204. if isinstance(exp, DeepExplanation):
  205. original_X, original_Y = c.parent.operands
  206. raise exp.as_assertion(original_X, original_Y)
  207. return exp
  208. def explanation(self):
  209. return self._explanation
  210. def _get_file_name(func):
  211. try:
  212. name = inspect.getfile(func)
  213. except AttributeError:
  214. name = get_function_code(func).co_filename
  215. return os.path.abspath(name)
  216. def _get_line_number(func):
  217. try:
  218. return inspect.getlineno(func)
  219. except AttributeError:
  220. return get_function_code(func).co_firstlineno
  221. def itemize_length(items):
  222. length = len(items)
  223. return '{0} item{1}'.format(length, length > 1 and "s" or "")