55from itertools import combinations
66import json
77from datetime import datetime
8+ from functools import lru_cache
89
910import pandas as pd
1011from jinja2 import Environment , FileSystemLoader
@@ -124,6 +125,7 @@ def get_pr_info_from_number(pr_number: str) -> dict:
124125 return response .json ()
125126
126127
128+ @lru_cache
127129def get_run_details (run_url : str ) -> dict :
128130 """
129131 Fetch run details for a given run URL.
@@ -151,28 +153,50 @@ def get_checks_fails(client: Client, job_url: str):
151153 Get tests that did not succeed for the given job URL.
152154 Exclude checks that have status 'error' as they are counted in get_checks_errors.
153155 """
154- columns = "check_status as job_status, check_name as job_name, test_status, test_name, report_url as results_link"
155- query = f"""SELECT { columns } FROM `gh-data`.checks
156- WHERE task_url LIKE '{ job_url } %'
157- AND test_status IN ('FAIL', 'ERROR')
158- AND check_status!='error'
159- ORDER BY check_name, test_name
160- """
156+ query = f"""SELECT job_status, job_name, status as test_status, test_name, results_link
157+ FROM (
158+ SELECT
159+ argMax(check_status, check_start_time) as job_status,
160+ check_name as job_name,
161+ argMax(test_status, check_start_time) as status,
162+ test_name,
163+ report_url as results_link,
164+ task_url
165+ FROM `gh-data`.checks
166+ GROUP BY check_name, test_name, report_url, task_url
167+ )
168+ WHERE task_url LIKE '{ job_url } %'
169+ AND test_status IN ('FAIL', 'ERROR')
170+ AND job_status!='error'
171+ ORDER BY job_name, test_name
172+ """
161173 return client .query_dataframe (query )
162174
163175
164176def get_checks_known_fails (client : Client , job_url : str , known_fails : dict ):
165177 """
166178 Get tests that are known to fail for the given job URL.
167179 """
168- assert len (known_fails ) > 0 , "cannot query the database with empty known fails"
169- columns = "check_status as job_status, check_name as job_name, test_status, test_name, report_url as results_link"
170- query = f"""SELECT { columns } FROM `gh-data`.checks
171- WHERE task_url LIKE '{ job_url } %'
172- AND test_status='BROKEN'
173- AND test_name IN ({ ',' .join (f"'{ test } '" for test in known_fails .keys ())} )
174- ORDER BY test_name, check_name
175- """
180+ if len (known_fails ) == 0 :
181+ return pd .DataFrame ()
182+
183+ query = f"""SELECT job_status, job_name, status as test_status, test_name, results_link
184+ FROM (
185+ SELECT
186+ argMax(check_status, check_start_time) as job_status,
187+ check_name as job_name,
188+ argMax(test_status, check_start_time) as status,
189+ test_name,
190+ report_url as results_link,
191+ task_url
192+ FROM `gh-data`.checks
193+ GROUP BY check_name, test_name, report_url, task_url
194+ )
195+ WHERE task_url LIKE '{ job_url } %'
196+ AND test_status='BROKEN'
197+ AND test_name IN ({ ',' .join (f"'{ test } '" for test in known_fails .keys ())} )
198+ ORDER BY job_name, test_name
199+ """
176200
177201 df = client .query_dataframe (query )
178202
@@ -193,12 +217,22 @@ def get_checks_errors(client: Client, job_url: str):
193217 """
194218 Get checks that have status 'error' for the given job URL.
195219 """
196- columns = "check_status as job_status, check_name as job_name, test_status, test_name, report_url as results_link"
197- query = f"""SELECT { columns } FROM `gh-data`.checks
198- WHERE task_url LIKE '{ job_url } %'
199- AND check_status=='error'
200- ORDER BY check_name, test_name
201- """
220+ query = f"""SELECT job_status, job_name, status as test_status, test_name, results_link
221+ FROM (
222+ SELECT
223+ argMax(check_status, check_start_time) as job_status,
224+ check_name as job_name,
225+ argMax(test_status, check_start_time) as status,
226+ test_name,
227+ report_url as results_link,
228+ task_url
229+ FROM `gh-data`.checks
230+ GROUP BY check_name, test_name, report_url, task_url
231+ )
232+ WHERE task_url LIKE '{ job_url } %'
233+ AND job_status=='error'
234+ ORDER BY job_name, test_name
235+ """
202236 return client .query_dataframe (query )
203237
204238
@@ -231,14 +265,14 @@ def get_regression_fails(client: Client, job_url: str):
231265 architecture as arch,
232266 test_name,
233267 argMax(result, start_time) AS status,
234- job_url,
235268 job_name,
236- report_url as results_link
269+ report_url as results_link,
270+ job_url
237271 FROM `gh-data`.clickhouse_regression_results
238272 GROUP BY architecture, test_name, job_url, job_name, report_url
239273 ORDER BY length(test_name) DESC
240274 )
241- WHERE job_url= '{ job_url } '
275+ WHERE job_url LIKE '{ job_url } % '
242276 AND status IN ('Fail', 'Error')
243277 """
244278 df = client .query_dataframe (query )
@@ -247,6 +281,99 @@ def get_regression_fails(client: Client, job_url: str):
247281 return df
248282
249283
284+ def get_new_fails_this_pr (
285+ client : Client ,
286+ pr_info : dict ,
287+ checks_fails : pd .DataFrame ,
288+ regression_fails : pd .DataFrame ,
289+ ):
290+ """
291+ Get tests that failed in the PR but passed in the base branch.
292+ Compares both checks and regression test results.
293+ """
294+ base_sha = pr_info .get ("base" , {}).get ("sha" )
295+ if not base_sha :
296+ raise Exception ("No base SHA found for PR" )
297+
298+ # Modify tables to have the same columns
299+ if len (checks_fails ) > 0 :
300+ checks_fails = checks_fails .copy ().drop (columns = ["job_status" ])
301+ if len (regression_fails ) > 0 :
302+ regression_fails = regression_fails .copy ()
303+ regression_fails ["job_name" ] = regression_fails .apply (
304+ lambda row : f"{ row ['arch' ]} { row ['job_name' ]} " .strip (), axis = 1
305+ )
306+ regression_fails ["test_status" ] = regression_fails ["status" ]
307+
308+ # Combine both types of fails and select only desired columns
309+ desired_columns = ["job_name" , "test_name" , "test_status" , "results_link" ]
310+ all_pr_fails = pd .concat ([checks_fails , regression_fails ], ignore_index = True )[
311+ desired_columns
312+ ]
313+ if len (all_pr_fails ) == 0 :
314+ return pd .DataFrame ()
315+
316+ # Get all checks from the base branch that didn't fail
317+ base_checks_query = f"""SELECT job_name, status as test_status, test_name, results_link
318+ FROM (
319+ SELECT
320+ check_name as job_name,
321+ argMax(test_status, check_start_time) as status,
322+ test_name,
323+ report_url as results_link,
324+ task_url
325+ FROM `gh-data`.checks
326+ WHERE commit_sha='{ base_sha } '
327+ GROUP BY check_name, test_name, report_url, task_url
328+ )
329+ WHERE test_status NOT IN ('FAIL', 'ERROR')
330+ ORDER BY job_name, test_name
331+ """
332+ base_checks = client .query_dataframe (base_checks_query )
333+
334+ # Get regression results from base branch that didn't fail
335+ base_regression_query = f"""SELECT arch, job_name, status, test_name, results_link
336+ FROM (
337+ SELECT
338+ architecture as arch,
339+ test_name,
340+ argMax(result, start_time) AS status,
341+ job_url,
342+ job_name,
343+ report_url as results_link
344+ FROM `gh-data`.clickhouse_regression_results
345+ WHERE results_link LIKE'%/{ base_sha } /%'
346+ GROUP BY architecture, test_name, job_url, job_name, report_url
347+ ORDER BY length(test_name) DESC
348+ )
349+ WHERE status NOT IN ('Fail', 'Error')
350+ """
351+ base_regression = client .query_dataframe (base_regression_query )
352+ if len (base_regression ) > 0 :
353+ base_regression ["job_name" ] = base_regression .apply (
354+ lambda row : f"{ row ['arch' ]} { row ['job_name' ]} " .strip (), axis = 1
355+ )
356+ base_regression ["test_status" ] = base_regression ["status" ]
357+ base_regression = base_regression .drop (columns = ["arch" , "status" ])
358+
359+ # Combine base results
360+ base_results = pd .concat ([base_checks , base_regression ], ignore_index = True )
361+
362+ # Find tests that failed in PR but passed in base
363+ pr_failed_tests = set (zip (all_pr_fails ["job_name" ], all_pr_fails ["test_name" ]))
364+ base_passed_tests = set (zip (base_results ["job_name" ], base_results ["test_name" ]))
365+
366+ new_fails = pr_failed_tests .intersection (base_passed_tests )
367+
368+ # Filter PR results to only include new fails
369+ mask = all_pr_fails .apply (
370+ lambda row : (row ["job_name" ], row ["test_name" ]) in new_fails , axis = 1
371+ )
372+ new_fails_df = all_pr_fails [mask ]
373+
374+ return new_fails_df
375+
376+
250377def get_cves (pr_number , commit_sha ):
251378 """
252379 Fetch Grype results from S3.
@@ -304,15 +431,15 @@ def get_cves(pr_number, commit_sha):
304431def url_to_html_link (url : str ) -> str :
305432 if not url :
306433 return ""
307- text = url .split ("/" )[- 1 ]
434+ text = url .split ("/" )[- 1 ]. replace ( "__" , "_" )
308435 if not text :
309436 text = "results"
310437 return f'<a href="{ url } ">{ text } </a>'
311438
312439
313440def format_test_name_for_linewrap (text : str ) -> str :
314441 """Tweak the test name to improve line wrapping."""
315- return text . replace ( ".py::" , "/" )
442+ return f'<span style="line-break: anywhere;"> { text } </span>'
316443
317444
318445def format_test_status (text : str ) -> str :
@@ -400,6 +527,7 @@ def main():
400527 "job_statuses" : get_commit_statuses (args .commit_sha ),
401528 "checks_fails" : get_checks_fails (db_client , args .actions_run_url ),
402529 "checks_known_fails" : [],
530+ "pr_new_fails" : [],
403531 "checks_errors" : get_checks_errors (db_client , args .actions_run_url ),
404532 "regression_fails" : get_regression_fails (db_client , args .actions_run_url ),
405533 "docker_images_cves" : (
@@ -427,13 +555,21 @@ def main():
427555 )
428556
429557 if args .pr_number == 0 :
430- pr_info_html = "Release"
558+ run_details = get_run_details (args .actions_run_url )
559+ branch_name = run_details .get ("head_branch" , "unknown branch" )
560+ pr_info_html = f"Release ({ branch_name } )"
431561 else :
432562 try :
433563 pr_info = get_pr_info_from_number (args .pr_number )
434564 pr_info_html = f"""<a href="https://github.com/{ GITHUB_REPO } /pull/{ pr_info ["number" ]} ">
435565 #{ pr_info .get ("number" )} ({ pr_info .get ("base" , {}).get ('ref' )} <- { pr_info .get ("head" , {}).get ('ref' )} ) { pr_info .get ("title" )}
436566 </a>"""
567+ fail_results ["pr_new_fails" ] = get_new_fails_this_pr (
568+ db_client ,
569+ pr_info ,
570+ fail_results ["checks_fails" ],
571+ fail_results ["regression_fails" ],
572+ )
437573 except Exception as e :
438574 pr_info_html = e
439575
@@ -450,9 +586,12 @@ def main():
450586 context = {
451587 "title" : "ClickHouse® CI Workflow Run Report" ,
452588 "github_repo" : GITHUB_REPO ,
589+ "s3_bucket" : S3_BUCKET ,
453590 "pr_info_html" : pr_info_html ,
591+ "pr_number" : args .pr_number ,
454592 "workflow_id" : args .actions_run_url .split ("/" )[- 1 ],
455593 "commit_sha" : args .commit_sha ,
594+ "base_sha" : "" if args .pr_number == 0 else pr_info .get ("base" , {}).get ("sha" ),
456595 "date" : f"{ datetime .utcnow ().strftime ('%Y-%m-%d %H:%M:%S' )} UTC" ,
457596 "is_preview" : args .mark_preview ,
458597 "counts" : {
@@ -466,6 +605,7 @@ def main():
466605 if not args .known_fails
467606 else len (fail_results ["checks_known_fails" ])
468607 ),
608+ "pr_new_fails" : len (fail_results ["pr_new_fails" ]),
469609 },
470610 "ci_jobs_status_html" : format_results_as_html_table (
471611 fail_results ["job_statuses" ]
@@ -487,6 +627,7 @@ def main():
487627 if not args .known_fails
488628 else format_results_as_html_table (fail_results ["checks_known_fails" ])
489629 ),
630+ "new_fails_html" : format_results_as_html_table (fail_results ["pr_new_fails" ]),
490631 }
491632
492633 # Render the template with the context
0 commit comments