Skip to content
Open
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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -133,3 +133,6 @@ docs/source/rst

# For local run (debug).
ocp_resources

# graft's local graph cache — regenerable, not committed (run `graft build`).
/graft/
4 changes: 3 additions & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
default_language_version:
python: python3.14

exclude: ^docs/site/

ci:
skip: [check-jsonschema]

Expand All @@ -16,7 +18,7 @@ repos:
stages: [pre-commit]

- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.16.4
rev: v0.16.8
hooks:
- id: ruff
stages: [pre-commit]
Expand Down
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,7 @@ When reviewing quarantine PRs, verify the **quarantine mechanism matches the fai
- **ALWAYS use `timeout_sampler`** - from `timeout_sampler` package for any operation that waits for a condition:
```python
from timeout_sampler import TimeoutSampler

for sample in TimeoutSampler(wait_timeout=60, sleep=5, func=check_condition):
if sample:
break
Expand Down
6 changes: 4 additions & 2 deletions docs/MULTIARCH.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,13 +58,15 @@ Multiarch-dedicated tests should be isolated from regular tests. Avoid modifying
Mark the entire file or specific classes with the `multiarch` marker:

```python
import pytest

# Module-level (preferred — marks the whole file)
pytestmark = [pytest.mark.multiarch]


# Class-level
@pytest.mark.multiarch
class TestMultiarchFeature:
...
class TestMultiarchFeature: ...
```

The `multiarch` marker is **required** on any test that runs in multiarch-dedicated mode. It also prevents the test from being collected on homogeneous clusters.
Expand Down
4 changes: 2 additions & 2 deletions docs/QUARANTINE_GUIDELINES.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ Conditional skip for product bug:
```python
import pytest


@pytest.mark.jira("CNV-12345", run=False) # Skips if the bug is open
def test_feature_a():
pass
Expand Down Expand Up @@ -99,8 +100,7 @@ from utilities.constants.pytest import QUARANTINED
reason=f"{QUARANTINED}: VM is going into running state which it shouldn't, CNV-123",
run=False,
)
def test_my_failing_test():
...
def test_my_failing_test(): ...
```

**Benefit**: Provides pytest summary insights:
Expand Down
15 changes: 11 additions & 4 deletions docs/SOFTWARE_TEST_DESCRIPTION.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,9 +114,9 @@ To exclude a whole new module from pytest collection, use:
# test_module_to_ignore.py
__test__ = False

def test_abc():
assert True # This test will not be collected or run

def test_abc():
assert True # This test will not be collected or run
```

To exclude new test classes from pytest collection, use:
Expand All @@ -129,8 +129,8 @@ class TestClass:
To exclude new tests from pytest collection, use:

```python
def test_abc():
...
def test_abc(): ...


test_abc.__test__ = False
```
Expand All @@ -145,6 +145,7 @@ def test_isolated_vms_cannot_communicate():
[NEGATIVE] Test that VMs on separate networks cannot ping each other.
"""


test_isolated_vms_cannot_communicate.__test__ = False
```

Expand Down Expand Up @@ -358,6 +359,7 @@ VM Snapshot and Restore Tests
STP Reference: https://example.com/stp/vm-snapshot-restore
"""


class TestSnapshotRestore:
"""
Tests for VM snapshot restore functionality.
Expand All @@ -372,6 +374,7 @@ class TestSnapshotRestore:
- File path="/data/after.txt", content="post-snapshot" (written after snapshot)
- VM Restored from snapshot, running and SSH accessible
"""

__test__ = False

def test_preserves_original_file(self):
Expand Down Expand Up @@ -411,6 +414,7 @@ class TestVMLifecycle:
Preconditions:
- VM Running latest Fedora virtual machine
"""

__test__ = False

def test_vm_restart_completes_successfully(self):
Expand Down Expand Up @@ -478,6 +482,7 @@ def test_flat_overlay_ping_between_vms():
- Ping succeeds with 0% packet loss
"""


