Skip to content

Commit cc25ca9

Browse files
committed
GNU/CI: use the aggregated-result.json files and move to python
1 parent eb8928a commit cc25ca9

File tree

2 files changed

+231
-118
lines changed

2 files changed

+231
-118
lines changed

.github/workflows/GnuTests.yml

Lines changed: 23 additions & 118 deletions
Original file line numberDiff line numberDiff line change
@@ -349,137 +349,42 @@ jobs:
349349
- name: Compare test failures VS reference
350350
shell: bash
351351
run: |
352-
## Compare test failures VS reference
353-
have_new_failures=""
354-
REF_LOG_FILE='${{ steps.vars.outputs.path_reference }}/test-logs/test-suite.log'
355-
ROOT_REF_LOG_FILE='${{ steps.vars.outputs.path_reference }}/test-logs/test-suite-root.log'
356-
SELINUX_REF_LOG_FILE='${{ steps.vars.outputs.path_reference }}/test-logs/selinux-test-suite.log'
357-
SELINUX_ROOT_REF_LOG_FILE='${{ steps.vars.outputs.path_reference }}/test-logs/selinux-test-suite-root.log'
358-
REF_SUMMARY_FILE='${{ steps.vars.outputs.path_reference }}/test-summary/gnu-result.json'
359-
360-
352+
## Compare test failures VS reference using JSON files
353+
REF_SUMMARY_FILE='${{ steps.vars.outputs.path_reference }}/aggregated-result/aggregated-result.json'
354+
CURRENT_SUMMARY_FILE='${{ steps.vars.outputs.AGGREGATED_SUMMARY_FILE }}'
361355
REPO_DEFAULT_BRANCH='${{ steps.vars.outputs.repo_default_branch }}'
362356
path_UUTILS='${{ steps.vars.outputs.path_UUTILS }}'
363-
# https://github.com/uutils/coreutils/issues/4294
364-
# https://github.com/uutils/coreutils/issues/4295
365-
IGNORE_INTERMITTENT="${path_UUTILS}/.github/workflows/ignore-intermittent.txt"
366357
367-
mkdir -p ${{ steps.vars.outputs.path_reference }}
358+
# Path to ignore file for intermittent issues
359+
IGNORE_INTERMITTENT="${path_UUTILS}/.github/workflows/ignore-intermittent.txt"
368360
361+
# Set up comment directory
369362
COMMENT_DIR="${{ steps.vars.outputs.path_reference }}/comment"
370363
mkdir -p ${COMMENT_DIR}
371364
echo ${{ github.event.number }} > ${COMMENT_DIR}/NR
372365
COMMENT_LOG="${COMMENT_DIR}/result.txt"
373366
374-
# The comment log might be downloaded from a previous run
375-
# We only want the new changes, so remove it if it exists.
376-
rm -f ${COMMENT_LOG}
377-
touch ${COMMENT_LOG}
378-
379-
compare_tests() {
380-
local new_log_file=$1
381-
local ref_log_file=$2
382-
local test_type=$3 # "standard" or "root"
383-
384-
if test -f "${ref_log_file}"; then
385-
echo "Reference ${test_type} test log SHA1/ID: $(sha1sum -- "${ref_log_file}") - ${test_type}"
386-
REF_ERROR=$(sed -n "s/^ERROR: \([[:print:]]\+\).*/\1/p" "${ref_log_file}"| sort)
387-
CURRENT_RUN_ERROR=$(sed -n "s/^ERROR: \([[:print:]]\+\).*/\1/p" "${new_log_file}" | sort)
388-
REF_FAILING=$(sed -n "s/^FAIL: \([[:print:]]\+\).*/\1/p" "${ref_log_file}"| sort)
389-
CURRENT_RUN_FAILING=$(sed -n "s/^FAIL: \([[:print:]]\+\).*/\1/p" "${new_log_file}" | sort)
390-
REF_SKIP=$(sed -n "s/^SKIP: \([[:print:]]\+\).*/\1/p" "${ref_log_file}"| sort)
391-
CURRENT_RUN_SKIP=$(sed -n "s/^SKIP: \([[:print:]]\+\).*/\1/p" "${new_log_file}" | sort)
392-
393-
echo "Detailed information:"
394-
echo "REF_ERROR = ${REF_ERROR}"
395-
echo "CURRENT_RUN_ERROR = ${CURRENT_RUN_ERROR}"
396-
echo "REF_FAILING = ${REF_FAILING}"
397-
echo "CURRENT_RUN_FAILING = ${CURRENT_RUN_FAILING}"
398-
echo "REF_SKIP_PASS = ${REF_SKIP}"
399-
echo "CURRENT_RUN_SKIP = ${CURRENT_RUN_SKIP}"
400-
401-
# Compare failing and error tests
402-
for LINE in ${CURRENT_RUN_FAILING}
403-
do
404-
if ! grep -Fxq ${LINE}<<<"${REF_FAILING}"
405-
then
406-
if ! grep ${LINE} ${IGNORE_INTERMITTENT}
407-
then
408-
MSG="GNU test failed: ${LINE}. ${LINE} is passing on '${REPO_DEFAULT_BRANCH}'. Maybe you have to rebase?"
409-
echo "::error ::$MSG"
410-
echo $MSG >> ${COMMENT_LOG}
411-
have_new_failures="true"
412-
else
413-
MSG="Skip an intermittent issue ${LINE} (fails in this run but passes in the 'main' branch)"
414-
echo "::notice ::$MSG"
415-
echo $MSG >> ${COMMENT_LOG}
416-
echo ""
417-
fi
418-
fi
419-
done
420-
421-
for LINE in ${REF_FAILING}
422-
do
423-
if ! grep -Fxq ${LINE}<<<"${CURRENT_RUN_FAILING}"
424-
then
425-
if ! grep ${LINE} ${IGNORE_INTERMITTENT}
426-
then
427-
MSG="Congrats! The gnu test ${LINE} is no longer failing!"
428-
echo "::notice ::$MSG"
429-
echo $MSG >> ${COMMENT_LOG}
430-
else
431-
MSG="Skipping an intermittent issue ${LINE} (passes in this run but fails in the 'main' branch)"
432-
echo "::notice ::$MSG"
433-
echo $MSG >> ${COMMENT_LOG}
434-
echo ""
435-
fi
436-
fi
437-
done
438-
439-
for LINE in ${CURRENT_RUN_ERROR}
440-
do
441-
if ! grep -Fxq ${LINE}<<<"${REF_ERROR}"
442-
then
443-
MSG="GNU test error: ${LINE}. ${LINE} is passing on '${REPO_DEFAULT_BRANCH}'. Maybe you have to rebase?"
444-
echo "::error ::$MSG"
445-
echo $MSG >> ${COMMENT_LOG}
446-
have_new_failures="true"
447-
fi
448-
done
449-
450-
for LINE in ${REF_ERROR}
451-
do
452-
if ! grep -Fxq ${LINE}<<<"${CURRENT_RUN_ERROR}"
453-
then
454-
MSG="Congrats! The gnu test ${LINE} is no longer ERROR! (might be PASS or FAIL)"
455-
echo "::warning ::$MSG"
456-
echo $MSG >> ${COMMENT_LOG}
457-
fi
458-
done
459-
460-
for LINE in ${REF_SKIP}
461-
do
462-
if ! grep -Fxq ${LINE}<<<"${CURRENT_RUN_SKIP}"
463-
then
464-
MSG="Congrats! The gnu test ${LINE} is no longer SKIP! (might be PASS, ERROR or FAIL)"
465-
echo "::warning ::$MSG"
466-
echo $MSG >> ${COMMENT_LOG}
467-
fi
468-
done
367+
COMPARISON_RESULT=0
368+
if test -f "${CURRENT_SUMMARY_FILE}"; then
369+
if test -f "${REF_SUMMARY_FILE}"; then
370+
echo "Reference summary SHA1/ID: $(sha1sum -- "${REF_SUMMARY_FILE}")"
371+
echo "Current summary SHA1/ID: $(sha1sum -- "${CURRENT_SUMMARY_FILE}")"
372+
373+
python3 ${path_UUTILS}/util/compare_test_results.py \
374+
--ignore-file "${IGNORE_INTERMITTENT}" \
375+
--output "${COMMENT_LOG}" \
376+
"${CURRENT_SUMMARY_FILE}" "${REF_SUMMARY_FILE}"
469377
378+
COMPARISON_RESULT=$?
470379
else
471-
echo "::warning ::Skipping ${test_type} test failure comparison; no prior reference test logs are available."
380+
echo "::warning ::Skipping test comparison; no prior reference summary is available at '${REF_SUMMARY_FILE}'."
472381
fi
473-
}
474-
475-
# Compare standard tests
476-
compare_tests '${{ steps.vars.outputs.path_GNU_tests }}/test-suite.log' "${REF_LOG_FILE}" "standard"
477-
478-
# Compare root tests
479-
compare_tests '${{ steps.vars.outputs.path_GNU_tests }}/test-suite-root.log' "${ROOT_REF_LOG_FILE}" "root"
382+
else
383+
echo "::error ::Failed to find summary of test results (missing '${CURRENT_SUMMARY_FILE}'); failing early"
384+
exit 1
385+
fi
480386
481-
# Set environment variable to indicate whether all failures are intermittent
482-
if [ -n "${have_new_failures}" ]; then
387+
if [ ${COMPARISON_RESULT} -eq 1 ]; then
483388
echo "ONLY_INTERMITTENT=false" >> $GITHUB_ENV
484389
echo "::error ::Found new non-intermittent test failures"
485390
exit 1

