create_new_muted_ya.py 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401
  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. muted_before_count = 0
  184. unmuted_stable = 0
  185. unmuted_deleted = 0
  186. # Apply mute check and filter out already muted tests
  187. for test in all_tests:
  188. testsuite = test.get('suite_folder')
  189. testcase = test.get('test_name')
  190. success_rate = test.get('success_rate')
  191. days_in_state = test.get('days_in_state')
  192. owner = test.get('owner')
  193. state = test.get('state')
  194. test_string = f"{testsuite} {testcase}\n"
  195. test_string_debug = f"{testsuite} {testcase} # owner {owner} success_rate {success_rate}%, state {state} days in state {days_in_state}\n"
  196. test_string = re.sub(r'\d+/(\d+)\]', r'*/*]', test_string)
  197. if (
  198. testsuite and testcase and mute_check(testsuite, testcase) or test_string in flaky_tests
  199. ) and test_string not in new_muted_ya_tests_with_flaky:
  200. if test_string not in muted_stable_tests and test_string not in deleted_tests:
  201. new_muted_ya_tests_with_flaky.append(test_string)
  202. new_muted_ya_tests_with_flaky_debug.append(test_string_debug)
  203. if testsuite and testcase and mute_check(testsuite, testcase):
  204. if test_string not in muted_ya_tests_sorted:
  205. muted_ya_tests_sorted.append(test_string)
  206. muted_ya_tests_sorted_debug.append(test_string_debug)
  207. muted_before_count += 1
  208. if test_string not in new_muted_ya_tests:
  209. if test_string not in muted_stable_tests and test_string not in deleted_tests:
  210. new_muted_ya_tests.append(test_string)
  211. new_muted_ya_tests_debug.append(test_string_debug)
  212. if test_string in muted_stable_tests:
  213. unmuted_stable += 1
  214. if test_string in deleted_tests:
  215. unmuted_deleted += 1
  216. unmuted_tests_debug.append(test_string_debug)
  217. muted_ya_tests_sorted = sorted(muted_ya_tests_sorted)
  218. add_lines_to_file(os.path.join(output_path, 'muted_ya_sorted.txt'), muted_ya_tests_sorted)
  219. muted_ya_tests_sorted_debug = sorted(muted_ya_tests_sorted_debug)
  220. add_lines_to_file(os.path.join(output_path, 'muted_ya_sorted_debug.txt'), muted_ya_tests_sorted_debug)
  221. new_muted_ya_tests = sorted(new_muted_ya_tests)
  222. add_lines_to_file(os.path.join(output_path, 'new_muted_ya.txt'), new_muted_ya_tests)
  223. new_muted_ya_tests_debug = sorted(new_muted_ya_tests_debug)
  224. add_lines_to_file(os.path.join(output_path, 'new_muted_ya_debug.txt'), new_muted_ya_tests_debug)
  225. new_muted_ya_tests_with_flaky = sorted(new_muted_ya_tests_with_flaky)
  226. add_lines_to_file(os.path.join(output_path, 'new_muted_ya_with_flaky.txt'), new_muted_ya_tests_with_flaky)
  227. new_muted_ya_tests_with_flaky_debug = sorted(new_muted_ya_tests_with_flaky_debug)
  228. add_lines_to_file(
  229. os.path.join(output_path, 'new_muted_ya_with_flaky_debug.txt'), new_muted_ya_tests_with_flaky_debug
  230. )
  231. unmuted_tests_debug = sorted(unmuted_tests_debug)
  232. add_lines_to_file(os.path.join(output_path, 'unmuted_debug.txt'), unmuted_tests_debug)
  233. logging.info(f"Muted before script: {muted_before_count} tests")
  234. logging.info(f"Muted stable : {len(muted_stable_tests)}")
  235. logging.info(f"Flaky tests : {len(flaky_tests)}")
  236. logging.info(f"Result: Muted without deleted and stable : {len(new_muted_ya_tests)}")
  237. logging.info(f"Result: Muted without deleted and stable, with flaky : {len(new_muted_ya_tests_with_flaky)}")
  238. logging.info(f"Result: Unmuted tests : stable {unmuted_stable} and deleted {unmuted_deleted}")
  239. except (KeyError, TypeError) as e:
  240. logging.error(f"Error processing test data: {e}. Check your query results for valid keys.")
  241. return []
  242. return len(new_muted_ya_tests)
  243. def read_tests_from_file(file_path):
  244. result = []
  245. with open(file_path, "r") as fp:
  246. for line in fp:
  247. line = line.strip()
  248. try:
  249. testsuite, testcase = line.split(" ", maxsplit=1)
  250. result.append({'testsuite': testsuite, 'testcase': testcase, 'full_name': f"{testsuite}/{testcase}"})
  251. except ValueError:
  252. log_print(f"cant parse line: {line!r}")
  253. continue
  254. return result
  255. def create_mute_issues(all_tests, file_path):
  256. base_date = datetime.datetime(1970, 1, 1)
  257. tests_from_file = read_tests_from_file(file_path)
  258. muted_tests_in_issues = get_muted_tests_from_issues()
  259. prepared_tests_by_suite = {}
  260. for test in all_tests:
  261. for test_from_file in tests_from_file:
  262. if test['full_name'] == test_from_file['full_name']:
  263. if test['full_name'] in muted_tests_in_issues:
  264. logging.info(
  265. f"test {test['full_name']} already have issue, {muted_tests_in_issues[test['full_name']][0]['url']}"
  266. )
  267. else:
  268. key = f"{test_from_file['testsuite']}:{test['owner']}"
  269. if not prepared_tests_by_suite.get(key):
  270. prepared_tests_by_suite[key] = []
  271. prepared_tests_by_suite[key].append(
  272. {
  273. 'mute_string': f"{ test.get('suite_folder')} {test.get('test_name')}",
  274. 'test_name': test.get('test_name'),
  275. 'suite_folder': test.get('suite_folder'),
  276. 'full_name': test.get('full_name'),
  277. 'success_rate': test.get('success_rate'),
  278. 'days_in_state': test.get('days_in_state'),
  279. 'date_window': (base_date + datetime.timedelta(days=test.get('date_window'))).date() ,
  280. 'owner': test.get('owner'),
  281. 'state': test.get('state'),
  282. 'summary': test.get('summary'),
  283. 'fail_count': test.get('fail_count'),
  284. 'pass_count': test.get('pass_count'),
  285. 'branch': test.get('branch'),
  286. }
  287. )
  288. results = []
  289. for item in prepared_tests_by_suite:
  290. title, body = generate_github_issue_title_and_body(prepared_tests_by_suite[item])
  291. result = create_and_add_issue_to_project(
  292. title, body, state='Muted', owner=prepared_tests_by_suite[item][0]['owner'].split('/', 1)[1]
  293. )
  294. if not result:
  295. break
  296. else:
  297. results.append(
  298. f"Created issue '{title}' for {prepared_tests_by_suite[item][0]['owner']}, url {result['issue_url']}"
  299. )
  300. print("\n\n")
  301. print("\n".join(results))
  302. def mute_worker(args):
  303. # Simplified Connection
  304. if "CI_YDB_SERVICE_ACCOUNT_KEY_FILE_CREDENTIALS" not in os.environ:
  305. print("Error: Env variable CI_YDB_SERVICE_ACCOUNT_KEY_FILE_CREDENTIALS is missing, skipping")
  306. return 1
  307. else:
  308. # Do not set up 'real' variable from gh workflows because it interfere with ydb tests
  309. # So, set up it locally
  310. os.environ["YDB_SERVICE_ACCOUNT_KEY_FILE_CREDENTIALS"] = os.environ[
  311. "CI_YDB_SERVICE_ACCOUNT_KEY_FILE_CREDENTIALS"
  312. ]
  313. mute_check = YaMuteCheck()
  314. mute_check.load(muted_ya_path)
  315. with ydb.Driver(
  316. endpoint=DATABASE_ENDPOINT,
  317. database=DATABASE_PATH,
  318. credentials=ydb.credentials_from_env_variables(),
  319. ) as driver:
  320. driver.wait(timeout=10, fail_fast=True)
  321. session = ydb.retry_operation_sync(lambda: driver.table_client.session().create())
  322. tc_settings = ydb.TableClientSettings().with_native_date_in_result_sets(enabled=True)
  323. all_tests = execute_query(driver)
  324. if args.mode == 'update_muted_ya':
  325. output_path = args.output_folder
  326. os.makedirs(output_path, exist_ok=True)
  327. apply_and_add_mutes(all_tests, output_path, mute_check)
  328. elif args.mode == 'create_issues':
  329. file_path = args.file_path
  330. create_mute_issues(all_tests, file_path)
  331. if __name__ == "__main__":
  332. parser = argparse.ArgumentParser(description="Add tests to mutes files based on flaky_today condition")
  333. subparsers = parser.add_subparsers(dest='mode', help="Mode to perform")
  334. update_muted_ya_parser = subparsers.add_parser('update_muted_ya', help='create new muted_ya')
  335. update_muted_ya_parser.add_argument('--output_folder', default=repo_path, required=False, help='Output folder.')
  336. create_issues_parser = subparsers.add_parser(
  337. 'create_issues',
  338. help='create issues by muted_ya like files',
  339. )
  340. create_issues_parser.add_argument(
  341. '--file_path', default=f'{repo_path}/mute_update/flaky.txt', required=False, help='file path'
  342. )
  343. args = parser.parse_args()
  344. mute_worker(args)