Skip to content

Commit

Permalink
Add testcase state in junit parser
Browse files Browse the repository at this point in the history
junit parser get the previous junit matching some criteria.
If testcases are added or are removed, the parser didn't show them in the ui.
This patch add a state for the testcase that discribe the state change.

It introduces 5 states:

 * RECOVERED
 * REGRESSED
 * UNCHANGED
 * REMOVED
 * ADDED

Change-Id: Ia5e1303e668fa86669181c4f9cd6dcc9f1f990b5
  • Loading branch information
rh-gvincent committed Nov 14, 2024
1 parent f5ea0c9 commit 0b67e9a
Show file tree
Hide file tree
Showing 5 changed files with 267 additions and 89 deletions.
37 changes: 22 additions & 15 deletions dci/api/v1/files.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,8 +77,7 @@ def get_previous_job_in_topic(job):
return None


def _get_previous_jsonunit(job, filename):
prev_job = get_previous_job_in_topic(job)
def _get_previous_testsuites(prev_job, filename):
if prev_job is None:
return None
query = flask.g.session.query(models2.TestsResult).filter(
Expand All @@ -93,14 +92,15 @@ def _get_previous_jsonunit(job, filename):
return None
test_file = base.get_resource_orm(models2.File, res.file_id)
file_descriptor = get_file_descriptor(test_file)
return junit.parse_junit(file_descriptor)
return junit.get_testsuites_from_junit(file_descriptor)


def _process_junit_file(values, junit_file, job):
jsonunit = junit.parse_junit(junit_file)
previous_jsonunit = _get_previous_jsonunit(job, values["name"])
testsuites = junit.add_regressions_and_successfix_to_tests(
previous_jsonunit, jsonunit
def _calculate_and_save_test_results(values, junit_file, job):
prev_job = get_previous_job_in_topic(job)
previous_testsuites = _get_previous_testsuites(prev_job, values["name"])
testsuites = junit.get_testsuites_from_junit(junit_file)
testsuites = junit.update_testsuites_with_testcase_changes(
previous_testsuites, testsuites
)
tests_results = junit.calculate_test_results(testsuites)

Expand Down Expand Up @@ -199,7 +199,7 @@ def create_files(user):
if new_file["mime"] == "application/junit":
try:
_, junit_file = store.get("files", file_path)
_process_junit_file(values, junit_file, job)
_calculate_and_save_test_results(values, junit_file, job)
except xml.etree.ElementTree.ParseError as xmlerror:
raise dci_exc.DCIException(message="Invalid XML: " + xmlerror.msg)

Expand Down Expand Up @@ -370,14 +370,21 @@ def get_junit_file(user, file_id):
):
raise dci_exc.Unauthorized()
junit_file = get_file_descriptor(file)
jsonunit = junit.parse_junit(junit_file)
testsuites = junit.get_testsuites_from_junit(junit_file)
job = base.get_resource_orm(models2.Job, file.job_id)
previous_jsonunit = _get_previous_jsonunit(job, file.name)
testsuites = junit.add_regressions_and_successfix_to_tests(
previous_jsonunit, jsonunit
)
prev_job = get_previous_job_in_topic(job)
previous_testsuites = _get_previous_testsuites(prev_job, file.name)
previous_job_info = {"id": prev_job.id, "name": prev_job.name} if prev_job else None
return flask.Response(
json.dumps({"testsuites": testsuites}),
json.dumps(
{
"job": {"id": job.id, "name": job.name},
"previous_job": previous_job_info,
"testsuites": junit.update_testsuites_with_testcase_changes(
previous_testsuites, testsuites
),
}
),
200,
content_type="application/json",
)
100 changes: 70 additions & 30 deletions dci/api/v1/junit.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ def parse_testsuite(testsuite_xml):
return testsuite


def parse_junit(file_descriptor):
def get_testsuites_from_junit(file_descriptor):
try:
testsuites = []
nb_of_testsuites = 0
Expand All @@ -135,36 +135,76 @@ def _get_unique_testcase_key(testsuite, testcase):
return "%s:%s:%s" % (testsuite["name"], testcase["classname"], testcase["name"])


def add_regressions_and_successfix_to_tests(testsuites1, testsuites2):
testcases_map = {}
if testsuites1:
for testsuite in testsuites1:
for testcase in testsuite["testcases"]:
testcase_key = _get_unique_testcase_key(testsuite, testcase)
testcases_map[testcase_key] = testcase

def _compare_testsuites(testsuite1, testsuite2):
testcases1_map = {}
if testsuite1:
for testcase in testsuite1["testcases"]:
testcase_key = _get_unique_testcase_key(testsuite1, testcase)
testcases1_map[testcase_key] = testcase

testcases2_map = {}
for testcase in testsuite2["testcases"]:
testcase_key = _get_unique_testcase_key(testsuite2, testcase)
testcases2_map[testcase_key] = testcase

testcases1_keys = set(testcases1_map.keys())
testcases2_keys = set(testcases2_map.keys())

testcases = []
removed_testcases = testcases1_keys - testcases2_keys
for removed_testcase_key in removed_testcases:
testcase = testcases1_map[removed_testcase_key]
testcase["successfix"] = False # tobedeleted
testcase["regression"] = False # tobedeleted
testcase["state"] = "REMOVED"
testcases.append(testcase)

added_testcases = testcases2_keys - testcases1_keys
for added_testcase_key in added_testcases:
testcase = testcases2_map[added_testcase_key]
testcase["successfix"] = False # tobedeleted
testcase["regression"] = False # tobedeleted
testcase["state"] = "ADDED"
testcases.append(testcase)

successfixes = 0
regressions = 0
unchanged = 0
for testcase_key_in_both in testcases1_keys & testcases2_keys:
previous_testcase = testcases1_map[testcase_key_in_both]
testcase = testcases2_map[testcase_key_in_both]
testcase["successfix"] = False
testcase["regression"] = False
if previous_testcase["action"] == testcase["action"]:
testcase["state"] = "UNCHANGED"
unchanged += 1
else:
if previous_testcase["action"] == "success":
testcase["state"] = "REGRESSED"
testcase["regression"] = True # tobedeleted
regressions += 1
else:
testcase["state"] = "RECOVERED"
testcase["successfix"] = True # tobedeleted
successfixes += 1
testcases.append(testcase)

testsuite2["testcases"] = sorted(testcases, key=lambda d: d["name"])
testsuite2["successfixes"] = successfixes
testsuite2["regressions"] = regressions
testsuite2["additions"] = len(added_testcases)
testsuite2["deletions"] = len(removed_testcases)
testsuite2["unchanged"] = unchanged
return testsuite2


def update_testsuites_with_testcase_changes(testsuites1, testsuites2):
testsuites1_map = {ts["name"]: ts for ts in testsuites1 or []}
testsuites = []
for testsuite in testsuites2:
testsuite["successfixes"] = 0
testsuite["regressions"] = 0
for testcase in testsuite["testcases"]:
testcase["successfix"] = False
testcase["regression"] = False
testcase_key = _get_unique_testcase_key(testsuite, testcase)
if testcase_key not in testcases_map:
continue
prev_testcase = testcases_map[testcase_key]
# if switch from success to failure then it's a regression
if testcase["action"] == "failure":
if prev_testcase["action"] == "success":
testcase["regression"] = True
testsuite["regressions"] += 1
# if switch from either failure/regression to success its successfix
elif testcase["action"] == "success":
if prev_testcase["action"] == "failure":
testcase["successfix"] = True
testsuite["successfixes"] += 1

return testsuites2
previous_testsuite = testsuites1_map.get(testsuite["name"])
testsuites.append(_compare_testsuites(previous_testsuite, testsuite))
return testsuites


def calculate_test_results(testsuites):
Expand Down
21 changes: 15 additions & 6 deletions tests/api/v1/test_files.py
Original file line number Diff line number Diff line change
Expand Up @@ -358,20 +358,25 @@ def test_purge_failure(admin, user, job_user_id, team_user_id):


@mock.patch("dci.api.v1.notifications.job_dispatcher")
def test_get_junit_file(_, user, job_user_id):
def test_get_junit_file(_, user, job_user):
junit_id = t_utils.create_file(
user,
job_user_id,
job_user["id"],
"Tempest",
tests_data.jobtest_one,
"application/junit",
)["id"]
testsuites = user.get("/api/v1/files/%s/junit" % junit_id).data["testsuites"]
assert len(testsuites) == 1
assert testsuites[0] == {
data = user.get("/api/v1/files/%s/junit" % junit_id).data
assert len(data["testsuites"]) == 1
assert data["job"]["id"] == job_user["id"]
assert data["job"]["name"] == job_user["name"]
assert data["previous_job"] is None
assert data["testsuites"][0] == {
"id": 0,
"additions": 3,
"deletions": 0,
"errors": 0,
"failures": 1,
"id": 0,
"name": "Kikoolol1",
"skipped": 0,
"success": 2,
Expand All @@ -385,6 +390,7 @@ def test_get_junit_file(_, user, job_user_id):
"message": None,
"name": "test_1",
"properties": [],
"state": "ADDED",
"stderr": None,
"stdout": None,
"time": 30.0,
Expand All @@ -399,6 +405,7 @@ def test_get_junit_file(_, user, job_user_id):
"message": None,
"name": "test_2",
"properties": [],
"state": "ADDED",
"stderr": None,
"stdout": None,
"time": 40.0,
Expand All @@ -413,6 +420,7 @@ def test_get_junit_file(_, user, job_user_id):
"message": None,
"name": "test_3[id-2fc6822e-b5a8-42ed-967b-11d86e881ce3,smoke]",
"properties": [],
"state": "ADDED",
"stderr": None,
"stdout": None,
"time": 40.0,
Expand All @@ -424,6 +432,7 @@ def test_get_junit_file(_, user, job_user_id):
],
"tests": 3,
"time": 110.0,
"unchanged": 0,
}


Expand Down
Loading

0 comments on commit 0b67e9a

Please sign in to comment.