upload_tests_results.py 11 KB

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