Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 16 additions & 15 deletions conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
from utilities.data_collector import (
collect_default_cnv_must_gather_with_vm_gather,
get_data_collector_dir,
get_scope_identifier,
Comment thread
dshchedr marked this conversation as resolved.
set_data_collector_directory,
set_data_collector_values,
)
Expand Down Expand Up @@ -644,12 +645,13 @@ def pytest_runtest_setup(item):
# before the setup work starts, insert current epoch time into the database
try:
db = Database(base_dir=item.config.getoption("--data-collector-output-dir"))
db.insert_test_start_time(
test_name=f"{item.fspath}::{item.name}",
start_time=int(datetime.datetime.now().strftime("%s")),
)
scope_marker = item.get_closest_marker(name="data_collector_scope")
scope_value = scope_marker.kwargs.get("scope") if scope_marker else None

name = get_scope_identifier(node=item, scope_value=scope_value)
db.insert_start_time(name=name, start_time=int(datetime.datetime.now().strftime("%s")))
except Exception as db_exception:
LOGGER.error(f"Database error: {db_exception}. Must-gather collection may not be accurate")
LOGGER.error(f"[DATA_COLLECTOR] Database error: {db_exception}. Must-gather collection may not be accurate")
BASIC_LOGGER.info(f"\n{separator(symbol_='-', val=item.name)}")
BASIC_LOGGER.info(f"{separator(symbol_='-', val='SETUP')}")
if "incremental" in item.keywords:
Expand Down Expand Up @@ -852,29 +854,28 @@ def get_inspect_command_namespace_string(node: Node, test_name: str) -> str:

def calculate_must_gather_timer(test_start_time):
if test_start_time > 0:
return int(datetime.datetime.now().strftime("%s")) - test_start_time
# Add 5-minute (300s) buffer to work around must-gather timing issues
return int(datetime.datetime.now().strftime("%s")) - test_start_time + 300
else:
LOGGER.warning(f"Could not get start time of test. Collecting must-gather for last {TIMEOUT_5MIN}s")
LOGGER.warning(f"[DATA_COLLECTOR] Could not get start time. Collecting must-gather for last {TIMEOUT_5MIN}s")
return TIMEOUT_5MIN


def pytest_exception_interact(node: Item | Collector, call: CallInfo[Any], report: TestReport | CollectReport) -> None:
BASIC_LOGGER.error(report.longreprtext)
if node.config.getoption("--data-collector") and not is_skip_must_gather(node=node):
test_name = f"{node.fspath}::{node.name}"
LOGGER.info(f"Must-gather collection is enabled for {test_name}.")
LOGGER.info(f"[DATA_COLLECTOR] Must-gather collection is enabled for {test_name}.")
inspect_str = get_inspect_command_namespace_string(test_name=test_name, node=node)
if call.excinfo and any([
isinstance(call.excinfo.value, exception_type) for exception_type in MUST_GATHER_IGNORE_EXCEPTION_LIST
]):
LOGGER.warning(f"Must-gather collection would be skipped for exception: {call.excinfo.type}")
LOGGER.warning(
f"[DATA_COLLECTOR] Must-gather collection would be skipped for exception: {call.excinfo.type}"
)
else:
try:
db = Database(base_dir=node.config.getoption("--data-collector-output-dir"))
test_start_time = db.get_test_start_time(test_name=test_name)
except Exception as db_exception:
test_start_time = 0
LOGGER.warning(f"Error: {db_exception} in accessing database.")
db = Database(base_dir=node.config.getoption("--data-collector-output-dir"))
test_start_time = db.get_start_time_for_collection(node=node)

try:
collection_dir = os.path.join(get_data_collector_dir(), "pytest_exception_interact")
Expand Down
1 change: 1 addition & 0 deletions pytest.ini
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ markers =

# Data collection markers
skip_must_gather_collection: skip must gather collection on failures
data_collector_scope: Set data collection scope (test, class, or module) for must-gather timing

