-
Notifications
You must be signed in to change notification settings - Fork 301
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Skip-build: true Required-githooks: true Signed-off-by: Ashley Pittman <ashley.m.pittman@intel.com>
- Loading branch information
1 parent
f9649aa
commit 585d4bf
Showing
4 changed files
with
208 additions
and
4 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
name: Jenkins status report | ||
|
||
on: | ||
pull_request: | ||
|
||
jobs: | ||
# Check and report Jenkins test results. Should use the check_suite trigger when stable, and | ||
# test the PR that triggered it obviously. | ||
jenkins_check: | ||
name: Check Jenkins results | ||
if: github.repository == 'daos-stack/daos' | ||
runs-on: [self-hosted, light] | ||
steps: | ||
- uses: actions/checkout@v4 | ||
- name: Run check | ||
run: ./ci/daily_status.py --pr ${{ github.event.pull_request.number }} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,190 @@ | ||
#!/usr/bin/env python | ||
|
||
"""Parse Jenkins build test results""" | ||
|
||
import argparse | ||
import json | ||
import sys | ||
import urllib | ||
from urllib.request import urlopen | ||
|
||
JENKINS_HOME = "https://build.hpdd.intel.com/job/daos-stack" | ||
|
||
|
||
class TestResult: | ||
"""Represents a single Jenkins test result""" | ||
|
||
def __init__(self, data, blocks): | ||
name = data["name"] | ||
if "-" in name: | ||
try: | ||
(num, full) = name.split("-", 1) | ||
test_num = int(num) | ||
if full[-5] == "-": | ||
full = full[:-5] | ||
name = f"{full} ({test_num})" | ||
except ValueError: | ||
pass | ||
self.name = name | ||
self.cname = data["className"] | ||
self.skipped = False | ||
self.passed = False | ||
self.failed = False | ||
assert data["status"] in ("PASSED", "FIXED", "SKIPPED", "FAILED", "REGRESSION") | ||
if data["status"] in ("PASSED", "FIXED"): | ||
self.passed = True | ||
elif data["status"] in ("FAILED", "REGRESSION"): | ||
self.failed = True | ||
elif data["status"] == "SKIPPED": | ||
self.skipped = True | ||
self.data = data | ||
self.blocks = blocks | ||
|
||
def info(self, prefix=""): | ||
"""Return a string describing the test""" | ||
return f"{prefix}{self.cname}\t\t{self.name}" | ||
|
||
def full_info(self): | ||
"""Return a longer string describing the test""" | ||
|
||
tcl = [] | ||
if self.blocks is not None: | ||
tcl.extend(reversed(self.blocks)) | ||
|
||
tcl.append(f"{self.cname}.{self.name}") | ||
details = self.data["errorDetails"] | ||
if details: | ||
return " / ".join(tcl) + "\n" + details.replace("\\n", "\n") | ||
return self.info() | ||
|
||
# Needed for set operations to compare results across sets. | ||
def __eq__(self, other): | ||
return self.name == other.name and self.cname == other.cname | ||
|
||
# Needed to be able to add results to sets. | ||
def __hash__(self): | ||
return hash((self.name, self.cname)) | ||
|
||
def __str__(self): | ||
return self.name | ||
|
||
def __repr__(self): | ||
return f"Test result of {self.cname}" | ||
|
||
|
||
def je_load(job_name="daily-testing", jid=None, what=None, tree=None): | ||
"""Fetch something from Jenkins and return as native type.""" | ||
if jid: | ||
if what: | ||
url = f"{JENKINS_HOME}/job/daos/job/{job_name}/{jid}/{what}/api/json" | ||
else: | ||
url = f"{JENKINS_HOME}/job/daos/job/{job_name}/{jid}/api/json" | ||
else: | ||
url = f"{JENKINS_HOME}/job/daos/job/{job_name}/api/json" | ||
|
||
if tree: | ||
url += f"?tree={tree}" | ||
|
||
with urlopen(url) as f: # nosec | ||
return json.load(f) | ||
|
||
|
||
def show_job(jid, job_name="daily-testing"): | ||
"""Show one job""" | ||
|
||
if not job_name.startswith("PR-"): | ||
jdata = je_load(job_name=job_name, jid=jid, tree="actions[causes]") | ||
if ( | ||
"causes" not in jdata["actions"][0] | ||
or jdata["actions"][0]["causes"][0]["_class"] | ||
!= "hudson.triggers.TimerTrigger$TimerTriggerCause" | ||
): | ||
return None | ||
|
||
try: | ||
jdata = je_load(job_name=job_name, jid=jid, what="testReport") | ||
except urllib.error.HTTPError: | ||
return None | ||
|
||
failed = [] | ||
|
||
assert not jdata["testActions"] | ||
for suite in jdata["suites"]: | ||
for k in suite["cases"]: | ||
tr = TestResult(k, suite["enclosingBlockNames"]) | ||
if not tr.failed: | ||
continue | ||
failed.append(tr) | ||
return failed | ||
|
||
|
||
def main(): | ||
"""Check the results of a PR""" | ||
|
||
parser = argparse.ArgumentParser(description="Check Jenkins test results") | ||
parser.add_argument("--pr", type=int, required=True) | ||
|
||
args = parser.parse_args() | ||
|
||
job_name = f"PR-{args.pr}" | ||
|
||
data = je_load(job_name=job_name) | ||
|
||
lcb = data["lastCompletedBuild"]["number"] | ||
|
||
all_failed = set() | ||
for build in data["builds"]: | ||
jid = build["number"] | ||
if jid > lcb: | ||
print(f"Job {jid} is of {job_name} is still running, skipping") | ||
continue | ||
failed = show_job(job_name=job_name, jid=jid) | ||
if not isinstance(failed, list): | ||
continue | ||
for test in failed: | ||
all_failed.add(test) | ||
break | ||
if not all_failed: | ||
print("No failed tests in PR, returning") | ||
|
||
print("PR had failed tests, checking against landings builds") | ||
|
||
job_name = "daily-testing" | ||
data = je_load(job_name=job_name) | ||
lcb = data["lastCompletedBuild"]["number"] | ||
main_failed = set() | ||
check_count = 0 | ||
for build in data["builds"]: | ||
jid = build["number"] | ||
if jid > lcb: | ||
print(f"Job {jid} is of {job_name} is still running, skipping") | ||
failed = show_job(job_name=job_name, jid=jid) | ||
if not isinstance(failed, list): | ||
continue | ||
for test in failed: | ||
main_failed.add(test) | ||
check_count += 1 | ||
if check_count > 14: | ||
break | ||
|
||
unexplained = all_failed.difference(main_failed) | ||
if not unexplained: | ||
print(f"Stopping checking at {check_count} builds, all failures explained") | ||
break | ||
|
||
ignore = all_failed.intersection(main_failed) | ||
if ignore: | ||
print("Tests which failed in the PR and have also failed in landings builds.") | ||
for test in ignore: | ||
print(test.full_info()) | ||
|
||
new = all_failed.difference(main_failed) | ||
if new: | ||
print("Tests which only failed in the PR") | ||
for test in new: | ||
print(test.full_info()) | ||
sys.exit(1) | ||
|
||
|
||
if __name__ == "__main__": | ||
main() |