Skip to content

Commit b00c771

Browse files
committed
overhaul broken tests system, now uses yaml, supports regex
update report to handle new known fails format allow message to be optional again fix broken tests report when there are no fails fixes and debug logging more debugging more debugging and fix not_message handling more debug and fix report fix broken tests report when check_types unspecified support opening logs with invalid chars in extract_fail_logs debugging missing integration tests logs fix get_log_paths returning compressed files use regex to match sanitizer timeouts
1 parent 1a155d2 commit b00c771

File tree

8 files changed

+480
-644
lines changed

8 files changed

+480
-644
lines changed

.github/actions/create_workflow_report/action.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ runs:
3030
pip install clickhouse-driver==0.2.8 numpy==1.26.4 pandas==2.0.3 jinja2==3.1.5
3131
3232
CMD="python3 .github/actions/create_workflow_report/create_workflow_report.py"
33-
ARGS="--actions-run-url $ACTIONS_RUN_URL --known-fails tests/broken_tests.json --cves --pr-number $PR_NUMBER"
33+
ARGS="--actions-run-url $ACTIONS_RUN_URL --known-fails tests/broken_tests.yaml --cves --pr-number $PR_NUMBER"
3434
3535
set +e -x
3636
if [[ "$FINAL" == "false" ]]; then

.github/actions/create_workflow_report/create_workflow_report.py

Lines changed: 70 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,16 @@
88
from functools import lru_cache
99
from glob import glob
1010
import urllib.parse
11+
import re
1112

1213
import pandas as pd
1314
from jinja2 import Environment, FileSystemLoader
1415
import requests
1516
from clickhouse_driver import Client
1617
import boto3
1718
from botocore.exceptions import NoCredentialsError
19+
import yaml
20+
1821

1922
DATABASE_HOST_VAR = "CHECKS_DATABASE_HOST"
2023
DATABASE_USER_VAR = "CLICKHOUSE_TEST_STAT_LOGIN"
@@ -166,6 +169,59 @@ def get_checks_fails(client: Client, commit_sha: str, branch_name: str):
166169
return client.query_dataframe(query)
167170

168171

