Skip to content

Conversation

@KrishnanPrash
Copy link
Contributor

@KrishnanPrash KrishnanPrash commented Oct 3, 2025

Overview:

As a part of the effort to restructure unit tests in Dynamo [Link to Context], this PR moves the unit vLLM test that lives within dynamo/tests/unit to components/src/dynamo/vllm.

Background:

  • Here is the file structure outlined by this PR:
src/dynamo/components/src
├── conftest.py
├── sglang
│   ├── args.py
│   ├── main.py
│   ├── ...
│   └── tests                     <----------------- [Future PR: SGLang unit tests]
│       ├── __init__.py            <----------------- To be added
│       └── test_sglang_unit.py    <----------------- To be added
├── trtllm
│   ├── engine.py
│   ├── main.py
│   ├── ...
│   └── tests                     <----------------- [Future PR: TRT-LLM unit tests]
│       ├── __init__.py            <----------------- To be added
│       └── test_trtllm_unit.py    <----------------- To be added
└── vllm
    ├── args.py
    ├── main.py
    ├── ...
    └── tests                     <----------------- [THIS PR] vLLM unit test folder
        ├── __init__.py.          <----------------- [THIS PR] 
        ├── test_args.py.         <----------------- [THIS PR] 
        └── ...
  • Currently, our CI runs pytest from /workspace. And pytest goes through two major steps before running the tests:
    • Collection: This step involves collecting any test*.py files in the subdirectories and importing them.
    • Filtering: Based on the markers used in the pytest command, it decides what tests are included/excluded in the final set.

Initially, attempted to introduce this structure as a part of #3362, but was running into python path and relative import errors.

Context for why we need conftest.py:: pytest_ignore_collect() [Link]

Since test_args.py, will indirectly perform import vllm, we need pytest_ignore_collect() to determine if vllm is present in the environment before allowing test_args.py to be collected by pytest.

But from conftest.py's perspective, it cannot distiguish between the vllm folder/module (dynamo.vllm) on the same level and the vllm library in site-packages.

Here is an example of what a build and test vllm job will error with:

(venv) root@4845aa7-lcedt:/workspace# pytest
========================================================================================= test session starts =========================================================================================
...
collecting 15 items                                                                                                                                                                                   
collected 527 items / 1 error                                                                                                                                                   

=============================================================================================== ERRORS ================================================================================================
___________________________________________________________________ ERROR collecting components/src/dynamo/vllm/tests/test_args.py ____________________________________________________________________
ImportError while importing test module '/workspace/components/src/dynamo/vllm/tests/test_args.py'.
Hint: make sure your test modules/packages have valid Python names.
Traceback:
/usr/lib/python3.12/importlib/__init__.py:90: in import_module
    return _bootstrap._gcd_import(name[level:], package, level)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
components/src/dynamo/vllm/tests/test_args.py:12: in <module>
    from dynamo.vllm.args import parse_args
components/src/dynamo/vllm/args.py:9: in <module>
    from vllm.config import KVTransferConfig
E   ModuleNotFoundError: No module named 'vllm.config'

It attempts to import the config submodule from dynamo.vllm due to the relative import.

Here are further debug logs added to pytest_ignore_collect as a reference for the claims made above:

