create_new_muted_ya.py 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405
  1. #!/usr/bin/env python3
  2. import argparse
  3. import configparser
  4. import datetime
  5. import os
  6. import re
  7. import ydb
  8. import logging
  9. from transform_ya_junit import YaMuteCheck
  10. from update_mute_issues import (
  11. create_and_add_issue_to_project,
  12. generate_github_issue_title_and_body,
  13. get_muted_tests_from_issues,
  14. )
  15. # Configure logging
  16. logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
  17. dir = os.path.dirname(__file__)
  18. config = configparser.ConfigParser()
  19. config_file_path = f"{dir}/../../config/ydb_qa_db.ini"
  20. repo_path = f"{dir}/../../../"
  21. muted_ya_path = '.github/config/muted_ya.txt'
  22. config.read(config_file_path)
  23. DATABASE_ENDPOINT = config["QA_DB"]["DATABASE_ENDPOINT"]
  24. DATABASE_PATH = config["QA_DB"]["DATABASE_PATH"]
  25. def execute_query(driver):
  26. query_string = '''
  27. SELECT * from (
  28. SELECT data.*,
  29. CASE WHEN new_flaky.full_name IS NOT NULL THEN True ELSE False END AS new_flaky_today,
  30. CASE WHEN flaky.full_name IS NOT NULL THEN True ELSE False END AS flaky_today,
  31. CASE WHEN muted_stable.full_name IS NOT NULL THEN True ELSE False END AS muted_stable_today,
  32. CASE WHEN muted_stable_n_days.full_name IS NOT NULL THEN True ELSE False END AS muted_stable_n_days_today,
  33. CASE WHEN deleted.full_name IS NOT NULL THEN True ELSE False END AS deleted_today
  34. FROM
  35. (SELECT test_name, suite_folder, full_name, date_window, build_type, branch, days_ago_window, history, history_class, pass_count, mute_count, fail_count, skip_count, success_rate, summary, owner, is_muted, is_test_chunk, state, previous_state, state_change_date, days_in_state, previous_state_filtered, state_change_date_filtered, days_in_state_filtered, state_filtered
  36. FROM `test_results/analytics/tests_monitor_test_with_filtered_states`) as data
  37. left JOIN
  38. (SELECT full_name, build_type, branch
  39. FROM `test_results/analytics/tests_monitor_test_with_filtered_states`
  40. WHERE state = 'Flaky'
  41. AND days_in_state = 1
  42. AND date_window = CurrentUtcDate()
  43. )as new_flaky
  44. ON
  45. data.full_name = new_flaky.full_name
  46. and data.build_type = new_flaky.build_type
  47. and data.branch = new_flaky.branch
  48. LEFT JOIN
  49. (SELECT full_name, build_type, branch
  50. FROM `test_results/analytics/tests_monitor_test_with_filtered_states`
  51. WHERE state = 'Flaky'
  52. AND date_window = CurrentUtcDate()
  53. )as flaky
  54. ON
  55. data.full_name = flaky.full_name
  56. and data.build_type = flaky.build_type
  57. and data.branch = flaky.branch
  58. LEFT JOIN
  59. (SELECT full_name, build_type, branch
  60. FROM `test_results/analytics/tests_monitor_test_with_filtered_states`
  61. WHERE state = 'Muted Stable'
  62. AND date_window = CurrentUtcDate()
  63. )as muted_stable
  64. ON
  65. data.full_name = muted_stable.full_name
  66. and data.build_type = muted_stable.build_type
  67. and data.branch = muted_stable.branch
  68. LEFT JOIN
  69. (SELECT full_name, build_type, branch
  70. FROM `test_results/analytics/tests_monitor_test_with_filtered_states`
  71. WHERE state= 'Muted Stable'
  72. AND days_in_state >= 14
  73. AND date_window = CurrentUtcDate()
  74. and is_test_chunk = 0
  75. )as muted_stable_n_days
  76. ON
  77. data.full_name = muted_stable_n_days.full_name
  78. and data.build_type = muted_stable_n_days.build_type
  79. and data.branch = muted_stable_n_days.branch
  80. LEFT JOIN
  81. (SELECT full_name, build_type, branch
  82. FROM `test_results/analytics/tests_monitor_test_with_filtered_states`
  83. WHERE state = 'no_runs'
  84. AND days_in_state >= 14
  85. AND date_window = CurrentUtcDate()
  86. and is_test_chunk = 0
  87. )as deleted
  88. ON
  89. data.full_name = deleted.full_name
  90. and data.build_type = deleted.build_type
  91. and data.branch = deleted.branch
  92. )
  93. where date_window = CurrentUtcDate() and branch = 'main'
  94. '''
  95. query = ydb.ScanQuery(query_string, {})
  96. table_client = ydb.TableClient(driver, ydb.TableClientSettings())
  97. it = table_client.scan_query(query)
  98. results = []
  99. while True:
  100. try:
  101. result = next(it)
  102. results = results + result.result_set.rows
  103. except StopIteration:
  104. break
  105. return results
  106. def add_lines_to_file(file_path, lines_to_add):
  107. try:
  108. os.makedirs(os.path.dirname(file_path), exist_ok=True)
  109. with open(file_path, 'w') as f:
  110. f.writelines(lines_to_add)
  111. logging.info(f"Lines added to {file_path}")
  112. except Exception as e:
  113. logging.error(f"Error adding lines to {file_path}: {e}")
  114. def apply_and_add_mutes(all_tests, output_path, mute_check):
  115. output_path = os.path.join(output_path, 'mute_update')
  116. all_tests = sorted(all_tests, key=lambda d: d['full_name'])
  117. try:
  118. deleted_tests = set(
  119. f"{test.get('suite_folder')} {test.get('test_name')}\n" for test in all_tests if test.get('deleted_today')
  120. )
  121. deleted_tests = sorted(deleted_tests)
  122. add_lines_to_file(os.path.join(output_path, 'deleted.txt'), deleted_tests)
  123. deleted_tests_debug = set(
  124. f"{test.get('suite_folder')} {test.get('test_name')} # owner {test.get('owner')} success_rate {test.get('success_rate')}%, state {test.get('state')} days in state {test.get('days_in_state')}\n"
  125. for test in all_tests
  126. if test.get('deleted_today')
  127. )
  128. deleted_tests_debug = sorted(deleted_tests_debug)
  129. add_lines_to_file(os.path.join(output_path, 'deleted_debug.txt'), deleted_tests_debug)
  130. muted_stable_tests = set(
  131. f"{test.get('suite_folder')} {test.get('test_name')}\n"
  132. for test in all_tests
  133. if test.get('muted_stable_n_days_today')
  134. )
  135. muted_stable_tests = sorted(muted_stable_tests)
  136. add_lines_to_file(os.path.join(output_path, 'muted_stable.txt'), muted_stable_tests)
  137. muted_stable_tests_debug = set(
  138. f"{test.get('suite_folder')} {test.get('test_name')} "
  139. + f"# owner {test.get('owner')} success_rate {test.get('success_rate')}%, state {test.get('state')} days in state {test.get('days_in_state')}\n"
  140. for test in all_tests
  141. if test.get('muted_stable_n_days_today')
  142. )
  143. muted_stable_tests_debug = sorted(muted_stable_tests_debug)
  144. add_lines_to_file(os.path.join(output_path, 'muted_stable_debug.txt'), muted_stable_tests_debug)
  145. # Add all flaky tests
  146. flaky_tests = set(
  147. re.sub(r'\d+/(\d+)\]', r'*/*]', f"{test.get('suite_folder')} {test.get('test_name')}\n")
  148. for test in all_tests
  149. if test.get('days_in_state') >= 1
  150. and test.get('flaky_today')
  151. and (test.get('pass_count') + test.get('fail_count')) >= 3
  152. and test.get('fail_count') > 2
  153. and test.get('fail_count')/(test.get('pass_count') + test.get('fail_count')) > 0.2 # <=80% success rate
  154. )
  155. flaky_tests = sorted(flaky_tests)
  156. add_lines_to_file(os.path.join(output_path, 'flaky.txt'), flaky_tests)
  157. flaky_tests_debug = set(
  158. re.sub(r'\d+/(\d+)\]', r'*/*]', f"{test.get('suite_folder')} {test.get('test_name')}")
  159. + f" # owner {test.get('owner')} success_rate {test.get('success_rate')}%, state {test.get('state')}, days in state {test.get('days_in_state')}, pass_count {test.get('pass_count')}, fail count {test.get('fail_count')}\n"
  160. for test in all_tests
  161. if test.get('days_in_state') >= 1
  162. and test.get('flaky_today')
  163. and (test.get('pass_count') + test.get('fail_count')) >= 3
  164. and test.get('fail_count') > 2
  165. and test.get('fail_count')/(test.get('pass_count') + test.get('fail_count')) > 0.2 # <=80% success rate
  166. )
  167. ## тесты может запускаться 1 раз в день. если за последние 7 дней набирается трешход то мьютим
  168. ## падения сегодня более весомы ??
  169. ## за 7 дней смотреть?
  170. #----
  171. ## Mute Flaky редко запускаемых тестов
  172. ## Разобраться почему 90 % флакающих тестов имеют только 1 падение и в статусе Flaky только 2 дня
  173. flaky_tests_debug = sorted(flaky_tests_debug)
  174. add_lines_to_file(os.path.join(output_path, 'flaky_debug.txt'), flaky_tests_debug)
  175. new_muted_ya_tests_debug = []
  176. new_muted_ya_tests = []
  177. new_muted_ya_tests_with_flaky = []
  178. new_muted_ya_tests_with_flaky_debug = []
  179. unmuted_tests_debug = []
  180. muted_ya_tests_sorted = []
  181. muted_ya_tests_sorted_debug = []
  182. deleted_tests_in_mute = []
  183. deleted_tests_in_mute_debug = []
  184. muted_before_count = 0
  185. unmuted_stable = 0
  186. unmuted_deleted = 0
  187. # Apply mute check and filter out already muted tests
  188. for test in all_tests:
  189. testsuite = test.get('suite_folder')
  190. testcase = test.get('test_name')
  191. success_rate = test.get('success_rate')
  192. days_in_state = test.get('days_in_state')
  193. owner = test.get('owner')
  194. state = test.get('state')
  195. test_string = f"{testsuite} {testcase}\n"
  196. test_string_debug = f"{testsuite} {testcase} # owner {owner} success_rate {success_rate}%, state {state} days in state {days_in_state}\n"
  197. test_string = re.sub(r'\d+/(\d+)\]', r'*/*]', test_string)
  198. if (
  199. testsuite and testcase and mute_check(testsuite, testcase) or test_string in flaky_tests
  200. ) and test_string not in new_muted_ya_tests_with_flaky:
  201. if test_string not in muted_stable_tests and test_string not in deleted_tests:
  202. new_muted_ya_tests_with_flaky.append(test_string)
  203. new_muted_ya_tests_with_flaky_debug.append(test_string_debug)
  204. if testsuite and testcase and mute_check(testsuite, testcase):
  205. if test_string not in muted_ya_tests_sorted:
  206. muted_ya_tests_sorted.append(test_string)
  207. muted_ya_tests_sorted_debug.append(test_string_debug)
  208. muted_before_count += 1
  209. if test_string not in new_muted_ya_tests:
  210. if test_string not in muted_stable_tests and test_string not in deleted_tests:
  211. new_muted_ya_tests.append(test_string)
  212. new_muted_ya_tests_debug.append(test_string_debug)
  213. if test_string in muted_stable_tests:
  214. unmuted_stable += 1
  215. if test_string in deleted_tests:
  216. unmuted_deleted += 1
  217. deleted_tests_in_mute.append(test_string)
  218. deleted_tests_in_mute_debug.append(test_string_debug)
  219. unmuted_tests_debug.append(test_string_debug)
  220. muted_ya_tests_sorted = sorted(muted_ya_tests_sorted)
  221. add_lines_to_file(os.path.join(output_path, 'muted_ya_sorted.txt'), muted_ya_tests_sorted)
  222. muted_ya_tests_sorted_debug = sorted(muted_ya_tests_sorted_debug)
  223. add_lines_to_file(os.path.join(output_path, 'muted_ya_sorted_debug.txt'), muted_ya_tests_sorted_debug)
  224. new_muted_ya_tests = sorted(new_muted_ya_tests)
  225. add_lines_to_file(os.path.join(output_path, 'new_muted_ya.txt'), new_muted_ya_tests)
  226. new_muted_ya_tests_debug = sorted(new_muted_ya_tests_debug)
  227. add_lines_to_file(os.path.join(output_path, 'new_muted_ya_debug.txt'), new_muted_ya_tests_debug)
  228. new_muted_ya_tests_with_flaky = sorted(new_muted_ya_tests_with_flaky)
  229. add_lines_to_file(os.path.join(output_path, 'new_muted_ya_with_flaky.txt'), new_muted_ya_tests_with_flaky)
  230. new_muted_ya_tests_with_flaky_debug = sorted(new_muted_ya_tests_with_flaky_debug)
  231. add_lines_to_file(
  232. os.path.join(output_path, 'new_muted_ya_with_flaky_debug.txt'), new_muted_ya_tests_with_flaky_debug
  233. )
  234. unmuted_tests_debug = sorted(unmuted_tests_debug)
  235. add_lines_to_file(os.path.join(output_path, 'unmuted_debug.txt'), unmuted_tests_debug)
  236. deleted_tests_in_mute = sorted(deleted_tests_in_mute)
  237. add_lines_to_file(os.path.join(output_path, 'deleted_tests_in_mute.txt'), deleted_tests_in_mute)
  238. deleted_tests_in_mute_debug = sorted(deleted_tests_in_mute_debug)
  239. add_lines_to_file(os.path.join(output_path, 'deleted_tests_in_mute_debug.txt'), deleted_tests_in_mute_debug)
  240. logging.info(f"Muted before script: {muted_before_count} tests")
  241. logging.info(f"Muted stable : {len(muted_stable_tests)}")
  242. logging.info(f"Flaky tests : {len(flaky_tests)}")
  243. logging.info(f"Result: Muted without deleted and stable : {len(new_muted_ya_tests)}")
  244. logging.info(f"Result: Muted without deleted and stable, with flaky : {len(new_muted_ya_tests_with_flaky)}")
  245. logging.info(f"Result: Unmuted tests : stable {unmuted_stable} and deleted {unmuted_deleted}")
  246. except (KeyError, TypeError) as e:
  247. logging.error(f"Error processing test data: {e}. Check your query results for valid keys.")
  248. return []
  249. return len(new_muted_ya_tests)
  250. def read_tests_from_file(file_path):
  251. result = []
  252. with open(file_path, "r") as fp:
  253. for line in fp:
  254. line = line.strip()
  255. try:
  256. testsuite, testcase = line.split(" ", maxsplit=1)
  257. result.append({'testsuite': testsuite, 'testcase': testcase, 'full_name': f"{testsuite}/{testcase}"})
  258. except ValueError:
  259. logging.warning(f"cant parse line: {line!r}")
  260. continue
  261. return result
  262. def create_mute_issues(all_tests, file_path):
  263. base_date = datetime.datetime(1970, 1, 1)
  264. tests_from_file = read_tests_from_file(file_path)
  265. muted_tests_in_issues = get_muted_tests_from_issues()
  266. prepared_tests_by_suite = {}
  267. for test in all_tests:
  268. for test_from_file in tests_from_file:
  269. if test['full_name'] == test_from_file['full_name']:
  270. if test['full_name'] in muted_tests_in_issues:
  271. logging.info(
  272. f"test {test['full_name']} already have issue, {muted_tests_in_issues[test['full_name']][0]['url']}"
  273. )
  274. else:
  275. key = f"{test_from_file['testsuite']}:{test['owner']}"
  276. if not prepared_tests_by_suite.get(key):
  277. prepared_tests_by_suite[key] = []
  278. prepared_tests_by_suite[key].append(
  279. {
  280. 'mute_string': f"{ test.get('suite_folder')} {test.get('test_name')}",
  281. 'test_name': test.get('test_name'),
  282. 'suite_folder': test.get('suite_folder'),
  283. 'full_name': test.get('full_name'),
  284. 'success_rate': test.get('success_rate'),
  285. 'days_in_state': test.get('days_in_state'),
  286. 'date_window': (base_date + datetime.timedelta(days=test.get('date_window'))).date() ,
  287. 'owner': test.get('owner'),
  288. 'state': test.get('state'),
  289. 'summary': test.get('summary'),
  290. 'fail_count': test.get('fail_count'),
  291. 'pass_count': test.get('pass_count'),
  292. 'branch': test.get('branch'),
  293. }
  294. )
  295. results = []
  296. for item in prepared_tests_by_suite:
  297. title, body = generate_github_issue_title_and_body(prepared_tests_by_suite[item])
  298. owner_value = prepared_tests_by_suite[item][0]['owner'].split('/', 1)[1] if '/' in prepared_tests_by_suite[item][0]['owner'] else prepared_tests_by_suite[item][0]['owner']
  299. result = create_and_add_issue_to_project(title, body, state='Muted', owner=owner_value)
  300. if not result:
  301. break
  302. else:
  303. results.append(
  304. f"Created issue '{title}' for {prepared_tests_by_suite[item][0]['owner']}, url {result['issue_url']}"
  305. )
  306. print("\n\n")
  307. print("\n".join(results))
  308. def mute_worker(args):
  309. # Simplified Connection
  310. if "CI_YDB_SERVICE_ACCOUNT_KEY_FILE_CREDENTIALS" not in os.environ:
  311. print("Error: Env variable CI_YDB_SERVICE_ACCOUNT_KEY_FILE_CREDENTIALS is missing, skipping")
  312. return 1
  313. else:
  314. # Do not set up 'real' variable from gh workflows because it interfere with ydb tests
  315. # So, set up it locally
  316. os.environ["YDB_SERVICE_ACCOUNT_KEY_FILE_CREDENTIALS"] = os.environ[
  317. "CI_YDB_SERVICE_ACCOUNT_KEY_FILE_CREDENTIALS"
  318. ]
  319. mute_check = YaMuteCheck()
  320. mute_check.load(muted_ya_path)
  321. with ydb.Driver(
  322. endpoint=DATABASE_ENDPOINT,
  323. database=DATABASE_PATH,
  324. credentials=ydb.credentials_from_env_variables(),
  325. ) as driver:
  326. driver.wait(timeout=10, fail_fast=True)
  327. all_tests = execute_query(driver)
  328. if args.mode == 'update_muted_ya':
  329. output_path = args.output_folder
  330. os.makedirs(output_path, exist_ok=True)
  331. apply_and_add_mutes(all_tests, output_path, mute_check)
  332. elif args.mode == 'create_issues':
  333. file_path = args.file_path
  334. create_mute_issues(all_tests, file_path)
  335. if __name__ == "__main__":
  336. parser = argparse.ArgumentParser(description="Add tests to mutes files based on flaky_today condition")
  337. subparsers = parser.add_subparsers(dest='mode', help="Mode to perform")
  338. update_muted_ya_parser = subparsers.add_parser('update_muted_ya', help='create new muted_ya')
  339. update_muted_ya_parser.add_argument('--output_folder', default=repo_path, required=False, help='Output folder.')
  340. create_issues_parser = subparsers.add_parser(
  341. 'create_issues',
  342. help='create issues by muted_ya like files',
  343. )
  344. create_issues_parser.add_argument(
  345. '--file_path', default=f'{repo_path}/mute_update/flaky.txt', required=False, help='file path'
  346. )
  347. args = parser.parse_args()
  348. mute_worker(args)