transform-ya-junit.py 6.3 KB

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