Skip to content

Commit 43ff06a

Browse files
Merge branch 'main' into bugfix/fix-test-scale-to-zero
2 parents a462abf + 9104aa4 commit 43ff06a

File tree

11 files changed

+1031
-573
lines changed

11 files changed

+1031
-573
lines changed

.flake8

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ fcn_exclude_functions =
1919
re,
2020
logging,
2121
LOGGER,
22+
BASIC_LOGGER,
2223
os,
2324
json,
2425
pytest,

.pre-commit-config.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ repos:
3535
- id: detect-secrets
3636

3737
- repo: https://github.com/astral-sh/ruff-pre-commit
38-
rev: v0.11.9
38+
rev: v0.11.10
3939
hooks:
4040
- id: ruff
4141
- id: ruff-format
@@ -49,7 +49,7 @@ repos:
4949
# - id: renovate-config-validator
5050

5151
- repo: https://github.com/gitleaks/gitleaks
52-
rev: v8.25.1
52+
rev: v8.26.0
5353
hooks:
5454
- id: gitleaks
5555

conftest.py

Lines changed: 98 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,19 @@
22
import os
33
import pathlib
44
import shutil
5+
import datetime
6+
import traceback
57

68
import shortuuid
9+
from _pytest.runner import CallInfo
10+
from _pytest.reports import TestReport
711
from pytest import (
812
Parser,
913
Session,
1014
FixtureRequest,
1115
FixtureDef,
1216
Item,
17+
Collector,
1318
Config,
1419
CollectReport,
1520
)
@@ -18,8 +23,15 @@
1823
from pytest_testconfig import config as py_config
1924

2025
from utilities.constants import KServeDeploymentType
26+
from utilities.database import Database
2127
from utilities.logger import separator, setup_logging
22-
28+
from utilities.must_gather_collector import (
29+
set_must_gather_collector_directory,
30+
set_must_gather_collector_values,
31+
get_must_gather_collector_dir,
32+
collect_rhoai_must_gather,
33+
get_base_dir,
34+
)
2335

2436
LOGGER = logging.getLogger(name=__name__)
2537
BASIC_LOGGER = logging.getLogger(name="basic")
@@ -31,6 +43,7 @@ def pytest_addoption(parser: Parser) -> None:
3143
runtime_group = parser.getgroup(name="Runtime details")
3244
upgrade_group = parser.getgroup(name="Upgrade options")
3345
platform_group = parser.getgroup(name="Platform")
46+
must_gather_group = parser.getgroup(name="MustGather")
3447
cluster_sanity_group = parser.getgroup(name="ClusterSanity")
3548

3649
# AWS config and credentials options
@@ -118,6 +131,12 @@ def pytest_addoption(parser: Parser) -> None:
118131
"--applications-namespace",
119132
help="RHOAI/ODH applications namespace",
120133
)
134+
must_gather_group.addoption(
135+
"--collect-must-gather",
136+
help="Indicate if must-gather should be collected on failure.",
137+
action="store_true",
138+
default=False,
139+
)
121140

