transform-ya-junit.py 8.0 KB

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