yatest_tools.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412
  1. # coding: utf-8
  2. import collections
  3. import functools
  4. import math
  5. import os
  6. import re
  7. import sys
  8. from . import config
  9. import yatest_lib.tools
  10. SEP = '/'
  11. TEST_MOD_PREFIX = '__tests__.'
  12. class SubtestInfo(object):
  13. skipped_prefix = '[SKIPPED] '
  14. @classmethod
  15. def from_str(cls, s):
  16. if s.startswith(SubtestInfo.skipped_prefix):
  17. s = s[len(SubtestInfo.skipped_prefix) :]
  18. skipped = True
  19. else:
  20. skipped = False
  21. return SubtestInfo(*s.rsplit(TEST_SUBTEST_SEPARATOR, 1), skipped=skipped)
  22. def __init__(self, test, subtest="", skipped=False, **kwargs):
  23. self.test = test
  24. self.subtest = subtest
  25. self.skipped = skipped
  26. for key, value in kwargs.iteritems():
  27. setattr(self, key, value)
  28. def __str__(self):
  29. s = ''
  30. if self.skipped:
  31. s += SubtestInfo.skipped_prefix
  32. return s + TEST_SUBTEST_SEPARATOR.join([self.test, self.subtest])
  33. def __repr__(self):
  34. return str(self)
  35. class Status(object):
  36. GOOD, XFAIL, FAIL, XPASS, MISSING, CRASHED, TIMEOUT = range(7)
  37. SKIPPED = -100
  38. NOT_LAUNCHED = -200
  39. CANON_DIFF = -300
  40. FLAKY = -1
  41. BY_NAME = {
  42. 'good': GOOD,
  43. 'fail': FAIL,
  44. 'xfail': XFAIL,
  45. 'xpass': XPASS,
  46. 'missing': MISSING,
  47. 'crashed': CRASHED,
  48. 'skipped': SKIPPED,
  49. 'flaky': FLAKY,
  50. 'not_launched': NOT_LAUNCHED,
  51. 'timeout': TIMEOUT,
  52. 'diff': CANON_DIFF,
  53. }
  54. TO_STR = {
  55. GOOD: 'good',
  56. FAIL: 'fail',
  57. XFAIL: 'xfail',
  58. XPASS: 'xpass',
  59. MISSING: 'missing',
  60. CRASHED: 'crashed',
  61. SKIPPED: 'skipped',
  62. FLAKY: 'flaky',
  63. NOT_LAUNCHED: 'not_launched',
  64. TIMEOUT: 'timeout',
  65. CANON_DIFF: 'diff',
  66. }
  67. class Test(object):
  68. def __init__(self, name, path, status=None, comment=None, subtests=None):
  69. self.name = name
  70. self.path = path
  71. self.status = status
  72. self.comment = comment
  73. self.subtests = subtests or []
  74. def __eq__(self, other):
  75. if not isinstance(other, Test):
  76. return False
  77. return self.name == other.name and self.path == other.path
  78. def __str__(self):
  79. return "Test [{} {}] - {} - {}".format(self.name, self.path, self.status, self.comment)
  80. def __repr__(self):
  81. return str(self)
  82. def add_subtest(self, subtest):
  83. self.subtests.append(subtest)
  84. def setup_status(self, status, comment):
  85. self.status = Status.BY_NAME[status or 'good']
  86. if len(self.subtests) != 0:
  87. self.status = max(self.status, max(s.status for s in self.subtests))
  88. self.comment = comment
  89. def subtests_by_status(self, status):
  90. return [x.status for x in self.subtests].count(status)
  91. TEST_SUBTEST_SEPARATOR = '::'
  92. # TODO: extract color theme logic from ya
  93. COLOR_THEME = {
  94. 'test_name': 'light-blue',
  95. 'test_project_path': 'dark-blue',
  96. 'test_dir_desc': 'dark-magenta',
  97. 'test_binary_path': 'light-gray',
  98. }
  99. # XXX: remove me
  100. class YaCtx(object):
  101. pass
  102. ya_ctx = YaCtx()
  103. TRACE_FILE_NAME = "ytest.report.trace"
  104. def lazy(func):
  105. memory = {}
  106. @functools.wraps(func)
  107. def wrapper(*args):
  108. # Disabling caching in test mode
  109. if config.is_test_mode():
  110. return func(*args)
  111. try:
  112. return memory[args]
  113. except KeyError:
  114. memory[args] = func(*args)
  115. return memory[args]
  116. return wrapper
  117. @lazy
  118. def _get_mtab():
  119. if os.path.exists("/etc/mtab"):
  120. with open("/etc/mtab") as afile:
  121. data = afile.read()
  122. return [line.split(" ") for line in data.split("\n") if line]
  123. return []
  124. @lazy
  125. def get_max_filename_length(dirname):
  126. """
  127. Return maximum filename length for the filesystem
  128. :return:
  129. """
  130. if sys.platform.startswith("linux"):
  131. # Linux user's may work on mounted ecryptfs filesystem
  132. # which has filename length limitations
  133. for entry in _get_mtab():
  134. mounted_dir, filesystem = entry[1], entry[2]
  135. # http://unix.stackexchange.com/questions/32795/what-is-the-maximum-allowed-filename-and-folder-size-with-ecryptfs
  136. if filesystem == "ecryptfs" and dirname and dirname.startswith(mounted_dir):
  137. return 140
  138. # default maximum filename length for most filesystems
  139. return 255
  140. def get_unique_file_path(dir_path, filename, cache=collections.defaultdict(set)):
  141. """
  142. Get unique filename in dir with proper filename length, using given filename/dir.
  143. File/dir won't be created (thread nonsafe)
  144. :param dir_path: path to dir
  145. :param filename: original filename
  146. :return: unique filename
  147. """
  148. max_suffix = 10000
  149. # + 1 symbol for dot before suffix
  150. tail_length = int(round(math.log(max_suffix, 10))) + 1
  151. # truncate filename length in accordance with filesystem limitations
  152. filename, extension = os.path.splitext(filename)
  153. # XXX
  154. if sys.platform.startswith("win"):
  155. # Trying to fit into MAX_PATH if it's possible.
  156. # Remove after DEVTOOLS-1646
  157. max_path = 260
  158. filename_len = len(dir_path) + len(extension) + tail_length + len(os.sep)
  159. if filename_len < max_path:
  160. filename = yatest_lib.tools.trim_string(filename, max_path - filename_len)
  161. filename = (
  162. yatest_lib.tools.trim_string(filename, get_max_filename_length(dir_path) - tail_length - len(extension))
  163. + extension
  164. )
  165. candidate = os.path.join(dir_path, filename)
  166. key = dir_path + filename
  167. counter = sorted(
  168. cache.get(
  169. key,
  170. {
  171. 0,
  172. },
  173. )
  174. )[-1]
  175. while os.path.exists(candidate):
  176. cache[key].add(counter)
  177. counter += 1
  178. assert counter < max_suffix
  179. candidate = os.path.join(dir_path, filename + ".{}".format(counter))
  180. return candidate
  181. def escape_for_fnmatch(s):
  182. return s.replace("[", "&#91;").replace("]", "&#93;")
  183. def get_python_cmd(opts=None, use_huge=True, suite=None):
  184. if opts and getattr(opts, 'flags', {}).get("USE_ARCADIA_PYTHON") == "no":
  185. return ["python"]
  186. if suite and not suite._use_arcadia_python:
  187. return ["python"]
  188. if use_huge:
  189. return ["$(PYTHON)/python"]
  190. ymake_path = opts.ymake_bin if opts and getattr(opts, 'ymake_bin', None) else "$(YMAKE)/ymake"
  191. return [ymake_path, "--python"]
  192. def normalize_name(name):
  193. replacements = [
  194. ("\\", "\\\\"),
  195. ("\n", "\\n"),
  196. ("\t", "\\t"),
  197. ("\r", "\\r"),
  198. ]
  199. for from_, to in replacements:
  200. name = name.replace(from_, to)
  201. return name
  202. @lazy
  203. def normalize_filename(filename):
  204. """
  205. Replace invalid for file names characters with string equivalents
  206. :param some_string: string to be converted to a valid file name
  207. :return: valid file name
  208. """
  209. not_allowed_pattern = r"[\[\]\/:*?\"\'<>|+\0\\\s\x0b\x0c]"
  210. filename = re.sub(not_allowed_pattern, ".", filename)
  211. return re.sub(r"\.{2,}", ".", filename)
  212. def get_test_log_file_path(output_dir, class_name, test_name, extension="log"):
  213. """
  214. get test log file path, platform dependant
  215. :param output_dir: dir where log file should be placed
  216. :param class_name: test class name
  217. :param test_name: test name
  218. :return: test log file name
  219. """
  220. if os.name == "nt":
  221. # don't add class name to the log's filename
  222. # to reduce it's length on windows
  223. filename = test_name
  224. else:
  225. filename = "{}.{}".format(class_name, test_name)
  226. if not filename:
  227. filename = "test"
  228. filename += "." + extension
  229. filename = normalize_filename(filename)
  230. return get_unique_file_path(output_dir, filename)
  231. @lazy
  232. def split_node_id(nodeid, test_suffix=None):
  233. path, possible_open_bracket, params = nodeid.partition('[')
  234. separator = "::"
  235. test_name = None
  236. if separator in path:
  237. path, test_name = path.split(separator, 1)
  238. path = _unify_path(path)
  239. class_name = os.path.basename(path)
  240. if test_name is None:
  241. test_name = class_name
  242. if test_suffix:
  243. test_name += "::" + test_suffix
  244. if separator in test_name:
  245. klass_name, test_name = test_name.split(separator, 1)
  246. if not test_suffix:
  247. # test suffix is used for flakes and pep8, no need to add class_name as it's === class_name
  248. class_name += separator + klass_name
  249. if separator in test_name:
  250. test_name = test_name.split(separator)[-1]
  251. test_name += possible_open_bracket + params
  252. return yatest_lib.tools.to_utf8(class_name), yatest_lib.tools.to_utf8(test_name)
  253. @lazy
  254. def _suffix_test_modules_tree():
  255. root = {}
  256. for module in sys.extra_modules:
  257. if not module.startswith(TEST_MOD_PREFIX):
  258. continue
  259. module = module[len(TEST_MOD_PREFIX) :]
  260. node = root
  261. for name in reversed(module.split('.')):
  262. if name == '__init__':
  263. continue
  264. node = node.setdefault(name, {})
  265. return root
  266. def _conftest_load_policy_is_local(path):
  267. return SEP in path and getattr(sys, "is_standalone_binary", False)
  268. class MissingTestModule(Exception):
  269. pass
  270. # If CONFTEST_LOAD_POLICY==LOCAL the path parameters is a true test file path. Something like
  271. # /-B/taxi/uservices/services/alt/gen/tests/build/services/alt/validation/test_generated_files.py
  272. # If CONFTEST_LOAD_POLICY is not LOCAL the path parameter is a module name with '.py' extension added. Example:
  273. # validation.test_generated_files.py
  274. # To make test names independent of the CONFTEST_LOAD_POLICY value replace path by module name if possible.
  275. @lazy
  276. def _unify_path(path):
  277. py_ext = ".py"
  278. path = path.strip()
  279. if _conftest_load_policy_is_local(path) and path.endswith(py_ext):
  280. # Try to find best match for path as a module among test modules and use it as a class name.
  281. # This is the only way to unify different CONFTEST_LOAD_POLICY modes
  282. suff_tree = _suffix_test_modules_tree()
  283. node, res = suff_tree, []
  284. assert path.endswith(py_ext), path
  285. parts = path[: -len(py_ext)].split(SEP)
  286. # Use SEP as trailing terminator to make an extra step
  287. # and find a proper match when parts is a full matching path
  288. for p in reversed([SEP] + parts):
  289. if p in node:
  290. node = node[p]
  291. res.append(p)
  292. else:
  293. if res:
  294. return '.'.join(reversed(res)) + py_ext
  295. else:
  296. # Top level test module
  297. if TEST_MOD_PREFIX + p in sys.extra_modules:
  298. return p + py_ext
  299. # Unknown module - raise an error
  300. break
  301. raise MissingTestModule("Can't find proper module for '{}' path among: {}".format(path, suff_tree))
  302. else:
  303. return path
  304. def colorize_pytest_error(text):
  305. error_prefix = "E "
  306. blocks = [text]
  307. while True:
  308. text = blocks.pop()
  309. err_start = text.find(error_prefix, 1)
  310. if err_start == -1:
  311. return ''.join(blocks + [text])
  312. for pos in range(err_start + 1, len(text) - 1):
  313. if text[pos] == '\n':
  314. if not text[pos + 1 :].startswith(error_prefix):
  315. err_end = pos + 1
  316. break
  317. else:
  318. err_end = len(text)
  319. bt, error, tail = text[:err_start], text[err_start:err_end], text[err_end:]
  320. filters = [
  321. # File path, line number and function name
  322. (
  323. re.compile(r"^(.*?):(\d+): in (\S+)", flags=re.MULTILINE),
  324. r"[[unimp]]\1[[rst]]:[[alt2]]\2[[rst]]: in [[alt1]]\3[[rst]]",
  325. ),
  326. ]
  327. for regex, substitution in filters:
  328. bt = regex.sub(substitution, bt)
  329. blocks.append(bt)
  330. blocks.append('[[bad]]' + error)
  331. blocks.append(tail)