transform-ya-junit.py 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273
  1. #!/usr/bin/env python3
  2. # Tool used to transform junit report. Performs the following:
  3. # - adds classname with relative path to test in 'testcase' node
  4. # - add 'url:logsdir' and other links with in 'testcase' node
  5. # - mutes tests
  6. import argparse
  7. import re
  8. import json
  9. import os
  10. import sys
  11. import urllib.parse
  12. import zipfile
  13. from typing import Set
  14. from xml.etree import ElementTree as ET
  15. from mute_utils import mute_target, pattern_to_re
  16. from junit_utils import add_junit_link_property, is_faulty_testcase
  17. def log_print(*args, **kwargs):
  18. print(*args, file=sys.stderr, **kwargs)
  19. class YaMuteCheck:
  20. def __init__(self):
  21. self.regexps = set()
  22. self.regexps = []
  23. def load(self, fn):
  24. with open(fn, "r") as fp:
  25. for line in fp:
  26. line = line.strip()
  27. try:
  28. testsuite, testcase = line.split(" ", maxsplit=1)
  29. except ValueError:
  30. log_print(f"SKIP INVALID MUTE CONFIG LINE: {line!r}")
  31. continue
  32. self.populate(testsuite, testcase)
  33. def populate(self, testsuite, testcase):
  34. check = []
  35. for p in (pattern_to_re(testsuite), pattern_to_re(testcase)):
  36. try:
  37. check.append(re.compile(p))
  38. except re.error:
  39. log_print(f"Unable to compile regex {p!r}")
  40. return
  41. self.regexps.append(tuple(check))
  42. def __call__(self, suite_name, test_name):
  43. for ps, pt in self.regexps:
  44. if ps.match(suite_name) and pt.match(test_name):
  45. return True
  46. return False
  47. class YTestReportTrace:
  48. def __init__(self, out_root):
  49. self.out_root = out_root
  50. self.traces = {}
  51. self.logs_dir = set()
  52. def abs_path(self, path):
  53. return path.replace("$(BUILD_ROOT)", self.out_root)
  54. def load(self, subdir):
  55. test_results_dir = os.path.join(self.out_root, f"{subdir}/test-results/")
  56. if not os.path.isdir(test_results_dir):
  57. log_print(f"Directory {test_results_dir} doesn't exist")
  58. return
  59. # find the test result
  60. for folder in os.listdir(test_results_dir):
  61. fn = os.path.join(self.out_root, test_results_dir, folder, "ytest.report.trace")
  62. if not os.path.isfile(fn):
  63. continue
  64. with open(fn, "r") as fp:
  65. for line in fp:
  66. event = json.loads(line.strip())
  67. if event["name"] == "subtest-finished":
  68. event = event["value"]
  69. cls = event["class"]
  70. subtest = event["subtest"]
  71. cls = cls.replace("::", ".")
  72. self.traces[(cls, subtest)] = event
  73. logs_dir = self.abs_path(event['logs']['logsdir'])
  74. self.logs_dir.add(logs_dir)
  75. def has(self, cls, name):
  76. return (cls, name) in self.traces
  77. def get_logs(self, cls, name):
  78. trace = self.traces.get((cls, name))
  79. if not trace:
  80. return {}
  81. logs = trace["logs"]
  82. result = {}
  83. for k, path in logs.items():
  84. if k == "logsdir":
  85. continue
  86. result[k] = self.abs_path(path)
  87. return result
  88. def filter_empty_logs(logs):
  89. result = {}
  90. for k, v in logs.items():
  91. if not os.path.isfile(v) or os.stat(v).st_size == 0:
  92. continue
  93. result[k] = v
  94. return result
  95. def save_log(build_root, fn, out_dir, log_url_prefix, trunc_size):
  96. fpath = os.path.relpath(fn, build_root)
  97. if out_dir is not None:
  98. out_fn = os.path.join(out_dir, fpath)
  99. fsize = os.stat(fn).st_size
  100. out_fn_dir = os.path.dirname(out_fn)
  101. if not os.path.isdir(out_fn_dir):
  102. os.makedirs(out_fn_dir, 0o700)
  103. if trunc_size and fsize > trunc_size:
  104. with open(fn, "rb") as in_fp:
  105. in_fp.seek(fsize - trunc_size)
  106. log_print(f"truncate {out_fn} to {trunc_size}")
  107. with open(out_fn, "wb") as out_fp:
  108. while 1:
  109. buf = in_fp.read(8192)
  110. if not buf:
  111. break
  112. out_fp.write(buf)
  113. else:
  114. os.symlink(fn, out_fn)
  115. quoted_fpath = urllib.parse.quote(fpath)
  116. return f"{log_url_prefix}{quoted_fpath}"
  117. def save_zip(suite_name, out_dir, url_prefix, logs_dir: Set[str]):
  118. arc_name = f"{suite_name.replace('/', '-')}.zip"
  119. arc_fn = os.path.join(out_dir, arc_name)
  120. zf = zipfile.ZipFile(arc_fn, mode="w", compression=zipfile.ZIP_DEFLATED, compresslevel=9)
  121. for path in logs_dir:
  122. # path is .../test-results/black/testing_out_stuff
  123. log_print(f"put {path} into {arc_name}")
  124. test_type = os.path.basename(os.path.dirname(path))
  125. for root, dirs, files in os.walk(path):
  126. for f in files:
  127. filename = os.path.join(root, f)
  128. zf.write(filename, os.path.join(test_type, os.path.relpath(filename, path)))
  129. zf.close()
  130. quoted_fpath = urllib.parse.quote(arc_name)
  131. return f"{url_prefix}{quoted_fpath}"
  132. def transform(fp, mute_check: YaMuteCheck, ya_out_dir, save_inplace, log_url_prefix, log_out_dir, log_truncate_size,
  133. test_stuff_out, test_stuff_prefix):
  134. tree = ET.parse(fp)
  135. root = tree.getroot()
  136. for suite in root.findall("testsuite"):
  137. suite_name = suite.get("name")
  138. traces = YTestReportTrace(ya_out_dir)
  139. traces.load(suite_name)
  140. has_fail_tests = False
  141. for case in suite.findall("testcase"):
  142. test_name = case.get("name")
  143. case.set("classname", suite_name)
  144. is_fail = is_faulty_testcase(case)
  145. has_fail_tests |= is_fail
  146. if mute_check(suite_name, test_name):
  147. log_print("mute", suite_name, test_name)
  148. mute_target(case)
  149. if is_fail and "." in test_name:
  150. test_cls, test_method = test_name.rsplit(".", maxsplit=1)
  151. logs = filter_empty_logs(traces.get_logs(test_cls, test_method))
  152. if logs:
  153. log_print(f"add {list(logs.keys())!r} properties for {test_cls}.{test_method}")
  154. for name, fn in logs.items():
  155. url = save_log(ya_out_dir, fn, log_out_dir, log_url_prefix, log_truncate_size)
  156. add_junit_link_property(case, name, url)
  157. if has_fail_tests:
  158. if not traces.logs_dir:
  159. log_print(f"no logsdir for {suite_name}")
  160. continue
  161. url = save_zip(suite_name, test_stuff_out, test_stuff_prefix, traces.logs_dir)
  162. for case in suite.findall("testcase"):
  163. add_junit_link_property(case, 'logsdir', url)
  164. if save_inplace:
  165. tree.write(fp.name)
  166. else:
  167. ET.indent(root)
  168. print(ET.tostring(root, encoding="unicode"))
  169. def main():
  170. parser = argparse.ArgumentParser()
  171. parser.add_argument(
  172. "-i", action="store_true", dest="save_inplace", default=False, help="modify input file in-place"
  173. )
  174. parser.add_argument("-m", help="muted test list")
  175. parser.add_argument('--public_dir', help='root directory for publication')
  176. parser.add_argument("--public_dir_url", help="url prefix for root directory")
  177. parser.add_argument("--log_out_dir", help="out dir to store logs (symlinked), relative to public_dir")
  178. parser.add_argument(
  179. "--log_truncate_size",
  180. type=int,
  181. default=134217728,
  182. help="truncate log after specific size, 0 disables truncation",
  183. )
  184. parser.add_argument("--ya_out", help="ya make output dir (for searching logs and artifacts)")
  185. parser.add_argument('--test_stuff_out', help='output dir for archive testing_out_stuff, relative to public_dir"')
  186. parser.add_argument("in_file", type=argparse.FileType("r"))
  187. args = parser.parse_args()
  188. mute_check = YaMuteCheck()
  189. if args.m:
  190. mute_check.load(args.m)
  191. log_out_dir = os.path.join(args.public_dir, args.log_out_dir)
  192. os.makedirs(log_out_dir, exist_ok=True)
  193. log_url_prefix = os.path.join(args.public_dir_url, args.log_out_dir)
  194. test_stuff_out = os.path.join(args.public_dir, args.test_stuff_out)
  195. os.makedirs(test_stuff_out, exist_ok=True)
  196. test_stuff_prefix = os.path.join(args.public_dir_url, args.test_stuff_out)
  197. transform(
  198. args.in_file,
  199. mute_check,
  200. args.ya_out,
  201. args.save_inplace,
  202. log_url_prefix,
  203. log_out_dir,
  204. args.log_truncate_size,
  205. test_stuff_out,
  206. test_stuff_prefix,
  207. )
  208. if __name__ == "__main__":
  209. main()