Skip to content
Merged
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: 3 additions & 3 deletions .github/workflows/install-smoke.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ on:
- 'src/**'
- 'pyproject.toml'
- 'uv.lock'
- 'scripts/test_install_smoke.py'
- 'tests/install_smoke/**'
- 'scripts/test_install_smoke.sh'
# Self-callout: re-run when this workflow changes so YAML edits are validated in PRs.
- '.github/workflows/install-smoke.yml'
Expand Down Expand Up @@ -58,5 +58,5 @@ jobs:
- name: List installed packages
run: VIRTUAL_ENV=.venv-smoke uv pip list

- name: Run import smoke test
run: .venv-smoke/bin/python scripts/test_install_smoke.py ${{ matrix.profile.name }}
- name: Run smoke test (imports + runtime checks)
run: .venv-smoke/bin/python -m tests.install_smoke ${{ matrix.profile.name }}
152 changes: 0 additions & 152 deletions scripts/test_install_smoke.py

This file was deleted.

12 changes: 6 additions & 6 deletions scripts/test_install_smoke.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@
# Local equivalent of .github/workflows/install-smoke.yml.
#
# For each install profile, builds the wheel and installs it into a
# clean venv (no dev deps), then runs the import smoke test for that
# profile. By default runs every known profile; pass a profile name
# to run just one.
# clean venv (no dev deps), then runs the smoke test for that profile
# (imports + any per-profile runtime checks). By default runs every
# known profile; pass a profile name to run just one.
#
# Available profiles (must match those in scripts/test_install_smoke.py):
# Available profiles (must match those in tests/install_smoke/__main__.py):
# base -- `pip install a2a-sdk`
# http-server -- `pip install a2a-sdk[http-server]`
# grpc -- `pip install a2a-sdk[grpc]`
Expand Down Expand Up @@ -87,8 +87,8 @@ for profile in "${PROFILES[@]}"; do
echo "--- Installed packages ---"
VIRTUAL_ENV="$venv_dir" uv pip list

echo "--- Running import smoke test ---"
if ! "$venv_dir/bin/python" scripts/test_install_smoke.py "$profile"; then
echo "--- Running smoke test (imports + runtime checks) ---"
if ! "$venv_dir/bin/python" -m tests.install_smoke "$profile"; then
FAILED_PROFILES+=("$profile")
fi
done
Expand Down
39 changes: 39 additions & 0 deletions tests/install_smoke/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# Install-smoke harness

This package is **not** a pytest test suite. It is invoked as a
standalone module from a freshly-installed, dev-deps-free venv:

```bash
python -m tests.install_smoke <profile>
```

The smoke venv is created by either
[`scripts/test_install_smoke.sh`](../../scripts/test_install_smoke.sh)
(local) or
[`.github/workflows/install-smoke.yml`](../../.github/workflows/install-smoke.yml)
(CI). The harness has no pytest dependency and uses only the Python
standard library plus the freshly-installed `a2a-sdk` wheel for the
profile under test.

For a given install profile (`base`, `http-server`, `grpc`,
`telemetry`, `sql`) it runs two phases:

1. **Imports**: every module listed for the profile in `__main__.py`
must import cleanly. Catches missing deps and accidental top-level
imports of optional extras.
2. **Runtime checks**: small public-API exercises that
actually call into the SDK. These catch regressions where imports
succeed but a real call fails.

#### Adding a new runtime check

1. Drop a module under `tests/install_smoke/runtime/` exposing two
names:
- `NAME: str` — short human-readable label.
- `check() -> None` — callable that raises on failure.
2. Register it in `RUNTIME_CHECKS` in
[`__main__.py`](./__main__.py) under each profile whose extras it
needs.

Use only the dependencies guaranteed by the target profile. Do not import
`pytest` or any dev-deps.
Empty file.
142 changes: 142 additions & 0 deletions tests/install_smoke/__main__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
"""Entry point for the install-smoke harness. See README.md."""

from __future__ import annotations

import importlib
import sys


