Skip to content

Commit c7c5431

Browse files
committed
overhaul broken tests system, now uses yaml, supports regex
1 parent c549f70 commit c7c5431

File tree

5 files changed

+574
-636
lines changed

5 files changed

+574
-636
lines changed

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: 82 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,81 @@
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+
"message": re.compile(test["message"]) if regex else test["message"],
57+
}
58+
59+
if test.get("not_message"):
60+
rule["not_message"] = (
61+
re.compile(test["not_message"]) if regex else test["not_message"]
62+
)
63+
if test.get("check_types"):
64+
rule["check_types"] = test["check_types"]
65+
66+
if regex:
67+
rule["regex"] = True
68+
compiled_rules["pattern"][re.compile(test["name"])] = rule
69+
else:
70+
compiled_rules["exact"][test["name"]] = rule
71+
72+
return compiled_rules
73+
74+
75+
def test_is_known_fail(test_name, test_logs, known_broken_tests, test_options_string):
76+
matching_rules = []
77+
78+
exact_rule = known_broken_tests["exact"].get(test_name)
79+
if exact_rule:
80+
matching_rules.append(exact_rule)
81+
82+
for name_re, data in known_broken_tests["pattern"].items():
83+
if name_re.fullmatch(test_name):
84+
matching_rules.append(data)
85+
86+
if not matching_rules:
87+
return False
3788

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
89+
def matches_field(field, log, is_regex):
90+
if field is None:
91+
return True
92+
if is_regex:
93+
return bool(field.search(log))
94+
return field in log
95+
96+
for rule_data in matching_rules:
97+
if rule_data.get("check_types") and not any(
98+
ct in test_options_string for ct in rule_data["check_types"]
99+
):
100+
continue # check_types didn't match → skip rule
101+
102+
is_regex = rule_data.get("regex", False)
103+
if matches_field(rule_data.get("not_message"), test_logs, is_regex):
104+
continue # not_message matched → skip rule
105+
if not matches_field(rule_data.get("message"), test_logs, is_regex):
106+
continue # message didn't match → skip rule
107+
108+
return rule_data["reason"]
109+
110+
return False
41111

42112

43113
class FTResultsProcessor:
@@ -75,7 +145,7 @@ def _process_test_output(self):
75145
test_results = []
76146
test_end = True
77147

78-
known_broken_tests = get_broken_tests_list()
148+
known_broken_tests = get_broken_tests_rules()
79149

80150
with open(self.tests_output_file, "r", encoding="utf-8") as test_file:
81151
for line in test_file:
@@ -161,34 +231,16 @@ def _process_test_output(self):
161231
)
162232

163233
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-
)
234+
broken_message = test_is_known_fail(
235+
test[0], test[3], known_broken_tests, test_options_string
236+
)
185237

186238
if broken_message:
187239
broken += 1
188240
failed -= 1
189241
test_results_[-1].set_status(Result.StatusExtended.BROKEN)
190242
test_results_[-1].set_label(Result.Label.BROKEN)
191-
test_results_[-1].info += broken_message
243+
test_results_[-1].info += "\nMarked as broken: " + broken_message
192244

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

ci/jobs/scripts/integration_tests_runner.py

Lines changed: 82 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -406,17 +406,43 @@ def _get_parallel_tests_skip_list(repo_path):
406406
return list(sorted(skip_list_tests))
407407

408408
@staticmethod
409-
def _get_broken_tests_list(repo_path: str) -> dict:
410-
skip_list_file_path = f"{repo_path}/tests/broken_tests.json"
409+
def _get_broken_tests_rules(repo_path: str) -> dict:
410+
broken_tests_file_path = f"{repo_path}/tests/broken_tests.yaml"
411411
if (
412-
not os.path.isfile(skip_list_file_path)
413-
or os.path.getsize(skip_list_file_path) == 0
412+
not os.path.isfile(broken_tests_file_path)
413+
or os.path.getsize(broken_tests_file_path) == 0
414414
):
415-
return {}
415+
raise ValueError(
416+
"There is something wrong with getting broken tests rules: "
417+
f"file '{broken_tests_file_path}' is empty or does not exist."
418+
)
416419

417-
with open(skip_list_file_path, "r", encoding="utf-8") as skip_list_file:
418-
skip_list_tests = json.load(skip_list_file)
419-
return skip_list_tests
420+
with open(broken_tests_file_path, "r", encoding="utf-8") as broken_tests_file:
421+
broken_tests = yaml.safe_load(broken_tests_file)
422+
423+
compiled_rules = {"exact": {}, "pattern": {}}
424+
425+
for test in broken_tests:
426+
regex = test.get("regex") is True
427+
rule = {
428+
"reason": test["reason"],
429+
"message": re.compile(test["message"]) if regex else test["message"],
430+
}
431+
432+
if test.get("not_message"):
433+
rule["not_message"] = (
434+
re.compile(test["not_message"]) if regex else test["not_message"]
435+
)
436+
if test.get("check_types"):
437+
rule["check_types"] = test["check_types"]
438+
439+
if regex:
440+
rule["regex"] = True
441+
compiled_rules["pattern"][re.compile(test["name"])] = rule
442+
else:
443+
compiled_rules["exact"][test["name"]] = rule
444+
445+
return compiled_rules
420446

