ya.py 37 KB

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