Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Multiple reportportal plugin improvements and fixes #3356

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
8 changes: 8 additions & 0 deletions docs/releases.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,14 @@
======================


The :ref:`/plugins/report/reportportal` plugin now handles the
timestamps for ``custom`` and ``restraint`` results correctly. It
should never happen that the result's ``end-time`` will be higher
then the ``start-time``. It should be also ensured that the start
time of all launch items is the same or higher then the start time
of a parent item/launch.


tmt-1.39.0
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Expand Down
3 changes: 3 additions & 0 deletions tmt/schemas/report/reportportal.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,9 @@ properties:
traceback-size-limit:
type: integer

ssl-verify:
type: boolean

required:
- how
- project
87 changes: 61 additions & 26 deletions tmt/steps/report/reportportal.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
import dataclasses
import datetime
import os
import re
from time import time
from typing import TYPE_CHECKING, Any, Optional, overload

import requests
import urllib3

import tmt.hardware
import tmt.log
import tmt.steps.report
import tmt.utils
from tmt.result import ResultOutcome
from tmt.utils import field, yaml_to_dict
from tmt.utils import field, format_timestamp, yaml_to_dict

if TYPE_CHECKING:
from tmt._compat.typing import TypeAlias
Expand Down Expand Up @@ -238,6 +239,13 @@ class ReportReportPortalData(tmt.steps.report.ReportStepData):
os.getenv('TMT_REPORT_ARTIFACTS_URL')),
help="Link to test artifacts provided for report plugins.")

ssl_verify: bool = field(
default=True,
option=('--ssl-verify / --no-ssl-verify'),
is_flag=True,
show_default=True,
help="Enable/disable the SSL verification for communication with ReportPortal.")

launch_url: Optional[str] = None
launch_uuid: Optional[str] = None
suite_uuid: Optional[str] = None
Expand Down Expand Up @@ -309,10 +317,10 @@ class ReportReportPortal(tmt.steps.report.ReportPlugin[ReportReportPortalData]):
TMT_TO_RP_RESULT_STATUS = {
ResultOutcome.PASS: "PASSED",
ResultOutcome.FAIL: "FAILED",
ResultOutcome.ERROR: "FAILED",
ResultOutcome.INFO: "SKIPPED",
ResultOutcome.WARN: "FAILED",
ResultOutcome.INFO: "SKIPPED"
}
ResultOutcome.ERROR: "FAILED",
ResultOutcome.SKIP: "SKIPPED"}

def handle_response(self, response: requests.Response) -> None:
""" Check the endpoint response and raise an exception if needed """
Expand Down Expand Up @@ -342,15 +350,19 @@ def check_options(self) -> None:
self.warn("Unexpected option combination: '--launch-rerun' "
"may cause an unexpected behaviour with launch-per-plan structure")

def time(self) -> str:
return str(int(time() * 1000))
@property
def datetime(self) -> str:
# Use the same format of timestramp as tmt does
return format_timestamp(datetime.datetime.now(datetime.timezone.utc))

