attach-logs.py 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160
  1. #!/usr/bin/env python3
  2. import argparse
  3. import io
  4. import os
  5. import glob
  6. import re
  7. from xml.etree import ElementTree as ET
  8. from pathlib import Path
  9. from typing import List
  10. from log_parser import ctest_log_parser, parse_yunit_fails, parse_gtest_fails, log_reader, GTEST_MARK, YUNIT_MARK
  11. from junit_utils import add_junit_log_property, create_error_testcase, create_error_testsuite, suite_case_iterator
  12. from ctest_utils import CTestLog
  13. fn_shard_part_re = re.compile(r"-\d+$")
  14. def make_filename(n, *parts):
  15. fn = f'{"-".join(parts)}'
  16. fn = re.sub(r"[^\w_-]", "", fn)
  17. if n > 0:
  18. fn = f"{fn}-{n}"
  19. return f"{fn}.log"
  20. def save_log(err_lines: List[str], out_path: Path, *parts):
  21. for x in range(128):
  22. fn = make_filename(x, *parts)
  23. path = out_path.joinpath(fn)
  24. try:
  25. with open(path, "xt") as fp:
  26. for line in err_lines:
  27. fp.write(f"{line}\n")
  28. except FileExistsError:
  29. pass
  30. else:
  31. print(f"save {fn} for {'::'.join(parts)}")
  32. return fn, path
  33. raise Exception("Unable to create file")
  34. def extract_logs(log_fp: io.StringIO, out_path: Path, url_prefix):
  35. # FIXME: memory inefficient because new buffer created every time
  36. ctestlog = CTestLog()
  37. for target, reason, ctest_buf in ctest_log_parser(log_fp):
  38. fn, _ = save_log(ctest_buf, out_path, target)
  39. log_url = f"{url_prefix}{fn}"
  40. shard = ctestlog.add_shard(target, reason, log_url)
  41. if not ctest_buf:
  42. continue
  43. line_no = 0
  44. while line_no < len(ctest_buf):
  45. if ctest_buf[line_no].startswith((YUNIT_MARK, GTEST_MARK)):
  46. break
  47. line_no += 1
  48. else:
  49. continue
  50. if ctest_buf[line_no].startswith(GTEST_MARK):
  51. for classname, method, err_lines in parse_gtest_fails(ctest_buf[line_no:]):
  52. fn, path = save_log(err_lines, out_path, classname, method)
  53. log_url = f"{url_prefix}{fn}"
  54. shard.add_testcase(classname, method, path, log_url)
  55. elif ctest_buf[line_no].startswith(YUNIT_MARK):
  56. for classname, method, err_lines in parse_yunit_fails(ctest_buf[line_no:]):
  57. fn, path = save_log(err_lines, out_path, classname, method)
  58. log_url = f"{url_prefix}{fn}"
  59. shard.add_testcase(classname, method, path, log_url)
  60. else:
  61. raise Exception("We checked known test markers in the while loop")
  62. return ctestlog
  63. def attach_to_ctest(ctest_log: CTestLog, ctest_path):
  64. tree = ET.parse(ctest_path)
  65. root = tree.getroot()
  66. changed = False
  67. for testcase in root.findall("testcase"):
  68. name = testcase.attrib["classname"]
  69. if ctest_log.has_error_shard(name):
  70. add_junit_log_property(testcase, ctest_log.get_shard(name).log_url)
  71. changed = True
  72. if changed:
  73. print(f"patch {ctest_path}")
  74. tree.write(ctest_path, xml_declaration=True, encoding="UTF-8")
  75. def attach_to_unittests(ctest_log: CTestLog, unit_path):
  76. all_found_tests = {}
  77. for fn in glob.glob(os.path.join(unit_path, "*.xml")):
  78. log_name = os.path.splitext(os.path.basename(fn))[0]
  79. common_shard_name = fn_shard_part_re.sub("", log_name)
  80. found_tests = all_found_tests.setdefault(common_shard_name, [])
  81. try:
  82. tree = ET.parse(fn)
  83. except ET.ParseError as e:
  84. print(f"Unable to parse {fn}: {e}")
  85. continue
  86. root = tree.getroot()
  87. changed = False
  88. for tsuite, tcase, cls, name in suite_case_iterator(root):
  89. test_log = ctest_log.get_log(common_shard_name, cls, name)
  90. if test_log is None:
  91. continue
  92. found_tests.append((cls, name))
  93. add_junit_log_property(tcase, test_log.url)
  94. changed = True
  95. if changed:
  96. print(f"patch {fn}")
  97. tree.write(fn, xml_declaration=True, encoding="UTF-8")
  98. for shard, found_tests in all_found_tests.items():
  99. extra_logs = ctest_log.get_extra_tests(shard, found_tests)
  100. if not extra_logs:
  101. continue
  102. fn = f"{shard}-0000.xml"
  103. print(f"create {fn}")
  104. testcases = [create_error_testcase(t.shard.name, t.classname, t.method, t.fn, t.url) for t in extra_logs]
  105. testsuite = create_error_testsuite(testcases)
  106. testsuite.write(os.path.join(unit_path, fn), xml_declaration=True, encoding="UTF-8")
  107. def main():
  108. parser = argparse.ArgumentParser()
  109. parser.add_argument("--url-prefix", default="./")
  110. parser.add_argument("--decompress", action="store_true", default=False, help="decompress ctest log")
  111. parser.add_argument("--ctest-report")
  112. parser.add_argument("--junit-reports-path")
  113. parser.add_argument("ctest_log")
  114. parser.add_argument("out_log_dir")
  115. args = parser.parse_args()
  116. ctest_log = extract_logs(log_reader(args.ctest_log, args.decompress), Path(args.out_log_dir), args.url_prefix)
  117. if ctest_log.has_logs:
  118. attach_to_ctest(ctest_log, args.ctest_report)
  119. attach_to_unittests(ctest_log, args.junit_reports_path)
  120. if __name__ == "__main__":
  121. main()