1
0

helpers.py 60 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773
  1. """
  2. :copyright: Copyright 2013-2017 by the SaltStack Team, see AUTHORS for more details.
  3. :license: Apache 2.0, see LICENSE for more details.
  4. tests.support.helpers
  5. ~~~~~~~~~~~~~~~~~~~~~
  6. Test support helpers
  7. """
  8. import base64
  9. import errno
  10. import fnmatch
  11. import functools
  12. import inspect
  13. import logging
  14. import os
  15. import random
  16. import shutil
  17. import socket
  18. import string
  19. import subprocess
  20. import sys
  21. import tempfile
  22. import textwrap
  23. import threading
  24. import time
  25. import types
  26. from contextlib import contextmanager
  27. import pytest
  28. import salt.ext.tornado.ioloop
  29. import salt.ext.tornado.web
  30. import salt.utils.files
  31. import salt.utils.platform
  32. import salt.utils.pycrypto
  33. import salt.utils.stringutils
  34. import salt.utils.versions
  35. from salt.ext import six
  36. from salt.ext.six.moves import builtins
  37. from saltfactories.exceptions import FactoryFailure as ProcessFailed
  38. from saltfactories.utils.ports import get_unused_localhost_port
  39. from saltfactories.utils.processes import ProcessResult
  40. from tests.support.mock import patch
  41. from tests.support.runtests import RUNTIME_VARS
  42. from tests.support.sminion import create_sminion
  43. from tests.support.unit import SkipTest, _id, skip, skipIf
  44. log = logging.getLogger(__name__)
  45. HAS_SYMLINKS = None
  46. PRE_PYTEST_SKIP_OR_NOT = "PRE_PYTEST_DONT_SKIP" not in os.environ
  47. PRE_PYTEST_SKIP_REASON = (
  48. "PRE PYTEST - This test was skipped before running under pytest"
  49. )
  50. PRE_PYTEST_SKIP = pytest.mark.skipif(
  51. PRE_PYTEST_SKIP_OR_NOT, reason=PRE_PYTEST_SKIP_REASON
  52. )
  53. SKIP_IF_NOT_RUNNING_PYTEST = skipIf(
  54. RUNTIME_VARS.PYTEST_SESSION is False, "These tests now require running under PyTest"
  55. )
  56. def no_symlinks():
  57. """
  58. Check if git is installed and has symlinks enabled in the configuration.
  59. """
  60. global HAS_SYMLINKS
  61. if HAS_SYMLINKS is not None:
  62. return not HAS_SYMLINKS
  63. output = ""
  64. try:
  65. output = subprocess.Popen(
  66. ["git", "config", "--get", "core.symlinks"],
  67. cwd=RUNTIME_VARS.TMP,
  68. stdout=subprocess.PIPE,
  69. ).communicate()[0]
  70. except OSError as exc:
  71. if exc.errno != errno.ENOENT:
  72. raise
  73. except subprocess.CalledProcessError:
  74. # git returned non-zero status
  75. pass
  76. HAS_SYMLINKS = False
  77. if output.strip() == "true":
  78. HAS_SYMLINKS = True
  79. return not HAS_SYMLINKS
  80. def destructiveTest(caller):
  81. """
  82. Mark a test case as a destructive test for example adding or removing users
  83. from your system.
  84. .. code-block:: python
  85. class MyTestCase(TestCase):
  86. @destructiveTest
  87. def test_create_user(self):
  88. pass
  89. """
  90. # Late import
  91. from tests.support.runtests import RUNTIME_VARS
  92. if RUNTIME_VARS.PYTEST_SESSION:
  93. setattr(caller, "__destructive_test__", True)
  94. if os.environ.get("DESTRUCTIVE_TESTS", "False").lower() == "false":
  95. reason = "Destructive tests are disabled"
  96. if not isinstance(caller, type):
  97. @functools.wraps(caller)
  98. def skip_wrapper(*args, **kwargs):
  99. raise SkipTest(reason)
  100. caller = skip_wrapper
  101. caller.__unittest_skip__ = True
  102. caller.__unittest_skip_why__ = reason
  103. return caller
  104. def expensiveTest(caller):
  105. """
  106. Mark a test case as an expensive test, for example, a test which can cost
  107. money(Salt's cloud provider tests).
  108. .. code-block:: python
  109. class MyTestCase(TestCase):
  110. @expensiveTest
  111. def test_create_user(self):
  112. pass
  113. """
  114. # Late import
  115. from tests.support.runtests import RUNTIME_VARS
  116. if RUNTIME_VARS.PYTEST_SESSION:
  117. setattr(caller, "__expensive_test__", True)
  118. if os.environ.get("EXPENSIVE_TESTS", "False").lower() == "false":
  119. reason = "Expensive tests are disabled"
  120. if not isinstance(caller, type):
  121. @functools.wraps(caller)
  122. def skip_wrapper(*args, **kwargs):
  123. raise SkipTest(reason)
  124. caller = skip_wrapper
  125. caller.__unittest_skip__ = True
  126. caller.__unittest_skip_why__ = reason
  127. return caller
  128. def slowTest(caller):
  129. """
  130. Mark a test case as a slow test.
  131. .. code-block:: python
  132. class MyTestCase(TestCase):
  133. @slowTest
  134. def test_that_takes_much_time(self):
  135. pass
  136. """
  137. # Late import
  138. from tests.support.runtests import RUNTIME_VARS
  139. if RUNTIME_VARS.PYTEST_SESSION:
  140. setattr(caller, "__slow_test__", True)
  141. return caller
  142. if os.environ.get("SLOW_TESTS", "False").lower() == "false":
  143. reason = "Slow tests are disabled"
  144. if not isinstance(caller, type):
  145. @functools.wraps(caller)
  146. def skip_wrapper(*args, **kwargs):
  147. raise SkipTest(reason)
  148. caller = skip_wrapper
  149. caller.__unittest_skip__ = True
  150. caller.__unittest_skip_why__ = reason
  151. return caller
  152. def flaky(caller=None, condition=True, attempts=4):
  153. """
  154. Mark a test as flaky. The test will attempt to run five times,
  155. looking for a successful run. After an immediate second try,
  156. it will use an exponential backoff starting with one second.
  157. .. code-block:: python
  158. class MyTestCase(TestCase):
  159. @flaky
  160. def test_sometimes_works(self):
  161. pass
  162. """
  163. if caller is None:
  164. return functools.partial(flaky, condition=condition, attempts=attempts)
  165. if isinstance(condition, bool) and condition is False:
  166. # Don't even decorate
  167. return caller
  168. elif callable(condition):
  169. if condition() is False:
  170. # Don't even decorate
  171. return caller
  172. if inspect.isclass(caller):
  173. attrs = [n for n in dir(caller) if n.startswith("test_")]
  174. for attrname in attrs:
  175. try:
  176. function = getattr(caller, attrname)
  177. if not inspect.isfunction(function) and not inspect.ismethod(function):
  178. continue
  179. setattr(
  180. caller,
  181. attrname,
  182. flaky(caller=function, condition=condition, attempts=attempts),
  183. )
  184. except Exception as exc: # pylint: disable=broad-except
  185. log.exception(exc)
  186. continue
  187. return caller
  188. @functools.wraps(caller)
  189. def wrap(cls):
  190. for attempt in range(0, attempts):
  191. try:
  192. if attempt > 0:
  193. # Run through setUp again
  194. # We only run it after the first iteration(>0) because the regular
  195. # test runner will have already ran setUp the first time
  196. setup = getattr(cls, "setUp", None)
  197. if callable(setup):
  198. setup()
  199. return caller(cls)
  200. except SkipTest as exc:
  201. cls.skipTest(exc.args[0])
  202. except Exception as exc: # pylint: disable=broad-except
  203. exc_info = sys.exc_info()
  204. if isinstance(exc, SkipTest):
  205. six.reraise(*exc_info)
  206. if not isinstance(exc, AssertionError) and log.isEnabledFor(
  207. logging.DEBUG
  208. ):
  209. log.exception(exc, exc_info=exc_info)
  210. if attempt >= attempts - 1:
  211. # We won't try to run tearDown once the attempts are exhausted
  212. # because the regular test runner will do that for us
  213. six.reraise(*exc_info)
  214. # Run through tearDown again
  215. teardown = getattr(cls, "tearDown", None)
  216. if callable(teardown):
  217. teardown()
  218. backoff_time = attempt ** 2
  219. log.info("Found Exception. Waiting %s seconds to retry.", backoff_time)
  220. time.sleep(backoff_time)
  221. return cls
  222. return wrap
  223. def requires_sshd_server(caller):
  224. """
  225. Mark a test as requiring the tests SSH daemon running.
  226. .. code-block:: python
  227. class MyTestCase(TestCase):
  228. @requiresSshdServer
  229. def test_create_user(self):
  230. pass
  231. """
  232. raise RuntimeError(
  233. "Please replace @requires_sshd_server with @pytest.mark.requires_sshd_server"
  234. )
  235. class RedirectStdStreams:
  236. """
  237. Temporarily redirect system output to file like objects.
  238. Default is to redirect to `os.devnull`, which just mutes output, `stdout`
  239. and `stderr`.
  240. """
  241. def __init__(self, stdout=None, stderr=None):
  242. # Late import
  243. import salt.utils.files
  244. if stdout is None:
  245. # pylint: disable=resource-leakage
  246. stdout = salt.utils.files.fopen(os.devnull, "w")
  247. # pylint: enable=resource-leakage
  248. if stderr is None:
  249. # pylint: disable=resource-leakage
  250. stderr = salt.utils.files.fopen(os.devnull, "w")
  251. # pylint: enable=resource-leakage
  252. self.__stdout = stdout
  253. self.__stderr = stderr
  254. self.__redirected = False
  255. self.patcher = patch.multiple(sys, stderr=self.__stderr, stdout=self.__stdout)
  256. def __enter__(self):
  257. self.redirect()
  258. return self
  259. def __exit__(self, exc_type, exc_value, traceback):
  260. self.unredirect()
  261. def redirect(self):
  262. self.old_stdout = sys.stdout
  263. self.old_stdout.flush()
  264. self.old_stderr = sys.stderr
  265. self.old_stderr.flush()
  266. self.patcher.start()
  267. self.__redirected = True
  268. def unredirect(self):
  269. if not self.__redirected:
  270. return
  271. try:
  272. self.__stdout.flush()
  273. self.__stdout.close()
  274. except ValueError:
  275. # already closed?
  276. pass
  277. try:
  278. self.__stderr.flush()
  279. self.__stderr.close()
  280. except ValueError:
  281. # already closed?
  282. pass
  283. self.patcher.stop()
  284. def flush(self):
  285. if self.__redirected:
  286. try:
  287. self.__stdout.flush()
  288. except Exception: # pylint: disable=broad-except
  289. pass
  290. try:
  291. self.__stderr.flush()
  292. except Exception: # pylint: disable=broad-except
  293. pass
  294. class TstSuiteLoggingHandler:
  295. """
  296. Simple logging handler which can be used to test if certain logging
  297. messages get emitted or not:
  298. .. code-block:: python
  299. with TstSuiteLoggingHandler() as handler:
  300. # (...) Do what ever you wish here
  301. handler.messages # here are the emitted log messages
  302. """
  303. def __init__(self, level=0, format="%(levelname)s:%(message)s"):
  304. self.level = level
  305. self.format = format
  306. self.activated = False
  307. self.prev_logging_level = None
  308. def activate(self):
  309. class Handler(logging.Handler):
  310. def __init__(self, level):
  311. logging.Handler.__init__(self, level)
  312. self.messages = []
  313. def emit(self, record):
  314. self.messages.append(self.format(record))
  315. self.handler = Handler(self.level)
  316. formatter = logging.Formatter(self.format)
  317. self.handler.setFormatter(formatter)
  318. logging.root.addHandler(self.handler)
  319. self.activated = True
  320. # Make sure we're running with the lowest logging level with our
  321. # tests logging handler
  322. current_logging_level = logging.root.getEffectiveLevel()
  323. if current_logging_level > logging.DEBUG:
  324. self.prev_logging_level = current_logging_level
  325. logging.root.setLevel(0)
  326. def deactivate(self):
  327. if not self.activated:
  328. return
  329. logging.root.removeHandler(self.handler)
  330. # Restore previous logging level if changed
  331. if self.prev_logging_level is not None:
  332. logging.root.setLevel(self.prev_logging_level)
  333. @property
  334. def messages(self):
  335. if not self.activated:
  336. return []
  337. return self.handler.messages
  338. def clear(self):
  339. self.handler.messages = []
  340. def __enter__(self):
  341. self.activate()
  342. return self
  343. def __exit__(self, type, value, traceback):
  344. self.deactivate()
  345. self.activated = False
  346. # Mimic some handler attributes and methods
  347. @property
  348. def lock(self):
  349. if self.activated:
  350. return self.handler.lock
  351. def createLock(self):
  352. if self.activated:
  353. return self.handler.createLock()
  354. def acquire(self):
  355. if self.activated:
  356. return self.handler.acquire()
  357. def release(self):
  358. if self.activated:
  359. return self.handler.release()
  360. class ForceImportErrorOn:
  361. """
  362. This class is meant to be used in mock'ed test cases which require an
  363. ``ImportError`` to be raised.
  364. >>> import os.path
  365. >>> with ForceImportErrorOn('os.path'):
  366. ... import os.path
  367. ...
  368. Traceback (most recent call last):
  369. File "<stdin>", line 2, in <module>
  370. File "salttesting/helpers.py", line 263, in __import__
  371. 'Forced ImportError raised for {0!r}'.format(name)
  372. ImportError: Forced ImportError raised for 'os.path'
  373. >>>
  374. >>> with ForceImportErrorOn(('os', 'path')):
  375. ... import os.path
  376. ... sys.modules.pop('os', None)
  377. ... from os import path
  378. ...
  379. <module 'os' from '/usr/lib/python2.7/os.pyc'>
  380. Traceback (most recent call last):
  381. File "<stdin>", line 4, in <module>
  382. File "salttesting/helpers.py", line 288, in __fake_import__
  383. name, ', '.join(fromlist)
  384. ImportError: Forced ImportError raised for 'from os import path'
  385. >>>
  386. >>> with ForceImportErrorOn(('os', 'path'), 'os.path'):
  387. ... import os.path
  388. ... sys.modules.pop('os', None)
  389. ... from os import path
  390. ...
  391. Traceback (most recent call last):
  392. File "<stdin>", line 2, in <module>
  393. File "salttesting/helpers.py", line 281, in __fake_import__
  394. 'Forced ImportError raised for {0!r}'.format(name)
  395. ImportError: Forced ImportError raised for 'os.path'
  396. >>>
  397. """
  398. def __init__(self, *module_names):
  399. self.__module_names = {}
  400. for entry in module_names:
  401. if isinstance(entry, (list, tuple)):
  402. modname = entry[0]
  403. self.__module_names[modname] = set(entry[1:])
  404. else:
  405. self.__module_names[entry] = None
  406. self.__original_import = builtins.__import__
  407. self.patcher = patch.object(builtins, "__import__", self.__fake_import__)
  408. def patch_import_function(self):
  409. self.patcher.start()
  410. def restore_import_funtion(self):
  411. self.patcher.stop()
  412. def __fake_import__(
  413. self, name, globals_=None, locals_=None, fromlist=None, level=None
  414. ):
  415. if six.PY2:
  416. if globals_ is None:
  417. globals_ = {}
  418. if locals_ is None:
  419. locals_ = {}
  420. if level is None:
  421. level = 0
  422. if fromlist is None:
  423. fromlist = []
  424. if name in self.__module_names:
  425. importerror_fromlist = self.__module_names.get(name)
  426. if importerror_fromlist is None:
  427. raise ImportError("Forced ImportError raised for {!r}".format(name))
  428. if importerror_fromlist.intersection(set(fromlist)):
  429. raise ImportError(
  430. "Forced ImportError raised for {!r}".format(
  431. "from {} import {}".format(name, ", ".join(fromlist))
  432. )
  433. )
  434. return self.__original_import(name, globals_, locals_, fromlist, level)
  435. def __enter__(self):
  436. self.patch_import_function()
  437. return self
  438. def __exit__(self, exc_type, exc_value, traceback):
  439. self.restore_import_funtion()
  440. class MockWraps:
  441. """
  442. Helper class to be used with the mock library.
  443. To be used in the ``wraps`` keyword of ``Mock`` or ``MagicMock`` where you
  444. want to trigger a side effect for X times, and afterwards, call the
  445. original and un-mocked method.
  446. As an example:
  447. >>> def original():
  448. ... print 'original'
  449. ...
  450. >>> def side_effect():
  451. ... print 'side effect'
  452. ...
  453. >>> mw = MockWraps(original, 2, side_effect)
  454. >>> mw()
  455. side effect
  456. >>> mw()
  457. side effect
  458. >>> mw()
  459. original
  460. >>>
  461. """
  462. def __init__(self, original, expected_failures, side_effect):
  463. self.__original = original
  464. self.__expected_failures = expected_failures
  465. self.__side_effect = side_effect
  466. self.__call_counter = 0
  467. def __call__(self, *args, **kwargs):
  468. try:
  469. if self.__call_counter < self.__expected_failures:
  470. if isinstance(self.__side_effect, types.FunctionType):
  471. return self.__side_effect()
  472. raise self.__side_effect
  473. return self.__original(*args, **kwargs)
  474. finally:
  475. self.__call_counter += 1
  476. def requires_network(only_local_network=False):
  477. """
  478. Simple decorator which is supposed to skip a test case in case there's no
  479. network connection to the internet.
  480. """
  481. def decorator(func):
  482. @functools.wraps(func)
  483. def wrapper(cls, *args, **kwargs):
  484. has_local_network = False
  485. # First lets try if we have a local network. Inspired in
  486. # verify_socket
  487. try:
  488. pubsock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  489. retsock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  490. pubsock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
  491. pubsock.bind(("", 18000))
  492. pubsock.close()
  493. retsock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
  494. retsock.bind(("", 18001))
  495. retsock.close()
  496. has_local_network = True
  497. except OSError:
  498. # I wonder if we just have IPV6 support?
  499. try:
  500. pubsock = socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
  501. retsock = socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
  502. pubsock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
  503. pubsock.bind(("", 18000))
  504. pubsock.close()
  505. retsock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
  506. retsock.bind(("", 18001))
  507. retsock.close()
  508. has_local_network = True
  509. except OSError:
  510. # Let's continue
  511. pass
  512. if only_local_network is True:
  513. if has_local_network is False:
  514. # Since we're only supposed to check local network, and no
  515. # local network was detected, skip the test
  516. cls.skipTest("No local network was detected")
  517. return func(cls)
  518. if os.environ.get("NO_INTERNET"):
  519. cls.skipTest("Environment variable NO_INTERNET is set.")
  520. # We are using the google.com DNS records as numerical IPs to avoid
  521. # DNS lookups which could greatly slow down this check
  522. for addr in (
  523. "173.194.41.198",
  524. "173.194.41.199",
  525. "173.194.41.200",
  526. "173.194.41.201",
  527. "173.194.41.206",
  528. "173.194.41.192",
  529. "173.194.41.193",
  530. "173.194.41.194",
  531. "173.194.41.195",
  532. "173.194.41.196",
  533. "173.194.41.197",
  534. ):
  535. sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  536. try:
  537. sock.settimeout(0.25)
  538. sock.connect((addr, 80))
  539. # We connected? Stop the loop
  540. break
  541. except OSError:
  542. # Let's check the next IP
  543. continue
  544. else:
  545. cls.skipTest("No internet network connection was detected")
  546. finally:
  547. sock.close()
  548. return func(cls, *args, **kwargs)
  549. return wrapper
  550. return decorator
  551. def with_system_user(
  552. username, on_existing="delete", delete=True, password=None, groups=None
  553. ):
  554. """
  555. Create and optionally destroy a system user to be used within a test
  556. case. The system user is created using the ``user`` salt module.
  557. The decorated testcase function must accept 'username' as an argument.
  558. :param username: The desired username for the system user.
  559. :param on_existing: What to do when the desired username is taken. The
  560. available options are:
  561. * nothing: Do nothing, act as if the user was created.
  562. * delete: delete and re-create the existing user
  563. * skip: skip the test case
  564. """
  565. if on_existing not in ("nothing", "delete", "skip"):
  566. raise RuntimeError(
  567. "The value of 'on_existing' can only be one of, "
  568. "'nothing', 'delete' and 'skip'"
  569. )
  570. if not isinstance(delete, bool):
  571. raise RuntimeError("The value of 'delete' can only be 'True' or 'False'")
  572. def decorator(func):
  573. @functools.wraps(func)
  574. def wrap(cls):
  575. # Let's add the user to the system.
  576. log.debug("Creating system user {!r}".format(username))
  577. kwargs = {"timeout": 60, "groups": groups}
  578. if salt.utils.platform.is_windows():
  579. kwargs.update({"password": password})
  580. create_user = cls.run_function("user.add", [username], **kwargs)
  581. if not create_user:
  582. log.debug("Failed to create system user")
  583. # The user was not created
  584. if on_existing == "skip":
  585. cls.skipTest("Failed to create system user {!r}".format(username))
  586. if on_existing == "delete":
  587. log.debug("Deleting the system user {!r}".format(username))
  588. delete_user = cls.run_function(
  589. "user.delete", [username, True, True]
  590. )
  591. if not delete_user:
  592. cls.skipTest(
  593. "A user named {!r} already existed on the "
  594. "system and re-creating it was not possible".format(
  595. username
  596. )
  597. )
  598. log.debug("Second time creating system user {!r}".format(username))
  599. create_user = cls.run_function("user.add", [username], **kwargs)
  600. if not create_user:
  601. cls.skipTest(
  602. "A user named {!r} already existed, was deleted "
  603. "as requested, but re-creating it was not possible".format(
  604. username
  605. )
  606. )
  607. if not salt.utils.platform.is_windows() and password is not None:
  608. if salt.utils.platform.is_darwin():
  609. hashed_password = password
  610. else:
  611. hashed_password = salt.utils.pycrypto.gen_hash(password=password)
  612. hashed_password = "'{}'".format(hashed_password)
  613. add_pwd = cls.run_function(
  614. "shadow.set_password", [username, hashed_password]
  615. )
  616. failure = None
  617. try:
  618. try:
  619. return func(cls, username)
  620. except Exception as exc: # pylint: disable=W0703
  621. log.error(
  622. "Running {!r} raised an exception: {}".format(func, exc),
  623. exc_info=True,
  624. )
  625. # Store the original exception details which will be raised
  626. # a little further down the code
  627. failure = sys.exc_info()
  628. finally:
  629. if delete:
  630. delete_user = cls.run_function(
  631. "user.delete", [username, True, True], timeout=60
  632. )
  633. if not delete_user:
  634. if failure is None:
  635. log.warning(
  636. "Although the actual test-case did not fail, "
  637. "deleting the created system user {!r} "
  638. "afterwards did.".format(username)
  639. )
  640. else:
  641. log.warning(
  642. "The test-case failed and also did the removal"
  643. " of the system user {!r}".format(username)
  644. )
  645. if failure is not None:
  646. # If an exception was thrown, raise it
  647. raise failure[1].with_traceback(failure[2])
  648. return wrap
  649. return decorator
  650. def with_system_group(group, on_existing="delete", delete=True):
  651. """
  652. Create and optionally destroy a system group to be used within a test
  653. case. The system user is crated using the ``group`` salt module.
  654. The decorated testcase function must accept 'group' as an argument.
  655. :param group: The desired group name for the system user.
  656. :param on_existing: What to do when the desired username is taken. The
  657. available options are:
  658. * nothing: Do nothing, act as if the group was created
  659. * delete: delete and re-create the existing user
  660. * skip: skip the test case
  661. """
  662. if on_existing not in ("nothing", "delete", "skip"):
  663. raise RuntimeError(
  664. "The value of 'on_existing' can only be one of, "
  665. "'nothing', 'delete' and 'skip'"
  666. )
  667. if not isinstance(delete, bool):
  668. raise RuntimeError("The value of 'delete' can only be 'True' or 'False'")
  669. def decorator(func):
  670. @functools.wraps(func)
  671. def wrap(cls):
  672. # Let's add the user to the system.
  673. log.debug("Creating system group {!r}".format(group))
  674. create_group = cls.run_function("group.add", [group])
  675. if not create_group:
  676. log.debug("Failed to create system group")
  677. # The group was not created
  678. if on_existing == "skip":
  679. cls.skipTest("Failed to create system group {!r}".format(group))
  680. if on_existing == "delete":
  681. log.debug("Deleting the system group {!r}".format(group))
  682. delete_group = cls.run_function("group.delete", [group])
  683. if not delete_group:
  684. cls.skipTest(
  685. "A group named {!r} already existed on the "
  686. "system and re-creating it was not possible".format(group)
  687. )
  688. log.debug("Second time creating system group {!r}".format(group))
  689. create_group = cls.run_function("group.add", [group])
  690. if not create_group:
  691. cls.skipTest(
  692. "A group named {!r} already existed, was deleted "
  693. "as requested, but re-creating it was not possible".format(
  694. group
  695. )
  696. )
  697. failure = None
  698. try:
  699. try:
  700. return func(cls, group)
  701. except Exception as exc: # pylint: disable=W0703
  702. log.error(
  703. "Running {!r} raised an exception: {}".format(func, exc),
  704. exc_info=True,
  705. )
  706. # Store the original exception details which will be raised
  707. # a little further down the code
  708. failure = sys.exc_info()
  709. finally:
  710. if delete:
  711. delete_group = cls.run_function("group.delete", [group])
  712. if not delete_group:
  713. if failure is None:
  714. log.warning(
  715. "Although the actual test-case did not fail, "
  716. "deleting the created system group {!r} "
  717. "afterwards did.".format(group)
  718. )
  719. else:
  720. log.warning(
  721. "The test-case failed and also did the removal"
  722. " of the system group {!r}".format(group)
  723. )
  724. if failure is not None:
  725. # If an exception was thrown, raise it
  726. raise failure[1].with_traceback(failure[2])
  727. return wrap
  728. return decorator
  729. def with_system_user_and_group(username, group, on_existing="delete", delete=True):
  730. """
  731. Create and optionally destroy a system user and group to be used within a
  732. test case. The system user is crated using the ``user`` salt module, and
  733. the system group is created with the ``group`` salt module.
  734. The decorated testcase function must accept both the 'username' and 'group'
  735. arguments.
  736. :param username: The desired username for the system user.
  737. :param group: The desired name for the system group.
  738. :param on_existing: What to do when the desired username is taken. The
  739. available options are:
  740. * nothing: Do nothing, act as if the user was created.
  741. * delete: delete and re-create the existing user
  742. * skip: skip the test case
  743. """
  744. if on_existing not in ("nothing", "delete", "skip"):
  745. raise RuntimeError(
  746. "The value of 'on_existing' can only be one of, "
  747. "'nothing', 'delete' and 'skip'"
  748. )
  749. if not isinstance(delete, bool):
  750. raise RuntimeError("The value of 'delete' can only be 'True' or 'False'")
  751. def decorator(func):
  752. @functools.wraps(func)
  753. def wrap(cls):
  754. # Let's add the user to the system.
  755. log.debug("Creating system user {!r}".format(username))
  756. create_user = cls.run_function("user.add", [username])
  757. log.debug("Creating system group {!r}".format(group))
  758. create_group = cls.run_function("group.add", [group])
  759. if not create_user:
  760. log.debug("Failed to create system user")
  761. # The user was not created
  762. if on_existing == "skip":
  763. cls.skipTest("Failed to create system user {!r}".format(username))
  764. if on_existing == "delete":
  765. log.debug("Deleting the system user {!r}".format(username))
  766. delete_user = cls.run_function(
  767. "user.delete", [username, True, True]
  768. )
  769. if not delete_user:
  770. cls.skipTest(
  771. "A user named {!r} already existed on the "
  772. "system and re-creating it was not possible".format(
  773. username
  774. )
  775. )
  776. log.debug("Second time creating system user {!r}".format(username))
  777. create_user = cls.run_function("user.add", [username])
  778. if not create_user:
  779. cls.skipTest(
  780. "A user named {!r} already existed, was deleted "
  781. "as requested, but re-creating it was not possible".format(
  782. username
  783. )
  784. )
  785. if not create_group:
  786. log.debug("Failed to create system group")
  787. # The group was not created
  788. if on_existing == "skip":
  789. cls.skipTest("Failed to create system group {!r}".format(group))
  790. if on_existing == "delete":
  791. log.debug("Deleting the system group {!r}".format(group))
  792. delete_group = cls.run_function("group.delete", [group])
  793. if not delete_group:
  794. cls.skipTest(
  795. "A group named {!r} already existed on the "
  796. "system and re-creating it was not possible".format(group)
  797. )
  798. log.debug("Second time creating system group {!r}".format(group))
  799. create_group = cls.run_function("group.add", [group])
  800. if not create_group:
  801. cls.skipTest(
  802. "A group named {!r} already existed, was deleted "
  803. "as requested, but re-creating it was not possible".format(
  804. group
  805. )
  806. )
  807. failure = None
  808. try:
  809. try:
  810. return func(cls, username, group)
  811. except Exception as exc: # pylint: disable=W0703
  812. log.error(
  813. "Running {!r} raised an exception: {}".format(func, exc),
  814. exc_info=True,
  815. )
  816. # Store the original exception details which will be raised
  817. # a little further down the code
  818. failure = sys.exc_info()
  819. finally:
  820. if delete:
  821. delete_user = cls.run_function(
  822. "user.delete", [username, True, True]
  823. )
  824. delete_group = cls.run_function("group.delete", [group])
  825. if not delete_user:
  826. if failure is None:
  827. log.warning(
  828. "Although the actual test-case did not fail, "
  829. "deleting the created system user {!r} "
  830. "afterwards did.".format(username)
  831. )
  832. else:
  833. log.warning(
  834. "The test-case failed and also did the removal"
  835. " of the system user {!r}".format(username)
  836. )
  837. if not delete_group:
  838. if failure is None:
  839. log.warning(
  840. "Although the actual test-case did not fail, "
  841. "deleting the created system group {!r} "
  842. "afterwards did.".format(group)
  843. )
  844. else:
  845. log.warning(
  846. "The test-case failed and also did the removal"
  847. " of the system group {!r}".format(group)
  848. )
  849. if failure is not None:
  850. # If an exception was thrown, raise it
  851. raise failure[1].with_traceback(failure[2])
  852. return wrap
  853. return decorator
  854. class WithTempfile:
  855. def __init__(self, **kwargs):
  856. self.create = kwargs.pop("create", True)
  857. if "dir" not in kwargs:
  858. kwargs["dir"] = RUNTIME_VARS.TMP
  859. if "prefix" not in kwargs:
  860. kwargs["prefix"] = "__salt.test."
  861. self.kwargs = kwargs
  862. def __call__(self, func):
  863. self.func = func
  864. return functools.wraps(func)(
  865. # pylint: disable=unnecessary-lambda
  866. lambda testcase, *args, **kwargs: self.wrap(testcase, *args, **kwargs)
  867. # pylint: enable=unnecessary-lambda
  868. )
  869. def wrap(self, testcase, *args, **kwargs):
  870. name = salt.utils.files.mkstemp(**self.kwargs)
  871. if not self.create:
  872. os.remove(name)
  873. try:
  874. return self.func(testcase, name, *args, **kwargs)
  875. finally:
  876. try:
  877. os.remove(name)
  878. except OSError:
  879. pass
  880. with_tempfile = WithTempfile
  881. class WithTempdir:
  882. def __init__(self, **kwargs):
  883. self.create = kwargs.pop("create", True)
  884. if "dir" not in kwargs:
  885. kwargs["dir"] = RUNTIME_VARS.TMP
  886. self.kwargs = kwargs
  887. def __call__(self, func):
  888. self.func = func
  889. return functools.wraps(func)(
  890. # pylint: disable=unnecessary-lambda
  891. lambda testcase, *args, **kwargs: self.wrap(testcase, *args, **kwargs)
  892. # pylint: enable=unnecessary-lambda
  893. )
  894. def wrap(self, testcase, *args, **kwargs):
  895. tempdir = tempfile.mkdtemp(**self.kwargs)
  896. if not self.create:
  897. os.rmdir(tempdir)
  898. try:
  899. return self.func(testcase, tempdir, *args, **kwargs)
  900. finally:
  901. shutil.rmtree(tempdir, ignore_errors=True)
  902. with_tempdir = WithTempdir
  903. def requires_system_grains(func):
  904. """
  905. Function decorator which loads and passes the system's grains to the test
  906. case.
  907. """
  908. @functools.wraps(func)
  909. def decorator(*args, **kwargs):
  910. if not hasattr(requires_system_grains, "__grains__"):
  911. # Late import
  912. from tests.support.sminion import build_minion_opts
  913. opts = build_minion_opts(minion_id="runtests-internal-sminion")
  914. requires_system_grains.__grains__ = salt.loader.grains(opts)
  915. kwargs["grains"] = requires_system_grains.__grains__
  916. return func(*args, **kwargs)
  917. return decorator
  918. @requires_system_grains
  919. def runs_on(grains=None, **kwargs):
  920. """
  921. Skip the test if grains don't match the values passed into **kwargs
  922. if a kwarg value is a list then skip if the grains don't match any item in the list
  923. """
  924. reason = kwargs.pop("reason", None)
  925. for kw, value in kwargs.items():
  926. if isinstance(value, list):
  927. if not any(str(grains.get(kw)).lower() != str(v).lower() for v in value):
  928. if reason is None:
  929. reason = "This test does not run on {}={}".format(
  930. kw, grains.get(kw)
  931. )
  932. return skip(reason)
  933. else:
  934. if str(grains.get(kw)).lower() != str(value).lower():
  935. if reason is None:
  936. reason = "This test runs on {}={}, not {}".format(
  937. kw, value, grains.get(kw)
  938. )
  939. return skip(reason)
  940. return _id
  941. @requires_system_grains
  942. def not_runs_on(grains=None, **kwargs):
  943. """
  944. Reverse of `runs_on`.
  945. Skip the test if any grains match the values passed into **kwargs
  946. if a kwarg value is a list then skip if the grains match any item in the list
  947. """
  948. reason = kwargs.pop("reason", None)
  949. for kw, value in kwargs.items():
  950. if isinstance(value, list):
  951. if any(str(grains.get(kw)).lower() == str(v).lower() for v in value):
  952. if reason is None:
  953. reason = "This test does not run on {}={}".format(
  954. kw, grains.get(kw)
  955. )
  956. return skip(reason)
  957. else:
  958. if str(grains.get(kw)).lower() == str(value).lower():
  959. if reason is None:
  960. reason = "This test does not run on {}={}, got {}".format(
  961. kw, value, grains.get(kw)
  962. )
  963. return skip(reason)
  964. return _id
  965. def _check_required_sminion_attributes(sminion_attr, *required_items):
  966. """
  967. :param sminion_attr: The name of the sminion attribute to check, such as 'functions' or 'states'
  968. :param required_items: The items that must be part of the designated sminion attribute for the decorated test
  969. :return The packages that are not available
  970. """
  971. # Late import
  972. from tests.support.sminion import create_sminion
  973. required_salt_items = set(required_items)
  974. sminion = create_sminion(minion_id="runtests-internal-sminion")
  975. available_items = list(getattr(sminion, sminion_attr))
  976. not_available_items = set()
  977. name = "__not_available_{items}s__".format(items=sminion_attr)
  978. if not hasattr(sminion, name):
  979. setattr(sminion, name, set())
  980. cached_not_available_items = getattr(sminion, name)
  981. for not_available_item in cached_not_available_items:
  982. if not_available_item in required_salt_items:
  983. not_available_items.add(not_available_item)
  984. required_salt_items.remove(not_available_item)
  985. for required_item_name in required_salt_items:
  986. search_name = required_item_name
  987. if "." not in search_name:
  988. search_name += ".*"
  989. if not fnmatch.filter(available_items, search_name):
  990. not_available_items.add(required_item_name)
  991. cached_not_available_items.add(required_item_name)
  992. return not_available_items
  993. def requires_salt_states(*names):
  994. """
  995. Makes sure the passed salt state is available. Skips the test if not
  996. .. versionadded:: 3000
  997. """
  998. not_available = _check_required_sminion_attributes("states", *names)
  999. if not_available:
  1000. return skip("Unavailable salt states: {}".format(*not_available))
  1001. return _id
  1002. def requires_salt_modules(*names):
  1003. """
  1004. Makes sure the passed salt module is available. Skips the test if not
  1005. .. versionadded:: 0.5.2
  1006. """
  1007. not_available = _check_required_sminion_attributes("functions", *names)
  1008. if not_available:
  1009. return skip("Unavailable salt modules: {}".format(*not_available))
  1010. return _id
  1011. def skip_if_binaries_missing(*binaries, **kwargs):
  1012. import salt.utils.path
  1013. if len(binaries) == 1:
  1014. if isinstance(binaries[0], (list, tuple, set, frozenset)):
  1015. binaries = binaries[0]
  1016. check_all = kwargs.pop("check_all", False)
  1017. message = kwargs.pop("message", None)
  1018. if kwargs:
  1019. raise RuntimeError(
  1020. "The only supported keyword argument is 'check_all' and "
  1021. "'message'. Invalid keyword arguments: {}".format(", ".join(kwargs.keys()))
  1022. )
  1023. if check_all:
  1024. for binary in binaries:
  1025. if salt.utils.path.which(binary) is None:
  1026. return skip(
  1027. "{}The {!r} binary was not found".format(
  1028. message and "{}. ".format(message) or "", binary
  1029. )
  1030. )
  1031. elif salt.utils.path.which_bin(binaries) is None:
  1032. return skip(
  1033. "{}None of the following binaries was found: {}".format(
  1034. message and "{}. ".format(message) or "", ", ".join(binaries)
  1035. )
  1036. )
  1037. return _id
  1038. def skip_if_not_root(func):
  1039. # Late import
  1040. from tests.support.runtests import RUNTIME_VARS
  1041. if RUNTIME_VARS.PYTEST_SESSION:
  1042. setattr(func, "__skip_if_not_root__", True)
  1043. if not sys.platform.startswith("win"):
  1044. if os.getuid() != 0:
  1045. func.__unittest_skip__ = True
  1046. func.__unittest_skip_why__ = (
  1047. "You must be logged in as root to run this test"
  1048. )
  1049. else:
  1050. current_user = salt.utils.win_functions.get_current_user()
  1051. if current_user != "SYSTEM":
  1052. if not salt.utils.win_functions.is_admin(current_user):
  1053. func.__unittest_skip__ = True
  1054. func.__unittest_skip_why__ = (
  1055. "You must be logged in as an Administrator to run this test"
  1056. )
  1057. return func
  1058. def repeat(caller=None, condition=True, times=5):
  1059. """
  1060. Repeat a test X amount of times until the first failure.
  1061. .. code-block:: python
  1062. class MyTestCase(TestCase):
  1063. @repeat
  1064. def test_sometimes_works(self):
  1065. pass
  1066. """
  1067. if caller is None:
  1068. return functools.partial(repeat, condition=condition, times=times)
  1069. if isinstance(condition, bool) and condition is False:
  1070. # Don't even decorate
  1071. return caller
  1072. elif callable(condition):
  1073. if condition() is False:
  1074. # Don't even decorate
  1075. return caller
  1076. if inspect.isclass(caller):
  1077. attrs = [n for n in dir(caller) if n.startswith("test_")]
  1078. for attrname in attrs:
  1079. try:
  1080. function = getattr(caller, attrname)
  1081. if not inspect.isfunction(function) and not inspect.ismethod(function):
  1082. continue
  1083. setattr(
  1084. caller,
  1085. attrname,
  1086. repeat(caller=function, condition=condition, times=times),
  1087. )
  1088. except Exception as exc: # pylint: disable=broad-except
  1089. log.exception(exc)
  1090. continue
  1091. return caller
  1092. @functools.wraps(caller)
  1093. def wrap(cls):
  1094. result = None
  1095. for attempt in range(1, times + 1):
  1096. log.info("%s test run %d of %s times", cls, attempt, times)
  1097. caller(cls)
  1098. return cls
  1099. return wrap
  1100. def http_basic_auth(login_cb=lambda username, password: False):
  1101. """
  1102. A crude decorator to force a handler to request HTTP Basic Authentication
  1103. Example usage:
  1104. .. code-block:: python
  1105. @http_basic_auth(lambda u, p: u == 'foo' and p == 'bar')
  1106. class AuthenticatedHandler(salt.ext.tornado.web.RequestHandler):
  1107. pass
  1108. """
  1109. def wrapper(handler_class):
  1110. def wrap_execute(handler_execute):
  1111. def check_auth(handler, kwargs):
  1112. auth = handler.request.headers.get("Authorization")
  1113. if auth is None or not auth.startswith("Basic "):
  1114. # No username/password entered yet, we need to return a 401
  1115. # and set the WWW-Authenticate header to request login.
  1116. handler.set_status(401)
  1117. handler.set_header("WWW-Authenticate", "Basic realm=Restricted")
  1118. else:
  1119. # Strip the 'Basic ' from the beginning of the auth header
  1120. # leaving the base64-encoded secret
  1121. username, password = base64.b64decode(auth[6:]).split(":", 1)
  1122. if login_cb(username, password):
  1123. # Authentication successful
  1124. return
  1125. else:
  1126. # Authentication failed
  1127. handler.set_status(403)
  1128. handler._transforms = []
  1129. handler.finish()
  1130. def _execute(self, transforms, *args, **kwargs):
  1131. check_auth(self, kwargs)
  1132. return handler_execute(self, transforms, *args, **kwargs)
  1133. return _execute
  1134. handler_class._execute = wrap_execute(handler_class._execute)
  1135. return handler_class
  1136. return wrapper
  1137. def generate_random_name(prefix, size=6):
  1138. """
  1139. Generates a random name by combining the provided prefix with a randomly generated
  1140. ascii string.
  1141. .. versionadded:: 2018.3.0
  1142. prefix
  1143. The string to prefix onto the randomly generated ascii string.
  1144. size
  1145. The number of characters to generate. Default: 6.
  1146. """
  1147. salt.utils.versions.warn_until_date(
  1148. "20220101",
  1149. "Please replace your call 'generate_random_name({0})' with 'random_string({0}, lowercase=False)' as "
  1150. "'generate_random_name' will be removed after {{date}}".format(prefix),
  1151. )
  1152. return random_string(prefix, size=size, lowercase=False)
  1153. def random_string(prefix, size=6, uppercase=True, lowercase=True, digits=True):
  1154. """
  1155. Generates a random string.
  1156. ..versionadded: 3001
  1157. Args:
  1158. prefix(str): The prefix for the random string
  1159. size(int): The size of the random string
  1160. uppercase(bool): If true, include uppercased ascii chars in choice sample
  1161. lowercase(bool): If true, include lowercased ascii chars in choice sample
  1162. digits(bool): If true, include digits in choice sample
  1163. Returns:
  1164. str: The random string
  1165. """
  1166. if not any([uppercase, lowercase, digits]):
  1167. raise RuntimeError(
  1168. "At least one of 'uppercase', 'lowercase' or 'digits' needs to be true"
  1169. )
  1170. choices = []
  1171. if uppercase:
  1172. choices.extend(string.ascii_uppercase)
  1173. if lowercase:
  1174. choices.extend(string.ascii_lowercase)
  1175. if digits:
  1176. choices.extend(string.digits)
  1177. return prefix + "".join(random.choice(choices) for _ in range(size))
  1178. class Webserver:
  1179. """
  1180. Starts a tornado webserver on 127.0.0.1 on a random available port
  1181. USAGE:
  1182. .. code-block:: python
  1183. from tests.support.helpers import Webserver
  1184. webserver = Webserver('/path/to/web/root')
  1185. webserver.start()
  1186. webserver.stop()
  1187. """
  1188. def __init__(self, root=None, port=None, wait=5, handler=None, ssl_opts=None):
  1189. """
  1190. root
  1191. Root directory of webserver. If not passed, it will default to the
  1192. location of the base environment of the integration suite's file
  1193. roots (tests/integration/files/file/base/)
  1194. port
  1195. Port on which to listen. If not passed, a random one will be chosen
  1196. at the time the start() function is invoked.
  1197. wait : 5
  1198. Number of seconds to wait for the socket to be open before raising
  1199. an exception
  1200. handler
  1201. Can be used to use a subclass of tornado.web.StaticFileHandler,
  1202. such as when enforcing authentication with the http_basic_auth
  1203. decorator.
  1204. """
  1205. if port is not None and not isinstance(port, int):
  1206. raise ValueError("port must be an integer")
  1207. if root is None:
  1208. root = RUNTIME_VARS.BASE_FILES
  1209. try:
  1210. self.root = os.path.realpath(root)
  1211. except AttributeError:
  1212. raise ValueError("root must be a string")
  1213. self.port = port
  1214. self.wait = wait
  1215. self.handler = (
  1216. handler if handler is not None else salt.ext.tornado.web.StaticFileHandler
  1217. )
  1218. self.web_root = None
  1219. self.ssl_opts = ssl_opts
  1220. def target(self):
  1221. """
  1222. Threading target which stands up the tornado application
  1223. """
  1224. self.ioloop = salt.ext.tornado.ioloop.IOLoop()
  1225. self.ioloop.make_current()
  1226. if self.handler == salt.ext.tornado.web.StaticFileHandler:
  1227. self.application = salt.ext.tornado.web.Application(
  1228. [(r"/(.*)", self.handler, {"path": self.root})]
  1229. )
  1230. else:
  1231. self.application = salt.ext.tornado.web.Application(
  1232. [(r"/(.*)", self.handler)]
  1233. )
  1234. self.application.listen(self.port, ssl_options=self.ssl_opts)
  1235. self.ioloop.start()
  1236. @property
  1237. def listening(self):
  1238. if self.port is None:
  1239. return False
  1240. sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  1241. return sock.connect_ex(("127.0.0.1", self.port)) == 0
  1242. def url(self, path):
  1243. """
  1244. Convenience function which, given a file path, will return a URL that
  1245. points to that path. If the path is relative, it will just be appended
  1246. to self.web_root.
  1247. """
  1248. if self.web_root is None:
  1249. raise RuntimeError("Webserver instance has not been started")
  1250. err_msg = (
  1251. "invalid path, must be either a relative path or a path "
  1252. "within {}".format(self.root)
  1253. )
  1254. try:
  1255. relpath = (
  1256. path if not os.path.isabs(path) else os.path.relpath(path, self.root)
  1257. )
  1258. if relpath.startswith(".." + os.sep):
  1259. raise ValueError(err_msg)
  1260. return "/".join((self.web_root, relpath))
  1261. except AttributeError:
  1262. raise ValueError(err_msg)
  1263. def start(self):
  1264. """
  1265. Starts the webserver
  1266. """
  1267. if self.port is None:
  1268. self.port = get_unused_localhost_port()
  1269. self.web_root = "http{}://127.0.0.1:{}".format(
  1270. "s" if self.ssl_opts else "", self.port
  1271. )
  1272. self.server_thread = threading.Thread(target=self.target)
  1273. self.server_thread.daemon = True
  1274. self.server_thread.start()
  1275. for idx in range(self.wait + 1):
  1276. if self.listening:
  1277. break
  1278. if idx != self.wait:
  1279. time.sleep(1)
  1280. else:
  1281. raise Exception(
  1282. "Failed to start tornado webserver on 127.0.0.1:{} within "
  1283. "{} seconds".format(self.port, self.wait)
  1284. )
  1285. def stop(self):
  1286. """
  1287. Stops the webserver
  1288. """
  1289. self.ioloop.add_callback(self.ioloop.stop)
  1290. self.server_thread.join()
  1291. class SaveRequestsPostHandler(salt.ext.tornado.web.RequestHandler):
  1292. """
  1293. Save all requests sent to the server.
  1294. """
  1295. received_requests = []
  1296. def post(self, *args): # pylint: disable=arguments-differ
  1297. """
  1298. Handle the post
  1299. """
  1300. self.received_requests.append(self.request)
  1301. def data_received(self): # pylint: disable=arguments-differ
  1302. """
  1303. Streaming not used for testing
  1304. """
  1305. raise NotImplementedError()
  1306. class MirrorPostHandler(salt.ext.tornado.web.RequestHandler):
  1307. """
  1308. Mirror a POST body back to the client
  1309. """
  1310. def post(self, *args): # pylint: disable=arguments-differ
  1311. """
  1312. Handle the post
  1313. """
  1314. body = self.request.body
  1315. log.debug("Incoming body: %s Incoming args: %s", body, args)
  1316. self.write(body)
  1317. def data_received(self): # pylint: disable=arguments-differ
  1318. """
  1319. Streaming not used for testing
  1320. """
  1321. raise NotImplementedError()
  1322. def dedent(text, linesep=os.linesep):
  1323. """
  1324. A wrapper around textwrap.dedent that also sets line endings.
  1325. """
  1326. linesep = salt.utils.stringutils.to_unicode(linesep)
  1327. unicode_text = textwrap.dedent(salt.utils.stringutils.to_unicode(text))
  1328. clean_text = linesep.join(unicode_text.splitlines())
  1329. if unicode_text.endswith("\n"):
  1330. clean_text += linesep
  1331. if not isinstance(text, str):
  1332. return salt.utils.stringutils.to_bytes(clean_text)
  1333. return clean_text
  1334. class PatchedEnviron:
  1335. def __init__(self, **kwargs):
  1336. self.cleanup_keys = kwargs.pop("__cleanup__", ())
  1337. self.kwargs = kwargs
  1338. self.original_environ = None
  1339. def __enter__(self):
  1340. self.original_environ = os.environ.copy()
  1341. for key in self.cleanup_keys:
  1342. os.environ.pop(key, None)
  1343. # Make sure there are no unicode characters in the self.kwargs if we're
  1344. # on Python 2. These are being added to `os.environ` and causing
  1345. # problems
  1346. if sys.version_info < (3,):
  1347. kwargs = self.kwargs.copy()
  1348. clean_kwargs = {}
  1349. for k in self.kwargs:
  1350. key = k
  1351. if isinstance(key, str):
  1352. key = key.encode("utf-8")
  1353. if isinstance(self.kwargs[k], str):
  1354. kwargs[k] = kwargs[k].encode("utf-8")
  1355. clean_kwargs[key] = kwargs[k]
  1356. self.kwargs = clean_kwargs
  1357. os.environ.update(**self.kwargs)
  1358. return self
  1359. def __exit__(self, *args):
  1360. os.environ.clear()
  1361. os.environ.update(self.original_environ)
  1362. patched_environ = PatchedEnviron
  1363. class VirtualEnv:
  1364. def __init__(self, venv_dir=None):
  1365. self.venv_dir = venv_dir or tempfile.mkdtemp(dir=RUNTIME_VARS.TMP)
  1366. if salt.utils.platform.is_windows():
  1367. self.venv_python = os.path.join(self.venv_dir, "Scripts", "python.exe")
  1368. else:
  1369. self.venv_python = os.path.join(self.venv_dir, "bin", "python")
  1370. self.venv_bin_dir = os.path.dirname(self.venv_python)
  1371. def __enter__(self):
  1372. try:
  1373. self._create_virtualenv()
  1374. except subprocess.CalledProcessError:
  1375. raise AssertionError("Failed to create virtualenv")
  1376. return self
  1377. def __exit__(self, *args):
  1378. salt.utils.files.rm_rf(self.venv_dir)
  1379. def install(self, *args, **kwargs):
  1380. return self.run(self.venv_python, "-m", "pip", "install", *args, **kwargs)
  1381. def run(self, *args, **kwargs):
  1382. check = kwargs.pop("check", True)
  1383. kwargs.setdefault("cwd", self.venv_dir)
  1384. kwargs.setdefault("stdout", subprocess.PIPE)
  1385. kwargs.setdefault("stderr", subprocess.PIPE)
  1386. kwargs.setdefault("universal_newlines", True)
  1387. proc = subprocess.run(args, check=False, **kwargs)
  1388. ret = ProcessResult(
  1389. exitcode=proc.returncode,
  1390. stdout=proc.stdout,
  1391. stderr=proc.stderr,
  1392. cmdline=proc.args,
  1393. )
  1394. log.debug(ret)
  1395. if check is True:
  1396. try:
  1397. proc.check_returncode()
  1398. except subprocess.CalledProcessError:
  1399. raise ProcessFailed(
  1400. "Command failed return code check",
  1401. cmdline=proc.args,
  1402. stdout=proc.stdout,
  1403. stderr=proc.stderr,
  1404. exitcode=proc.returncode,
  1405. )
  1406. return ret
  1407. def _get_real_python(self):
  1408. """
  1409. The reason why the virtualenv creation is proxied by this function is mostly
  1410. because under windows, we can't seem to properly create a virtualenv off of
  1411. another virtualenv(we can on linux) and also because, we really don't want to
  1412. test virtualenv creation off of another virtualenv, we want a virtualenv created
  1413. from the original python.
  1414. Also, on windows, we must also point to the virtualenv binary outside the existing
  1415. virtualenv because it will fail otherwise
  1416. """
  1417. try:
  1418. if salt.utils.platform.is_windows():
  1419. return os.path.join(sys.real_prefix, os.path.basename(sys.executable))
  1420. else:
  1421. python_binary_names = [
  1422. "python{}.{}".format(*sys.version_info),
  1423. "python{}".format(*sys.version_info),
  1424. "python",
  1425. ]
  1426. for binary_name in python_binary_names:
  1427. python = os.path.join(sys.real_prefix, "bin", binary_name)
  1428. if os.path.exists(python):
  1429. break
  1430. else:
  1431. raise AssertionError(
  1432. "Couldn't find a python binary name under '{}' matching: {}".format(
  1433. os.path.join(sys.real_prefix, "bin"), python_binary_names
  1434. )
  1435. )
  1436. return python
  1437. except AttributeError:
  1438. return sys.executable
  1439. def _create_virtualenv(self):
  1440. sminion = create_sminion()
  1441. sminion.functions.virtualenv.create(
  1442. self.venv_dir, python=self._get_real_python()
  1443. )
  1444. self.install("-U", "pip")
  1445. # https://github.com/pypa/setuptools/issues?q=is%3Aissue+setuptools+50+
  1446. self.install("-U", "setuptools<50.0.0")
  1447. @contextmanager
  1448. def change_cwd(path):
  1449. """
  1450. Context manager helper to change CWD for a with code block and restore
  1451. it at the end
  1452. """
  1453. old_cwd = os.getcwd()
  1454. try:
  1455. os.chdir(path)
  1456. # Do stuff
  1457. yield
  1458. finally:
  1459. # Restore Old CWD
  1460. os.chdir(old_cwd)
  1461. @functools.lru_cache(maxsize=1)
  1462. def get_virtualenv_binary_path():
  1463. # Under windows we can't seem to properly create a virtualenv off of another
  1464. # virtualenv, we can on linux but we will still point to the virtualenv binary
  1465. # outside the virtualenv running the test suite, if that's the case.
  1466. try:
  1467. real_prefix = sys.real_prefix
  1468. # The above attribute exists, this is a virtualenv
  1469. if salt.utils.platform.is_windows():
  1470. virtualenv_binary = os.path.join(real_prefix, "Scripts", "virtualenv.exe")
  1471. else:
  1472. # We need to remove the virtualenv from PATH or we'll get the virtualenv binary
  1473. # from within the virtualenv, we don't want that
  1474. path = os.environ.get("PATH")
  1475. if path is not None:
  1476. path_items = path.split(os.pathsep)
  1477. for item in path_items[:]:
  1478. if item.startswith(sys.base_prefix):
  1479. path_items.remove(item)
  1480. os.environ["PATH"] = os.pathsep.join(path_items)
  1481. virtualenv_binary = salt.utils.path.which("virtualenv")
  1482. if path is not None:
  1483. # Restore previous environ PATH
  1484. os.environ["PATH"] = path
  1485. if not virtualenv_binary.startswith(real_prefix):
  1486. virtualenv_binary = None
  1487. if virtualenv_binary and not os.path.exists(virtualenv_binary):
  1488. # It doesn't exist?!
  1489. virtualenv_binary = None
  1490. except AttributeError:
  1491. # We're not running inside a virtualenv
  1492. virtualenv_binary = None
  1493. return virtualenv_binary