172+
def get_broken_tests_rules(broken_tests_file_path):
173+
with open(broken_tests_file_path, "r", encoding="utf-8") as broken_tests_file:
174+
broken_tests = yaml.safe_load(broken_tests_file)
175+
176+
compiled_rules = {"exact": {}, "pattern": {}}
177+
178+
for test in broken_tests:
179+
regex = test.get("regex") is True
180+
rule = {
181+
"reason": test["reason"],
182+
}
183+
184+
if test.get("check_types"):
185+
rule["check_types"] = test["check_types"]
186+
187+
if regex:
188+
rule["regex"] = True
189+
compiled_rules["pattern"][re.compile(test["name"])] = rule
190+
else:
191+
compiled_rules["exact"][test["name"]] = rule
192+
193+
return compiled_rules
194+
195+
196+
def get_known_fail_reason(test_name: str, check_name: str, known_fails: dict):
197+
"""
198+
Returns the reason why a test is known to fail based on its name and build context.
199+
200+
- Exact-name rules are checked first.
201+
- Pattern-name rules are checked next (first match wins).
202+
- Message/not_message conditions are ignored.
203+
"""
204+
# 1. Exact-name rules
205+
rule_data = known_fails["exact"].get(test_name)
206+
if rule_data:
207+
check_types = rule_data.get("check_types", [])
208+
if not check_types or any(
209+
check_type in check_name for check_type in check_types
210+
):
211+
return rule_data["reason"]
212+
213+
# 2. Pattern-name rules
214+
for name_re, rule_data in known_fails["pattern"].items():
215+
if name_re.fullmatch(test_name):
216+
check_types = rule_data.get("check_types", [])
217+
if not check_types or any(
218+
check_type in check_name for check_type in check_types
219+
):
220+
return rule_data["reason"]
221+
222+
return "No reason given"
223+
224+
169225
def get_checks_known_fails(
170226
client: Client, commit_sha: str, branch_name: str, known_fails: dict
171227
):
@@ -189,19 +245,22 @@ def get_checks_known_fails(
189245
GROUP BY check_name, test_name, report_url, task_url
190246
)
191247
WHERE test_status='BROKEN'
192-
AND test_name IN ({','.join(f"'{test}'" for test in known_fails.keys())})
193248
ORDER BY job_name, test_name
194249
"""
195250

196251
df = client.query_dataframe(query)
197252

253+
if df.shape[0] == 0:
254+
return df
255+
198256
df.insert(
199257
len(df.columns) - 1,
200258
"reason",
201-
df["test_name"]
202-
.astype(str)
203-
.apply(
204-
lambda test_name: known_fails[test_name].get("reason", "No reason given")
259+
df.apply(
260+
lambda row: get_known_fail_reason(
261+
row["test_name"], row["job_name"], known_fails
262+
),
263+
axis=1,
205264
),
206265
)
207266

@@ -654,7 +713,7 @@ def create_workflow_report(
654713
pr_number: int = None,
655714
commit_sha: str = None,
656715
no_upload: bool = False,
657-
known_fails: str = None,
716+
known_fails_file_path: str = None,
658717
check_cves: bool = False,
659718
mark_preview: bool = False,
660719
) -> str:
@@ -710,15 +769,12 @@ def create_workflow_report(
710769
# This might occur when run in preview mode.
711770
cves_not_checked = not check_cves or fail_results["docker_images_cves"] is ...
712771

713-
if known_fails:
714-
if not os.path.exists(known_fails):
715-
print(f"Known fails file {known_fails} not found.")
716-
exit(1)
717-
718-
with open(known_fails) as f:
719-
known_fails = json.load(f)
772+
if known_fails_file_path:
773+
if not os.path.exists(known_fails_file_path):
774+
print(f"WARNING:Known fails file {known_fails_file_path} not found.")
775+
else:
776+
known_fails = get_broken_tests_rules(known_fails_file_path)
720777

721-
if known_fails:
722778
fail_results["checks_known_fails"] = get_checks_known_fails(
723779
db_client, commit_sha, branch_name, known_fails
724780
)
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
#!/bin/bash
22
# This script is for generating preview reports when invoked as a post-hook from a praktika job
33
pip install clickhouse-driver==0.2.8 numpy==1.26.4 pandas==2.0.3 jinja2==3.1.5
4-
ARGS="--mark-preview --known-fails tests/broken_tests.json --cves --actions-run-url $GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID --pr-number $PR_NUMBER"
4+
ARGS="--mark-preview --known-fails tests/broken_tests.yaml --cves --actions-run-url $GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID --pr-number $PR_NUMBER"
55
CMD="python3 .github/actions/create_workflow_report/create_workflow_report.py"
66
$CMD $ARGS
77

ci/defs/job_configs.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@
3939
"./tests/config",
4040
"./tests/*.txt",
4141
"./ci/docker/stateless-test",
42-
"./tests/broken_tests.json",
42+
"./tests/broken_tests.yaml",
4343
],
4444
),
4545
result_name_for_cidb="Tests",
@@ -686,7 +686,7 @@ class JobConfigs:
686686
"./ci/jobs/scripts/integration_tests_runner.py",
687687
"./tests/integration/",
688688
"./ci/docker/integration",
689-
"./tests/broken_tests.json",
689+
"./tests/broken_tests.yaml",
690690
],
691691
),
692692
).parametrize(
@@ -710,7 +710,7 @@ class JobConfigs:
710710
"./ci/jobs/scripts/integration_tests_runner.py",
711711
"./tests/integration/",
712712
"./ci/docker/integration",
713-
"./tests/broken_tests.json",
713+
"./tests/broken_tests.yaml",
714714
],
715715
),
716716
).parametrize(
@@ -752,7 +752,7 @@ class JobConfigs:
752752
"./ci/jobs/scripts/integration_tests_runner.py",
753753
"./tests/integration/",
754754
"./ci/docker/integration",
755-
"./tests/broken_tests.json",
755+
"./tests/broken_tests.yaml",
756756
],
757757
),
758758
allow_merge_on_failure=True,
@@ -777,7 +777,7 @@ class JobConfigs:
777777
"./ci/jobs/scripts/integration_tests_runner.py",
778778
"./tests/integration/",
779779
"./ci/docker/integration",
780-
"./tests/broken_tests.json",
780+
"./tests/broken_tests.yaml",
781781
],
782782
),
783783
requires=[ArtifactNames.CH_AMD_ASAN],

ci/jobs/scripts/functional_tests_results.py

Lines changed: 103 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@
33
import os
44
import traceback
55
from typing import List
6+
import re
7+
8+
import yaml
69

710
from praktika.result import Result
811

@@ -30,14 +33,99 @@
3033
# out.writerow(status)
3134

3235

33-
def get_broken_tests_list() -> dict:
34-
file_path = "tests/broken_tests.json"
35-
if not os.path.isfile(file_path) or os.path.getsize(file_path) == 0:
36-
return {}
36+
def get_broken_tests_rules() -> dict:
37+
broken_tests_file_path = "tests/broken_tests.yaml"
38+
if (
39+
not os.path.isfile(broken_tests_file_path)
40+
or os.path.getsize(broken_tests_file_path) == 0
41+
):
42+
raise ValueError(
43+
"There is something wrong with getting broken tests rules: "
44+
f"file '{broken_tests_file_path}' is empty or does not exist."
45+
)
46+
47+
with open(broken_tests_file_path, "r", encoding="utf-8") as broken_tests_file:
48+
broken_tests = yaml.safe_load(broken_tests_file)
49+
50+
compiled_rules = {"exact": {}, "pattern": {}}
51+
52+
for test in broken_tests:
53+
regex = test.get("regex") is True
54+
rule = {
55+
"reason": test["reason"],
56+
}
57+
58+
if test.get("message"):
59+
rule["message"] = re.compile(test["message"]) if regex else test["message"]
60+
61+
if test.get("not_message"):
62+
rule["not_message"] = (
63+
re.compile(test["not_message"]) if regex else test["not_message"]
64+
)
65+
if test.get("check_types"):
66+
rule["check_types"] = test["check_types"]
67+
68+
if regex:
69+
rule["regex"] = True
70+
compiled_rules["pattern"][re.compile(test["name"])] = rule
71+
else:
72+
compiled_rules["exact"][test["name"]] = rule
73+
74+
print(
75+
f"INFO: Compiled {len(compiled_rules['exact'])} exact rules and {len(compiled_rules['pattern'])} pattern rules"
76+
)
77+
78+
return compiled_rules
79+
80+
81+
def test_is_known_fail(test_name, test_logs, known_broken_tests, test_options_string):
82+
matching_rules = []
83+
84+
print(f"Checking known broken tests for failed test: {test_name}")
85+
print("Potential matching rules:")
86+
exact_rule = known_broken_tests["exact"].get(test_name)
87+
if exact_rule:
88+
print(f"{test_name} - {exact_rule}")
89+
matching_rules.append(exact_rule)
90+
91+
for name_re, data in known_broken_tests["pattern"].items():
92+
if name_re.fullmatch(test_name):
93+
print(f"{name_re} - {data}")
94+
matching_rules.append(data)
95+
96+
if not matching_rules:
97+
return False
98+
99+
def matches_substring(substring, log, is_regex):
100+
if log is None:
101+
return False
102+
if is_regex:
103+
return bool(substring.search(log))
104+
return substring in log
105+
106+
for rule_data in matching_rules:
107+
if rule_data.get("check_types") and not any(
108+
ct in test_options_string for ct in rule_data["check_types"]
109+
):
110+
print(
111+
f"Check types didn't match: '{rule_data['check_types']}' not in '{test_options_string}'"
112+
)
113+
continue # check_types didn't match → skip rule
37114

38-
with open(file_path, "r", encoding="utf-8") as skip_list_file:
39-
skip_list_tests = json.load(skip_list_file)
40-
return skip_list_tests
115+
is_regex = rule_data.get("regex", False)
116+
not_message = rule_data.get("not_message")
117+
if not_message and matches_substring(not_message, test_logs, is_regex):
118+
print(f"Skip rule: Not message matched: '{rule_data['not_message']}'")
119+
continue # not_message matched → skip rule
120+
message = rule_data.get("message")
121+
if message and not matches_substring(message, test_logs, is_regex):
122+
print(f"Skip rule: Message didn't match: '{rule_data['message']}'")
123+
continue
124+
125+
print(f"Test {test_name} matched rule: {rule_data}")
126+
return rule_data["reason"]
127+
128+
return False
41129

42130

43131
class FTResultsProcessor:
@@ -75,7 +163,7 @@ def _process_test_output(self):
75163
test_results = []
76164
test_end = True
77165

78-
known_broken_tests = get_broken_tests_list()
166+
known_broken_tests = get_broken_tests_rules()
79167

80168
with open(self.tests_output_file, "r", encoding="utf-8") as test_file:
81169
for line in test_file:
@@ -161,34 +249,19 @@ def _process_test_output(self):
161249
)
162250

163251
if test[1] == "FAIL":
164-
broken_message = None
165-
if test[0] in known_broken_tests.keys():
166-
message = known_broken_tests[test[0]].get("message")
167-
check_types = known_broken_tests[test[0]].get("check_types")
168-
if check_types and not any(
169-
check_type in test_options_string
170-
for check_type in check_types
171-
):
172-
broken_message = None
173-
elif message:
174-
if message in test_results_[-1].info:
175-
broken_message = (
176-
f"\nMarked as broken, matched message: '{message}'"
177-
)
178-
else:
179-
broken_message = f"\nMarked as broken, no message specified"
180-
181-
if broken_message and check_types:
182-
broken_message += (
183-
f", matched one or more check types {check_types}"
184-
)
252+
broken_message = test_is_known_fail(
253+
test[0],
254+
test_results_[-1].info,
255+
known_broken_tests,
256+
test_options_string,
257+
)
185258

186259
if broken_message:
187260
broken += 1
188261
failed -= 1
189262
test_results_[-1].set_status(Result.StatusExtended.BROKEN)
190263
test_results_[-1].set_label(Result.Label.BROKEN)
191-
test_results_[-1].info += broken_message
264+
test_results_[-1].info += "\nMarked as broken: " + broken_message
192265

193266
except Exception as e:
194267
print(f"ERROR: Failed to parse test results: {test}")

0 commit comments

Comments
 (0)