# Test types
destructive: Destructive tests
Expand Down
1 change: 1 addition & 0 deletions tests/virt/cluster/aaq/test_aaq_allocation_methods.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
"updated_namespace_with_aaq_label",
),
pytest.mark.arm64,
pytest.mark.data_collector_scope(scope="module"),
]


Expand Down
1 change: 1 addition & 0 deletions tests/virt/cluster/aaq/test_arq.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
"updated_namespace_with_aaq_label",
),
pytest.mark.gating,
pytest.mark.data_collector_scope(scope="module"),
]


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@
wait_for_console,
)

pytestmark = pytest.mark.data_collector_scope(scope="module")

LOGGER = logging.getLogger(__name__)
TESTS_CLASS_NAME = "TestCommonTemplatesCentos"

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@
wait_for_console,
)

pytestmark = pytest.mark.data_collector_scope(scope="module")

LOGGER = logging.getLogger(__name__)
TESTS_CLASS_NAME = "TestCommonTemplatesFedora"

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,11 @@
wait_for_console,
)

pytestmark = [pytest.mark.post_upgrade, pytest.mark.gating]
pytestmark = [
pytest.mark.post_upgrade,
pytest.mark.gating,
pytest.mark.data_collector_scope(scope="module"),
]


LOGGER = logging.getLogger(__name__)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,12 @@
validate_virtctl_guest_agent_data_over_time,
)

pytestmark = [pytest.mark.post_upgrade, pytest.mark.special_infra, pytest.mark.high_resource_vm]
pytestmark = [
pytest.mark.post_upgrade,
pytest.mark.special_infra,
pytest.mark.high_resource_vm,
pytest.mark.data_collector_scope(scope="module"),
]

LOGGER = logging.getLogger(__name__)
TESTS_CLASS_NAME = "TestCommonTemplatesWindows"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,11 @@

LOGGER = logging.getLogger(__name__)

pytestmark = [pytest.mark.arm64, pytest.mark.rwx_default_storage]
pytestmark = [
pytest.mark.arm64,
pytest.mark.rwx_default_storage,
pytest.mark.data_collector_scope(scope="module"),
]


def wait_for_vm_uid_mismatch(vmi, vmi_old_uid):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
running_vm,
)

pytestmark = pytest.mark.data_collector_scope(scope="module")

NAMESPACE_LABEL = {"awesome-namespace-label": ""}

DEFAULT_MIGRATION_POLICY_PARAMETERS = {
Expand Down
2 changes: 1 addition & 1 deletion tests/virt/node/general/test_machinetype.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
wait_for_updated_kv_value,
)

pytestmark = pytest.mark.post_upgrade
pytestmark = [pytest.mark.post_upgrade, pytest.mark.data_collector_scope(scope="module")]
LOGGER = logging.getLogger(__name__)


Expand Down
2 changes: 1 addition & 1 deletion tests/virt/node/hotplug/test_cpu_memory_hotplug.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
restart_vm_wait_for_running_vm,
)

pytestmark = pytest.mark.rwx_default_storage
pytestmark = [pytest.mark.rwx_default_storage, pytest.mark.data_collector_scope(scope="module")]


LOGGER = logging.getLogger(__name__)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,11 @@
start_and_fetch_processid_on_windows_vm,
)

pytestmark = [pytest.mark.post_upgrade, pytest.mark.rwx_default_storage]
pytestmark = [
pytest.mark.post_upgrade,
pytest.mark.rwx_default_storage,
pytest.mark.data_collector_scope(scope="module"),
]


LOGGER = logging.getLogger(__name__)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,11 @@
start_and_fetch_processid_on_windows_vm,
)

pytestmark = [pytest.mark.rwx_default_storage, pytest.mark.usefixtures("created_post_copy_migration_policy")]
pytestmark = [
pytest.mark.rwx_default_storage,
pytest.mark.usefixtures("created_post_copy_migration_policy"),
pytest.mark.data_collector_scope(scope="module"),
]


LOGGER = logging.getLogger(__name__)
Expand Down
21 changes: 21 additions & 0 deletions utilities/data_collector.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@
import shlex
from functools import cache

