upload_tests_results.py 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276
  1. #!/usr/bin/env python3
  2. import argparse
  3. import configparser
  4. import fnmatch
  5. import os
  6. import subprocess
  7. import sys
  8. import time
  9. import ydb
  10. import xml.etree.ElementTree as ET
  11. from codeowners import CodeOwners
  12. from concurrent.futures import ThreadPoolExecutor, as_completed
  13. def parse_junit_xml(test_results_file, build_type, job_name, job_id, commit, branch, pull, run_timestamp):
  14. tree = ET.parse(test_results_file)
  15. root = tree.getroot()
  16. results = []
  17. for testsuite in root.findall(".//testsuite"):
  18. suite_folder = testsuite.get("name")
  19. for testcase in testsuite.findall("testcase"):
  20. name = testcase.get("name")
  21. duration = testcase.get("time")
  22. status_description = ""
  23. status = "passed"
  24. if testcase.find("properties/property/[@name='mute']") is not None:
  25. status = "mute"
  26. status_description = testcase.find(
  27. "properties/property/[@name='mute']"
  28. ).get("value")
  29. elif testcase.find("failure") is not None:
  30. status = "failure"
  31. status_description = testcase.find("failure").text
  32. elif testcase.find("error") is not None:
  33. status = "error"
  34. status_description = testcase.find("error").text
  35. elif testcase.find("skipped") is not None:
  36. status = "skipped"
  37. status_description = testcase.find("skipped").text
  38. results.append(
  39. {
  40. "build_type": build_type,
  41. "commit": commit,
  42. "branch": branch,
  43. "pull": pull,
  44. "run_timestamp": run_timestamp,
  45. "job_name": job_name,
  46. "job_id": job_id,
  47. "suite_folder": suite_folder,
  48. "test_name": name,
  49. "duration": float(duration),
  50. "status": status,
  51. "status_description": status_description.replace("\r\n", ";;").replace("\n", ";;").replace("\"", "'"),
  52. "log": "" if testcase.find("properties/property/[@name='url:log']") is None else testcase.find("properties/property/[@name='url:log']").get('value'),
  53. "logsdir": "" if testcase.find("properties/property/[@name='url:logsdir']") is None else testcase.find("properties/property/[@name='url:logsdir']").get('value'),
  54. "stderr": "" if testcase.find("properties/property/[@name='url:stderr']") is None else testcase.find("properties/property/[@name='url:stderr']").get('value'),
  55. "stdout": "" if testcase.find("properties/property/[@name='url:stdout']") is None else testcase.find("properties/property/[@name='url:stdout']").get('value'),
  56. }
  57. )
  58. return results
  59. def get_codeowners_for_tests(codeowners_file_path, tests_data):
  60. with open(codeowners_file_path, 'r') as file:
  61. data = file.read()
  62. owners_odj = CodeOwners(data)
  63. tests_data_with_owners = []
  64. for test in tests_data:
  65. target_path = f'{test["suite_folder"]}'
  66. owners = owners_odj.of(target_path)
  67. test["owners"] = joined_owners=";;".join([(":".join(x)) for x in owners])
  68. tests_data_with_owners.append(test)
  69. return tests_data_with_owners
  70. def create_table(session, sql):
  71. try:
  72. session.execute_scheme(sql)
  73. except ydb.issues.AlreadyExists:
  74. pass
  75. except Exception as e:
  76. print(f"Error creating table: {e}, sql:\n{sql}", file=sys.stderr)
  77. raise e
  78. def create_tests_table(session, table_path):
  79. # Creating of new table if not exist yet
  80. sql = f"""
  81. --!syntax_v1
  82. CREATE TABLE IF NOT EXISTS `{table_path}` (
  83. build_type Utf8,
  84. job_name Utf8,
  85. job_id Uint64,
  86. commit Utf8,
  87. branch Utf8,
  88. pull Utf8,
  89. run_timestamp Timestamp,
  90. test_id Utf8,
  91. suite_folder Utf8,
  92. test_name Utf8,
  93. duration Double,
  94. status Utf8,
  95. status_description Utf8,
  96. owners Utf8,
  97. log Utf8,
  98. logsdir Utf8,
  99. stderr Utf8,
  100. stdout Utf8,
  101. PRIMARY KEY(test_id)
  102. );
  103. """
  104. create_table(session, sql)
  105. def upload_results(pool, sql):
  106. with pool.checkout() as session:
  107. try:
  108. session.transaction().execute(sql, commit_tx=True)
  109. except Exception as e:
  110. print(f"Error executing query {e}, sql:\n{sql}", file=sys.stderr)
  111. raise e
  112. def prepare_and_upload_tests(pool, path, results, batch_size):
  113. total_records = len(results)
  114. if total_records == 0:
  115. print("Error:Object stored test results is empty, check test results artifacts")
  116. return 1
  117. with ThreadPoolExecutor(max_workers=10) as executor:
  118. for start in range(0, total_records, batch_size):
  119. futures = []
  120. end = min(start + batch_size, total_records)
  121. batch = results[start:end]
  122. sql = f"""--!syntax_v1
  123. UPSERT INTO `{path}`
  124. (build_type,
  125. job_name,
  126. job_id,
  127. commit,
  128. branch,
  129. pull,
  130. run_timestamp,
  131. test_id,
  132. suite_folder,
  133. test_name,
  134. duration,
  135. status,
  136. status_description,
  137. log,
  138. logsdir,
  139. stderr,
  140. stdout,
  141. owners) VALUES"""
  142. values = []
  143. for index, result in enumerate(batch):
  144. values.append(
  145. f"""
  146. ("{result['build_type']}",
  147. "{result['job_name']}",
  148. {result['job_id']},
  149. "{result['commit']}",
  150. "{result['branch']}",
  151. "{result['pull']}",
  152. DateTime::FromSeconds({result['run_timestamp']}),
  153. "{result['pull']}_{result['run_timestamp']}_{start+index}",
  154. "{result['suite_folder']}",
  155. "{result['test_name']}",
  156. {result['duration']},
  157. "{result['status']}",
  158. "{result['status_description']}",
  159. "{result['log']}",
  160. "{result['logsdir']}",
  161. "{result['stderr']}",
  162. "{result['stdout']}",
  163. "{result['owners']}")
  164. """
  165. )
  166. sql += ", ".join(values) + ";"
  167. futures.append(executor.submit(upload_results, pool, sql))
  168. for future in as_completed(futures):
  169. future.result() # Raise exception if occurred
  170. def create_pool(endpoint, database):
  171. driver_config = ydb.DriverConfig(
  172. endpoint,
  173. database,
  174. credentials=ydb.credentials_from_env_variables()
  175. )
  176. driver = ydb.Driver(driver_config)
  177. driver.wait(fail_fast=True, timeout=5)
  178. return ydb.SessionPool(driver)
  179. def main():
  180. parser = argparse.ArgumentParser()
  181. parser.add_argument('--test-results-file', action='store', dest="test_results_file", required=True, help='XML with results of tests')
  182. parser.add_argument('--build-type', action='store', dest="build_type", required=True, help='build type')
  183. parser.add_argument('--commit', default='store', dest="commit", required=True, help='commit sha')
  184. parser.add_argument('--branch', default='store', dest="branch", required=True, help='branch name ')
  185. parser.add_argument('--pull', action='store', dest="pull",required=True, help='pull number')
  186. parser.add_argument('--run-timestamp', action='store', dest="run_timestamp",required=True, help='time of test run start')
  187. parser.add_argument('--job-name', action='store', dest="job_name",required=True, help='job name where launched')
  188. parser.add_argument('--job-id', action='store', dest="job_id",required=True, help='job id of workflow')
  189. args = parser.parse_args()
  190. test_results_file = args.test_results_file
  191. build_type = args.build_type
  192. commit = args.commit
  193. branch = args.branch
  194. pull = args.pull
  195. run_timestamp = args.run_timestamp
  196. job_name = args.job_name
  197. job_id = args.job_id
  198. path_in_database = "test_results"
  199. batch_size_default = 30
  200. dir = os.path.dirname(__file__)
  201. git_root = f"{dir}/../.."
  202. codeowners = f"{git_root}/.github/CODEOWNERS"
  203. config = configparser.ConfigParser()
  204. config_file_path = f"{git_root}/.github/config/ydb_qa_db.ini"
  205. config.read(config_file_path)
  206. DATABASE_ENDPOINT = config["QA_DB"]["DATABASE_ENDPOINT"]
  207. DATABASE_PATH = config["QA_DB"]["DATABASE_PATH"]
  208. if "CI_YDB_SERVICE_ACCOUNT_KEY_FILE_CREDENTIALS" not in os.environ:
  209. print(
  210. "Error: Env variable CI_YDB_SERVICE_ACCOUNT_KEY_FILE_CREDENTIALS is missing, skipping"
  211. )
  212. return 0
  213. else:
  214. # Do not set up 'real' variable from gh workflows because it interfere with ydb tests
  215. # So, set up it locally
  216. os.environ["YDB_SERVICE_ACCOUNT_KEY_FILE_CREDENTIALS"] = os.environ[
  217. "CI_YDB_SERVICE_ACCOUNT_KEY_FILE_CREDENTIALS"
  218. ]
  219. test_table_name = f"{path_in_database}/test_runs_results"
  220. # Prepare connection and table
  221. pool = create_pool(DATABASE_ENDPOINT, DATABASE_PATH)
  222. with pool.checkout() as session:
  223. create_tests_table(session, test_table_name)
  224. # Parse and upload
  225. results = parse_junit_xml(
  226. test_results_file, build_type, job_name, job_id, commit, branch, pull, run_timestamp
  227. )
  228. result_with_owners = get_codeowners_for_tests(codeowners, results)
  229. prepare_and_upload_tests(
  230. pool, test_table_name, results, batch_size=batch_size_default
  231. )
  232. if __name__ == "__main__":
  233. main()