# Modules under each list MUST be importable with only that profile's
# extras installed -- no leakage from other extras (grpc, http-server,
# sql, signing, telemetry, vertex, etc.). Modules that require
# optional extras must use try/except ImportError guards internally.
CORE_MODULES = [
'a2a',
'a2a.client',
'a2a.client.auth',
'a2a.client.base_client',
'a2a.client.card_resolver',
'a2a.client.client',
'a2a.client.client_factory',
'a2a.client.errors',
'a2a.client.interceptors',
'a2a.client.optionals',
'a2a.client.transports',
'a2a.server',
'a2a.server.agent_execution',
'a2a.server.context',
'a2a.server.events',
'a2a.server.request_handlers',
'a2a.server.tasks',
'a2a.types',
'a2a.utils',
'a2a.utils.constants',
'a2a.utils.error_handlers',
'a2a.utils.version_validator',
'a2a.utils.proto_utils',
'a2a.utils.task',
'a2a.helpers.agent_card',
'a2a.helpers.proto_helpers',
]

HTTP_SERVER_MODULES = [
'a2a.server.routes',
'a2a.server.routes.agent_card_routes',
'a2a.server.routes.common',
'a2a.server.routes.jsonrpc_dispatcher',
'a2a.server.routes.jsonrpc_routes',
'a2a.server.routes.rest_dispatcher',
'a2a.server.routes.rest_routes',
]

GRPC_MODULES = [
'a2a.server.request_handlers.grpc_handler',
'a2a.client.transports.grpc',
'a2a.compat.v0_3.grpc_handler',
'a2a.compat.v0_3.grpc_transport',
]

TELEMETRY_MODULES = [
'a2a.utils.telemetry',
]

SQL_MODULES = [
'a2a.server.models',
'a2a.server.tasks.database_task_store',
'a2a.server.tasks.database_push_notification_config_store',
]


PROFILES: dict[str, list[str]] = {
'base': CORE_MODULES,
'http-server': CORE_MODULES + HTTP_SERVER_MODULES,
'grpc': CORE_MODULES + GRPC_MODULES,
'telemetry': CORE_MODULES + TELEMETRY_MODULES,
'sql': CORE_MODULES + SQL_MODULES,
}


# Imported lazily in `main()` so a check that needs one profile's
# extras can't break the harness when running a different profile.
RUNTIME_CHECKS: dict[str, list[str]] = {
'base': ['tests.install_smoke.runtime.base_send_message'],
}


def main(argv: list[str]) -> int:
profile = argv[1] if len(argv) > 1 else 'base'
if profile not in PROFILES:
print(f'Unknown profile {profile!r}. Available: {sorted(PROFILES)}')
return 1

modules = PROFILES[profile]
import_failures: list[str] = []
for module_name in modules:
try:
importlib.import_module(module_name)
except Exception as e: # noqa: BLE001, PERF203
import_failures.append(f'{module_name}: {e}')

print(f'Profile: {profile}')
print(f'Tested {len(modules)} modules')
print(f' Passed: {len(modules) - len(import_failures)}')
print(f' Failed: {len(import_failures)}')

if import_failures:
print('\nFAILED imports:')
for failure in import_failures:
print(f' - {failure}')
return 1

print('\nAll modules imported successfully.')

runtime_checks = RUNTIME_CHECKS.get(profile, [])
if not runtime_checks:
return 0

print(f'\nRunning {len(runtime_checks)} runtime check(s):')
runtime_failures: list[str] = []
for module_path in runtime_checks:
label = module_path
try:
module = importlib.import_module(module_path)
label = module.NAME
module.check()
except Exception as e: # noqa: BLE001, PERF203
runtime_failures.append(f'{label}: {type(e).__name__}: {e}')
print(f' - FAIL: {label}')
else:
print(f' - OK: {label}')

if runtime_failures:
print('\nFAILED runtime checks:')
for failure in runtime_failures:
print(f' - {failure}')
return 1

print('\nAll runtime checks passed.')
return 0


if __name__ == '__main__':
sys.exit(main(sys.argv))
Empty file.
Loading
Loading