from _pytest.nodes import Collector
from ocp_resources.namespace import Namespace
from ocp_resources.resource import get_client
from ocp_utilities.monitoring import Prometheus
from pytest import Item
from pytest_testconfig import config as py_config

import utilities.hco
Expand Down Expand Up @@ -175,3 +177,22 @@ def prepare_pytest_item_data_dir(item, output_dir):
)
os.makedirs(item_dir_log, exist_ok=True)
return item_dir_log


def get_scope_identifier(node: Item | Collector, scope_value: str | None) -> str:
"""
Get the identifier name based on data collection scope.

Args:
node: Pytest node (Item or Collector).
scope_value: Scope value from marker ("module", "class", or None for test).

Returns:
Database key for this scope.
"""
if scope_value == "module":
return str(node.fspath)
elif scope_value == "class":
return f"{node.fspath}::{node.parent.name}" if node.parent else str(node.fspath)
else:
return f"{node.fspath}::{node.name}"
76 changes: 65 additions & 11 deletions utilities/database.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import datetime
import logging

from _pytest.nodes import Collector
from pytest import Item
from sqlalchemy import Integer, String, create_engine
from sqlalchemy.orm import DeclarativeBase, Mapped, Session, mapped_column

from utilities.data_collector import get_data_collector_base
from utilities.data_collector import get_data_collector_base, get_scope_identifier

LOGGER = logging.getLogger(__name__)

Expand Down Expand Up @@ -32,17 +35,68 @@ def __init__(
self.engine = create_engine(url=self.connection_string, echo=self.verbose)
Base.metadata.create_all(bind=self.engine)

def insert_test_start_time(self, test_name: str, start_time: int) -> None:
def insert_start_time(self, name: str, start_time: int) -> None:
"""
Insert start time only if it doesn't exist.

Args:
name (str): Test/class/module identifier.
start_time (int): Start time in seconds since epoch.
"""
with Session(bind=self.engine) as db_session:
new_table_entry = CnvTestTable(test_name=test_name, start_time=start_time)
db_session.add(new_table_entry)
db_session.commit()
existing_entry = db_session.query(CnvTestTable).filter_by(test_name=name).first()

if not existing_entry:
new_entry = CnvTestTable(test_name=name, start_time=start_time)
db_session.add(new_entry)
db_session.commit()

def get_start_time(self, name: str) -> int | None:
"""
Get the start time for a test/class/module.

def get_test_start_time(self, test_name: str) -> int:
Args:
name (str): Test/class/module identifier.

Returns:
int | None: Start time in seconds since epoch, or None if not found.
"""
with Session(bind=self.engine) as db_session:
return (
db_session.query(CnvTestTable)
.with_entities(CnvTestTable.start_time)
.filter_by(test_name=test_name)
.one()[0]
result = (
db_session.query(CnvTestTable).with_entities(CnvTestTable.start_time).filter_by(test_name=name).first()
)
return result[0] if result else None

def get_start_time_for_collection(self, node: Item | Collector) -> int:
"""
Get test start time based on data_collector_scope marker.

Determines the appropriate scope (test, class, or module) from the marker,
retrieves the start time from the database, and logs the time delta.

Args:
node: Pytest node (Item or Collector).

Returns:
Start time in seconds since epoch, or 0 if not found.
"""
try:
# Check data_collector_scope marker
scope_marker = node.get_closest_marker(name="data_collector_scope")
scope_value = scope_marker.kwargs.get("scope") if scope_marker else None

name = get_scope_identifier(node=node, scope_value=scope_value)
scope_label = scope_value.upper() if scope_value else "TEST"

test_start_time = self.get_start_time(name=name)
if test_start_time:
time_delta = int(datetime.datetime.now().strftime("%s")) - test_start_time
LOGGER.info(f"[DATA_COLLECTOR] {scope_label} scope: {time_delta}s ({time_delta // 60}m)")
else:
test_start_time = 0
LOGGER.warning(f"[DATA_COLLECTOR] Start time not found for {name}")
except Exception as db_exception:
test_start_time = 0
LOGGER.warning(f"[DATA_COLLECTOR] Error: {db_exception} in accessing database.")

return test_start_time
Loading