transform-ya-junit.py 6.2 KB

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