ya.py 37 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959
  1. # coding: utf-8
  2. import base64
  3. import errno
  4. import re
  5. import sys
  6. import os
  7. import logging
  8. import fnmatch
  9. import json
  10. import time
  11. import traceback
  12. import collections
  13. import signal
  14. import inspect
  15. import warnings
  16. import attr
  17. import faulthandler
  18. import py
  19. import pytest
  20. import six
  21. import _pytest
  22. import _pytest._io
  23. import _pytest.mark
  24. import _pytest.outcomes
  25. import _pytest.skipping
  26. from _pytest.warning_types import PytestUnhandledCoroutineWarning
  27. from yatest_lib import test_splitter
  28. try:
  29. import resource
  30. except ImportError:
  31. resource = None
  32. try:
  33. import library.python.pytest.yatest_tools as tools
  34. except ImportError:
  35. # fallback for pytest script mode
  36. import yatest_tools as tools
  37. try:
  38. from library.python import filelock
  39. except ImportError:
  40. filelock = None
  41. import yatest_lib.tools
  42. import yatest_lib.external as canon
  43. import yatest_lib.ya
  44. from library.python.pytest import context
  45. console_logger = logging.getLogger("console")
  46. yatest_logger = logging.getLogger("ya.test")
  47. _pytest.main.EXIT_NOTESTSCOLLECTED = 0
  48. SHUTDOWN_REQUESTED = False
  49. pytest_config = None
  50. def configure_pdb_on_demand():
  51. import signal
  52. if hasattr(signal, "SIGUSR1"):
  53. def on_signal(*args):
  54. import ipdb
  55. ipdb.set_trace()
  56. signal.signal(signal.SIGUSR1, on_signal)
  57. class CustomImporter(object):
  58. def __init__(self, roots):
  59. self._roots = roots
  60. def find_module(self, fullname, package_path=None):
  61. for path in self._roots:
  62. full_path = self._get_module_path(path, fullname)
  63. if os.path.exists(full_path) and os.path.isdir(full_path) and not os.path.exists(os.path.join(full_path, "__init__.py")):
  64. open(os.path.join(full_path, "__init__.py"), "w").close()
  65. return None
  66. def _get_module_path(self, path, fullname):
  67. return os.path.join(path, *fullname.split('.'))
  68. class YaTestLoggingFileHandler(logging.FileHandler):
  69. pass
  70. class _TokenFilterFormatter(logging.Formatter):
  71. def __init__(self, fmt):
  72. super(_TokenFilterFormatter, self).__init__(fmt)
  73. self._replacements = []
  74. if not self._replacements:
  75. if six.PY2:
  76. for k, v in os.environ.iteritems():
  77. if k.endswith('TOKEN') and v:
  78. self._replacements.append(v)
  79. elif six.PY3:
  80. for k, v in os.environ.items():
  81. if k.endswith('TOKEN') and v:
  82. self._replacements.append(v)
  83. self._replacements = sorted(self._replacements)
  84. def _filter(self, s):
  85. for r in self._replacements:
  86. s = s.replace(r, "[SECRET]")
  87. return s
  88. def format(self, record):
  89. return self._filter(super(_TokenFilterFormatter, self).format(record))
  90. def setup_logging(log_path, level=logging.DEBUG, *other_logs):
  91. logs = [log_path] + list(other_logs)
  92. root_logger = logging.getLogger()
  93. for i in range(len(root_logger.handlers) - 1, -1, -1):
  94. if isinstance(root_logger.handlers[i], YaTestLoggingFileHandler):
  95. root_logger.handlers.pop(i).close()
  96. root_logger.setLevel(level)
  97. for log_file in logs:
  98. file_handler = YaTestLoggingFileHandler(log_file)
  99. log_format = '%(asctime)s - %(levelname)s - %(name)s - %(funcName)s: %(message)s'
  100. file_handler.setFormatter(_TokenFilterFormatter(log_format))
  101. file_handler.setLevel(level)
  102. root_logger.addHandler(file_handler)
  103. def pytest_addoption(parser):
  104. parser.addoption("--build-root", action="store", dest="build_root", default="", help="path to the build root")
  105. parser.addoption("--dep-root", action="append", dest="dep_roots", default=[], help="path to the dep build roots")
  106. parser.addoption("--source-root", action="store", dest="source_root", default="", help="path to the source root")
  107. parser.addoption("--data-root", action="store", dest="data_root", default="", help="path to the arcadia_tests_data root")
  108. parser.addoption("--output-dir", action="store", dest="output_dir", default="", help="path to the test output dir")
  109. parser.addoption("--python-path", action="store", dest="python_path", default="", help="path the canonical python binary")
  110. parser.addoption("--valgrind-path", action="store", dest="valgrind_path", default="", help="path the canonical valgring binary")
  111. parser.addoption("--test-filter", action="append", dest="test_filter", default=None, help="test filter")
  112. parser.addoption("--test-file-filter", action="store", dest="test_file_filter", default=None, help="test file filter")
  113. parser.addoption("--test-param", action="append", dest="test_params", default=None, help="test parameters")
  114. parser.addoption("--test-log-level", action="store", dest="test_log_level", choices=["critical", "error", "warning", "info", "debug"], default="debug", help="test log level")
  115. parser.addoption("--mode", action="store", choices=[yatest_lib.ya.RunMode.List, yatest_lib.ya.RunMode.Run], dest="mode", default=yatest_lib.ya.RunMode.Run, help="testing mode")
  116. parser.addoption("--test-list-file", action="store", dest="test_list_file")
  117. parser.addoption("--modulo", default=1, type=int)
  118. parser.addoption("--modulo-index", default=0, type=int)
  119. parser.addoption("--partition-mode", default='SEQUENTIAL', help="Split tests according to partitoin mode")
  120. parser.addoption("--split-by-tests", action='store_true', help="Split test execution by tests instead of suites", default=False)
  121. parser.addoption("--project-path", action="store", default="", help="path to CMakeList where test is declared")
  122. parser.addoption("--build-type", action="store", default="", help="build type")
  123. parser.addoption("--flags", action="append", dest="flags", default=[], help="build flags (-D)")
  124. parser.addoption("--sanitize", action="store", default="", help="sanitize mode")
  125. parser.addoption("--test-stderr", action="store_true", default=False, help="test stderr")
  126. parser.addoption("--test-debug", action="store_true", default=False, help="test debug mode")
  127. parser.addoption("--root-dir", action="store", default=None)
  128. parser.addoption("--ya-trace", action="store", dest="ya_trace_path", default=None, help="path to ya trace report")
  129. parser.addoption("--ya-version", action="store", dest="ya_version", default=0, type=int, help="allows to be compatible with ya and the new changes in ya-dev")
  130. parser.addoption(
  131. "--test-suffix", action="store", dest="test_suffix", default=None, help="add suffix to every test name"
  132. )
  133. parser.addoption("--gdb-path", action="store", dest="gdb_path", default="", help="path the canonical gdb binary")
  134. parser.addoption("--collect-cores", action="store_true", dest="collect_cores", default=False, help="allows core dump file recovering during test")
  135. parser.addoption("--sanitizer-extra-checks", action="store_true", dest="sanitizer_extra_checks", default=False, help="enables extra checks for tests built with sanitizers")
  136. parser.addoption("--report-deselected", action="store_true", dest="report_deselected", default=False, help="report deselected tests to the trace file")
  137. parser.addoption("--pdb-on-sigusr1", action="store_true", default=False, help="setup pdb.set_trace on SIGUSR1")
  138. parser.addoption("--test-tool-bin", help="Path to test_tool")
  139. parser.addoption("--test-list-path", dest="test_list_path", action="store", help="path to test list", default="")
  140. def from_ya_test():
  141. return "YA_TEST_RUNNER" in os.environ
  142. def pytest_configure(config):
  143. global pytest_config
  144. pytest_config = config
  145. config.option.continue_on_collection_errors = True
  146. config.addinivalue_line("markers", "ya:external")
  147. config.from_ya_test = from_ya_test()
  148. config.test_logs = collections.defaultdict(dict)
  149. config.test_metrics = {}
  150. config.suite_metrics = {}
  151. config.configure_timestamp = time.time()
  152. context = {
  153. "project_path": config.option.project_path,
  154. "test_stderr": config.option.test_stderr,
  155. "test_debug": config.option.test_debug,
  156. "build_type": config.option.build_type,
  157. "test_traceback": config.option.tbstyle,
  158. "flags": config.option.flags,
  159. "sanitize": config.option.sanitize,
  160. }
  161. if config.option.collectonly:
  162. config.option.mode = yatest_lib.ya.RunMode.List
  163. config.ya = yatest_lib.ya.Ya(
  164. config.option.mode,
  165. config.option.source_root,
  166. config.option.build_root,
  167. config.option.dep_roots,
  168. config.option.output_dir,
  169. config.option.test_params,
  170. context,
  171. config.option.python_path,
  172. config.option.valgrind_path,
  173. config.option.gdb_path,
  174. config.option.data_root,
  175. )
  176. config.option.test_log_level = {
  177. "critical": logging.CRITICAL,
  178. "error": logging.ERROR,
  179. "warning": logging.WARN,
  180. "info": logging.INFO,
  181. "debug": logging.DEBUG,
  182. }[config.option.test_log_level]
  183. if not config.option.collectonly:
  184. setup_logging(os.path.join(config.ya.output_dir, "run.log"), config.option.test_log_level)
  185. config.current_item_nodeid = None
  186. config.current_test_name = None
  187. config.test_cores_count = 0
  188. config.collect_cores = config.option.collect_cores
  189. config.sanitizer_extra_checks = config.option.sanitizer_extra_checks
  190. try:
  191. config.test_tool_bin = config.option.test_tool_bin
  192. except AttributeError:
  193. logging.info("test_tool_bin not specified")
  194. if config.sanitizer_extra_checks:
  195. for envvar in ['LSAN_OPTIONS', 'ASAN_OPTIONS']:
  196. if envvar in os.environ:
  197. os.environ.pop(envvar)
  198. if envvar + '_ORIGINAL' in os.environ:
  199. os.environ[envvar] = os.environ[envvar + '_ORIGINAL']
  200. extra_sys_path = []
  201. # Arcadia paths from the test DEPENDS section of ya.make
  202. extra_sys_path.append(os.path.join(config.option.source_root, config.option.project_path))
  203. # Build root is required for correct import of protobufs, because imports are related to the root
  204. # (like import devtools.dummy_arcadia.protos.lib.my_proto_pb2)
  205. extra_sys_path.append(config.option.build_root)
  206. for path in config.option.dep_roots:
  207. if os.path.isabs(path):
  208. extra_sys_path.append(path)
  209. else:
  210. extra_sys_path.append(os.path.join(config.option.source_root, path))
  211. sys_path_set = set(sys.path)
  212. for path in extra_sys_path:
  213. if path not in sys_path_set:
  214. sys.path.append(path)
  215. sys_path_set.add(path)
  216. os.environ["PYTHONPATH"] = os.pathsep.join(sys.path)
  217. if not config.option.collectonly:
  218. if config.option.ya_trace_path:
  219. config.ya_trace_reporter = TraceReportGenerator(config.option.ya_trace_path)
  220. else:
  221. config.ya_trace_reporter = DryTraceReportGenerator(config.option.ya_trace_path)
  222. config.ya_version = config.option.ya_version
  223. sys.meta_path.append(CustomImporter([config.option.build_root] + [os.path.join(config.option.build_root, dep) for dep in config.option.dep_roots]))
  224. if config.option.pdb_on_sigusr1:
  225. configure_pdb_on_demand()
  226. # Dump python backtrace in case of any errors
  227. faulthandler.enable()
  228. if hasattr(signal, "SIGQUIT"):
  229. # SIGQUIT is used by test_tool to teardown tests which overruns timeout
  230. faulthandler.register(signal.SIGQUIT, chain=True)
  231. if hasattr(signal, "SIGUSR2"):
  232. signal.signal(signal.SIGUSR2, _graceful_shutdown)
  233. session_should_exit = False
  234. def _graceful_shutdown_on_log(should_exit):
  235. if should_exit:
  236. pytest.exit("Graceful shutdown requested")
  237. def pytest_runtest_logreport(report):
  238. _graceful_shutdown_on_log(session_should_exit)
  239. def pytest_runtest_logstart(nodeid, location):
  240. _graceful_shutdown_on_log(session_should_exit)
  241. def pytest_runtest_logfinish(nodeid, location):
  242. _graceful_shutdown_on_log(session_should_exit)
  243. def _graceful_shutdown(*args):
  244. global session_should_exit
  245. session_should_exit = True
  246. try:
  247. import library.python.coverage
  248. library.python.coverage.stop_coverage_tracing()
  249. except ImportError:
  250. pass
  251. traceback.print_stack(file=sys.stderr)
  252. capman = pytest_config.pluginmanager.getplugin("capturemanager")
  253. capman.suspend(in_=True)
  254. _graceful_shutdown_on_log(not capman.is_globally_capturing())
  255. def _get_rusage():
  256. return resource and resource.getrusage(resource.RUSAGE_SELF)
  257. def _collect_test_rusage(item):
  258. if resource and hasattr(item, "rusage"):
  259. finish_rusage = _get_rusage()
  260. ya_inst = pytest_config.ya
  261. def add_metric(attr_name, metric_name=None, modifier=None):
  262. if not metric_name:
  263. metric_name = attr_name
  264. if not modifier:
  265. modifier = lambda x: x
  266. if hasattr(item.rusage, attr_name):
  267. ya_inst.set_metric_value(metric_name, modifier(getattr(finish_rusage, attr_name) - getattr(item.rusage, attr_name)))
  268. for args in [
  269. ("ru_maxrss", "ru_rss", lambda x: x*1024), # to be the same as in util/system/rusage.cpp
  270. ("ru_utime",),
  271. ("ru_stime",),
  272. ("ru_ixrss", None, lambda x: x*1024),
  273. ("ru_idrss", None, lambda x: x*1024),
  274. ("ru_isrss", None, lambda x: x*1024),
  275. ("ru_majflt", "ru_major_pagefaults"),
  276. ("ru_minflt", "ru_minor_pagefaults"),
  277. ("ru_nswap",),
  278. ("ru_inblock",),
  279. ("ru_oublock",),
  280. ("ru_msgsnd",),
  281. ("ru_msgrcv",),
  282. ("ru_nsignals",),
  283. ("ru_nvcsw",),
  284. ("ru_nivcsw",),
  285. ]:
  286. add_metric(*args)
  287. def _get_item_tags(item):
  288. tags = []
  289. for key, value in item.keywords.items():
  290. if key == 'pytestmark' and isinstance(value, list):
  291. for mark in value:
  292. tags.append(mark.name)
  293. elif isinstance(value, _pytest.mark.MarkDecorator):
  294. tags.append(key)
  295. return tags
  296. def pytest_runtest_setup(item):
  297. item.rusage = _get_rusage()
  298. pytest_config.test_cores_count = 0
  299. pytest_config.current_item_nodeid = item.nodeid
  300. class_name, test_name = tools.split_node_id(item.nodeid)
  301. test_log_path = tools.get_test_log_file_path(pytest_config.ya.output_dir, class_name, test_name)
  302. setup_logging(
  303. os.path.join(pytest_config.ya.output_dir, "run.log"),
  304. pytest_config.option.test_log_level,
  305. test_log_path
  306. )
  307. pytest_config.test_logs[item.nodeid]['log'] = test_log_path
  308. pytest_config.test_logs[item.nodeid]['logsdir'] = pytest_config.ya.output_dir
  309. pytest_config.current_test_log_path = test_log_path
  310. pytest_config.current_test_name = "{}::{}".format(class_name, test_name)
  311. separator = "#" * 100
  312. yatest_logger.info(separator)
  313. yatest_logger.info(test_name)
  314. yatest_logger.info(separator)
  315. yatest_logger.info("Test setup")
  316. test_item = CrashedTestItem(item.nodeid, pytest_config.option.test_suffix)
  317. pytest_config.ya_trace_reporter.on_start_test_class(test_item)
  318. pytest_config.ya_trace_reporter.on_start_test_case(test_item)
  319. def pytest_runtest_teardown(item, nextitem):
  320. yatest_logger.info("Test teardown")
  321. def pytest_runtest_call(item):
  322. class_name, test_name = tools.split_node_id(item.nodeid)
  323. yatest_logger.info("Test call (class_name: %s, test_name: %s)", class_name, test_name)
  324. def pytest_deselected(items):
  325. config = pytest_config
  326. if config.option.report_deselected:
  327. for item in items:
  328. deselected_item = DeselectedTestItem(item.nodeid, config.option.test_suffix)
  329. config.ya_trace_reporter.on_start_test_class(deselected_item)
  330. config.ya_trace_reporter.on_finish_test_case(deselected_item)
  331. config.ya_trace_reporter.on_finish_test_class(deselected_item)
  332. @pytest.mark.trylast
  333. def pytest_collection_modifyitems(items, config):
  334. def filter_items(filters):
  335. filtered_items = []
  336. deselected_items = []
  337. for item in items:
  338. canonical_node_id = str(CustomTestItem(item.nodeid, pytest_config.option.test_suffix))
  339. matched = False
  340. for flt in filters:
  341. if "::" not in flt and "*" not in flt:
  342. flt += "*" # add support for filtering by module name
  343. if canonical_node_id.endswith(flt) or fnmatch.fnmatch(tools.escape_for_fnmatch(canonical_node_id), tools.escape_for_fnmatch(flt)):
  344. matched = True
  345. if matched:
  346. filtered_items.append(item)
  347. else:
  348. deselected_items.append(item)
  349. config.hook.pytest_deselected(items=deselected_items)
  350. items[:] = filtered_items
  351. def filter_by_full_name(filters):
  352. filter_set = {flt for flt in filters}
  353. filtered_items = []
  354. deselected_items = []
  355. for item in items:
  356. if item.nodeid in filter_set:
  357. filtered_items.append(item)
  358. else:
  359. deselected_items.append(item)
  360. config.hook.pytest_deselected(items=deselected_items)
  361. items[:] = filtered_items
  362. # XXX - check to be removed when tests for peerdirs don't run
  363. for item in items:
  364. if not item.nodeid:
  365. item._nodeid = os.path.basename(item.location[0])
  366. if os.path.exists(config.option.test_list_path):
  367. with open(config.option.test_list_path, 'r') as afile:
  368. chunks = json.load(afile)
  369. filters = chunks[config.option.modulo_index]
  370. filter_by_full_name(filters)
  371. else:
  372. if config.option.test_filter:
  373. filter_items(config.option.test_filter)
  374. partition_mode = config.option.partition_mode
  375. modulo = config.option.modulo
  376. if modulo > 1:
  377. items[:] = sorted(items, key=lambda item: item.nodeid)
  378. modulo_index = config.option.modulo_index
  379. split_by_tests = config.option.split_by_tests
  380. items_by_classes = {}
  381. res = []
  382. for item in items:
  383. if item.nodeid.count("::") == 2 and not split_by_tests:
  384. class_name = item.nodeid.rsplit("::", 1)[0]
  385. if class_name not in items_by_classes:
  386. items_by_classes[class_name] = []
  387. res.append(items_by_classes[class_name])
  388. items_by_classes[class_name].append(item)
  389. else:
  390. res.append([item])
  391. chunk_items = test_splitter.get_splitted_tests(res, modulo, modulo_index, partition_mode, is_sorted=True)
  392. items[:] = []
  393. for item in chunk_items:
  394. items.extend(item)
  395. yatest_logger.info("Modulo %s tests are: %s", modulo_index, chunk_items)
  396. if config.option.mode == yatest_lib.ya.RunMode.Run:
  397. for item in items:
  398. test_item = NotLaunchedTestItem(item.nodeid, config.option.test_suffix)
  399. config.ya_trace_reporter.on_start_test_class(test_item)
  400. config.ya_trace_reporter.on_finish_test_case(test_item)
  401. config.ya_trace_reporter.on_finish_test_class(test_item)
  402. elif config.option.mode == yatest_lib.ya.RunMode.List:
  403. tests = []
  404. for item in items:
  405. item = CustomTestItem(item.nodeid, pytest_config.option.test_suffix, item.keywords)
  406. record = {
  407. "class": item.class_name,
  408. "test": item.test_name,
  409. "tags": _get_item_tags(item),
  410. }
  411. tests.append(record)
  412. if config.option.test_list_file:
  413. with open(config.option.test_list_file, 'w') as afile:
  414. json.dump(tests, afile)
  415. # TODO prettyboy remove after test_tool release - currently it's required for backward compatibility
  416. sys.stderr.write(json.dumps(tests))
  417. def pytest_collectreport(report):
  418. if not report.passed:
  419. if hasattr(pytest_config, 'ya_trace_reporter'):
  420. test_item = TestItem(report, None, pytest_config.option.test_suffix)
  421. pytest_config.ya_trace_reporter.on_error(test_item)
  422. else:
  423. sys.stderr.write(yatest_lib.tools.to_utf8(report.longrepr))
  424. @pytest.mark.tryfirst
  425. def pytest_pyfunc_call(pyfuncitem):
  426. testfunction = pyfuncitem.obj
  427. iscoroutinefunction = getattr(inspect, "iscoroutinefunction", None)
  428. if iscoroutinefunction is not None and iscoroutinefunction(testfunction):
  429. msg = "Coroutine functions are not natively supported and have been skipped.\n"
  430. msg += "You need to install a suitable plugin for your async framework, for example:\n"
  431. msg += " - pytest-asyncio\n"
  432. msg += " - pytest-trio\n"
  433. msg += " - pytest-tornasync"
  434. warnings.warn(PytestUnhandledCoroutineWarning(msg.format(pyfuncitem.nodeid)))
  435. _pytest.outcomes.skip(msg="coroutine function and no async plugin installed (see warnings)")
  436. funcargs = pyfuncitem.funcargs
  437. testargs = {arg: funcargs[arg] for arg in pyfuncitem._fixtureinfo.argnames}
  438. pyfuncitem.retval = testfunction(**testargs)
  439. return True
  440. @pytest.hookimpl(hookwrapper=True)
  441. def pytest_runtest_makereport(item, call):
  442. def logreport(report, result, call):
  443. test_item = TestItem(report, result, pytest_config.option.test_suffix)
  444. if not pytest_config.suite_metrics and context.Ctx.get("YA_PYTEST_START_TIMESTAMP"):
  445. pytest_config.suite_metrics["pytest_startup_duration"] = call.start - context.Ctx["YA_PYTEST_START_TIMESTAMP"]
  446. pytest_config.ya_trace_reporter.dump_suite_metrics()
  447. pytest_config.ya_trace_reporter.on_log_report(test_item)
  448. if report.outcome == "failed":
  449. yatest_logger.error(report.longrepr)
  450. if report.when == "call":
  451. _collect_test_rusage(item)
  452. pytest_config.ya_trace_reporter.on_finish_test_case(test_item)
  453. elif report.when == "setup":
  454. pytest_config.ya_trace_reporter.on_start_test_class(test_item)
  455. if report.outcome != "passed":
  456. pytest_config.ya_trace_reporter.on_start_test_case(test_item)
  457. pytest_config.ya_trace_reporter.on_finish_test_case(test_item)
  458. else:
  459. pytest_config.ya_trace_reporter.on_start_test_case(test_item)
  460. elif report.when == "teardown":
  461. if report.outcome == "failed":
  462. pytest_config.ya_trace_reporter.on_start_test_case(test_item)
  463. pytest_config.ya_trace_reporter.on_finish_test_case(test_item)
  464. else:
  465. pytest_config.ya_trace_reporter.on_finish_test_case(test_item, duration_only=True)
  466. pytest_config.ya_trace_reporter.on_finish_test_class(test_item)
  467. outcome = yield
  468. rep = outcome.get_result()
  469. result = None
  470. if hasattr(item, 'retval') and item.retval is not None:
  471. result = item.retval
  472. if not pytest_config.from_ya_test:
  473. ti = TestItem(rep, result, pytest_config.option.test_suffix)
  474. tr = pytest_config.pluginmanager.getplugin('terminalreporter')
  475. tr.write_line("{} - Validating canonical data is not supported when running standalone binary".format(ti), yellow=True, bold=True)
  476. logreport(rep, result, call)
  477. def pytest_make_parametrize_id(config, val, argname):
  478. # Avoid <, > symbols in canondata file names
  479. if inspect.isfunction(val) and val.__name__ == "<lambda>":
  480. return str(argname)
  481. return None
  482. def get_formatted_error(report):
  483. if isinstance(report.longrepr, tuple):
  484. text = ""
  485. for entry in report.longrepr:
  486. text += colorize(entry)
  487. else:
  488. text = colorize(report.longrepr)
  489. text = yatest_lib.tools.to_utf8(text)
  490. return text
  491. def colorize(longrepr):
  492. # use default pytest colorization
  493. if pytest_config.option.tbstyle != "short":
  494. io = py.io.TextIO()
  495. if six.PY2:
  496. writer = py.io.TerminalWriter(file=io)
  497. else:
  498. writer = _pytest._io.TerminalWriter(file=io)
  499. # enable colorization
  500. writer.hasmarkup = True
  501. if hasattr(longrepr, 'reprtraceback') and hasattr(longrepr.reprtraceback, 'toterminal'):
  502. longrepr.reprtraceback.toterminal(writer)
  503. return io.getvalue().strip()
  504. return yatest_lib.tools.to_utf8(longrepr)
  505. text = yatest_lib.tools.to_utf8(longrepr)
  506. pos = text.find("E ")
  507. if pos == -1:
  508. return text
  509. bt, error = text[:pos], text[pos:]
  510. filters = [
  511. # File path, line number and function name
  512. (re.compile(r"^(.*?):(\d+): in (\S+)", flags=re.MULTILINE), r"[[unimp]]\1[[rst]]:[[alt2]]\2[[rst]]: in [[alt1]]\3[[rst]]"),
  513. ]
  514. for regex, substitution in filters:
  515. bt = regex.sub(substitution, bt)
  516. return "{}[[bad]]{}".format(bt, error)
  517. class TestItem(object):
  518. def __init__(self, report, result, test_suffix):
  519. self._result = result
  520. self.nodeid = report.nodeid
  521. self._class_name, self._test_name = tools.split_node_id(self.nodeid, test_suffix)
  522. self._error = None
  523. self._status = None
  524. self._process_report(report)
  525. self._duration = hasattr(report, 'duration') and report.duration or 0
  526. self._keywords = getattr(report, "keywords", {})
  527. def _process_report(self, report):
  528. if report.longrepr:
  529. self.set_error(report)
  530. if hasattr(report, 'when') and report.when != "call":
  531. self.set_error(report.when + " failed:\n" + self._error)
  532. else:
  533. self.set_error("")
  534. report_teststatus = _pytest.skipping.pytest_report_teststatus(report)
  535. if report_teststatus is not None:
  536. report_teststatus = report_teststatus[0]
  537. if report_teststatus == 'xfailed':
  538. self._status = 'xfail'
  539. self.set_error(report.wasxfail, 'imp')
  540. elif report_teststatus == 'xpassed':
  541. self._status = 'xpass'
  542. self.set_error("Test unexpectedly passed")
  543. elif report.skipped:
  544. self._status = 'skipped'
  545. self.set_error(yatest_lib.tools.to_utf8(report.longrepr[-1]))
  546. elif report.passed:
  547. self._status = 'good'
  548. self.set_error("")
  549. else:
  550. self._status = 'fail'
  551. @property
  552. def status(self):
  553. return self._status
  554. def set_status(self, status):
  555. self._status = status
  556. @property
  557. def test_name(self):
  558. return tools.normalize_name(self._test_name)
  559. @property
  560. def class_name(self):
  561. return tools.normalize_name(self._class_name)
  562. @property
  563. def error(self):
  564. return self._error
  565. def set_error(self, entry, marker='bad'):
  566. if isinstance(entry, _pytest.reports.BaseReport):
  567. self._error = get_formatted_error(entry)
  568. else:
  569. self._error = "[[{}]]{}".format(yatest_lib.tools.to_str(marker), yatest_lib.tools.to_str(entry))
  570. @property
  571. def duration(self):
  572. return self._duration
  573. @property
  574. def result(self):
  575. if 'not_canonize' in self._keywords:
  576. return None
  577. return self._result
  578. @property
  579. def keywords(self):
  580. return self._keywords
  581. def __str__(self):
  582. return "{}::{}".format(self.class_name, self.test_name)
  583. class CustomTestItem(TestItem):
  584. def __init__(self, nodeid, test_suffix, keywords=None):
  585. self._result = None
  586. self.nodeid = nodeid
  587. self._class_name, self._test_name = tools.split_node_id(nodeid, test_suffix)
  588. self._duration = 0
  589. self._error = ""
  590. self._keywords = keywords if keywords is not None else {}
  591. class NotLaunchedTestItem(CustomTestItem):
  592. def __init__(self, nodeid, test_suffix):
  593. super(NotLaunchedTestItem, self).__init__(nodeid, test_suffix)
  594. self._status = "not_launched"
  595. class CrashedTestItem(CustomTestItem):
  596. def __init__(self, nodeid, test_suffix):
  597. super(CrashedTestItem, self).__init__(nodeid, test_suffix)
  598. self._status = "crashed"
  599. class DeselectedTestItem(CustomTestItem):
  600. def __init__(self, nodeid, test_suffix):
  601. super(DeselectedTestItem, self).__init__(nodeid, test_suffix)
  602. self._status = "deselected"
  603. class TraceReportGenerator(object):
  604. def __init__(self, out_file_path):
  605. self._filename = out_file_path
  606. self._file = open(out_file_path, 'w')
  607. self._wreckage_filename = out_file_path + '.wreckage'
  608. self._test_messages = {}
  609. self._test_duration = {}
  610. # Some machinery to avoid data corruption due sloppy fork()
  611. self._current_test = (None, None)
  612. self._pid = os.getpid()
  613. self._check_intricate_respawn()
  614. def _check_intricate_respawn(self):
  615. pid_file = self._filename + '.pid'
  616. try:
  617. # python2 doesn't support open(f, 'x')
  618. afile = os.fdopen(os.open(pid_file, os.O_WRONLY | os.O_EXCL | os.O_CREAT), 'w')
  619. afile.write(str(self._pid))
  620. afile.close()
  621. return
  622. except OSError as e:
  623. if e.errno != errno.EEXIST:
  624. raise
  625. # Looks like the test binary was respawned
  626. if from_ya_test():
  627. try:
  628. with open(pid_file) as afile:
  629. prev_pid = afile.read()
  630. except Exception as e:
  631. prev_pid = '(failed to obtain previous pid: {})'.format(e)
  632. parts = [
  633. "Aborting test run: test machinery found that the test binary {} has already been run before.".format(sys.executable),
  634. "Looks like test has incorrect respawn/relaunch logic within test binary.",
  635. "Test should not try to restart itself - this is a poorly designed test case that leads to errors and could corrupt internal test machinery files.",
  636. "Debug info: previous pid:{} current:{}".format(prev_pid, self._pid),
  637. ]
  638. msg = '\n'.join(parts)
  639. yatest_logger.error(msg)
  640. if filelock:
  641. lock = filelock.FileLock(self._wreckage_filename + '.lock')
  642. lock.acquire()
  643. with open(self._wreckage_filename, 'a') as afile:
  644. self._file = afile
  645. self._dump_trace('chunk_event', {"errors": [('fail', '[[bad]]' + msg)]})
  646. raise Exception(msg)
  647. else:
  648. # Test binary is launched without `ya make -t`'s testing machinery - don't rely on clean environment
  649. pass
  650. def on_start_test_class(self, test_item):
  651. pytest_config.ya.set_test_item_node_id(test_item.nodeid)
  652. class_name = test_item.class_name.decode('utf-8') if sys.version_info[0] < 3 else test_item.class_name
  653. self._current_test = (class_name, None)
  654. self.trace('test-started', {'class': class_name})
  655. def on_finish_test_class(self, test_item):
  656. pytest_config.ya.set_test_item_node_id(test_item.nodeid)
  657. self.trace('test-finished', {'class': test_item.class_name.decode('utf-8') if sys.version_info[0] < 3 else test_item.class_name})
  658. def on_start_test_case(self, test_item):
  659. class_name = yatest_lib.tools.to_utf8(test_item.class_name)
  660. subtest_name = yatest_lib.tools.to_utf8(test_item.test_name)
  661. message = {
  662. 'class': class_name,
  663. 'subtest': subtest_name,
  664. }
  665. if test_item.nodeid in pytest_config.test_logs:
  666. message['logs'] = pytest_config.test_logs[test_item.nodeid]
  667. pytest_config.ya.set_test_item_node_id(test_item.nodeid)
  668. self._current_test = (class_name, subtest_name)
  669. self.trace('subtest-started', message)
  670. def on_finish_test_case(self, test_item, duration_only=False):
  671. if test_item.result is not None:
  672. try:
  673. result = canon.serialize(test_item.result)
  674. except Exception as e:
  675. yatest_logger.exception("Error while serializing test results")
  676. test_item.set_error("Invalid test result: {}".format(e))
  677. test_item.set_status("fail")
  678. result = None
  679. else:
  680. result = None
  681. if duration_only and test_item.nodeid in self._test_messages: # add teardown time
  682. message = self._test_messages[test_item.nodeid]
  683. else:
  684. comment = self._test_messages[test_item.nodeid]['comment'] if test_item.nodeid in self._test_messages else ''
  685. comment += self._get_comment(test_item)
  686. message = {
  687. 'class': yatest_lib.tools.to_utf8(test_item.class_name),
  688. 'subtest': yatest_lib.tools.to_utf8(test_item.test_name),
  689. 'status': test_item.status,
  690. 'comment': comment,
  691. 'result': result,
  692. 'metrics': pytest_config.test_metrics.get(test_item.nodeid),
  693. 'is_diff_test': 'diff_test' in test_item.keywords,
  694. 'tags': _get_item_tags(test_item),
  695. }
  696. if test_item.nodeid in pytest_config.test_logs:
  697. message['logs'] = pytest_config.test_logs[test_item.nodeid]
  698. message['time'] = self._test_duration.get(test_item.nodeid, test_item.duration)
  699. self.trace('subtest-finished', message)
  700. self._test_messages[test_item.nodeid] = message
  701. def dump_suite_metrics(self):
  702. message = {"metrics": pytest_config.suite_metrics}
  703. self.trace("suite-event", message)
  704. def on_error(self, test_item):
  705. self.trace('chunk_event', {"errors": [(test_item.status, self._get_comment(test_item))]})
  706. def on_log_report(self, test_item):
  707. if test_item.nodeid in self._test_duration:
  708. self._test_duration[test_item.nodeid] += test_item._duration
  709. else:
  710. self._test_duration[test_item.nodeid] = test_item._duration
  711. @staticmethod
  712. def _get_comment(test_item):
  713. msg = yatest_lib.tools.to_utf8(test_item.error)
  714. if not msg:
  715. return ""
  716. return msg + "[[rst]]"
  717. def _dump_trace(self, name, value):
  718. event = {
  719. 'timestamp': time.time(),
  720. 'value': value,
  721. 'name': name
  722. }
  723. data = yatest_lib.tools.to_str(json.dumps(event, ensure_ascii=False))
  724. self._file.write(data + '\n')
  725. self._file.flush()
  726. def _check_sloppy_fork(self, name, value):
  727. if self._pid == os.getpid():
  728. return
  729. yatest_logger.error("Skip tracing to avoid data corruption, name = %s, value = %s", name, value)
  730. try:
  731. # Lock wreckage tracefile to avoid race if multiple tests use fork sloppily
  732. if filelock:
  733. lock = filelock.FileLock(self._wreckage_filename + '.lock')
  734. lock.acquire()
  735. with open(self._wreckage_filename, 'a') as afile:
  736. self._file = afile
  737. parts = [
  738. "It looks like you have leaked process - it could corrupt internal test machinery files.",
  739. "Usually it happens when you casually use fork() without os._exit(),",
  740. "which results in two pytest processes running at the same time.",
  741. "Pid of the original pytest's process is {}, however current process has {} pid.".format(self._pid, os.getpid()),
  742. ]
  743. if self._current_test[1]:
  744. parts.append("Most likely the problem is in '{}' test.".format(self._current_test))
  745. else:
  746. parts.append("Most likely new process was created before any test was launched (during the import stage?).")
  747. if value.get('comment'):
  748. comment = value.get('comment', '').strip()
  749. # multiline comment
  750. newline_required = '\n' if '\n' in comment else ''
  751. parts.append("Debug info: name = '{}' comment:{}{}".format(name, newline_required, comment))
  752. else:
  753. val_str = json.dumps(value, ensure_ascii=False).encode('utf-8')
  754. parts.append("Debug info: name = '{}' value = '{}'".format(name, base64.b64encode(val_str)))
  755. msg = "[[bad]]{}".format('\n'.join(parts))
  756. class_name, subtest_name = self._current_test
  757. if subtest_name:
  758. data = {
  759. 'class': class_name,
  760. 'subtest': subtest_name,
  761. 'status': 'fail',
  762. 'comment': msg,
  763. }
  764. # overwrite original status
  765. self._dump_trace('subtest-finished', data)
  766. else:
  767. self._dump_trace('chunk_event', {"errors": [('fail', msg)]})
  768. except Exception as e:
  769. yatest_logger.exception(e)
  770. finally:
  771. os._exit(38)
  772. def trace(self, name, value):
  773. self._check_sloppy_fork(name, value)
  774. self._dump_trace(name, value)
  775. class DryTraceReportGenerator(TraceReportGenerator):
  776. """
  777. Generator does not write any information.
  778. """
  779. def __init__(self, *args, **kwargs):
  780. self._test_messages = {}
  781. self._test_duration = {}
  782. def trace(self, name, value):
  783. pass