test_flat_overlay_ping_between_vms.__test__ = False
```

Expand Down Expand Up @@ -509,6 +514,7 @@ def test_isolated_vms_cannot_communicate():
- Ping fails with 100% packet loss
"""


test_isolated_vms_cannot_communicate.__test__ = False
```

Expand Down Expand Up @@ -542,6 +548,7 @@ def test_online_disk_resize():
- Disk size inside VM is greater than original size
"""


test_online_disk_resize.__test__ = False
```

Expand Down
4 changes: 2 additions & 2 deletions libs/infra/images.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,12 +67,12 @@ class Windows:
DOCKER_IMAGE_DIR: str = "docker-local/kubevirt-common-instancetypes"
UEFI_WIN_DIR: str = f"{DIR}/uefi"
HA_DIR: str = f"{DIR}/HA-images"
ISO_BASE_DIR = f"{DIR}/install_iso"
ISO_BASE_DIR: str = f"{DIR}/install_iso"
ISO_WIN10_DIR: str = f"{ISO_BASE_DIR}/win10"
ISO_WIN11_DIR: str = f"{ISO_BASE_DIR}/win11"
ISO_WIN2022_DIR: str = f"{ISO_BASE_DIR}/win2022"
ISO_WIN2025_DIR: str = f"{ISO_BASE_DIR}/win2025"
CONTAINER_DISK_DV_SIZE = "40Gi"
CONTAINER_DISK_DV_SIZE: str = "40Gi"
DEFAULT_DV_SIZE: str = "70Gi"
DEFAULT_MEMORY_SIZE: str = "8Gi"
DEFAULT_MEMORY_SIZE_WSL: str = "12Gi"
Expand Down
11 changes: 6 additions & 5 deletions libs/net/nodenetworkconfigurationpolicy.py
Original file line number Diff line number Diff line change
Expand Up @@ -202,11 +202,12 @@ def _confirm_status_success_on_update(self) -> Iterator[None]:
# it must be fetched and stored before the update, and compared with the new time-stamp after.
initial_success_status_time = self._get_last_successful_transition_time()

yield

# In case the update has been executed before the NNCP reached a success status,
# initial_success_status_time is empty and the wait is treated without considering the timestamp.
self.wait_for_status_success(last_success_timestamp=initial_success_status_time)
try:
yield
finally:
# In case the update has been executed before the NNCP reached a success status,
# initial_success_status_time is empty and the wait is treated without considering the timestamp.
self.wait_for_status_success(last_success_timestamp=initial_success_status_time)

def _get_last_successful_transition_time(self) -> str:
for condition in self.instance.status.conditions:
Expand Down
6 changes: 3 additions & 3 deletions libs/vm/vm.py
Original file line number Diff line number Diff line change
Expand Up @@ -228,10 +228,10 @@ def from_existing(
Returns:
BaseVirtualMachine instance bound to an existing cluster resource.
"""
obj = cls.__new__(cls) # noqa: FCN001
obj = object.__new__(cls)

