exclusions.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476
  1. # testing/exclusions.py
  2. # Copyright (C) 2005-2026 the SQLAlchemy authors and contributors
  3. # <see AUTHORS file>
  4. #
  5. # This module is part of SQLAlchemy and is released under
  6. # the MIT License: https://www.opensource.org/licenses/mit-license.php
  7. # mypy: ignore-errors
  8. import contextlib
  9. import operator
  10. import re
  11. import sys
  12. from . import config
  13. from .. import util
  14. from ..util import decorator
  15. from ..util.compat import inspect_getfullargspec
  16. def skip_if(predicate, reason=None):
  17. rule = compound()
  18. pred = _as_predicate(predicate, reason)
  19. rule.skips.add(pred)
  20. return rule
  21. def fails_if(predicate, reason=None):
  22. rule = compound()
  23. pred = _as_predicate(predicate, reason)
  24. rule.fails.add(pred)
  25. return rule
  26. def warns_if(predicate, expression, assert_):
  27. rule = compound()
  28. pred = _as_predicate(predicate)
  29. rule.warns[pred] = (expression, assert_)
  30. return rule
  31. class compound:
  32. def __init__(self):
  33. self.fails = set()
  34. self.skips = set()
  35. self.warns = {}
  36. def __add__(self, other):
  37. return self.add(other)
  38. def as_skips(self):
  39. rule = compound()
  40. rule.skips.update(self.skips)
  41. rule.skips.update(self.fails)
  42. return rule
  43. def add(self, *others):
  44. copy = compound()
  45. copy.fails.update(self.fails)
  46. copy.skips.update(self.skips)
  47. copy.warns.update(self.warns)
  48. for other in others:
  49. copy.fails.update(other.fails)
  50. copy.skips.update(other.skips)
  51. copy.warns.update(other.warns)
  52. return copy
  53. def not_(self):
  54. copy = compound()
  55. copy.fails.update(NotPredicate(fail) for fail in self.fails)
  56. copy.skips.update(NotPredicate(skip) for skip in self.skips)
  57. copy.warns.update(
  58. {
  59. NotPredicate(warn): element
  60. for warn, element in self.warns.items()
  61. }
  62. )
  63. return copy
  64. @property
  65. def enabled(self):
  66. return self.enabled_for_config(config._current)
  67. def enabled_for_config(self, config):
  68. for predicate in self.skips.union(self.fails):
  69. if predicate(config):
  70. return False
  71. else:
  72. return True
  73. def matching_warnings(self, config):
  74. return [
  75. message
  76. for predicate, (message, assert_) in self.warns.items()
  77. if predicate(config)
  78. ]
  79. def matching_config_reasons(self, config):
  80. return [
  81. predicate._as_string(config)
  82. for predicate in self.skips.union(self.fails)
  83. if predicate(config)
  84. ]
  85. def _extend(self, other):
  86. self.skips.update(other.skips)
  87. self.fails.update(other.fails)
  88. self.warns.update(other.warns)
  89. def __call__(self, fn):
  90. if hasattr(fn, "_sa_exclusion_extend"):
  91. fn._sa_exclusion_extend._extend(self)
  92. return fn
  93. @decorator
  94. def decorate(fn, *args, **kw):
  95. return self._do(config._current, fn, *args, **kw)
  96. decorated = decorate(fn)
  97. decorated._sa_exclusion_extend = self
  98. return decorated
  99. @contextlib.contextmanager
  100. def fail_if(self):
  101. all_fails = compound()
  102. all_fails.fails.update(self.skips.union(self.fails))
  103. try:
  104. yield
  105. except Exception as ex:
  106. all_fails._expect_failure(config._current, ex)
  107. else:
  108. all_fails._expect_success(config._current)
  109. def _do(self, cfg, fn, *args, **kw):
  110. for skip in self.skips:
  111. if skip(cfg):
  112. msg = "'%s' : %s" % (
  113. config.get_current_test_name(),
  114. skip._as_string(cfg),
  115. )
  116. config.skip_test(msg)
  117. if self.warns:
  118. from .assertions import expect_warnings
  119. @contextlib.contextmanager
  120. def _expect_warnings():
  121. with contextlib.ExitStack() as stack:
  122. for expression, assert_ in self.warns.values():
  123. stack.enter_context(
  124. expect_warnings(expression, assert_=assert_)
  125. )
  126. yield
  127. ctx = _expect_warnings()
  128. else:
  129. ctx = contextlib.nullcontext()
  130. try:
  131. with ctx:
  132. return_value = fn(*args, **kw)
  133. except Exception as ex:
  134. self._expect_failure(cfg, ex, name=fn.__name__)
  135. else:
  136. self._expect_success(cfg, name=fn.__name__)
  137. return return_value
  138. def _expect_failure(self, config, ex, name="block"):
  139. for fail in self.fails:
  140. if fail(config):
  141. print(
  142. "%s failed as expected (%s): %s "
  143. % (name, fail._as_string(config), ex)
  144. )
  145. break
  146. else:
  147. raise ex.with_traceback(sys.exc_info()[2])
  148. def _expect_success(self, config, name="block"):
  149. if not self.fails:
  150. return
  151. for fail in self.fails:
  152. if fail(config):
  153. raise AssertionError(
  154. "Unexpected success for '%s' (%s)"
  155. % (
  156. name,
  157. " and ".join(
  158. fail._as_string(config) for fail in self.fails
  159. ),
  160. )
  161. )
  162. def only_if(predicate, reason=None):
  163. predicate = _as_predicate(predicate)
  164. return skip_if(NotPredicate(predicate), reason)
  165. def succeeds_if(predicate, reason=None):
  166. predicate = _as_predicate(predicate)
  167. return fails_if(NotPredicate(predicate), reason)
  168. class Predicate:
  169. @classmethod
  170. def as_predicate(cls, predicate, description=None):
  171. if isinstance(predicate, compound):
  172. return cls.as_predicate(predicate.enabled_for_config, description)
  173. elif isinstance(predicate, Predicate):
  174. if description and predicate.description is None:
  175. predicate.description = description
  176. return predicate
  177. elif isinstance(predicate, (list, set)):
  178. return OrPredicate(
  179. [cls.as_predicate(pred) for pred in predicate], description
  180. )
  181. elif isinstance(predicate, tuple):
  182. return SpecPredicate(*predicate)
  183. elif isinstance(predicate, str):
  184. tokens = re.match(
  185. r"([\+\w]+)\s*(?:(>=|==|!=|<=|<|>)\s*([\d\.]+))?", predicate
  186. )
  187. if not tokens:
  188. raise ValueError(
  189. "Couldn't locate DB name in predicate: %r" % predicate
  190. )
  191. db = tokens.group(1)
  192. op = tokens.group(2)
  193. spec = (
  194. tuple(int(d) for d in tokens.group(3).split("."))
  195. if tokens.group(3)
  196. else None
  197. )
  198. return SpecPredicate(db, op, spec, description=description)
  199. elif callable(predicate):
  200. return LambdaPredicate(predicate, description)
  201. else:
  202. assert False, "unknown predicate type: %s" % predicate
  203. def _format_description(self, config, negate=False):
  204. bool_ = self(config)
  205. if negate:
  206. bool_ = not negate
  207. return self.description % {
  208. "driver": (
  209. config.db.url.get_driver_name() if config else "<no driver>"
  210. ),
  211. "database": (
  212. config.db.url.get_backend_name() if config else "<no database>"
  213. ),
  214. "doesnt_support": "doesn't support" if bool_ else "does support",
  215. "does_support": "does support" if bool_ else "doesn't support",
  216. }
  217. def _as_string(self, config=None, negate=False):
  218. raise NotImplementedError()
  219. class BooleanPredicate(Predicate):
  220. def __init__(self, value, description=None):
  221. self.value = value
  222. self.description = description or "boolean %s" % value
  223. def __call__(self, config):
  224. return self.value
  225. def _as_string(self, config, negate=False):
  226. return self._format_description(config, negate=negate)
  227. class SpecPredicate(Predicate):
  228. def __init__(self, db, op=None, spec=None, description=None):
  229. self.db = db
  230. self.op = op
  231. self.spec = spec
  232. self.description = description
  233. _ops = {
  234. "<": operator.lt,
  235. ">": operator.gt,
  236. "==": operator.eq,
  237. "!=": operator.ne,
  238. "<=": operator.le,
  239. ">=": operator.ge,
  240. "in": operator.contains,
  241. "between": lambda val, pair: val >= pair[0] and val <= pair[1],
  242. }
  243. def __call__(self, config):
  244. if config is None:
  245. return False
  246. engine = config.db
  247. if "+" in self.db:
  248. dialect, driver = self.db.split("+")
  249. else:
  250. dialect, driver = self.db, None
  251. if dialect and engine.name != dialect:
  252. return False
  253. if driver is not None and engine.driver != driver:
  254. return False
  255. if self.op is not None:
  256. assert driver is None, "DBAPI version specs not supported yet"
  257. version = _server_version(engine)
  258. oper = (
  259. hasattr(self.op, "__call__") and self.op or self._ops[self.op]
  260. )
  261. return oper(version, self.spec)
  262. else:
  263. return True
  264. def _as_string(self, config, negate=False):
  265. if self.description is not None:
  266. return self._format_description(config)
  267. elif self.op is None:
  268. if negate:
  269. return "not %s" % self.db
  270. else:
  271. return "%s" % self.db
  272. else:
  273. if negate:
  274. return "not %s %s %s" % (self.db, self.op, self.spec)
  275. else:
  276. return "%s %s %s" % (self.db, self.op, self.spec)
  277. class LambdaPredicate(Predicate):
  278. def __init__(self, lambda_, description=None, args=None, kw=None):
  279. spec = inspect_getfullargspec(lambda_)
  280. if not spec[0]:
  281. self.lambda_ = lambda db: lambda_()
  282. else:
  283. self.lambda_ = lambda_
  284. self.args = args or ()
  285. self.kw = kw or {}
  286. if description:
  287. self.description = description
  288. elif lambda_.__doc__:
  289. self.description = lambda_.__doc__
  290. else:
  291. self.description = "custom function"
  292. def __call__(self, config):
  293. return self.lambda_(config)
  294. def _as_string(self, config, negate=False):
  295. return self._format_description(config)
  296. class NotPredicate(Predicate):
  297. def __init__(self, predicate, description=None):
  298. self.predicate = predicate
  299. self.description = description
  300. def __call__(self, config):
  301. return not self.predicate(config)
  302. def _as_string(self, config, negate=False):
  303. if self.description:
  304. return self._format_description(config, not negate)
  305. else:
  306. return self.predicate._as_string(config, not negate)
  307. class OrPredicate(Predicate):
  308. def __init__(self, predicates, description=None):
  309. self.predicates = predicates
  310. self.description = description
  311. def __call__(self, config):
  312. for pred in self.predicates:
  313. if pred(config):
  314. return True
  315. return False
  316. def _eval_str(self, config, negate=False):
  317. if negate:
  318. conjunction = " and "
  319. else:
  320. conjunction = " or "
  321. return conjunction.join(
  322. p._as_string(config, negate=negate) for p in self.predicates
  323. )
  324. def _negation_str(self, config):
  325. if self.description is not None:
  326. return "Not " + self._format_description(config)
  327. else:
  328. return self._eval_str(config, negate=True)
  329. def _as_string(self, config, negate=False):
  330. if negate:
  331. return self._negation_str(config)
  332. else:
  333. if self.description is not None:
  334. return self._format_description(config)
  335. else:
  336. return self._eval_str(config)
  337. _as_predicate = Predicate.as_predicate
  338. def _is_excluded(db, op, spec):
  339. return SpecPredicate(db, op, spec)(config._current)
  340. def _server_version(engine):
  341. """Return a server_version_info tuple."""
  342. # force metadata to be retrieved
  343. conn = engine.connect()
  344. version = getattr(engine.dialect, "server_version_info", None)
  345. if version is None:
  346. version = ()
  347. conn.close()
  348. return version
  349. def db_spec(*dbs):
  350. return OrPredicate([Predicate.as_predicate(db) for db in dbs])
  351. def open(): # noqa
  352. return skip_if(BooleanPredicate(False, "mark as execute"))
  353. def closed(reason="marked as skip"):
  354. return skip_if(BooleanPredicate(True, reason))
  355. def fails(reason=None):
  356. return fails_if(BooleanPredicate(True, reason or "expected to fail"))
  357. def future():
  358. return fails_if(BooleanPredicate(True, "Future feature"))
  359. def fails_on(db, reason=None):
  360. return fails_if(db, reason)
  361. def fails_on_everything_except(*dbs):
  362. return succeeds_if(OrPredicate([Predicate.as_predicate(db) for db in dbs]))
  363. def skip(db, reason=None):
  364. return skip_if(db, reason)
  365. def only_on(dbs, reason=None):
  366. return only_if(
  367. OrPredicate(
  368. [Predicate.as_predicate(db, reason) for db in util.to_list(dbs)]
  369. )
  370. )
  371. def exclude(db, op, spec, reason=None):
  372. return skip_if(SpecPredicate(db, op, spec), reason)
  373. def against(config, *queries):
  374. assert queries, "no queries sent!"
  375. return OrPredicate([Predicate.as_predicate(query) for query in queries])(
  376. config
  377. )