122141
# Cluster sanity options
123142
cluster_sanity_group.addoption(
@@ -205,14 +224,22 @@ def _add_upgrade_test(_item: Item, _upgrade_deployment_modes: list[str]) -> bool
205224

206225

207226
def pytest_sessionstart(session: Session) -> None:
208-
tests_log_file = session.config.getoption("log_file") or "pytest-tests.log"
227+
log_file = session.config.getoption("log_file") or "pytest-tests.log"
228+
tests_log_file = os.path.join(get_base_dir(), log_file)
229+
LOGGER.info(f"Writing tests log to {tests_log_file}")
209230
if os.path.exists(tests_log_file):
210231
pathlib.Path(tests_log_file).unlink()
211-
232+
if session.config.getoption("--collect-must-gather"):
233+
session.config.option.must_gather_db = Database()
212234
session.config.option.log_listener = setup_logging(
213235
log_file=tests_log_file,
214236
log_level=session.config.getoption("log_cli_level") or logging.INFO,
215237
)
238+
must_gather_dict = set_must_gather_collector_values()
239+
shutil.rmtree(
240+
path=must_gather_dict["must_gather_base_directory"],
241+
ignore_errors=True,
242+
)
216243

217244

218245
def pytest_fixture_setup(fixturedef: FixtureDef[Any], request: FixtureRequest) -> None:
@@ -226,9 +253,23 @@ def pytest_runtest_setup(item: Item) -> None:
226253
2. Adds `fail_if_missing_dependent_operators` fixture for Serverless tests.
227254
3. Adds fixtures to enable KServe/model mesh in DSC for model server tests.
228255
"""
229-
230256
BASIC_LOGGER.info(f"\n{separator(symbol_='-', val=item.name)}")
231257
BASIC_LOGGER.info(f"{separator(symbol_='-', val='SETUP')}")
258+
if item.config.getoption("--collect-must-gather"):
259+
# set must-gather collection directory:
260+
set_must_gather_collector_directory(item=item, directory_path=get_must_gather_collector_dir())
261+
262+
# At the begining of setup work, insert current epoch time into the database to indicate test
263+
# start time
264+
265+
try:
266+
db = item.config.option.must_gather_db
267+
db.insert_test_start_time(
268+
test_name=f"{item.fspath}::{item.name}",
269+
start_time=int(datetime.datetime.now().timestamp()),
270+
)
271+
except Exception as db_exception:
272+
LOGGER.error(f"Database error: {db_exception}. Must-gather collection may not be accurate")
232273

233274
if KServeDeploymentType.SERVERLESS.lower() in item.keywords:
234275
item.fixturenames.insert(0, "fail_if_missing_dependent_operators")
@@ -252,6 +293,10 @@ def pytest_runtest_call(item: Item) -> None:
252293

253294
def pytest_runtest_teardown(item: Item) -> None:
254295
BASIC_LOGGER.info(f"{separator(symbol_='-', val='TEARDOWN')}")
296+
# reset must-gather collector after each tests
297+
py_config["must_gather_collector"]["collector_directory"] = py_config["must_gather_collector"][
298+
"must_gather_base_directory"
299+
]
255300

256301

257302
def pytest_report_teststatus(report: CollectReport, config: Config) -> None:
@@ -276,10 +321,56 @@ def pytest_sessionfinish(session: Session, exitstatus: int) -> None:
276321
session.config.option.log_listener.stop()
277322
if session.config.option.setupplan or session.config.option.collectonly:
278323
return
279-
base_dir = py_config["tmp_base_dir"]
280-
LOGGER.info(f"Deleting pytest base dir {base_dir}")
281-
shutil.rmtree(path=base_dir, ignore_errors=True)
324+
if session.config.getoption("--collect-must-gather"):
325+
db = session.config.option.must_gather_db
326+
file_path = db.database_file_path
327+
LOGGER.info(f"Removing database file path {file_path}")
328+
if os.path.exists(file_path):
329+
os.remove(file_path)
330+
# clean up the empty folders
331+
collector_directory = py_config["must_gather_collector"]["must_gather_base_directory"]
332+
if os.path.exists(collector_directory):
333+
for root, dirs, files in os.walk(collector_directory, topdown=False):
334+
for _dir in dirs:
335+
dir_path = os.path.join(root, _dir)
336+
if not os.listdir(dir_path):
337+
shutil.rmtree(path=dir_path, ignore_errors=True)
338+
LOGGER.info(f"Deleting pytest base dir {session.config.option.basetemp}")
339+
shutil.rmtree(path=session.config.option.basetemp, ignore_errors=True)
282340

283341
reporter: Optional[TerminalReporter] = session.config.pluginmanager.get_plugin("terminalreporter")
284342
if reporter:
285343
reporter.summary_stats()
344+
345+
346+
def calculate_must_gather_timer(test_start_time: int) -> int:
347+
default_duration = 300
348+
if test_start_time > 0:
349+
duration = int(datetime.datetime.now().timestamp()) - test_start_time
350+
return duration if duration > 60 else default_duration
351+
else:
352+
LOGGER.warning(f"Could not get start time of test. Collecting must-gather for last {default_duration}s")
353+
return default_duration
354+
355+
356+
def pytest_exception_interact(node: Item | Collector, call: CallInfo[Any], report: TestReport | CollectReport) -> None:
357+
LOGGER.error(report.longreprtext)
358+
if node.config.getoption("--collect-must-gather"):
359+
test_name = f"{node.fspath}::{node.name}"
360+
LOGGER.info(f"Must-gather collection is enabled for {test_name}.")
361+
362+
try:
363+
db = node.config.option.must_gather_db
364+
test_start_time = db.get_test_start_time(test_name=test_name)
365+
except Exception as db_exception:
366+
test_start_time = 0
367+
LOGGER.warning(f"Error: {db_exception} in accessing database.")
368+
369+
try:
370+
collect_rhoai_must_gather(
371+
since=calculate_must_gather_timer(test_start_time=test_start_time),
372+
target_dir=os.path.join(get_must_gather_collector_dir(), "pytest_exception_interact"),
373+
)
374+
375+
except Exception as current_exception:
376+
LOGGER.warning(f"Failed to collect logs: {test_name}: {current_exception} {traceback.format_exc()}")

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ dependencies = [
6565
"jira>=3.8.0",
6666
"openshift-python-wrapper>=11.0.50",
6767
"semver>=3.0.4",
68+
"sqlalchemy>=2.0.40",
6869
"pytest-order>=1.3.0",
6970
"marshmallow==3.26.1,<4", # this version is needed for pytest-jira
7071
]

tests/global_config.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@
33
distribution: str = "downstream"
44
applications_namespace: str = "redhat-ods-applications" # overwritten in conftest.py if distribution is upstream
55
dsc_name: str = "default-dsc"
6+
must_gather_base_dir: str = "must-gather-base-dir"
67
dsci_name: str = "default-dsci"
78
dependent_operators: str = "servicemeshoperator,authorino-operator,serverless-operator"
8-
99
use_unprivileged_client: bool = True
1010

1111
for _dir in dir():

utilities/constants.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -272,3 +272,5 @@ class RunTimeConfig:
272272
},
273273
"commands": {"GRPC": "vllm_tgis_adapter"},
274274
}
275+
276+
RHOAI_OPERATOR_NAMESPACE = "redhat-ods-operator"

utilities/database.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import logging
2+
import os
3+
4+
from sqlalchemy import Integer, String, create_engine
5+
from sqlalchemy.orm import Mapped, Session, mapped_column
6+
from sqlalchemy.orm import DeclarativeBase
7+
from utilities.must_gather_collector import get_base_dir
8+
9+
LOGGER = logging.getLogger(__name__)
10+
11+
TEST_DB = "opendatahub-tests.db"
12+
13+
14+
class Base(DeclarativeBase):
15+
pass
16+
17+
18+
class OpenDataHubTestTable(Base):
19+
__tablename__ = "OpenDataHubTestTable"
20+
21+
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True, nullable=False)
22+
test_name: Mapped[str] = mapped_column(String(500))
23+
start_time: Mapped[int] = mapped_column(Integer, nullable=False)
24+
25+
26+
class Database:
27+
def __init__(self, database_file_name: str = TEST_DB, verbose: bool = True) -> None:
28+
self.database_file_path = os.path.join(get_base_dir(), database_file_name)
29+
self.connection_string = f"sqlite:///{self.database_file_path}"
30+
self.verbose = verbose
31+
self.engine = create_engine(url=self.connection_string, echo=self.verbose)
32+
Base.metadata.create_all(bind=self.engine)
33+
34+
def insert_test_start_time(self, test_name: str, start_time: int) -> None:
35+
with Session(bind=self.engine) as db_session:
36+
new_table_entry = OpenDataHubTestTable(test_name=test_name, start_time=start_time)
37+
db_session.add(new_table_entry)
38+
db_session.commit()
39+
40+
def get_test_start_time(self, test_name: str) -> int:
41+
with Session(bind=self.engine) as db_session:
42+
result_row = (
43+
db_session.query(OpenDataHubTestTable)
44+
.with_entities(OpenDataHubTestTable.start_time)
45+
.filter_by(test_name=test_name)
46+
.first()
47+
)
48+
if result_row:
49+
start_time_value = result_row[0]
50+
else:
51+
start_time_value = 0
52+
LOGGER.warning(f"No test found with name: {test_name}")
53+
return start_time_value

utilities/exceptions.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,12 @@ def __str__(self) -> str:
9696
return f"Failed to log in as user {self.user}."
9797

9898

99+
class InvalidArgumentsError(Exception):
100+
"""Raised when mutually exclusive or invalid argument combinations are passed."""
101+
102+
pass
103+
104+
99105
class ResourceNotReadyError(Exception):
100106
pass
101107

0 commit comments

Comments
 (0)