util/compare_test_results.py

Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
#!/usr/bin/env python3
2+
"""
3+
Compare GNU test results between current run and reference to identify
4+
regressions and fixes.
5+
6+
7+
Arguments:
8+
CURRENT_JSON Path to the current run's aggregated results JSON file
9+
REFERENCE_JSON Path to the reference (main branch) aggregated
10+
results JSON file
11+
--ignore-file Path to file containing list of tests to ignore
12+
(for intermittent issues)
13+
--output Path to output file for GitHub comment content
14+
"""
15+
16+
import argparse
17+
import json
18+
import os
19+
import sys
20+
21+
22+
def flatten_test_results(results):
23+
"""Convert nested JSON test results to a flat dictionary of test paths to statuses."""
24+
flattened = {}
25+
for util, tests in results.items():
26+
for test_name, status in tests.items():
27+
test_path = f"{util}/{test_name}"
28+
flattened[test_path] = status
29+
return flattened
30+
31+
32+
def load_ignore_list(ignore_file):
33+
"""Load list of tests to ignore from file."""
34+
if not os.path.exists(ignore_file):
35+
return set()
36+
37+
with open(ignore_file, "r") as f:
38+
return {line.strip() for line in f if line.strip() and not line.startswith("#")}
39+
40+
41+
def identify_test_changes(current_flat, reference_flat):
42+
"""
43+
Identify different categories of test changes between current and reference results.
44+
45+
Args:
46+
current_flat (dict): Flattened dictionary of current test results
47+
reference_flat (dict): Flattened dictionary of reference test results
48+
49+
Returns:
50+
tuple: Four lists containing regressions, fixes, newly_skipped, and newly_passing tests
51+
"""
52+
# Find regressions (tests that were passing but now failing)
53+
regressions = []
54+
for test_path, status in current_flat.items():
55+
if status == "FAIL" or status == "ERROR":
56+
if test_path in reference_flat:
57+
if (
58+
reference_flat[test_path] == "PASS"
59+
or reference_flat[test_path] == "SKIP"
60+
):
61+
regressions.append(test_path)
62+
63+
# Find fixes (tests that were failing but now passing)
64+
fixes = []
65+
for test_path, status in reference_flat.items():
66+
if status == "FAIL" or status == "ERROR":
67+
if test_path in current_flat:
68+
if current_flat[test_path] == "PASS":
69+
fixes.append(test_path)
70+
71+
# Find newly skipped tests (were passing, now skipped)
72+
newly_skipped = []
73+
for test_path, status in current_flat.items():
74+
if (
75+
status == "SKIP"
76+
and test_path in reference_flat
77+
and reference_flat[test_path] == "PASS"
78+
):
79+
newly_skipped.append(test_path)
80+
81+
# Find newly passing tests (were skipped, now passing)
82+
newly_passing = []
83+
for test_path, status in current_flat.items():
84+
if (
85+
status == "PASS"
86+
and test_path in reference_flat
87+
and reference_flat[test_path] == "SKIP"
88+
):
89+
newly_passing.append(test_path)
90+
91+
return regressions, fixes, newly_skipped, newly_passing
92+
93+
94+
def main():
95+
parser = argparse.ArgumentParser(
96+
description="Compare GNU test results and identify regressions and fixes"
97+
)
98+
parser.add_argument("current_json", help="Path to current run JSON results")
99+
parser.add_argument("reference_json", help="Path to reference JSON results")
100+
parser.add_argument(
101+
"--ignore-file",
102+
required=True,
103+
help="Path to file with tests to ignore (for intermittent issues)",
104+
)
105+
parser.add_argument("--output", help="Path to output file for GitHub comment")
106+
107+
args = parser.parse_args()
108+
109+
# Load test results
110+
try:
111+
with open(args.current_json, "r") as f:
112+
current_results = json.load(f)
113+
except (FileNotFoundError, json.JSONDecodeError) as e:
114+
sys.stderr.write(f"Error loading current results: {e}\n")
115+
return 1
116+
117+
try:
118+
with open(args.reference_json, "r") as f:
119+
reference_results = json.load(f)
120+
except (FileNotFoundError, json.JSONDecodeError) as e:
121+
sys.stderr.write(f"Error loading reference results: {e}\n")
122+
sys.stderr.write("Skipping comparison as reference is not available.\n")
123+
return 0
124+
125+
# Load ignore list (required)
126+
if not os.path.exists(args.ignore_file):
127+
sys.stderr.write(f"Error: Ignore file {args.ignore_file} does not exist\n")
128+
print(f"::error ::Ignore file {args.ignore_file} does not exist")
129+
return 1
130+
131+
ignore_list = load_ignore_list(args.ignore_file)
132+
print(f"Loaded {len(ignore_list)} tests to ignore from {args.ignore_file}")
133+
134+
# Flatten result structures for easier comparison
135+
current_flat = flatten_test_results(current_results)
136+
reference_flat = flatten_test_results(reference_results)
137+
138+
# Identify different categories of test changes
139+
regressions, fixes, newly_skipped, newly_passing = identify_test_changes(
140+
current_flat, reference_flat
141+
)
142+
143+
# Filter out intermittent issues from regressions
144+
real_regressions = [r for r in regressions if r not in ignore_list]
145+
intermittent_regressions = [r for r in regressions if r in ignore_list]
146+
147+
output_lines = []
148+
have_new_failures = len(real_regressions) > 0
149+
150+
# Print summary stats
151+
print(f"Total tests in current run: {len(current_flat)}")
152+
print(f"Total tests in reference: {len(reference_flat)}")
153+
print(f"New regressions: {len(real_regressions)}")
154+
print(f"Intermittent regressions: {len(intermittent_regressions)}")
155+
print(f"Fixed tests: {len(fixes)}")
156+
print(f"Newly skipped tests: {len(newly_skipped)}")
157+
print(f"Newly passing tests (previously skipped): {len(newly_passing)}")
158+
159+
# Report regressions
160+
if real_regressions:
161+
print("\nREGRESSIONS (non-intermittent failures):", file=sys.stderr)
162+
for test in sorted(real_regressions):
163+
msg = f"GNU test failed: {test}. {test} is passing on 'main'. Maybe you have to rebase?"
164+
print(f"::error ::{msg}", file=sys.stderr)
165+
output_lines.append(msg)
166+
167+
# Report intermittent issues
168+
if intermittent_regressions:
169+
print("\nINTERMITTENT ISSUES (ignored):", file=sys.stderr)
170+
for test in sorted(intermittent_regressions):
171+
msg = f"Skip an intermittent issue {test} (fails in this run but passes in the 'main' branch)"
172+
print(f"::notice ::{msg}", file=sys.stderr)
173+
output_lines.append(msg)
174+
175+
# Report fixes
176+
if fixes:
177+
print("\nFIXED TESTS:", file=sys.stderr)
178+
for test in sorted(fixes):
179+
msg = f"Congrats! The gnu test {test} is no longer failing!"
180+
print(f"::notice ::{msg}", file=sys.stderr)
181+
output_lines.append(msg)
182+
183+
# Report newly skipped and passing tests
184+
if newly_skipped:
185+
print("\nNEWLY SKIPPED TESTS:", file=sys.stderr)
186+
for test in sorted(newly_skipped):
187+
msg = f"Note: The gnu test {test} is now being skipped but was previously passing."
188+
print(f"::warning ::{msg}", file=sys.stderr)
189+
output_lines.append(msg)
190+
191+
if newly_passing:
192+
print("\nNEWLY PASSING TESTS (previously skipped):", file=sys.stderr)
193+
for test in sorted(newly_passing):
194+
msg = f"Congrats! The gnu test {test} is now passing!"
195+
print(f"::notice ::{msg}", file=sys.stderr)
196+
output_lines.append(msg)
197+
198+
if args.output and output_lines:
199+
with open(args.output, "w") as f:
200+
for line in output_lines:
201+
f.write(f"{line}\n")
202+
203+
# Return exit code based on whether we found regressions
204+
return 1 if have_new_failures else 0
205+
206+
207+
if __name__ == "__main__":
208+
sys.exit(main())

0 commit comments

Comments
 (0)