421447
@staticmethod
422448
def group_test_by_file(tests):
@@ -469,6 +495,43 @@ def get_log_paths(test_name):
469495

470496
broken_tests_log = os.path.join(self.result_path, "broken_tests_handler.log")
471497

498+
def test_is_known_fail(test_name, test_logs):
499+
matching_rules = []
500+
501+
exact_rule = known_broken_tests["exact"].get(test_name)
502+
if exact_rule:
503+
matching_rules.append(exact_rule)
504+
505+
for name_re, data in known_broken_tests["pattern"].items():
506+
if name_re.fullmatch(test_name):
507+
matching_rules.append(data)
508+
509+
if not matching_rules:
510+
return False
511+
512+
def matches_field(field, log, is_regex):
513+
if field is None:
514+
return True
515+
if is_regex:
516+
return bool(field.search(log))
517+
return field in log
518+
519+
for rule_data in matching_rules:
520+
if rule_data.get("check_types") and not any(
521+
ct in context_name for ct in rule_data["check_types"]
522+
):
523+
continue # check_types didn't match → skip rule
524+
525+
is_regex = rule_data.get("regex", False)
526+
if matches_field(rule_data.get("not_message"), test_logs, is_regex):
527+
continue # not_message matched → skip rule
528+
if not matches_field(rule_data.get("message"), test_logs, is_regex):
529+
continue # message didn't match → skip rule
530+
531+
return rule_data["reason"]
532+
533+
return False
534+
472535
with open(broken_tests_log, "a") as log_file:
473536
log_file.write(f"{len(known_broken_tests)} Known broken tests\n")
474537
for status, tests in counters.items():
@@ -479,45 +542,21 @@ def get_log_paths(test_name):
479542
log_file.write(
480543
f"Checking test {failed_test} (status: {fail_status})\n"
481544
)
482-
if failed_test not in known_broken_tests.keys():
483-
log_file.write(
484-
f"Test {failed_test} is not in known broken tests\n"
485-
)
486-
continue
487-
488-
check_types = known_broken_tests[failed_test].get("check_types")
489-
fail_message = known_broken_tests[failed_test].get("message")
490545

491-
if check_types and not any(
492-
check_type in context_name for check_type in check_types
493-
):
546+
# Should only care about the most recent log file
547+
log_path = get_log_paths(failed_test)[0]
548+
test_log = extract_fail_logs(log_path).get(
549+
failed_test.split("::")[-1]
550+
)
551+
known_fail_reason = test_is_known_fail(failed_test, test_log)
552+
if known_fail_reason is not False:
494553
log_file.write(
495-
f"Test {context_name} {failed_test} is only known to be broken for check types {check_types}\n"
554+
f"Test {failed_test} is known to fail: {known_fail_reason}\n"
496555
)
497-
mark_as_broken = False
498-
elif not fail_message:
499-
log_file.write("No fail message specified, marking as broken\n")
500-
mark_as_broken = True
501-
else:
502-
log_file.write(f"Looking for fail message: {fail_message}\n")
503-
mark_as_broken = False
504-
for log_path in get_log_paths(failed_test):
505-
if log_path.endswith(".log"):
506-
log_file.write(f"Checking log file: {log_path}\n")
507-
fail_logs = extract_fail_logs(log_path).get(
508-
failed_test.split("::")[-1]
509-
)
510-
if fail_logs and fail_message in fail_logs:
511-
log_file.write("Found fail message in logs\n")
512-
mark_as_broken = True
513-
break
514-
515-
if mark_as_broken:
516-
log_file.write(f"Moving test to BROKEN state\n")
517556
counters[fail_status].remove(failed_test)
518557
counters["BROKEN"].append(failed_test)
519558
else:
520-
log_file.write("Test not marked as broken\n")
559+
log_file.write(f"Test {failed_test} is not known to fail\n")
521560

522561
for status, tests in counters.items():
523562
log_file.write(f"Total tests in {status} state: {len(tests)}\n")
@@ -796,7 +835,7 @@ def run_flaky_check(self, should_fail=False):
796835
} # type: Dict
797836
tests_times = defaultdict(float) # type: Dict
798837
tests_log_paths = defaultdict(list)
799-
known_broken_tests = self._get_broken_tests_list(self.repo_path)
838+
known_broken_tests = self._get_broken_tests_rules(self.repo_path)
800839
id_counter = 0
801840
for test_to_run in tests_to_run:
802841
tries_num = 1 if should_fail else FLAKY_TRIES_COUNT
@@ -1234,7 +1273,7 @@ def run_normal_check(self):
12341273
tests_times = defaultdict(float)
12351274
tests_log_paths = defaultdict(list)
12361275
items_to_run = list(grouped_tests.items())
1237-
known_broken_tests = self._get_broken_tests_list(self.repo_path)
1276+
known_broken_tests = self._get_broken_tests_rules(self.repo_path)
12381277
logging.info("Total test groups %s", len(items_to_run))
12391278
if self.shuffle_test_groups():
12401279
logging.info("Shuffling test groups")

0 commit comments

Comments
 (0)