upload_tests_results.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257
  1. #!/usr/bin/env python3
  2. import argparse
  3. import configparser
  4. import datetime
  5. import fnmatch
  6. import os
  7. import posixpath
  8. import shlex
  9. import subprocess
  10. import sys
  11. import time
  12. import xml.etree.ElementTree as ET
  13. import ydb
  14. from codeowners import CodeOwners
  15. from concurrent.futures import ThreadPoolExecutor, as_completed
  16. from decimal import Decimal
  17. def create_tables(pool, table_path):
  18. print(f"> create table if not exists:'{table_path}'")
  19. def callee(session):
  20. session.execute_scheme(f"""
  21. CREATE table IF NOT EXISTS `{table_path}` (
  22. build_type Utf8 NOT NULL,
  23. job_name Utf8,
  24. job_id Uint64,
  25. commit Utf8,
  26. branch Utf8 NOT NULL,
  27. pull Utf8,
  28. run_timestamp Timestamp NOT NULL,
  29. test_id Utf8 NOT NULL,
  30. suite_folder Utf8 NOT NULL,
  31. test_name Utf8 NOT NULL,
  32. duration Double,
  33. status Utf8 NOT NULL,
  34. status_description Utf8,
  35. owners Utf8,
  36. log Utf8,
  37. logsdir Utf8,
  38. stderr Utf8,
  39. stdout Utf8,
  40. PRIMARY KEY (`test_name`, `suite_folder`,build_type, branch, status, run_timestamp)
  41. )
  42. PARTITION BY HASH(`suite_folder`,test_name, build_type, branch )
  43. WITH (STORE = COLUMN)
  44. """)
  45. return pool.retry_operation_sync(callee)
  46. def bulk_upsert(table_client, table_path, rows):
  47. print(f"> bulk upsert: {table_path}")
  48. column_types = (
  49. ydb.BulkUpsertColumns()
  50. .add_column("branch", ydb.OptionalType(ydb.PrimitiveType.Utf8))
  51. .add_column("build_type", ydb.OptionalType(ydb.PrimitiveType.Utf8))
  52. .add_column("commit", ydb.OptionalType(ydb.PrimitiveType.Utf8))
  53. .add_column("duration", ydb.OptionalType(ydb.PrimitiveType.Double))
  54. .add_column("job_id", ydb.OptionalType(ydb.PrimitiveType.Uint64))
  55. .add_column("job_name", ydb.OptionalType(ydb.PrimitiveType.Utf8))
  56. .add_column("log", ydb.OptionalType(ydb.PrimitiveType.Utf8))
  57. .add_column("logsdir", ydb.OptionalType(ydb.PrimitiveType.Utf8))
  58. .add_column("owners", ydb.OptionalType(ydb.PrimitiveType.Utf8))
  59. .add_column("pull", ydb.OptionalType(ydb.PrimitiveType.Utf8))
  60. .add_column("run_timestamp", ydb.OptionalType(ydb.PrimitiveType.Timestamp))
  61. .add_column("status", ydb.OptionalType(ydb.PrimitiveType.Utf8))
  62. .add_column("status_description", ydb.OptionalType(ydb.PrimitiveType.Utf8))
  63. .add_column("stderr", ydb.OptionalType(ydb.PrimitiveType.Utf8))
  64. .add_column("stdout", ydb.OptionalType(ydb.PrimitiveType.Utf8))
  65. .add_column("suite_folder", ydb.OptionalType(ydb.PrimitiveType.Utf8))
  66. .add_column("test_id", ydb.OptionalType(ydb.PrimitiveType.Utf8))
  67. .add_column("test_name", ydb.OptionalType(ydb.PrimitiveType.Utf8))
  68. )
  69. table_client.bulk_upsert(table_path, rows, column_types)
  70. def parse_junit_xml(test_results_file, build_type, job_name, job_id, commit, branch, pull, run_timestamp):
  71. tree = ET.parse(test_results_file)
  72. root = tree.getroot()
  73. results = []
  74. for testsuite in root.findall(".//testsuite"):
  75. suite_folder = testsuite.get("name")
  76. for testcase in testsuite.findall("testcase"):
  77. name = testcase.get("name")
  78. duration = testcase.get("time")
  79. status_description = ""
  80. status = "passed"
  81. if testcase.find("properties/property/[@name='mute']") is not None:
  82. status = "mute"
  83. status_description = testcase.find(
  84. "properties/property/[@name='mute']"
  85. ).get("value")
  86. elif testcase.find("failure") is not None:
  87. status = "failure"
  88. status_description = testcase.find("failure").text
  89. elif testcase.find("error") is not None:
  90. status = "error"
  91. status_description = testcase.find("error").text
  92. elif testcase.find("skipped") is not None:
  93. status = "skipped"
  94. status_description = testcase.find("skipped").text
  95. results.append(
  96. {
  97. "build_type": build_type,
  98. "commit": commit,
  99. "branch": branch,
  100. "pull": pull,
  101. "run_timestamp": int(run_timestamp)*1000000,
  102. "job_name": job_name,
  103. "job_id": int(job_id),
  104. "suite_folder": suite_folder,
  105. "test_name": name,
  106. "duration": Decimal(duration),
  107. "status": status,
  108. "status_description": status_description.replace("\r\n", ";;").replace("\n", ";;").replace("\"", "'"),
  109. "log": "" if testcase.find("properties/property/[@name='url:log']") is None else testcase.find("properties/property/[@name='url:log']").get('value'),
  110. "logsdir": "" if testcase.find("properties/property/[@name='url:logsdir']") is None else testcase.find("properties/property/[@name='url:logsdir']").get('value'),
  111. "stderr": "" if testcase.find("properties/property/[@name='url:stderr']") is None else testcase.find("properties/property/[@name='url:stderr']").get('value'),
  112. "stdout": "" if testcase.find("properties/property/[@name='url:stdout']") is None else testcase.find("properties/property/[@name='url:stdout']").get('value'),
  113. }
  114. )
  115. return results
  116. def get_codeowners_for_tests(codeowners_file_path, tests_data):
  117. with open(codeowners_file_path, 'r') as file:
  118. data = file.read()
  119. owners_odj = CodeOwners(data)
  120. tests_data_with_owners = []
  121. for test in tests_data:
  122. target_path = f'{test["suite_folder"]}'
  123. owners = owners_odj.of(target_path)
  124. test["owners"] = joined_owners = ";;".join(
  125. [(":".join(x)) for x in owners])
  126. tests_data_with_owners.append(test)
  127. return tests_data_with_owners
  128. def main():
  129. parser = argparse.ArgumentParser()
  130. parser.add_argument('--test-results-file', action='store',
  131. required=True, help='XML with results of tests')
  132. parser.add_argument('--build-type', action='store',
  133. required=True, help='build type')
  134. parser.add_argument('--commit', default='store',
  135. dest="commit", required=True, help='commit sha')
  136. parser.add_argument('--branch', default='store',
  137. dest="branch", required=True, help='branch name ')
  138. parser.add_argument('--pull', action='store', dest="pull",
  139. required=True, help='pull number')
  140. parser.add_argument('--run-timestamp', action='store',
  141. dest="run_timestamp", required=True, help='time of test run start')
  142. parser.add_argument('--job-name', action='store', dest="job_name",
  143. required=True, help='job name where launched')
  144. parser.add_argument('--job-id', action='store', dest="job_id",
  145. required=True, help='job id of workflow')
  146. args = parser.parse_args()
  147. test_results_file = args.test_results_file
  148. build_type = args.build_type
  149. commit = args.commit
  150. branch = args.branch
  151. pull = args.pull
  152. run_timestamp = args.run_timestamp
  153. job_name = args.job_name
  154. job_id = args.job_id
  155. path_in_database = "test_results"
  156. dir = os.path.dirname(__file__)
  157. git_root = f"{dir}/../../.."
  158. codeowners = f"{git_root}/.github/TESTOWNERS"
  159. config = configparser.ConfigParser()
  160. config_file_path = f"{git_root}/.github/config/ydb_qa_db.ini"
  161. config.read(config_file_path)
  162. DATABASE_ENDPOINT = config["QA_DB"]["DATABASE_ENDPOINT"]
  163. DATABASE_PATH = config["QA_DB"]["DATABASE_PATH"]
  164. if "CI_YDB_SERVICE_ACCOUNT_KEY_FILE_CREDENTIALS" not in os.environ:
  165. print(
  166. "Error: Env variable CI_YDB_SERVICE_ACCOUNT_KEY_FILE_CREDENTIALS is missing, skipping"
  167. )
  168. return 1
  169. else:
  170. # Do not set up 'real' variable from gh workflows because it interfere with ydb tests
  171. # So, set up it locally
  172. os.environ["YDB_SERVICE_ACCOUNT_KEY_FILE_CREDENTIALS"] = os.environ[
  173. "CI_YDB_SERVICE_ACCOUNT_KEY_FILE_CREDENTIALS"
  174. ]
  175. test_table_name = f"{path_in_database}/test_runs_column"
  176. full_path = posixpath.join(DATABASE_PATH, test_table_name)
  177. with ydb.Driver(
  178. endpoint=DATABASE_ENDPOINT,
  179. database=DATABASE_PATH,
  180. credentials=ydb.credentials_from_env_variables(),
  181. ) as driver:
  182. driver.wait(timeout=10, fail_fast=True)
  183. session = ydb.retry_operation_sync(
  184. lambda: driver.table_client.session().create()
  185. )
  186. # Parse and upload
  187. results = parse_junit_xml(
  188. test_results_file, build_type, job_name, job_id, commit, branch, pull, run_timestamp
  189. )
  190. result_with_owners = get_codeowners_for_tests(codeowners, results)
  191. prepared_for_update_rows = []
  192. for index, row in enumerate(result_with_owners):
  193. prepared_for_update_rows.append({
  194. 'branch': row['branch'],
  195. 'build_type': row['build_type'],
  196. 'commit': row['commit'],
  197. 'duration': row['duration'],
  198. 'job_id': row['job_id'],
  199. 'job_name': row['job_name'],
  200. 'log': row['log'],
  201. 'logsdir': row['logsdir'],
  202. 'owners': row['owners'],
  203. 'pull': row['pull'],
  204. 'run_timestamp': row['run_timestamp'],
  205. 'status_description': row['status_description'],
  206. 'status': row['status'],
  207. 'stderr': row['stderr'],
  208. 'stdout': row['stdout'],
  209. 'suite_folder': row['suite_folder'],
  210. 'test_id': f"{row['pull']}_{row['run_timestamp']}_{index}",
  211. 'test_name': row['test_name'],
  212. })
  213. print(f'upserting runs: {len(prepared_for_update_rows)} rows')
  214. if prepared_for_update_rows:
  215. with ydb.SessionPool(driver) as pool:
  216. create_tables(pool, test_table_name)
  217. bulk_upsert(driver.table_client, full_path,
  218. prepared_for_update_rows)
  219. print('tests updated')
  220. else:
  221. print('nothing to upsert')
  222. if __name__ == "__main__":
  223. main()