create_new_muted_ya.py 18 KB

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