transform_ya_junit.py 8.6 KB

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