[DEBUG] Found backend test path: /workspace/components/src/dynamo/vllm/tests/__init__.py
[DEBUG] Looking for module: vllm
[DEBUG] find_spec('vllm') = ModuleSpec(name='vllm', loader=<_frozen_importlib_external.SourceFileLoader object at 0x741298b222a0>, origin='/workspace/components/src/dynamo/vllm/__init__.py', submodule_search_locations=['/workspace/components/src/dynamo/vllm'])
[DEBUG] Module origin: /workspace/components/src/dynamo/vllm/__init__.py
[DEBUG] Module submodule_search_locations: ['/workspace/components/src/dynamo/vllm']
[DEBUG] WARNING: Found dynamo's wrapper package at /workspace/components/src/dynamo/vllm/__init__.py
[DEBUG] This may cause import errors!
[DEBUG] Module found - COLLECTING (but may fail if it's the wrapper)
[DEBUG] Found backend test path: /workspace/components/src/dynamo/vllm/tests/test_args.py
[DEBUG] Looking for module: vllm
[DEBUG] find_spec('vllm') = ModuleSpec(name='vllm', loader=<_frozen_importlib_external.SourceFileLoader object at 0x741298b76c60>, origin='/workspace/components/src/dynamo/vllm/__init__.py', submodule_search_locations=['/workspace/components/src/dynamo/vllm'])
[DEBUG] Module origin: /workspace/components/src/dynamo/vllm/__init__.py
[DEBUG] Module submodule_search_locations: ['/workspace/components/src/dynamo/vllm']
[DEBUG] WARNING: Found dynamo's wrapper package at /workspace/components/src/dynamo/vllm/__init__.py

@KrishnanPrash KrishnanPrash requested review from a team as code owners October 3, 2025 23:31
@github-actions github-actions bot added the ci Issues/PRs that reference CI build/test label Oct 3, 2025
@KrishnanPrash KrishnanPrash marked this pull request as draft October 3, 2025 23:31
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Oct 3, 2025

Walkthrough

Adds CI workflow changes splitting vLLM tests into unit and e2e pytest actions, refines test markers, and keeps e2e conditional on non-arm64. Introduces a pytest collection hook to skip backend tests when required frameworks are missing. Adds vLLM unit tests for custom Jinja template argument handling and a tests package init with headers.

Changes

Cohort / File(s) Summary
CI workflow: vLLM test split
.github/workflows/container-validation-backends.yml
Replaces single test step with two pytest-action steps: unit (unit and vllm and gpu_1) and e2e (e2e and vllm and gpu_1 and not slow), preserving e2e skip on arm64. Steps run post Docker tag/push.
Dynamo test collection gating
components/src/dynamo/conftest.py
Adds pytest_ignore_collect to map test paths (vllm, sglang, tensorrt_llm) to required modules, skip collection if module missing via importlib.util.find_spec, emit debug logs, and warn on detected wrapper origins.
vLLM tests scaffolding
components/src/dynamo/vllm/tests/__init__.py
New file with SPDX/license headers; no functional code.
vLLM arg parsing unit tests
components/src/dynamo/vllm/tests/test_args.py
Adds three tests validating custom Jinja template path parsing: invalid path raises FileNotFoundError, valid absolute path stored, env var expansion applied. Uses monkeypatch and CLI arg simulation.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor Dev as Developer
  participant GH as GitHub Actions (vLLM job)
  participant PyUnit as pytest-action (unit)
  participant PyE2E as pytest-action (e2e)

  Dev->>GH: Push/PR triggers workflow
  rect rgb(230,240,255)
    note right of GH: Build & push container image
    GH->>GH: Docker tag & push
  end
  rect rgb(235,255,235)
    GH->>PyUnit: Run unit tests (marks: unit and vllm and gpu_1)
    PyUnit-->>GH: Results
  end
  alt matrix.platform.arch != 'arm64'
    rect rgb(255,245,230)
      GH->>PyE2E: Run e2e tests (marks: e2e and vllm and gpu_1 and not slow)
      PyE2E-->>GH: Results
    end
  else arm64
    GH-->>GH: Skip e2e step
  end
Loading
sequenceDiagram
  autonumber
  participant PyTest as pytest collector
  participant Hook as dynamo/conftest.py
  participant FS as Test file path
  participant Import as importlib.util

  PyTest->>Hook: pytest_ignore_collect(FS)
  Hook->>Hook: Map FS to required module (vllm/sglang/tensorrt_llm)
  Hook->>Import: find_spec(module)
  alt spec is None
    Hook-->>PyTest: return True (skip collection)
  else spec found
    Hook-->>PyTest: return None (collect normally)
  end
Loading

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Poem

I thump my paws on YAML fields, hooray!
Two trails of tests now hop their tidy way.
If modules hide, I sniff—then skip the lot,
While Jinja paths expand right on the spot.
With ears up high, I merge and hum:
CI goes brrr; all carrots greenlit—yum! 🥕

Pre-merge checks

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Description Check ⚠️ Warning The pull request description includes an Overview and background but omits the mandatory Details, Where should the reviewer start, and Related Issues sections from the repository’s template, leaving reviewers without a concise summary of the specific changes, guidance on which files to inspect, or a clear link to the issue being addressed. Please update the PR description to follow the repository template by adding a Details section that outlines the exact code changes, a Where should the reviewer start section that points to key files and lines for review, and a Related Issues section using action keywords to reference the relevant issue number.
✅ Passed checks (2 passed)
Check name Status Explanation
Title Check ✅ Passed The title succinctly describes the primary change of relocating backend unit tests from the legacy test directory to the new components path, matching the PR’s main refactoring goal.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
.github/workflows/container-validation-backends.yml (1)

80-90: Remove gpu_1 marker from the unit test step

Tests in components/src/dynamo/vllm/tests/test_args.py don’t require GPU, so pytest_marks: "unit and vllm and gpu_1" will break on arm64 runners. Change to pytest_marks: "unit and vllm" or add if: ${{ matrix.platform.arch != 'arm64' }}.

🧹 Nitpick comments (5)
components/src/dynamo/conftest.py (2)

13-13: Mark the unused parameter as intentional.

The config parameter is required by the pytest hook signature but isn't used in this implementation. Prefix it with an underscore to indicate it's intentionally unused and silence the linter warning.

Apply this diff:

-def pytest_ignore_collect(collection_path, config):
+def pytest_ignore_collect(collection_path, _config):

21-28: Use pathlib for robust path matching.

The substring matching approach (if path_pattern in path_str) is fragile and could incorrectly match paths like backup/vllm/tests_old/ or my_vllm/tests/. Use pathlib.Path to match against actual path components.

Apply this diff:

     path_str = str(collection_path)
+    path = Path(collection_path)

     # Map backend test paths to required modules
     backend_requirements = {
-        "/vllm/tests/": "vllm",
-        "/sglang/tests/": "sglang",
-        "/trtllm/tests/": "tensorrt_llm",
+        ("vllm", "tests"): "vllm",
+        ("sglang", "tests"): "sglang",
+        ("trtllm", "tests"): "tensorrt_llm",
     }

-    for path_pattern, required_module in backend_requirements.items():
-        if path_pattern in path_str:
+    for path_components, required_module in backend_requirements.items():
+        # Check if path contains the backend/tests directory structure
+        if all(part in path.parts for part in path_components):

Note: You'll need to add from pathlib import Path at the top of the file.

components/src/dynamo/vllm/tests/test_args.py (3)

14-15: Fragile path computation with magic index.

The line Path(__file__).parents[5] uses a hardcoded index to traverse up the directory tree, which is brittle and will break if the test file moves or the repository structure changes. The magic number 5 has no explanation.

Consider these alternatives:

Option 1: Use a relative path from the test file's location:

-TEST_DIR = Path(__file__).parents[5] / "tests"
+TEST_DIR = Path(__file__).parent.parent.parent.parent.parent / "tests"

(Still fragile, but more explicit about the traversal)

Option 2: Search upward for a marker file (preferred):

+def find_repo_root(start_path: Path) -> Path:
+    """Find repository root by searching for a marker file."""
+    current = start_path
+    while current != current.parent:
+        if (current / "pyproject.toml").exists() or (current / ".git").exists():
+            return current
+        current = current.parent
+    raise RuntimeError("Could not find repository root")
+
-TEST_DIR = Path(__file__).parents[5] / "tests"
+TEST_DIR = find_repo_root(Path(__file__)) / "tests"

Option 3: Use an environment variable for test fixtures:

+import os
+
-TEST_DIR = Path(__file__).parents[5] / "tests"
+TEST_DIR = Path(os.getenv("DYNAMO_REPO_ROOT", Path(__file__).parents[5])) / "tests"

44-64: Consider verifying fixture file existence.

The test assumes JINJA_TEMPLATE_PATH points to an existing file but doesn't verify this. If the fixture is missing or the path computation in line 15 is wrong, the test will fail with a potentially confusing error message.

Add a fixture existence check or use a temporary file:

Option 1: Verify fixture exists (simpler):

 def test_custom_jinja_template_valid_path(monkeypatch):
     """Test that valid absolute path is stored correctly."""
+    assert Path(JINJA_TEMPLATE_PATH).exists(), f"Test fixture not found: {JINJA_TEMPLATE_PATH}"
     monkeypatch.setattr(

Option 2: Use a temporary file (more robust):

-def test_custom_jinja_template_valid_path(monkeypatch):
+def test_custom_jinja_template_valid_path(monkeypatch, tmp_path):
     """Test that valid absolute path is stored correctly."""
+    template_file = tmp_path / "custom_template.jinja"
+    template_file.write_text("{% mock content %}")
+    
     monkeypatch.setattr(
         sys,
         "argv",
         [
             "dynamo.vllm",
             "--model",
             "Qwen/Qwen3-0.6B",
             "--custom-jinja-template",
-            JINJA_TEMPLATE_PATH,
+            str(template_file),
         ],
     )

     config = parse_args()

-    assert config.custom_jinja_template == JINJA_TEMPLATE_PATH
+    assert config.custom_jinja_template == str(template_file)

18-21: Reconsider GPU marker for unit tests.

These tests mock sys.argv and call parse_args() to validate argument parsing logic. They don't perform any GPU operations or require GPU resources. The @pytest.mark.gpu_1 marker seems incorrect for pure unit tests.

This relates to the issue flagged in .github/workflows/container-validation-backends.yml (lines 80-90). If these tests don't genuinely require GPU:

 @pytest.mark.unit
 @pytest.mark.vllm
-@pytest.mark.gpu_1
 @pytest.mark.pre_merge

Apply the same change to lines 44-47 and 67-70. Alternatively, if the gpu_1 marker is used for organizational purposes (e.g., "tests that run in GPU containers"), document this in the test docstrings or a testing guide.

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c48f49a and 53e7cf3.

📒 Files selected for processing (4)
  • .github/workflows/container-validation-backends.yml (1 hunks)
  • components/src/dynamo/conftest.py (1 hunks)
  • components/src/dynamo/vllm/tests/__init__.py (1 hunks)
  • components/src/dynamo/vllm/tests/test_args.py (1 hunks)
🧰 Additional context used
🪛 Ruff (0.13.3)
components/src/dynamo/conftest.py

13-13: Unused function argument: config

(ARG001)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (6)
  • GitHub Check: trtllm (amd64)
  • GitHub Check: trtllm (arm64)
  • GitHub Check: vllm (amd64)
  • GitHub Check: vllm (arm64)
  • GitHub Check: sglang
  • GitHub Check: Build and Test - dynamo
🔇 Additional comments (3)
components/src/dynamo/vllm/tests/__init__.py (1)

1-2: LGTM!

Standard package initialization with proper license headers.

components/src/dynamo/vllm/tests/test_args.py (1)

67-92: LGTM with minor note.

The test correctly validates environment variable expansion in template paths. The assertions properly verify both that the variable was expanded (line 91) and that the result matches the expected path (line 92).

Note: This test shares the same fixture path dependency as test_custom_jinja_template_valid_path. If you implement fixture verification there (see previous comment), consider applying it here as well.

components/src/dynamo/conftest.py (1)

42-54: Inconsistent wrapper handling in conftest.py.
The detection of Dynamo’s wrapper package logs a warning yet doesn’t alter control flow, so wrapper specs still proceed to collection. Either:

  • Skip wrappers by updating the if spec is None check to also catch spec.origin containing “/dynamo/”.
  • Remove the wrapper warning if wrappers are acceptable.
  • Add a TODO/FIXME with an issue link if this is under investigation.

Which approach aligns with the intended behavior?

Comment on lines +29 to +54
print(f"[DEBUG] Found backend test path: {path_str}")
print(f"[DEBUG] Looking for module: {required_module}")

spec = importlib.util.find_spec(required_module)
print(f"[DEBUG] find_spec('{required_module}') = {spec}")

if spec is not None:
print(f"[DEBUG] Module origin: {spec.origin}")
print(
f"[DEBUG] Module submodule_search_locations: {spec.submodule_search_locations}"
)

# Check if this is dynamo's wrapper or the real package
if spec.origin and "/dynamo/" in spec.origin:
print(
f"[DEBUG] WARNING: Found dynamo's wrapper package at {spec.origin}"
)
print("[DEBUG] This may cause import errors!")

if spec is None:
print("[DEBUG] Module not found - SKIPPING collection")
return True # Module not available, skip this file
else:
print(
"[DEBUG] Module found - COLLECTING (but may fail if it's the wrapper)"
)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Replace print statements with proper logging.

The function contains extensive debug output using print() statements, which will clutter test output and cannot be controlled via log levels. This appears to be temporary debugging code that should be removed or replaced with proper logging before merging.

Use pytest's logging instead:

 import importlib.util
+import logging
+
+logger = logging.getLogger(__name__)


 def pytest_ignore_collect(collection_path, _config):
     """Skip collecting backend test files if their framework isn't installed.

     Checks for backend test directories and corresponding installed packages.
     """
     path_str = str(collection_path)

     # Map backend test paths to required modules
     backend_requirements = {
         "/vllm/tests/": "vllm",
         "/sglang/tests/": "sglang",
         "/trtllm/tests/": "tensorrt_llm",
     }

     for path_pattern, required_module in backend_requirements.items():
         if path_pattern in path_str:
-            print(f"[DEBUG] Found backend test path: {path_str}")
-            print(f"[DEBUG] Looking for module: {required_module}")
+            logger.debug(f"Found backend test path: {path_str}")
+            logger.debug(f"Looking for module: {required_module}")

             spec = importlib.util.find_spec(required_module)
-            print(f"[DEBUG] find_spec('{required_module}') = {spec}")
+            logger.debug(f"find_spec('{required_module}') = {spec}")

             if spec is not None:
-                print(f"[DEBUG] Module origin: {spec.origin}")
-                print(
-                    f"[DEBUG] Module submodule_search_locations: {spec.submodule_search_locations}"
-                )
+                logger.debug(f"Module origin: {spec.origin}")
+                logger.debug(f"Module submodule_search_locations: {spec.submodule_search_locations}")

                 # Check if this is dynamo's wrapper or the real package
                 if spec.origin and "/dynamo/" in spec.origin:
-                    print(
-                        f"[DEBUG] WARNING: Found dynamo's wrapper package at {spec.origin}"
-                    )
-                    print("[DEBUG] This may cause import errors!")
+                    logger.warning(f"Found dynamo's wrapper package at {spec.origin} - may cause import errors")

             if spec is None:
-                print("[DEBUG] Module not found - SKIPPING collection")
+                logger.info(f"Module {required_module} not found - skipping collection of {path_str}")
                 return True  # Module not available, skip this file
-            else:
-                print(
-                    "[DEBUG] Module found - COLLECTING (but may fail if it's the wrapper)"
-                )
+            logger.debug(f"Module {required_module} found - collecting {path_str}")

     return None  # Not a backend test or module available
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
print(f"[DEBUG] Found backend test path: {path_str}")
print(f"[DEBUG] Looking for module: {required_module}")
spec = importlib.util.find_spec(required_module)
print(f"[DEBUG] find_spec('{required_module}') = {spec}")
if spec is not None:
print(f"[DEBUG] Module origin: {spec.origin}")
print(
f"[DEBUG] Module submodule_search_locations: {spec.submodule_search_locations}"
)
# Check if this is dynamo's wrapper or the real package
if spec.origin and "/dynamo/" in spec.origin:
print(
f"[DEBUG] WARNING: Found dynamo's wrapper package at {spec.origin}"
)
print("[DEBUG] This may cause import errors!")
if spec is None:
print("[DEBUG] Module not found - SKIPPING collection")
return True # Module not available, skip this file
else:
print(
"[DEBUG] Module found - COLLECTING (but may fail if it's the wrapper)"
)
import importlib.util
import logging
logger = logging.getLogger(__name__)
def pytest_ignore_collect(collection_path, _config):
"""Skip collecting backend test files if their framework isn't installed.
Checks for backend test directories and corresponding installed packages.
"""
path_str = str(collection_path)
# Map backend test paths to required modules
backend_requirements = {
"/vllm/tests/": "vllm",
"/sglang/tests/": "sglang",
"/trtllm/tests/": "tensorrt_llm",
}
for path_pattern, required_module in backend_requirements.items():
if path_pattern in path_str:
logger.debug(f"Found backend test path: {path_str}")
logger.debug(f"Looking for module: {required_module}")
spec = importlib.util.find_spec(required_module)
logger.debug(f"find_spec('{required_module}') = {spec}")
if spec is not None:
logger.debug(f"Module origin: {spec.origin}")
logger.debug(f"Module submodule_search_locations: {spec.submodule_search_locations}")
# Check if this is dynamo's wrapper or the real package
if spec.origin and "/dynamo/" in spec.origin:
logger.warning(
f"Found dynamo's wrapper package at {spec.origin} - may cause import errors"
)
if spec is None:
logger.info(f"Module {required_module} not found - skipping collection of {path_str}")
return True # Module not available, skip this file
logger.debug(f"Module {required_module} found - collecting {path_str}")
return None # Not a backend test or module available
🤖 Prompt for AI Agents
In components/src/dynamo/conftest.py around lines 29 to 54, replace all print()
debug statements with proper logging: import the logging module (if not present)
and create a module logger = logging.getLogger(__name__), then convert prints to
logger.debug(...) for normal debug lines and logger.warning(...) for the WARNING
line; remove any prints that were only for temporary debugging and ensure
messages remain informative and use string interpolation or f-strings passed to
the logger so pytest caplog and log-level controls can manage the output.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci Issues/PRs that reference CI build/test size/L

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants