Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
34f44ba
fix(docs): Update `nemo-platform` README.md to be more public-facing
matthewgrossman May 28, 2026
b36af3e
add readme
matthewgrossman May 28, 2026
569b6d4
Merge branch 'main' into mgrossman/aircore-679-fix-nemo-platform-pypi…
matthewgrossman May 28, 2026
2051bf1
Merge branch 'main' into mgrossman/aircore-679-fix-nemo-platform-pypi…
matthewgrossman May 29, 2026
dbc2afb
fix up nemo-platform-plugin
matthewgrossman May 29, 2026
72d7c8f
apply code review feedback
matthewgrossman May 29, 2026
b3ed16a
remove coming soon
matthewgrossman May 29, 2026
c9600ef
code review
matthewgrossman Jun 1, 2026
7326046
feat(ci): Add e2e tests
matthewgrossman Jun 1, 2026
ebdee3f
Merge branch 'main' into mgrossman/aircore-679-fix-nemo-platform-pypi…
matthewgrossman Jun 1, 2026
8aa78e5
fix conftest
matthewgrossman Jun 1, 2026
b002fd1
fix wheel-build
matthewgrossman Jun 1, 2026
c1f8c67
run latest version
matthewgrossman Jun 1, 2026
ed36b2d
bring into PRs
matthewgrossman Jun 1, 2026
6bc3597
comments
matthewgrossman Jun 1, 2026
b57fea8
Merge branch 'main' into mgrossman/aircore-679-fix-nemo-platform-pypi…
matthewgrossman Jun 1, 2026
87eea1f
fixes
matthewgrossman Jun 1, 2026
7765621
fixes
matthewgrossman Jun 1, 2026
6a3e4a0
Merge branch 'main' into mgrossman/aircore-679-fix-nemo-platform-pypi…
matthewgrossman Jun 1, 2026
bd5012c
add e2e test
matthewgrossman Jun 1, 2026
539cf94
fixes
matthewgrossman Jun 1, 2026
14c8005
add testing.md
matthewgrossman Jun 1, 2026
4e24680
fix name
matthewgrossman Jun 1, 2026
e60a061
add comment
matthewgrossman Jun 1, 2026
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
7 changes: 3 additions & 4 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -275,11 +275,10 @@ test-all-script: ## Run all unit tests using the helper script (with summary)
@echo "Running all unit tests with summary..."
uv run --frozen python tools/run_all_tests.py

# NOTE: disabled this in favour of the other test-e2e target (see below)
.PHONY: test-e2e
# test-e2e: ## Run Python end-to-end tests (customer workflows and blueprints)
# @echo "Running Python end-to-end tests..."
# uv run --frozen pytest -v -m e2e
test-e2e: ## Run e2e tests against nemo services (starts/stops services automatically)
@echo "Running e2e tests..."
uv run --frozen pytest e2e -v --run-e2e $(PYTEST_EXTRA)

.PHONY: test-regression
test-regression: ## Run Python regression tests (functional microservice baseline tests)
Expand Down
54 changes: 29 additions & 25 deletions TESTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,35 +113,46 @@ def test_create_and_fetch_entity(client, db_session):

### 3. End-to-End (E2E) Tests

**Objective**: Ensure that customers can orchestrate services together for common workflows and blueprints on real deployed infrastructure.
**Objective**: Ensure that services work together correctly when running as a real platform process.

**Characteristics**:

- Test complete customer workflows on actual deployments
- Uses testcontainers with Docker or Kubernetes backends
- Multiple services working together with real infrastructure
- Slowest tests (minutes to hours)
- Requires deployed infrastructure (no mocking of Jobs or Inference)
- Start the platform via `nemo services start` (real process, real ports)
- Hit services with an external HTTP client (the NeMoPlatform SDK)
- Test startup machinery, port binding, config resolution, and cross-service workflows
- Slower than integration tests (tens of seconds for startup) but faster than Docker/K8s e2e

**How to run**:

```bash
# Start services, run tests, stop services (all automatic)
make test-e2e

# Or manually
uv run --frozen pytest e2e -v --run-e2e

# If you already have services running
NMP_BASE_URL=http://localhost:8080 uv run --frozen pytest e2e -v --run-e2e
Comment thread
matthewgrossman marked this conversation as resolved.
```

**Prerequisites**: `make bootstrap` must have been run. The harness runs `nemo services start`
on a free port with a unique `--instance` name, so it won't conflict with your dev instance.

**When to Write E2E Tests**:

Write E2E tests when you need to:
- **Verify cross-service workflows**: Test operations that span multiple services (e.g., create workspace → upload file → run job → get results)
- **Test real infrastructure**: Validate jobs, inference, or storage backends that cannot be mocked
- **Validate customer scenarios**: Test complete workflows as users would experience them
- **Validate the real startup path**: Ensure config resolution, service discovery, and health checks work
- **Test authentication/authorization**: Verify role-based access control across multiple services
- **Test async workflows**: Validate long-running operations, job lifecycles, and event propagation
- **Ensure service integration**: Verify that services work together correctly in production-like setups
- **Ensure service integration**: Verify that services work together correctly end-to-end

**What NOT to E2E Test**:
- Single service APIs (use integration tests)
- Business logic within a service (use unit/integration tests)
- Every permutation of inputs (E2E should focus on critical paths)
- Implementation details (test user-visible behavior)

For detailed E2E test documentation, configuration options, and best practices, see: **[e2e/README.md](e2e/README.md)**

**Location**: `e2e/` (root-level for deployed infrastructure tests)
**Location**: `e2e/` (root-level)

### 4. Infrastructure Tests

Expand Down Expand Up @@ -395,19 +406,12 @@ uv run python tools/run_all_tests.py
make test-integration
uv run pytest -v -m integration

# End-to-end tests (Docker backend - recommended for local dev)
make test-e2e-docker
uv run pytest e2e --docker -v

# End-to-end tests (Kubernetes backend: local minikube or custom cluster)
make test-e2e-minikube
uv run pytest e2e --kubernetes --cluster-url=https://my-cluster.example.com -v

# E2E with custom registry and tag
uv run pytest e2e --docker --registry=my-registry --tag=v1.0.0 -v
# End-to-end tests (starts nemo services automatically)
make test-e2e
uv run --frozen pytest e2e -v --run-e2e

# E2E with custom config
uv run pytest e2e --docker --config=e2e/quickstart/custom.yaml -v
# E2E against an already-running instance
NMP_BASE_URL=http://localhost:8080 uv run --frozen pytest e2e -v --run-e2e

# Regression tests
make test-regression
Expand Down
3 changes: 3 additions & 0 deletions conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,9 @@ def pytest_runtest_setup(item):
if "slow" in [marker.name for marker in item.iter_markers()]:
if not item.config.getoption("--run-slow"):
skip_test("Skipping slow test (use --run-slow to run)")
if "e2e" in [marker.name for marker in item.iter_markers()]:
if not item.config.getoption("--run-e2e"):
skip_test("Skipping e2e test (use --run-e2e to run)")


from xdist.scheduler.loadscope import LoadScopeScheduling # noqa: E402
Expand Down
121 changes: 121 additions & 0 deletions e2e/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
"""E2E test fixtures that run against a real ``nemo services`` process.

Usage::

# Start services, run e2e tests, stop services
make test-e2e

# Or manually
uv run --frozen pytest e2e -v --run-e2e

# If you already have services running
NMP_BASE_URL=http://localhost:9090 uv run --frozen pytest e2e -v --run-e2e

When ``NMP_BASE_URL`` is set the harness skips service startup/shutdown and
connects to the given URL. Otherwise it starts ``nemo services start`` on a
free port, waits for ``/health/ready``, and tears the instance down after the
session.
"""

import logging
import os
import socket
import subprocess
import uuid
from collections.abc import Iterator

import pytest
from nemo_platform import NeMoPlatform

logger = logging.getLogger(__name__)


def _find_free_port() -> int:
"""Bind to port 0 and let the OS assign a free port."""
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.bind(("127.0.0.1", 0))
return s.getsockname()[1]


@pytest.fixture(scope="session")
def _services() -> Iterator[str]:
"""Start ``nemo services`` and yield the base URL.

Skipped when ``NMP_BASE_URL`` is already set (external services).
"""
external_url = os.environ.get("NMP_BASE_URL")
if external_url:
yield external_url
return

port = _find_free_port()
instance = f"e2e-test-{port}"
url = f"http://127.0.0.1:{port}"

logger.info("Starting nemo services on port %d (instance=%s)", port, instance)

result = subprocess.run(
[
"nemo",
"services",
"start",
"--service-group",
"all",
"--port",
str(port),
"--instance",
instance,
],
capture_output=True,
text=True,
timeout=120,
)
Comment thread
matthewgrossman marked this conversation as resolved.
Outdated

if result.returncode != 0:
pytest.fail(
f"nemo services start failed (exit {result.returncode}):\nstdout: {result.stdout}\nstderr: {result.stderr}"
)

logger.info("Platform services started: %s", result.stdout.strip())

# Set NMP_BASE_URL so the SDK picks it up automatically
os.environ["NMP_BASE_URL"] = url

yield url

logger.info("Stopping nemo services (instance=%s)", instance)
stop_result = subprocess.run(
[
"nemo",
"services",
"stop",
"--instance",
instance,
"--port",
str(port),
Comment thread
matthewgrossman marked this conversation as resolved.
Outdated
],
capture_output=True,
text=True,
timeout=60,
)
if stop_result.returncode != 0:
logger.warning(
"nemo services stop failed (exit %d): %s",
stop_result.returncode,
stop_result.stderr,
)
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated


@pytest.fixture(scope="session")
def sdk(_services: str) -> NeMoPlatform:
"""Provide an SDK client connected to the running platform."""
return NeMoPlatform(base_url=_services, max_retries=2)


@pytest.fixture(scope="function")
def workspace(sdk: NeMoPlatform) -> Iterator[str]:
"""Create a unique workspace for each test, deleted on teardown."""
name = f"e2e-{uuid.uuid4().hex[:8]}"
sdk.workspaces.create(name=name)
yield name
sdk.workspaces.delete(name)
37 changes: 37 additions & 0 deletions e2e/test_smoke.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
"""Smoke tests that verify the platform is reachable and core APIs respond.

These are intentionally minimal — they validate the e2e harness works and
that services are up. Add more substantive tests in separate files.
"""

from nemo_platform import NeMoPlatform


def test_health_ready(sdk: NeMoPlatform):
"""GET /health/ready returns 200 when all services are up."""
resp = sdk._client.get("/health/ready")
assert resp.status_code == 200


def test_health_live(sdk: NeMoPlatform):
"""GET /health/live returns 200 (liveness probe)."""
resp = sdk._client.get("/health/live")
assert resp.status_code == 200


def test_create_and_delete_workspace(workspace: str, sdk: NeMoPlatform):
"""Workspace CRUD round-trips through the platform.

Uses the ``workspace`` fixture which creates a unique workspace
and deletes it on teardown.
"""
page = sdk.workspaces.list()
names = [w.name for w in page.data]
assert workspace in names


def test_list_workspaces(sdk: NeMoPlatform, workspace: str):
"""Listing workspaces returns at least the test workspace."""
page = sdk.workspaces.list()
names = [w.name for w in page.data]
assert workspace in names
Loading
Loading