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
6 changes: 6 additions & 0 deletions isvctl/configs/suites/storage.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -472,6 +472,9 @@ tests:
nfs_storage_class: "{{ steps.setup_cluster.csi.nfs_storage_class | default('', true) }}"
image: "busybox:1.36"
node_selector: {}
# Dev-sanity value, NOT the acceptance requirement. HSS07-02 / N-013a
# requires 1,000,000 files in a single directory (the check's own
# default); raise this per-NCP for a production validation run.
files_count: 10000
pvc_size: "10Gi"
bind_timeout_s: 120
Expand All @@ -485,6 +488,9 @@ tests:
nfs_storage_class: "{{ steps.setup_cluster.csi.nfs_storage_class | default('', true) }}"
image: "busybox:1.36"
node_selector: {}
# Dev-sanity value, NOT the acceptance requirement. HSS07-02 / N-013b
# requires 500,000 subdirectories in a single directory (the check's
# own default); raise this per-NCP for a production validation run.
dirs_count: 500
pvc_size: "10Gi"
bind_timeout_s: 120
Expand Down
17 changes: 17 additions & 0 deletions isvctl/src/isvctl/orchestrator/loop.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
from pathlib import Path
from typing import Any

from isvtest.core.discovery import discover_all_tests
from isvtest.core.resolution import (
DECLARABLE_CAPABILITIES,
ErrorReason,
Expand Down Expand Up @@ -340,6 +341,21 @@ def _apply_step_validation_gates(steps: list[Any], released_tests: set[str] | No
return gated_steps


def _warn_unmet_prerequisites(entries: list[ResolvedEntry]) -> None:
"""Warn about checks whose local prerequisites are unmet, before any of them runs.

The checks still fail on their own terms when they execute; reporting here
means a run does not spend minutes on the checks ahead of them first.
"""
class_map = {cls.__name__: cls for cls in discover_all_tests()}
for entry in entries:
class_key = resolve_class_key(entry.entry.name, class_map)
if class_key is None:
continue
if unmet := class_map[class_key].preflight(entry.rendered_params or {}):
logger.warning("Validation '%s' will fail: %s", entry.entry.name, unmet)


def _apply_capability_step_gates(
steps: list[Any],
validation_entries: list[ValidationEntry],
Expand Down Expand Up @@ -674,6 +690,7 @@ def _run_steps_mode(
)
ready_entries = [entry for entry in resolved_phase_entries if entry.is_ready]
terminal_before_pytest = [entry for entry in resolved_phase_entries if not entry.is_ready]
_warn_unmet_prerequisites(ready_entries)

# Write a per-phase stub for entries pre-resolved to SKIPPED/ERROR.
# Suite name matches pytest's so the merger collapses them into one suite.
Expand Down
40 changes: 40 additions & 0 deletions isvctl/tests/test_orchestrator_loop.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@

"""Tests for orchestrator loop."""

import logging
import xml.etree.ElementTree as ET
from pathlib import Path

Expand All @@ -37,6 +38,7 @@
_apply_capability_step_gates,
_entries_missing_from_junit,
_merge_junit_xmls,
_warn_unmet_prerequisites,
_write_terminal_junit_xml,
)
from isvctl.orchestrator.step_executor import (
Expand Down Expand Up @@ -79,6 +81,44 @@ def test_explicit_step_requires_gate_unbound_lifecycle_steps() -> None:
assert all(not step.skip for step in kubernetes_steps)


class TestWarnUnmetPrerequisites:
"""A check's unmet local prerequisite is reported before the phase runs."""

def _entry(self, name: str, params: dict[str, str]) -> ResolvedEntry:
return ResolvedEntry(
entry=ValidationEntry(name=name, category="k8s_filesystem", params_template={}),
rendered_params=params,
)

def test_missing_vendored_pjdfstest_is_reported(
self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path, caplog: pytest.LogCaptureFixture
) -> None:
monkeypatch.setattr("isvtest.validations.k8s_filesystem._PJDFSTEST_SRC_DIR", tmp_path / "absent")
entries = [self._entry("K8sPosixComplianceCheck", {"shared_fs_storage_class": "sc-rwx"})]

with caplog.at_level(logging.WARNING):
_warn_unmet_prerequisites(entries)

assert "make vendor-pjdfstest" in caplog.text

def test_satisfied_prerequisite_is_quiet(
self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path, caplog: pytest.LogCaptureFixture
) -> None:
monkeypatch.setattr("isvtest.validations.k8s_filesystem._PJDFSTEST_SRC_DIR", tmp_path)
entries = [self._entry("K8sPosixComplianceCheck", {"shared_fs_storage_class": "sc-rwx"})]

with caplog.at_level(logging.WARNING):
_warn_unmet_prerequisites(entries)

assert caplog.text == ""

def test_unknown_validation_name_is_ignored(self, caplog: pytest.LogCaptureFixture) -> None:
with caplog.at_level(logging.WARNING):
_warn_unmet_prerequisites([self._entry("NoSuchCheck", {})])

assert caplog.text == ""


def test_python_script_path_falls_back_to_current_working_directory(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
Expand Down
15 changes: 15 additions & 0 deletions isvtest/src/isvtest/core/validation.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,21 @@ def run(self) -> None:
"""
pass

@classmethod
def preflight(cls, config: dict[str, Any]) -> str | None:
"""Return why a local prerequisite is unmet, or None when runnable.

The orchestrator evaluates this for every ready check before running a
phase's validations, so a missing local prerequisite (a vendored source
tree, a required binary) is reported up front instead of after every
check ahead of it has finished. The check must still enforce the same
prerequisite in ``run()``: this hook only reports, it does not gate.
Implementations must be cheap and local: no cluster calls, no network.
Return None when the prerequisite is satisfied, or when the check would
skip anyway for other reasons.
"""
return None

def run_command(self, cmd: str, timeout: int | None = None, display_cmd: str | None = None) -> CommandResult:
"""Run a command using the configured runner.

Expand Down
Loading