VirtualMachine.__init__( # noqa: FCN001
obj,
VirtualMachine.__init__(
self=obj,
namespace=namespace,
name=name,
client=client,
Expand Down
2 changes: 1 addition & 1 deletion scripts/coderabbit_retry/coderabbit_retry.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
"""Retry CodeRabbit reviews that hit rate limits.
r"""Retry CodeRabbit reviews that hit rate limits.

Scans open, non-draft, non-WIP, non-stale, non-conflicting PRs (updated in the
last 2 days) for CodeRabbit rate-limit comments and re-triggers review once the
Expand Down
40 changes: 23 additions & 17 deletions scripts/std_placeholder_stats/std_placeholder_stats.py
Original file line number Diff line number Diff line change
Expand Up @@ -413,41 +413,47 @@ def output_text(placeholder_files: list[PlaceholderFile]) -> None:
output_lines: list[str] = []

if total_tests > 0:
output_lines.append(separator(symbol="="))
output_lines.append("STD PLACEHOLDER TESTS (not yet implemented)")
output_lines.append(separator(symbol="="))
output_lines.append("")
output_lines.extend((
separator(symbol="="),
"STD PLACEHOLDER TESTS (not yet implemented)",
separator(symbol="="),
"",
))

for placeholder_file in placeholder_files:
lines = _format_placeholder_lines(placeholder_file=placeholder_file)
if lines:
output_lines.extend(lines)

output_lines.append("")
output_lines.append(separator(symbol="-"))
output_lines.extend(("", separator(symbol="-")))
test_word = "test" if total_tests == 1 else "tests"
file_word = "file" if total_files == 1 else "files"
output_lines.append(f"Total: {total_tests} placeholder {test_word} in {total_files} {file_word}")
output_lines.append(separator(symbol="="))
output_lines.extend((
f"Total: {total_tests} placeholder {test_word} in {total_files} {file_word}",
separator(symbol="="),
))

if total_disabled > 0:
output_lines.append("")
output_lines.append(separator(symbol="="))
output_lines.append("DISABLED TESTS (implemented but marked __test__ = False)")
output_lines.append(separator(symbol="="))
output_lines.append("")
output_lines.extend((
"",
separator(symbol="="),
"DISABLED TESTS (implemented but marked __test__ = False)",
separator(symbol="="),
"",
))

for placeholder_file in placeholder_files:
lines = _format_disabled_lines(placeholder_file=placeholder_file)
if lines:
output_lines.extend(lines)

output_lines.append("")
output_lines.append(separator(symbol="-"))
output_lines.extend(("", separator(symbol="-")))
test_word = "test" if total_disabled == 1 else "tests"
file_word = "file" if disabled_files == 1 else "files"
output_lines.append(f"Total: {total_disabled} disabled {test_word} in {disabled_files} {file_word}")
output_lines.append(separator(symbol="="))
output_lines.extend((
f"Total: {total_disabled} disabled {test_word} in {disabled_files} {file_word}",
separator(symbol="="),
))

for line in output_lines:
LOGGER.info(line)
Expand Down
23 changes: 11 additions & 12 deletions scripts/tests_analyzer/compare_coderabbit_decisions.py
Original file line number Diff line number Diff line change
Expand Up @@ -493,13 +493,15 @@ def compare_pr(

def generate_detailed_mismatch_analysis(result: ComparisonResult) -> list[str]:
"""Generate detailed analysis for a mismatch case."""
lines = []
lines: list[str] = []

lines.append(f"### PR #{result.pr_number} - [{result.pr_title}]({result.pr_url})")
lines.append("")
lines.append(f"**Author:** {result.pr_author}")
lines.append(f"**CodeRabbit decision:** {'Run' if result.coderabbit.should_run else 'Skip'}")
lines.append(f"**Analyzer decision:** {'Run' if result.analyzer.should_run else 'Skip'}")
lines.extend((
f"### PR #{result.pr_number} - [{result.pr_title}]({result.pr_url})",
"",
f"**Author:** {result.pr_author}",
f"**CodeRabbit decision:** {'Run' if result.coderabbit.should_run else 'Skip'}",
f"**Analyzer decision:** {'Run' if result.analyzer.should_run else 'Skip'}",
))
if result.analyzer.marker_expression:
lines.append(f"**Marker expression:** `{result.analyzer.marker_expression}`")
lines.append("")
Expand Down Expand Up @@ -529,8 +531,7 @@ def generate_detailed_mismatch_analysis(result: ComparisonResult) -> list[str]:
lines.append("")

# Show analyzer reasoning
lines.append(f"**Analyzer reasoning:** {result.analyzer.reason}")
lines.append("")
lines.extend((f"**Analyzer reasoning:** {result.analyzer.reason}", ""))

return lines

Expand Down Expand Up @@ -569,8 +570,7 @@ def generate_markdown_report(results: list[ComparisonResult], repo: str, *, deta

if comparable:
accuracy = (len(matches) / len(comparable)) * 100
lines.append(f"**Agreement Rate:** {accuracy:.1f}%")
lines.append("")
lines.extend((f"**Agreement Rate:** {accuracy:.1f}%", ""))

# Mismatches section (most important)
if mismatches:
Expand Down Expand Up @@ -604,8 +604,7 @@ def generate_markdown_report(results: list[ComparisonResult], repo: str, *, deta
])
for mismatch in mismatches:
lines.extend(generate_detailed_mismatch_analysis(result=mismatch))
lines.append("---")
lines.append("")
lines.extend(("---", ""))

# Matches section
if matches:
Expand Down
36 changes: 19 additions & 17 deletions scripts/tests_analyzer/pytest_marker_analyzer.py
Original file line number Diff line number Diff line change
Expand Up @@ -576,11 +576,7 @@ def check_parametrize_marks(decorator: ast.AST, marker_names: set[str]) -> bool:
return False

# Check each element in the list for pytest.param(..., marks=...)
for element in param_values.elts:
if has_marker_in_param(node=element, marker_names=marker_names):
return True

return False
return any(has_marker_in_param(node=element, marker_names=marker_names) for element in param_values.elts)


def has_marker_in_param(node: ast.AST, marker_names: set[str]) -> bool:
Expand Down Expand Up @@ -1591,7 +1587,7 @@ def _get_diff_content(
file_path=str(relative_path),
token=token,
)
return diff_content if diff_content else None
return diff_content or None

try:
result = subprocess.run(
Expand Down Expand Up @@ -2083,7 +2079,7 @@ def _analyze_single_test_dependencies(
max_depth = MAX_TRANSITIVE_IMPORT_DEPTH

while to_visit and current_depth < max_depth:
current_level = to_visit[:]
current_level = to_visit.copy()
to_visit = []

for dep_file in current_level:
Expand Down Expand Up @@ -3724,25 +3720,31 @@ def analyze_impact(self, changed_files: list[Path]) -> AnalysisResult:
def format_markdown_output(result: AnalysisResult) -> str:
"""Format analysis result as Markdown."""
output = ["## Test Execution Plan", ""]
output.append(f"**Run tests with marker expression `{result.marker_expression}`: {result.should_run_tests}**")
output.append("")
output.append(f"**Reason:** {result.reason}")
output.append("")
output.extend((
f"**Run tests with marker expression `{result.marker_expression}`: {result.should_run_tests}**",
"",
f"**Reason:** {result.reason}",
"",
))

if result.affected_tests:
output.append(f"### Affected tests with marker expression `{result.marker_expression}`:")
for test in result.affected_tests:
output.append(f"- `{test['node_id']}`")
output.append(f" - Test file: `{test['test_file']}`")
output.append(f" - Dependencies affected: {len(test['dependencies'])}")
output.extend((
f"- `{test['node_id']}`",
f" - Test file: `{test['test_file']}`",
f" - Dependencies affected: {len(test['dependencies'])}",
))
for dep in test["dependencies"][:3]: # Show first 3 dependencies
output.append(f" - `{dep}`")
if len(test["dependencies"]) > 3:
output.append(f" - ... and {len(test['dependencies']) - 3} more")
output.append("")

output.append(f"**Total tests with marker expression `{result.marker_expression}`:** {result.total_tests}")
output.append(f"**Changed files:** {len(result.changed_files)}")
output.extend((
f"**Total tests with marker expression `{result.marker_expression}`:** {result.total_tests}",
f"**Changed files:** {len(result.changed_files)}",
))

return "\n".join(output)

Expand Down Expand Up @@ -3923,7 +3925,7 @@ def run_github_mode(args: argparse.Namespace) -> tuple[AnalysisResult | None, in
workdir.mkdir(parents=True, exist_ok=True)
else:
# Use custom temp base if specified, otherwise system default
temp_base = args.work_dir if args.work_dir else None
temp_base = args.work_dir or None
if temp_base:
temp_base.mkdir(parents=True, exist_ok=True)
temp_dir = tempfile.mkdtemp(prefix="pytest_marker_analyzer_", dir=temp_base)
Expand Down
Loading
Loading