def get_headers(self) -> dict[str, str]:
return {"Authorization": "bearer " + str(self.data.token),
"accept": "*/*",
@property
def headers(self) -> dict[str, str]:
return {"Authorization": f"Bearer {self.data.token}",
"Accept": "*/*",
"Content-Type": "application/json"}

def get_url(self) -> str:
@property
def url(self) -> str:
return f"{self.data.url}/api/{self.data.api_version}/{self.data.project}"

def construct_launch_attributes(self, suite_per_plan: bool,
Expand Down Expand Up @@ -396,21 +408,21 @@ def get_defect_type_locator(self, session: requests.Session,
return str(dt_locator)

def rp_api_get(self, session: requests.Session, path: str) -> requests.Response:
response = session.get(url=f"{self.get_url()}/{path}",
headers=self.get_headers())
response = session.get(url=f"{self.url}/{path}",
headers=self.headers)
self.handle_response(response)
return response

def rp_api_post(self, session: requests.Session, path: str, json: JSON) -> requests.Response:
response = session.post(url=f"{self.get_url()}/{path}",
headers=self.get_headers(),
response = session.post(url=f"{self.url}/{path}",
headers=self.headers,
json=json)
self.handle_response(response)
return response

def rp_api_put(self, session: requests.Session, path: str, json: JSON) -> requests.Response:
response = session.put(url=f"{self.get_url()}/{path}",
headers=self.get_headers(),
response = session.put(url=f"{self.url}/{path}",
headers=self.headers,
json=json)
self.handle_response(response)
return response
Expand Down Expand Up @@ -449,13 +461,24 @@ def go(self, *, logger: Optional[tmt.log.Logger] = None) -> None:

self.check_options()

launch_time = self.time()
# If SSL verification is disabled, do not print warnings with urllib3
if not self.data.ssl_verify:
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
self.warn("SSL verification is disabled for all requests being made to ReportPortal "
f"instance ({self.data.url}).")

# Use the current datetime as a default, but this is the worst case scenario
# and we should use timestamps from results log as much as possible.
launch_time = self.datetime

# Support for idle tests
executed = bool(self.step.plan.execute.results())
if executed:
# launch time should be the earliest start time of all plans
launch_time = min([r.start_time or self.time()
# Launch time should be the earliest start time of all plans.
#
# The datetime *strings* are in fact sorted here, but finding the minimum will work,
# because the datetime in ISO format is designed to be lexicographically sortable.
launch_time = min([r.start_time or self.datetime
for r in self.step.plan.execute.results()])

# Create launch, suites (if "--suite_per_plan") and tests;
Expand Down Expand Up @@ -516,6 +539,8 @@ def go(self, *, logger: Optional[tmt.log.Logger] = None) -> None:
504, # Gateway Timeout
)) as session:

session.verify = self.data.ssl_verify

if create_launch:

# Create a launch
Expand Down Expand Up @@ -582,9 +607,11 @@ def go(self, *, logger: Optional[tmt.log.Logger] = None) -> None:
self.verbose("uuid", suite_uuid, "yellow", shift=1)
self.data.suite_uuid = suite_uuid

# The first test starts with the launch (at the worst case)
test_time = launch_time

for result, test in self.step.plan.execute.results_for_tests(
self.step.plan.discover.tests()):
test_time = self.time()
test_name = None
test_description = ''
test_link = None
Expand All @@ -595,7 +622,10 @@ def go(self, *, logger: Optional[tmt.log.Logger] = None) -> None:
if result:
serial_number = result.serial_number
test_name = result.name
test_time = result.start_time or self.time()

# Use the actual timestamp or reuse the old one if missing
test_time = result.start_time or test_time

# for guests, save their primary address
if result.guest.primary_address:
item_attributes.append({
Expand Down Expand Up @@ -646,15 +676,20 @@ def go(self, *, logger: Optional[tmt.log.Logger] = None) -> None:
"type": "step",
"testCaseId": test_id,
"startTime": test_time})

item_uuid = yaml_to_dict(response.text).get("id")
assert item_uuid is not None
self.verbose("uuid", item_uuid, "yellow", shift=1)
self.data.test_uuids[serial_number] = item_uuid
else:
item_uuid = self.data.test_uuids[serial_number]

# Support for idle tests
status = "SKIPPED"
if result:
# Shift the timestamp to the end of a test
test_time = result.end_time or test_time

# For each log
for index, log_path in enumerate(result.log):
try:
Expand All @@ -679,7 +714,7 @@ def go(self, *, logger: Optional[tmt.log.Logger] = None) -> None:
"itemUuid": item_uuid,
"launchUuid": launch_uuid,
"level": level,
"time": result.end_time})
"time": test_time})

# Write out failures
if index == 0 and status == "FAILED":
Expand All @@ -696,9 +731,7 @@ def go(self, *, logger: Optional[tmt.log.Logger] = None) -> None:
"itemUuid": item_uuid,
"launchUuid": launch_uuid,
"level": "ERROR",
"time": result.end_time})

test_time = result.end_time or self.time()
"time": test_time})

# Finish the test item
response = self.rp_api_put(
Expand All @@ -710,6 +743,8 @@ def go(self, *, logger: Optional[tmt.log.Logger] = None) -> None:
"status": status,
"issue": {
"issueType": self.get_defect_type_locator(session, defect_type)}})

# The launch ends with the last test
launch_time = test_time

if create_suite:
Expand Down
Loading